fix(xfdf): read an indirect /F when deciding a comment is locked against import

This commit is contained in:
aiongg
2026-10-22 18:02:33 +00:00
parent c2e96de0b2
commit 8adf8e2d3b
2 changed files with 27 additions and 3 deletions
+4 -3
View File
@@ -1037,8 +1037,9 @@ const LOCKED: i64 = 128;
/// An existing annotation an import must never replace, move or unlock: one the user locked, a
/// redaction mark (replacing it would drop the region the user marked to remove), or a form
/// widget or field (replacing it would set the field's value and policy around the form setter).
fn import_protected(x: &Dict) -> bool {
x.int(b"F").unwrap_or(0) & LOCKED != 0 || matches!(x.name(b"Subtype"), Some(b"Redact" | b"Widget")) || x.contains(b"FT")
fn import_protected(doc: &Document, x: &Dict) -> bool {
let flags = x.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0);
flags & LOCKED != 0 || matches!(x.name(b"Subtype"), Some(b"Redact" | b"Widget")) || x.contains(b"FT")
}
fn place(doc: &mut Document, page_ref: ObjRef, mut d: Dict) -> Result<ObjRef, DataError> {
@@ -1089,7 +1090,7 @@ fn place(doc: &mut Document, page_ref: ObjRef, mut d: Dict) -> Result<ObjRef, Da
for a in annots {
let existing = a.as_ref().and_then(|r| doc.get(r).as_dict().cloned());
let same = nm.is_some() && existing.as_ref().and_then(|x| x.get(b"NM").and_then(|n| doc.resolve(n).as_string().map(|s| s.to_text()))) == nm;
if same && existing.as_ref().is_some_and(import_protected) {
if same && existing.as_ref().is_some_and(|x| import_protected(doc, x)) {
clash = true;
kept.push(a);
} else if same {
+23
View File
@@ -1842,3 +1842,26 @@ fn xfdf_line_ending_names_are_escaped() {
let want = String::from_utf8(hostile).unwrap();
assert_eq!((el.attribute("head"), el.attribute("tail")), (Some(want.as_str()), Some(want.as_str())));
}
/// A comment locked through an indirect `/F` is as protected from a same-named import as one
/// with the flag written inline.
#[test]
fn a_comment_locked_by_an_indirect_flag_is_not_replaced_by_an_import() {
let mut doc = blank();
let page = pdfcraft_model::pages(&doc)[0].obj;
let flag = doc.add(Object::Int(128)); // Locked
let mut note = Dict::new();
note.set(b"Type".to_vec(), Object::name("Annot"));
note.set(b"Subtype".to_vec(), Object::name("Text"));
note.set(b"NM".to_vec(), PdfString::text("n1"));
note.set(b"Rect".to_vec(), Object::Array(vec![10.into(), 10.into(), 30.into(), 30.into()]));
note.set(b"F".to_vec(), Object::Ref(flag));
let note = doc.add(Object::Dict(note));
doc.update_dict(page, |p| p.set(b"Annots".to_vec(), Object::Array(vec![Object::Ref(note)]))).unwrap();
let before = doc.get(note);
let xfdf = br#"<xfdf><annots><text page="0" name="n1" rect="0,0,1,1"/></annots></xfdf>"#;
assert_eq!(import(&mut doc, xfdf).unwrap().comments, 1);
let annots = doc.get(page).as_dict().and_then(|p| p.get(b"Annots").and_then(Object::as_array).cloned()).unwrap();
assert!(annots.contains(&Object::Ref(note)), "the locked comment was replaced");
assert_eq!(doc.get(note), before, "the locked comment was changed");
}