//! The calculation order (`/CO`, ISO 32000-2 §12.7.3): the calculated fields in the order they //! recalculate, the fields each one's formula reads, and what is wrong with that order. use std::cmp::Reverse; use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; use pdfcraft_cos::{Document, ObjRef, Object}; use crate::{Field, FormError, acroform, fields}; /// At most this many reads between calculated fields are checked. A group name stands for every /// field under it, so a form could otherwise make a check build billions of them. const MAX_LINKS: usize = 1 << 20; /// A calculated field, at its place in the calculation order. #[derive(Clone, Debug, PartialEq)] pub struct CalculatedField { /// The field's full name. pub name: String, /// The fields its formula reads, each once, as the formula names them: a name also stands for /// the fields under it. A script's names built at run time (`getField("row" + i)`) aren't here. pub reads: Vec, /// `/CO` lists it. A field it leaves out calculates after the listed ones. pub listed: bool, /// What is wrong with its place in the order. pub issues: Vec, } /// Something wrong with a calculated field's place in the calculation order. #[derive(Clone, Debug, PartialEq)] pub enum CalculationIssue { /// It reads `field`, which calculates after it, so it uses that field's previous value. RunsTooEarly { field: String }, /// Its formula reads its own result: it reads `fields`, which read it back (itself, when it /// reads itself directly). No order is right for a loop: one of the formulas has to change. Cycle { fields: Vec }, /// `/CO` leaves it out. PdfCraft calculates it after the listed fields, but other PDF viewers /// may not calculate it at all. NotInOrder, } /// A form's calculation order and what is wrong with it. #[derive(Clone, Debug, Default, PartialEq)] pub struct CalculationOrder { /// The calculated fields in the order [`crate::recalculate`] runs them: as `/CO` lists them, /// then the ones it leaves out, in field order. pub fields: Vec, /// `/CO` entries that calculate nothing: a field without a calculation, a second entry for a /// field, or an object that isn't a field (written as `12 0 R`). pub extra: Vec, /// Fix order's order, as indexes into `fields`. fixed: Vec, } impl CalculationOrder { /// The names in the order Fix order writes: each field after the calculated fields it reads, /// keeping the current order wherever that allows. Reads within a loop are left as they are. pub fn fixed(&self) -> Vec { self.fixed.iter().filter_map(|&i| self.fields.get(i)).map(|f| f.name.clone()).collect() } /// Whether Fix order changes anything: a field moves, joins `/CO`, or an extra entry goes. pub fn fixable(&self) -> bool { !self.extra.is_empty() || self.fields.iter().any(|f| !f.listed) || self.fixed.iter().enumerate().any(|(k, &i)| k != i) } } /// Whether a field has a calculation: a Calculate tab formula or a custom Calculate script. pub(crate) fn is_calculated(f: &Field) -> bool { f.actions.calculate != crate::af::Calculate::None || f.actions.scripts.calculate.is_some() } /// The entries of `/CO`, as written. pub(crate) fn co_entries(doc: &Document) -> Vec { acroform(doc).and_then(|af| af.get(b"CO").map(|c| doc.resolve(c)).and_then(|c| c.as_array().cloned())).unwrap_or_default() } /// Keep `/CO` in step with whether field `obj` calculates: a field that gets a calculation joins /// the end of the order, one that loses it leaves, and one whose calculation changes keeps its /// place, so editing a formula doesn't undo an order set by hand. pub(crate) fn keep_listed(doc: &mut Document, obj: ObjRef, calculates: bool) -> Result<(), FormError> { let mut co = co_entries(doc); match (co.iter().any(|o| o.as_ref() == Some(obj)), calculates) { (false, true) => co.push(Object::Ref(obj)), (true, false) => co.retain(|o| o.as_ref() != Some(obj)), _ => return Ok(()), } write_co(doc, co) } /// Write `/CO` (none when `co` is empty), creating the form if needed. fn write_co(doc: &mut Document, co: Vec) -> Result<(), FormError> { let af_ref = crate::author::ensure_form(doc)?; doc.update_dict(af_ref, |d| { if co.is_empty() { d.remove(b"CO"); } else { d.set(b"CO".to_vec(), Object::Array(co)); } })?; Ok(()) } /// The calculated fields in the order they recalculate, as indexes into `fields` paired with /// whether `/CO` lists them, and the `/CO` entries that calculate nothing. pub(crate) fn run_order(doc: &Document, fields: &[Field]) -> (Vec<(usize, bool)>, Vec) { let mut by_obj: HashMap = HashMap::with_capacity(fields.len()); for (i, f) in fields.iter().enumerate() { by_obj.entry(f.obj).or_insert(i); } let mut placed = HashSet::new(); let (mut run, mut extra) = (Vec::new(), Vec::new()); for entry in co_entries(doc) { let field = entry.as_ref().and_then(|r| by_obj.get(&r).copied()).and_then(|i| Some((i, fields.get(i)?))); match field { Some((i, f)) if is_calculated(f) && placed.insert(i) => run.push((i, true)), Some((_, f)) => extra.push(f.name.clone()), None => extra.push(entry.as_ref().map_or_else(|| "not a reference".to_owned(), |r| r.to_string())), } } run.extend(fields.iter().enumerate().filter(|(i, f)| is_calculated(f) && !placed.contains(i)).map(|(i, _)| (i, false))); (run, extra) } #[cfg(test)] thread_local! { /// Read names compared while merging a field's reads. pub(crate) static NAMES_COMPARED: std::cell::Cell = const { std::cell::Cell::new(0) }; } /// The form's calculation order (`fields` as [`fields`] reads them), with each calculated field's /// reads and issues, and Fix order's order. pub fn calculation_order(doc: &Document, fields: &[Field]) -> CalculationOrder { let (run, extra) = run_order(doc, fields); let calculated: Vec<(&Field, bool)> = run.iter().filter_map(|&(i, listed)| Some((fields.get(i)?, listed))).collect(); let reads: Vec> = calculated .iter() .map(|(f, _)| { let mut names = f.actions.calculate.reads(); if let Some(js) = &f.actions.scripts.calculate { let mut known: HashSet = names.iter().cloned().collect(); for name in crate::af::calculation_reads(js) { #[cfg(test)] NAMES_COMPARED.with(|n| n.set(n.get() + 1)); if known.insert(name.clone()) { names.push(name); } } } names }) .collect(); let links = links(calculated.iter().map(|(f, _)| f.name.as_str()), &reads); let component = components(&links); let name = |k: usize| calculated.get(k).map(|(f, _)| f.name.clone()).unwrap_or_default(); let mut out = Vec::with_capacity(calculated.len()); for (k, ((f, listed), reads)) in calculated.iter().zip(reads).enumerate() { let own = component.get(k).copied(); let mut issues = Vec::new(); let read = links.get(k).map(Vec::as_slice).unwrap_or_default(); for &j in read { if j > k && component.get(j).copied() != own { issues.push(CalculationIssue::RunsTooEarly { field: name(j) }); } } // Only the loop's fields it reads itself are named, so a loop through thousands of fields // isn't listed again for each of them. let looped: Vec = read.iter().filter(|&&j| component.get(j).copied() == own).map(|&j| name(j)).collect(); if !looped.is_empty() { issues.push(CalculationIssue::Cycle { fields: looped }); } if !listed { issues.push(CalculationIssue::NotInOrder); } out.push(CalculatedField { name: f.name.clone(), reads, listed: *listed, issues }); } let fixed = fixed_order(&links, &component); CalculationOrder { fields: out, extra, fixed } } /// For each calculated field (by its place in `names`), the places of the calculated fields it /// reads, sorted. A read name finds the field of that name and the fields under it, as /// [`crate::recalculate`] looks values up. fn links<'a>(names: impl Iterator, reads: &[Vec]) -> Vec> { let mut sorted: Vec<(&str, usize)> = names.enumerate().map(|(k, n)| (n, k)).collect(); sorted.sort_unstable(); let mut budget = MAX_LINKS; reads .iter() .map(|names| { let mut out = Vec::new(); for name in names { let prefix = format!("{name}."); let exact = sorted.partition_point(|(n, _)| *n < name.as_str()); let under = sorted.partition_point(|(n, _)| *n < prefix.as_str()); let same = sorted.get(exact..).unwrap_or_default().iter().take_while(|(n, _)| n == name); let below = sorted.get(under..).unwrap_or_default().iter().take_while(|(n, _)| n.starts_with(&prefix)); for &(_, k) in same.chain(below) { if budget == 0 { break; } budget -= 1; out.push(k); } } out.sort_unstable(); out.dedup(); out }) .collect() } /// The strongly connected components of `links` (Tarjan's algorithm, without recursion): one /// component number per node. Fields in one component read each other in a loop. fn components(links: &[Vec]) -> Vec { const UNSEEN: usize = usize::MAX; let n = links.len(); let (mut index, mut low, mut component) = (vec![UNSEEN; n], vec![0; n], vec![UNSEEN; n]); let mut on_stack = vec![false; n]; let (mut stack, mut work): (Vec, Vec<(usize, usize)>) = (Vec::new(), Vec::new()); let (mut next, mut count) = (0, 0); // Every index below is a node: `links` only holds places below `n` (see `links`). for root in 0..n { if index[root] != UNSEEN { continue; } work.push((root, 0)); while let Some((v, e)) = work.pop() { if e == 0 { index[v] = next; low[v] = next; next += 1; stack.push(v); on_stack[v] = true; } if let Some(&w) = links[v].get(e) { work.push((v, e + 1)); if index[w] == UNSEEN { work.push((w, 0)); } else if on_stack[w] { low[v] = low[v].min(index[w]); } continue; } if low[v] == index[v] { while let Some(w) = stack.pop() { on_stack[w] = false; component[w] = count; if w == v { break; } } count += 1; } if let Some(&(parent, _)) = work.last() { low[parent] = low[parent].min(low[v]); } } } component } /// Fix order: every node after the nodes it reads in other components, the earliest ready node /// first, so an order that is already right stays as it is and otherwise only what must move /// moves. Reads within a component (a loop) are left out: no order satisfies them. fn fixed_order(links: &[Vec], component: &[usize]) -> Vec { let n = links.len(); let mut waits = vec![0usize; n]; let mut readers: Vec> = vec![Vec::new(); n]; for (k, read) in links.iter().enumerate() { for &j in read { if component.get(j) != component.get(k) && let (Some(w), Some(r)) = (waits.get_mut(k), readers.get_mut(j)) { *w += 1; r.push(k); } } } let mut ready: BinaryHeap> = waits.iter().enumerate().filter(|(_, w)| **w == 0).map(|(k, _)| Reverse(k)).collect(); let mut out = Vec::with_capacity(n); while let Some(Reverse(k)) = ready.pop() { out.push(k); for &r in readers.get(k).map(Vec::as_slice).unwrap_or_default() { if let Some(w) = waits.get_mut(r) { *w = w.saturating_sub(1); if *w == 0 { ready.push(Reverse(r)); } } } } // Without the reads inside loops no loop is left, so every node is placed; kept total anyway. if out.len() < n { let placed: HashSet = out.iter().copied().collect(); out.extend((0..n).filter(|k| !placed.contains(k))); } out } /// Set the calculation order (Acrobat: Set Field Calculation Order): `names` calculate first, in /// that order, then the calculated fields left out, in their current order. `/CO` is written /// afresh, so it lists every calculated field once and nothing else. /// /// Two calculated fields can share a full name in a malformed form, and the order then names /// both: a repeated name stands for the next of them in the current order, so the current order /// sets back unchanged. pub fn set_calculation_order(doc: &mut Document, names: &[String]) -> Result<(), FormError> { let all = fields(doc); let (run, _) = run_order(doc, &all); let mut by_name: HashMap<&str, VecDeque> = HashMap::with_capacity(run.len()); for &(i, _) in &run { if let Some(f) = all.get(i) { by_name.entry(f.name.as_str()).or_default().push_back(i); } } let mut chosen = HashSet::new(); let mut co = Vec::with_capacity(names.len()); for name in names { let Some(next) = by_name.get_mut(name.as_str()) else { return Err(if all.iter().any(|f| f.name == *name) { FormError::Invalid(format!("{name} has no calculation")) } else { FormError::NoSuchField(name.clone()) }); }; let i = next.pop_front().ok_or_else(|| FormError::Invalid(format!("{name} is in the order twice")))?; if let Some(f) = all.get(i) { chosen.insert(i); co.push(Object::Ref(f.obj)); } } co.extend(run.iter().filter(|(i, _)| !chosen.contains(i)).filter_map(|&(i, _)| all.get(i)).map(|f| Object::Ref(f.obj))); write_co(doc, co) } #[cfg(test)] mod tests { use super::*; /// Fix order on `links` (node `k` reads `links[k]`), as places. fn fix(links: &[Vec]) -> Vec { fixed_order(links, &components(links)) } #[test] fn loops_are_found_without_recursion() { // 0 ↔ 1, 2 reads itself, 3 reads 0, 4 alone. let c = components(&[vec![1], vec![0], vec![2], vec![0], vec![]]); assert_eq!(c[0], c[1]); assert!(c[2] != c[0] && c[3] != c[0] && c[4] != c[0] && c[3] != c[4]); // A chain a million fields long is one pass, not a million nested calls. let n = 1_000_000; let chain: Vec> = (0..n).map(|k| if k + 1 < n { vec![k + 1] } else { vec![0] }).collect(); let c = components(&chain); assert!(c.iter().all(|x| *x == c[0]), "one loop through every field"); } #[test] fn fix_order_moves_only_what_must_move() { // Already right: unchanged. assert_eq!(fix(&[vec![], vec![0], vec![1], vec![]]), [0, 1, 2, 3]); // 0 reads 2: it moves after 2, the rest keep their order. assert_eq!(fix(&[vec![2], vec![], vec![], vec![]]), [1, 2, 0, 3]); // A chain written backwards comes out forwards. assert_eq!(fix(&[vec![1], vec![2], vec![]]), [2, 1, 0]); // A loop (0 ↔ 1) keeps its order; 2, which reads it, goes after it. assert_eq!(fix(&[vec![1], vec![0], vec![]]), [0, 1, 2]); assert_eq!(fix(&[vec![], vec![2], vec![1]]), [0, 1, 2]); assert_eq!(fix(&[vec![1, 2], vec![0], vec![]]), [1, 2, 0], "a loop member still waits for what it reads outside the loop"); // Reading itself is a loop, not a reason to move. assert_eq!(fix(&[vec![0], vec![]]), [0, 1]); } #[test] fn a_name_finds_the_fields_under_it() { let names = ["row.1", "row", "row.10", "rows", "row-1", "total"]; let reads = vec![vec![], vec![], vec![], vec![], vec![], vec!["row".to_owned(), "missing".to_owned(), "row".to_owned()]]; assert_eq!(links(names.into_iter(), &reads)[5], [0, 1, 2], "row, row.1 and row.10; not rows or row-1"); } /// Two thousand formulas each reading a group of two thousand fields would be four million /// reads; the checks stop at their cap. #[test] fn reads_of_a_large_group_are_capped() { let n = 2000; let names: Vec = (0..n).map(|i| format!("g.{i}")).chain((0..n).map(|i| format!("r{i}"))).collect(); let reads: Vec> = (0..2 * n).map(|i| if i < n { Vec::new() } else { vec!["g".to_owned()] }).collect(); let total: usize = links(names.iter().map(String::as_str), &reads).iter().map(Vec::len).sum(); assert_eq!(total, MAX_LINKS); } proptest::proptest! { /// Fix order is a reordering of the fields after which none reads a field that calculates /// later, except within a loop; an order that is already right comes back unchanged. #[test] fn fix_order_leaves_nothing_too_early(links in proptest::collection::vec(proptest::collection::vec(0usize..12, 0..4), 1..12)) { let n = links.len(); let links: Vec> = links.into_iter().map(|mut l| { l.retain(|j| *j < n); l.sort_unstable(); l.dedup(); l }).collect(); let component = components(&links); // Components are exactly the nodes that reach each other. let reach = |from: usize| { let mut seen = vec![false; n]; let mut todo = vec![from]; while let Some(v) = todo.pop() { for &w in &links[v] { if !seen[w] { seen[w] = true; todo.push(w); } } } seen }; let reaches: Vec> = (0..n).map(reach).collect(); for a in 0..n { for b in 0..n { if a != b { proptest::prop_assert_eq!(component[a] == component[b], reaches[a][b] && reaches[b][a]); } } } let fixed = fixed_order(&links, &component); let mut sorted = fixed.clone(); sorted.sort_unstable(); proptest::prop_assert_eq!(sorted, (0..n).collect::>(), "a reordering"); let mut at = vec![0; n]; for (place, &k) in fixed.iter().enumerate() { at[k] = place; } for (k, read) in links.iter().enumerate() { for &j in read { proptest::prop_assert!(component[j] == component[k] || at[j] < at[k], "{k} reads {j}: {fixed:?}"); } } // Fixed once, the order needs no more moves. let renumbered: Vec> = fixed.iter().map(|&k| { let mut l: Vec = links[k].iter().map(|&j| at[j]).collect(); l.sort_unstable(); l }).collect(); proptest::prop_assert_eq!(fix(&renumbered), (0..n).collect::>()); } } }