use crate::detect::Format;
use crate::error::{Result, StryptError};
use crate::formats::{ParseLimits, StripOptions};
use crate::report::{Finding, InspectOptions, MetadataKind, Note};
use super::zip::{self, Entry};
const OLE_MAGIC: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
pub(crate) struct Part<'a> {
pub(crate) entry: Entry<'a>,
pub(crate) data: Option<Vec<u8>>,
}
impl Part<'_> {
pub(crate) fn name(&self) -> Option<&str> {
self.entry.name_str()
}
pub(crate) fn text(&self) -> Option<&str> {
std::str::from_utf8(self.data.as_deref()?).ok()
}
}
pub(crate) fn read_parts<'a>(
input: &'a [u8],
format: Format,
limits: &ParseLimits,
) -> Result<Vec<Part<'a>>> {
let entries = zip::read(input, limits).map_err(|e| e.into_strypt(format))?;
let mut budget = limits.max_expanded_bytes;
let mut parts = Vec::with_capacity(entries.len());
for entry in entries {
let data = if entry.is_directory() {
None
} else {
let contents = entry.contents(budget).map_err(|e| e.into_strypt(format))?;
let owned = contents.into_owned();
zip::spend(&mut budget, as_u64(owned.len())).map_err(|e| e.into_strypt(format))?;
Some(owned)
};
parts.push(Part { entry, data });
}
Ok(parts)
}
pub(crate) fn refuse_nested_containers(
parts: &[Part<'_>],
format: Format,
notes: &mut Vec<Note>,
) -> Result<()> {
for part in parts {
let Some(data) = part.data.as_deref() else {
continue;
};
let name = part.name().unwrap_or("<non-utf8 entry name>");
let nested = if data.starts_with(&OLE_MAGIC) {
Some("an OLE compound file")
} else if data.starts_with(b"PK\x03\x04") {
Some("a nested archive")
} else if matches!(crate::detect::detect(data), Ok(Format::Pdf)) {
Some("an embedded PDF")
} else {
None
};
if let Some(what) = nested {
notes.push(Note::OutOfScopeContent {
location: format!("{name} ({what})"),
});
return Err(StryptError::Malformed {
format,
offset: None,
detail: crate::error::MalformedDetail::UnsupportedFeature,
});
}
}
Ok(())
}
pub(crate) enum Action {
Copy,
Drop,
Rewrite(Vec<u8>),
}
pub(crate) struct Decision {
pub(crate) action: Action,
pub(crate) findings: Vec<Finding>,
pub(crate) notes: Vec<Note>,
}
impl Decision {
pub(crate) const fn copy() -> Self {
Self {
action: Action::Copy,
findings: Vec::new(),
notes: Vec::new(),
}
}
pub(crate) fn unexamined(location: impl Into<String>, bytes: usize) -> Self {
Self {
action: Action::Copy,
findings: Vec::new(),
notes: vec![Note::UnparsedRegion {
location: location.into(),
bytes: as_u64(bytes),
}],
}
}
}
pub(crate) enum Embedded {
Unchanged,
Stripped {
bytes: Vec<u8>,
findings: Vec<Finding>,
notes: Vec<Note>,
},
}
pub(crate) fn strip_embedded_image(
format: Format,
data: &[u8],
name: &str,
options: &InspectOptions,
limits: &ParseLimits,
) -> Result<Embedded> {
let Some(handler) = crate::registry::handler_for(format) else {
return Err(StryptError::UnsupportedFormat {
format: crate::error::UnsupportedKind::NotYetImplemented(format),
});
};
let stripped = handler.strip(
data,
&StripOptions {
inspect: options.clone(),
limits: *limits,
},
)?;
if stripped.report.removed.is_empty() {
return Ok(Embedded::Unchanged);
}
let findings = stripped
.report
.removed
.into_iter()
.map(|mut finding| {
finding.location = format!("{name} → {}", finding.location);
finding
})
.collect();
Ok(Embedded::Stripped {
bytes: stripped.bytes,
findings,
notes: stripped.report.notes,
})
}
pub(crate) fn embedded_image_format(data: &[u8]) -> Option<Format> {
match crate::detect::detect(data) {
Ok(
format @ (Format::Jpeg
| Format::Png
| Format::Webp
| Format::Gif
| Format::Tiff
| Format::Heif
| Format::Avif),
) => Some(format),
_ => None,
}
}
pub(crate) fn container_findings(parts: &[Part<'_>]) -> Vec<Finding> {
let mut findings = Vec::new();
let timestamped = parts
.iter()
.filter(|p| p.entry.modified != zip::NORMALISED_DOS_DATETIME)
.count();
if timestamped > 0 {
findings.push(Finding::new(
MetadataKind::Timestamp,
"ZIP entry headers",
as_u64(timestamped),
));
}
let host_fields = parts
.iter()
.filter(|p| zip::extra_names_the_host(p.entry.extra))
.count();
if host_fields > 0 {
findings.push(Finding::new(
MetadataKind::DeviceIdentity,
"ZIP entry extra fields",
as_u64(host_fields),
));
}
findings
}
pub(crate) fn as_u64(n: usize) -> u64 {
u64::try_from(n).unwrap_or(u64::MAX)
}