use {
crate::{
bundle_signing::{BundleFileHandler, SignedMachOInfo},
error::AppleCodesignError,
macho::DigestType,
},
plist::{Dictionary, Value},
slog::{debug, info, Logger},
std::{cmp::Ordering, collections::BTreeMap, convert::TryFrom, io::Write},
tugger_apple_bundle::DirectoryBundleFile,
};
#[derive(Clone, PartialEq)]
enum FilesValue {
Required(Vec<u8>),
Optional(Vec<u8>),
}
impl std::fmt::Debug for FilesValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Required(digest) => f
.debug_struct("FilesValue")
.field("required", &true)
.field("digest", &hex::encode(digest))
.finish(),
Self::Optional(digest) => f
.debug_struct("FilesValue")
.field("required", &false)
.field("digest", &hex::encode(digest))
.finish(),
}
}
}
impl std::fmt::Display for FilesValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Required(digest) => {
f.write_fmt(format_args!("{} (required)", hex::encode(digest)))
}
Self::Optional(digest) => {
f.write_fmt(format_args!("{} (optional)", hex::encode(digest)))
}
}
}
}
impl TryFrom<&Value> for FilesValue {
type Error = AppleCodesignError;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
match v {
Value::Data(digest) => Ok(Self::Required(digest.to_vec())),
Value::Dictionary(dict) => {
let mut digest = None;
let mut optional = None;
for (key, value) in dict.iter() {
match key.as_str() {
"hash" => {
let data = value.as_data().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected <data> for files <dict> entry, got {:?}",
value
))
})?;
digest = Some(data.to_vec());
}
"optional" => {
let v = value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected boolean for optional key, got {:?}",
value
))
})?;
optional = Some(v);
}
key => {
return Err(AppleCodesignError::ResourcesPlistParse(format!(
"unexpected key in files dict: {}",
key
)));
}
}
}
match (digest, optional) {
(Some(digest), Some(true)) => Ok(Self::Optional(digest)),
(Some(digest), Some(false)) => Ok(Self::Required(digest)),
_ => Err(AppleCodesignError::ResourcesPlistParse(
"missing hash or optional key".to_string(),
)),
}
}
_ => Err(AppleCodesignError::ResourcesPlistParse(format!(
"bad value in files <dict>; expected <data> or <dict>, got {:?}",
v
))),
}
}
}
impl From<&FilesValue> for Value {
fn from(v: &FilesValue) -> Self {
match v {
FilesValue::Required(digest) => Self::Data(digest.to_vec()),
FilesValue::Optional(digest) => {
let mut dict = Dictionary::new();
dict.insert("hash".to_string(), Value::Data(digest.to_vec()));
dict.insert("optional".to_string(), Value::Boolean(true));
Self::Dictionary(dict)
}
}
}
}
#[derive(Clone, PartialEq)]
struct Files2Value {
cdhash: Option<Vec<u8>>,
hash2: Option<Vec<u8>>,
optional: Option<bool>,
requirement: Option<String>,
symlink: Option<String>,
}
impl std::fmt::Debug for Files2Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Files2Value")
.field(
"cdhash",
&format_args!("{:?}", self.cdhash.as_ref().map(hex::encode)),
)
.field(
"hash2",
&format_args!("{:?}", self.hash2.as_ref().map(hex::encode)),
)
.field("optional", &format_args!("{:?}", self.optional))
.field("requirement", &format_args!("{:?}", self.requirement))
.field("symlink", &format_args!("{:?}", self.symlink))
.finish()
}
}
impl TryFrom<&Value> for Files2Value {
type Error = AppleCodesignError;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
let dict = v.as_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse("files2 value should be a dict".to_string())
})?;
let mut hash2 = None;
let mut cdhash = None;
let mut optional = None;
let mut requirement = None;
let mut symlink = None;
for (key, value) in dict.iter() {
match key.as_str() {
"cdhash" => {
let data = value.as_data().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected <data> for files2 cdhash entry, got {:?}",
value
))
})?;
cdhash = Some(data.to_vec());
}
"hash2" => {
let data = value.as_data().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected <data> for files2 hash entry, got {:?}",
value
))
})?;
hash2 = Some(data.to_vec());
}
"optional" => {
let v = value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected bool for optional key, got {:?}",
value
))
})?;
optional = Some(v);
}
"requirement" => {
let v = value.as_string().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected string for requirement key, got {:?}",
value
))
})?;
requirement = Some(v.to_string());
}
"symlink" => {
symlink = Some(
value
.as_string()
.ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected string for symlink key, got {:?}",
value
))
})?
.to_string(),
);
}
key => {
return Err(AppleCodesignError::ResourcesPlistParse(format!(
"unexpected key in files2 dict entry: {}",
key
)));
}
}
}
Ok(Self {
cdhash,
hash2,
optional,
requirement,
symlink,
})
}
}
impl From<&Files2Value> for Value {
fn from(v: &Files2Value) -> Self {
let mut dict = Dictionary::new();
if let Some(cdhash) = &v.cdhash {
dict.insert("cdhash".to_string(), Value::Data(cdhash.to_vec()));
}
if let Some(hash2) = &v.hash2 {
dict.insert("hash2".to_string(), Value::Data(hash2.to_vec()));
}
if let Some(optional) = &v.optional {
dict.insert("optional".to_string(), Value::Boolean(*optional));
}
if let Some(requirement) = &v.requirement {
dict.insert(
"requirement".to_string(),
Value::String(requirement.to_string()),
);
}
if let Some(symlink) = &v.symlink {
dict.insert("symlink".to_string(), Value::String(symlink.to_string()));
}
Value::Dictionary(dict)
}
}
#[derive(Clone, Debug, PartialEq)]
struct RulesValue {
omit: bool,
required: bool,
weight: Option<f64>,
}
impl TryFrom<&Value> for RulesValue {
type Error = AppleCodesignError;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
match v {
Value::Boolean(true) => Ok(Self {
omit: false,
required: true,
weight: None,
}),
Value::Dictionary(dict) => {
let mut omit = None;
let mut optional = None;
let mut weight = None;
for (key, value) in dict {
match key.as_str() {
"omit" => {
omit = Some(value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"rules omit key value not a boolean; got {:?}",
value
))
})?);
}
"optional" => {
optional = Some(value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"rules optional key value not a boolean, got {:?}",
value
))
})?);
}
"weight" => {
weight = Some(value.as_real().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"rules weight key value not a real, got {:?}",
value
))
})?);
}
key => {
return Err(AppleCodesignError::ResourcesPlistParse(format!(
"extra key in rules dict: {}",
key
)));
}
}
}
Ok(Self {
omit: omit.unwrap_or(false),
required: !optional.unwrap_or(false),
weight,
})
}
_ => Err(AppleCodesignError::ResourcesPlistParse(
"invalid value for rules entry".to_string(),
)),
}
}
}
impl From<&RulesValue> for Value {
fn from(v: &RulesValue) -> Self {
if v.required && !v.omit && v.weight.is_none() {
Value::Boolean(true)
} else {
let mut dict = Dictionary::new();
if v.omit {
dict.insert("omit".to_string(), Value::Boolean(true));
}
if !v.required {
dict.insert("optional".to_string(), Value::Boolean(true));
}
if let Some(weight) = v.weight {
dict.insert("weight".to_string(), Value::Real(weight));
}
Value::Dictionary(dict)
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct Rules2Value {
nested: Option<bool>,
omit: Option<bool>,
optional: Option<bool>,
weight: Option<f64>,
}
impl TryFrom<&Value> for Rules2Value {
type Error = AppleCodesignError;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
let dict = v.as_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse("rules2 value should be a dict".to_string())
})?;
let mut nested = None;
let mut omit = None;
let mut optional = None;
let mut weight = None;
for (key, value) in dict.iter() {
match key.as_str() {
"nested" => {
nested = Some(value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected bool for rules2 nested key, got {:?}",
value
))
})?);
}
"omit" => {
omit = Some(value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected bool for rules2 omit key, got {:?}",
value
))
})?);
}
"optional" => {
optional = Some(value.as_boolean().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected bool for rules2 optional key, got {:?}",
value
))
})?);
}
"weight" => {
weight = Some(value.as_real().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expected real for rules2 weight key, got {:?}",
value
))
})?);
}
key => {
return Err(AppleCodesignError::ResourcesPlistParse(format!(
"unexpected key in rules dict entry: {}",
key
)));
}
}
}
Ok(Self {
nested,
omit,
optional,
weight,
})
}
}
impl From<&Rules2Value> for Value {
fn from(v: &Rules2Value) -> Self {
let mut dict = Dictionary::new();
if let Some(true) = v.nested {
dict.insert("nested".to_string(), Value::Boolean(true));
}
if let Some(true) = v.omit {
dict.insert("omit".to_string(), Value::Boolean(true));
}
if let Some(true) = v.optional {
dict.insert("optional".to_string(), Value::Boolean(true));
}
if let Some(weight) = v.weight {
dict.insert("weight".to_string(), Value::Real(weight));
}
if dict.is_empty() {
Value::Boolean(true)
} else {
Value::Dictionary(dict)
}
}
}
#[derive(Clone, Debug)]
pub struct CodeResourcesRule {
pub pattern: String,
pub exclude: bool,
pub nested: bool,
pub omit: bool,
pub optional: bool,
pub weight: Option<u32>,
re: regex::Regex,
}
impl PartialEq for CodeResourcesRule {
fn eq(&self, other: &Self) -> bool {
self.pattern == other.pattern
&& self.exclude == other.exclude
&& self.nested == other.nested
&& self.omit == other.omit
&& self.optional == other.optional
&& self.weight == other.weight
}
}
impl Eq for CodeResourcesRule {}
impl PartialOrd for CodeResourcesRule {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let our_weight = self.weight.unwrap_or(1);
let their_weight = other.weight.unwrap_or(1);
match self.exclude.cmp(&other.exclude) {
Ordering::Equal => their_weight.partial_cmp(&our_weight),
Ordering::Greater => Some(Ordering::Less),
Ordering::Less => Some(Ordering::Greater),
}
}
}
impl Ord for CodeResourcesRule {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap()
}
}
impl CodeResourcesRule {
pub fn new(pattern: impl ToString) -> Result<Self, AppleCodesignError> {
Ok(Self {
pattern: pattern.to_string(),
exclude: false,
nested: false,
omit: false,
optional: false,
weight: None,
re: regex::Regex::new(&pattern.to_string())
.map_err(|e| AppleCodesignError::ResourcesBadRegex(pattern.to_string(), e))?,
})
}
pub fn exclude(mut self) -> Self {
self.exclude = true;
self
}
pub fn nested(mut self) -> Self {
self.nested = true;
self
}
pub fn omit(mut self) -> Self {
self.omit = true;
self
}
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
pub fn weight(mut self, v: u32) -> Self {
self.weight = Some(v);
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CodeResources {
files: BTreeMap<String, FilesValue>,
files2: BTreeMap<String, Files2Value>,
rules: BTreeMap<String, RulesValue>,
rules2: BTreeMap<String, Rules2Value>,
}
impl CodeResources {
pub fn from_xml(xml: &[u8]) -> Result<Self, AppleCodesignError> {
let plist = Value::from_reader_xml(xml).map_err(AppleCodesignError::ResourcesPlist)?;
let dict = plist.into_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(
"plist root element should be a <dict>".to_string(),
)
})?;
let mut files = BTreeMap::new();
let mut files2 = BTreeMap::new();
let mut rules = BTreeMap::new();
let mut rules2 = BTreeMap::new();
for (key, value) in dict.iter() {
match key.as_ref() {
"files" => {
let dict = value.as_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expecting files to be a dict, got {:?}",
value
))
})?;
for (key, value) in dict {
files.insert(key.to_string(), FilesValue::try_from(value)?);
}
}
"files2" => {
let dict = value.as_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expecting files2 to be a dict, got {:?}",
value
))
})?;
for (key, value) in dict {
files2.insert(key.to_string(), Files2Value::try_from(value)?);
}
}
"rules" => {
let dict = value.as_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expecting rules to be a dict, got {:?}",
value
))
})?;
for (key, value) in dict {
rules.insert(key.to_string(), RulesValue::try_from(value)?);
}
}
"rules2" => {
let dict = value.as_dictionary().ok_or_else(|| {
AppleCodesignError::ResourcesPlistParse(format!(
"expecting rules2 to be a dict, got {:?}",
value
))
})?;
for (key, value) in dict {
rules2.insert(key.to_string(), Rules2Value::try_from(value)?);
}
}
key => {
return Err(AppleCodesignError::ResourcesPlistParse(format!(
"unexpected key in root dict: {}",
key
)));
}
}
}
Ok(Self {
files,
files2,
rules,
rules2,
})
}
pub fn to_writer_xml(&self, mut writer: impl Write) -> Result<(), AppleCodesignError> {
let value = Value::from(self);
let mut data = Vec::<u8>::new();
value
.to_writer_xml(&mut data)
.map_err(AppleCodesignError::ResourcesPlist)?;
let data = String::from_utf8(data).expect("XML should be valid UTF-8");
let data = data.replace("<true />", "<true/>");
writer.write_all(data.as_bytes())?;
writer.write_all(b"\n")?;
Ok(())
}
pub fn add_rule(&mut self, rule: CodeResourcesRule) {
self.rules.insert(
rule.pattern,
RulesValue {
omit: rule.omit,
required: !rule.optional,
weight: rule.weight.map(|x| x as f64),
},
);
}
pub fn add_rule2(&mut self, rule: CodeResourcesRule) {
self.rules2.insert(
rule.pattern,
Rules2Value {
nested: if rule.nested { Some(true) } else { None },
omit: if rule.omit { Some(true) } else { None },
optional: if rule.optional { Some(true) } else { None },
weight: rule.weight.map(|x| x as f64),
},
);
}
pub fn seal_regular_file(
&mut self,
path: impl ToString,
content: impl AsRef<[u8]>,
optional: bool,
) -> Result<(), AppleCodesignError> {
let sha1 = DigestType::Sha1.digest(content.as_ref())?;
let sha256 = DigestType::Sha256.digest(content.as_ref())?;
let path = path.to_string();
self.files.insert(
path.clone(),
if optional {
FilesValue::Optional(sha1)
} else {
FilesValue::Required(sha1)
},
);
self.files2.insert(
path,
Files2Value {
cdhash: None,
hash2: Some(sha256),
optional: if optional { Some(true) } else { None },
requirement: None,
symlink: None,
},
);
Ok(())
}
pub fn seal_symlink(&mut self, path: impl ToString, target: impl ToString) {
self.files2.insert(
path.to_string(),
Files2Value {
cdhash: None,
hash2: None,
optional: None,
requirement: None,
symlink: Some(target.to_string()),
},
);
}
pub fn seal_macho(
&mut self,
path: impl ToString,
info: &SignedMachOInfo,
optional: bool,
) -> Result<(), AppleCodesignError> {
self.files2.insert(
path.to_string(),
Files2Value {
cdhash: Some(DigestType::Sha256Truncated.digest(&info.code_directory_blob)?),
hash2: None,
optional: if optional { Some(true) } else { None },
requirement: info.designated_code_requirement.clone(),
symlink: None,
},
);
Ok(())
}
}
impl From<&CodeResources> for Value {
fn from(cr: &CodeResources) -> Self {
let mut dict = Dictionary::new();
if !cr.files.is_empty() {
dict.insert(
"files".to_string(),
Value::Dictionary(
cr.files
.iter()
.map(|(key, value)| (key.to_string(), Value::from(value)))
.collect::<Dictionary>(),
),
);
}
if !cr.files2.is_empty() {
dict.insert(
"files2".to_string(),
Value::Dictionary(
cr.files2
.iter()
.map(|(key, value)| (key.to_string(), Value::from(value)))
.collect::<Dictionary>(),
),
);
}
if !cr.rules.is_empty() {
dict.insert(
"rules".to_string(),
Value::Dictionary(
cr.rules
.iter()
.map(|(key, value)| (key.to_string(), Value::from(value)))
.collect::<Dictionary>(),
),
);
}
if !cr.rules2.is_empty() {
dict.insert(
"rules2".to_string(),
Value::Dictionary(
cr.rules2
.iter()
.map(|(key, value)| (key.to_string(), Value::from(value)))
.collect::<Dictionary>(),
),
);
}
Value::Dictionary(dict)
}
}
#[derive(Clone, Debug, Default)]
pub struct CodeResourcesBuilder {
rules: Vec<CodeResourcesRule>,
resources: CodeResources,
}
impl CodeResourcesBuilder {
pub fn default_resources_rules() -> Result<Self, AppleCodesignError> {
let mut slf = Self::default();
slf.add_rule(CodeResourcesRule::new("^version.plist$")?);
slf.add_rule(CodeResourcesRule::new("^Resources/")?);
slf.add_rule(
CodeResourcesRule::new("^Resources/.*\\.lproj/")?
.optional()
.weight(1000),
);
slf.add_rule(CodeResourcesRule::new("^Resources/Base\\.lproj/")?.weight(1010));
slf.add_rule(
CodeResourcesRule::new("^Resources/.*\\.lproj/locversion.plist$")?
.omit()
.weight(1100),
);
slf.add_rule2(CodeResourcesRule::new("^.*")?);
slf.add_rule2(CodeResourcesRule::new("^[^/]+$")?.nested().weight(10));
slf.add_rule2(CodeResourcesRule::new("^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/")?
.nested().weight(10));
slf.add_rule2(CodeResourcesRule::new(".*\\.dSYM($|/)")?.weight(11));
slf.add_rule2(
CodeResourcesRule::new("^(.*/)?\\.DS_Store$")?
.omit()
.weight(2000),
);
slf.add_rule2(CodeResourcesRule::new("^Info\\.plist$")?.omit().weight(20));
slf.add_rule2(CodeResourcesRule::new("^version\\.plist$")?.weight(20));
slf.add_rule2(CodeResourcesRule::new("^embedded\\.provisionprofile$")?.weight(20));
slf.add_rule2(CodeResourcesRule::new("^PkgInfo$")?.omit().weight(20));
slf.add_rule2(CodeResourcesRule::new("^Resources/")?.weight(20));
slf.add_rule2(
CodeResourcesRule::new("^Resources/.*\\.lproj/")?
.optional()
.weight(1000),
);
slf.add_rule2(CodeResourcesRule::new("^Resources/Base\\.lproj/")?.weight(1010));
slf.add_rule2(
CodeResourcesRule::new("^Resources/.*\\.lproj/locversion.plist$")?
.omit()
.weight(1100),
);
Ok(slf)
}
pub fn default_no_resources_rules() -> Result<Self, AppleCodesignError> {
let mut slf = Self::default();
slf.add_rule(CodeResourcesRule::new("^version.plist$")?);
slf.add_rule(CodeResourcesRule::new("^.*")?);
slf.add_rule(
CodeResourcesRule::new("^.*\\.lproj")?
.optional()
.weight(1000),
);
slf.add_rule(CodeResourcesRule::new("^Base\\.lproj")?.weight(1010));
slf.add_rule(
CodeResourcesRule::new("^.*\\.lproj/locversion.plist$")?
.omit()
.weight(1100),
);
slf.add_rule2(CodeResourcesRule::new("^.*")?);
slf.add_rule2(CodeResourcesRule::new(".*\\.dSYM($|/)")?.weight(11));
slf.add_rule2(
CodeResourcesRule::new("^(.*/)?\\.DS_Store$")?
.omit()
.weight(2000),
);
slf.add_rule2(CodeResourcesRule::new("^Info\\.plist$")?.omit().weight(20));
slf.add_rule2(CodeResourcesRule::new("^version\\.plist$")?.weight(20));
slf.add_rule2(CodeResourcesRule::new("^embedded\\.provisionprofile$")?.weight(20));
slf.add_rule2(CodeResourcesRule::new("^PkgInfo$")?.omit().weight(20));
slf.add_rule2(
CodeResourcesRule::new("^.*\\.lproj/")?
.optional()
.weight(1000),
);
slf.add_rule2(CodeResourcesRule::new("^Base\\.lproj")?.weight(1010));
slf.add_rule2(
CodeResourcesRule::new("^.*\\.lproj/locversion.plist$")?
.omit()
.weight(1100),
);
Ok(slf)
}
pub fn add_rule(&mut self, rule: CodeResourcesRule) {
self.resources.add_rule(rule);
}
pub fn add_rule2(&mut self, rule: CodeResourcesRule) {
self.rules.push(rule.clone());
self.rules.sort();
self.resources.add_rule2(rule);
}
pub fn add_exclusion_rule(&mut self, rule: CodeResourcesRule) {
self.rules.push(rule);
self.rules.sort();
}
fn find_rule(&self, path: &str) -> Option<CodeResourcesRule> {
let parts = path.split('/').collect::<Vec<_>>();
let mut exclude_override = false;
let rule = self.rules.iter().find(|rule| {
if rule.nested {
for last_part in 1..parts.len() - 1 {
let parent = parts[0..last_part].join("/");
if rule.re.is_match(&parent) && parts[last_part - 1].contains('.') {
exclude_override = true;
return true;
}
}
}
if rule.exclude {
for last_part in 1..parts.len() - 1 {
let parent = parts[0..last_part].join("/");
if rule.re.is_match(&parent) {
return true;
}
}
}
rule.re.is_match(&path)
});
if let Some(rule) = rule {
let mut rule = rule.clone();
if exclude_override {
rule.exclude = true;
}
Some(rule)
} else {
None
}
}
pub fn process_file(
&mut self,
log: &Logger,
file: &DirectoryBundleFile,
file_handler: &dyn BundleFileHandler,
) -> Result<(), AppleCodesignError> {
let relative_path = file.relative_path().to_string_lossy().replace("\\", "/");
let relative_path = relative_path
.strip_prefix("Contents/")
.unwrap_or(&relative_path)
.to_string();
info!(log, "processing {}", relative_path);
let rule = match self.find_rule(relative_path.as_ref()) {
Some(rule) => {
debug!(
log,
"{} matches {} rule {}",
relative_path,
if rule.exclude || rule.omit {
"exclusion"
} else {
"inclusion"
},
rule.pattern
);
if rule.exclude {
return Ok(());
} else if rule.omit {
return file_handler.install_file(log, file);
}
rule
}
None => {
debug!(log, "{} doesn't match any rule; processing", relative_path);
return Ok(());
}
};
if let Some(target) = file
.symlink_target()
.map_err(AppleCodesignError::DirectoryBundle)?
{
let target = target.to_string_lossy().replace("\\", "/");
info!(log, "sealing symlink {} -> {}", relative_path, target);
self.resources.seal_symlink(relative_path, target);
file_handler.install_file(log, file)?;
} else {
let data = std::fs::read(file.absolute_path())?;
if rule.nested {
let macho_info = file_handler.sign_and_install_macho(log, file)?;
info!(log, "sealing Mach-O file {}", relative_path);
self.resources
.seal_macho(relative_path, &macho_info, rule.optional)?;
} else {
info!(log, "sealing regular file {}", relative_path);
self.resources
.seal_regular_file(relative_path, data, rule.optional)?;
file_handler.install_file(log, file)?;
}
}
Ok(())
}
pub fn add_signed_macho_file(
&mut self,
path: &str,
info: &SignedMachOInfo,
) -> Result<(), AppleCodesignError> {
self.resources.seal_macho(path, info, false)
}
pub fn write_code_resources(&self, writer: impl Write) -> Result<(), AppleCodesignError> {
self.resources.to_writer_xml(writer)
}
}
#[cfg(test)]
mod tests {
use super::*;
const FIREFOX_SNIPPET: &str = r#"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/XUL.sig</key>
<data>Y0SEPxyC6hCQ+rl4LTRmXy7F9DQ=</data>
<key>Resources/en.lproj/InfoPlist.strings</key>
<dict>
<key>hash</key>
<data>U8LTYe+cVqPcBu9aLvcyyfp+dAg=</data>
<key>optional</key>
<true/>
</dict>
<key>Resources/firefox-bin.sig</key>
<data>ZvZ3yDciAF4kB9F06Xr3gKi3DD4=</data>
</dict>
<key>files2</key>
<dict>
<key>Library/LaunchServices/org.mozilla.updater</key>
<dict>
<key>hash2</key>
<data>iMnDHpWkKTI6xLi9Av93eNuIhxXhv3C18D4fljCfw2Y=</data>
</dict>
<key>TestOptional</key>
<dict>
<key>hash2</key>
<data>iMnDHpWkKTI6xLi9Av93eNuIhxXhv3C18D4fljCfw2Y=</data>
<key>optional</key>
<true/>
</dict>
<key>MacOS/XUL</key>
<dict>
<key>cdhash</key>
<data>NevNMzQBub9OjomMUAk2xBumyHM=</data>
<key>requirement</key>
<string>anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "43AQ936H96"</string>
</dict>
<key>MacOS/SafariForWebKitDevelopment</key>
<dict>
<key>symlink</key>
<string>/Library/Application Support/Apple/Safari/SafariForWebKitDevelopment</string>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>optional</key>
<dict>
<key>optional</key>
<true/>
</dict>
</dict>
</dict>
</plist>"#;
#[test]
fn parse_firefox() {
let resources = CodeResources::from_xml(FIREFOX_SNIPPET.as_bytes()).unwrap();
let mut buffer = Vec::<u8>::new();
resources.to_writer_xml(&mut buffer).unwrap();
let resources2 = CodeResources::from_xml(&buffer).unwrap();
assert_eq!(resources, resources2);
}
}