mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-15 07:12:52 +00:00
fix(forms): a deleted field leaves the calculation order
Deleting a field took it off its pages and out of the field tree but left its entry in the form's /CO array. That reference kept the field dictionary, its value and its widgets reachable, so a full save after applying redactions over the field still wrote the redacted value.
This commit is contained in:
1 parent
3e968c8aa2
commit
f42fb0ad49
3 files changed
+54
No files matched your search
@@ -1280,6 +1280,31 @@ pub fn delete_field(doc: &mut Document, name: &str) -> Result<(), FormError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Out of the calculation order too: a `/CO` entry would keep the field, its value and its
|
||||
// widgets reachable for a full save.
|
||||
drop_from_calculation_order(doc, |r| r == f.obj || widgets.contains(&r))
|
||||
}
|
||||
|
||||
/// Remove the `/CO` entries `gone` matches, whether the form and the array are held directly or
|
||||
/// by reference.
|
||||
fn drop_from_calculation_order(doc: &mut Document, gone: impl Fn(ObjRef) -> bool) -> Result<(), FormError> {
|
||||
let Some(root) = doc.root() else { return Ok(()) };
|
||||
let Some(af_obj) = doc.get(root).as_dict().and_then(|d| d.get(b"AcroForm").cloned()) else { return Ok(()) };
|
||||
let Some(mut af) = doc.resolve(&af_obj).as_dict().cloned() else { return Ok(()) };
|
||||
let Some(co_obj) = af.get(b"CO").cloned() else { return Ok(()) };
|
||||
let co = doc.resolve(&co_obj).as_array().cloned().unwrap_or_default();
|
||||
let kept: Vec<Object> = co.iter().filter(|o| !o.as_ref().is_some_and(&gone)).cloned().collect();
|
||||
if kept.len() == co.len() {
|
||||
return Ok(());
|
||||
}
|
||||
match (co_obj.as_ref(), af_obj.as_ref()) {
|
||||
(Some(r), _) if doc.get(r).as_array().is_some() => doc.set(r, Object::Array(kept)),
|
||||
(_, Some(af_ref)) => doc.update_dict(af_ref, |d| d.set(b"CO".to_vec(), Object::Array(kept)))?,
|
||||
(_, None) => {
|
||||
af.set(b"CO".to_vec(), Object::Array(kept));
|
||||
doc.update_dict(root, |d| d.set(b"AcroForm".to_vec(), Object::Dict(af)))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2462,6 +2462,7 @@ fn unused_descriptions_in_shared_or_inherited_resources_are_retired() {
|
||||
}
|
||||
|
||||
mod property_retirement;
|
||||
mod surviving_references;
|
||||
|
||||
/// `[(FIRSTARR)] [(LASTARR)] TJ` is malformed (a surplus array operand), but the operator still
|
||||
/// draws only the one immediately before it (LASTARR, 7 glyphs, 35 pt wide from x = 10) -- not
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//! Redacted content must leave the file: no other reference may keep it reachable by a full save.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Whether `secret` survives a full save of `doc`, in the file's bytes or in a decoded stream.
|
||||
fn full_save_holds(doc: &Document, secret: &[u8]) -> bool {
|
||||
let bytes = write_full(doc, &SaveOptions::default()).unwrap();
|
||||
let found = |data: &[u8]| data.windows(secret.len()).any(|w| w == secret);
|
||||
if found(&bytes) {
|
||||
return true;
|
||||
}
|
||||
let saved = Document::open(Arc::new(bytes)).unwrap();
|
||||
saved.scan_objects().into_iter().any(|(_, o)| matches!(&*o, Object::Stream(s) if s.decoded().is_ok_and(|d| found(&d))))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_redacted_field_listed_in_the_calculation_order_leaves_the_saved_file() {
|
||||
let mut doc = one_page(b"", "", vec![]);
|
||||
pdfcraft_forms::add_field(&mut doc, 0, [10.0, 50.0, 100.0, 70.0], &pdfcraft_forms::NewField::Text { multiline: false }, Some("ssn")).unwrap();
|
||||
pdfcraft_forms::set_value(&mut doc, "ssn", &pdfcraft_forms::FieldValue::Text("SSN-078-05-1120".into())).unwrap();
|
||||
let field = pdfcraft_forms::fields(&doc).swap_remove(0).obj;
|
||||
let root = doc.root().unwrap();
|
||||
let af = doc.get(root).as_dict().unwrap().reference(b"AcroForm").unwrap();
|
||||
doc.update_dict(af, |d| d.set(b"CO".to_vec(), Object::Array(vec![Object::Ref(field)]))).unwrap();
|
||||
mark(&mut doc, 0, &[[0.0, 40.0, 120.0, 80.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().fields, 1);
|
||||
assert!(!full_save_holds(&doc, b"SSN-078-05-1120"), "the field's value is still reachable");
|
||||
}
|
||||
Reference in new issue
Block a user