diff --git a/src/game_engine/unity/mono/attachment_tests.rs b/src/game_engine/unity/mono/attachment_tests.rs new file mode 100644 index 00000000..f6378ae5 --- /dev/null +++ b/src/game_engine/unity/mono/attachment_tests.rs @@ -0,0 +1,113 @@ +//! Attachment to an older Linux player whose engine is in the executable, +//! rather than a separate `UnityPlayer.so` module. + +use super::{profiles, Module}; +use crate::runtime::mock::{poll_once, with_modules}; +use std::{task::Poll, vec, vec::Vec}; + +const RUNTIME: u64 = 0x7F00_0000_0000; +#[cfg(feature = "alloc")] +const EXECUTABLE: u64 = 0x7F00_0010_0000; + +// The real Unity 5.6.7f1 Linux legacy-Mono fixture ships this libmono.so ID +// and no UnityPlayer.so. The process memory below is synthetic: it provides +// a GNU note, the assembly export and its getter, but does not run Unity. +const BUILD_ID: [u8; 20] = [ + 0xC1, 0xA5, 0x3E, 0xA7, 0x10, 0x9A, 0x2D, 0xA5, 0x82, 0x20, 0xAB, 0x30, 0xF4, 0xCA, 0xB7, 0xC8, + 0xCE, 0x8F, 0x38, 0x13, +]; + +fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); +} + +fn runtime() -> Vec { + let mut image = vec![0; 0x2000]; + put(&mut image, 0, b"\x7fELF"); + image[4..7].copy_from_slice(&[2, 1, 1]); + put(&mut image, 0x10, &3_u16.to_le_bytes()); + put(&mut image, 0x12, &0x3E_u16.to_le_bytes()); + put(&mut image, 0x20, &0x40_u64.to_le_bytes()); + put(&mut image, 0x36, &56_u16.to_le_bytes()); + put(&mut image, 0x38, &3_u16.to_le_bytes()); + put(&mut image, 0x40, &1_u32.to_le_bytes()); // PT_LOAD + put(&mut image, 0x60, &0x2000_u64.to_le_bytes()); + put(&mut image, 0x68, &0x2000_u64.to_le_bytes()); + put(&mut image, 0x78, &4_u32.to_le_bytes()); // PT_NOTE + put(&mut image, 0x88, &0x200_u64.to_le_bytes()); + put(&mut image, 0x98, &0x24_u64.to_le_bytes()); + put(&mut image, 0xA0, &0x24_u64.to_le_bytes()); + put(&mut image, 0xB0, &2_u32.to_le_bytes()); // PT_DYNAMIC + put(&mut image, 0xC0, &0x300_u64.to_le_bytes()); + put(&mut image, 0xD0, &0x30_u64.to_le_bytes()); + put(&mut image, 0xD8, &0x30_u64.to_le_bytes()); + put(&mut image, 0x200, &4_u32.to_le_bytes()); + put(&mut image, 0x204, &20_u32.to_le_bytes()); + put(&mut image, 0x208, &3_u32.to_le_bytes()); + put(&mut image, 0x20C, b"GNU\0"); + put(&mut image, 0x210, &BUILD_ID); + for (slot, (tag, value)) in [(6_u64, RUNTIME + 0x400), (5, RUNTIME + 0x500), (10, 0x80)] + .into_iter() + .enumerate() + { + put(&mut image, 0x300 + slot * 16, &tag.to_le_bytes()); + put(&mut image, 0x308 + slot * 16, &value.to_le_bytes()); + } + put(&mut image, 0x418, &1_u32.to_le_bytes()); // second Elf64_Sym name + put(&mut image, 0x420, &0x800_u64.to_le_bytes()); // symbol value + put(&mut image, 0x428, &0x100_u64.to_le_bytes()); // symbol size + put(&mut image, 0x430, &0x80_u32.to_le_bytes()); // terminate symbol walk + put(&mut image, 0x501, b"mono_assembly_foreach\0"); + put(&mut image, 0x800, &[0x48, 0x8B, 0x3D]); + put(&mut image, 0x803, &0x1F9_i32.to_le_bytes()); // global at 0xA00 + image +} + +#[test] +fn an_explicit_profile_needs_neither_a_player_nor_alloc() { + let runtime = runtime(); + with_modules( + &[(RUNTIME, &runtime)], + &[("libmono.so", RUNTIME, runtime.len() as u64)], + |process| { + let profile = profiles::UNITY_5_6_0F1_LINUX_MONO_X86_64; + let attached = Module::attach(process, profile).unwrap(); + assert_eq!(attached.profile, profile); + let Poll::Ready(attached) = poll_once(Module::wait_attach(process, profile)) else { + panic!("the explicit profile should attach immediately"); + }; + assert_eq!(attached.profile, profile); + }, + ); +} + +#[cfg(feature = "alloc")] +#[test] +fn an_unknown_legacy_runtime_uses_the_executables_version() { + let runtime = runtime(); + let mut executable = vec![0; 0x2000]; + let version = b"\x005.6.7f1\0"; + put(&mut executable, 0x100, version); + with_modules( + &[(EXECUTABLE, &executable), (RUNTIME, &runtime)], + &[ + ("fixture", EXECUTABLE, executable.len() as u64), + ("libmono.so", RUNTIME, runtime.len() as u64), + ], + |process| { + // Ensure this goes through version detection, not a known-ID match. + assert!(super::linux_builds::find(&BUILD_ID).is_none()); + assert_eq!( + Module::unity_version(process, super::BinaryFormat::ELF), + Some((5, 6, 7, 0)) + ); + let profile = profiles::UNITY_5_6_0F1_LINUX_MONO_X86_64; + let attached = Module::attach_auto_detect(process).unwrap(); + assert_eq!(attached.profile, profile); + let Poll::Ready(attached) = poll_once(Module::wait_attach_auto_detect(process)) else { + panic!("the executable's version should allow automatic attachment"); + }; + assert_eq!(attached.profile, profile); + }, + ); +} diff --git a/src/game_engine/unity/mono/linux_builds.rs b/src/game_engine/unity/mono/linux_builds.rs index a89552bb..ed5a4840 100644 --- a/src/game_engine/unity/mono/linux_builds.rs +++ b/src/game_engine/unity/mono/linux_builds.rs @@ -73,10 +73,10 @@ const fn id(written: &str) -> [u8; N] { } pub(super) const BUILDS: &[Build] = &[ - // 5.6.7f1, libmono.so + // 5.6.0f1, libmono.so Build { - build_id: &id::<20>("c1a53ea7109a2da58220ab30f4cab7c8ce8f3813"), - unity: (5, 6, 7, 3267), + build_id: &id::<20>("83bc621e6ccf123cafbc71c0894207071968f29e"), + unity: (5, 6, 0, 23754), profile: Profile { pointer_size: PointerSize::Bit64, library: Library::Mono, @@ -124,10 +124,59 @@ pub(super) const BUILDS: &[Build] = &[ v_table: MonoVTableOffsets { vtable: 0x48 }, }, }, - // 2017.3.0f3, libmonobdwgc-2.0.so + // 2017.1.0f1, libmonobdwgc-2.0.so Build { - build_id: &id::<20>("3cf4e78c4ef6b520fb777738602ac56c15f8ff0f"), - unity: (2017, 3, 0, 63597), + build_id: &id::<20>("488594493d54ab4951119e260d8d039d0fb94b53"), + unity: (2017, 1, 0, 32737), + profile: Profile { + pointer_size: PointerSize::Bit64, + library: Library::MonoBdwgc, + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x60, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x4A8, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + class_kind: None, + instance_size: Some(0x1C), + parent: 0x28, + nested_in: Some(0x30), + name: 0x40, + namespace: 0x48, + vtable_size: 0x54, + fields: 0xA8, + runtime_info: 0xF0, + field_count: 0x8C, + next_class_cache: 0xF8, + }, + generic: GenericOffsets { + generic_class: Some(0xD8), + container_class: Some(0x0), + }, + type_words: TypeOffsets { + data: Some(0x0), + kind: Some(0xA), + }, + field: FieldInfoOffsets { + type_: Some(0x0), + name: 0x8, + offset: 0x18, + stride: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }, + }, + // 2017.2.0f1, libmonobdwgc-2.0.so + Build { + build_id: &id::<20>("a276fd2ef4fd4fe54270bf3a1d8cc294f32c8684"), + unity: (2017, 2, 0, 58714), profile: Profile { pointer_size: PointerSize::Bit64, library: Library::MonoBdwgc, @@ -173,10 +222,112 @@ pub(super) const BUILDS: &[Build] = &[ v_table: MonoVTableOffsets { vtable: 0x40 }, }, }, - // 2017.4.40f1, libmono.so + // 2017.4.6f1, libmono.so + Build { + build_id: &id::<20>("806bb9c8b6bbb0b9aa964d423694bdd5d29aee4c"), + unity: (2017, 4, 6, 20272), + profile: Profile { + pointer_size: PointerSize::Bit64, + library: Library::Mono, + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x58, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x3D0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + class_kind: None, + instance_size: Some(0x1C), + parent: 0x28, + nested_in: Some(0x30), + name: 0x48, + namespace: 0x50, + vtable_size: 0x18, + fields: 0xA8, + runtime_info: 0xF8, + field_count: 0x94, + next_class_cache: 0x100, + }, + generic: GenericOffsets { + generic_class: Some(0xD8), + container_class: Some(0x0), + }, + type_words: TypeOffsets { + data: Some(0x0), + kind: Some(0xA), + }, + field: FieldInfoOffsets { + type_: Some(0x0), + name: 0x8, + offset: 0x18, + stride: 0x20, + }, + // Nothing reads this: these builds keep their statics in the slot + // `vtable_size` points at. + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // 2018.1.0f1, libmono.so + Build { + build_id: &id::<20>("8297a83ffb74d772baf82ebd05fa663e0bf9feda"), + unity: (2018, 1, 0, 30795), + profile: Profile { + pointer_size: PointerSize::Bit64, + library: Library::Mono, + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x58, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x3D0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + class_kind: None, + instance_size: Some(0x1C), + parent: 0x28, + nested_in: Some(0x30), + name: 0x40, + namespace: 0x48, + vtable_size: 0x18, + fields: 0xA0, + runtime_info: 0xF0, + field_count: 0x8C, + next_class_cache: 0xF8, + }, + generic: GenericOffsets { + generic_class: Some(0xD0), + container_class: Some(0x0), + }, + type_words: TypeOffsets { + data: Some(0x0), + kind: Some(0xA), + }, + field: FieldInfoOffsets { + type_: Some(0x0), + name: 0x8, + offset: 0x18, + stride: 0x20, + }, + // Nothing reads this: these builds keep their statics in the slot + // `vtable_size` points at. + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // 2018.1.7f1, libmono.so Build { - build_id: &id::<20>("93b5b95d7a6112b3f7d53b40e79a663cbcd62b14"), - unity: (2017, 4, 40, 5126), + build_id: &id::<20>("488c9cc9d9ed712a104549ba008c4dfc8504886e"), + unity: (2018, 1, 7, 46210), profile: Profile { pointer_size: PointerSize::Bit64, library: Library::Mono, @@ -224,10 +375,10 @@ pub(super) const BUILDS: &[Build] = &[ v_table: MonoVTableOffsets { vtable: 0x48 }, }, }, - // 2021.2.20f1, UnityPlayer.so + // 2021.2.0f1, UnityPlayer.so Build { - build_id: &id::<8>("8efc4cbe377f5467"), - unity: (2021, 2, 20, 62729), + build_id: &id::<8>("18a01e8f0aa6d238"), + unity: (2021, 2, 0, 61932), profile: Profile { pointer_size: PointerSize::Bit64, library: Library::MonoBdwgc, @@ -280,15 +431,15 @@ mod tests { use super::{find, id, BUILDS}; use crate::PointerSize; - // The build ID of 2017.3's Mono library, as it is stored. + // The build ID of 2017.2's Mono library, as it is stored. const STORED: [u8; 20] = [ - 0x3C, 0xF4, 0xE7, 0x8C, 0x4E, 0xF6, 0xB5, 0x20, 0xFB, 0x77, 0x77, 0x38, 0x60, 0x2A, 0xC5, - 0x6C, 0x15, 0xF8, 0xFF, 0x0F, + 0xA2, 0x76, 0xFD, 0x2E, 0xF4, 0xFD, 0x4F, 0xE5, 0x42, 0x70, 0xBF, 0x3A, 0x1D, 0x8C, 0xC2, + 0x94, 0xF3, 0x2C, 0x86, 0x84, ]; #[test] fn parses_written_build_ids_into_stored_bytes() { - assert_eq!(id::<20>("3cf4e78c4ef6b520fb777738602ac56c15f8ff0f"), STORED); + assert_eq!(id::<20>("a276fd2ef4fd4fe54270bf3a1d8cc294f32c8684"), STORED); } #[test] diff --git a/src/game_engine/unity/mono/mod.rs b/src/game_engine/unity/mono/mod.rs index 56968999..793d522f 100644 --- a/src/game_engine/unity/mono/mod.rs +++ b/src/game_engine/unity/mono/mod.rs @@ -1,23 +1,37 @@ //! Support for attaching to Unity games that are using the standard Mono //! backend. +//! +//! Automatic attachment requires the `alloc` feature so that a game without +//! a separate player module can be identified through its executable. A +//! `no_std` auto splitter using allocation must provide a global allocator. Use +//! [`Module::attach`] or [`Module::wait_attach`] with a known [`Profile`] to +//! attach without allocation on Windows and Linux. macOS support requires +//! `alloc` for Mach-O symbol lookup even with an explicit profile. #[cfg(feature = "alloc")] use crate::file_format::macho; use arrayvec::ArrayVec; use bytemuck::CheckedBitPattern; +#[cfg(feature = "alloc")] use core::fmt; +#[cfg(feature = "alloc")] +use crate::print_limited; + use crate::{ file_format::{elf, pe}, future::retry, - print_limited, signature::Signature, string::ArrayCString, Address, Address32, Error, PointerSize, Process, }; +// The tables also supply the allocation-free profile constants. Their lookup +// helpers are only used by automatic attachment, which requires `alloc`. +#[cfg_attr(not(feature = "alloc"), allow(dead_code))] mod builds; mod image; +#[cfg_attr(not(feature = "alloc"), allow(dead_code))] mod linux_builds; #[cfg(feature = "alloc")] mod mac_builds; @@ -33,8 +47,10 @@ pub use offsets::{ ImageOffsets, MonoVTableOffsets, Profile, TypeOffsets, }; #[cfg(all(test, not(target_family = "wasm")))] -mod collections_tests; +mod attachment_tests; #[cfg(all(test, not(target_family = "wasm")))] +mod collections_tests; +#[cfg(all(test, feature = "alloc", not(target_family = "wasm")))] mod identity_tests; #[cfg(all(test, not(target_family = "wasm")))] mod profiles_tests; @@ -68,6 +84,7 @@ pub enum Library { /// The identity of one exact runtime binary, and the module it was read /// from, so a build nobody has measured is reported as the file to look at. +#[cfg(feature = "alloc")] enum Identity { Debug(pe::DebugId), Build(elf::BuildId, &'static str), @@ -75,6 +92,7 @@ enum Identity { Uuid(macho::Uuid), } +#[cfg(feature = "alloc")] impl Identity { /// Reads what names the Mono library's build. On ELF the library's own /// build ID answers whenever it has one, and `UnityPlayer.so`'s answers @@ -118,6 +136,7 @@ impl Identity { } } +#[cfg(feature = "alloc")] impl fmt::Debug for Identity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -135,12 +154,17 @@ impl Module { /// measured for that exact build. Otherwise it uses the newest build of /// the same runtime library at or below the game's Unity version. On /// Windows, a game older than every build doesn't attach. On Linux and - /// Mac, it gets the oldest build. A game that ships - /// no player module (Windows games before Unity 2017.2, Mac and Linux - /// games for some versions more) carries its version in its own - /// executable, and reading it there needs the `alloc` feature. + /// Mac, it gets the oldest build. + /// + /// This requires the `alloc` feature. A game without a separate player + /// module carries its version in its executable, and finding that + /// executable allocates its path. No fixed-size path buffer is used. + /// A `no_std` auto splitter must provide a global allocator. /// If you know the build in advance, use [`attach`](Self::attach) with - /// its profile instead. + /// its profile instead; on Windows and Linux, explicit-profile attachment + /// does not allocate. + #[cfg(feature = "alloc")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub fn attach_auto_detect(process: &Process) -> Option { let (module_range, format, name, library) = Self::find_runtime_module(process)?; let pointer_size = Self::pointer_size(process, module_range, format)?; @@ -190,8 +214,10 @@ impl Module { /// with the provided measured [`Profile`]. The profile's pointer width /// and runtime library must match the target process. Use a built-in /// [`profiles`] constant for a known player, or construct a custom - /// profile for another measured layout. If the target is not known in - /// advance, use [`attach_auto_detect`](Self::attach_auto_detect) instead. + /// profile for another measured layout. On Windows and Linux, this does + /// not allocate or require the `alloc` feature. macOS requires `alloc` + /// for Mach-O symbol lookup. If the target is not known in advance, + /// enable `alloc` and use `Module::attach_auto_detect` instead. pub fn attach(process: &Process, profile: Profile) -> Option { let (module_range, format, _, library) = Self::find_runtime_module(process)?; let pointer_size = Self::pointer_size(process, module_range, format)?; @@ -229,6 +255,7 @@ impl Module { /// of `UnityPlayer.dll`. On Linux and Mac the player carries it as a /// string. A game without a player module keeps both in its executable, /// which only the `alloc` feature can name. + #[cfg(feature = "alloc")] fn unity_version(process: &Process, format: BinaryFormat) -> Option<(u16, u16, u16, u16)> { let player = match format { BinaryFormat::PE => process.get_module_range("UnityPlayer.dll").ok(), @@ -238,7 +265,6 @@ impl Module { #[allow(unreachable_patterns)] _ => None, }; - #[cfg(feature = "alloc")] let player = player.or_else(|| process.get_main_module_range().ok()); let player = player?; @@ -263,6 +289,7 @@ impl Module { /// the letter of the release counts, so a date or a build number in the /// same module is passed over. A player can hold older version strings /// too, so this returns the newest one. + #[cfg(feature = "alloc")] fn version_string(process: &Process, module: (Address, u64)) -> Option<(u16, u16, u16, u16)> { const FOUR_DIGITS: Signature<6> = Signature::new("00 3? 3? 3? 3? 2E"); const ONE_DIGIT: Signature<4> = Signature::new("00 3? 2E 3?"); @@ -279,6 +306,7 @@ impl Module { /// Parses `major.minor.patch` followed by a release letter out of the /// head of `text`. + #[cfg(feature = "alloc")] fn parse_version(text: &[u8]) -> Option<(u16, u16, u16, u16)> { // Reads a number of up to four digits and returns what follows it. fn number(text: &[u8]) -> Option<(u16, &[u8])> { @@ -734,14 +762,21 @@ impl Module { /// /// This is the `await`able version of the /// [`attach_auto_detect`](Self::attach_auto_detect) function, yielding back - /// to the runtime between each try. + /// to the runtime between each try. It requires the `alloc` feature so + /// that the executable's path can be allocated when no player module is + /// present. For allocation-free attachment, use [`wait_attach`](Self::wait_attach) + /// with a known profile instead. + #[cfg(feature = "alloc")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub async fn wait_attach_auto_detect(process: &Process) -> Module { retry(|| Self::attach_auto_detect(process)).await } /// Attaches to a Unity game that is using the standard Mono backend with /// the provided measured [`Profile`]. The profile's pointer width and - /// runtime library must match the target process. + /// runtime library must match the target process. On Windows and Linux, + /// this does not allocate or require the `alloc` feature. macOS requires + /// `alloc` for Mach-O symbol lookup. /// /// This is the `await`able version of [`attach`](Self::attach), yielding /// back to the runtime between each try. diff --git a/src/game_engine/unity/mono/profiles.rs b/src/game_engine/unity/mono/profiles.rs index 1a1ae8d3..7a006ac5 100644 --- a/src/game_engine/unity/mono/profiles.rs +++ b/src/game_engine/unity/mono/profiles.rs @@ -62,14 +62,20 @@ profiles! { profiles! { linux_builds::BUILDS; - /// Unity 5.6.7f1, `libmono.so`, x86-64. - UNITY_5_6_7F1_LINUX_MONO_X86_64 = 0; - /// Unity 2017.3.0f3, `libmonobdwgc-2.0.so`, x86-64. - UNITY_2017_3_0F3_LINUX_MONO_BDWGC_X86_64 = 1; - /// Unity 2017.4.40f1, `libmono.so`, x86-64. - UNITY_2017_4_40F1_LINUX_MONO_X86_64 = 2; - /// Unity 2021.2.20f1, `libmonobdwgc-2.0.so`, x86-64. - UNITY_2021_2_20F1_LINUX_MONO_BDWGC_X86_64 = 3; + /// Unity 5.6.0f1, `libmono.so`, x86-64. + UNITY_5_6_0F1_LINUX_MONO_X86_64 = 0; + /// Unity 2017.1.0f1, `libmonobdwgc-2.0.so`, x86-64. + UNITY_2017_1_0F1_LINUX_MONO_BDWGC_X86_64 = 1; + /// Unity 2017.2.0f1, `libmonobdwgc-2.0.so`, x86-64. + UNITY_2017_2_0F1_LINUX_MONO_BDWGC_X86_64 = 2; + /// Unity 2017.4.6f1, `libmono.so`, x86-64. + UNITY_2017_4_6F1_LINUX_MONO_X86_64 = 3; + /// Unity 2018.1.0f1, `libmono.so`, x86-64. + UNITY_2018_1_0F1_LINUX_MONO_X86_64 = 4; + /// Unity 2018.1.7f1, `libmono.so`, x86-64. + UNITY_2018_1_7F1_LINUX_MONO_X86_64 = 5; + /// Unity 2021.2.0f1, `libmonobdwgc-2.0.so`, x86-64. + UNITY_2021_2_0F1_LINUX_MONO_BDWGC_X86_64 = 6; } profiles! { diff --git a/src/game_engine/unity/mono/profiles_tests.rs b/src/game_engine/unity/mono/profiles_tests.rs index 50a5e33b..09152571 100644 --- a/src/game_engine/unity/mono/profiles_tests.rs +++ b/src/game_engine/unity/mono/profiles_tests.rs @@ -110,26 +110,41 @@ fn windows_constants_name_the_expected_profiles() { #[test] fn linux_constants_name_the_expected_profiles() { - let profiles: [((u16, u16, u16, u16), Library, Profile); 4] = [ + let profiles: [((u16, u16, u16, u16), Library, Profile); 7] = [ ( - (5, 6, 7, 3267), + (5, 6, 0, 23754), Library::Mono, - UNITY_5_6_7F1_LINUX_MONO_X86_64, + UNITY_5_6_0F1_LINUX_MONO_X86_64, ), ( - (2017, 3, 0, 63597), + (2017, 1, 0, 32737), Library::MonoBdwgc, - UNITY_2017_3_0F3_LINUX_MONO_BDWGC_X86_64, + UNITY_2017_1_0F1_LINUX_MONO_BDWGC_X86_64, ), ( - (2017, 4, 40, 5126), + (2017, 2, 0, 58714), + Library::MonoBdwgc, + UNITY_2017_2_0F1_LINUX_MONO_BDWGC_X86_64, + ), + ( + (2017, 4, 6, 20272), + Library::Mono, + UNITY_2017_4_6F1_LINUX_MONO_X86_64, + ), + ( + (2018, 1, 0, 30795), Library::Mono, - UNITY_2017_4_40F1_LINUX_MONO_X86_64, + UNITY_2018_1_0F1_LINUX_MONO_X86_64, ), ( - (2021, 2, 20, 62729), + (2018, 1, 7, 46210), + Library::Mono, + UNITY_2018_1_7F1_LINUX_MONO_X86_64, + ), + ( + (2021, 2, 0, 61932), Library::MonoBdwgc, - UNITY_2021_2_20F1_LINUX_MONO_BDWGC_X86_64, + UNITY_2021_2_0F1_LINUX_MONO_BDWGC_X86_64, ), ]; for (unity, library, profile) in profiles { diff --git a/src/game_engine/unity/mono/version_tests.rs b/src/game_engine/unity/mono/version_tests.rs index 367b0f86..e5e85287 100644 --- a/src/game_engine/unity/mono/version_tests.rs +++ b/src/game_engine/unity/mono/version_tests.rs @@ -4,12 +4,19 @@ #[cfg(feature = "alloc")] use super::mac_builds; -use super::{builds, linux_builds, Library, Module}; +#[cfg(feature = "alloc")] +use super::Module; +use super::{builds, linux_builds, Library}; +#[cfg(feature = "alloc")] use crate::runtime::mock::{unity_player_image, with_modules, with_process}; -use crate::{Address, PointerSize, Process}; +use crate::PointerSize; +#[cfg(feature = "alloc")] +use crate::{Address, Process}; +#[cfg(feature = "alloc")] use std::vec; use std::vec::Vec; +#[cfg(feature = "alloc")] const BASE: u64 = 0x1900_0000; // Checks the rule against every entry of the Windows table, so adding or @@ -79,15 +86,28 @@ fn linux_builds_take_the_nearest_too() { .unwrap() .unity }; + assert_eq!( + linux((2017, 1, 0, 0), Library::MonoBdwgc), + (2017, 1, 0, 32737) + ); assert_eq!( linux((2020, 1, 0, 0), Library::MonoBdwgc), - (2017, 3, 0, 63597) + (2017, 2, 0, 58714) + ); + assert_eq!( + linux((2021, 2, 0, 0), Library::MonoBdwgc), + (2021, 2, 0, 61932) ); assert_eq!( linux((6000, 0, 0, 0), Library::MonoBdwgc), - (2021, 2, 20, 62729) + (2021, 2, 0, 61932) ); - assert_eq!(linux((2017, 2, 0, 0), Library::Mono), (5, 6, 7, 3267)); + assert_eq!(linux((2017, 2, 0, 0), Library::Mono), (5, 6, 0, 23754)); + assert_eq!(linux((2017, 4, 6, 0), Library::Mono), (2017, 4, 6, 20272)); + // libmono.so goes back to the 5.6.0 layout in 2018.1.0, then forward + // again in 2018.1.7. + assert_eq!(linux((2018, 1, 0, 0), Library::Mono), (2018, 1, 0, 30795)); + assert_eq!(linux((2018, 1, 7, 0), Library::Mono), (2018, 1, 7, 46210)); } #[cfg(feature = "alloc")] @@ -100,6 +120,7 @@ fn mac_builds_take_the_nearest_too() { // Linux and Mac players carry no file version. The Unity version sits in // the player as a string like `2021.3.11f1`, and only a string of that // shape counts, so a date or a build number nearby is skipped. +#[cfg(feature = "alloc")] #[test] fn the_version_string_in_the_player_names_major_minor_and_patch() { let version = |text: &[u8]| { @@ -121,6 +142,7 @@ fn the_version_string_in_the_player_names_major_minor_and_patch() { // A Linux player holds older version strings too, like `2018.3.0a1` and // `5.0.0a1`, sometimes ahead of its own. The newest one is the player's. +#[cfg(feature = "alloc")] #[test] fn the_newest_version_string_in_the_player_is_its_version() { let mut image = vec![0; 0x2000]; @@ -133,6 +155,7 @@ fn the_newest_version_string_in_the_player_is_its_version() { } // On Windows the file version of `UnityPlayer.dll` is the Unity version. +#[cfg(feature = "alloc")] #[test] fn the_player_file_version_is_the_unity_version() { let player = unity_player_image((2021, 3, 11, 23713)); @@ -179,22 +202,3 @@ fn the_executable_carries_the_unity_version_when_there_is_no_player() { }, ); } - -#[cfg(not(feature = "alloc"))] -#[test] -fn without_alloc_a_game_with_no_player_has_no_version() { - let executable = unity_player_image((5, 6, 7, 3267)); - with_modules( - &[(BASE, &executable)], - &[ - ("game.exe", BASE, 0x1000), - ("mono.dll", BASE + 0x10000, 0x1000), - ], - |process| { - assert_eq!( - Module::unity_version(process, super::BinaryFormat::PE), - None - ); - }, - ); -} diff --git a/src/runtime/mock.rs b/src/runtime/mock.rs index c91b14af..4e70af15 100644 --- a/src/runtime/mock.rs +++ b/src/runtime/mock.rs @@ -55,6 +55,7 @@ pub fn with_modules( /// A `UnityPlayer.dll` image of 0x1000 bytes: a PE32+ header for x64 whose /// resource directory holds one version resource carrying the given file /// version as its four parts. +#[cfg(feature = "alloc")] pub fn unity_player_image(unity: (u16, u16, u16, u16)) -> Vec { fn put(image: &mut [u8], at: usize, bytes: &[u8]) { image[at..at + bytes.len()].copy_from_slice(bytes);