Skip to main content

rakata_formats/gff/
path.rs

1//! Path-addressed reads and edits of a loaded [`GffStruct`].
2//!
3//! # Why this exists
4//!
5//! A `Gff` tree could be read and it could be written, with nothing in between.
6//! The only public mutation was [`GffStruct::push_field`], which appends, so the
7//! only way to change a value was to parse into a typed view, mutate that, and
8//! rebuild with `to_gff()`. Rebuilding starts from an empty struct and therefore
9//! drops every field the view does not model. That is right for authoring a new
10//! file and it is data loss for editing one somebody else wrote.
11//!
12//! These operations name a field by path and leave the rest of the tree alone.
13//!
14//! # Replace never appends
15//!
16//! Appending a second copy of a label would leave the original winning on
17//! lookup: the file changes, the behaviour does not, and nothing reports it.
18//! [`set`] replaces in place and refuses a label that is not already there;
19//! [`insert`] adds one and refuses a label that is.
20//!
21//! Keeping them apart is deliberate. A single upsert would turn a mistyped
22//! label into a silently-created field rather than an error.
23//!
24//! [`set`]: GffStruct::set
25//! [`insert`]: GffStruct::insert
26//!
27//! # A label carried twice is an error, not a first match
28//!
29//! A struct really can hold one label several times, and vanilla content does:
30//! every `EntryList` and `ReplyList` node in a `.dlg` carries `SoundExists` six
31//! times over, and nothing on record says why. Resolving that by position
32//! answers a question nobody has established the answer to, so every walk here
33//! reports [`AmbiguousLabel`] instead. Editing all of them is a different
34//! request and needs a different call.
35//!
36//! [`AmbiguousLabel`]: GffPathError::AmbiguousLabel
37//!
38//! # What this does not do
39//!
40//! No validation. Whether a label belongs on this format, whether a value is in
41//! range, whether a required field is missing: all of that is `rakata-lint`'s,
42//! and the schemas it needs live above this crate.
43//!
44//! Struct ids on inserted list elements are the caller's to choose.
45//! [`push_element`](GffStruct::push_element) takes a whole [`GffStruct`], which
46//! carries its own id, because several lists have the engine skip an element
47//! whose id is wrong and which value is right is per-format knowledge. See the
48//! GIT and PTH pages in `docs/src/formats/gff/` for the ones that matter.
49//!
50//! # A list element cannot be inserted at an index, on purpose
51//!
52//! Appending is the only way to add a list element here. Inserting into the
53//! middle would shift every later index, and DLG links address their targets by
54//! index, so a mid-list insertion silently breaks every link past the insertion
55//! point. Nothing else wants it either: GIT lists are typed by struct id rather
56//! than ordered, and inventory order is cosmetic.
57//!
58//! So the omission is a decision. Adding it later as an obvious convenience
59//! would be adding a footgun.
60//!
61//! A struct's *fields* are the opposite case, which is why
62//! [`insert_at_position`] exists beside this. Nothing addresses a field by its
63//! position, so putting one back where it was breaks nothing, and the writer
64//! emits fields in the order the struct holds them: restoring a removed field
65//! on the end rewrites the file from the hole onwards for no reason.
66//!
67//! [`insert_at_position`]: GffStruct::insert_at_position
68//!
69//! # Every walker here is private, and that is load-bearing
70//!
71//! The general shape this could have taken is a `struct_at_mut(path)` handing
72//! back a `&mut GffStruct`, and it is the wrong one. A caller holding a mutable
73//! struct can reach [`GffStruct::push_field`] and append over a label that is
74//! already there, which is the silent-inert-edit this whole module exists to
75//! prevent. Every operation is narrow so that the only ways in are ones that
76//! cannot produce a duplicate label.
77
78use std::fmt;
79use std::str::FromStr;
80
81use thiserror::Error;
82
83use super::label::{GffLabel, GffLabelError};
84use super::{GffField, GffStruct, GffValue};
85use crate::schema::gff_value_type;
86
87/// One step of a [`GffPath`].
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum GffPathSegment {
90    /// Name a field of the struct currently in hand.
91    Field(GffLabel),
92    /// Take an element of the list currently in hand.
93    Index(usize),
94}
95
96/// An address into a GFF tree, such as `ClassList[0].Class`.
97///
98/// Rendering gives the same dot notation `rakata-lint` already prints in
99/// `LintDiagnostic.fields`, so a consumer wanting to navigate to a finding
100/// rather than display it has one address type to use for both.
101#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
102pub struct GffPath {
103    segments: Vec<GffPathSegment>,
104}
105
106impl GffPath {
107    /// Creates a path from its segments.
108    pub fn new(segments: Vec<GffPathSegment>) -> Self {
109        Self { segments }
110    }
111
112    /// The steps this path takes, in order.
113    pub fn segments(&self) -> &[GffPathSegment] {
114        &self.segments
115    }
116
117    /// Whether this path names nothing.
118    pub fn is_empty(&self) -> bool {
119        self.segments.is_empty()
120    }
121
122    /// This path with its last step dropped, or `None` when it has none.
123    ///
124    /// What a list mutation records against: appending to or removing from a
125    /// list changes the list, and the index the caller named is not where the
126    /// change is once the siblings have shifted.
127    pub fn parent(&self) -> Option<Self> {
128        let (_, rest) = self.segments.split_last()?;
129        Some(Self {
130            segments: rest.to_vec(),
131        })
132    }
133
134    /// Extends this path by one step, which is how a walk builds one.
135    pub fn then(&self, segment: GffPathSegment) -> Self {
136        let mut segments = self.segments.clone();
137        segments.push(segment);
138        Self { segments }
139    }
140
141    /// Whether `prefix` names this node or one of its ancestors.
142    ///
143    /// Segment by segment rather than over the rendered text, and that is the
144    /// whole reason this is a method. `"ItemList".starts_with("ItemLis")` is
145    /// true and `ItemList` is not under `ItemLis`, so a prefix test written on
146    /// the strings answers yes for a sibling whose label merely begins the same
147    /// way. Comparing segments cannot make that mistake, because a label is
148    /// matched whole.
149    ///
150    /// Reflexive, matching the slice method it is built on: a path starts with
151    /// itself. Callers wanting proper descendants exclude equality themselves,
152    /// which is what the tree does when deciding whether a collapsed node hides
153    /// a row, since a collapsed node has to keep drawing itself.
154    ///
155    /// Ordering makes this cheap to use over a set. [`GffPath`] sorts by
156    /// segments, so every descendant of a path sorts contiguously after it with
157    /// nothing able to sort between; a `BTreeMap` of changes answers "anything
158    /// under here" with one `range` and this test on the first entry.
159    pub fn starts_with(&self, prefix: &Self) -> bool {
160        self.segments.starts_with(&prefix.segments)
161    }
162
163    /// Renders the path up to and including `upto` segments.
164    ///
165    /// Used so an error names where the walk stopped rather than the whole
166    /// path, which is what tells a caller which step was wrong.
167    fn prefix(&self, upto: usize) -> String {
168        GffPath::new(self.segments[..upto.min(self.segments.len())].to_vec()).to_string()
169    }
170}
171
172impl fmt::Display for GffPath {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        let mut first = true;
175        for segment in &self.segments {
176            match segment {
177                GffPathSegment::Field(label) => {
178                    if !first {
179                        write!(f, ".")?;
180                    }
181                    write!(f, "{label}")?;
182                }
183                GffPathSegment::Index(index) => write!(f, "[{index}]")?,
184            }
185            first = false;
186        }
187        Ok(())
188    }
189}
190
191/// A path string that does not describe an address.
192#[derive(Debug, Clone, PartialEq, Eq, Error)]
193pub enum GffPathParseError {
194    /// The text between separators was empty.
195    #[error("empty path segment in `{path}`")]
196    EmptySegment {
197        /// The path as supplied.
198        path: String,
199    },
200    /// A `[...]` did not hold a number.
201    #[error("`{index}` in `{path}` is not a list index")]
202    BadIndex {
203        /// The path as supplied.
204        path: String,
205        /// The text found between the brackets.
206        index: String,
207    },
208    /// A `[` was never closed.
209    #[error("unclosed `[` in `{path}`")]
210    UnclosedIndex {
211        /// The path as supplied.
212        path: String,
213    },
214    /// A segment is not usable as a GFF label.
215    #[error("`{segment}` in `{path}` is not a valid label: {source}")]
216    BadLabel {
217        /// The path as supplied.
218        path: String,
219        /// The offending segment.
220        segment: String,
221        /// Why the label was rejected.
222        source: GffLabelError,
223    },
224}
225
226impl FromStr for GffPath {
227    type Err = GffPathParseError;
228
229    /// Parses dot notation with bracketed list indices.
230    ///
231    /// `PropertiesList[0].PropertyName` is three segments: a field, an index,
232    /// and a field.
233    fn from_str(text: &str) -> Result<Self, Self::Err> {
234        let path = || text.to_owned();
235        let mut segments = Vec::new();
236
237        for part in text.split('.') {
238            let (name, mut rest) = match part.find('[') {
239                Some(at) => (&part[..at], &part[at..]),
240                None => (part, ""),
241            };
242            if name.is_empty() {
243                return Err(GffPathParseError::EmptySegment { path: path() });
244            }
245            segments.push(GffPathSegment::Field(GffLabel::new(name).map_err(
246                |source| GffPathParseError::BadLabel {
247                    path: path(),
248                    segment: name.to_owned(),
249                    source,
250                },
251            )?));
252
253            while !rest.is_empty() {
254                let end = rest
255                    .find(']')
256                    .ok_or_else(|| GffPathParseError::UnclosedIndex { path: path() })?;
257                let digits = &rest[1..end];
258                let index = digits
259                    .parse::<usize>()
260                    .map_err(|_| GffPathParseError::BadIndex {
261                        path: path(),
262                        index: digits.to_owned(),
263                    })?;
264                segments.push(GffPathSegment::Index(index));
265                rest = &rest[end + 1..];
266            }
267        }
268
269        Ok(Self { segments })
270    }
271}
272
273/// A path that does not resolve, or an edit the tree will not take.
274///
275/// A `Result` rather than a hit-plus-diagnostics pair. A tier walk can half
276/// succeed, answering from a lower tier while a higher one was unreadable, and
277/// both halves matter. Addressing a path cannot: it arrives or it does not, for
278/// one reason.
279#[derive(Debug, Clone, PartialEq, Eq, Error)]
280pub enum GffPathError {
281    /// A path with no segments names nothing to act on.
282    #[error("an empty path names no field")]
283    EmptyPath,
284    /// A path started by indexing, with no list yet in hand.
285    #[error("`{path}` starts with a list index, but the root is a struct")]
286    IndexAtRoot {
287        /// The path as supplied.
288        path: String,
289    },
290    /// The struct has no field under that label.
291    #[error("no field `{label}` at `{at}`")]
292    NoSuchField {
293        /// Where the walk had reached.
294        at: String,
295        /// The label looked for.
296        label: String,
297    },
298    /// The struct carries that label more than once.
299    ///
300    /// Which copy the caller meant is not recoverable from the path, and the
301    /// copies are not known to agree on anything but their value today.
302    #[error("`{at}` carries `{label}` {count} times, so the path names no single field")]
303    AmbiguousLabel {
304        /// Where the walk had reached.
305        at: String,
306        /// The label looked for.
307        label: String,
308        /// How many fields carry it.
309        count: usize,
310    },
311    /// The list is shorter than the index.
312    #[error("`{at}` has {len} element(s), so index {index} is out of range")]
313    IndexOutOfRange {
314        /// Where the walk had reached.
315        at: String,
316        /// The index asked for.
317        index: usize,
318        /// How many elements the list holds.
319        len: usize,
320    },
321    /// A field was indexed and is not a list.
322    #[error("`{at}` is {actual}, which cannot be indexed")]
323    NotAList {
324        /// Where the walk had reached.
325        at: String,
326        /// The type actually found.
327        actual: &'static str,
328    },
329    /// A field was descended into and is not a struct.
330    #[error("`{at}` is {actual}, which has no fields")]
331    NotAStruct {
332        /// Where the walk had reached.
333        at: String,
334        /// The type actually found.
335        actual: &'static str,
336    },
337    /// The replacement is a different type from what the field holds.
338    ///
339    /// The existing field's type is the only oracle available here. The
340    /// per-format schemas that could answer "what *should* this label be" live
341    /// in `rakata-generics`, above this crate, and asking that question is
342    /// validation, which belongs to lint.
343    #[error("`{at}` holds {existing} and the replacement is {replacement}; use `insert` after `remove` to change a field's type deliberately")]
344    TypeMismatch {
345        /// The field being replaced.
346        at: String,
347        /// The type currently stored.
348        existing: &'static str,
349        /// The type offered.
350        replacement: &'static str,
351    },
352    /// `insert` was given a label the struct already carries.
353    #[error("`{at}` already exists; `set` replaces a field and `insert` adds one")]
354    AlreadyExists {
355        /// The field that is already present.
356        at: String,
357    },
358    /// A field operation ended at a list index, or the reverse.
359    #[error("`{operation}` needs a path ending in {expected}, and `{at}` does not")]
360    WrongSegmentKind {
361        /// The path as supplied.
362        at: String,
363        /// The operation that could not use it.
364        operation: &'static str,
365        /// The kind of last segment the operation needs.
366        expected: &'static str,
367    },
368}
369
370/// Where a partial walk of a path has arrived.
371enum Cursor<'a> {
372    Struct(&'a mut GffStruct),
373    Value(&'a mut GffValue),
374}
375
376impl GffStruct {
377    /// Reads the value a path names.
378    ///
379    /// The read counterpart to [`Self::set`], and the way to do a
380    /// read-modify-write over a large file without building a typed view per
381    /// field.
382    ///
383    /// # Errors
384    ///
385    /// Every operation here walks the path the same way and can fail the same
386    /// way partway along it: [`GffPathError::EmptyPath`] for a path with no
387    /// segments, [`GffPathError::IndexAtRoot`] for one starting with an index,
388    /// [`GffPathError::NotAStruct`] or [`GffPathError::NotAList`] when a
389    /// segment's value is not the kind the next segment needs,
390    /// [`GffPathError::IndexOutOfRange`] when a list is shorter than the index,
391    /// [`GffPathError::NoSuchField`] when a struct does not carry the label,
392    /// and [`GffPathError::AmbiguousLabel`] when it carries it more than once.
393    /// That last one is a refusal rather than a first match; see the module
394    /// docs.
395    ///
396    /// On top of those, [`GffPathError::WrongSegmentKind`] when `path` ends in
397    /// an index. That lands on a bare [`GffStruct`], which is not a
398    /// [`GffValue`]; [`Self::struct_id`] is the call that takes such a path.
399    pub fn get(&self, path: &GffPath) -> Result<&GffValue, GffPathError> {
400        let segments = path.segments();
401        if segments.is_empty() {
402            return Err(GffPathError::EmptyPath);
403        }
404
405        let mut current: Option<&GffValue> = None;
406        let mut structure: &GffStruct = self;
407
408        for (depth, segment) in segments.iter().enumerate() {
409            match segment {
410                GffPathSegment::Field(label) => {
411                    if let Some(value) = current {
412                        structure = as_struct(value, path, depth)?;
413                    }
414                    let at = sole_position(&structure.fields, label, path, depth)?;
415                    current = Some(&structure.fields[at].value);
416                }
417                GffPathSegment::Index(index) => {
418                    let Some(value) = current else {
419                        return Err(GffPathError::IndexAtRoot {
420                            path: path.to_string(),
421                        });
422                    };
423                    let list = as_list(value, path, depth)?;
424                    structure = list
425                        .get(*index)
426                        .ok_or_else(|| GffPathError::IndexOutOfRange {
427                            at: path.prefix(depth),
428                            index: *index,
429                            len: list.len(),
430                        })?;
431                    current = None;
432                }
433            }
434        }
435
436        // A path ending in an index names a struct, which is not a field value.
437        current.ok_or_else(|| GffPathError::WrongSegmentKind {
438            at: path.to_string(),
439            operation: "get",
440            expected: "a field label",
441        })
442    }
443
444    /// Replaces the value a path names, returning what was there.
445    ///
446    /// Refuses a label the struct does not already carry, and refuses a
447    /// replacement of a different type from the one stored. Both refusals exist
448    /// because the alternative is a file that looks edited and is not: an
449    /// appended duplicate loses to the original on lookup, and a widened
450    /// integer shifts every byte after it.
451    ///
452    /// To change a field's type on purpose, [`remove`](Self::remove) it and
453    /// [`insert`](Self::insert) it back.
454    ///
455    /// # Errors
456    ///
457    /// The walk failures [`Self::get`] lists, plus
458    /// [`GffPathError::TypeMismatch`] when `value` is not the type the field
459    /// already holds. [`GffPathError::NoSuchField`] is the ordinary way a
460    /// mistyped label arrives here rather than becoming a new field.
461    pub fn set(&mut self, path: &GffPath, value: GffValue) -> Result<GffValue, GffPathError> {
462        let depth = path.segments().len() - 1;
463        let (structure, label) = self.field_parent_mut(path, "set")?;
464        let at = sole_position(&structure.fields, &label, path, depth)?;
465        let existing = &mut structure.fields[at];
466
467        let stored = gff_value_type(&existing.value);
468        let offered = gff_value_type(&value);
469        if stored != offered {
470            return Err(GffPathError::TypeMismatch {
471                at: path.to_string(),
472                existing: stored.name(),
473                replacement: offered.name(),
474            });
475        }
476
477        Ok(std::mem::replace(&mut existing.value, value))
478    }
479
480    /// Adds a field a path names, which must not already be there.
481    ///
482    /// The value's own type decides the field's type, there being nothing to
483    /// preserve.
484    ///
485    /// On disk a struct holding one field stores that field's index directly
486    /// and a struct holding more stores an offset into the shared index array,
487    /// so adding a second field to a one-field struct changes the struct's
488    /// encoded shape. Nothing here has to arrange that: the writer picks the
489    /// shape from the field count when it encodes.
490    ///
491    /// # Errors
492    ///
493    /// The walk failures [`Self::get`] lists for the path's parent, plus
494    /// [`GffPathError::AlreadyExists`] when the struct already carries that
495    /// label. Adding a second copy is refused rather than done, because the
496    /// original would go on winning every lookup.
497    pub fn insert(&mut self, path: &GffPath, value: GffValue) -> Result<(), GffPathError> {
498        self.insert_at_position(path, value, usize::MAX)
499    }
500
501    /// Adds a field among its siblings rather than after them.
502    ///
503    /// `position` is clamped to the end, so `usize::MAX` appends and a position
504    /// from a struct that has since lost fields still lands somewhere valid.
505    ///
506    /// This exists because putting a field back is not the same operation as
507    /// adding one. A GFF struct stores its fields in order and the writer emits
508    /// them in that order, so restoring a removed field by appending produces a
509    /// file that differs from the original everywhere after the hole, which is
510    /// what an undo of a deletion did before.
511    ///
512    /// # Errors
513    ///
514    /// The same as [`Self::insert`]. `position` is clamped rather than
515    /// checked, so it is never the reason this fails.
516    pub fn insert_at_position(
517        &mut self,
518        path: &GffPath,
519        value: GffValue,
520        position: usize,
521    ) -> Result<(), GffPathError> {
522        let (structure, label) = self.field_parent_mut(path, "insert")?;
523        if structure.fields.iter().any(|field| field.label == label) {
524            return Err(GffPathError::AlreadyExists {
525                at: path.to_string(),
526            });
527        }
528        let position = position.min(structure.fields.len());
529        structure.fields.insert(position, GffField { label, value });
530        Ok(())
531    }
532
533    /// Where the field `path` names sits among its siblings.
534    ///
535    /// `None` when the parent does not resolve, the path does not end in a
536    /// label, or no such field is there.
537    pub fn position(&self, path: &GffPath) -> Option<usize> {
538        let segments = path.segments();
539        let GffPathSegment::Field(label) = segments.last()? else {
540            return None;
541        };
542        let structure = if segments.len() == 1 {
543            // A one-segment path names a field of this struct itself, and
544            // there is no parent address to resolve.
545            self
546        } else {
547            let parent = GffPath::new(segments[..segments.len() - 1].to_vec());
548            match self.get(&parent) {
549                Ok(GffValue::Struct(nested)) => nested.as_ref(),
550                _ => return None,
551            }
552        };
553        structure
554            .fields
555            .iter()
556            .position(|field| field.label == *label)
557    }
558
559    /// Removes what a path names, returning it.
560    ///
561    /// A path ending in a label removes that field. A path ending in an index
562    /// removes that list element and returns it wrapped in a
563    /// [`GffValue::Struct`].
564    ///
565    /// Removing an element does not renumber its siblings. No list in the
566    /// engine reads an element's struct id as a position, so the ids that
567    /// remain still mean what they meant.
568    ///
569    /// # Errors
570    ///
571    /// The walk failures [`Self::get`] lists. Nothing is removed when any of
572    /// them fires, so a failed remove leaves the tree as it was.
573    pub fn remove(&mut self, path: &GffPath) -> Result<GffValue, GffPathError> {
574        let segments = path.segments();
575        let Some(last) = segments.last() else {
576            return Err(GffPathError::EmptyPath);
577        };
578
579        match last {
580            GffPathSegment::Field(label) => {
581                let depth = segments.len() - 1;
582                let structure = self.walk_to_struct(path, depth)?;
583                let at = sole_position(&structure.fields, label, path, depth)?;
584                Ok(structure.fields.remove(at).value)
585            }
586            GffPathSegment::Index(index) => {
587                let list = self.walk_to_list(path, segments.len() - 1)?;
588                if *index >= list.len() {
589                    return Err(GffPathError::IndexOutOfRange {
590                        at: path.prefix(segments.len() - 1),
591                        index: *index,
592                        len: list.len(),
593                    });
594                }
595                Ok(GffValue::Struct(Box::new(list.remove(*index))))
596            }
597        }
598    }
599
600    /// Appends `element` to the list a path names, returning its index.
601    ///
602    /// The element carries its own `struct_id`, and choosing it is the caller's
603    /// job. Several lists have the engine compare that id against a per-list
604    /// constant and silently skip an element that does not match, and which
605    /// constant belongs to which list is per-format knowledge that lives above
606    /// this crate.
607    ///
608    /// # Errors
609    ///
610    /// The walk failures [`Self::get`] lists, with
611    /// [`GffPathError::NotAList`] the one that means `path` named a field
612    /// that is not a list. The element's `struct_id` is not checked against
613    /// anything, since nothing here knows which list wants which id.
614    pub fn push_element(
615        &mut self,
616        path: &GffPath,
617        element: GffStruct,
618    ) -> Result<usize, GffPathError> {
619        let list = self.walk_to_list(path, path.segments().len())?;
620        list.push(element);
621        Ok(list.len() - 1)
622    }
623
624    /// Reads the struct id of the list element a path names.
625    ///
626    /// An element's id is not a field and not a [`GffValue`], so [`get`] cannot
627    /// reach it: a path ending in an index lands on a bare [`GffStruct`]. A
628    /// nested struct *field* needs nothing special, since `get` hands back the
629    /// [`GffValue::Struct`] and the id is on the struct inside it.
630    ///
631    /// Some formats carry meaning there. A UTC's equipment slot **is** its
632    /// `Equip_ItemList` element's struct id and appears in no field, so reading
633    /// which slot an item occupies is exactly this call.
634    ///
635    /// [`get`]: Self::get
636    ///
637    /// # Errors
638    ///
639    /// The walk failures [`Self::get`] lists, plus
640    /// [`GffPathError::WrongSegmentKind`] when `path` ends in a label rather
641    /// than an index. That is the mirror of `get`'s refusal: this call takes
642    /// exactly the paths that one will not.
643    pub fn struct_id(&self, path: &GffPath) -> Result<i32, GffPathError> {
644        let (list, index) = self.element_parent(path)?;
645        list.get(index)
646            .map(|element| element.struct_id)
647            .ok_or_else(|| GffPathError::IndexOutOfRange {
648                at: path.prefix(path.segments().len() - 1),
649                index,
650                len: list.len(),
651            })
652    }
653
654    /// Sets the struct id of the list element a path names, returning the old.
655    ///
656    /// The counterpart to [`struct_id`](Self::struct_id), and the way to move a
657    /// UTC equipment item between slots: the slot is the id, so re-equipping is
658    /// a change to an existing element rather than a removal and an append.
659    ///
660    /// Which ids a list accepts is the caller's to know. Several lists have the
661    /// engine compare the id against a per-list constant and silently skip an
662    /// element that does not match, and that mapping is per-format knowledge
663    /// living above this crate.
664    ///
665    /// # Errors
666    ///
667    /// The same as [`Self::struct_id`]. `struct_id` itself is not validated,
668    /// so an id no list accepts is written without complaint.
669    pub fn set_struct_id(&mut self, path: &GffPath, struct_id: i32) -> Result<i32, GffPathError> {
670        let index = self.element_index(path)?;
671        let at = path.prefix(path.segments().len() - 1);
672        let list = self.walk_to_list(path, path.segments().len() - 1)?;
673        let len = list.len();
674        let element =
675            list.get_mut(index)
676                .ok_or(GffPathError::IndexOutOfRange { at, index, len })?;
677        Ok(std::mem::replace(&mut element.struct_id, struct_id))
678    }
679
680    /// The index a struct-id path ends with, refusing one that ends elsewhere.
681    fn element_index(&self, path: &GffPath) -> Result<usize, GffPathError> {
682        let segments = path.segments();
683        let Some(last) = segments.last() else {
684            return Err(GffPathError::EmptyPath);
685        };
686        match last {
687            GffPathSegment::Index(index) => Ok(*index),
688            GffPathSegment::Field(_) => Err(GffPathError::WrongSegmentKind {
689                at: path.to_string(),
690                operation: "struct_id",
691                expected: "a list index",
692            }),
693        }
694    }
695
696    /// Resolves a struct-id path to the list holding the element, and its index.
697    fn element_parent(&self, path: &GffPath) -> Result<(&Vec<GffStruct>, usize), GffPathError> {
698        let index = self.element_index(path)?;
699        let segments = path.segments();
700
701        // A path that is only an index has no list in front of it, which the
702        // parent walk below would report as an empty path rather than as the
703        // root not being a list.
704        if segments.len() == 1 {
705            return Err(GffPathError::IndexAtRoot {
706                path: path.to_string(),
707            });
708        }
709
710        let parent = GffPath::new(segments[..segments.len() - 1].to_vec());
711        match self.get(&parent)? {
712            GffValue::List(list) => Ok((list, index)),
713            other => Err(GffPathError::NotAList {
714                at: parent.to_string(),
715                actual: gff_value_type(other).name(),
716            }),
717        }
718    }
719
720    /// Splits a path into the struct holding its last field and that label.
721    fn field_parent_mut(
722        &mut self,
723        path: &GffPath,
724        operation: &'static str,
725    ) -> Result<(&mut GffStruct, GffLabel), GffPathError> {
726        let segments = path.segments();
727        let Some(last) = segments.last() else {
728            return Err(GffPathError::EmptyPath);
729        };
730        let GffPathSegment::Field(label) = last else {
731            return Err(GffPathError::WrongSegmentKind {
732                at: path.to_string(),
733                operation,
734                expected: "a field label",
735            });
736        };
737        let label = *label;
738        Ok((self.walk_to_struct(path, segments.len() - 1)?, label))
739    }
740
741    /// Walks the first `depth` segments and requires a struct at the end.
742    fn walk_to_struct(
743        &mut self,
744        path: &GffPath,
745        depth: usize,
746    ) -> Result<&mut GffStruct, GffPathError> {
747        match self.descend_to(path, depth)? {
748            Cursor::Struct(structure) => Ok(structure),
749            Cursor::Value(value) => {
750                let actual = gff_value_type(value).name();
751                match value {
752                    GffValue::Struct(inner) => Ok(inner.as_mut()),
753                    _ => Err(GffPathError::NotAStruct {
754                        at: path.prefix(depth),
755                        actual,
756                    }),
757                }
758            }
759        }
760    }
761
762    /// Walks the first `depth` segments and requires a list at the end.
763    fn walk_to_list(
764        &mut self,
765        path: &GffPath,
766        depth: usize,
767    ) -> Result<&mut Vec<GffStruct>, GffPathError> {
768        match self.descend_to(path, depth)? {
769            Cursor::Struct(_) => Err(GffPathError::NotAList {
770                at: path.prefix(depth),
771                actual: "a struct",
772            }),
773            Cursor::Value(value) => {
774                let actual = gff_value_type(value).name();
775                match value {
776                    GffValue::List(list) => Ok(list),
777                    _ => Err(GffPathError::NotAList {
778                        at: path.prefix(depth),
779                        actual,
780                    }),
781                }
782            }
783        }
784    }
785
786    /// Descends the first `depth` segments of `path`.
787    ///
788    /// Not to be confused with [`walk`](Self::walk), which is the public
789    /// traversal of a whole tree. This positions a cursor for one edit.
790    fn descend_to(&mut self, path: &GffPath, depth: usize) -> Result<Cursor<'_>, GffPathError> {
791        let mut cursor = Cursor::Struct(self);
792
793        for (at, segment) in path.segments().iter().take(depth).enumerate() {
794            cursor = match segment {
795                GffPathSegment::Field(label) => {
796                    let structure = match cursor {
797                        Cursor::Struct(structure) => structure,
798                        Cursor::Value(value) => {
799                            let actual = gff_value_type(value).name();
800                            match value {
801                                GffValue::Struct(inner) => inner.as_mut(),
802                                _ => {
803                                    return Err(GffPathError::NotAStruct {
804                                        at: path.prefix(at),
805                                        actual,
806                                    })
807                                }
808                            }
809                        }
810                    };
811                    let found = sole_position(&structure.fields, label, path, at)?;
812                    Cursor::Value(&mut structure.fields[found].value)
813                }
814                GffPathSegment::Index(index) => {
815                    let Cursor::Value(value) = cursor else {
816                        return Err(GffPathError::IndexAtRoot {
817                            path: path.to_string(),
818                        });
819                    };
820                    let actual = gff_value_type(value).name();
821                    let GffValue::List(list) = value else {
822                        return Err(GffPathError::NotAList {
823                            at: path.prefix(at),
824                            actual,
825                        });
826                    };
827                    let len = list.len();
828                    Cursor::Struct(list.get_mut(*index).ok_or(GffPathError::IndexOutOfRange {
829                        at: path.prefix(at),
830                        index: *index,
831                        len,
832                    })?)
833                }
834            };
835        }
836
837        Ok(cursor)
838    }
839}
840
841/// The one position `label` occupies, refusing both zero copies and several.
842///
843/// Every field lookup in this module goes through here, so an ambiguous label
844/// fails wherever it appears in a path rather than only in its last segment.
845fn sole_position(
846    fields: &[GffField],
847    label: &GffLabel,
848    path: &GffPath,
849    depth: usize,
850) -> Result<usize, GffPathError> {
851    let mut carried = fields
852        .iter()
853        .enumerate()
854        .filter(|(_, field)| field.label == *label)
855        .map(|(index, _)| index);
856
857    let first = carried.next().ok_or_else(|| GffPathError::NoSuchField {
858        at: path.prefix(depth),
859        label: label.to_string(),
860    })?;
861
862    let count = 1 + carried.count();
863    if count > 1 {
864        return Err(GffPathError::AmbiguousLabel {
865            at: path.prefix(depth),
866            label: label.to_string(),
867            count,
868        });
869    }
870    Ok(first)
871}
872
873/// Borrows a value as the struct it must be to be descended into.
874fn as_struct<'a>(
875    value: &'a GffValue,
876    path: &GffPath,
877    depth: usize,
878) -> Result<&'a GffStruct, GffPathError> {
879    match value {
880        GffValue::Struct(inner) => Ok(inner.as_ref()),
881        other => Err(GffPathError::NotAStruct {
882            at: path.prefix(depth),
883            actual: gff_value_type(other).name(),
884        }),
885    }
886}
887
888/// Borrows a value as the list it must be to be indexed.
889fn as_list<'a>(
890    value: &'a GffValue,
891    path: &GffPath,
892    depth: usize,
893) -> Result<&'a Vec<GffStruct>, GffPathError> {
894    match value {
895        GffValue::List(list) => Ok(list),
896        other => Err(GffPathError::NotAList {
897            at: path.prefix(depth),
898            actual: gff_value_type(other).name(),
899        }),
900    }
901}