1use std::collections::{BTreeSet, HashMap};
22use std::sync::Arc;
23
24use crate::base::{CellRef, RangeRef, Value};
25use crate::formula::adjust::Edit;
26use crate::model::{Cell, CompareOp, InputKind, MAX_MACRO_REF, Macro, OnConflict, RepeatOf, Sheet, Step, StepKind, Workbook};
27use serde::Serialize;
28use serde_json::{Value as Json, json};
29
30use super::policy::{Policy, macro_policy, previewable, writes_file};
31use super::resolve::{self, Env, Place, ResolveError};
32use crate::engine::{EngineError, Result, Selection, Session, Trust, UiRequest};
33
34pub const MAX_EXECUTED_STEPS: u64 = 100_000;
36pub const DEFAULT_TIME_LIMIT_MS: f64 = 60_000.0;
39pub const MAX_INPUT_CELLS: u64 = 1_000_000;
41pub const MAX_PARAM_BYTES: u64 = 256 * 1024 * 1024;
44const MAX_LISTED_RANGES: usize = 500;
46
47pub type Bound = (String, Place, bool);
49
50#[derive(Clone, Debug, PartialEq, Serialize)]
52pub struct StepError {
53 pub step: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub command: Option<String>,
57 pub message: String,
58 #[serde(skip_serializing_if = "std::ops::Not::not")]
60 pub edge: bool,
61 #[serde(skip_serializing_if = "std::ops::Not::not")]
63 pub confirm: bool,
64 #[serde(skip_serializing_if = "Vec::is_empty")]
67 pub wrote_files: Vec<String>,
68}
69
70impl StepError {
71 fn new(step: &str, command: Option<&str>, message: impl Into<String>) -> StepError {
72 StepError {
73 step: step.to_string(),
74 command: command.map(str::to_string),
75 message: message.into(),
76 edge: false,
77 confirm: false,
78 wrote_files: Vec::new(),
79 }
80 }
81
82 fn resolve(step: &str, command: Option<&str>, e: ResolveError) -> StepError {
83 let edge = matches!(e, ResolveError::Edge(_));
84 StepError { edge, ..StepError::new(step, command, e.message()) }
85 }
86
87 pub fn describe(&self) -> String {
89 match &self.command {
90 Some(c) => format!("Step {} ({c}): {}", self.step, self.message),
91 None => format!("Step {}: {}", self.step, self.message),
92 }
93 }
94
95 pub fn into_engine(self, name: &str) -> EngineError {
96 let text = format!("Macro '{name}' stopped. {} {}", self.describe(), rolled_back(&self.wrote_files));
97 if self.confirm { EngineError::Confirm(text) } else { EngineError::Other(text) }
98 }
99}
100
101pub fn rolled_back(wrote_files: &[String]) -> String {
103 match wrote_files {
104 [] => "Nothing was changed.".into(),
105 [one] => format!("The workbook was put back; the file {one} wrote stays."),
106 many => format!("The workbook was put back; the files {} wrote stay.", join_and(many)),
107 }
108}
109
110fn json_bytes(v: &Json) -> u64 {
112 match v {
113 Json::String(t) => t.len() as u64 + 2,
114 Json::Array(a) => a.iter().map(json_bytes).fold(2, u64::saturating_add),
115 Json::Object(o) => o.iter().map(|(k, v)| (k.len() as u64 + 3).saturating_add(json_bytes(v))).fold(2, u64::saturating_add),
116 _ => 8,
117 }
118}
119
120pub fn gate(m: &Macro, trust: Trust) -> Result<Vec<String>> {
125 let mut asks = BTreeSet::new();
126 let mut err = None;
127 walk_enabled(&m.steps, "", &mut |path, id, params| {
128 if err.is_some() {
129 return;
130 }
131 match macro_policy(id, params, trust) {
132 Policy::Allow => {}
133 Policy::Ask(why) => {
134 asks.insert(why);
135 }
136 Policy::NeedsPermission(what) => {
137 if m.permissions.iter().any(|p| p == id) {
138 asks.insert(format!("runs {id}, which {what}"));
139 } else {
140 err = Some(format!("Step {path} ({id}) {what}. Add {id} to the macro's permissions to allow it."));
141 }
142 }
143 Policy::Deny(why) => err = Some(format!("Step {path} ({id}) can't run in a macro: {why}.")),
144 }
145 });
146 match err {
147 Some(e) => Err(EngineError::Other(format!("Macro '{}' can't run. {e}", m.name))),
148 None => Ok(asks.into_iter().collect()),
149 }
150}
151
152fn walk_enabled(steps: &[Step], prefix: &str, f: &mut impl FnMut(&str, &str, &Json)) {
154 for (i, st) in steps.iter().enumerate() {
155 if !st.enabled {
156 continue;
157 }
158 let path = child_path(prefix, i);
159 match &st.kind {
160 StepKind::Command { id, params } => f(&path, id, params),
161 StepKind::Repeat { body, .. } => walk_enabled(body, &path, f),
162 StepKind::If { then, otherwise, .. } => {
163 walk_enabled(then, &format!("{path}.then"), f);
164 walk_enabled(otherwise, &format!("{path}.else"), f);
165 }
166 StepKind::Stop { .. } => {}
167 }
168 }
169}
170
171fn child_path(prefix: &str, i: usize) -> String {
172 if prefix.is_empty() { (i + 1).to_string() } else { format!("{prefix}.{}", i + 1) }
173}
174
175pub fn approve(s: &mut Session, m: &Macro, doc: u64, asks: &[String], approve: bool) -> Result<()> {
178 if asks.is_empty() {
179 return Ok(());
180 }
181 let key = (doc, super::fingerprint(m));
182 if s.macro_approvals.contains(&key) {
183 return Ok(());
184 }
185 if approve {
186 s.macro_approvals.insert(key);
187 return Ok(());
188 }
189 Err(EngineError::Approve(format!("Run macro '{}'? It {}.", m.name, join_and(asks))))
190}
191
192fn join_and(items: &[String]) -> String {
193 match items {
194 [] => String::new(),
195 [one] => one.clone(),
196 [init @ .., last] => format!("{} and {last}", init.join(", ")),
197 }
198}
199
200#[derive(Clone, Debug)]
204pub struct Prepared {
205 pub mac: Macro,
206 pub doc: u64,
207 pub trust: Trust,
208 pub inputs: Vec<Bound>,
209 pub output: Option<Place>,
210}
211
212impl Prepared {
213 pub fn inputs_json(&self, wb: &Workbook) -> Json {
214 let env = Env { wb, active_sheet: wb.active_sheet(), inputs: &[], output: None, item: None };
215 Json::Object(self.inputs.iter().map(|(n, p, _)| (n.clone(), json!(resolve::a1(*p, &env)))).collect())
216 }
217}
218
219pub fn prepare(s: &Session, m: &Macro, given: Option<&Json>) -> Result<Prepared> {
222 let d = s.doc()?;
223 let wb = &d.wb;
224 let mut inputs: Vec<Bound> = Vec::new();
225 let mut problems = Vec::new();
226 for (i, input) in m.inputs.iter().enumerate() {
227 let named = given.and_then(Json::as_object).and_then(|o| o.iter().find(|(k, _)| k.eq_ignore_ascii_case(&input.name)).map(|(_, v)| v));
228 let text = named.and_then(Json::as_str).or_else(|| if i == 0 { given.and_then(Json::as_str) } else { None }).or(input.default_ref.as_deref());
229 let place = match text {
230 Some(t) => {
231 let env = Env { wb, active_sheet: wb.active_sheet(), inputs: &inputs, output: None, item: None };
232 match resolve::resolve_ref(t, &env) {
233 Ok(p) => p,
234 Err(e) => {
235 problems.push(format!("{}: {}", input.name, e.message()));
236 continue;
237 }
238 }
239 }
240 None if i == 0 => Place { sheet: wb.active_sheet(), range: d.selection.current() },
241 None => {
242 problems.push(format!("{0} needs a range: pass inputs: {{\"{0}\": \"A1:B5\"}}.", input.name));
243 continue;
244 }
245 };
246 let place = trim(wb, place);
247 problems.extend(check_input(wb, input, place));
248 inputs.push((input.name.clone(), place, input.header));
249 }
250 if !problems.is_empty() {
251 return Err(EngineError::Other(format!("Macro '{}' can't run on these inputs. {}", m.name, problems.join(" "))));
252 }
253 let output = match &m.output.anchor {
254 Some(a) if !a.trim().is_empty() => {
255 let env = Env { wb, active_sheet: wb.active_sheet(), inputs: &inputs, output: None, item: None };
256 Some(
257 resolve::resolve_ref(a, &env)
258 .map_err(|e| EngineError::Other(format!("Macro '{}': its output anchor {}", m.name, lower_first(e.message()))))?,
259 )
260 }
261 _ => None,
262 };
263 Ok(Prepared { mac: m.clone(), doc: d.uid, trust: d.trust, inputs, output })
264}
265
266fn lower_first(s: &str) -> String {
267 let mut c = s.chars();
268 match c.next() {
269 Some(f) => f.to_lowercase().chain(c).collect(),
270 None => String::new(),
271 }
272}
273
274fn trim(wb: &Workbook, p: Place) -> Place {
276 if !(p.range.is_full_cols() || p.range.is_full_rows()) {
277 return p;
278 }
279 let used = wb.sheet(p.sheet).and_then(Sheet::used_range);
280 let range = used.and_then(|u| p.range.intersection(&u)).unwrap_or(RangeRef::cell(p.range.start));
281 Place { sheet: p.sheet, range }
282}
283
284fn check_input(wb: &Workbook, input: &crate::model::NamedInput, place: Place) -> Vec<String> {
286 let mut out = Vec::new();
287 let r = place.range;
288 let name = &input.name;
289 let at = r.a1();
290 if input.kind == InputKind::Cell && !r.is_single() {
291 out.push(format!("{name} must be a single cell; {at} is {} cells.", r.count()));
292 return out;
293 }
294 let header = u32::from(input.header && r.height() > 0);
295 let rows = r.height().saturating_sub(header);
296 let between = |what: &str, n: u32, [min, max]: [u32; 2]| -> Option<String> {
297 let plural = |k: u32| if k == 1 { what.to_string() } else { format!("{what}s") };
298 if max > 0 && min == max && n != min {
299 Some(format!("exactly {min} {}", plural(min)))
300 } else if n < min {
301 Some(format!("at least {min} {}", plural(min)))
302 } else if max > 0 && n > max {
303 Some(format!("at most {max} {}", plural(max)))
304 } else {
305 None
306 }
307 };
308 if let Some(want) = between("data row", rows, input.rows) {
309 out.push(format!("{name} needs {want}; {at} has {rows}."));
310 }
311 if let Some(want) = between("column", r.width(), input.cols) {
312 out.push(format!("{name} needs {want}; {at} has {}.", r.width()));
313 }
314 if r.count() > MAX_INPUT_CELLS {
315 out.push(format!("{name} is too large to check ({} cells; at most {}).", r.count(), MAX_INPUT_CELLS));
316 return out;
317 }
318 let Some(sh) = wb.sheet(place.sheet) else { return out };
319 let mut blanks = Vec::new();
320 for (ci, col) in (r.start.col..=r.end.col).enumerate() {
321 let ty = input.col_types.get(ci).copied().unwrap_or_default();
322 let mut wrong = Vec::new();
323 for row in r.start.row + header..=r.end.row {
324 let c = CellRef::new(row, col);
325 let v = sh.value(c);
326 if v.is_empty() || v.as_text() == Some("") {
327 if input.non_empty {
328 blanks.push(c);
329 }
330 continue;
331 }
332 let date = || wb.styles.get(sh.style_id(c)).num_fmt.as_str().to_string();
333 let ok = match ty {
334 crate::model::ColType::Any => true,
335 crate::model::ColType::Number => v.is_number(),
336 crate::model::ColType::Text => v.is_text(),
337 crate::model::ColType::Bool => matches!(v, Value::Bool(_)),
338 crate::model::ColType::Date => v.is_number() && crate::engine::display::number_format(&date()).is_date(),
339 };
340 if !ok {
341 wrong.push((c, holds(&v, ty)));
342 }
343 }
344 if !wrong.is_empty() {
345 let head = if input.header {
346 Some(crate::engine::display::cell_text(wb, sh, CellRef::new(r.start.row, col))).filter(|h| !h.trim().is_empty())
347 } else {
348 None
349 };
350 let column = match head {
351 Some(h) => format!("{name} column {} ({})", ci + 1, h.trim()),
352 None => format!("{name} column {}", ci + 1),
353 };
354 let listed: Vec<String> = wrong.iter().take(3).map(|(c, what)| format!("{} holds {what}", c.a1())).collect();
355 let more = if wrong.len() > 3 { format!(" and {} more cells don't", wrong.len() - 3) } else { String::new() };
356 out.push(format!("{column} must hold {}; {}{more}.", ty.noun(), listed.join(", ")));
357 }
358 }
359 if let Some(first) = blanks.first() {
360 let more = if blanks.len() > 1 { format!(" (and {} more)", blanks.len() - 1) } else { String::new() };
361 out.push(format!("{name} must have no blank cells; {} is blank{more}.", first.a1()));
362 }
363 out
364}
365
366fn holds(v: &Value, wanted: crate::model::ColType) -> &'static str {
368 match v {
369 Value::Number(_) if wanted == crate::model::ColType::Date => "a number that isn't a date",
370 Value::Number(_) => "a number",
371 Value::Text(_) => "text",
372 Value::Bool(_) => "TRUE or FALSE",
373 Value::Error(_) => "an error",
374 Value::Array(_) => "an array",
375 Value::Empty => "nothing",
376 }
377}
378
379#[derive(Clone, Copy, Debug)]
383pub struct Stopwatch {
384 #[cfg(not(target_arch = "wasm32"))]
385 start: std::time::Instant,
386}
387
388impl Stopwatch {
389 pub fn start() -> Stopwatch {
390 #[cfg(not(target_arch = "wasm32"))]
391 #[allow(clippy::disallowed_methods)] let w = Stopwatch { start: std::time::Instant::now() };
393 #[cfg(target_arch = "wasm32")]
394 let w = Stopwatch {};
395 w
396 }
397
398 fn elapsed_ms(&self) -> Option<f64> {
400 #[cfg(not(target_arch = "wasm32"))]
401 let ms = Some(self.start.elapsed().as_secs_f64() * 1000.0);
402 #[cfg(target_arch = "wasm32")]
403 let ms = None;
404 ms
405 }
406}
407
408#[derive(Clone, Debug)]
409struct Frame {
410 steps: Arc<Vec<Step>>,
411 next: usize,
412 prefix: String,
415 repeat: Option<Loop>,
416}
417
418#[derive(Clone, Copy, Debug)]
419struct Loop {
420 of: RepeatOf,
421 over: Place,
422 at: u32,
424 count: u32,
425}
426
427impl Loop {
428 fn item(&self) -> Place {
429 let r = self.over.range;
430 let range = match self.of {
431 RepeatOf::Rows => {
432 let row = r.start.row.saturating_add(self.at);
433 RangeRef::new(CellRef::new(row, r.start.col), CellRef::new(row, r.end.col))
434 }
435 RepeatOf::Columns => {
436 let col = r.start.col.saturating_add(self.at);
437 RangeRef::new(CellRef::new(r.start.row, col), CellRef::new(r.end.row, col))
438 }
439 };
440 Place { sheet: self.over.sheet, range }
441 }
442}
443
444#[derive(Clone, Debug)]
446pub struct Machine {
447 pub name: String,
448 pub doc: u64,
449 pub inputs: Vec<Bound>,
450 pub output: Option<Place>,
451 permissions: Vec<String>,
452 trust: Trust,
453 frames: Vec<Frame>,
454 pub executed: u64,
456 clock: Stopwatch,
457 pub time_limit_ms: f64,
458 pub dry_run: bool,
461 pub stopped: Option<(String, bool)>,
463 pub partial: Option<(String, String)>,
465 pub last_params: Option<Json>,
467 pub wrote_files: Vec<String>,
469 bodies: HashMap<String, Arc<Vec<Step>>>,
472 param_bytes: u64,
474 guard: Option<Guard>,
477}
478
479#[derive(Clone, Debug)]
481struct Guard {
482 start: Arc<Workbook>,
485 origin: Vec<Option<usize>>,
486 partial: Option<String>,
488 ask: bool,
490}
491
492impl Guard {
493 fn new(start: Arc<Workbook>, partial: Option<&Json>, ask: bool) -> Guard {
494 let partial = partial.map(|p| {
495 let text = |k: &str| p.get(k).and_then(Json::as_str).unwrap_or_default().to_string();
496 format!("step {} ({})", text("step"), text("reason"))
497 });
498 Guard { origin: (0..start.sheets.len()).map(Some).collect(), start, partial, ask }
499 }
500
501 fn check(&mut self, prev: &Workbook, after: &Workbook, inputs: &[Bound]) -> String {
504 let (pairs, _) = pair_sheets(prev, after);
505 let mut origin = vec![None; after.sheets.len()];
506 let mut parts = Vec::new();
507 let mut objects = 0usize;
508 for (pi, ai) in pairs {
509 let Some(si) = self.origin.get(pi).copied().flatten() else { continue };
511 let Some(ss) = self.start.sheets.get(si) else { continue };
512 let Some(ai) = ai else {
513 parts.push(format!("the sheet {}", ss.name));
514 continue;
515 };
516 if let Some(o) = origin.get_mut(ai) {
517 *o = Some(si);
518 }
519 let (Some(ps), Some(sa)) = (prev.sheets.get(pi), after.sheets.get(ai)) else { continue };
520 if Arc::ptr_eq(ps, sa) {
521 continue;
522 }
523 let in_input = |c: CellRef| inputs.iter().any(|(_, p, _)| p.sheet == ai && p.range.contains(c));
525 let mut hit: Vec<CellRef> = ps
526 .cells
527 .diff(&sa.cells)
528 .into_iter()
529 .filter(|&c| !in_input(c) && !recalculated_only(ps.cell(c), sa.cell(c)))
530 .filter(|&c| ss.cell(c).is_some_and(has_content) && content_differs(ss.cell(c), sa.cell(c)))
531 .collect();
532 if !hit.is_empty() {
533 let mut listed = ranges(&mut hit);
534 if listed.len() > 3 {
535 listed.truncate(3);
536 listed.push("…".into());
537 }
538 parts.push(format!("{} on {}", listed.join(", "), sa.name));
539 }
540 objects += deleted(&ss.charts, &ps.charts, &sa.charts, |o| o.id)
541 + deleted(&ss.images, &ps.images, &sa.images, |o| o.id)
542 + deleted(&ss.shapes, &ps.shapes, &sa.shapes, |o| o.id)
543 + deleted(&ss.controls, &ps.controls, &sa.controls, |o| o.id);
544 }
545 if objects > 0 {
546 parts.push(format!("{objects} object{}", if objects == 1 { "" } else { "s" }));
547 }
548 self.origin = origin;
549 parts.join("; ")
550 }
551}
552
553fn deleted<T>(start: &[T], prev: &[T], after: &[T], id: impl Fn(&T) -> u32) -> usize {
555 let kept: BTreeSet<u32> = after.iter().map(&id).collect();
556 let had: BTreeSet<u32> = start.iter().map(&id).collect();
557 prev.iter().map(&id).filter(|i| !kept.contains(i) && had.contains(i)).count()
558}
559
560impl Machine {
561 pub fn new(p: &Prepared, dry_run: bool) -> Machine {
562 let mut m = Machine {
563 name: p.mac.name.clone(),
564 doc: p.doc,
565 inputs: p.inputs.clone(),
566 output: p.output,
567 permissions: p.mac.permissions.clone(),
568 trust: p.trust,
569 frames: vec![Frame { steps: Arc::new(p.mac.steps.clone()), next: 0, prefix: String::new(), repeat: None }],
570 executed: 0,
571 clock: Stopwatch::start(),
572 time_limit_ms: DEFAULT_TIME_LIMIT_MS,
573 dry_run,
574 stopped: None,
575 partial: None,
576 last_params: None,
577 wrote_files: Vec::new(),
578 bodies: HashMap::new(),
579 param_bytes: 0,
580 guard: None,
581 };
582 m.settle();
583 m
584 }
585
586 fn settle(&mut self) {
588 while let Some(f) = self.frames.last_mut() {
589 if let Some(st) = f.steps.get(f.next) {
590 if st.enabled {
591 return;
592 }
593 f.next += 1;
594 continue;
595 }
596 if let Some(l) = f.repeat.as_mut()
597 && l.at.saturating_add(1) < l.count
598 {
599 l.at += 1;
600 f.next = 0;
601 continue;
602 }
603 self.frames.pop();
604 }
605 }
606
607 pub fn is_done(&self) -> bool {
608 self.frames.is_empty()
609 }
610
611 pub fn restart_clock(&mut self) {
613 self.clock = Stopwatch::start();
614 }
615
616 pub fn current_json(&self, wb: &Workbook) -> Json {
619 let Some((path, st)) = self.current() else { return Json::Null };
620 let env = self.env(wb);
621 let mut out = json!({"step": path, "breakpoint": st.breakpoint});
622 if !st.note.is_empty() {
623 out["note"] = json!(st.note);
624 }
625 match &st.kind {
626 StepKind::Command { id, params } => {
627 out["kind"] = json!("command");
628 out["command"] = json!(id);
629 match resolve::resolve_params(params, &env) {
630 Ok(p) => out["params"] = p,
631 Err(e) => out["paramsError"] = json!(e.message()),
632 }
633 }
634 StepKind::Repeat { over, of, .. } => {
635 out["kind"] = json!("repeat");
636 out["over"] =
638 json!(resolve::resolve_ref(over, &env).map(|p| resolve::a1(trim(wb, p), &env)).unwrap_or_else(|e| e.message().to_string()));
639 out["of"] = json!(if *of == RepeatOf::Rows { "rows" } else { "columns" });
640 }
641 StepKind::If { cell, .. } => {
642 out["kind"] = json!("if");
643 out["cell"] = json!(resolve::resolve_ref(cell, &env).map(|p| resolve::a1(p, &env)).unwrap_or_else(|e| e.message().to_string()));
644 }
645 StepKind::Stop { message, .. } => {
646 out["kind"] = json!("stop");
647 out["message"] = json!(message);
648 }
649 }
650 if let Some((at, count)) = self.iteration() {
651 out["iteration"] = json!({"at": at, "of": count});
652 }
653 out
654 }
655
656 pub fn current(&self) -> Option<(String, &Step)> {
658 let f = self.frames.last()?;
659 f.steps.get(f.next).map(|st| (child_path(&f.prefix, f.next), st))
660 }
661
662 pub fn iteration(&self) -> Option<(u32, u32)> {
664 self.frames.iter().rev().find_map(|f| f.repeat).map(|l| (l.at + 1, l.count))
665 }
666
667 fn item(&self) -> Option<(RepeatOf, Place)> {
668 self.frames.iter().rev().find_map(|f| f.repeat).map(|l| (l.of, l.item()))
669 }
670
671 pub fn run_to_end(&mut self, s: &mut Session) -> std::result::Result<(), StepError> {
673 while !self.is_done() {
674 self.step(s)?;
675 }
676 Ok(())
677 }
678
679 pub fn step(&mut self, s: &mut Session) -> std::result::Result<(), StepError> {
681 let Some((path, steps, i)) = self.frames.last().map(|f| (child_path(&f.prefix, f.next), Arc::clone(&f.steps), f.next)) else { return Ok(()) };
683 let Some(st) = steps.get(i) else { return Ok(()) };
684 if let Some(f) = self.frames.last_mut() {
685 f.next += 1;
686 }
687 let mut r = self.exec(s, &path, st);
688 if let Err(e) = r.as_mut() {
689 e.wrote_files.clone_from(&self.wrote_files);
690 self.frames.clear();
691 }
692 self.settle();
693 r
694 }
695
696 fn exec(&mut self, s: &mut Session, path: &str, st: &Step) -> std::result::Result<(), StepError> {
697 self.executed += 1;
698 let at = |msg: String| StepError::new(path, None, msg);
699 if self.executed > MAX_EXECUTED_STEPS {
700 return Err(at(format!("The macro ran more than {MAX_EXECUTED_STEPS} steps and was stopped.")));
701 }
702 if self.clock.elapsed_ms().is_some_and(|ms| ms > self.time_limit_ms) {
703 return Err(at(format!("The macro ran longer than {} seconds and was stopped.", (self.time_limit_ms / 1000.0).round())));
704 }
705 let Some(i) = s.docs.iter().position(|d| d.uid == self.doc) else { return Err(at("The workbook was closed.".into())) };
707 s.set_active(i);
708 match &st.kind {
709 StepKind::Command { id, params } => self.command(s, path, id, params),
710 StepKind::Repeat { over, of, max, body } => {
711 let place = {
712 let wb = &s.doc().map_err(|e| at(e.to_string()))?.wb;
713 let env = self.env(wb);
714 let p = resolve::resolve_ref(over, &env).map_err(|e| StepError::resolve(path, None, e))?;
715 trim(wb, p)
716 };
717 let items = match of {
718 RepeatOf::Rows => place.range.height(),
719 RepeatOf::Columns => place.range.width(),
720 };
721 let cap = (*max).min(MAX_EXECUTED_STEPS as u32);
722 if items > cap {
723 let what = if *of == RepeatOf::Rows { "rows" } else { "columns" };
724 return Err(at(format!("Repeat over {} has {items} {what}, more than its limit of {cap}.", place.range.a1())));
725 }
726 if items > 0 && body.iter().any(|b| b.enabled) {
727 let repeat = Some(Loop { of: *of, over: place, at: 0, count: items });
728 let steps = self.body(path.to_string(), body);
729 self.frames.push(Frame { steps, next: 0, prefix: path.to_string(), repeat });
730 }
731 Ok(())
732 }
733 StepKind::If { cell, op, value, then, otherwise } => {
734 let v = {
735 let wb = &s.doc().map_err(|e| at(e.to_string()))?.wb;
736 let env = self.env(wb);
737 let p = resolve::resolve_ref(cell, &env).map_err(|e| StepError::resolve(path, None, e))?;
738 if !p.range.is_single() {
739 return Err(at(format!("If looks at one cell; {} is {} cells.", p.range.a1(), p.range.count())));
740 }
741 wb.sheet(p.sheet).map(|sh| sh.value(p.range.start)).unwrap_or_default()
742 };
743 let (branch, name) = if compare(&v, *op, value) { (then, "then") } else { (otherwise, "else") };
744 if branch.iter().any(|b| b.enabled) {
745 let prefix = format!("{path}.{name}");
746 let steps = self.body(prefix.clone(), branch);
747 self.frames.push(Frame { steps, next: 0, prefix, repeat: None });
748 }
749 Ok(())
750 }
751 StepKind::Stop { message, rollback } => {
752 self.stopped = Some((message.clone(), *rollback));
753 self.frames.clear();
754 Ok(())
755 }
756 }
757 }
758
759 fn follow(&mut self, before: &Workbook, after: &Workbook, edits: &[(String, Edit)]) -> std::result::Result<(), String> {
764 let same_sheets = before.sheets.len() == after.sheets.len() && before.sheets.iter().zip(&after.sheets).all(|(b, a)| b.name == a.name);
765 if same_sheets && edits.is_empty() {
766 return Ok(());
767 }
768 let (pairs, _) = pair_sheets(before, after);
769 let follow = |what: &str, p: Place| follow_place(before, after, &pairs, edits, p).ok_or_else(|| format!("It deleted the cells of {what}."));
770 for (name, place, _) in self.inputs.iter_mut() {
771 *place = follow(name, *place)?;
772 }
773 if let Some(o) = self.output {
774 self.output = Some(follow("the output anchor", o)?);
775 }
776 for f in self.frames.iter_mut() {
777 let Some(l) = f.repeat.as_mut() else { continue };
778 let over = follow(&format!("the range Repeat {} goes over", f.prefix), l.over)?;
779 if (over.range.height(), over.range.width()) != (l.over.range.height(), l.over.range.width()) {
780 return Err(format!("It changed the size of the range Repeat {} goes over.", f.prefix));
781 }
782 l.over = over;
783 }
784 Ok(())
785 }
786
787 fn body(&mut self, prefix: String, body: &[Step]) -> Arc<Vec<Step>> {
789 Arc::clone(self.bodies.entry(prefix).or_insert_with(|| Arc::new(body.to_vec())))
790 }
791
792 fn env<'a>(&'a self, wb: &'a Workbook) -> Env<'a> {
793 Env { wb, active_sheet: wb.active_sheet(), inputs: &self.inputs, output: self.output, item: self.item() }
794 }
795
796 fn command(&mut self, s: &mut Session, path: &str, id: &str, params: &Json) -> std::result::Result<(), StepError> {
797 let fail = |msg: String| StepError::new(path, Some(id), msg);
798 let resolved = {
800 let wb = &s.doc().map_err(|e| fail(e.to_string()))?.wb;
801 resolve::resolve_params(params, &self.env(wb)).map_err(|e| StepError::resolve(path, Some(id), e))?
802 };
803 match (macro_policy(id, params, self.trust), macro_policy(id, &resolved, self.trust)) {
804 (_, Policy::Deny(why)) | (Policy::Deny(why), _) => return Err(fail(format!("{id} can't run in a macro: {why}."))),
805 (written, Policy::NeedsPermission(what)) => {
806 if !self.permissions.iter().any(|p| p == id) {
807 return Err(fail(format!("{id} {what}. Add it to the macro's permissions to allow it.")));
808 }
809 if !matches!(written, Policy::NeedsPermission(_)) {
811 return Err(fail(format!("{id} {what} only once its parameters are filled in, so it wasn't approved.")));
812 }
813 if self.dry_run {
814 self.partial = Some((path.to_string(), format!("{id} {what}")));
815 self.frames.clear();
816 return Ok(());
817 }
818 }
819 (_, Policy::Allow | Policy::Ask(_)) => {}
820 }
821 if self.dry_run && !previewable(id) {
822 self.partial = Some((path.to_string(), format!("{id} can't be previewed")));
823 self.frames.clear();
824 return Ok(());
825 }
826 self.param_bytes = self.param_bytes.saturating_add(json_bytes(&resolved));
827 if self.param_bytes > MAX_PARAM_BYTES {
828 return Err(fail(format!(
829 "The macro passed more than {} MB of parameters to its steps and was stopped.",
830 MAX_PARAM_BYTES / (1024 * 1024)
831 )));
832 }
833 let file_step = matches!(macro_policy(id, &resolved, self.trust), Policy::NeedsPermission(_));
834 let writes = writes_file(id, &resolved);
835 self.last_params = Some(resolved.clone());
836 let prev = s.docs.iter_mut().find(|d| d.uid == self.doc).map(|d| {
837 d.edits = Some(Vec::new());
838 d.wb.clone()
839 });
840 let queued = s.ui_requests.len();
841 let sandbox = (!file_step).then(crate::engine::io::ScriptSandbox::enter);
844 let r = s.execute(id, resolved);
845 drop(sandbox);
846 let edits = s.docs.iter_mut().find(|d| d.uid == self.doc).and_then(|d| d.edits.take()).unwrap_or_default();
847 let mut blocked = None;
850 let asked: Vec<UiRequest> = s.ui_requests.drain(queued.min(s.ui_requests.len())..).collect();
851 for q in asked {
852 match q {
853 UiRequest::Dialog(name, _) => blocked = Some(format!("{id} would open the {name} dialog; give the step the parameters it needs.")),
854 UiRequest::OpenUrl(_) => blocked = Some(format!("{id} would open a web page, which macros don't do.")),
855 UiRequest::EditCell(_) | UiRequest::InsertName(_) => {}
856 m @ UiRequest::Message(_) => s.ui_requests.push(m),
857 }
858 }
859 r.map_err(|e| fail(e.to_string()))?;
860 if writes {
861 self.wrote_files.push(format!("step {path} ({id})"));
862 }
863 if let Some(b) = blocked {
864 return Err(fail(b));
865 }
866 let after = s.docs.iter().find(|d| d.uid == self.doc).map(|d| d.wb.clone()).ok_or_else(|| fail("The workbook was closed.".into()))?;
867 if let Some(prev) = &prev {
868 self.follow(prev, &after, &edits).map_err(fail)?;
869 }
870 if let (Some(g), Some(prev)) = (self.guard.as_mut(), prev) {
871 let what = g.check(&prev, &after, &self.inputs);
872 if !what.is_empty() {
873 let msg = match &g.partial {
874 Some(at) => format!("It would write over data ({what}); the check before the run stopped at {at} and couldn't see this."),
875 None => format!("It would write over data ({what}); the check before the run didn't see this write."),
877 };
878 return Err(StepError { confirm: g.ask, ..fail(msg) });
879 }
880 }
881 Ok(())
882 }
883}
884
885fn follow_place(before: &Workbook, after: &Workbook, pairs: &[(usize, Option<usize>)], edits: &[(String, Edit)], p: Place) -> Option<Place> {
889 let sheet = pairs.iter().find(|(b, _)| *b == p.sheet).and_then(|(_, a)| *a)?;
890 let mut name = after.sheet(sheet)?.name.clone();
891 let old_name = before.sheet(p.sheet).map(|s| s.name.as_str()).unwrap_or_default();
892 let mut text = format!("{}!{}", resolve::quote_sheet(&name), p.range.a1());
893 for (target, e) in edits {
894 if target != &name && target != old_name {
895 continue;
896 }
897 let expr = crate::formula::parse(&text).ok()?;
898 text = crate::formula::print(&crate::formula::adjust::adjust(expr, target, target, e));
899 if text.contains("#REF!") {
900 return None;
901 }
902 let env = Env { wb: after, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
904 name = after.sheet(resolve::resolve_ref(&text, &env).ok()?.sheet)?.name.clone();
905 }
906 let env = Env { wb: after, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
907 if text.contains('$') {
908 text = text.replace('$', "");
909 }
910 resolve::resolve_ref(&text, &env).ok()
911}
912
913fn compare(v: &Value, op: CompareOp, want: &Json) -> bool {
915 let empty = v.is_empty() || v.as_text() == Some("");
916 let want_text = || match want {
917 Json::String(t) => t.clone(),
918 Json::Null => String::new(),
919 other => other.to_string(),
920 };
921 let ord = || -> Option<std::cmp::Ordering> {
922 let w = match want {
923 Json::Null => Value::Empty,
924 Json::Bool(b) => Value::Bool(*b),
925 Json::Number(n) => Value::number(n.as_f64()?),
926 Json::String(t) => match (v, t.trim().parse::<f64>()) {
928 (Value::Number(_), Ok(n)) => Value::number(n),
929 _ => Value::text(t.as_str()),
930 },
931 _ => return None,
932 };
933 match (v, &w) {
934 (Value::Error(e), Value::Text(t)) => {
935 Some(if e.as_str().eq_ignore_ascii_case(t.trim()) { std::cmp::Ordering::Equal } else { std::cmp::Ordering::Less })
936 }
937 (Value::Error(_), _) | (_, Value::Error(_)) | (Value::Array(_), _) => None,
938 _ => Some(crate::base::value::compare(v, &w)),
939 }
940 };
941 use std::cmp::Ordering::*;
942 match op {
943 CompareOp::Empty => empty,
944 CompareOp::NotEmpty => !empty,
945 CompareOp::Contains => v.display().to_lowercase().contains(&want_text().to_lowercase()),
946 CompareOp::Eq => ord() == Some(Equal),
947 CompareOp::Ne => ord() != Some(Equal),
948 CompareOp::Gt => ord() == Some(Greater),
949 CompareOp::Ge => matches!(ord(), Some(Greater | Equal)),
950 CompareOp::Lt => ord() == Some(Less),
951 CompareOp::Le => matches!(ord(), Some(Less | Equal)),
952 }
953}
954
955#[derive(Clone, Debug, Default, Serialize)]
959#[serde(rename_all = "camelCase")]
960pub struct Footprint {
961 #[serde(skip)]
964 pub executed: u64,
965 #[serde(skip)]
966 pub param_bytes: u64,
967 #[serde(skip)]
969 pub rolls_back: bool,
970 #[serde(skip_serializing_if = "std::ops::Not::not")]
973 pub log_full: bool,
974 pub sheets: Vec<SheetFootprint>,
975 pub sheets_added: Vec<String>,
976 pub sheets_removed: Vec<String>,
977 pub objects_added: usize,
978 pub objects_changed: usize,
979 pub objects_removed: usize,
980 #[serde(skip_serializing_if = "Option::is_none")]
982 pub partial: Option<Json>,
983 #[serde(skip_serializing_if = "Option::is_none")]
985 pub error: Option<StepError>,
986 #[serde(skip_serializing_if = "Option::is_none")]
988 pub stopped: Option<String>,
989}
990
991#[derive(Clone, Debug, Default, Serialize)]
993#[serde(rename_all = "camelCase")]
994pub struct SheetFootprint {
995 pub sheet: String,
996 #[serde(skip)]
997 pub index: usize,
998 pub cells: Vec<String>,
1000 pub changed: usize,
1001 pub conflicts: Vec<String>,
1003 pub conflict_count: usize,
1004 #[serde(skip_serializing_if = "Option::is_none")]
1006 pub bounds: Option<String>,
1007 #[serde(skip)]
1008 pub bounds_range: Option<RangeRef>,
1009}
1010
1011impl Footprint {
1012 pub fn conflict_count(&self) -> usize {
1014 self.sheets.iter().map(|s| s.conflict_count).sum::<usize>() + self.sheets_removed.len() + self.objects_removed
1015 }
1016
1017 pub fn describe_conflicts(&self) -> String {
1019 let mut parts: Vec<String> = self
1020 .sheets
1021 .iter()
1022 .filter(|s| s.conflict_count > 0)
1023 .map(|s| {
1024 let mut listed = s.conflicts.iter().take(3).cloned().collect::<Vec<_>>().join(", ");
1025 if s.conflicts.len() > 3 {
1026 listed.push_str(", …");
1027 }
1028 format!("{listed} on {}", s.sheet)
1029 })
1030 .collect();
1031 parts.extend(self.sheets_removed.iter().map(|n| format!("the sheet {n}")));
1032 if self.objects_removed > 0 {
1033 parts.push(format!("{} object{}", self.objects_removed, if self.objects_removed == 1 { "" } else { "s" }));
1034 }
1035 parts.join("; ")
1036 }
1037}
1038
1039struct Saved {
1042 active: usize,
1043 docs: Vec<u64>,
1044 saved: Option<Arc<Workbook>>,
1047 revision: Option<u64>,
1050 sheet_selections: Vec<Selection>,
1051 unlocked_ranges: Vec<(String, String)>,
1052 trust: Trust,
1053 go_to_recent: Vec<String>,
1054 clipboard: Option<crate::engine::Clipboard>,
1055 prefs: crate::engine::Prefs,
1056 mode: crate::engine::Mode,
1057 journal: usize,
1060 recording: Option<usize>,
1061 ui_requests: usize,
1062 format_painter: Option<(Arc<Workbook>, usize, RangeRef, bool)>,
1063 last_find: Option<Json>,
1064 hide_ruler: bool,
1065 windows: (Vec<crate::engine::WindowState>, Option<u64>, u64, u64),
1066 draw: (String, String, f32),
1067 comments_pane: bool,
1068 show_all_notes: bool,
1069}
1070
1071impl Saved {
1072 fn take(s: &mut Session, doc: u64) -> Saved {
1074 let d = s.docs.iter().find(|d| d.uid == doc);
1075 let saved = Saved {
1076 active: s.active,
1077 docs: s.docs.iter().map(|d| d.uid).collect(),
1078 saved: d.map(|d| d.saved.clone()),
1079 revision: d.map(|d| d.revision),
1080 sheet_selections: d.map(|d| d.sheet_selections.clone()).unwrap_or_default(),
1081 unlocked_ranges: d.map(|d| d.unlocked_ranges.clone()).unwrap_or_default(),
1082 trust: d.map_or(Trust::Local, |d| d.trust),
1083 go_to_recent: d.map(|d| d.go_to_recent.clone()).unwrap_or_default(),
1084 clipboard: s.clipboard.clone(),
1085 prefs: s.prefs.clone(),
1086 mode: s.mode,
1087 journal: s.journal.len(),
1088 recording: s.recording,
1089 ui_requests: s.ui_requests.len(),
1090 format_painter: s.format_painter.clone(),
1091 last_find: s.last_find.clone(),
1092 hide_ruler: s.hide_ruler,
1093 windows: (s.windows.clone(), s.active_window, s.next_window, s.activation),
1094 draw: (s.draw_tool.clone(), s.draw_color.clone(), s.draw_width),
1095 comments_pane: s.comments_pane,
1096 show_all_notes: s.show_all_notes,
1097 };
1098 s.journal_paused = s.journal_paused.saturating_add(1);
1099 saved
1100 }
1101
1102 fn restore(self, s: &mut Session, doc: u64) {
1103 s.docs.retain(|d| self.docs.contains(&d.uid));
1104 if let Some(d) = s.docs.iter_mut().find(|d| d.uid == doc) {
1105 if let Some(saved) = self.saved {
1106 d.saved = saved;
1107 }
1108 if let Some(revision) = self.revision {
1109 d.revision = revision;
1110 }
1111 d.sheet_selections = self.sheet_selections;
1112 d.unlocked_ranges = self.unlocked_ranges;
1113 d.trust = self.trust;
1114 d.go_to_recent = self.go_to_recent;
1115 }
1116 s.active = self.active.min(s.docs.len().saturating_sub(1));
1117 s.clipboard = self.clipboard;
1118 s.prefs = self.prefs;
1119 s.mode = self.mode;
1120 s.truncate_journal(self.journal);
1121 s.recording = self.recording;
1122 s.journal_paused = s.journal_paused.saturating_sub(1);
1123 s.ui_requests.truncate(self.ui_requests);
1124 s.format_painter = self.format_painter;
1125 s.last_find = self.last_find;
1126 s.hide_ruler = self.hide_ruler;
1127 (s.windows, s.active_window, s.next_window, s.activation) = self.windows;
1128 (s.draw_tool, s.draw_color, s.draw_width) = self.draw;
1129 s.comments_pane = self.comments_pane;
1130 s.show_all_notes = self.show_all_notes;
1131 }
1132}
1133
1134pub fn footprint(s: &mut Session, p: &Prepared, run: &Machine) -> Result<Footprint> {
1138 let i = s.docs.iter().position(|d| d.uid == p.doc).ok_or(EngineError::NoDocument)?;
1139 s.set_active(i);
1140 let before = s.doc()?.wb.clone();
1141 let token = s.group_begin("Check Macro")?;
1142 let saved = Saved::take(s, p.doc);
1144 if let Some(d) = s.docs.iter_mut().find(|d| d.uid == p.doc) {
1145 d.touched = Some(Vec::new());
1146 }
1147 let mut m = Machine::new(p, true);
1148 (m.clock, m.time_limit_ms, m.executed, m.param_bytes) = (run.clock, run.time_limit_ms, run.executed, run.param_bytes);
1149 let outcome = m.run_to_end(s);
1150 let (after, touched) = match s.docs.iter_mut().find(|d| d.uid == p.doc) {
1151 Some(d) => (d.wb.clone(), d.touched.take().unwrap_or_default()),
1152 None => (before.clone(), Vec::new()),
1153 };
1154 let rolled = s.group_rollback(token);
1155 saved.restore(s, p.doc);
1156 rolled?;
1157 let (after, touched) = if matches!(m.stopped, Some((_, true))) { (before.clone(), Vec::new()) } else { (after, touched) };
1158 let mut fp = diff(&before, &after, &p.inputs, &touched);
1159 fp.log_full = touched.len() >= crate::engine::cmd::TOUCH_LOG_LIMIT;
1160 (fp.executed, fp.param_bytes) = (m.executed, m.param_bytes);
1161 fp.rolls_back = matches!(m.stopped, Some((_, true)));
1162 fp.error = outcome.err();
1163 fp.stopped = m.stopped.map(|(msg, _)| msg);
1164 fp.partial = m.partial.map(|(step, reason)| json!({"step": step, "reason": reason}));
1165 Ok(fp)
1166}
1167
1168fn diff(before: &Workbook, after: &Workbook, inputs: &[Bound], touched: &[(String, CellRef)]) -> Footprint {
1171 let mut fp = Footprint::default();
1172 let mut by_sheet: HashMap<&str, BTreeSet<CellRef>> = HashMap::new();
1174 for (n, c) in touched {
1175 by_sheet.entry(n.as_str()).or_default().insert(*c);
1176 }
1177 let (pairs, added) = pair_sheets(before, after);
1178 fp.sheets_added = added.into_iter().filter_map(|a| after.sheet(a).map(|s| s.name.clone())).collect();
1179 for (bi, ai) in pairs {
1180 let Some(bs) = before.sheets.get(bi) else { continue };
1181 let Some(ai) = ai else {
1182 fp.sheets_removed.push(bs.name.clone());
1183 continue;
1184 };
1185 let Some(after_sheet) = after.sheets.get(ai) else { continue };
1186 let rewritten = by_sheet.remove(after_sheet.name.as_str()).unwrap_or_default();
1187 if Arc::ptr_eq(bs, after_sheet) && rewritten.is_empty() {
1188 continue;
1189 }
1190 let (added, changed, removed) = objects(bs, after_sheet);
1191 fp.objects_added += added;
1192 fp.objects_changed += changed;
1193 fp.objects_removed += removed;
1194 let in_input = |c: CellRef| inputs.iter().any(|(_, p, _)| p.sheet == bi && p.range.contains(c));
1195 let mut written = Vec::new();
1196 let mut conflicts = Vec::new();
1197 let mut cells: BTreeSet<CellRef> = bs.cells.diff(&after_sheet.cells).into_iter().collect();
1198 cells.extend(rewritten.iter().copied());
1199 for c in cells {
1200 if in_input(c) {
1201 continue;
1202 }
1203 let (b, a) = (bs.cell(c), after_sheet.cell(c));
1204 if recalculated_only(b, a) && !rewritten.contains(&c) {
1205 continue;
1206 }
1207 written.push(c);
1208 if b.is_some_and(has_content) && content_differs(b, a) {
1209 conflicts.push(c);
1210 }
1211 }
1212 let Some(first) = written.first() else { continue };
1213 let bounds = written.iter().fold(RangeRef::cell(*first), |r, c| r.union(&RangeRef::cell(*c)));
1214 fp.sheets.push(SheetFootprint {
1215 sheet: bs.name.clone(),
1216 index: bi,
1217 changed: written.len(),
1218 conflict_count: conflicts.len(),
1219 cells: ranges(&mut written),
1220 conflicts: ranges(&mut conflicts),
1221 bounds: Some(bounds.a1()),
1222 bounds_range: Some(bounds),
1223 });
1224 }
1225 fp
1226}
1227
1228pub(crate) fn pair_sheets(before: &Workbook, after: &Workbook) -> (Vec<(usize, Option<usize>)>, Vec<usize>) {
1232 let mut pairs: Vec<(usize, Option<usize>)> = before.sheets.iter().enumerate().map(|(bi, sh)| (bi, after.sheet_index(&sh.name))).collect();
1233 let matched: BTreeSet<usize> = pairs.iter().filter_map(|(_, a)| *a).collect();
1234 let mut spare = (0..after.sheets.len()).filter(|a| !matched.contains(a));
1235 for (_, a) in pairs.iter_mut().filter(|(_, a)| a.is_none()) {
1236 *a = spare.next();
1237 }
1238 (pairs, spare.collect())
1239}
1240
1241fn recalculated_only(b: Option<&Cell>, a: Option<&Cell>) -> bool {
1244 match (b, a) {
1245 (Some(b), Some(a)) => match (&b.formula, &a.formula) {
1246 (Some(x), Some(y)) => x.text == y.text && x.array == y.array && b.style == a.style,
1247 (None, None) => match (&b.py, &a.py) {
1248 (Some(x), Some(y)) => x.code == y.code && x.object == y.object && b.style == a.style,
1249 _ => false,
1250 },
1251 _ => false,
1252 },
1253 _ => false,
1254 }
1255}
1256
1257fn has_content(c: &Cell) -> bool {
1259 c.formula.is_some() || c.py.is_some() || !(c.value.is_empty() || c.value.as_text() == Some(""))
1260}
1261
1262fn content_differs(b: Option<&Cell>, a: Option<&Cell>) -> bool {
1266 match (b, a) {
1267 (Some(b), Some(a)) => match (&b.formula, &a.formula) {
1268 (Some(x), Some(y)) => x.text != y.text,
1269 (None, None) => match (&b.py, &a.py) {
1270 (Some(x), Some(y)) => x.code != y.code || x.object != y.object,
1271 (None, None) => b.value != a.value,
1272 _ => true,
1273 },
1274 _ => true,
1275 },
1276 (Some(b), None) => has_content(b),
1277 _ => false,
1278 }
1279}
1280
1281fn objects(b: &Sheet, a: &Sheet) -> (usize, usize, usize) {
1283 fn count<T: PartialEq>(b: &[T], a: &[T], id: impl Fn(&T) -> u32) -> (usize, usize, usize) {
1284 let added = a.iter().filter(|x| !b.iter().any(|y| id(y) == id(x))).count();
1285 let removed = b.iter().filter(|x| !a.iter().any(|y| id(y) == id(x))).count();
1286 let changed = b.iter().filter(|x| a.iter().any(|y| id(y) == id(x) && y != *x)).count();
1287 (added, changed, removed)
1288 }
1289 let parts = [
1290 count(&b.charts, &a.charts, |o| o.id),
1291 count(&b.images, &a.images, |o| o.id),
1292 count(&b.shapes, &a.shapes, |o| o.id),
1293 count(&b.controls, &a.controls, |o| o.id),
1294 ];
1295 parts.iter().fold((0, 0, 0), |t, p| (t.0 + p.0, t.1 + p.1, t.2 + p.2))
1296}
1297
1298pub(crate) fn ranges(cells: &mut [CellRef]) -> Vec<String> {
1300 cells.sort_unstable_by_key(|c| (c.row, c.col));
1301 let mut runs: Vec<RangeRef> = Vec::new();
1302 for c in cells.iter() {
1303 match runs.last_mut() {
1304 Some(r) if r.end.row == c.row && r.end.col.checked_add(1) == Some(c.col) => r.end.col = c.col,
1305 _ => runs.push(RangeRef::cell(*c)),
1306 }
1307 }
1308 let mut merged: Vec<RangeRef> = Vec::new();
1309 for r in runs {
1310 match merged
1312 .iter_mut()
1313 .rev()
1314 .take(64)
1315 .find(|m| m.start.col == r.start.col && m.end.col == r.end.col && m.end.row.checked_add(1) == Some(r.start.row))
1316 {
1317 Some(m) => m.end.row = r.end.row,
1318 None => merged.push(r),
1319 }
1320 }
1321 merged.iter().take(MAX_LISTED_RANGES).map(RangeRef::a1).collect()
1322}
1323
1324fn make_room(s: &mut Session, fp: &Footprint, p: &Prepared, down: bool) -> Result<()> {
1329 let wb = s.doc()?.wb.clone();
1330 if let Some(why) = shift_refusal(&wb, fp, p, down) {
1331 return Err(EngineError::Other(why));
1332 }
1333 let env = Env { wb: &wb, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
1334 let moves: Vec<String> = fp
1335 .sheets
1336 .iter()
1337 .filter(|s| s.conflict_count > 0)
1338 .filter_map(|sh| sh.bounds_range.map(|block| resolve::a1(Place { sheet: sh.index, range: block }, &env)))
1339 .collect();
1340 for range in moves {
1341 s.execute("home.insertCells", json!({"range": range, "shift": if down { "down" } else { "right" }}))?;
1342 }
1343 Ok(())
1344}
1345
1346fn shift_refusal(wb: &Workbook, fp: &Footprint, p: &Prepared, down: bool) -> Option<String> {
1349 if !fp.sheets_removed.is_empty() || fp.objects_removed > 0 {
1350 return Some("Shifting can't make room: the macro deletes sheets or objects.".into());
1351 }
1352 if fp.log_full {
1353 return Some("Shifting can't make room: the macro writes more cells than the footprint can follow.".into());
1354 }
1355 let env = Env { wb, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
1356 for sh in fp.sheets.iter().filter(|s| s.conflict_count > 0) {
1357 let Some(block) = sh.bounds_range else { continue };
1358 let moved = |r: &RangeRef| {
1361 if down {
1362 r.start.col <= block.end.col && r.end.col >= block.start.col && r.end.row >= block.start.row
1363 } else {
1364 r.start.row <= block.end.row && r.end.row >= block.start.row && r.end.col >= block.start.col
1365 }
1366 };
1367 if let Some((name, pl)) = p.inputs.iter().map(|(n, pl, _)| (n.as_str(), *pl)).find(|(_, pl)| pl.sheet == sh.index && moved(&pl.range)) {
1370 return Some(format!(
1371 "Shifting the data would move {name} ({}). Run with onConflict overwrite or fail, or make room first.",
1372 resolve::a1(pl, &env)
1373 ));
1374 }
1375 }
1376 None
1377}
1378
1379pub struct Begun {
1383 pub mac: Macro,
1384 pub prepared: Prepared,
1385 pub rule: OnConflict,
1386 pub time_limit_ms: f64,
1387 pub inputs: Json,
1388}
1389
1390pub fn begin(s: &mut Session, name: &str, p: &Json) -> Result<Begun> {
1393 let d = s.doc()?;
1394 let (doc, trust) = (d.uid, d.trust);
1395 let m = super::find(&d.wb, name)
1396 .and_then(|i| d.wb.macros.get(i))
1397 .cloned()
1398 .ok_or_else(|| EngineError::Other(format!("There is no macro called '{name}'.")))?;
1399 check_params(p, &m)?;
1400 let asks = gate(&m, trust)?;
1401 approve(s, &m, doc, &asks, p.get("approve").and_then(Json::as_bool).unwrap_or(false))?;
1402 let m = with_output(m, p);
1403 let prepared = prepare(s, &m, p.get("inputs"))?;
1404 let rule = conflict_rule(p, &m)?;
1405 let time_limit_ms = time_limit(p);
1406 let inputs = prepared.inputs_json(&s.doc()?.wb);
1407 Ok(Begun { mac: m, prepared, rule, time_limit_ms, inputs })
1408}
1409
1410pub fn make_way(s: &mut Session, b: &Begun) -> Result<(Machine, bool)> {
1419 let (m, rule) = (&b.mac, b.rule);
1420 let mut machine = Machine::new(&b.prepared, false);
1421 machine.time_limit_ms = b.time_limit_ms;
1422 if rule == OnConflict::Overwrite {
1423 return Ok((machine, false));
1424 }
1425 let mut fp = footprint(s, &b.prepared, &machine)?;
1427 (machine.executed, machine.param_bytes) = (fp.executed, fp.param_bytes);
1428 if let Some(e) = fp.error.take() {
1429 return Err(e.into_engine(&m.name));
1430 }
1431 let mut shifted = false;
1432 if fp.conflict_count() > 0 {
1433 let what = fp.describe_conflicts();
1434 match rule {
1435 OnConflict::Ask => {
1436 let unseen = fp.partial.as_ref().map_or(String::new(), |p| {
1438 let text = |k: &str| p.get(k).and_then(Json::as_str).unwrap_or_default().to_string();
1439 format!(
1440 " The check stopped at step {} ({}), so overwriting also lets the steps after it write over data unchecked.",
1441 text("step"),
1442 text("reason")
1443 )
1444 });
1445 return Err(EngineError::Confirm(format!(
1446 "Macro '{}' would write over cells that hold data ({what}).{unseen} Overwrite them, or run again with onConflict shiftDown, shiftRight or fail",
1447 m.name
1448 )));
1449 }
1450 OnConflict::Fail | OnConflict::Overwrite => {
1451 return Err(EngineError::Other(format!("Macro '{}' would write over cells that hold data ({what}). Nothing was changed.", m.name)));
1452 }
1453 OnConflict::ShiftDown | OnConflict::ShiftRight => {
1454 make_room(s, &fp, &b.prepared, rule == OnConflict::ShiftDown)?;
1455 fp = footprint(s, &b.prepared, &machine)?;
1456 (machine.executed, machine.param_bytes) = (fp.executed, fp.param_bytes);
1457 if let Some(e) = fp.error.take() {
1458 return Err(e.into_engine(&m.name));
1459 }
1460 if fp.conflict_count() > 0 {
1461 return Err(EngineError::Other(format!(
1462 "Macro '{}': shifting the data didn't make room ({}). Nothing was changed.",
1463 m.name,
1464 fp.describe_conflicts()
1465 )));
1466 }
1467 shifted = true;
1468 }
1469 }
1470 }
1471 let start = s.docs.iter().find(|d| d.uid == b.prepared.doc).ok_or(EngineError::NoDocument)?.wb.clone();
1472 machine.guard = Some(Guard::new(start, fp.partial.as_ref(), rule == OnConflict::Ask));
1473 Ok((machine, shifted))
1474}
1475
1476pub fn run(s: &mut Session, name: &str, p: &Json) -> Result<Json> {
1479 let b = begin(s, name, p)?;
1480 let label = format!("Run Macro: {}", b.mac.name);
1481 s.grouped(&label, |s| {
1482 let (mut machine, shifted) = make_way(s, &b)?;
1483 let name = &b.mac.name;
1484 let before_run = machine.executed;
1486 machine.run_to_end(s).map_err(|e| e.into_engine(name))?;
1487 match machine.stopped {
1488 Some((msg, true)) => Err(EngineError::Other(format!("Macro '{name}' stopped: {msg} {}", rolled_back(&machine.wrote_files)))),
1489 stopped => {
1490 let mut out = json!({"macro": name, "ran": machine.executed.saturating_sub(before_run), "inputs": b.inputs});
1491 if let Some((msg, _)) = stopped {
1492 out["stopped"] = json!(msg);
1493 }
1494 if shifted {
1495 out["shifted"] = json!(b.rule.name());
1496 }
1497 Ok(out)
1498 }
1499 }
1500 })
1501}
1502
1503fn with_output(mut m: Macro, p: &Json) -> Macro {
1508 if let Some(o) = p.get("output").and_then(Json::as_str).map(str::trim).filter(|o| !o.is_empty()) {
1509 m.output.anchor = Some(o.to_string());
1510 }
1511 m
1512}
1513
1514pub fn check_params(p: &Json, m: &Macro) -> Result<()> {
1519 let refuse = |msg: String| Err(EngineError::Other(format!("Macro '{}': {msg}", m.name)));
1520 let names = || m.inputs.iter().map(|i| i.name.clone()).collect::<Vec<_>>().join(", ");
1521 match p.get("inputs") {
1522 None | Some(Json::Null) => {}
1523 Some(Json::String(_)) if !m.inputs.is_empty() => {}
1524 Some(Json::String(_)) => return refuse("it takes no inputs.".into()),
1525 Some(Json::Object(o)) => {
1526 for (k, v) in o {
1527 if !m.inputs.iter().any(|i| i.name.eq_ignore_ascii_case(k)) {
1528 return refuse(if m.inputs.is_empty() {
1529 format!("it has no input called '{k}'; it takes no inputs.")
1530 } else {
1531 format!("it has no input called '{k}' (its inputs: {}).", names())
1532 });
1533 }
1534 if !v.is_string() {
1535 return refuse(format!("give the input {k} as reference text, such as \"B2:D9\"."));
1536 }
1537 }
1538 }
1539 Some(_) => return refuse("give `inputs` as reference text, such as \"B2:D9\", or as {\"Input\": \"B2:D9\"}.".into()),
1540 }
1541 let given = |k: &str| p.get(k).filter(|v| !v.is_null());
1542 if given("onConflict").is_some_and(|v| !v.is_string()) {
1543 return refuse("`onConflict` must be ask, fail, overwrite, shiftDown or shiftRight.".into());
1544 }
1545 for k in ["overwrite", "approve"] {
1546 if given(k).is_some_and(|v| !v.is_boolean()) {
1547 return refuse(format!("`{k}` must be true or false."));
1548 }
1549 }
1550 if let Some(o) = given("output") {
1551 let ok = o.as_str().map(str::trim).is_some_and(|t| !t.is_empty() && t.chars().count() <= MAX_MACRO_REF && !t.chars().any(char::is_control));
1552 if !ok {
1553 return refuse(format!("`output` must be reference text of at most {MAX_MACRO_REF} characters, such as \"F2\" or \"Sheet2!F2\"."));
1554 }
1555 }
1556 if given("timeLimit").is_some_and(|v| !v.as_f64().is_some_and(|t| t > 0.0)) {
1557 return refuse("`timeLimit` must be a number of seconds above 0.".into());
1558 }
1559 Ok(())
1560}
1561
1562fn time_limit(p: &Json) -> f64 {
1564 p.get("timeLimit").and_then(Json::as_f64).map(|t| (t * 1000.0).clamp(1.0, 600_000.0)).unwrap_or(DEFAULT_TIME_LIMIT_MS)
1565}
1566
1567fn conflict_rule(p: &Json, m: &Macro) -> Result<OnConflict> {
1570 let rule = match p.get("onConflict").and_then(Json::as_str) {
1571 Some(t) => OnConflict::parse(t)
1572 .ok_or_else(|| EngineError::Other(format!("onConflict must be ask, fail, overwrite, shiftDown or shiftRight, not '{t}'.")))?,
1573 None => m.output.on_conflict,
1574 };
1575 if rule == OnConflict::Ask && p.get("overwrite").and_then(Json::as_bool) == Some(true) {
1576 return Ok(OnConflict::Overwrite);
1577 }
1578 Ok(rule)
1579}
1580
1581fn shifted_conflicts(s: &mut Session, fp: &Footprint, p: &Prepared, budget: &Machine, down: bool) -> Result<Option<String>> {
1584 let token = s.group_begin("Check Macro")?;
1585 let saved = Saved::take(s, p.doc);
1586 let mut spent = budget.clone();
1587 (spent.executed, spent.param_bytes) = (fp.executed, fp.param_bytes);
1588 let again = make_room(s, fp, p, down).and_then(|()| footprint(s, p, &spent));
1589 let rolled = s.group_rollback(token);
1590 saved.restore(s, p.doc);
1591 rolled?;
1592 Ok(match again {
1593 Err(e) => Some(e.to_string()),
1594 Ok(again) => match (&again.error, again.conflict_count()) {
1595 (Some(e), _) => Some(e.describe()),
1596 (None, 0) => None,
1597 (None, _) => Some(format!("Shifting the data doesn't make room ({}).", again.describe_conflicts())),
1598 },
1599 })
1600}
1601
1602pub fn check(s: &mut Session, name: &str, p: &Json) -> Result<Json> {
1604 let d = s.doc()?;
1605 let (doc, trust) = (d.uid, d.trust);
1606 let m = super::find(&d.wb, name)
1607 .and_then(|i| d.wb.macros.get(i))
1608 .cloned()
1609 .ok_or_else(|| EngineError::Other(format!("There is no macro called '{name}'.")))?;
1610 let mut out = json!({"macro": m.name, "ok": false, "onConflict": m.output.on_conflict.name()});
1611 let refused = |mut out: Json, e: String| {
1612 out["errors"] = json!([e]);
1613 Ok(out)
1614 };
1615 if let Err(e) = check_params(p, &m) {
1616 return refused(out, e.to_string());
1617 }
1618 let rule = match conflict_rule(p, &m) {
1619 Ok(r) => r,
1620 Err(e) => return refused(out, e.to_string()),
1621 };
1622 out["onConflict"] = json!(rule.name());
1623 let asks = match gate(&m, trust) {
1624 Ok(a) => a,
1625 Err(e) => return refused(out, e.to_string()),
1626 };
1627 let approved = s.macro_approvals.contains(&(doc, super::fingerprint(&m)));
1628 if !asks.is_empty() && !approved {
1629 out["needsApproval"] = json!(asks);
1631 }
1632 let prepared = match prepare(s, &with_output(m, p), p.get("inputs")) {
1633 Ok(pr) => pr,
1634 Err(e) => return refused(out, e.to_string()),
1635 };
1636 out["inputs"] = prepared.inputs_json(&s.doc()?.wb);
1637 if let Some(o) = prepared.output {
1638 let wb = &s.doc()?.wb;
1639 out["output"] = json!(resolve::a1(o, &Env { wb, active_sheet: wb.active_sheet(), inputs: &[], output: None, item: None }));
1640 }
1641 if !asks.is_empty() && !approved {
1642 return Ok(out);
1643 }
1644 let mut budget = Machine::new(&prepared, false);
1645 budget.time_limit_ms = time_limit(p);
1646 let fp = footprint(s, &prepared, &budget)?;
1647 let mut problems = Vec::new();
1649 if let Some(e) = &fp.error {
1650 problems.push(e.describe());
1651 }
1652 if let (true, Some(msg)) = (fp.rolls_back, &fp.stopped) {
1653 problems.push(format!("A Stop step ends the run and rolls it back: {msg}"));
1654 }
1655 let shifts = matches!(rule, OnConflict::ShiftDown | OnConflict::ShiftRight);
1656 if fp.conflict_count() > 0 {
1657 let what = fp.describe_conflicts();
1658 match rule {
1659 OnConflict::Ask => problems.push(format!("It would write over cells that hold data ({what}); the run asks first.")),
1660 OnConflict::Fail => problems.push(format!("It would write over cells that hold data ({what}).")),
1661 OnConflict::Overwrite => {}
1662 OnConflict::ShiftDown | OnConflict::ShiftRight => {
1663 let down = rule == OnConflict::ShiftDown;
1664 if let Some(why) = shift_refusal(&s.doc()?.wb, &fp, &prepared, down) {
1665 problems.push(why);
1666 } else if let Some(why) = shifted_conflicts(s, &fp, &prepared, &budget, down)? {
1667 problems.push(why);
1668 }
1669 }
1670 }
1671 }
1672 let passes: u64 = match rule {
1674 OnConflict::Overwrite => 1,
1675 _ if shifts && fp.conflict_count() > 0 => 3,
1676 _ => 2,
1677 };
1678 if fp.executed.saturating_mul(passes) > MAX_EXECUTED_STEPS {
1679 problems.push(format!("The run would execute more than {MAX_EXECUTED_STEPS} steps, counting the check before it ({} here).", fp.executed));
1680 }
1681 if fp.param_bytes.saturating_mul(passes) > MAX_PARAM_BYTES {
1682 problems.push(format!("The run would pass more than {} MB of parameters, counting the check before it.", MAX_PARAM_BYTES / (1024 * 1024)));
1683 }
1684 out["ok"] = json!(problems.is_empty());
1685 out["conflicts"] = json!(fp.conflict_count());
1686 if !problems.is_empty() {
1687 out["errors"] = json!(problems);
1688 }
1689 out["footprint"] = serde_json::to_value(&fp).unwrap_or(Json::Null);
1690 Ok(out)
1691}