Skip to main content

pfx_editor_doc/
patch.rs

1use std::ops::Range;
2use std::path::{Path, PathBuf};
3
4use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, TableLike, Value};
5
6use crate::edit::Edit;
7use crate::error::DocError;
8use crate::files::Files;
9use crate::splice::diff;
10use crate::stamp::Stamp;
11
12pub const LABEL_KEY: &str = "name";
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub enum Key {
16    Table(String),
17    Object(String),
18    Index(usize),
19}
20
21#[derive(Clone, Debug)]
22pub enum Op {
23    Set { path: Vec<Key>, value: Item },
24    Unset { path: Vec<Key> },
25    Push { path: Vec<Key>, value: Item },
26    Insert { path: Vec<Key>, value: Item },
27}
28
29impl Op {
30    pub fn path(&self) -> &[Key] {
31        match self {
32            Op::Set { path, .. }
33            | Op::Unset { path }
34            | Op::Push { path, .. }
35            | Op::Insert { path, .. } => path,
36        }
37    }
38
39    pub fn verb(&self) -> &'static str {
40        match self {
41            Op::Set { .. } => "set",
42            Op::Unset { .. } => "unset",
43            Op::Push { .. } => "push",
44            Op::Insert { .. } => "insert",
45        }
46    }
47}
48
49fn bare(key: &str) -> bool {
50    !key.is_empty()
51        && key
52            .bytes()
53            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
54}
55
56pub fn path_text(path: &[Key]) -> String {
57    let mut out = String::new();
58    for key in path {
59        match key {
60            Key::Table(name) => {
61                if !out.is_empty() {
62                    out.push('.');
63                }
64                if bare(name) {
65                    out.push_str(name);
66                } else {
67                    out.push('"');
68                    for c in name.chars() {
69                        if c == '"' || c == '\\' {
70                            out.push('\\');
71                        }
72                        out.push(c);
73                    }
74                    out.push('"');
75                }
76            }
77            Key::Object(name) => {
78                out.push('[');
79                out.push_str(name);
80                out.push(']');
81            }
82            Key::Index(i) => {
83                out.push('[');
84                out.push_str(&i.to_string());
85                out.push(']');
86            }
87        }
88    }
89    out
90}
91
92pub fn parse_path(text: &str) -> Result<Vec<Key>, String> {
93    let mut keys = Vec::new();
94    let mut chars = text.chars().peekable();
95    let mut expect_key = true;
96    while let Some(&c) = chars.peek() {
97        match c {
98            '[' => {
99                chars.next();
100                let mut inner = String::new();
101                loop {
102                    match chars.next() {
103                        Some(']') => break,
104                        Some(c) => inner.push(c),
105                        None => return Err(format!("`{text}` has an unclosed `[`")),
106                    }
107                }
108                if inner.is_empty() {
109                    return Err(format!("`{text}` has an empty `[]`"));
110                }
111                keys.push(if inner.bytes().all(|b| b.is_ascii_digit()) {
112                    Key::Index(
113                        inner
114                            .parse()
115                            .map_err(|_| format!("`{inner}` is too large an index"))?,
116                    )
117                } else {
118                    Key::Object(inner)
119                });
120                expect_key = false;
121            }
122            '.' if !expect_key => {
123                chars.next();
124                expect_key = true;
125            }
126            '"' if expect_key => {
127                chars.next();
128                let mut name = String::new();
129                loop {
130                    match chars.next() {
131                        Some('"') => break,
132                        Some('\\') => match chars.next() {
133                            Some(c) => name.push(c),
134                            None => return Err(format!("`{text}` ends inside a quoted key")),
135                        },
136                        Some(c) => name.push(c),
137                        None => return Err(format!("`{text}` has an unclosed quote")),
138                    }
139                }
140                keys.push(Key::Table(name));
141                expect_key = false;
142            }
143            _ if expect_key => {
144                let mut name = String::new();
145                while let Some(&c) = chars.peek() {
146                    if c == '.' || c == '[' {
147                        break;
148                    }
149                    name.push(c);
150                    chars.next();
151                }
152                if !bare(&name) {
153                    return Err(format!("`{name}` is not a bare key; quote it"));
154                }
155                keys.push(Key::Table(name));
156                expect_key = false;
157            }
158            _ => return Err(format!("`{text}` has `{c}` where a `.` or `[` belongs")),
159        }
160    }
161    if keys.is_empty() || (expect_key && !text.is_empty()) {
162        return Err(format!("`{text}` is not a path"));
163    }
164    Ok(keys)
165}
166
167enum At<'a> {
168    Table(&'a dyn TableLike),
169    Tables(&'a ArrayOfTables),
170    Value(&'a Value),
171}
172
173fn at_item(item: &Item) -> Option<At<'_>> {
174    match item {
175        Item::Table(t) => Some(At::Table(t)),
176        Item::ArrayOfTables(a) => Some(At::Tables(a)),
177        Item::Value(Value::InlineTable(t)) => Some(At::Table(t)),
178        Item::Value(v) => Some(At::Value(v)),
179        Item::None => None,
180    }
181}
182
183fn label_of(t: &dyn TableLike) -> Option<&str> {
184    t.get(LABEL_KEY).and_then(Item::as_str)
185}
186
187fn inline_label(v: &Value) -> Option<&str> {
188    v.as_inline_table()
189        .and_then(|t| t.get(LABEL_KEY))
190        .and_then(Value::as_str)
191}
192
193fn step<'a>(at: At<'a>, key: &Key) -> Option<At<'a>> {
194    match (at, key) {
195        (At::Table(t), Key::Table(k)) => t.get(k).and_then(at_item),
196        (At::Tables(a), Key::Object(n) | Key::Table(n)) => a
197            .iter()
198            .find(|t| label_of(*t) == Some(n.as_str()))
199            .map(|t| At::Table(t)),
200        (At::Tables(a), Key::Index(i)) => a.get(*i).map(|t| At::Table(t)),
201        (At::Value(Value::Array(a)), Key::Index(i)) => a.get(*i).and_then(value_at_item),
202        (At::Value(Value::Array(a)), Key::Object(n)) => a
203            .iter()
204            .find(|v| inline_label(v) == Some(n.as_str()))
205            .and_then(value_at_item),
206        _ => None,
207    }
208}
209
210fn value_at_item(v: &Value) -> Option<At<'_>> {
211    match v {
212        Value::InlineTable(t) => Some(At::Table(t)),
213        v => Some(At::Value(v)),
214    }
215}
216
217pub fn exists(doc: &DocumentMut, path: &[Key]) -> bool {
218    let mut at = At::Table(doc.as_table());
219    for key in path {
220        match step(at, key) {
221            Some(next) => at = next,
222            None => return false,
223        }
224    }
225    true
226}
227
228pub fn value_at(doc: &DocumentMut, path: &[Key]) -> Option<Value> {
229    let mut at = At::Table(doc.as_table());
230    for key in path {
231        at = step(at, key)?;
232    }
233    match at {
234        At::Value(v) => Some(v.clone()),
235        At::Table(t) => {
236            let mut inline = InlineTable::new();
237            for (k, item) in t.iter() {
238                if let Some(v) = item.as_value() {
239                    inline.insert(k, v.clone());
240                }
241            }
242            Some(Value::InlineTable(inline))
243        }
244        At::Tables(_) => None,
245    }
246}
247
248fn len_at(doc: &DocumentMut, path: &[Key]) -> Result<Option<usize>, String> {
249    let mut at = At::Table(doc.as_table());
250    for (n, key) in path.iter().enumerate() {
251        match step(at, key) {
252            Some(next) => at = next,
253            None if n + 1 == path.len() => return Ok(None),
254            None => return Err(format!("nothing at `{}`", path_text(&path[..=n]))),
255        }
256    }
257    match at {
258        At::Tables(a) => Ok(Some(a.len())),
259        At::Value(Value::Array(a)) => Ok(Some(a.len())),
260        _ => Err(format!("`{}` is not an array", path_text(path))),
261    }
262}
263
264enum Action {
265    Set(Item),
266    Unset,
267    Insert(Item),
268}
269
270struct Done {
271    old: Option<Item>,
272    slot: Key,
273}
274
275fn fresh(value: &Value) -> bool {
276    value.decor().prefix().is_none() && value.decor().suffix().is_none()
277}
278
279fn keep_look(old: &Item, new: &mut Item) {
280    match (old, new) {
281        (Item::Value(old), Item::Value(new)) if fresh(new) => {
282            *new.decor_mut() = old.decor().clone();
283        }
284        (Item::Table(old), Item::Table(new)) if new.position().is_none() => {
285            new.set_position(old.position());
286            *new.decor_mut() = old.decor().clone();
287        }
288        _ => {}
289    }
290}
291
292fn into_value(item: Item) -> Result<Value, String> {
293    item.into_value()
294        .map_err(|_| "expected a value here, not a table".to_string())
295}
296
297fn edit_item(item: &mut Item, path: &[Key], action: Action) -> Result<Done, String> {
298    match item {
299        Item::Table(t) => edit_table(t, false, path, action),
300        Item::ArrayOfTables(a) => edit_tables(a, path, action),
301        Item::Value(Value::InlineTable(t)) => edit_table(t, true, path, action),
302        Item::Value(Value::Array(a)) => edit_array(a, path, action),
303        Item::Value(_) => Err(format!("`{}` has nothing inside it", path_text(&path[..1]))),
304        Item::None => Err("nothing is there".to_string()),
305    }
306}
307
308fn edit_table(
309    t: &mut dyn TableLike,
310    inline: bool,
311    path: &[Key],
312    action: Action,
313) -> Result<Done, String> {
314    let Key::Table(key) = &path[0] else {
315        return Err(format!(
316            "`{}` is not a key; a table is addressed by key name",
317            path_text(&path[..1])
318        ));
319    };
320    if path.len() > 1 {
321        let child = t.get_mut(key).ok_or_else(|| format!("no key `{key}`"))?;
322        return edit_item(child, &path[1..], action);
323    }
324    let slot = Key::Table(key.clone());
325    match action {
326        Action::Set(mut new) => {
327            if inline {
328                new = Item::Value(into_value(new)?);
329            }
330            match t.get_mut(key) {
331                Some(old_slot) => {
332                    keep_look(old_slot, &mut new);
333                    let old = std::mem::replace(old_slot, new);
334                    Ok(Done {
335                        old: Some(old),
336                        slot,
337                    })
338                }
339                None => {
340                    t.insert(key, new);
341                    Ok(Done { old: None, slot })
342                }
343            }
344        }
345        Action::Unset => match t.remove(key) {
346            Some(old) => Ok(Done {
347                old: Some(old),
348                slot,
349            }),
350            None => Err(format!("no key `{key}` to unset")),
351        },
352        Action::Insert(_) => Err(format!(
353            "`{key}` is a key; insert takes a position in an array"
354        )),
355    }
356}
357
358fn find_table(a: &ArrayOfTables, name: &str) -> Option<usize> {
359    a.iter().position(|t| label_of(t) == Some(name))
360}
361
362fn table_of(item: Item) -> Result<Table, String> {
363    match item {
364        Item::Table(t) => Ok(t),
365        Item::Value(Value::InlineTable(t)) => Ok(t.into_table()),
366        _ => Err("expected a table".to_string()),
367    }
368}
369
370fn edit_tables(a: &mut ArrayOfTables, path: &[Key], action: Action) -> Result<Done, String> {
371    let len = a.len();
372    let found = match &path[0] {
373        Key::Index(i) => Some(*i),
374        Key::Object(n) | Key::Table(n) => find_table(a, n),
375    };
376    if path.len() > 1 {
377        let at = found.ok_or_else(|| format!("no entry `{}`", path_text(&path[..1])))?;
378        let table = a
379            .get_mut(at)
380            .ok_or_else(|| format!("no entry {at}, the array has {len}"))?;
381        return edit_table(table, false, &path[1..], action);
382    }
383    match (action, found) {
384        (Action::Set(item), Some(at)) if at < len => {
385            let mut new = Item::Table(table_of(item)?);
386            let old = Item::Table(a.get(at).cloned().expect("in range"));
387            keep_look(&old, &mut new);
388            a.replace(at, table_of(new)?);
389            Ok(Done {
390                old: Some(old),
391                slot: Key::Index(at),
392            })
393        }
394        (Action::Set(item), at) if at.is_none_or(|at| at == len) => {
395            a.push(table_of(item)?);
396            Ok(Done {
397                old: None,
398                slot: Key::Index(len),
399            })
400        }
401        (Action::Set(_), _) => Err(format!(
402            "no room at `{}`, the array has {len}",
403            path_text(&path[..1])
404        )),
405        (Action::Unset, Some(at)) if at < len => Ok(Done {
406            old: Some(Item::Table(a.remove(at))),
407            slot: Key::Index(at),
408        }),
409        (Action::Unset, _) => Err(format!("no entry `{}` to remove", path_text(&path[..1]))),
410        (Action::Insert(item), Some(at)) if at <= len && matches!(path[0], Key::Index(_)) => {
411            a.insert(at, table_of(item)?);
412            Ok(Done {
413                old: None,
414                slot: Key::Index(at),
415            })
416        }
417        (Action::Insert(_), _) => Err(format!(
418            "insert takes a position up to {len}, not `{}`",
419            path_text(&path[..1])
420        )),
421    }
422}
423
424fn find_value(a: &Array, key: &Key) -> Option<usize> {
425    match key {
426        Key::Index(i) => Some(*i),
427        Key::Object(n) => a.iter().position(|v| inline_label(v) == Some(n.as_str())),
428        Key::Table(_) => None,
429    }
430}
431
432fn edit_array(a: &mut Array, path: &[Key], action: Action) -> Result<Done, String> {
433    let len = a.len();
434    if let Key::Table(name) = &path[0] {
435        return Err(format!(
436            "`{name}` is not a position; an array is addressed by index or by name"
437        ));
438    }
439    let found = find_value(a, &path[0]);
440    if path.len() > 1 {
441        let at = found.ok_or_else(|| format!("no element `{}`", path_text(&path[..1])))?;
442        let element = a
443            .get_mut(at)
444            .ok_or_else(|| format!("no element {at}, the array has {len}"))?;
445        return match element {
446            Value::InlineTable(t) => edit_table(t, true, &path[1..], action),
447            Value::Array(inner) => edit_array(inner, &path[1..], action),
448            _ => Err(format!("element {at} has nothing inside it")),
449        };
450    }
451    let put = |a: &mut Array, at: usize, mut value: Value| {
452        if fresh(&value) {
453            let near = a
454                .get(at)
455                .or_else(|| at.checked_sub(1).and_then(|i| a.get(i)));
456            let prefix = near
457                .and_then(|v| v.decor().prefix())
458                .and_then(|p| p.as_str())
459                .map(str::to_string);
460            match prefix {
461                Some(prefix) if prefix.contains('\n') => value.decor_mut().set_prefix(prefix),
462                _ if at > 0 => value.decor_mut().set_prefix(" "),
463                _ => {}
464            }
465            if at == 0
466                && let Some(next) = a.get_mut(0)
467                && next.decor().prefix().and_then(|p| p.as_str()) == Some("")
468            {
469                next.decor_mut().set_prefix(" ");
470            }
471        }
472        a.insert_formatted(at, value);
473    };
474    match (action, found) {
475        (Action::Set(item), Some(at)) if at < len => {
476            let old = a.replace(at, into_value(item)?);
477            Ok(Done {
478                old: Some(Item::Value(old)),
479                slot: Key::Index(at),
480            })
481        }
482        (Action::Set(item), at) if at.is_none_or(|at| at == len) => {
483            put(a, len, into_value(item)?);
484            Ok(Done {
485                old: None,
486                slot: Key::Index(len),
487            })
488        }
489        (Action::Set(_), _) => Err(format!(
490            "no element `{}`, the array has {len}",
491            path_text(&path[..1])
492        )),
493        (Action::Unset, Some(at)) if at < len => Ok(Done {
494            old: Some(Item::Value(a.remove(at))),
495            slot: Key::Index(at),
496        }),
497        (Action::Unset, _) => Err(format!(
498            "no element `{}` to remove, the array has {len}",
499            path_text(&path[..1])
500        )),
501        (Action::Insert(item), Some(at)) if at <= len && matches!(path[0], Key::Index(_)) => {
502            put(a, at, into_value(item)?);
503            Ok(Done {
504                old: None,
505                slot: Key::Index(at),
506            })
507        }
508        (Action::Insert(_), _) => Err(format!(
509            "insert takes a position up to {len}, not `{}`",
510            path_text(&path[..1])
511        )),
512    }
513}
514
515fn renamed(path: &mut [Key], value: &Item, old: &Item) {
516    let n = path.len();
517    if n < 2 || !matches!(&path[n - 1], Key::Table(k) if k == LABEL_KEY) {
518        return;
519    }
520    let (Some(new), Some(was)) = (value.as_str(), old.as_str()) else {
521        return;
522    };
523    if matches!(&path[n - 2], Key::Object(x) if x == was) {
524        path[n - 2] = Key::Object(new.to_string());
525    }
526}
527
528fn with_slot(path: &[Key], slot: Key) -> Vec<Key> {
529    let mut path = path.to_vec();
530    if let Some(last) = path.last_mut() {
531        *last = slot;
532    }
533    path
534}
535
536fn new_array(value: Item) -> Result<Item, String> {
537    match value {
538        Item::Table(t) => {
539            let mut list = ArrayOfTables::new();
540            list.push(t);
541            Ok(Item::ArrayOfTables(list))
542        }
543        Item::Value(v) => {
544            let mut array = Array::new();
545            array.push(v);
546            Ok(Item::Value(Value::Array(array)))
547        }
548        _ => Err("push takes a value or a table".to_string()),
549    }
550}
551
552pub fn edit(doc: &mut DocumentMut, op: &Op) -> Result<Op, String> {
553    if op.path().is_empty() {
554        return Err("the path is empty".to_string());
555    }
556    let root = doc.as_item_mut();
557    match op {
558        Op::Set { path, value } => {
559            let done = edit_item(root, path, Action::Set(value.clone()))?;
560            let mut back = with_slot(path, done.slot);
561            Ok(match done.old {
562                Some(old) => {
563                    renamed(&mut back, value, &old);
564                    Op::Set {
565                        path: back,
566                        value: old,
567                    }
568                }
569                None => Op::Unset { path: back },
570            })
571        }
572        Op::Unset { path } => {
573            let done = edit_item(root, path, Action::Unset)?;
574            let old = done.old.ok_or("nothing was there")?;
575            Ok(match done.slot {
576                Key::Index(_) => Op::Insert {
577                    path: with_slot(path, done.slot),
578                    value: old,
579                },
580                slot => Op::Set {
581                    path: with_slot(path, slot),
582                    value: old,
583                },
584            })
585        }
586        Op::Insert { path, value } => {
587            let done = edit_item(root, path, Action::Insert(value.clone()))?;
588            Ok(Op::Unset {
589                path: with_slot(path, done.slot),
590            })
591        }
592        Op::Push { path, value } => match len_at(doc, path)? {
593            Some(len) => {
594                let mut at = path.clone();
595                at.push(Key::Index(len));
596                edit(
597                    doc,
598                    &Op::Insert {
599                        path: at,
600                        value: value.clone(),
601                    },
602                )
603            }
604            None => edit(
605                doc,
606                &Op::Set {
607                    path: path.clone(),
608                    value: new_array(value.clone())?,
609                },
610            ),
611        },
612    }
613}
614
615#[derive(Clone, Debug, PartialEq, Eq)]
616pub struct Replaced {
617    pub range: Range<usize>,
618    pub old: String,
619    pub new: String,
620}
621
622impl Replaced {
623    pub fn between(before: &str, after: &str) -> Replaced {
624        let (start, old_end, new_end) = diff(before, after);
625        Replaced {
626            range: start..old_end,
627            old: before[start..old_end].to_string(),
628            new: after[start..new_end].to_string(),
629        }
630    }
631
632    pub fn new_range(&self) -> Range<usize> {
633        self.range.start..self.range.start + self.new.len()
634    }
635
636    pub fn is_empty(&self) -> bool {
637        self.old == self.new
638    }
639
640    pub fn rebase(&self, base: &str, pending: &str) -> Option<String> {
641        if base.get(self.range.clone()) != Some(self.old.as_str()) {
642            return None;
643        }
644        let splice = |text: &str, at: Range<usize>| {
645            let mut out = String::with_capacity(text.len() + self.new.len());
646            out.push_str(&text[..at.start]);
647            out.push_str(&self.new);
648            out.push_str(&text[at.end..]);
649            out
650        };
651        if base == pending {
652            return Some(splice(base, self.range.clone()));
653        }
654        let (start, base_end, pending_end) = diff(base, pending);
655        if base_end < self.range.start {
656            let shift = pending_end as isize - base_end as isize;
657            let at = (self.range.start as isize + shift) as usize
658                ..(self.range.end as isize + shift) as usize;
659            Some(splice(pending, at))
660        } else if start > self.range.end {
661            Some(splice(pending, self.range.clone()))
662        } else {
663            None
664        }
665    }
666}
667
668#[derive(Clone, Debug)]
669struct Exact {
670    range: Range<usize>,
671    old: String,
672    new: String,
673    stamp: Stamp,
674    back: Vec<Op>,
675}
676
677#[derive(Clone, Debug)]
678struct Applied {
679    inverse: Vec<Op>,
680    replaced: Replaced,
681    after: Stamp,
682}
683
684#[derive(Clone, Debug)]
685pub struct TomlPatch {
686    label: String,
687    file: PathBuf,
688    ops: Vec<Op>,
689    exact: Option<Exact>,
690    applied: Option<Applied>,
691}
692
693impl TomlPatch {
694    pub fn new(file: impl Into<PathBuf>, ops: Vec<Op>) -> TomlPatch {
695        let label = match ops.first() {
696            Some(op) => format!("{} {}", op.verb(), path_text(op.path())),
697            None => "nothing".to_string(),
698        };
699        TomlPatch {
700            label,
701            file: file.into(),
702            ops,
703            exact: None,
704            applied: None,
705        }
706    }
707
708    pub fn set(file: impl Into<PathBuf>, path: Vec<Key>, value: impl Into<Item>) -> TomlPatch {
709        TomlPatch::new(
710            file,
711            vec![Op::Set {
712                path,
713                value: value.into(),
714            }],
715        )
716    }
717
718    pub fn unset(file: impl Into<PathBuf>, path: Vec<Key>) -> TomlPatch {
719        TomlPatch::new(file, vec![Op::Unset { path }])
720    }
721
722    pub fn remove(file: impl Into<PathBuf>, path: Vec<Key>) -> TomlPatch {
723        TomlPatch::unset(file, path).labelled_as("remove")
724    }
725
726    pub fn push(file: impl Into<PathBuf>, path: Vec<Key>, value: impl Into<Item>) -> TomlPatch {
727        TomlPatch::new(
728            file,
729            vec![Op::Push {
730                path,
731                value: value.into(),
732            }],
733        )
734    }
735
736    pub fn insert(file: impl Into<PathBuf>, path: Vec<Key>, value: impl Into<Item>) -> TomlPatch {
737        TomlPatch::new(
738            file,
739            vec![Op::Insert {
740                path,
741                value: value.into(),
742            }],
743        )
744    }
745
746    fn labelled_as(mut self, verb: &str) -> TomlPatch {
747        if let Some(op) = self.ops.first() {
748            self.label = format!("{verb} {}", path_text(op.path()));
749        }
750        self
751    }
752
753    pub fn labelled(mut self, label: impl Into<String>) -> TomlPatch {
754        self.label = label.into();
755        self
756    }
757
758    pub fn and(mut self, op: Op) -> TomlPatch {
759        self.ops.push(op);
760        self
761    }
762
763    pub fn file(&self) -> &Path {
764        &self.file
765    }
766
767    pub fn ops(&self) -> &[Op] {
768        &self.ops
769    }
770
771    pub fn replaced(&self) -> Option<&Replaced> {
772        self.applied.as_ref().map(|applied| &applied.replaced)
773    }
774
775    fn exact_after(&self, before: &str) -> Option<(String, Vec<Op>)> {
776        let exact = self.exact.as_ref()?;
777        if Stamp::of(before) != exact.stamp || before.get(exact.range.clone())? != exact.old {
778            return None;
779        }
780        let mut after = String::with_capacity(before.len() + exact.new.len());
781        after.push_str(&before[..exact.range.start]);
782        after.push_str(&exact.new);
783        after.push_str(&before[exact.range.end..]);
784        Some((after, exact.back.clone()))
785    }
786}
787
788impl Edit for TomlPatch {
789    fn label(&self) -> &str {
790        &self.label
791    }
792
793    fn files(&self) -> Vec<PathBuf> {
794        vec![self.file.clone()]
795    }
796
797    fn apply(&mut self, files: &mut Files) -> Result<(), DocError> {
798        let file = files
799            .get(&self.file)
800            .ok_or_else(|| DocError::new(&self.file, "", "the file is not open"))?;
801        let doc = file.doc().ok_or_else(|| {
802            DocError::new(
803                &self.file,
804                "",
805                "the file does not parse, so no patch applies until it does",
806            )
807        })?;
808        let before = file.text();
809        let (after, inverse) =
810            match self.exact_after(before) {
811                Some(exact) => exact,
812                None => {
813                    let mut doc = doc.clone();
814                    let mut inverse = Vec::with_capacity(self.ops.len());
815                    for op in &self.ops {
816                        inverse.push(edit(&mut doc, op).map_err(|text| {
817                            DocError::new(&self.file, path_text(op.path()), text)
818                        })?);
819                    }
820                    inverse.reverse();
821                    (doc.to_string(), inverse)
822                }
823            };
824        let parsed = after.parse::<DocumentMut>().map_err(|error| {
825            DocError::new(
826                &self.file,
827                "",
828                format!(
829                    "the patch would leave text that does not parse: {}",
830                    error.message()
831                ),
832            )
833        })?;
834        let replaced = Replaced::between(before, &after);
835        let stamp = Stamp::of(&after);
836        files.set_parsed(&self.file, after, parsed)?;
837        self.applied = Some(Applied {
838            inverse,
839            replaced,
840            after: stamp,
841        });
842        Ok(())
843    }
844
845    fn inverse(&self) -> Box<dyn Edit> {
846        let Some(applied) = &self.applied else {
847            return Box::new(TomlPatch::new(self.file.clone(), Vec::new()).labelled(&self.label));
848        };
849        let replaced = &applied.replaced;
850        Box::new(TomlPatch {
851            label: self.label.clone(),
852            file: self.file.clone(),
853            ops: applied.inverse.clone(),
854            exact: Some(Exact {
855                range: replaced.new_range(),
856                old: replaced.new.clone(),
857                new: replaced.old.clone(),
858                stamp: applied.after,
859                back: self.ops.clone(),
860            }),
861            applied: None,
862        })
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869    use toml_edit::value;
870
871    fn path(text: &str) -> Vec<Key> {
872        parse_path(text).unwrap()
873    }
874
875    fn doc(text: &str) -> DocumentMut {
876        text.parse().unwrap()
877    }
878
879    #[test]
880    fn a_path_reads_and_writes_as_text() {
881        let keys = path("object[crate].at[0]");
882        assert_eq!(
883            keys,
884            vec![
885                Key::Table("object".into()),
886                Key::Object("crate".into()),
887                Key::Table("at".into()),
888                Key::Index(0),
889            ]
890        );
891        assert_eq!(path_text(&keys), "object[crate].at[0]");
892        let quoted = path("\"a.b\".c");
893        assert_eq!(quoted[0], Key::Table("a.b".into()));
894        assert_eq!(path_text(&quoted), "\"a.b\".c");
895        for bad in ["", "a.", "a[", "a[]", "a b", "a..b", ".a"] {
896            assert!(parse_path(bad).is_err(), "{bad}");
897        }
898    }
899
900    #[test]
901    fn set_keeps_the_values_comment_and_its_inverse_restores_it() {
902        let text = "[box]\nat = [0.0, 0.05, 0.0]   # sits on the floor\n";
903        let mut d = doc(text);
904        let back = edit(
905            &mut d,
906            &Op::Set {
907                path: path("box.at[1]"),
908                value: value(0.5),
909            },
910        )
911        .unwrap();
912        assert_eq!(
913            d.to_string(),
914            "[box]\nat = [0.0, 0.5, 0.0]   # sits on the floor\n"
915        );
916        edit(&mut d, &back).unwrap();
917        assert_eq!(d.to_string(), text);
918    }
919
920    #[test]
921    fn entries_are_found_by_name_and_renames_invert() {
922        let text = "[[object]]\nname = \"a\"\n\n[[object]]\nname = \"b\"\nsize = 1\n";
923        let mut d = doc(text);
924        let back = edit(
925            &mut d,
926            &Op::Set {
927                path: path("object[b].name"),
928                value: value("c"),
929            },
930        )
931        .unwrap();
932        assert_eq!(back.path(), path("object[c].name").as_slice());
933        edit(&mut d, &back).unwrap();
934        assert_eq!(d.to_string(), text);
935    }
936
937    #[test]
938    fn push_and_remove_invert_each_other() {
939        let text = "list = [1, 2]\n\n[[object]]\nname = \"a\"\n";
940        let mut d = doc(text);
941        let back = edit(
942            &mut d,
943            &Op::Push {
944                path: path("list"),
945                value: value(3),
946            },
947        )
948        .unwrap();
949        assert_eq!(d["list"].as_array().unwrap().len(), 3);
950        assert_eq!(back.path(), path("list[2]").as_slice());
951        edit(&mut d, &back).unwrap();
952        assert_eq!(d.to_string(), text);
953        let back = edit(
954            &mut d,
955            &Op::Unset {
956                path: path("list[0]"),
957            },
958        )
959        .unwrap();
960        assert!(matches!(back, Op::Insert { .. }));
961        edit(&mut d, &back).unwrap();
962        assert_eq!(d.to_string(), text);
963        let back = edit(
964            &mut d,
965            &Op::Unset {
966                path: path("object[a]"),
967            },
968        )
969        .unwrap();
970        assert!(
971            d.get("object")
972                .unwrap()
973                .as_array_of_tables()
974                .unwrap()
975                .is_empty()
976        );
977        edit(&mut d, &back).unwrap();
978        assert_eq!(d.to_string(), text);
979    }
980
981    #[test]
982    fn inserted_elements_follow_their_neighbours_spacing() {
983        let mut d = doc("a = [\"x\", \"y\"]\nb = [\n  1,\n  2,\n]\n");
984        for op in [
985            Op::Insert {
986                path: path("a[0]"),
987                value: value("w"),
988            },
989            Op::Push {
990                path: path("a"),
991                value: value("z"),
992            },
993            Op::Push {
994                path: path("b"),
995                value: value(3),
996            },
997            Op::Insert {
998                path: path("b[0]"),
999                value: value(0),
1000            },
1001        ] {
1002            edit(&mut d, &op).unwrap();
1003        }
1004        assert_eq!(
1005            d.to_string(),
1006            "a = [\"w\", \"x\", \"y\", \"z\"]\nb = [\n  0,\n  1,\n  2,\n  3,\n]\n"
1007        );
1008    }
1009
1010    #[test]
1011    fn push_onto_a_missing_key_makes_the_array() {
1012        let mut d = doc("a = 1\n");
1013        let back = edit(
1014            &mut d,
1015            &Op::Push {
1016                path: path("tags"),
1017                value: value("x"),
1018            },
1019        )
1020        .unwrap();
1021        assert_eq!(d["tags"].as_array().unwrap().len(), 1);
1022        edit(&mut d, &back).unwrap();
1023        assert_eq!(d.to_string(), "a = 1\n");
1024    }
1025
1026    #[test]
1027    fn bad_paths_are_refused_with_a_reason() {
1028        let mut d = doc("a = 1\nlist = [1]\n[t]\nx = { y = 1 }\n");
1029        let cases = [
1030            Op::Unset { path: path("b") },
1031            Op::Set {
1032                path: path("a.b"),
1033                value: value(1),
1034            },
1035            Op::Set {
1036                path: path("list[5]"),
1037                value: value(1),
1038            },
1039            Op::Insert {
1040                path: path("t.x"),
1041                value: value(1),
1042            },
1043            Op::Set {
1044                path: path("list.x"),
1045                value: value(1),
1046            },
1047        ];
1048        for op in cases {
1049            assert!(edit(&mut d, &op).is_err(), "{op:?}");
1050        }
1051        assert_eq!(d.to_string(), "a = 1\nlist = [1]\n[t]\nx = { y = 1 }\n");
1052    }
1053
1054    #[test]
1055    fn a_table_value_set_into_an_inline_table_becomes_inline() {
1056        let mut d = doc("x = { y = 1 }\n");
1057        let mut table = Table::new();
1058        table.insert("z", value(2));
1059        edit(
1060            &mut d,
1061            &Op::Set {
1062                path: path("x.w"),
1063                value: Item::Table(table),
1064            },
1065        )
1066        .unwrap();
1067        assert_eq!(d["x"]["w"]["z"].as_integer(), Some(2));
1068        assert!(d.to_string().parse::<DocumentMut>().is_ok());
1069    }
1070
1071    #[test]
1072    fn value_at_reads_a_value_or_a_tables_values() {
1073        let d = doc("[[object]]\nname = \"a\"\nat = [1, 2]\n");
1074        assert_eq!(
1075            value_at(&d, &path("object[a].at[1]")).unwrap().as_integer(),
1076            Some(2)
1077        );
1078        assert!(value_at(&d, &path("object[a]")).unwrap().is_inline_table());
1079        assert!(value_at(&d, &path("object[b]")).is_none());
1080        assert!(exists(&d, &path("object[0].at")));
1081    }
1082
1083    #[test]
1084    fn a_replaced_range_rebases_pending_text_or_refuses_it() {
1085        let base = "a = 1\nb = 2\nc = 3\n";
1086        let replaced = Replaced::between(base, "a = 1\nb = 20\nc = 3\n");
1087        assert_eq!(replaced.range, 11..11);
1088        assert_eq!(
1089            replaced.rebase(base, "a = 100\nb = 2\nc = 3\n").as_deref(),
1090            Some("a = 100\nb = 20\nc = 3\n")
1091        );
1092        assert_eq!(
1093            replaced.rebase(base, "a = 1\nb = 2\nc = 30\n").as_deref(),
1094            Some("a = 1\nb = 20\nc = 30\n")
1095        );
1096        assert_eq!(replaced.rebase(base, "a = 1\nb = 25\nc = 3\n"), None);
1097        assert_eq!(
1098            replaced.rebase(base, base).as_deref(),
1099            Some("a = 1\nb = 20\nc = 3\n")
1100        );
1101    }
1102}