1use crate::{Error, GAME_DOMAIN};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct NxmLink {
10 pub game: String,
12 pub mod_id: u32,
14 pub file_id: u32,
16 pub key: String,
18 pub expires: String,
20 pub user_id: Option<String>,
22}
23
24impl NxmLink {
25 pub fn parse(link: &str) -> Result<NxmLink, Error> {
27 let bad = |why: &str| Error::Answer(format!("not a Banished nxm:// link: {why}"));
28 let rest = link
29 .trim()
30 .strip_prefix("nxm://")
31 .ok_or_else(|| bad("it does not start with nxm://"))?;
32 let (path, query) = rest.split_once('?').unwrap_or((rest, ""));
33 let parts: Vec<&str> = path.trim_end_matches('/').split('/').collect();
34 let [game, "mods", mod_id, "files", file_id] = parts[..] else {
35 return Err(bad("expected nxm://banished/mods/ID/files/ID"));
36 };
37 if !game.eq_ignore_ascii_case(GAME_DOMAIN) {
38 return Err(bad(&format!("it is for {game}")));
39 }
40 let param = |name: &str| {
41 query
42 .split('&')
43 .filter_map(|p| p.split_once('='))
44 .find(|(k, _)| *k == name)
45 .map(|(_, v)| v.to_string())
46 };
47 Ok(NxmLink {
48 game: game.to_ascii_lowercase(),
49 mod_id: mod_id
50 .parse()
51 .map_err(|_| bad("the mod id is not a number"))?,
52 file_id: file_id
53 .parse()
54 .map_err(|_| bad("the file id is not a number"))?,
55 key: param("key").ok_or_else(|| bad("it has no key"))?,
56 expires: param("expires").ok_or_else(|| bad("it has no expiry time"))?,
57 user_id: param("user_id"),
58 })
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn reads_a_link() {
68 let l =
69 NxmLink::parse("nxm://Banished/mods/19/files/49?key=k1&expires=1700000000&user_id=5")
70 .unwrap();
71 assert_eq!((l.mod_id, l.file_id), (19, 49));
72 assert_eq!(l.key, "k1");
73 assert_eq!(l.expires, "1700000000");
74 assert_eq!(l.user_id.as_deref(), Some("5"));
75 assert!(NxmLink::parse("nxm://skyrim/mods/1/files/2?key=a&expires=1").is_err());
76 assert!(NxmLink::parse("nxm://banished/mods/1/files/2").is_err());
77 assert!(NxmLink::parse("https://www.nexusmods.com/banished/mods/1").is_err());
78 }
79}