nitai/projects
noxstrap / src / crates / nox-roblox / tests / pipeline.rs
126 lines · 5.5 KB Raw
1use nox_roblox::manifest::{package_dir, Manifest};
2use nox_roblox::md5::{md5_hex, to_hex, Md5};
3use std::process::Command;
4
5const REAL_MANIFEST: &str = "v0\nRobloxApp.zip\naeaa4432a6005a7cead977db05447511\n134901428\n173551147\ncontent-avatar.zip\n961cd6b55ab23d2efcc9a7319191aa37\n293687\n1020627\nRobloxPlayerInstaller.exe\nd41d8cd98f00b204e9800998ecf8427e\n100\n100\n";
6
7#[test]
8fn parses_a_real_manifest() {
9 let m = Manifest::parse("version-abc", REAL_MANIFEST).unwrap();
10 assert_eq!(m.packages.len(), 3);
11 assert_eq!(m.packages[0].name, "RobloxApp.zip");
12 assert_eq!(m.packages[0].md5, "aeaa4432a6005a7cead977db05447511");
13 assert_eq!(m.packages[0].packed, 134901428);
14 assert_eq!(m.packages[0].unpacked, 173551147);
15 // The installer executable has no destination, so it is not installable.
16 assert_eq!(m.installable().len(), 2);
17 assert_eq!(m.total_packed(), 134901428 + 293687);
18}
19
20#[test]
21fn rejects_broken_manifests() {
22 assert!(Manifest::parse("v", "").is_err());
23 assert!(Manifest::parse("v", "v1\na\nb\n1\n2\n").is_err());
24 assert!(Manifest::parse("v", "v0\na\nb\n1\n").is_err());
25 assert!(Manifest::parse("v", "v0\na\nb\nxx\n2\n").is_err());
26}
27
28#[test]
29fn every_package_in_a_live_manifest_has_a_destination() {
30 // Captured from the live WindowsPlayer manifest; if Roblox adds a package we
31 // do not know where to put, this is where it should show up.
32 let live = [
33 "RobloxApp.zip", "WebView2.zip", "WebView2RuntimeInstaller.zip", "content-avatar.zip",
34 "content-configs.zip", "content-fonts.zip", "content-models.zip",
35 "content-platform-dictionaries.zip", "content-platform-fonts.zip", "content-sky.zip",
36 "content-sounds.zip", "content-terrain.zip", "content-textures2.zip",
37 "content-textures3.zip", "extracontent-luapackages.zip", "extracontent-models.zip",
38 "extracontent-places.zip", "extracontent-textures.zip", "extracontent-translations.zip",
39 "shaders.zip", "ssl.zip",
40 ];
41 for p in live {
42 assert!(package_dir(p).is_some(), "no destination mapped for {p}");
43 }
44 assert!(package_dir("RobloxPlayerInstaller.exe").is_none());
45 assert!(package_dir("something-new.zip").is_none());
46}
47
48#[test]
49fn md5_matches_python() {
50 if Command::new("python3").arg("--version").output().is_err() {
51 return;
52 }
53 let mut cases: Vec<Vec<u8>> = vec![
54 Vec::new(),
55 b"a".to_vec(),
56 b"abc".to_vec(),
57 b"message digest".to_vec(),
58 b"abcdefghijklmnopqrstuvwxyz".to_vec(),
59 b"12345678901234567890123456789012345678901234567890123456789012345678901234567890".to_vec(),
60 ];
61 // Every length across a block boundary, where padding logic tends to break.
62 for n in 0..200usize {
63 cases.push((0..n).map(|i| (i * 7 % 251) as u8).collect());
64 }
65 cases.push(vec![0xab; 100_000]);
66
67 let script = std::env::temp_dir().join("nox-md5.py");
68 std::fs::write(&script, "import sys,hashlib\nfor line in sys.stdin:\n line=line.strip()\n print(hashlib.md5(bytes.fromhex(line)).hexdigest())\n").unwrap();
69 let input: String = cases.iter().map(|c| format!("{}\n", to_hex(c))).collect();
70 let mut child = Command::new("python3")
71 .arg(&script)
72 .stdin(std::process::Stdio::piped())
73 .stdout(std::process::Stdio::piped())
74 .spawn()
75 .unwrap();
76 use std::io::Write;
77 child.stdin.as_mut().unwrap().write_all(input.as_bytes()).unwrap();
78 let out = child.wait_with_output().unwrap();
79 let want: Vec<&str> = std::str::from_utf8(&out.stdout).unwrap().lines().collect();
80 assert_eq!(want.len(), cases.len());
81 for (i, c) in cases.iter().enumerate() {
82 assert_eq!(md5_hex(c), want[i], "md5 differs for case {i} (len {})", c.len());
83 }
84
85 // Streaming in odd-sized chunks must equal hashing in one go.
86 let big = &cases[cases.len() - 1];
87 let mut h = Md5::new();
88 for chunk in big.chunks(7) {
89 h.update(chunk);
90 }
91 assert_eq!(to_hex(&h.finish()), md5_hex(big));
92}
93
94#[test]
95fn launch_passes_the_uri_through_untouched() {
96 use nox_roblox::launch::plan;
97 use nox_roblox::Binary;
98 let root = std::path::Path::new("/tmp/nox-root");
99 let uri = "roblox-player:1+launchmode:play+gameinfo:TICKET+placelauncherurl:https%3A%2F%2Fx.com%2Fy%3Fa%3D1%26b%3D2+launchtime:123";
100 let l = plan(root, "version-abc", Binary::Player, Some(uri));
101 assert_eq!(l.args, vec![uri.to_string()], "the uri must not be split or re-encoded");
102 assert!(l.exe.ends_with("RobloxPlayerBeta.exe"));
103 assert!(l.working_dir.ends_with("version-abc"));
104 let none = plan(root, "version-abc", Binary::Player, None);
105 assert_eq!(none.args, vec!["--app".to_string()]);
106}
107
108#[test]
109fn install_state_round_trips_on_disk() {
110 let root = std::env::temp_dir().join("nox-roblox-state");
111 let _ = std::fs::remove_dir_all(&root);
112 assert!(!nox_roblox::is_installed(&root, "version-x"));
113 assert!(nox_roblox::installed_versions(&root).is_empty());
114
115 let dir = nox_roblox::version_dir(&root, "version-x");
116 std::fs::create_dir_all(&dir).unwrap();
117 std::fs::write(dir.join(".noxstrap-installed"), "version-x").unwrap();
118 // Marker alone is not enough; the executable has to be there too.
119 assert!(!nox_roblox::is_installed(&root, "version-x"));
120 std::fs::write(dir.join("RobloxPlayerBeta.exe"), b"stub").unwrap();
121 assert!(nox_roblox::is_installed(&root, "version-x"));
122 assert_eq!(nox_roblox::installed_versions(&root), vec!["version-x".to_string()]);
123
124 nox_roblox::remove_version(&root, "version-x").unwrap();
125 assert!(!nox_roblox::is_installed(&root, "version-x"));
126}