mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 13:54:09 +00:00
M8: a form /Matrix through a reference to a reference is refused
Review of the walk's read of a form's `/Matrix`. Through `Document::resolve` it followed a reference to a reference, for the matrix and for each entry, as the app's renderer does; Poppler doesn't, and reads such a matrix as the identity and such an entry as 0. With `/Matrix [1 0 0 1 0 8 0 R]`, `8 0 obj 9 0 R` and `9 0 obj 200`, a mark where Poppler shows the text applied with success and left it in the file, where `main`, reading any entry by reference as the identity, removed it. `matrix_of` now follows each reference one hop, and a reference to a reference fails as a malformed matrix: where the content draws can't be told, so Apply refuses the page, as for a matrix that isn't six numbers. The refusal now says "isn't six numbers given directly or through one reference", and `matrix_of`'s doc names the appearances it reads, not patterns. The proof, which reads the matrix of forms and appearances with it, treats such a matrix as malformed too. Test: `a_form_whose_matrix_goes_through_a_reference_to_a_reference_is_refused` (an entry and the whole matrix, with a mark where either reader draws the text). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
cf26980471
commit
4f107a6f40
3 files changed
+46
-10
No files matched your search
@@ -1075,7 +1075,7 @@ fn xobject(
|
||||
// form moved onto a mark would be tested against the wrong place and never walked.
|
||||
let m = match crate::verify::matrix_of(doc, &s.dict) {
|
||||
Ok(m) => m,
|
||||
// Not six numbers. Removing hidden layers alone never asks where content lands, so
|
||||
// Not six numbers given directly or through one reference. Removing hidden layers alone never asks where content lands, so
|
||||
// the form is walked as it stands.
|
||||
Err(()) if scope.everywhere() && scope.hidden_text.is_none() => Matrix::IDENTITY,
|
||||
// Otherwise where the form draws, and so whether it reaches a mark or leaves the
|
||||
@@ -1085,7 +1085,9 @@ fn xobject(
|
||||
if scope.mode == Mode::Apply {
|
||||
out.error = Some(CosError::Syntax {
|
||||
offset: 0,
|
||||
detail: "a form XObject's /Matrix is not six numbers, so where it draws can't be told".into(),
|
||||
detail:
|
||||
"a form XObject's /Matrix isn't six numbers given directly or through one reference, so where it draws can't be told"
|
||||
.into(),
|
||||
});
|
||||
} else if scope.everywhere() {
|
||||
scope.layer_blocks += 1;
|
||||
|
||||
@@ -72,6 +72,30 @@ fn a_form_whose_matrix_is_not_six_numbers_is_refused() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a reference, for the matrix or one of its entries: Poppler doesn't follow it (it
|
||||
/// reads such a matrix as the identity, such an entry as 0), the app's renderer follows it and
|
||||
/// reads the move, so where the form draws can't be told, and the page is refused, as for a matrix that isn't six numbers. Read as either,
|
||||
/// a mark where the other reader shows the text would leave it in the file.
|
||||
#[test]
|
||||
fn a_form_whose_matrix_goes_through_a_reference_to_a_reference_is_refused() {
|
||||
for (matrix, target) in [("/Matrix [1 0 0 1 0 8 0 R]", &b"200"[..]), ("/Matrix 8 0 R", b"[1 0 0 1 0 200]")] {
|
||||
for mark_at in [MARK, [0.0, 0.0, 300.0, 100.0]] {
|
||||
let form = stream(
|
||||
&format!("/Type /XObject /Subtype /Form /BBox [0 0 300 100] {matrix} /Resources << /Font << /F1 5 0 R >> >>"),
|
||||
b"BT /F1 10 Tf 20 60 Td (SECRET) Tj ET",
|
||||
);
|
||||
let mut doc = one_page(b"/Fm Do", "/XObject << /Fm 7 0 R >>", vec![form, b"9 0 R".to_vec(), target.to_vec()]);
|
||||
mark(&mut doc, 0, &[mark_at], "");
|
||||
let result = apply(&mut doc, None);
|
||||
assert!(
|
||||
matches!(&result, Err(RedactError::Cos(pdfcraft_cos::CosError::Syntax { detail, .. })) if detail.contains("/Matrix")),
|
||||
"{matrix}, 8 0 obj 9 0 R, 9 0 obj {}, mark at {mark_at:?}: {result:?}",
|
||||
String::from_utf8_lossy(target)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The scans count such a form as content that may remain: the verify pass under a mark, and
|
||||
/// Remove Hidden Information's scan, whose removal then refuses the page.
|
||||
#[test]
|
||||
|
||||
@@ -396,17 +396,20 @@ pub(super) fn push_squashed(into: &mut String, s: &str) {
|
||||
into.extend(s.chars().filter(|c| !c.is_whitespace() && *c != '\u{feff}'));
|
||||
}
|
||||
|
||||
/// A form's or pattern's `/Matrix`, the array and each entry direct or by reference: absent or null
|
||||
/// A form's or an annotation appearance's `/Matrix`, the array and each entry direct or by reference: absent or null
|
||||
/// is the identity, present but malformed is an error (guessing the identity would put its content in the
|
||||
/// wrong place). The redaction walk reads a form's with it too.
|
||||
/// wrong place). A reference to a reference is malformed here: readers disagree on following it
|
||||
/// (Poppler doesn't, the app's renderer does), so where the content draws can't be told. The
|
||||
/// redaction walk reads a form's with it too.
|
||||
pub(crate) fn matrix_of(doc: &Document, dict: &Dict) -> Result<Matrix, ()> {
|
||||
let Some(m) = dict.get(b"Matrix") else { return Ok(Matrix::IDENTITY) };
|
||||
// Read in place when written directly, and the length checked before any entry is resolved:
|
||||
// the walk reads it at every `Do`, and an array of any length would be copied each time.
|
||||
// Each reference is followed one hop. The array is read in place when written directly, and
|
||||
// its length checked before any entry is read: the walk reads it at every `Do`, and an array of
|
||||
// any length would be copied each time.
|
||||
let held;
|
||||
let m = match m {
|
||||
Object::Ref(_) => {
|
||||
held = doc.resolve(m);
|
||||
Object::Ref(r) => {
|
||||
held = doc.get(*r);
|
||||
&*held
|
||||
}
|
||||
direct => direct,
|
||||
@@ -416,8 +419,15 @@ pub(crate) fn matrix_of(doc: &Document, dict: &Dict) -> Result<Matrix, ()> {
|
||||
return Ok(Matrix::IDENTITY);
|
||||
}
|
||||
let items = m.as_array().filter(|a| a.len() == 6).ok_or(())?;
|
||||
let items: Vec<Object> = items.iter().map(|x| (*doc.resolve(x)).clone()).collect();
|
||||
Matrix::from_operands(&items).filter(|m| m.0.iter().all(|v| v.is_finite())).ok_or(())
|
||||
let mut values = [0.0; 6];
|
||||
for (value, item) in values.iter_mut().zip(items) {
|
||||
let n = match item {
|
||||
Object::Ref(r) => doc.get(*r).as_f64(),
|
||||
direct => direct.as_f64(),
|
||||
};
|
||||
*value = n.filter(|n| n.is_finite()).ok_or(())?;
|
||||
}
|
||||
Ok(Matrix(values))
|
||||
}
|
||||
|
||||
/// A form's `/BBox` as `[x0 y0 x1 y1]`: absent is `None`, present but malformed is an error.
|
||||
|
||||
Reference in new issue
Block a user