use crate::container_meta::{ContainerFormat, clean_file, inspect_file};
use crate::error::{CumError, Result};
use crate::image_meta::{clean_image, detect_image_format, inspect_image};
use crate::types::{CleanOutput, CleanStats, ImageFormat, InspectOutput, MediaHint, MetaFinding};
use crate::unicode::{CleanOpts, InspectOpts, clean_text, inspect_text};
pub const MAX_INPUT_BYTES: usize = 256 * 1024 * 1024;
pub fn resolve_format(bytes: &[u8], hint: Option<&MediaHint>) -> Option<MediaHint> {
if let Some(h) = hint {
return Some(h.clone());
}
if let Some(img_fmt) = detect_image_format(bytes) {
return Some(match img_fmt {
ImageFormat::Png => MediaHint::Png,
ImageFormat::Jpeg => MediaHint::Jpeg,
ImageFormat::Webp => MediaHint::Webp,
ImageFormat::Svg => MediaHint::Svg,
});
}
if bytes.starts_with(b"%PDF-") {
return Some(MediaHint::Pdf);
}
if bytes.starts_with(b"PK\x03\x04") || bytes.starts_with(b"PK\x05\x06") {
return Some(MediaHint::Docx);
}
if ContainerFormat::detect(bytes).is_some() {
return Some(MediaHint::Html);
}
if std::str::from_utf8(bytes).is_ok() {
return Some(MediaHint::Text);
}
None
}
pub fn clean(bytes: &[u8], hint: Option<MediaHint>) -> Result<CleanOutput> {
if bytes.len() > MAX_INPUT_BYTES {
return Err(CumError::InputTooLarge {
limit: MAX_INPUT_BYTES,
actual: bytes.len(),
});
}
let format = resolve_format(bytes, hint.as_ref()).ok_or_else(|| {
CumError::UnsupportedFormat(
"could not detect media format; pass an explicit MediaHint".into(),
)
})?;
match &format {
MediaHint::Text => {
let text = std::str::from_utf8(bytes)
.map_err(|e| CumError::ParseError(format!("not valid UTF-8: {e}")))?;
let opts = CleanOpts::safe();
let (cleaned, stats) = clean_text(text, &opts)?;
Ok(CleanOutput {
bytes: cleaned.into_bytes(),
stats,
format,
})
}
MediaHint::Png | MediaHint::Jpeg | MediaHint::Webp | MediaHint::Svg => {
let cleaned = clean_image(bytes)?;
let removed = bytes.len().abs_diff(cleaned.len());
Ok(CleanOutput {
bytes: cleaned,
stats: CleanStats {
removed_count: 0,
replaced_count: 0,
metadata_chunks_removed: usize::from(removed > 0),
summary: vec!["Image metadata stripped.".into()],
},
format,
})
}
MediaHint::Pdf => {
let (cleaned, stats) = clean_file(bytes, &ContainerFormat::Pdf)?;
Ok(CleanOutput {
bytes: cleaned,
stats,
format,
})
}
MediaHint::Docx => {
let (cleaned, stats) = clean_file(bytes, &ContainerFormat::Docx)?;
Ok(CleanOutput {
bytes: cleaned,
stats,
format,
})
}
MediaHint::Odt => {
let (cleaned, stats) = clean_file(bytes, &ContainerFormat::Odt)?;
Ok(CleanOutput {
bytes: cleaned,
stats,
format,
})
}
MediaHint::Html => {
let (cleaned, stats) = clean_file(bytes, &ContainerFormat::Html)?;
Ok(CleanOutput {
bytes: cleaned,
stats,
format,
})
}
MediaHint::Markdown => {
let (cleaned, stats) = clean_file(bytes, &ContainerFormat::Markdown)?;
Ok(CleanOutput {
bytes: cleaned,
stats,
format,
})
}
}
}
pub fn inspect(bytes: &[u8], hint: Option<MediaHint>) -> Result<InspectOutput> {
if bytes.len() > MAX_INPUT_BYTES {
return Err(CumError::InputTooLarge {
limit: MAX_INPUT_BYTES,
actual: bytes.len(),
});
}
let format = resolve_format(bytes, hint.as_ref()).ok_or_else(|| {
CumError::UnsupportedFormat(
"could not detect media format; pass an explicit MediaHint".into(),
)
})?;
match &format {
MediaHint::Text => {
let text = std::str::from_utf8(bytes)
.map_err(|e| CumError::ParseError(format!("not valid UTF-8: {e}")))?;
let opts = InspectOpts::default();
let report = inspect_text(text, &opts)?;
Ok(InspectOutput {
text_report: Some(report),
image_report: None,
meta_findings: vec![],
format,
})
}
MediaHint::Png | MediaHint::Jpeg | MediaHint::Webp | MediaHint::Svg => {
let report = inspect_image(bytes)?;
Ok(InspectOutput {
text_report: None,
image_report: Some(report),
meta_findings: vec![],
format,
})
}
MediaHint::Pdf => {
let findings = inspect_file(bytes, &ContainerFormat::Pdf);
Ok(InspectOutput {
text_report: None,
image_report: None,
meta_findings: findings,
format,
})
}
MediaHint::Docx => {
let findings = inspect_file(bytes, &ContainerFormat::Docx);
Ok(InspectOutput {
text_report: None,
image_report: None,
meta_findings: findings,
format,
})
}
MediaHint::Odt => {
let findings = inspect_file(bytes, &ContainerFormat::Odt);
Ok(InspectOutput {
text_report: None,
image_report: None,
meta_findings: findings,
format,
})
}
MediaHint::Html => {
let findings = inspect_file(bytes, &ContainerFormat::Html);
Ok(InspectOutput {
text_report: None,
image_report: None,
meta_findings: findings,
format,
})
}
MediaHint::Markdown => {
let findings = inspect_file(bytes, &ContainerFormat::Markdown);
Ok(InspectOutput {
text_report: None,
image_report: None,
meta_findings: findings,
format,
})
}
}
}
pub fn inspect_and_clean(
bytes: &[u8],
hint: Option<MediaHint>,
) -> Result<(InspectOutput, CleanOutput)> {
let inspect_out = inspect(bytes, hint.clone())?;
let clean_out = clean(bytes, hint)?;
Ok((inspect_out, clean_out))
}
pub fn format_name(hint: &MediaHint) -> &'static str {
match hint {
MediaHint::Text => "plain text",
MediaHint::Png => "PNG image",
MediaHint::Jpeg => "JPEG image",
MediaHint::Webp => "WebP image",
MediaHint::Svg => "SVG document",
MediaHint::Pdf => "PDF document",
MediaHint::Docx => "DOCX document",
MediaHint::Odt => "ODT document",
MediaHint::Html => "HTML document",
MediaHint::Markdown => "Markdown document",
}
}
pub fn all_findings(output: &InspectOutput) -> Vec<&MetaFinding> {
let mut out = Vec::new();
if let Some(img) = &output.image_report {
out.extend(img.findings.iter());
}
out.extend(output.meta_findings.iter());
out
}