--- a/crates/xodus-cli/src/commands/run.rs +++ b/crates/xodus-cli/src/commands/run.rs @@ -146,6 +146,7 @@ launch_image: &Path, materialize_chain: bool, ) -> (String, String) { + if materialize_chain { return (String::new(), String::new()); } let source_nt = source.to_string_lossy().replace('/', "\\"); let source_nt = source_nt.trim_end_matches('\\'); let mirror_unix = mirror_nt_prefix.map(|prefix| prefix.replace('\\', "/")); @@ -303,9 +304,7 @@ let entry_path = checked_relative_path(entry_suffix)?; let mut decrypted = HashMap::new(); for (name, fd) in fds { - if name.to_ascii_lowercase().ends_with(".exe") || checked_relative_path(name)? == entry_path { - decrypted.insert(checked_relative_path(name)?, *fd); - } + decrypted.insert(checked_relative_path(name)?, *fd); } if !decrypted.contains_key(&entry_path) { return Err(std::io::Error::new( @@ -325,7 +324,8 @@ } } let decrypted_files: HashSet = decrypted.keys().cloned().collect(); - let mirror = launch_view_directory(source_root, ".xodus-chain-")?; + let mirror = Builder::new().prefix("droiddeck-xbox-") + .permissions(std::os::unix::fs::PermissionsExt::from_mode(0o700)).tempdir()?; populate_launch_view( source_root, mirror.path(), @@ -725,6 +725,22 @@ ); } + if wine == "droiddeck:prepare" { + use std::io::Write; + if !materialize_chain { return ExitCode::FAILURE; } + let ready = serde_json::json!({ + "exe": stub_path, + "playfab": playfab_auth_header(client, tokens).await, + }); + println!("DROIDDECK_LAUNCH_V1 {}", ready); + let _ = std::io::stdout().flush(); + let mut release = String::new(); + let _ = std::io::stdin().read_line(&mut release); + drop(launch_mirror); + cleanup().await; + return ExitCode::SUCCESS; + } + let mut command = Command::new(wine); command .arg(&stub_path) @@ -831,21 +847,15 @@ } #[test] - fn full_launch_view_uses_real_executables_and_preserves_encrypted_dll_maps() { + fn full_launch_view_needs_no_inherited_descriptor_maps() { let entry = String::from("\\Launcher.exe"); let child = String::from("\\Bin\\Child.ExE"); let dll = String::from("\\Bin\\Encrypted.dll"); let files = [(&entry, 10), (&child, 11), (&dll, 12)]; let (nt, unix) = launch_file_maps(&files, Path::new("/package"), Some("\\view"), &entry, 10, Path::new("/view/Launcher.exe"), true); - assert!(nt.contains("10:\\??\\Z:\\package\\Launcher.exe")); - assert!(unix.contains("10:/package/Launcher.exe")); - assert!(!nt.contains("\\view\\Launcher.exe")); - assert!(!nt.contains("\\view\\Bin\\Child.ExE")); - assert!(!unix.contains("/view/Launcher.exe")); - assert!(!unix.contains("/view/Bin/Child.ExE")); - assert!(nt.contains("12:\\??\\Z:\\view\\Bin\\Encrypted.dll")); - assert!(unix.contains("12:/view/Bin/Encrypted.dll")); + assert!(nt.is_empty()); + assert!(unix.is_empty()); } #[test] @@ -945,6 +955,44 @@ } #[test] + fn launch_view_materializes_encrypted_dlls_in_private_storage() { + let root = tempfile::tempdir().unwrap(); + std::fs::write(root.path().join("Game.exe"), b"encrypted").unwrap(); + std::fs::write(root.path().join("Secret.dll"), b"encrypted-dll").unwrap(); + let mut image = tempfile::tempfile().unwrap(); + image.write_all(&fake_pe()).unwrap(); + let exe = String::from("Game.exe"); + let dll = String::from("Secret.dll"); + let files = [(&exe, image.as_raw_fd()), (&dll, image.as_raw_fd())]; + let (view, _, count, _) = create_launch_chain_mirror(root.path(), &exe, &files).unwrap(); + assert_eq!(count, 2); + assert_eq!(std::fs::read(view.path().join("Secret.dll")).unwrap(), fake_pe()); + assert!(!view.path().join("Secret.dll").is_symlink()); + assert_eq!(std::fs::read(root.path().join("Secret.dll")).unwrap(), b"encrypted-dll"); + use std::os::unix::fs::PermissionsExt; + assert_eq!(std::fs::metadata(view.path()).unwrap().permissions().mode() & 0o077, 0); + } + + + #[test] + fn launch_view_rejects_invalid_decrypted_library_without_changing_sources() { + let root = tempfile::tempdir().unwrap(); + std::fs::write(root.path().join("Game.exe"), b"encrypted executable").unwrap(); + std::fs::write(root.path().join("Bad.dll"), b"encrypted library").unwrap(); + let mut exe_image = tempfile::tempfile().unwrap(); + exe_image.write_all(&fake_pe()).unwrap(); + let mut dll_image = tempfile::tempfile().unwrap(); + dll_image.write_all(b"not a PE library").unwrap(); + let exe = String::from("Game.exe"); + let dll = String::from("Bad.dll"); + let images = [(&exe, exe_image.as_raw_fd()), (&dll, dll_image.as_raw_fd())]; + let error = create_launch_chain_mirror(root.path(), &exe, &images).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); + assert_eq!(std::fs::read(root.path().join("Game.exe")).unwrap(), b"encrypted executable"); + assert_eq!(std::fs::read(root.path().join("Bad.dll")).unwrap(), b"encrypted library"); + } + + #[test] fn launch_chain_rejects_parent_path_components() { assert!(checked_relative_path("..\\outside.exe").is_err()); } @@ -961,16 +1009,20 @@ assert_eq!(name, "Bin\\MINECRAFT.WINDOWS.EXE"); assert!(!fcntl_getfd(file.as_fd()).unwrap().contains(FdFlags::CLOEXEC)); let dll_name = String::from("Encrypted.dll"); - let mut dll = tempfile::tempfile().unwrap(); dll.write_all(b"synthetic decrypted library").unwrap(); + std::fs::write(root.path().join("Encrypted.dll"), b"encrypted library").unwrap(); + let mut dll = tempfile::tempfile().unwrap(); dll.write_all(&fake_pe()).unwrap(); let images = [(&dll_name, dll.as_raw_fd()), (&name, file.as_raw_fd())]; assert!(select_launch_entry(&images[..1], Some("bin/Minecraft.Windows.exe")).is_err()); assert_eq!(select_launch_entry(&images, Some("bin/Minecraft.Windows.exe")).unwrap().1, file.as_raw_fd()); let (mirror, entry, count, bytes) = create_launch_chain_mirror(root.path(), &name, &images).unwrap(); - assert_eq!(count, 1); assert_eq!(bytes, original.len() as u64); + assert_eq!(count, 2); assert_eq!(bytes, 2 * original.len() as u64); assert_eq!(std::fs::read(entry).unwrap(), original); - let (nt, _) = launch_file_maps(&images, root.path(), Some("\\synthetic-view"), &name, file.as_raw_fd(), &mirror.path().join("Bin/MINECRAFT.WINDOWS.EXE"), true); - assert!(nt.contains("synthetic-view\\Encrypted.dll")); - assert!(!nt.contains("synthetic-view\\Bin\\MINECRAFT.WINDOWS.EXE")); + let (nt, unix) = launch_file_maps(&images, root.path(), Some("\\synthetic-view"), &name, file.as_raw_fd(), &mirror.path().join("Bin/MINECRAFT.WINDOWS.EXE"), true); + assert!(nt.is_empty()); + assert!(unix.is_empty()); + assert_eq!(std::fs::read(mirror.path().join("Encrypted.dll")).unwrap(), fake_pe()); + assert!(!std::fs::symlink_metadata(mirror.path().join("Encrypted.dll")).unwrap().file_type().is_symlink()); + assert_eq!(std::fs::read(root.path().join("Encrypted.dll")).unwrap(), b"encrypted library"); assert_eq!(std::fs::read(root.path().join("Bin/MINECRAFT.WINDOWS.EXE")).unwrap(), original); assert_eq!(std::fs::read(root.path().join("MicrosoftGame.config")).unwrap(), b"metadata unchanged"); }