mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 13:22:42 +00:00
address review: clear direct annotation actions and count unlisted widget fields
This commit is contained in:
@@ -486,6 +486,7 @@ pub fn scan(doc: &Document) -> Vec<(Hidden, usize)> {
|
||||
let pages = pdfcraft_model::pages(doc);
|
||||
let annots: Vec<Dict> = pages.iter().flat_map(|p| annots_of(doc, &p.dict)).filter_map(|a| doc.resolve(&a).as_dict().cloned()).collect();
|
||||
let count_sub = |s: &[u8]| annots.iter().filter(|a| a.name(b"Subtype") == Some(s)).count();
|
||||
let listed = listed_annotations(doc);
|
||||
let names = sub(doc, &cat, b"Names").unwrap_or_default();
|
||||
let owned_keys: Vec<&[u8]> = OWNED.iter().map(|o| o.1).collect();
|
||||
// A graph nested too deeply to walk counts as holding everything, so the removal runs and
|
||||
@@ -528,9 +529,10 @@ pub fn scan(doc: &Document) -> Vec<(Hidden, usize)> {
|
||||
+ usize::from(cat.get(b"OpenAction").is_some_and(|a| doc.resolve(a).as_dict().is_some()))
|
||||
+ owned(Hidden::LinksActionsScripts)
|
||||
+ annots.iter().filter(|a| primary_action_only(a)).count()
|
||||
// fields no page lists (a listed widget is counted with the annotations above)
|
||||
+ field_dicts(doc)
|
||||
.into_iter()
|
||||
.filter(|&r| doc.get(r).as_dict().is_some_and(|d| !d.contains(b"Subtype") && primary_action_only(d)))
|
||||
.filter(|r| !listed.contains(r) && doc.get(*r).as_dict().is_some_and(primary_action_only))
|
||||
.count()
|
||||
+ sub(doc, &names, b"JavaScript").map_or(0, |t| name_tree_len(doc, &t))
|
||||
}
|
||||
@@ -601,9 +603,18 @@ pub fn remove_hidden(doc: &mut Document, which: &[Hidden]) -> Result<Vec<(Hidden
|
||||
let mut kept = Vec::new();
|
||||
for a in list {
|
||||
let Some(r) = a.as_ref() else {
|
||||
if !doc.resolve(&a).as_dict().is_some_and(drop_annot) {
|
||||
kept.push(a);
|
||||
let mut a = a;
|
||||
if doc.resolve(&a).as_dict().is_some_and(drop_annot) {
|
||||
continue;
|
||||
}
|
||||
// a direct annotation holds its own actions: removed from the entry kept in the array
|
||||
if which.contains(&Hidden::LinksActionsScripts)
|
||||
&& let Object::Dict(d) = &mut a
|
||||
{
|
||||
d.remove(b"AA");
|
||||
d.remove(b"A");
|
||||
}
|
||||
kept.push(a);
|
||||
continue;
|
||||
};
|
||||
let d = doc.get(r).as_dict().cloned().unwrap_or_default();
|
||||
|
||||
@@ -3922,3 +3922,40 @@ fn screen_and_field_primary_actions_are_removed() {
|
||||
let saved = write_full(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(!saved.windows(14).any(|w| w == b"SCRIPT_PAYLOAD"), "the script is still in the file");
|
||||
}
|
||||
|
||||
/// A direct annotation dictionary in `/Annots` holds its own action: removal clears it there, not
|
||||
/// only from annotations that are indirect objects.
|
||||
#[test]
|
||||
fn removing_actions_clears_a_direct_annotation_action() {
|
||||
use crate::sanitize::{Hidden, remove_hidden, scan};
|
||||
let objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Annots [<< /Type /Annot /Subtype /Screen /Rect [10 10 50 50] /A << /S /JavaScript /JS (app.alert\\(1\\)) >> >>] >>"
|
||||
.to_vec(),
|
||||
stream("", b"BT ET"),
|
||||
];
|
||||
let mut doc = pdf(objs);
|
||||
let counts: HashMap<Hidden, usize> = scan(&doc).into_iter().collect();
|
||||
assert!(counts[&Hidden::LinksActionsScripts] > 0, "{counts:?}");
|
||||
remove_hidden(&mut doc, &[Hidden::LinksActionsScripts]).unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
assert!(!bytes.windows(10).any(|w| w == b"JavaScript"), "the direct annotation's action survived the save");
|
||||
}
|
||||
|
||||
/// A widget field no page lists, with `/Subtype`, still counts for the actions scan when it holds
|
||||
/// an action, as removal clears it.
|
||||
#[test]
|
||||
fn an_unlisted_widget_field_action_is_counted() {
|
||||
use crate::sanitize::{Hidden, scan};
|
||||
let objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] >> >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R >>".to_vec(),
|
||||
stream("", b"BT ET"),
|
||||
b"<< /Type /Annot /Subtype /Widget /FT /Btn /T (b) /Rect [0 0 10 10] /A << /S /JavaScript /JS (x) >> >>".to_vec(),
|
||||
];
|
||||
let doc = pdf(objs);
|
||||
let counts: HashMap<Hidden, usize> = scan(&doc).into_iter().collect();
|
||||
assert!(counts[&Hidden::LinksActionsScripts] > 0, "{counts:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user