Skip to main content

macho_dyld_cache/
lib.rs

1#![deny(missing_docs)]
2//! Offline dyld shared-cache family parsing and Mach-O reconstruction.
3
4use std::collections::{BTreeMap, BTreeSet};
5use std::ops::Range;
6
7use macho_core::MaterializationLimits;
8use serde::Serialize;
9
10pub use macho_core::format;
11
12/// The error module.
13pub mod error;
14pub(crate) use error::Error;
15pub use error::{DyldCacheError, DyldCacheErrorKind, Result};
16
17mod completeness;
18mod family;
19mod materialize;
20mod parse;
21mod rewrite;
22
23pub use family::*;
24pub use parse::*;
25
26fn read_u32_le(data: &[u8], offset: usize) -> Result<u32> {
27    let end = offset
28        .checked_add(4)
29        .ok_or_else(|| Error::bounds(offset as u64, 4, data.len() as u64))?;
30    if end > data.len() {
31        return Err(Error::bounds(offset as u64, 4, data.len() as u64));
32    }
33    let bytes: [u8; 4] = data[offset..end]
34        .try_into()
35        .expect("slice length guaranteed by bounds check");
36    Ok(u32::from_le_bytes(bytes))
37}
38
39fn read_u64_le(data: &[u8], offset: usize) -> Result<u64> {
40    let end = offset
41        .checked_add(8)
42        .ok_or_else(|| Error::bounds(offset as u64, 8, data.len() as u64))?;
43    if end > data.len() {
44        return Err(Error::bounds(offset as u64, 8, data.len() as u64));
45    }
46    let bytes: [u8; 8] = data[offset..end]
47        .try_into()
48        .expect("slice length guaranteed by bounds check");
49    Ok(u64::from_le_bytes(bytes))
50}
51
52fn read_uuid(data: &[u8], offset: usize) -> Result<[u8; 16]> {
53    data.get(offset..offset.saturating_add(16))
54        .and_then(|bytes| bytes.try_into().ok())
55        .ok_or_else(|| Error::bounds(offset as u64, 16, data.len() as u64))
56}
57
58fn read_fixed_c_string(data: &[u8], offset: usize, size: usize, subject: &str) -> Result<String> {
59    let bytes = data
60        .get(offset..offset.saturating_add(size))
61        .ok_or_else(|| Error::bounds(offset as u64, size as u64, data.len() as u64))?;
62    let end = bytes
63        .iter()
64        .position(|byte| *byte == 0)
65        .unwrap_or(bytes.len());
66    let value = std::str::from_utf8(&bytes[..end])
67        .map_err(|_| Error::format(format!("{subject} is not UTF-8")))?;
68    Ok(value.to_owned())
69}
70
71fn format_uuid(uuid: [u8; 16]) -> String {
72    format!(
73        "{:02X}{:02X}{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
74        uuid[0],
75        uuid[1],
76        uuid[2],
77        uuid[3],
78        uuid[4],
79        uuid[5],
80        uuid[6],
81        uuid[7],
82        uuid[8],
83        uuid[9],
84        uuid[10],
85        uuid[11],
86        uuid[12],
87        uuid[13],
88        uuid[14],
89        uuid[15]
90    )
91}
92
93fn read_c_string(data: &[u8], offset: usize, subject: &str) -> Result<String> {
94    const MAX_CACHE_PATH_BYTES: usize = 4096;
95    let remaining = data
96        .get(offset..)
97        .ok_or_else(|| Error::bounds(offset as u64, 1, data.len() as u64))?;
98    let bounded = &remaining[..remaining.len().min(MAX_CACHE_PATH_BYTES)];
99    let length = bounded.iter().position(|byte| *byte == 0).ok_or_else(|| {
100        Error::format(format!(
101            "{subject} at {offset:#x} is not NUL-terminated within {} bytes",
102            bounded.len()
103        ))
104    })?;
105    let path = std::str::from_utf8(&bounded[..length])
106        .map_err(|_| Error::format(format!("{subject} at {offset:#x} is not UTF-8")))?;
107    if path.is_empty() || !path.starts_with('/') {
108        return Err(Error::format(format!(
109            "{subject} at {offset:#x} is not a nonempty absolute install path"
110        )));
111    }
112    Ok(path.to_owned())
113}
114
115#[cfg(test)]
116mod tests;