mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
redact: the shared-JBIG2 walk names by object number, and reads what it stores by number and generation
Main's #1143 (863af864a), merged in77b9cfa31, makes cos read a reference whose generation isn't its object's as null. The walk that finds what shares a /JBIG2Globals dictionary with what left fetched through `Document::get`, but remembered what it read by object number alone, so a reference of another generation and the object's own one shared what the first of them read: - What a parameters value names (`Run::names_in`): `/DecodeParms [10 1 R 10 0 R]` on an image that left, object 10 naming dictionary 9, read 10 1 R as null, named nothing, and remembered that for object 10. The 10 0 R after it found it, so nothing named 9, and the dictionary stayed in the file with the image another page kept. - A size, filters or parameters an image is stored again with (`Run::follow`): an image whose `/Width 11 1 R` reads as null can't be stored again, and refused the run alone. Once another image had read 11 0 R, it took that width and was stored with it. What an image names is now read by object number, whatever generation the reference gives (`Run::fetch_numbered`), in `names_in` and in the `/JBIG2Globals` chain alike: a reader that doesn't compare generations draws with that object, so what an image that left names this way may hold its shapes. That is how the chain already named `9 1 R` (the "another generation" way out), and how both read before #1143. What is read by number is remembered by number. The values an image is stored again with are read as cos reads them, and remembered by number and generation, so a reference of another generation reads its own way. `kept`'s comment said a reference reads the object of its number whatever its generation, which is no longer so for a full save. It says how `reachable_objects` reads one now. The README and `Run`'s comments say which reads take the number and which the generation too. Tests: - `a_redacted_scans_shared_jbig2_dictionary_leaves_the_file` gains three ways out: - parameters through another generation, then their own; - parameters through another generation alone; - a `/JBIG2Globals` reference to a reference of another generation. - `a_size_through_another_generation_isnt_the_one_its_own_reference_reads` stores no image with the width another reference read. It refuses instead. 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
77b9cfa310
commit
0a00862e91
3 files changed
+73
-14
No files matched your search
@@ -34,7 +34,10 @@ clear_marks(&mut doc, None)?; // remove marks without app
|
||||
the same way, but it has changed the document by the time it refuses: a caller passes a copy it
|
||||
can drop. Another dictionary holding the same shapes in other bytes isn't found, nor is a copy
|
||||
nothing names, and an image that hasn't left keeps its globals with its pixels: one another page
|
||||
still draws, or one something that doesn't draw it still refers to. Finding what to store again
|
||||
still draws, or one something that doesn't draw it still refers to. What an image names as
|
||||
globals is read by object number, whatever generation a reference gives, as a reader that doesn't
|
||||
compare generations draws it (cos reads such a reference as null); what one is stored again with,
|
||||
its size, filters and parameters, is read as cos reads it. Finding what to store again
|
||||
reads each object at most once for each way it is read (a `/JBIG2Globals` chain named part way,
|
||||
or a ring of references or a chain of more than 32, may be read again), and what it fetches, remembers and decodes (a decode that
|
||||
fails counting as much as it was allowed, and an image the dictionaries it names) counts against
|
||||
|
||||
@@ -15,8 +15,9 @@ use pdfcraft_cos::{CosError, DECODE_PARMS_KEYS, Dict, Document, ImageBudget, MAX
|
||||
/// The references followed from `/JBIG2Globals` towards a stream, so that a ring of them ends.
|
||||
const MAX_HOPS: usize = 32;
|
||||
|
||||
/// The objects a full save would keep, by number: a reference reads the object of its number,
|
||||
/// whatever generation it gives. Fails only when a full save couldn't number them all.
|
||||
/// The objects a full save would keep, by number, as [`reachable_objects`] finds them: a reference
|
||||
/// whose generation isn't its object's reads as null there (#1143), so it keeps nothing. Fails only
|
||||
/// when a full save couldn't number them all.
|
||||
pub(crate) fn kept(doc: &Document) -> Result<HashSet<u32>, CosError> {
|
||||
reachable_objects(doc)
|
||||
}
|
||||
@@ -75,8 +76,11 @@ enum How {
|
||||
}
|
||||
|
||||
/// What one redaction or Remove Hidden Information run reads, to find what shares a dictionary with
|
||||
/// what left, against [`RUN_BUDGET`]. Each object is fetched at most once a run for each way it is
|
||||
/// read, and kept only as what is read of it: a value many streams share, by reference, through a
|
||||
/// what left, against [`RUN_BUDGET`]. What a stream names is read by object number, whatever
|
||||
/// generation a reference gives ([`Run::fetch_numbered`]); what one is stored again with is read as
|
||||
/// cos reads it, by number and generation, so a reference of another generation reads its own way.
|
||||
/// Each object is fetched at most once a run for each way it is read, and kept only as what is read
|
||||
/// of it: a value many streams share, by reference, through a
|
||||
/// chain of them or through an entry, costs it once however many there are. The exceptions are a
|
||||
/// `/JBIG2Globals` chain named part way, which is followed again from there, and a ring of
|
||||
/// references or a chain past the limit, read again (up to 33 fetches) wherever it is met; every
|
||||
@@ -85,8 +89,10 @@ pub(crate) struct Run {
|
||||
budget: usize,
|
||||
/// What was spent of the budget.
|
||||
pub(crate) spent: usize,
|
||||
/// What a reference leads to, as it is read, under every reference on the way.
|
||||
reduced: HashMap<(u32, How), Rc<Object>>,
|
||||
/// What a reference leads to, as it is read, under every reference on the way, by number and
|
||||
/// generation: one whose generation isn't its object's reads as null (#1143), and must not give
|
||||
/// another reference to that number what it read, nor take what that one read.
|
||||
reduced: HashMap<(pdfcraft_cos::ObjRef, How), Rc<Object>>,
|
||||
/// What a reference leads to names, under every reference on the way, with the key of the one
|
||||
/// that names what it ends at: aliases and chains find it, and are counted as that one.
|
||||
names: HashMap<Key, (Rc<[u32]>, Key)>,
|
||||
@@ -119,10 +125,19 @@ impl Run {
|
||||
Ok(doc.get(r))
|
||||
}
|
||||
|
||||
/// Fetches the object of `r`'s number, whatever generation `r` gives, counted. Cos reads a
|
||||
/// reference whose generation isn't its object's as null (#1143), but a reader that doesn't
|
||||
/// compare generations draws with the object: what an image that left names this way may hold
|
||||
/// its shapes, so it is named, as what names it this way is a user of it.
|
||||
fn fetch_numbered(&mut self, doc: &Document, r: pdfcraft_cos::ObjRef) -> Result<Arc<Object>, crate::RedactError> {
|
||||
self.fetch(doc, pdfcraft_cos::ObjRef::new(r.num, doc.generation(r.num)))
|
||||
}
|
||||
|
||||
/// What `v` leads to through references, as `followed` goes, kept as `read` makes it. Each
|
||||
/// reference is looked up before it is fetched, and what it leads to is remembered under every
|
||||
/// one on the way, so one remembered costs nothing more, and an alias or a chain only the
|
||||
/// references before it. A ring, or a chain past the limit, is read as the reference it ends at.
|
||||
/// one on the way, so one remembered costs nothing more, and a chain only the references before
|
||||
/// it. A reference of another generation is looked up as itself, and read as cos reads it. A
|
||||
/// ring, or a chain past the limit, is read as the reference it ends at.
|
||||
fn follow(
|
||||
&mut self,
|
||||
doc: &Document,
|
||||
@@ -130,17 +145,17 @@ impl Run {
|
||||
how: How,
|
||||
read: impl FnOnce(&mut Self, &Document, &Object) -> Result<Object, crate::RedactError>,
|
||||
) -> Result<Rc<Object>, crate::RedactError> {
|
||||
let mut keys: Vec<(u32, How)> = Vec::new();
|
||||
let mut keys: Vec<(pdfcraft_cos::ObjRef, How)> = Vec::new();
|
||||
let mut fetched: Option<Arc<Object>> = None;
|
||||
for _ in 0..=MAX_HOPS {
|
||||
let Some(r) = fetched.as_deref().unwrap_or(v).as_ref() else { break };
|
||||
if let Some(hit) = self.reduced.get(&(r.num, how)).cloned() {
|
||||
if let Some(hit) = self.reduced.get(&(r, how)).cloned() {
|
||||
for k in keys {
|
||||
self.reduced.insert(k, hit.clone());
|
||||
}
|
||||
return Ok(hit);
|
||||
}
|
||||
keys.push((r.num, how));
|
||||
keys.push((r, how));
|
||||
fetched = Some(self.fetch(doc, r)?);
|
||||
}
|
||||
let made = Rc::new(read(self, doc, fetched.as_deref().unwrap_or(v))?);
|
||||
@@ -177,7 +192,7 @@ impl Run {
|
||||
return Ok((hit, Some(at)));
|
||||
}
|
||||
keys.push(key);
|
||||
fetched = Some(self.fetch(doc, r)?);
|
||||
fetched = Some(self.fetch_numbered(doc, r)?);
|
||||
}
|
||||
self.read += 1;
|
||||
let held = fetched.as_deref().unwrap_or(v);
|
||||
@@ -227,7 +242,7 @@ impl Run {
|
||||
break;
|
||||
}
|
||||
found.push(at.num);
|
||||
match &*self.fetch(doc, at)? {
|
||||
match &*self.fetch_numbered(doc, at)? {
|
||||
Object::Ref(next) => at = *next,
|
||||
_ => break,
|
||||
}
|
||||
|
||||
@@ -1932,6 +1932,27 @@ fn ways_out(scan: &SharedScan) -> Vec<(&'static str, [f64; 4], SharedPages)> {
|
||||
SharedPages { first: format!("{} /FFilter /JBIG2Decode /FDecodeParms << /JBIG2Globals 9 0 R >>", shared_image(SHARED)), ..base() },
|
||||
),
|
||||
("another generation", COVERS, SharedPages { first: shared_image("/DecodeParms << /JBIG2Globals 9 1 R >>"), ..base() }),
|
||||
// Cos reads a reference of another generation as null (#1143); a reader that doesn't compare
|
||||
// generations draws with what it names. Read first, it mustn't hide what the other names.
|
||||
(
|
||||
"parameters through another generation, then their own",
|
||||
COVERS,
|
||||
SharedPages {
|
||||
first: shared_image("/DecodeParms [10 1 R 10 0 R]").replace("/Filter /JBIG2Decode", "/Filter [/ASCIIHexDecode /JBIG2Decode]"),
|
||||
extra: vec![b"<< /JBIG2Globals 9 0 R >>".to_vec()],
|
||||
..base()
|
||||
},
|
||||
),
|
||||
(
|
||||
"parameters through another generation",
|
||||
COVERS,
|
||||
SharedPages { first: shared_image("/DecodeParms 10 1 R"), extra: vec![b"<< /JBIG2Globals 9 0 R >>".to_vec()], ..base() },
|
||||
),
|
||||
(
|
||||
"a reference to it of another generation",
|
||||
COVERS,
|
||||
SharedPages { first: shared_image("/DecodeParms << /JBIG2Globals 10 1 R >>"), extra: vec![b"9 0 R".to_vec()], ..base() },
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1954,6 +1975,26 @@ fn a_redacted_scans_shared_jbig2_dictionary_leaves_the_file() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A size given through a reference of another generation is null, as cos reads it (#1143), whatever
|
||||
/// another image read through the reference of that number's own: the image can't be stored again, so
|
||||
/// the redaction refuses, rather than storing it with the size the other reference read.
|
||||
#[test]
|
||||
fn a_size_through_another_generation_isnt_the_one_its_own_reference_reads() {
|
||||
let scan = shared_scan();
|
||||
let stale = shared_image(SHARED).replace("/Width 400", "/Width 11 1 R");
|
||||
let mut doc = SharedPages {
|
||||
content: [DRAWS, b" q 200 0 0 20 0 200 cm /Stale Do Q".as_slice()].concat(),
|
||||
resources: "/XObject << /Im 7 0 R /Stale 12 0 R >>".into(),
|
||||
other: shared_image(SHARED).replace("/Width 400", "/Width 11 0 R"),
|
||||
extra: vec![b"null".to_vec(), b"400".to_vec(), stream(&stale, scan.page.clone())],
|
||||
..SharedPages::new(&scan)
|
||||
}
|
||||
.build(&scan);
|
||||
mark(&mut doc, 0, &[COVERS], "");
|
||||
let result = apply(&mut doc, None);
|
||||
assert!(matches!(result, Err(RedactError::SharedJbig2Globals(1))), "{result:?}");
|
||||
}
|
||||
|
||||
/// However the image that stays names the dictionary, it comes out without it: its parameters by
|
||||
/// reference, behind another filter, or a copy of the dictionary, as a file put back together from
|
||||
/// pages split apart has, each page with its own. It is also read when its size or filter is given
|
||||
|
||||
Reference in new issue
Block a user