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
|
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
|
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
|
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,
|
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
|
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
|
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.
|
/// The references followed from `/JBIG2Globals` towards a stream, so that a ring of them ends.
|
||||||
const MAX_HOPS: usize = 32;
|
const MAX_HOPS: usize = 32;
|
||||||
|
|
||||||
/// The objects a full save would keep, by number: a reference reads the object of its number,
|
/// The objects a full save would keep, by number, as [`reachable_objects`] finds them: a reference
|
||||||
/// whatever generation it gives. Fails only when a full save couldn't number them all.
|
/// 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> {
|
pub(crate) fn kept(doc: &Document) -> Result<HashSet<u32>, CosError> {
|
||||||
reachable_objects(doc)
|
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 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
|
/// what left, against [`RUN_BUDGET`]. What a stream names is read by object number, whatever
|
||||||
/// read, and kept only as what is read of it: a value many streams share, by reference, through a
|
/// 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
|
/// 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
|
/// `/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
|
/// 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,
|
budget: usize,
|
||||||
/// What was spent of the budget.
|
/// What was spent of the budget.
|
||||||
pub(crate) spent: usize,
|
pub(crate) spent: usize,
|
||||||
/// What a reference leads to, as it is read, under every reference on the way.
|
/// What a reference leads to, as it is read, under every reference on the way, by number and
|
||||||
reduced: HashMap<(u32, How), Rc<Object>>,
|
/// 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
|
/// 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.
|
/// that names what it ends at: aliases and chains find it, and are counted as that one.
|
||||||
names: HashMap<Key, (Rc<[u32]>, Key)>,
|
names: HashMap<Key, (Rc<[u32]>, Key)>,
|
||||||
@@ -119,10 +125,19 @@ impl Run {
|
|||||||
Ok(doc.get(r))
|
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
|
/// 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
|
/// 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
|
/// one on the way, so one remembered costs nothing more, and a chain only the references before
|
||||||
/// references before it. A ring, or a chain past the limit, is read as the reference it ends at.
|
/// 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(
|
fn follow(
|
||||||
&mut self,
|
&mut self,
|
||||||
doc: &Document,
|
doc: &Document,
|
||||||
@@ -130,17 +145,17 @@ impl Run {
|
|||||||
how: How,
|
how: How,
|
||||||
read: impl FnOnce(&mut Self, &Document, &Object) -> Result<Object, crate::RedactError>,
|
read: impl FnOnce(&mut Self, &Document, &Object) -> Result<Object, crate::RedactError>,
|
||||||
) -> Result<Rc<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;
|
let mut fetched: Option<Arc<Object>> = None;
|
||||||
for _ in 0..=MAX_HOPS {
|
for _ in 0..=MAX_HOPS {
|
||||||
let Some(r) = fetched.as_deref().unwrap_or(v).as_ref() else { break };
|
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 {
|
for k in keys {
|
||||||
self.reduced.insert(k, hit.clone());
|
self.reduced.insert(k, hit.clone());
|
||||||
}
|
}
|
||||||
return Ok(hit);
|
return Ok(hit);
|
||||||
}
|
}
|
||||||
keys.push((r.num, how));
|
keys.push((r, how));
|
||||||
fetched = Some(self.fetch(doc, r)?);
|
fetched = Some(self.fetch(doc, r)?);
|
||||||
}
|
}
|
||||||
let made = Rc::new(read(self, doc, fetched.as_deref().unwrap_or(v))?);
|
let made = Rc::new(read(self, doc, fetched.as_deref().unwrap_or(v))?);
|
||||||
@@ -177,7 +192,7 @@ impl Run {
|
|||||||
return Ok((hit, Some(at)));
|
return Ok((hit, Some(at)));
|
||||||
}
|
}
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
fetched = Some(self.fetch(doc, r)?);
|
fetched = Some(self.fetch_numbered(doc, r)?);
|
||||||
}
|
}
|
||||||
self.read += 1;
|
self.read += 1;
|
||||||
let held = fetched.as_deref().unwrap_or(v);
|
let held = fetched.as_deref().unwrap_or(v);
|
||||||
@@ -227,7 +242,7 @@ impl Run {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
found.push(at.num);
|
found.push(at.num);
|
||||||
match &*self.fetch(doc, at)? {
|
match &*self.fetch_numbered(doc, at)? {
|
||||||
Object::Ref(next) => at = *next,
|
Object::Ref(next) => at = *next,
|
||||||
_ => break,
|
_ => 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() },
|
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() }),
|
("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
|
/// 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
|
/// 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
|
/// 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