mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:50:14 +00:00
fix(audit): resolve an indirect /Subtype before skipping an object
The annotation scan and the form XObject walk read /Subtype only as a direct name. A /Subtype stored as a reference to the name read as none, so a /Redact annotation or a covering form named that way was skipped as an unrelated object and the page reported clean. Both now follow the reference, and a reference that cannot be loaded fails the audit.
This commit is contained in:
@@ -259,7 +259,7 @@ impl<'a> Walker<'a> {
|
||||
let resolved = self.doc.resolve(entry);
|
||||
let Object::Stream(stream) = &*resolved else { return Ok(()) };
|
||||
let dict = &stream.dict;
|
||||
if dict.name(b"Subtype") != Some(b"Form") {
|
||||
if crate::subtype(self.doc, dict)?.as_deref() != Some(b"Form") {
|
||||
return Ok(());
|
||||
}
|
||||
let matrix = dict
|
||||
|
||||
+11
-1
@@ -229,7 +229,7 @@ fn annots(doc: &Document, page: &Dict) -> Result<Vec<AnnotationCover>, AuditErro
|
||||
continue;
|
||||
}
|
||||
let rect = [v[0].min(v[2]), v[1].min(v[3]), v[0].max(v[2]), v[1].max(v[3])];
|
||||
match dict.name(b"Subtype") {
|
||||
match subtype(doc, &dict)?.as_deref() {
|
||||
Some(b"Redact") => out.push(AnnotationCover::Redact(rect)),
|
||||
Some(b"Square") => {
|
||||
let flags = dict.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0);
|
||||
@@ -249,6 +249,16 @@ fn annots(doc: &Document, page: &Dict) -> Result<Vec<AnnotationCover>, AuditErro
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The `/Subtype` name of `dict`, following an indirect reference. A reference that cannot be
|
||||
/// loaded fails the audit: the subtype decides whether the object is checked at all.
|
||||
fn subtype(doc: &Document, dict: &Dict) -> Result<Option<Vec<u8>>, AuditError> {
|
||||
let name = match dict.get(b"Subtype") {
|
||||
Some(pdfcraft_cos::Object::Ref(r)) => doc.try_get(r.num)?.as_name().map(<[u8]>::to_vec),
|
||||
other => other.and_then(pdfcraft_cos::Object::as_name).map(<[u8]>::to_vec),
|
||||
};
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
fn annotation_color(doc: &Document, dict: &Dict, key: &[u8]) -> Option<[f64; 3]> {
|
||||
let values: Vec<f64> = dict.get(key).map(|o| doc.resolve(o))?.as_array()?.iter().map(|o| doc.resolve(o).as_f64()).collect::<Option<_>>()?;
|
||||
if !values.iter().all(|v| v.is_finite()) {
|
||||
|
||||
@@ -602,3 +602,32 @@ fn rectangle_filled_after_restore_is_a_cover() {
|
||||
assert_eq!(findings.len(), 1, "{findings:?}");
|
||||
assert!(findings[0].covered_text.contains("SECRET"), "{findings:?}");
|
||||
}
|
||||
|
||||
/// A `/Subtype` may be an indirect reference to the name, like any other value: a `/Redact`
|
||||
/// annotation and a covering form XObject named that way are still checked.
|
||||
#[test]
|
||||
fn indirect_subtype_is_resolved() {
|
||||
let mut b = Builder::new();
|
||||
b.add("<< /Type /Catalog /Pages 2 0 R >>");
|
||||
b.add("<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
|
||||
b.add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> /Annots [6 0 R] >>");
|
||||
b.add_stream(b"BT /F1 24 Tf 100 700 Td (DO NOT SHARE) Tj ET\n");
|
||||
b.add(FONT);
|
||||
b.add("<< /Type /Annot /Subtype 7 0 R /Rect [95 685 305 725] >>");
|
||||
b.add("/Redact");
|
||||
let findings = audit_redactions(&open(b.build())).expect("audit must run");
|
||||
assert_eq!(findings.len(), 1, "{findings:?}");
|
||||
assert_eq!(findings[0].kind, FindingKind::UnappliedMark);
|
||||
|
||||
let mut b = Builder::new();
|
||||
b.add("<< /Type /Catalog /Pages 2 0 R >>");
|
||||
b.add("<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
|
||||
b.add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 6 0 R >> /XObject << /X 5 0 R >> >> >>");
|
||||
b.add_stream(b"BT /F1 24 Tf 100 700 Td (SECRET) Tj ET\n/X Do\n");
|
||||
b.add_stream_with(b"0 g\n100 688 220 32 re f\n", "/Type /XObject /Subtype 7 0 R /BBox [0 0 612 792]");
|
||||
b.add(FONT);
|
||||
b.add("/Form");
|
||||
let findings = audit_redactions(&open(b.build())).expect("audit must run");
|
||||
assert_eq!(findings.len(), 1, "{findings:?}");
|
||||
assert_eq!(findings[0].kind, FindingKind::CoveredText);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user