use crate::adapter::package::opc;
use crate::adapter::package::zip::{ZipMember, scan};
use crate::error::{Error, Result};
use crate::limits::Limits;
const FLAG_ENCRYPTED: u16 = 0x0001;
pub fn build_opc_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
let physical = scan(source, limits)?;
physical.validate(source.len() as u64)?;
let mut members: Vec<(u32, String)> = Vec::with_capacity(physical.members.len());
for member in &physical.members {
let name = core::str::from_utf8(&member.name).map_err(|_| {
Error::invalid_package_structure("package member name is not valid UTF-8")
})?;
let part = opc::part_name_from_member(name)?;
members.push((member.id.ordinal, part));
}
let mut content_types: Option<Vec<u8>> = None;
let mut rels: Vec<(Option<u32>, Vec<u8>)> = Vec::new();
let mut issues: Vec<String> = Vec::new();
for member in &physical.members {
let Ok(raw_name) = core::str::from_utf8(&member.name) else {
continue;
};
let is_content_types = raw_name.eq_ignore_ascii_case("[Content_Types].xml");
let is_package_rels = raw_name.eq_ignore_ascii_case("_rels/.rels");
let owner_member = if is_content_types || is_package_rels {
None
} else {
part_rels_owner_member(raw_name)
};
if !is_content_types && !is_package_rels && owner_member.is_none() {
continue;
}
let Some(bytes) = decode_metadata_member(source, member, limits) else {
issues.push(format!(
"opc: metadata member {raw_name:?} could not be decoded"
));
continue;
};
if is_content_types {
content_types = Some(bytes);
} else if is_package_rels {
rels.push((None, bytes));
} else if let Some(owner) = owner_member {
let owner_part = format!("/{owner}");
match members
.iter()
.find(|(_, n)| n.eq_ignore_ascii_case(&owner_part))
{
Some((ordinal, _)) => rels.push((Some(*ordinal), bytes)),
None => issues.push(format!(
"opc: relationships member {raw_name:?} has no owner part"
)),
}
}
}
let rel_views: Vec<(Option<u32>, &[u8])> = rels
.iter()
.map(|(owner, bytes)| (*owner, bytes.as_slice()))
.collect();
let mut model = opc::parse_opc(&members, content_types.as_deref(), &rel_views, limits)?;
model.issues.extend(issues);
Ok(model.encode())
}
fn part_rels_owner_member(name: &str) -> Option<String> {
let marker = "_rels/";
let idx = name.rfind(marker)?;
if idx != 0 && name.as_bytes()[idx - 1] != b'/' {
return None;
}
let file = &name[idx + marker.len()..];
let owner_file = file.strip_suffix(".rels")?;
if owner_file.is_empty() {
return None;
}
Some(format!("{}{owner_file}", &name[..idx]))
}
fn decode_metadata_member(source: &[u8], member: &ZipMember, limits: Limits) -> Option<Vec<u8>> {
if member.flags & FLAG_ENCRYPTED != 0 {
return None;
}
if member.uncompressed_size > limits.max_xml_part_bytes {
return None;
}
let off = usize::try_from(member.data.0).ok()?;
let len = usize::try_from(member.data.1).ok()?;
let end = off.checked_add(len)?;
let raw = source.get(off..end)?;
match member.method {
0 => Some(raw.to_vec()),
8 => crate::field::derive::inflate_raw_deflate(raw, member.uncompressed_size, limits).ok(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rels_owner_member_recognition() {
assert_eq!(
part_rels_owner_member("word/_rels/document.xml.rels").as_deref(),
Some("word/document.xml")
);
assert_eq!(
part_rels_owner_member("_rels/foo.rels").as_deref(),
Some("foo")
);
assert_eq!(part_rels_owner_member("_rels/.rels"), None);
assert_eq!(part_rels_owner_member("word/document.xml"), None);
assert_eq!(part_rels_owner_member("a_rels/b.rels"), None);
}
}