fix(forms): align has_password_flag traversal depth with field enumeration (#4570)

* fix(forms): align has_password_flag traversal depth with field enumeration

* fix(forms): extend field_chain depth to 64 and fix test formatting
This commit is contained in:
Dylan M. Taylor
2026-10-24 21:07:09 -04:00
committed by GitHub
parent f4f91d0d5c
commit 4c69b8cf88
2 changed files with 63 additions and 4 deletions
+4 -4
View File
@@ -1315,7 +1315,7 @@ fn password_fields(doc: &Document) -> Vec<Field> {
/// again only makes the answer cautious. It reads only `/Ff`, `/Kids` and `/Parent`, so a save
/// or recovery snapshot of a form without such a field doesn't build every [`Field`]. Like
/// [`fields`], it looks at the form's field tree and at the widgets on the pages, which may be
/// missing from `/Fields`. Each dictionary is visited once and at most 32 levels deep.
/// missing from `/Fields`. Each dictionary is visited once and at most 64 levels deep.
fn has_password_flag(doc: &Document) -> bool {
let sets_it = |r: ObjRef| doc.get(r).as_dict().and_then(|d| field_flags(doc, d)).is_some_and(|ff| ff & flags::PASSWORD != 0);
let top = acroform(doc).and_then(|af| af.get(b"Fields").map(|o| doc.resolve(o)).and_then(|o| o.as_array().cloned())).unwrap_or_default();
@@ -1323,7 +1323,7 @@ fn has_password_flag(doc: &Document) -> bool {
let mut seen = std::collections::HashSet::new();
while let Some((o, depth)) = stack.pop() {
let Object::Ref(r) = o else { continue };
if depth > 32 || !seen.insert(r) {
if depth > 64 || !seen.insert(r) {
continue;
}
if sets_it(r) {
@@ -1363,11 +1363,11 @@ pub fn restore_password_values(doc: &mut Document, values: &[(String, String)])
Ok(())
}
/// `r` and its field ancestors (`/Parent`), nearest first, at most 32 deep and without repeats.
/// `r` and its field ancestors (`/Parent`), nearest first, at most 64 deep and without repeats.
fn field_chain(doc: &Document, r: ObjRef) -> Vec<ObjRef> {
let mut chain = vec![r];
let mut cur = r;
while chain.len() < 32 {
while chain.len() < 64 {
let Some(Object::Ref(parent)) = doc.get(cur).as_dict().and_then(|d| d.get(b"Parent").cloned()) else { break };
if chain.contains(&parent) {
break;
+59
View File
@@ -4088,6 +4088,65 @@ fn a_password_field_missing_from_the_field_list_is_still_cleared() {
}
}
/// A password field nested beyond depth 32 (e.g. depth 35) is recognized by `has_password_flag`
/// and `has_password_values`, matching the field tree walk limit of 64.
#[test]
fn a_password_field_nested_beyond_depth_32_is_recognized() {
let target_depth = 35;
let leaf_obj_num = 5 + target_depth;
let mut objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
"<< /Type /Page /Parent 2 0 R >>".into(),
"<< /Fields [5 0 R] /DA (/Helv 0 Tf 0 g) >>".into(),
];
for d in 0..target_depth {
let obj_num = 5 + d;
let next_obj_num = obj_num + 1;
let parent_entry = if d == 0 { String::new() } else { format!(" /Parent {} 0 R", obj_num - 1) };
objs.push(format!("<< /T (node{d}) /Kids [{next_obj_num} 0 R]{parent_entry} >>"));
}
objs.push(format!(
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (pass) /Ff 8192 /V (secret35) /Parent {} 0 R /Rect [50 600 150 620] /P 3 0 R >>",
leaf_obj_num - 1
));
let mut doc = document(&objs);
assert!(has_password_flag(&doc));
assert!(has_password_values(&doc));
assert_eq!(clear_password_values(&mut doc).unwrap(), 1);
assert!(!has_password_values(&doc));
}
/// A password field whose widget is reachable through a page's Annots and whose inherited Password
/// flag is deeper than 32 levels is recognized by `has_password_flag`.
#[test]
fn a_page_widget_inheriting_password_flag_beyond_depth_32_is_recognized() {
let target_depth = 35;
let leaf_obj_num = 5 + target_depth;
let mut objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
format!("<< /Type /Page /Parent 2 0 R /Annots [{leaf_obj_num} 0 R] >>"),
"<< /Fields [5 0 R] /DA (/Helv 0 Tf 0 g) >>".into(),
];
for d in 0..target_depth {
let obj_num = 5 + d;
let next_obj_num = obj_num + 1;
let parent_entry = if d == 0 { String::new() } else { format!(" /Parent {} 0 R", obj_num - 1) };
let ff_entry = if d == 0 { " /Ff 8192" } else { "" };
objs.push(format!("<< /T (node{d}){ff_entry} /Kids [{next_obj_num} 0 R]{parent_entry} >>"));
}
objs.push(format!(
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (pass) /V (secret_chain) /Parent {} 0 R /Rect [50 600 150 620] /P 3 0 R >>",
leaf_obj_num - 1
));
let mut doc = document(&objs);
assert!(has_password_flag(&doc));
assert!(has_password_values(&doc));
assert_eq!(clear_password_values(&mut doc).unwrap(), 1);
assert!(!has_password_values(&doc));
}
/// #4018: a value that doesn't fit its box at the field's font size is reported (it is drawn cut
/// at the box). An auto-sized field shrinks to fit, and a short or empty value fits.
#[test]