| 1 | use nox_roblox::manifest::{package_dir, Manifest}; |
| 2 | use nox_roblox::md5::{md5_hex, to_hex, Md5}; |
| 3 | use std::process::Command; |
| 4 | |
| 5 | const REAL_MANIFEST: &str = "v0\nRobloxApp.zip\naeaa4432a6005a7cead977db05447511\n134901428\n173551147\ncontent-avatar.zip\n961cd6b55ab23d2efcc9a7319191aa37\n293687\n1020627\nRobloxPlayerInstaller.exe\nd41d8cd98f00b204e9800998ecf8427e\n100\n100\n"; |
| 6 | |
| 7 | #[test] |
| 8 | fn 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] |
| 21 | fn 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] |
| 29 | fn 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] |
| 49 | fn 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] |
| 95 | fn 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] |
| 109 | fn 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 | } |