Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
macos-catalog
src
device_tree.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
device_tree.rs
Blame
History
Raw
//! The name a Mac gives itself. Each board's device tree in a restore image has a //! `product-description`, the English name About This Mac shows, for example //! `MacBook Pro (14-inch, Nov 2024)`. use crate::Error; /// The `product-description` of an IM4P-wrapped device tree. pub fn product_description(im4p: &[u8]) -> Result<String, Error> { let tree = payload(im4p)?; let tree = match tree.starts_with(b"bvx") { true => { let mut decoded = Vec::new(); lzfse_rust::decode_bytes(tree, &mut decoded).map_err(|_| Error::InvalidCatalog)?; decoded } false => tree.to_vec(), }; let value = property(&tree, b"product-description")?.ok_or(Error::InvalidCatalog)?; let value = value.split(|&byte| byte == 0).next().unwrap_or_default(); String::from_utf8(value.to_vec()).map_err(|_| Error::InvalidCatalog) } /// The payload of an IM4P: `SEQUENCE { "IM4P", type, description, OCTET STRING, … }`. fn payload(im4p: &[u8]) -> Result<&[u8], Error> { let (0x30, mut fields, _) = der(im4p)? else { return Err(Error::InvalidCatalog); }; for expected in [0x16, 0x16, 0x16, 0x04] { let (tag, content, rest) = der(fields)?; if tag != expected { return Err(Error::InvalidCatalog); } if tag == 0x04 { return Ok(content); } fields = rest; } unreachable!() } /// One DER element: its tag, its content and what follows it. fn der(bytes: &[u8]) -> Result<(u8, &[u8], &[u8]), Error> { let (&tag, bytes) = bytes.split_first().ok_or(Error::InvalidCatalog)?; let (&first, mut bytes) = bytes.split_first().ok_or(Error::InvalidCatalog)?; let length = match first { 0..=0x7F => first as usize, 0x81..=0x84 => { let (length, rest) = bytes .split_at_checked((first & 0x7F) as usize) .ok_or(Error::InvalidCatalog)?; bytes = rest; length .iter() .fold(0, |length, &byte| length << 8 | byte as usize) } _ => return Err(Error::InvalidCatalog), }; let (content, rest) = bytes .split_at_checked(length) .ok_or(Error::InvalidCatalog)?; Ok((tag, content, rest)) } /// The first property named `name` in a flattened Apple device tree. Each node is /// its property and child counts, its properties (a 32-byte name, a length whose /// top bit marks a placeholder, and the value padded to four bytes), then its /// children. fn property<'a>(tree: &'a [u8], name: &[u8]) -> Result<Option<&'a [u8]>, Error> { fn node<'a>(tree: &'a [u8], at: &mut usize, name: &[u8]) -> Result<Option<&'a [u8]>, Error> { let properties = u32_at(tree, *at)?; let children = u32_at(tree, *at + 4)?; *at += 8; let mut found = None; for _ in 0..properties { let key = tree.get(*at..*at + 32).ok_or(Error::InvalidCatalog)?; let length = (u32_at(tree, *at + 32)? & 0x7FFF_FFFF) as usize; let value = tree .get(*at + 36..*at + 36 + length) .ok_or(Error::InvalidCatalog)?; if found.is_none() && key.split(|&byte| byte == 0).next() == Some(name) { found = Some(value); } *at += 36 + length.next_multiple_of(4); } for _ in 0..children { let child = node(tree, at, name)?; found = found.or(child); } Ok(found) } node(tree, &mut 0, name) } fn u32_at(bytes: &[u8], at: usize) -> Result<u32, Error> { bytes .get(at..at + 4) .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) .ok_or(Error::InvalidCatalog) } #[cfg(test)] mod tests { use super::*; fn property(name: &str, value: &[u8]) -> Vec<u8> { let mut bytes = name.as_bytes().to_vec(); bytes.resize(32, 0); bytes.extend((value.len() as u32).to_le_bytes()); bytes.extend(value); bytes.resize(bytes.len().next_multiple_of(4), 0); bytes } fn node(properties: &[Vec<u8>], children: &[Vec<u8>]) -> Vec<u8> { let mut bytes = (properties.len() as u32).to_le_bytes().to_vec(); bytes.extend((children.len() as u32).to_le_bytes()); bytes.extend(properties.concat()); bytes.extend(children.concat()); bytes } fn der(tag: u8, content: &[u8]) -> Vec<u8> { let mut bytes = vec![tag]; match content.len() { length @ 0..0x80 => bytes.push(length as u8), length => { bytes.push(0x82); bytes.extend((length as u16).to_be_bytes()); } } bytes.extend(content); bytes } #[test] fn reads_the_product_description_from_a_nested_node() { let product = node( &[property("product-description", b"Mac mini (M5 Pro)\0")], &[], ); let tree = node( &[ property("model", b"Mac17,16\0"), property("name", &[1; 200]), ], &[node(&[property("name", b"chosen\0")], &[]), product], ); let im4p = der( 0x30, &[ der(0x16, b"IM4P"), der(0x16, b"dtre"), der(0x16, b"EmbeddedDeviceTrees"), der(0x04, &tree), ] .concat(), ); assert_eq!(product_description(&im4p).unwrap(), "Mac mini (M5 Pro)"); assert!(product_description(&im4p[..im4p.len() - 8]).is_err()); } }