fix(engine): require modify for console field-property changes and optimizing, copy for structure bookmarks, refuse read-only image fields

This commit is contained in:
Dylan M. Taylor
2026-11-03 11:44:32 +00:00
parent 14ae839a28
commit 9f1cdef896
4 changed files with 51 additions and 2 deletions
+5 -2
View File
@@ -27,7 +27,6 @@ pub(crate) fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Re
| Edit::SetBookmarkPage { .. }
| Edit::SetBookmarkColor { .. }
| Edit::SetBookmarkStyle { .. }
| Edit::BookmarksFromStructure
| Edit::NumberPages { .. } => {
if p.assemble() {
Ok(())
@@ -37,7 +36,7 @@ pub(crate) fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Re
}
// Creates bookmarks (assembly), and their titles are the document's text, copied out of it
// (Table 22, bit 5), as an export of it would be.
Edit::BookmarksFromTextStyles { .. } => {
Edit::BookmarksFromTextStyles { .. } | Edit::BookmarksFromStructure => {
if !p.assemble() {
Err(EditError::NotPermitted("page changes"))
} else if !p.copy() {
@@ -594,6 +593,10 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E
ran.map_err(EditError::Invalid)?;
}
Edit::SetFieldImage { name, image } => {
// As filling a value does: a read-only field keeps its picture.
if pdfcraft_forms::fields(doc).iter().any(|f| f.name == *name && f.read_only()) {
return Err(pdfcraft_forms::FormError::ReadOnly(name.clone()).into());
}
let (img, _) = embed_image(doc, name, image)?;
let px = match &*doc.get(img) {
pdfcraft_cos::Object::Stream(s) => (s.dict.int(b"Width").unwrap_or(1) as u32, s.dict.int(b"Height").unwrap_or(1) as u32),
+6
View File
@@ -298,6 +298,12 @@ impl Session {
};
let mut o = pdfcraft_js::run(script, &event, &runner.doc, &states, &runner.doc_scripts, Limits::default());
let changes = changes(&states, &o.changed);
// Console input is the user's, not the document's: making a field read-only, required,
// hidden or shown is a change to the form (Table 22, bit 4), which filling it in is not.
let properties = |c: &FieldChange| c.read_only.is_some() || c.required.is_some() || c.display.is_some();
if target.is_none() && changes.iter().any(properties) && !doc.allows_modification() {
return Err(EditError::NotPermitted("changes to the document"));
}
let resets: Vec<Vec<String>> = o
.requests
.iter()
+4
View File
@@ -64,6 +64,10 @@ impl Session {
return Err(EditError::Signed);
}
let editor = doc.editor.as_ref().ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?;
// A rewrite that takes data out of the document, as Remove Hidden Information does.
if !doc.allows_modification() {
return Err(EditError::NotPermitted("changes to the document"));
}
Ok(OptimizeJob {
cos: editor.cos.clone(),
settings: settings.clone(),
+36
View File
@@ -10251,3 +10251,39 @@ fn image_transform_engine_refuses_singular_matrices_without_mutation() {
assert_eq!(s.get(id).unwrap().bytes, before);
}
}
/// Table 22, bit 4 (modify) withheld while forms may be filled in: the console's script may
/// set values, but not make a field read-only, required or hidden, which the property editor
/// refuses too; optimizing (a rewrite that removes data) is refused; bookmarks from the
/// structure copy the headings' text out, so they need copying (bit 5) as the text-style
/// bookmarks do.
#[test]
fn fill_only_documents_refuse_property_changes_from_the_console_and_optimizing() {
let encrypt = |bytes: Vec<u8>, permissions: i32| {
let mut doc = pdfcraft_cos::Document::open(Arc::new(bytes)).unwrap();
doc.set_encryption(&pdfcraft_cos::NewEncryption {
algorithm: pdfcraft_cos::Algorithm::Aes256,
user_password: "",
owner_password: "owner",
permissions,
encrypt_metadata: true,
seed: [7; 32],
})
.unwrap();
Arc::new(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap())
};
let refused = Err(EditError::NotPermitted("changes to the document"));
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, encrypt(scripted_form(), -1 ^ 0b1000), None).unwrap();
assert!(s.get(id).unwrap().allows_form_filling() && !s.get(id).unwrap().allows_modification());
assert_eq!(s.run_javascript(id, "getField('total').readonly = true;", None).map(|_| ()), refused);
assert_eq!(s.run_javascript(id, "getField('total').display = display.hidden;", None).map(|_| ()), refused);
assert!(!s.get(id).unwrap().form.iter().find(|f| f.name == "total").unwrap().read_only(), "nothing changed");
s.run_javascript(id, "getField('price').value = 3;", None).unwrap();
assert_eq!(s.get(id).unwrap().form.iter().find(|f| f.name == "price").unwrap().value, ["3"], "values may be filled in");
assert_eq!(s.optimize_job(id, &optimize::Settings::default(), &[]).map(|_| ()), refused);
let id = s.open("tagged.pdf", None, encrypt(tagged_headings_fixture(), -1 ^ 0b1_0000), None).unwrap();
assert_eq!(s.apply(id, Edit::BookmarksFromStructure), Err(EditError::NotPermitted("copying text and images")));
assert!(s.get(id).unwrap().info.outline.is_empty());
}