From fb8e789ddf90841419059a61b69fcc819ba62355 Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Mon, 6 Apr 2026 22:22:28 -0600 Subject: [PATCH 1/7] implement simplistic version parsing --- src/mach/mod.rs | 1 + src/mach/version.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++ tests/macho.rs | 9 +++ 3 files changed, 142 insertions(+) create mode 100644 src/mach/version.rs diff --git a/src/mach/mod.rs b/src/mach/mod.rs index 034df02d4..126bb7479 100644 --- a/src/mach/mod.rs +++ b/src/mach/mod.rs @@ -20,6 +20,7 @@ pub mod load_command; pub mod relocation; pub mod segment; pub mod symbols; +pub mod version; pub use self::constants::cputype; diff --git a/src/mach/version.rs b/src/mach/version.rs new file mode 100644 index 000000000..b61912a35 --- /dev/null +++ b/src/mach/version.rs @@ -0,0 +1,132 @@ +/*- + * Copyright: see LICENSE file + */ + +use crate::error::Error; +use crate::mach::cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64}; +use crate::mach::load_command::CommandVariant; +use crate::mach::{Mach, MachO, SingleArch}; +use std::cmp::Ordering; +use std::collections::VecDeque; +use std::str::FromStr; +use std::{env, fmt}; + +#[derive(Eq, Debug)] +pub struct Version { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +impl fmt::Display for Version { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write strictly the first element into the supplied output + // stream: `f`. Returns `fmt::Result` which indicates whether the + // operation succeeded or failed. Note that `write!` uses syntax which + // is very similar to `println!`. + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + let mao = self.major.cmp(&other.major); + let mio = self.minor.cmp(&other.minor); + let pao = self.patch.cmp(&other.patch); + if mao == Ordering::Equal && mio == Ordering::Equal { + pao + } else if mao == Ordering::Equal { + mio + } else { + mao + } + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + self.major == other.major && self.minor == other.minor && self.patch == other.patch + } +} + +impl FromStr for Version { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut parts = s + .trim() + .split('.') + .map(|p| p.parse::().unwrap()) + .take(3) + .collect::>(); + + Ok(Self { + major: parts.pop_front().unwrap(), + minor: parts.pop_front().unwrap_or(0), + patch: parts.pop_front().unwrap_or(0), + }) + } +} + +impl From for Version { + fn from(packed: u32) -> Self { + // X.Y.Z is encoded in nibbles xxxx.yy.zz + // 12.6 = 0b0000_0000_0000_1100_0000_0110_0000_0000 + let major = (packed & 0b1111_1111_1111_1111_0000_0000_0000_0000u32) >> 16; + let minor = (packed & 0b0000_0000_0000_0000_1111_1111_0000_0000u32) >> 8; + let patch = (packed & 0b0000_0000_0000_0000_0000_0000_1111_1111u32) >> 0; + Self { + major, + minor, + patch, + } + } +} + +impl From> for Version { + fn from(b: MachO) -> Self { + let packed = b + .load_commands + .iter() + .find_map(|c| match c.command { + CommandVariant::VersionMinMacosx(v) => Some(v.version), + CommandVariant::BuildVersion(v) => Some(v.minos), + _ => None, + }) + .unwrap(); + Self::from(packed) + } +} + +impl From> for Version { + fn from(b: Mach) -> Self { + match b { + Mach::Binary(b) => Version::from(b), + Mach::Fat(f) => { + match f + .find(|r| { + r.unwrap().cputype + == match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { + Ok("x86_64") => CPU_TYPE_X86_64, + Ok("aarch64") => CPU_TYPE_ARM64, + _ => panic!("unknown arch"), + } + }) + .unwrap() + .ok() + .unwrap() + { + SingleArch::MachO(b) => Version::from(b), + SingleArch::Archive(_) => panic!("lib is an archive?"), + } + } + } + } +} diff --git a/tests/macho.rs b/tests/macho.rs index eac0d9828..6ff51d764 100644 --- a/tests/macho.rs +++ b/tests/macho.rs @@ -524,6 +524,15 @@ fn parse_sections() { } } +#[test] +fn version() { + let bytes = &DEADBEEF_MACH_64; + let mach = Mach::parse(&bytes[..]).unwrap(); + let actual: version::Version = mach.into(); + let expected = version::Version { major: 10, minor: 10, patch: 0 }; + assert_eq!(actual, expected); +} + #[test] fn iter_symbols() { let bytes = &DEADBEEF_MACH_64; From fc85965d18fbccafbfb3efafffda1a33304476a1 Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Mon, 6 Apr 2026 22:43:47 -0600 Subject: [PATCH 2/7] fix lints --- tests/macho.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/macho.rs b/tests/macho.rs index 6ff51d764..3e83f5368 100644 --- a/tests/macho.rs +++ b/tests/macho.rs @@ -529,7 +529,11 @@ fn version() { let bytes = &DEADBEEF_MACH_64; let mach = Mach::parse(&bytes[..]).unwrap(); let actual: version::Version = mach.into(); - let expected = version::Version { major: 10, minor: 10, patch: 0 }; + let expected = version::Version { + major: 10, + minor: 10, + patch: 0, + }; assert_eq!(actual, expected); } From db2f39e29ce45f22edd6bae733c14536c25815c9 Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Mon, 6 Apr 2026 22:50:10 -0600 Subject: [PATCH 3/7] try to be nostd friendly --- src/mach/version.rs | 111 +++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/src/mach/version.rs b/src/mach/version.rs index b61912a35..efd200f82 100644 --- a/src/mach/version.rs +++ b/src/mach/version.rs @@ -6,10 +6,12 @@ use crate::error::Error; use crate::mach::cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64}; use crate::mach::load_command::CommandVariant; use crate::mach::{Mach, MachO, SingleArch}; -use std::cmp::Ordering; -use std::collections::VecDeque; -use std::str::FromStr; -use std::{env, fmt}; +if_std! { + use std::cmp::Ordering; + use std::collections::VecDeque; + use std::str::FromStr; + use std::{env, fmt}; +} #[derive(Eq, Debug)] pub struct Version { @@ -18,35 +20,37 @@ pub struct Version { pub patch: u32, } -impl fmt::Display for Version { - // This trait requires `fmt` with this exact signature. - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Write strictly the first element into the supplied output - // stream: `f`. Returns `fmt::Result` which indicates whether the - // operation succeeded or failed. Note that `write!` uses syntax which - // is very similar to `println!`. - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) +if_std! { + impl fmt::Display for Version { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write strictly the first element into the supplied output + // stream: `f`. Returns `fmt::Result` which indicates whether the + // operation succeeded or failed. Note that `write!` uses syntax which + // is very similar to `println!`. + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } } -} -impl Ord for Version { - fn cmp(&self, other: &Self) -> Ordering { - let mao = self.major.cmp(&other.major); - let mio = self.minor.cmp(&other.minor); - let pao = self.patch.cmp(&other.patch); - if mao == Ordering::Equal && mio == Ordering::Equal { - pao - } else if mao == Ordering::Equal { - mio - } else { - mao + impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + let mao = self.major.cmp(&other.major); + let mio = self.minor.cmp(&other.minor); + let pao = self.patch.cmp(&other.patch); + if mao == Ordering::Equal && mio == Ordering::Equal { + pao + } else if mao == Ordering::Equal { + mio + } else { + mao + } } } -} -impl PartialOrd for Version { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } } @@ -55,23 +59,24 @@ impl PartialEq for Version { self.major == other.major && self.minor == other.minor && self.patch == other.patch } } +if_std! { + impl FromStr for Version { + type Err = Error; -impl FromStr for Version { - type Err = Error; - - fn from_str(s: &str) -> Result { - let mut parts = s + fn from_str(s: &str) -> Result { + let mut parts = s .trim() .split('.') .map(|p| p.parse::().unwrap()) .take(3) .collect::>(); - Ok(Self { - major: parts.pop_front().unwrap(), - minor: parts.pop_front().unwrap_or(0), - patch: parts.pop_front().unwrap_or(0), - }) + Ok(Self { + major: parts.pop_front().unwrap(), + minor: parts.pop_front().unwrap_or(0), + patch: parts.pop_front().unwrap_or(0), + }) + } } } @@ -105,26 +110,28 @@ impl From> for Version { } } -impl From> for Version { - fn from(b: Mach) -> Self { - match b { - Mach::Binary(b) => Version::from(b), - Mach::Fat(f) => { - match f +if_std! { + impl From> for Version { + fn from(b: Mach) -> Self { + match b { + Mach::Binary(b) => Version::from(b), + Mach::Fat(f) => { + match f .find(|r| { r.unwrap().cputype - == match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { - Ok("x86_64") => CPU_TYPE_X86_64, - Ok("aarch64") => CPU_TYPE_ARM64, - _ => panic!("unknown arch"), - } + == match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { + Ok("x86_64") => CPU_TYPE_X86_64, + Ok("aarch64") => CPU_TYPE_ARM64, + _ => panic!("unknown arch"), + } }) .unwrap() .ok() .unwrap() - { - SingleArch::MachO(b) => Version::from(b), - SingleArch::Archive(_) => panic!("lib is an archive?"), + { + SingleArch::MachO(b) => Version::from(b), + SingleArch::Archive(_) => panic!("lib is an archive?"), + } } } } From 6884397c1d21a7cb2fa1931af1d765f9a665a849 Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Mon, 6 Apr 2026 23:04:49 -0600 Subject: [PATCH 4/7] organize file --- src/mach/version.rs | 83 ++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/mach/version.rs b/src/mach/version.rs index efd200f82..f61388318 100644 --- a/src/mach/version.rs +++ b/src/mach/version.rs @@ -2,11 +2,13 @@ * Copyright: see LICENSE file */ -use crate::error::Error; -use crate::mach::cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64}; +use crate::mach::MachO; use crate::mach::load_command::CommandVariant; -use crate::mach::{Mach, MachO, SingleArch}; + if_std! { + use crate::mach::{Mach, SingleArch}; + use crate::error::Error; + use crate::mach::cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64}; use std::cmp::Ordering; use std::collections::VecDeque; use std::str::FromStr; @@ -20,6 +22,42 @@ pub struct Version { pub patch: u32, } +impl From for Version { + fn from(packed: u32) -> Self { + // X.Y.Z is encoded in nibbles xxxx.yy.zz + // 12.6 = 0b0000_0000_0000_1100_0000_0110_0000_0000 + let major = (packed & 0b1111_1111_1111_1111_0000_0000_0000_0000u32) >> 16; + let minor = (packed & 0b0000_0000_0000_0000_1111_1111_0000_0000u32) >> 8; + let patch = (packed & 0b0000_0000_0000_0000_0000_0000_1111_1111u32) >> 0; + Self { + major, + minor, + patch, + } + } +} + +impl From> for Version { + fn from(b: MachO) -> Self { + let packed = b + .load_commands + .iter() + .find_map(|c| match c.command { + CommandVariant::VersionMinMacosx(v) => Some(v.version), + CommandVariant::BuildVersion(v) => Some(v.minos), + _ => None, + }) + .unwrap(); + Self::from(packed) + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + self.major == other.major && self.minor == other.minor && self.patch == other.patch + } +} + if_std! { impl fmt::Display for Version { // This trait requires `fmt` with this exact signature. @@ -52,14 +90,7 @@ if_std! { Some(self.cmp(other)) } } -} -impl PartialEq for Version { - fn eq(&self, other: &Self) -> bool { - self.major == other.major && self.minor == other.minor && self.patch == other.patch - } -} -if_std! { impl FromStr for Version { type Err = Error; @@ -78,39 +109,7 @@ if_std! { }) } } -} - -impl From for Version { - fn from(packed: u32) -> Self { - // X.Y.Z is encoded in nibbles xxxx.yy.zz - // 12.6 = 0b0000_0000_0000_1100_0000_0110_0000_0000 - let major = (packed & 0b1111_1111_1111_1111_0000_0000_0000_0000u32) >> 16; - let minor = (packed & 0b0000_0000_0000_0000_1111_1111_0000_0000u32) >> 8; - let patch = (packed & 0b0000_0000_0000_0000_0000_0000_1111_1111u32) >> 0; - Self { - major, - minor, - patch, - } - } -} -impl From> for Version { - fn from(b: MachO) -> Self { - let packed = b - .load_commands - .iter() - .find_map(|c| match c.command { - CommandVariant::VersionMinMacosx(v) => Some(v.version), - CommandVariant::BuildVersion(v) => Some(v.minos), - _ => None, - }) - .unwrap(); - Self::from(packed) - } -} - -if_std! { impl From> for Version { fn from(b: Mach) -> Self { match b { From e090fa454a5a607cd47fd5976fd8db3742359c4a Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Mon, 6 Apr 2026 23:20:13 -0600 Subject: [PATCH 5/7] Test more of the implementation --- tests/macho.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/macho.rs b/tests/macho.rs index 3e83f5368..e355a2cea 100644 --- a/tests/macho.rs +++ b/tests/macho.rs @@ -529,12 +529,9 @@ fn version() { let bytes = &DEADBEEF_MACH_64; let mach = Mach::parse(&bytes[..]).unwrap(); let actual: version::Version = mach.into(); - let expected = version::Version { - major: 10, - minor: 10, - patch: 0, - }; - assert_eq!(actual, expected); + let expected = "10.10.0".parse::(); + assert!(expected.is_ok()); // Test parsing strings + assert_eq!(actual, expected.unwrap()); // Test parsing binaries } #[test] From 11cee3d3a6d4e13d6d9e8927a5a24bf5ebd97a00 Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Tue, 21 Apr 2026 10:36:55 -0600 Subject: [PATCH 6/7] fixup use of unwrap --- src/mach/version.rs | 94 +++++++++++++++++++++++++-------------------- tests/macho.rs | 19 ++++++++- 2 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/mach/version.rs b/src/mach/version.rs index f61388318..9f7587a42 100644 --- a/src/mach/version.rs +++ b/src/mach/version.rs @@ -6,13 +6,13 @@ use crate::mach::MachO; use crate::mach::load_command::CommandVariant; if_std! { + use crate::error; use crate::mach::{Mach, SingleArch}; - use crate::error::Error; - use crate::mach::cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64}; + use crate::mach::cputype::CpuType; use std::cmp::Ordering; - use std::collections::VecDeque; + use std::collections::{VecDeque,HashMap}; use std::str::FromStr; - use std::{env, fmt}; + use std::fmt; } #[derive(Eq, Debug)] @@ -26,29 +26,24 @@ impl From for Version { fn from(packed: u32) -> Self { // X.Y.Z is encoded in nibbles xxxx.yy.zz // 12.6 = 0b0000_0000_0000_1100_0000_0110_0000_0000 - let major = (packed & 0b1111_1111_1111_1111_0000_0000_0000_0000u32) >> 16; - let minor = (packed & 0b0000_0000_0000_0000_1111_1111_0000_0000u32) >> 8; - let patch = (packed & 0b0000_0000_0000_0000_0000_0000_1111_1111u32) >> 0; Self { - major, - minor, - patch, + major: (packed & 0b1111_1111_1111_1111_0000_0000_0000_0000u32) >> 16, + minor: (packed & 0b0000_0000_0000_0000_1111_1111_0000_0000u32) >> 8, + patch: (packed & 0b0000_0000_0000_0000_0000_0000_1111_1111u32) >> 0, } } } -impl From> for Version { - fn from(b: MachO) -> Self { - let packed = b - .load_commands +impl MachO<'_> { + pub fn version(&self) -> Option { + self.load_commands .iter() .find_map(|c| match c.command { CommandVariant::VersionMinMacosx(v) => Some(v.version), CommandVariant::BuildVersion(v) => Some(v.minos), _ => None, }) - .unwrap(); - Self::from(packed) + .map(Version::from) } } @@ -92,47 +87,62 @@ if_std! { } impl FromStr for Version { - type Err = Error; + type Err = error::Error; fn from_str(s: &str) -> Result { let mut parts = s .trim() .split('.') - .map(|p| p.parse::().unwrap()) + .map(|p| p.parse::().unwrap_or(0)) .take(3) .collect::>(); - Ok(Self { - major: parts.pop_front().unwrap(), - minor: parts.pop_front().unwrap_or(0), - patch: parts.pop_front().unwrap_or(0), - }) + if parts.front().is_some_and(|major| *major > 0) { + Ok(Self { + major: parts.pop_front().unwrap(), // existance checked in conditional + minor: parts.pop_front().unwrap_or(0), + patch: parts.pop_front().unwrap_or(0), + }) + } else { + Err(error::Error::Malformed("Missing major version from target version, version string should look like: X.Y.Z".to_string())) + } } } - impl From> for Version { - fn from(b: Mach) -> Self { + impl TryFrom> for Vec { + type Error = error::Error; + + fn try_from(b: Mach) -> Result { match b { - Mach::Binary(b) => Version::from(b), + Mach::Binary(b) => b.version().ok_or(error::Error::Malformed("Binary has no version".to_string())).map(|v|vec![v]), + Mach::Fat(f) => f.into_iter().map(|r| r.map(|s| match s { + SingleArch::MachO(b) => b.version().ok_or_else(||error::Error::Malformed("Missing or corrupted version".to_string())), + SingleArch::Archive(_) => Err(error::Error::Malformed("lib is an archive?".to_string())), + }).flatten()).collect(), + } + } + } + + impl Mach<'_> { + pub fn versions(self) -> HashMap { + let mut hash = HashMap::new(); + match self { + Mach::Binary(b) => { + if let Some(v) = b.version() { + hash.insert(b.header.cputype, v); + } + }, Mach::Fat(f) => { - match f - .find(|r| { - r.unwrap().cputype - == match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { - Ok("x86_64") => CPU_TYPE_X86_64, - Ok("aarch64") => CPU_TYPE_ARM64, - _ => panic!("unknown arch"), + for r in f.into_iter() { + if let Ok(SingleArch::MachO(b)) = r { + if let Some(v) = b.version() { + hash.insert(b.header.cputype, v); + } } - }) - .unwrap() - .ok() - .unwrap() - { - SingleArch::MachO(b) => Version::from(b), - SingleArch::Archive(_) => panic!("lib is an archive?"), } - } - } + }, + }; + hash } } } diff --git a/tests/macho.rs b/tests/macho.rs index e355a2cea..88b2ace46 100644 --- a/tests/macho.rs +++ b/tests/macho.rs @@ -528,10 +528,25 @@ fn parse_sections() { fn version() { let bytes = &DEADBEEF_MACH_64; let mach = Mach::parse(&bytes[..]).unwrap(); - let actual: version::Version = mach.into(); + let actual_hash = mach.versions(); + let actual = &actual_hash[&cputype::CPU_TYPE_X86_64]; let expected = "10.10.0".parse::(); assert!(expected.is_ok()); // Test parsing strings - assert_eq!(actual, expected.unwrap()); // Test parsing binaries + assert_eq!(actual_hash.len(), 1); // Test parsing binaries + assert_eq!(actual, &expected.unwrap()); // Test parsing binaries + assert_eq!(format!("{}", actual), "10.10.0"); // Test formatting version +} + +#[test] +fn version_from_macho() { + let bytes = &DEADBEEF_MACH_64; + if let Mach::Binary(binary) = Mach::parse(bytes.as_ref()).unwrap() { + let expected = "10.10.0".parse::().unwrap(); + let actual = binary.version(); + assert_eq!(actual, Some(expected)); + } else { + panic!("got mach fat from regular binary"); + } } #[test] From e1ae33c419eac19245a262aaa6d34162d93662ad Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Tue, 21 Apr 2026 11:31:32 -0600 Subject: [PATCH 7/7] deal with old rust compiler --- src/mach/version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mach/version.rs b/src/mach/version.rs index 9f7587a42..ec62442af 100644 --- a/src/mach/version.rs +++ b/src/mach/version.rs @@ -118,7 +118,7 @@ if_std! { Mach::Fat(f) => f.into_iter().map(|r| r.map(|s| match s { SingleArch::MachO(b) => b.version().ok_or_else(||error::Error::Malformed("Missing or corrupted version".to_string())), SingleArch::Archive(_) => Err(error::Error::Malformed("lib is an archive?".to_string())), - }).flatten()).collect(), + }).and_then(std::convert::identity)).collect(), } } }