use crate::error::Result;
use crate::field::dag::SourceServer;
use crate::limits::Limits;
const SIG_EOCD: &[u8; 4] = b"PK\x05\x06";
const SIG_CENTRAL: &[u8; 4] = b"PK\x01\x02";
const LOCAL_HEADER_FIXED: u64 = 30;
struct CdEntry {
name: Vec<u8>,
local_offset: u64,
compressed_size: u64,
}
pub fn opc_model_from_units(source: &dyn SourceServer, limits: Limits) -> Result<Option<Vec<u8>>> {
#[cfg(feature = "opc")]
{
let Some(image) = sparse_image(source, limits, is_opc_metadata)? else {
return Ok(None);
};
Ok(crate::field::opc::build_opc_model(&image, limits).ok())
}
#[cfg(not(feature = "opc"))]
{
let _ = (source, limits);
Ok(None)
}
}
pub fn odt_model_from_units(source: &dyn SourceServer, limits: Limits) -> Result<Option<Vec<u8>>> {
#[cfg(feature = "odt")]
{
let Some(image) = sparse_image(source, limits, is_odf_part)? else {
return Ok(None);
};
Ok(crate::adapter::odt::build_odt_model(&image, limits).ok())
}
#[cfg(not(feature = "odt"))]
{
let _ = (source, limits);
Ok(None)
}
}
fn is_opc_metadata(name: &[u8]) -> bool {
let Ok(s) = core::str::from_utf8(name) else {
return false;
};
s.eq_ignore_ascii_case("[Content_Types].xml")
|| s.eq_ignore_ascii_case("_rels/.rels")
|| is_part_rels_member(s)
}
#[cfg(feature = "opc")]
fn is_part_rels_member(s: &str) -> bool {
crate::field::opc::part_rels_owner_member(s).is_some()
}
#[cfg(not(feature = "opc"))]
fn is_part_rels_member(_s: &str) -> bool {
false
}
fn is_odf_part(name: &[u8]) -> bool {
name == b"mimetype" || name == b"META-INF/manifest.xml"
}
fn sparse_image(
source: &dyn SourceServer,
limits: Limits,
select: fn(&[u8]) -> bool,
) -> Result<Option<Vec<u8>>> {
let Some(len) = source.exact_len() else {
return Ok(None);
};
if len == 0 || len > limits.max_input_bytes {
return Ok(None);
}
let Ok(len_usize) = usize::try_from(len) else {
return Ok(None);
};
let Some((cd_off, dir)) = source.zip_directory(limits)? else {
return Ok(None);
};
if cd_off.saturating_add(dir.len() as u64) != len {
return Ok(None);
}
let entries = match parse_directory(&dir, cd_off) {
Some(e) => e,
None => return Ok(None),
};
let mut image = vec![0u8; len_usize];
image[cd_off as usize..].copy_from_slice(&dir);
for e in &entries {
let Some(head) = source.read_exact(e.local_offset, LOCAL_HEADER_FIXED, limits)? else {
return Ok(None);
};
if head.len() != LOCAL_HEADER_FIXED as usize || &head[0..4] != b"PK\x03\x04" {
return Ok(None);
}
let name_len = u64::from(u16le(&head, 26).unwrap_or(u16::MAX));
let extra_len = u64::from(u16le(&head, 28).unwrap_or(u16::MAX));
let Some(lfh_len) = LOCAL_HEADER_FIXED
.checked_add(name_len)
.and_then(|v| v.checked_add(extra_len))
else {
return Ok(None);
};
let Some(data_off) = e.local_offset.checked_add(lfh_len) else {
return Ok(None);
};
let Some(data_end) = data_off.checked_add(e.compressed_size) else {
return Ok(None);
};
if data_end > cd_off || data_end > len {
return Ok(None);
}
let fill_end = if select(&e.name) { data_end } else { data_off };
let Some(span) = source.read_exact(e.local_offset, fill_end - e.local_offset, limits)?
else {
return Ok(None);
};
if span.len() as u64 != fill_end - e.local_offset {
return Ok(None);
}
image[e.local_offset as usize..fill_end as usize].copy_from_slice(&span);
}
Ok(Some(image))
}
fn parse_directory(dir: &[u8], cd_off: u64) -> Option<Vec<CdEntry>> {
let eocd = rfind_eocd(dir)?;
let entries = u16le(dir, eocd + 10)?;
let cd_size = u32le(dir, eocd + 12)?;
let cd_off_field = u32le(dir, eocd + 16)?;
if entries == u16::MAX || cd_size == u32::MAX || cd_off_field == u32::MAX {
return None;
}
if u64::from(cd_off_field) != cd_off {
return None;
}
let cd = dir.get(0..cd_size as usize)?;
let mut out = Vec::new();
let mut p = 0usize;
while p < cd.len() {
if cd.get(p..p + 4)? != SIG_CENTRAL {
return None;
}
let comp = u32le(cd, p + 20)?;
let uncomp = u32le(cd, p + 24)?;
let name_len = u16le(cd, p + 28)? as usize;
let extra_len = u16le(cd, p + 30)? as usize;
let comment_len = u16le(cd, p + 32)? as usize;
let lo = u32le(cd, p + 42)?;
if comp == u32::MAX || uncomp == u32::MAX || lo == u32::MAX {
return None;
}
let name = cd.get(p + 46..p + 46 + name_len)?.to_vec();
out.push(CdEntry {
name,
local_offset: u64::from(lo),
compressed_size: u64::from(comp),
});
p = p.checked_add(46 + name_len + extra_len + comment_len)?;
}
if out.len() != entries as usize {
return None;
}
Some(out)
}
fn rfind_eocd(dir: &[u8]) -> Option<usize> {
if dir.len() < 22 {
return None;
}
(0..=dir.len() - SIG_EOCD.len())
.rev()
.find(|&i| &dir[i..i + SIG_EOCD.len()] == SIG_EOCD)
}
fn u16le(d: &[u8], i: usize) -> Option<u16> {
let s = d.get(i..i + 2)?;
Some(u16::from_le_bytes([s[0], s[1]]))
}
fn u32le(d: &[u8], i: usize) -> Option<u32> {
let s = d.get(i..i + 4)?;
Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}