M6: form_set_props refuses a notation it couldn't store as written

A notation is kept in the script's /** BVCALC … EVCALC **/ comment and read
back from there. One holding EVCALC was stored cut short ("a EVCALC b"
read back as "a", and its JavaScript translation didn't parse), one
holding */ ended the comment, and an empty one became "event.value = ;".
form_set_props now refuses each (af::notation_storable: it has to read back
as itself). The test also pins a notation beside op alone and beside
fields alone (from the review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
This commit is contained in:
Grzegorz Martens
2026-10-29 20:48:38 +01:00
co-authored by Claude Opus 5.5
parent fefb99a786
commit fd3634cc38
5 changed files with 31 additions and 2 deletions
+5 -1
View File
@@ -1,7 +1,7 @@
//! Form tools: list fields, fill them (several at once, one undo step), clear the form, and
//! prepare a form (add fields one at a time or as a table, change and delete them).
use pdfcraft_engine::form_scripts::{CalcOp, Calculate, Format, Validate};
use pdfcraft_engine::form_scripts::{CalcOp, Calculate, Format, Validate, notation_storable};
use pdfcraft_engine::{CalculationIssue, Edit, FieldProps, FieldValue, FormField, FormFieldKind, NewField, field_flags};
use serde_json::{Value, json};
@@ -134,6 +134,10 @@ fn calculate_arg(v: &Value) -> Result<Calculate> {
return Err(bad("calculate takes a notation, or op and fields, not both"));
}
let n = n.as_str().ok_or_else(|| bad("notation must be a string such as \"Price * Qty\""))?;
// Refused rather than stored as a different formula.
if !notation_storable(n) {
return Err(bad("the notation can't be stored as written: it is empty, or holds EVCALC or */, which would end it early"));
}
return Ok(Calculate::Notation(n.to_string()));
}
let op = get("op").ok_or_else(|| bad("calculate needs op and fields, or a notation"))?;
+6
View File
@@ -5172,6 +5172,12 @@ fn calculate_and_validate_refuse_what_they_would_drop() {
};
for (calculate, says) in [
(json!({ "notation": "a", "op": "sum", "fields": ["b"] }), "not both"),
(json!({ "notation": "a", "op": "sum" }), "not both"),
(json!({ "notation": "a", "fields": ["b"] }), "not both"),
// A notation that wouldn't read back as itself from the script's comment.
(json!({ "notation": "a EVCALC b" }), "can't be stored"),
(json!({ "notation": "a */ x /*" }), "can't be stored"),
(json!({ "notation": " " }), "can't be stored"),
(json!({ "op": "sum", "fields": ["a", 5] }), "field names"),
(json!({ "op": "sum", "fields": "a" }), "list of field names"),
(json!({ "notation": 5 }), "notation must be a string"),
+1 -1
View File
@@ -22,7 +22,7 @@ use calc::{Notation, eval_notation, notation_name, notation_tokens, unique};
pub use calc::{calculate, calculation_reads, notation_js, reads_built_names, rename_in_calculation};
pub use format::{DATE_PRESETS, DateTime, TIME_PRESETS, format_date, format_value, js_number, keystroke, make_number, parse_date, validate};
use script::{Arg, call};
pub use script::{calculate_js, format_js, parse_calculate, parse_format, parse_validate, validate_js};
pub use script::{calculate_js, format_js, notation_storable, parse_calculate, parse_format, parse_validate, validate_js};
/// The Format tab.
#[derive(Clone, Debug, PartialEq, Default)]
+9
View File
@@ -325,6 +325,15 @@ pub fn validate_js(v: &Validate) -> Option<String> {
}
}
/// Whether simplified field notation `expr` can be stored: the script keeps it in a comment,
/// between `BVCALC` and `EVCALC`, and it has to read back from there as itself. An empty one, or
/// one holding `EVCALC` or `*/` (either ends it early), can't.
pub fn notation_storable(expr: &str) -> bool {
!expr.trim().is_empty()
&& calculate_js(&Calculate::Notation(expr.to_owned())).and_then(|js| parse_calculate(&js))
== Some(Calculate::Notation(expr.trim().to_owned()))
}
pub fn calculate_js(c: &Calculate) -> Option<String> {
match c {
Calculate::None => None,
+10
View File
@@ -217,6 +217,16 @@ fn hostile_scripts_are_read_in_one_pass() {
assert_eq!(rename_in_calculation(&nested, "a", "b"), Some(format!("{}\"b\"", "AFSimple_Calculate(\"SUM\", new Array(".repeat(100_000))));
}
/// A notation is stored only when it reads back from the script's comment as itself.
#[test]
fn a_notation_is_stored_only_when_it_reads_back() {
assert!(notation_storable("Price * Qty"));
assert!(notation_storable(" a + b "), "the spaces around it are left out when it is read");
for expr in ["", " ", "a EVCALC b", "a */ x /*", "a */ b"] {
assert!(!notation_storable(expr), "{expr:?}");
}
}
/// Notation is read through one lexer: field names with escapes, numbers and operators.
#[test]
fn notation_is_read_by_one_lexer() {