M11: PDF/A verify reads a Named action's name through a reference (6.5.1)

A Named action whose /N refers to /NextPage (or another page move) is
allowed and survives Save as PDF/A; the test checks that conversion
keeps such an action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-11-05 10:00:38 +08:00
1 parent 33cd765828
commit d9abc9b82c
2 files changed
+23 -4

No files matched your search

+6 -4
View File
@@ -770,7 +770,7 @@ fn walk_chain(doc: &Document, a: &Dict, read: &mut usize, seen: &mut std::collec
if *read > MAX_ACTION_CHAIN {
return Some("too deeply chained".into());
}
if let Some(s) = action_forbidden(d) {
if let Some(s) = action_forbidden(doc, d) {
return Some(s);
}
match d.get(b"Next") {
@@ -789,13 +789,15 @@ fn walk_chain(doc: &Document, a: &Dict, read: &mut usize, seen: &mut std::collec
}
/// The forbidden kind of the action `d` itself, if it is one: a forbidden type, or a Named
/// action for anything but the four of [`NAMED_ACTIONS`].
fn action_forbidden(d: &Dict) -> Option<String> {
/// action for anything but the four of [`NAMED_ACTIONS`], its name given directly or by
/// reference.
fn action_forbidden(doc: &Document, d: &Dict) -> Option<String> {
let s = d.name(b"S")?;
if FORBIDDEN_ACTIONS.contains(&s) {
return Some(String::from_utf8_lossy(s).into_owned());
}
match d.name(b"N") {
let n = d.get(b"N").map(|n| doc.resolve(n));
match n.as_deref().and_then(Object::as_name) {
_ if s != b"Named" => None,
Some(n) if NAMED_ACTIONS.contains(&n) => None,
Some(n) => Some(format!("Named {}", String::from_utf8_lossy(n))),
+17
View File
@@ -501,6 +501,7 @@ fn set_state_no_op_and_named_actions_are_checked() {
"<< /Type /Pages /Kids [] /Count 0 >>",
&outline,
"<< /Type /Outlines /First 3 0 R /Last 3 0 R /Count 1 >>",
"/NextPage",
],
"",
)
@@ -517,10 +518,26 @@ fn set_state_no_op_and_named_actions_are_checked() {
for allowed in ["NextPage", "PrevPage", "FirstPage", "LastPage"] {
assert!(actions(&format!("<< /S /Named /N /{allowed} >>")).is_empty(), "{allowed}");
}
// The name given by reference (object 5 is /NextPage).
assert!(actions("<< /S /Named /N 5 0 R >>").is_empty());
// Save as PDF/A removes them.
for action in ["<< /S /SetState /T [] >>", "<< /S /NOP >>", "<< /S /Named /N /Print >>"] {
let mut doc = doc_with(action);
convert(&mut doc, Level::A2b).unwrap();
assert!(verify(&doc, Level::A2b).iter().all(|i| i.clause != "6.5.1"), "{action}");
}
// A permitted Named action survives it, also one naming its page move by reference.
for action in ["<< /S /Named /N /NextPage >>", "<< /S /Named /N 5 0 R >>"] {
let mut doc = doc_with(action);
convert(&mut doc, Level::A2b).unwrap();
let outline = doc.get(ObjRef { num: 3, generation: 0 });
let kept = outline
.as_dict()
.and_then(|d| d.get(b"A"))
.and_then(Object::as_dict)
.map(|a| (a.name(b"S").map(<[u8]>::to_vec), a.get(b"N").map(|n| doc.resolve(n))));
let (s, n) = kept.unwrap_or_else(|| panic!("{action}: the action is gone"));
assert_eq!(s.as_deref(), Some(&b"Named"[..]), "{action}");
assert_eq!(n.as_deref().and_then(Object::as_name), Some(&b"NextPage"[..]), "{action}");
}
}