Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
apple-codesign-quick
src
bundle
code_resources.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
code_resources.rs
Blame
History
Raw
use crate::error::{CodeSignError, Result}; use crate::file_bytes::read_file_bytes; use plist::{Dictionary, Value}; use sha1::Digest as _; use std::path::Path; /// The files and files2 dictionaries of a bundle's _CodeSignature/CodeResources. #[derive(Clone, Debug, Default)] pub struct CodeResourcesMaps { pub files: Dictionary, pub files2: Dictionary, hashes: Vec<FileHash>, } impl CodeResourcesMaps { pub(crate) fn insert_hash(&mut self, hash: FileHash) { let FileHash { relative_path, sha1, sha256, } = &hash; let optional = is_optional(relative_path); if !omitted_from_files(relative_path) { let entry = if optional { Value::Dictionary(Dictionary::from_iter([ ("hash", Value::Data(sha1.to_vec())), ("optional", Value::Boolean(true)), ])) } else { Value::Data(sha1.to_vec()) }; self.files.insert(relative_path.clone(), entry); } if !omitted_from_files2(relative_path) { let mut entry = Dictionary::from_iter([ ("hash", Value::Data(sha1.to_vec())), ("hash2", Value::Data(sha256.to_vec())), ]); if optional { entry.insert("optional".to_string(), Value::Boolean(true)); } self.files2 .insert(relative_path.clone(), Value::Dictionary(entry)); } self.hashes.push(hash); } /// Seal a signed nested bundle: its own resources, then its executable and CodeResources. /// iOS wants every nested file listed individually, unlike macOS' "nested" rule, and the /// rules apply to the paths as seen from here. pub(crate) fn insert_nested( &mut self, root: &Path, nested_path: &Path, executable: &str, nested: CodeResourcesMaps, ) -> Result<()> { let nested_root = relative_path(root, nested_path)?; for mut hash in nested.hashes { hash.relative_path = format!("{nested_root}/{}", hash.relative_path); self.insert_hash(hash); } self.insert_hash(FileHash::of_file(root, &nested_path.join(executable))?); self.insert_hash(FileHash::of_file( root, &nested_path .join(super::CODE_SIGNATURE_DIR) .join(super::CODE_RESOURCES_FILE), )?); Ok(()) } pub(crate) fn encode(&self) -> Result<Vec<u8>> { let code_resources = Dictionary::from_iter([ ("files", Value::Dictionary(self.files.clone())), ("files2", Value::Dictionary(self.files2.clone())), ("rules", rules_value(&RULES)), ("rules2", rules_value(&RULES2)), ]); let mut data = Vec::new(); plist::to_writer_xml(&mut data, &code_resources)?; Ok(data) } } #[derive(Clone, Debug)] pub(crate) struct FileHash { pub relative_path: String, pub sha1: [u8; 20], pub sha256: [u8; 32], } impl FileHash { pub(crate) fn of_file(root: &Path, path: &Path) -> Result<Self> { let relative_path = relative_path(root, path)?; let data = read_file_bytes(path)?; let data = data.as_slice(); // Hashing big files twice in a row is the slow part of sealing, so I split it across two threads. let (sha1, sha256) = if data.len() >= 1024 * 1024 { rayon::join( || sha1::Sha1::digest(data).into(), || sha2::Sha256::digest(data).into(), ) } else { ( sha1::Sha1::digest(data).into(), sha2::Sha256::digest(data).into(), ) }; Ok(FileHash { relative_path, sha1, sha256, }) } } // `^.*\.lproj/` is optional, but `^Base\.lproj/` weighs more and isn't: only our own Base.lproj // is required, a nested bundle's one is optional from here. fn is_optional(relative_path: &str) -> bool { relative_path.contains(".lproj/") && !relative_path.starts_with("Base.lproj/") } fn omitted_from_files(relative_path: &str) -> bool { relative_path.ends_with(".lproj/locversion.plist") } // Info.plist and PkgInfo are only omitted for the bundle that owns them: a parent still seals them. fn omitted_from_files2(relative_path: &str) -> bool { omitted_from_files(relative_path) || matches!(relative_path, "Info.plist" | "PkgInfo" | ".DS_Store") || relative_path.ends_with("/.DS_Store") } #[derive(Clone, Copy, Debug)] enum Rule { Include, Weight(f64), Optional(f64), Omit(f64), } const RULES: [(&str, Rule); 5] = [ ("^.*", Rule::Include), ("^.*\\.lproj/", Rule::Optional(1000.0)), ("^.*\\.lproj/locversion.plist$", Rule::Omit(1100.0)), ("^Base\\.lproj/", Rule::Weight(1010.0)), ("^version.plist$", Rule::Include), ]; const RULES2: [(&str, Rule); 10] = [ (".*\\.dSYM($|/)", Rule::Weight(11.0)), ("^(.*/)?\\.DS_Store$", Rule::Omit(2000.0)), ("^.*", Rule::Include), ("^.*\\.lproj/", Rule::Optional(1000.0)), ("^.*\\.lproj/locversion.plist$", Rule::Omit(1100.0)), ("^Base\\.lproj/", Rule::Weight(1010.0)), ("^Info\\.plist$", Rule::Omit(20.0)), ("^PkgInfo$", Rule::Omit(20.0)), ("^embedded\\.provisionprofile$", Rule::Weight(20.0)), ("^version\\.plist$", Rule::Weight(20.0)), ]; fn rules_value(rules: &[(&str, Rule)]) -> Value { let rules = rules .iter() .map(|(pattern, rule)| { let rule = match *rule { Rule::Include => Value::Boolean(true), Rule::Weight(weight) => weighted([], weight), Rule::Optional(weight) => weighted([("optional", Value::Boolean(true))], weight), Rule::Omit(weight) => weighted([("omit", Value::Boolean(true))], weight), }; (*pattern, rule) }) .collect(); Value::Dictionary(rules) } fn weighted<const N: usize>(entries: [(&str, Value); N], weight: f64) -> Value { let mut rule = Dictionary::from_iter(entries); rule.insert("weight".to_string(), Value::Real(weight)); Value::Dictionary(rule) } /// A path relative to `root`, with forward slashes as CodeResources wants them. pub(crate) fn relative_path(root: &Path, path: &Path) -> Result<String> { let invalid_path = || CodeSignError::InvalidBundlePath(path.to_path_buf()); let components: Result<Vec<&str>> = path .strip_prefix(root) .map_err(|_| invalid_path())? .components() .map(|component| component.as_os_str().to_str().ok_or_else(invalid_path)) .collect(); Ok(components?.join("/")) } #[cfg(test)] mod tests { use super::*; fn test_hash(relative_path: &str) -> FileHash { FileHash { relative_path: relative_path.to_string(), sha1: [1; 20], sha256: [2; 32], } } #[test] fn files2_omits_only_the_current_bundles_info_and_pkginfo() { let mut child = CodeResourcesMaps::default(); child.insert_hash(test_hash("Info.plist")); child.insert_hash(test_hash("PkgInfo")); assert!(child.files.contains_key("Info.plist")); assert!(!child.files2.contains_key("Info.plist")); assert!(!child.files2.contains_key("PkgInfo")); let test_root = crate::bundle::tests::test_dir("files2-omission"); let child_path = test_root.join("PlugIns/Widget.appex"); std::fs::create_dir_all(child_path.join(super::super::CODE_SIGNATURE_DIR)).unwrap(); std::fs::write(child_path.join("Widget"), b"executable").unwrap(); std::fs::write( child_path .join(super::super::CODE_SIGNATURE_DIR) .join(super::super::CODE_RESOURCES_FILE), b"resources", ) .unwrap(); let mut parent = CodeResourcesMaps::default(); parent .insert_nested(&test_root, &child_path, "Widget", child) .unwrap(); assert!( parent .files2 .contains_key("PlugIns/Widget.appex/Info.plist") ); assert!(parent.files2.contains_key("PlugIns/Widget.appex/PkgInfo")); assert!(parent.files2.contains_key("PlugIns/Widget.appex/Widget")); assert!( parent .files2 .contains_key("PlugIns/Widget.appex/_CodeSignature/CodeResources") ); std::fs::remove_dir_all(test_root).unwrap(); } #[test] fn lproj_files_are_optional_except_our_own_base_lproj() { let mut maps = CodeResourcesMaps::default(); for path in [ "en.lproj/Localizable.strings", "Base.lproj/Main.storyboardc", "PlugIns/Widget.appex/Base.lproj/Main.storyboardc", ] { maps.insert_hash(test_hash(path)); } let is_optional_in = |map: &Dictionary, path| { map[path] .as_dictionary() .is_some_and(|entry| entry.contains_key("optional")) }; for (path, optional) in [ ("en.lproj/Localizable.strings", true), ("Base.lproj/Main.storyboardc", false), ("PlugIns/Widget.appex/Base.lproj/Main.storyboardc", true), ] { assert_eq!( is_optional_in(&maps.files, path), optional, "{path} in files" ); assert_eq!( is_optional_in(&maps.files2, path), optional, "{path} in files2" ); } assert!( maps.files["Base.lproj/Main.storyboardc"] .as_data() .is_some() ); } }