Skip to main content

steel_utils/nbt/
path.rs

1use std::{error::Error, fmt};
2
3use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
4use text_components::TextComponent;
5
6use super::{
7    SnbtErrorKind, compare_nbt, nbt_list_values as list_as_tags, parse_snbt_compound_argument,
8};
9use crate::translations;
10
11/// Error returned when parsing an NBT path.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct NbtPathError {
14    cursor: usize,
15    kind: NbtPathErrorKind,
16}
17
18impl NbtPathError {
19    const fn new(cursor: usize, kind: NbtPathErrorKind) -> Self {
20        Self { cursor, kind }
21    }
22
23    /// Returns the byte cursor where parsing failed.
24    #[must_use]
25    pub const fn cursor(&self) -> usize {
26        self.cursor
27    }
28
29    /// Returns the specific parse failure.
30    #[must_use]
31    pub const fn kind(&self) -> &NbtPathErrorKind {
32        &self.kind
33    }
34
35    /// Returns the parse failure as a text component.
36    #[must_use]
37    pub fn component(&self) -> TextComponent {
38        self.kind.component()
39    }
40}
41
42impl fmt::Display for NbtPathError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(
45            f,
46            "NBT path parse error at byte {}: {}",
47            self.cursor, self.kind
48        )
49    }
50}
51
52impl Error for NbtPathError {}
53
54/// Specific reason why NBT path parsing failed.
55#[non_exhaustive]
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum NbtPathErrorKind {
58    /// Non-whitespace input remained after a complete path.
59    TrailingData,
60    /// No path node was present.
61    ExpectedPath,
62    /// A path node used invalid syntax.
63    InvalidNode,
64    /// A grammar symbol was required at the cursor.
65    ExpectedSymbol(char),
66    /// A quoted path key was required.
67    ExpectedQuotedString,
68    /// A quoted path key was not terminated.
69    UnclosedQuotedString,
70    /// A quoted path key contained an unsupported escape.
71    InvalidEscape(char),
72    /// A list index was required.
73    ExpectedIndex,
74    /// A list index could not be parsed as an integer.
75    InvalidIndex(String),
76    /// An embedded compound pattern contained invalid SNBT.
77    InvalidSnbt(SnbtErrorKind),
78}
79
80impl NbtPathErrorKind {
81    fn component(&self) -> TextComponent {
82        match self {
83            Self::TrailingData => TextComponent::from(&translations::ARGUMENT_NBT_TRAILING),
84            Self::ExpectedPath | Self::InvalidNode => {
85                TextComponent::from(&translations::ARGUMENTS_NBTPATH_NODE_INVALID)
86            }
87            Self::ExpectedIndex => TextComponent::from(&translations::PARSING_INT_EXPECTED),
88            Self::InvalidIndex(value) => translations::PARSING_INT_INVALID
89                .message([value.to_owned()])
90                .component(),
91            Self::ExpectedSymbol(symbol) => translations::PARSING_EXPECTED
92                .message([symbol.to_string()])
93                .component(),
94            Self::ExpectedQuotedString => {
95                TextComponent::from(&translations::PARSING_QUOTE_EXPECTED_START)
96            }
97            Self::UnclosedQuotedString => {
98                TextComponent::from(&translations::PARSING_QUOTE_EXPECTED_END)
99            }
100            Self::InvalidEscape(character) => translations::PARSING_QUOTE_ESCAPE
101                .message([character.to_string()])
102                .component(),
103            Self::InvalidSnbt(kind) => kind.component(),
104        }
105    }
106}
107
108impl fmt::Display for NbtPathErrorKind {
109    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::TrailingData => formatter.write_str("trailing data"),
112            Self::ExpectedPath => formatter.write_str("expected NBT path"),
113            Self::InvalidNode => formatter.write_str("invalid NBT path node"),
114            Self::ExpectedSymbol(symbol) => write!(formatter, "expected '{symbol}'"),
115            Self::ExpectedQuotedString => formatter.write_str("expected quoted string"),
116            Self::UnclosedQuotedString => formatter.write_str("unclosed quoted string"),
117            Self::InvalidEscape(character) => write!(formatter, "invalid escape '{character}'"),
118            Self::ExpectedIndex => formatter.write_str("expected list index"),
119            Self::InvalidIndex(value) => write!(formatter, "invalid list index '{value}'"),
120            Self::InvalidSnbt(kind) => fmt::Display::fmt(kind, formatter),
121        }
122    }
123}
124
125/// Error returned when mutating NBT through a path.
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum NbtPathMutationError {
128    /// An intermediate path node did not resolve to any tag.
129    NothingFound(String),
130    /// The inserted value would exceed vanilla's maximum NBT depth.
131    TooDeep,
132}
133
134impl fmt::Display for NbtPathMutationError {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Self::NothingFound(path) => write!(f, "nothing found at NBT path '{path}'"),
138            Self::TooDeep => write!(f, "NBT path mutation would exceed maximum depth"),
139        }
140    }
141}
142
143impl Error for NbtPathMutationError {}
144
145/// Parsed vanilla NBT path.
146#[derive(Clone, Debug, PartialEq)]
147pub struct NbtPath {
148    original: String,
149    nodes: Vec<NbtPathNodeEntry>,
150}
151
152#[derive(Clone, Debug, PartialEq)]
153struct NbtPathNodeEntry {
154    node: NbtPathNode,
155    original_end: usize,
156}
157
158impl NbtPath {
159    /// Returns the path as written in command input.
160    #[must_use]
161    pub fn as_str(&self) -> &str {
162        &self.original
163    }
164
165    /// Returns cloned tags selected by this path.
166    #[must_use]
167    pub fn get(&self, tag: &NbtTag) -> Vec<NbtTag> {
168        let mut tags = vec![tag.clone()];
169        for entry in &self.nodes {
170            tags = entry.node.get(&tags);
171            if tags.is_empty() {
172                break;
173            }
174        }
175        tags
176    }
177
178    /// Returns the number of tags matched by this path.
179    #[must_use]
180    pub fn count_matching(&self, tag: &NbtTag) -> usize {
181        let mut tags = vec![tag.clone()];
182        for entry in &self.nodes {
183            tags = entry.node.get(&tags);
184            if tags.is_empty() {
185                return 0;
186            }
187        }
188        tags.len()
189    }
190
191    /// Sets every tag selected by this path to `value`.
192    ///
193    /// Missing intermediate compound/list parents are created the same way
194    /// vanilla's `NbtPathArgument.NbtPath#set` creates them. The return value is
195    /// the number of changed target tags.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if an intermediate path node cannot resolve or if the
200    /// inserted value would exceed vanilla's maximum NBT depth.
201    pub fn set(&self, tag: &mut NbtTag, value: NbtTag) -> Result<usize, NbtPathMutationError> {
202        if is_too_deep(&value, self.nodes.len()) {
203            return Err(NbtPathMutationError::TooDeep);
204        }
205
206        let Some((last, parents)) = self.nodes.split_last() else {
207            return Ok(0);
208        };
209        if parents.is_empty() {
210            return Ok(last.node.set_tag(tag, &value));
211        }
212
213        self.set_at(tag, 0, &value).map(|outcome| outcome.modified)
214    }
215
216    fn set_at(
217        &self,
218        tag: &mut NbtTag,
219        node_index: usize,
220        value: &NbtTag,
221    ) -> Result<SetOutcome, NbtPathMutationError> {
222        let entry = &self.nodes[node_index];
223        if node_index + 1 == self.nodes.len() {
224            return Ok(SetOutcome {
225                found: true,
226                modified: entry.node.set_tag(tag, value),
227            });
228        }
229
230        let preferred_child = self.nodes[node_index + 1]
231            .node
232            .create_preferred_parent_tag();
233        let mut modified = 0;
234        let mut deeper_found = false;
235        let found = entry
236            .node
237            .get_or_create_tag(tag, &preferred_child, |child| {
238                let outcome = self.set_at(child, node_index + 1, value)?;
239                modified += outcome.modified;
240                deeper_found |= outcome.found;
241                Ok(outcome.found)
242            })?;
243        if !found {
244            return Err(NbtPathMutationError::NothingFound(
245                self.original[..entry.original_end].to_owned(),
246            ));
247        }
248
249        Ok(SetOutcome {
250            found: deeper_found,
251            modified,
252        })
253    }
254}
255
256struct SetOutcome {
257    found: bool,
258    modified: usize,
259}
260
261/// Parses one complete NBT path.
262///
263/// # Errors
264///
265/// Returns an error when the input is not a valid NBT path or has trailing data.
266pub fn parse_nbt_path(input: &str) -> Result<NbtPath, NbtPathError> {
267    let (path, cursor) = parse_nbt_path_argument(input)?;
268    if cursor != input.len() {
269        return Err(NbtPathError::new(cursor, NbtPathErrorKind::TrailingData));
270    }
271    Ok(path)
272}
273
274/// Parses one NBT path and returns the byte cursor consumed by it.
275///
276/// # Errors
277///
278/// Returns an error when the input does not start with a valid NBT path.
279pub fn parse_nbt_path_argument(input: &str) -> Result<(NbtPath, usize), NbtPathError> {
280    let mut parser = Parser::new(input);
281    let path = parser.parse()?;
282    Ok((path, parser.cursor))
283}
284
285#[derive(Clone, Debug, PartialEq)]
286enum NbtPathNode {
287    CompoundChild(String),
288    MatchObject { name: String, pattern: NbtCompound },
289    MatchRootObject(NbtCompound),
290    AllElements,
291    IndexedElement(i32),
292    MatchElement(NbtCompound),
293}
294
295impl NbtPathNode {
296    fn get(&self, input: &[NbtTag]) -> Vec<NbtTag> {
297        let mut output = Vec::new();
298        for tag in input {
299            match self {
300                Self::CompoundChild(name) => {
301                    if let NbtTag::Compound(compound) = tag
302                        && let Some(child) = compound.get(name)
303                    {
304                        output.push(child.clone());
305                    }
306                }
307                Self::MatchObject { name, pattern } => {
308                    if let NbtTag::Compound(compound) = tag
309                        && let Some(child) = compound.get(name)
310                        && compound_pattern_matches(pattern, child)
311                    {
312                        output.push(child.clone());
313                    }
314                }
315                Self::MatchRootObject(pattern) => {
316                    if compound_pattern_matches(pattern, tag) {
317                        output.push(tag.clone());
318                    }
319                }
320                Self::AllElements => {
321                    output.extend(collection_elements(tag));
322                }
323                Self::IndexedElement(index) => {
324                    let elements = collection_elements(tag);
325                    if let Some(element) = indexed_element(&elements, *index) {
326                        output.push(element);
327                    }
328                }
329                Self::MatchElement(pattern) => {
330                    output.extend(
331                        collection_elements(tag)
332                            .into_iter()
333                            .filter(|tag| compound_pattern_matches(pattern, tag)),
334                    );
335                }
336            }
337        }
338        output
339    }
340
341    fn get_or_create_tag(
342        &self,
343        parent: &mut NbtTag,
344        child: &NbtTag,
345        mut visitor: impl FnMut(&mut NbtTag) -> Result<bool, NbtPathMutationError>,
346    ) -> Result<bool, NbtPathMutationError> {
347        match self {
348            Self::CompoundChild(name) => {
349                let NbtTag::Compound(compound) = parent else {
350                    return Ok(false);
351                };
352                ensure_compound_child(compound, name, child.clone());
353                let Some(tag) = compound.get_mut(name) else {
354                    return Ok(false);
355                };
356                visitor(tag)
357            }
358            Self::MatchObject { name, pattern } => {
359                let NbtTag::Compound(compound) = parent else {
360                    return Ok(false);
361                };
362                if !compound.contains(name) {
363                    compound.insert(name.as_str(), NbtTag::Compound(pattern.clone()));
364                }
365                let Some(tag) = compound.get_mut(name) else {
366                    return Ok(false);
367                };
368                if compound_pattern_matches(pattern, tag) {
369                    visitor(tag)
370                } else {
371                    Ok(false)
372                }
373            }
374            Self::MatchRootObject(pattern) => {
375                if compound_pattern_matches(pattern, parent) {
376                    visitor(parent)
377                } else {
378                    Ok(false)
379                }
380            }
381            Self::AllElements => visit_all_collection_elements(parent, child, visitor),
382            Self::IndexedElement(index) => {
383                visit_indexed_collection_element(parent, *index, visitor)
384            }
385            Self::MatchElement(pattern) => visit_matching_list_elements(parent, pattern, visitor),
386        }
387    }
388
389    fn create_preferred_parent_tag(&self) -> NbtTag {
390        match self {
391            Self::CompoundChild(_) | Self::MatchObject { .. } | Self::MatchRootObject(_) => {
392                NbtTag::Compound(NbtCompound::new())
393            }
394            Self::AllElements | Self::IndexedElement(_) | Self::MatchElement(_) => {
395                NbtTag::List(NbtList::default())
396            }
397        }
398    }
399
400    fn set_tag(&self, parent: &mut NbtTag, value: &NbtTag) -> usize {
401        match self {
402            Self::CompoundChild(name) => set_compound_child(parent, name, value),
403            Self::MatchObject { name, pattern } => {
404                set_matching_compound_child(parent, name, pattern, value)
405            }
406            Self::MatchRootObject(_) => 0,
407            Self::AllElements => set_all_collection_elements(parent, value),
408            Self::IndexedElement(index) => set_indexed_collection_element(parent, *index, value),
409            Self::MatchElement(pattern) => set_matching_list_elements(parent, pattern, value),
410        }
411    }
412}
413
414fn compound_pattern_matches(pattern: &NbtCompound, tag: &NbtTag) -> bool {
415    compare_nbt(Some(&NbtTag::Compound(pattern.clone())), Some(tag), true)
416}
417
418fn ensure_compound_child(compound: &mut NbtCompound, name: &str, child: NbtTag) {
419    if !compound.contains(name) {
420        compound.insert(name, child);
421    }
422}
423
424fn set_compound_child(parent: &mut NbtTag, name: &str, value: &NbtTag) -> usize {
425    let NbtTag::Compound(compound) = parent else {
426        return 0;
427    };
428
429    if compound.get(name).is_some_and(|current| current == value) {
430        return 0;
431    }
432    if let Some(current) = compound.get_mut(name) {
433        *current = value.clone();
434    } else {
435        compound.insert(name, value.clone());
436    }
437    1
438}
439
440fn set_matching_compound_child(
441    parent: &mut NbtTag,
442    name: &str,
443    pattern: &NbtCompound,
444    value: &NbtTag,
445) -> usize {
446    let NbtTag::Compound(compound) = parent else {
447        return 0;
448    };
449    let Some(current) = compound.get_mut(name) else {
450        return 0;
451    };
452    if !compound_pattern_matches(pattern, current) || current == value {
453        return 0;
454    }
455
456    *current = value.clone();
457    1
458}
459
460fn visit_all_collection_elements(
461    parent: &mut NbtTag,
462    child: &NbtTag,
463    mut visitor: impl FnMut(&mut NbtTag) -> Result<bool, NbtPathMutationError>,
464) -> Result<bool, NbtPathMutationError> {
465    ensure_non_empty_collection(parent, child);
466    let Some(mut elements) = collection_elements_for_mutation(parent) else {
467        return Ok(false);
468    };
469    if elements.is_empty() {
470        return Ok(false);
471    }
472
473    let mut found = false;
474    let mut changed = false;
475    for element in &mut elements {
476        let before = element.clone();
477        found |= visitor(element)?;
478        changed |= *element != before;
479    }
480    if changed {
481        replace_collection(parent, elements);
482    }
483    Ok(found)
484}
485
486fn visit_indexed_collection_element(
487    parent: &mut NbtTag,
488    index: i32,
489    mut visitor: impl FnMut(&mut NbtTag) -> Result<bool, NbtPathMutationError>,
490) -> Result<bool, NbtPathMutationError> {
491    let Some(mut elements) = collection_elements_for_mutation(parent) else {
492        return Ok(false);
493    };
494    let Some(actual_index) = actual_collection_index(elements.len(), index) else {
495        return Ok(false);
496    };
497
498    let before = elements[actual_index].clone();
499    let found = visitor(&mut elements[actual_index])?;
500    if elements[actual_index] != before {
501        replace_collection(parent, elements);
502    }
503    Ok(found)
504}
505
506fn visit_matching_list_elements(
507    parent: &mut NbtTag,
508    pattern: &NbtCompound,
509    mut visitor: impl FnMut(&mut NbtTag) -> Result<bool, NbtPathMutationError>,
510) -> Result<bool, NbtPathMutationError> {
511    let NbtTag::List(list) = parent else {
512        return Ok(false);
513    };
514
515    ensure_matching_list_element(list, pattern);
516    let mut elements = list_as_tags(list);
517    let mut found = false;
518    let mut changed = false;
519    for element in elements
520        .iter_mut()
521        .filter(|element| compound_pattern_matches(pattern, element))
522    {
523        let before = element.clone();
524        found |= visitor(element)?;
525        changed |= *element != before;
526    }
527    if changed {
528        *list = NbtList::from(elements);
529    }
530    Ok(found)
531}
532
533fn ensure_non_empty_collection(parent: &mut NbtTag, child: &NbtTag) {
534    if collection_len(parent) != Some(0) {
535        return;
536    }
537    add_collection_element(parent, child.clone());
538}
539
540fn ensure_matching_list_element(list: &mut NbtList, pattern: &NbtCompound) {
541    let mut elements = list_as_tags(list);
542    if elements
543        .iter()
544        .any(|element| compound_pattern_matches(pattern, element))
545    {
546        return;
547    }
548
549    elements.push(NbtTag::Compound(pattern.clone()));
550    *list = NbtList::from(elements);
551}
552
553fn set_all_collection_elements(parent: &mut NbtTag, value: &NbtTag) -> usize {
554    let Some(len) = collection_len(parent) else {
555        return 0;
556    };
557    if len == 0 {
558        add_collection_element(parent, value.clone());
559        return 1;
560    }
561
562    let Some(elements) = collection_elements_for_mutation(parent) else {
563        return 0;
564    };
565    let changed_count = elements.iter().filter(|element| *element != value).count();
566    if changed_count == 0 {
567        return 0;
568    }
569
570    if replace_collection_with_repeated(parent, len, value) {
571        changed_count
572    } else {
573        clear_collection(parent);
574        0
575    }
576}
577
578fn set_indexed_collection_element(parent: &mut NbtTag, index: i32, value: &NbtTag) -> usize {
579    let Some(len) = collection_len(parent) else {
580        return 0;
581    };
582    let Some(actual_index) = actual_collection_index(len, index) else {
583        return 0;
584    };
585    if collection_element(parent, actual_index).is_some_and(|current| &current == value) {
586        return 0;
587    }
588
589    usize::from(set_collection_element(parent, actual_index, value.clone()))
590}
591
592fn set_matching_list_elements(parent: &mut NbtTag, pattern: &NbtCompound, value: &NbtTag) -> usize {
593    let NbtTag::List(list) = parent else {
594        return 0;
595    };
596    if list_is_empty(list) {
597        return usize::from(add_list_element(list, value.clone()));
598    }
599
600    let mut elements = list_as_tags(list);
601    let mut changed = 0;
602    for element in &mut elements {
603        if !compound_pattern_matches(pattern, element) || element == value {
604            continue;
605        }
606        *element = value.clone();
607        changed += 1;
608    }
609    if changed > 0 {
610        *list = NbtList::from(elements);
611    }
612    changed
613}
614
615const fn collection_len(tag: &NbtTag) -> Option<usize> {
616    match tag {
617        NbtTag::List(list) => Some(list_len(list)),
618        NbtTag::ByteArray(values) => Some(values.len()),
619        NbtTag::IntArray(values) => Some(values.len()),
620        NbtTag::LongArray(values) => Some(values.len()),
621        _ => None,
622    }
623}
624
625const fn list_len(list: &NbtList) -> usize {
626    match list {
627        NbtList::Empty => 0,
628        NbtList::Byte(values) => values.len(),
629        NbtList::Short(values) => values.len(),
630        NbtList::Int(values) => values.len(),
631        NbtList::Long(values) => values.len(),
632        NbtList::Float(values) => values.len(),
633        NbtList::Double(values) => values.len(),
634        NbtList::ByteArray(values) => values.len(),
635        NbtList::String(values) => values.len(),
636        NbtList::List(values) => values.len(),
637        NbtList::Compound(values) => values.len(),
638        NbtList::IntArray(values) => values.len(),
639        NbtList::LongArray(values) => values.len(),
640    }
641}
642
643const fn list_is_empty(list: &NbtList) -> bool {
644    list_len(list) == 0
645}
646
647fn actual_collection_index(len: usize, index: i32) -> Option<usize> {
648    let actual_index = if index < 0 {
649        len.checked_add_signed(index as isize)?
650    } else {
651        usize::try_from(index).ok()?
652    };
653    (actual_index < len).then_some(actual_index)
654}
655
656fn collection_element(tag: &NbtTag, index: usize) -> Option<NbtTag> {
657    match tag {
658        NbtTag::List(list) => list_as_tags(list).get(index).cloned(),
659        NbtTag::ByteArray(values) => values.get(index).map(|value| NbtTag::Byte(*value as i8)),
660        NbtTag::IntArray(values) => values.get(index).copied().map(NbtTag::Int),
661        NbtTag::LongArray(values) => values.get(index).copied().map(NbtTag::Long),
662        _ => None,
663    }
664}
665
666fn collection_elements_for_mutation(tag: &NbtTag) -> Option<Vec<NbtTag>> {
667    match tag {
668        NbtTag::List(list) => Some(list_as_tags(list)),
669        NbtTag::ByteArray(values) => Some(
670            values
671                .iter()
672                .map(|value| NbtTag::Byte(*value as i8))
673                .collect(),
674        ),
675        NbtTag::IntArray(values) => Some(values.iter().copied().map(NbtTag::Int).collect()),
676        NbtTag::LongArray(values) => Some(values.iter().copied().map(NbtTag::Long).collect()),
677        _ => None,
678    }
679}
680
681fn replace_collection(parent: &mut NbtTag, elements: Vec<NbtTag>) {
682    match parent {
683        NbtTag::List(list) => *list = NbtList::from(elements),
684        NbtTag::ByteArray(values) => {
685            if let Some(replacement) = tags_to_byte_array(elements) {
686                *values = replacement;
687            }
688        }
689        NbtTag::IntArray(values) => {
690            if let Some(replacement) = tags_to_int_array(elements) {
691                *values = replacement;
692            }
693        }
694        NbtTag::LongArray(values) => {
695            if let Some(replacement) = tags_to_long_array(elements) {
696                *values = replacement;
697            }
698        }
699        _ => {}
700    }
701}
702
703fn replace_collection_with_repeated(parent: &mut NbtTag, len: usize, value: &NbtTag) -> bool {
704    match parent {
705        NbtTag::List(list) => {
706            *list = NbtList::from(vec![value.clone(); len]);
707            true
708        }
709        NbtTag::ByteArray(values) => {
710            let Some(value) = nbt_byte_value(value) else {
711                return false;
712            };
713            *values = vec![value as u8; len];
714            true
715        }
716        NbtTag::IntArray(values) => {
717            let Some(value) = nbt_int_value(value) else {
718                return false;
719            };
720            *values = vec![value; len];
721            true
722        }
723        NbtTag::LongArray(values) => {
724            let Some(value) = nbt_long_value(value) else {
725                return false;
726            };
727            *values = vec![value; len];
728            true
729        }
730        _ => false,
731    }
732}
733
734fn clear_collection(parent: &mut NbtTag) {
735    match parent {
736        NbtTag::List(list) => clear_list(list),
737        NbtTag::ByteArray(values) => values.clear(),
738        NbtTag::IntArray(values) => values.clear(),
739        NbtTag::LongArray(values) => values.clear(),
740        _ => {}
741    }
742}
743
744fn clear_list(list: &mut NbtList) {
745    match list {
746        NbtList::Empty => {}
747        NbtList::Byte(values) => values.clear(),
748        NbtList::Short(values) => values.clear(),
749        NbtList::Int(values) => values.clear(),
750        NbtList::Long(values) => values.clear(),
751        NbtList::Float(values) => values.clear(),
752        NbtList::Double(values) => values.clear(),
753        NbtList::ByteArray(values) => values.clear(),
754        NbtList::String(values) => values.clear(),
755        NbtList::List(values) => values.clear(),
756        NbtList::Compound(values) => values.clear(),
757        NbtList::IntArray(values) => values.clear(),
758        NbtList::LongArray(values) => values.clear(),
759    }
760}
761
762fn add_collection_element(parent: &mut NbtTag, value: NbtTag) -> bool {
763    match parent {
764        NbtTag::List(list) => add_list_element(list, value),
765        NbtTag::ByteArray(values) => {
766            let Some(value) = nbt_byte_value(&value) else {
767                return false;
768            };
769            values.push(value as u8);
770            true
771        }
772        NbtTag::IntArray(values) => {
773            let Some(value) = nbt_int_value(&value) else {
774                return false;
775            };
776            values.push(value);
777            true
778        }
779        NbtTag::LongArray(values) => {
780            let Some(value) = nbt_long_value(&value) else {
781                return false;
782            };
783            values.push(value);
784            true
785        }
786        _ => false,
787    }
788}
789
790fn add_list_element(list: &mut NbtList, value: NbtTag) -> bool {
791    let mut elements = list_as_tags(list);
792    elements.push(value);
793    *list = NbtList::from(elements);
794    true
795}
796
797fn set_collection_element(parent: &mut NbtTag, index: usize, value: NbtTag) -> bool {
798    match parent {
799        NbtTag::List(list) => set_list_element(list, index, value),
800        NbtTag::ByteArray(values) => {
801            let Some(value) = nbt_byte_value(&value) else {
802                return false;
803            };
804            let Some(current) = values.get_mut(index) else {
805                return false;
806            };
807            *current = value as u8;
808            true
809        }
810        NbtTag::IntArray(values) => {
811            let Some(value) = nbt_int_value(&value) else {
812                return false;
813            };
814            let Some(current) = values.get_mut(index) else {
815                return false;
816            };
817            *current = value;
818            true
819        }
820        NbtTag::LongArray(values) => {
821            let Some(value) = nbt_long_value(&value) else {
822                return false;
823            };
824            let Some(current) = values.get_mut(index) else {
825                return false;
826            };
827            *current = value;
828            true
829        }
830        _ => false,
831    }
832}
833
834fn set_list_element(list: &mut NbtList, index: usize, value: NbtTag) -> bool {
835    let mut elements = list_as_tags(list);
836    let Some(current) = elements.get_mut(index) else {
837        return false;
838    };
839    *current = value;
840    *list = NbtList::from(elements);
841    true
842}
843
844fn tags_to_byte_array(tags: Vec<NbtTag>) -> Option<Vec<u8>> {
845    tags.into_iter()
846        .map(|tag| {
847            let value = nbt_byte_value(&tag)?;
848            Some(value as u8)
849        })
850        .collect()
851}
852
853fn tags_to_int_array(tags: Vec<NbtTag>) -> Option<Vec<i32>> {
854    tags.into_iter().map(|tag| nbt_int_value(&tag)).collect()
855}
856
857fn tags_to_long_array(tags: Vec<NbtTag>) -> Option<Vec<i64>> {
858    tags.into_iter().map(|tag| nbt_long_value(&tag)).collect()
859}
860
861const fn nbt_byte_value(tag: &NbtTag) -> Option<i8> {
862    Some(match tag {
863        NbtTag::Byte(value) => *value,
864        NbtTag::Short(value) => *value as u8 as i8,
865        NbtTag::Int(value) => *value as u8 as i8,
866        NbtTag::Long(value) => *value as u8 as i8,
867        NbtTag::Float(value) => value.floor() as i32 as u8 as i8,
868        NbtTag::Double(value) => value.floor() as i32 as u8 as i8,
869        _ => return None,
870    })
871}
872
873fn nbt_int_value(tag: &NbtTag) -> Option<i32> {
874    Some(match tag {
875        NbtTag::Byte(value) => i32::from(*value),
876        NbtTag::Short(value) => i32::from(*value),
877        NbtTag::Int(value) => *value,
878        NbtTag::Long(value) => *value as i32,
879        NbtTag::Float(value) => value.floor() as i32,
880        NbtTag::Double(value) => value.floor() as i32,
881        _ => return None,
882    })
883}
884
885fn nbt_long_value(tag: &NbtTag) -> Option<i64> {
886    Some(match tag {
887        NbtTag::Byte(value) => i64::from(*value),
888        NbtTag::Short(value) => i64::from(*value),
889        NbtTag::Int(value) => i64::from(*value),
890        NbtTag::Long(value) => *value,
891        NbtTag::Float(value) => *value as i64,
892        NbtTag::Double(value) => value.floor() as i64,
893        _ => return None,
894    })
895}
896
897fn is_too_deep(tag: &NbtTag, depth: usize) -> bool {
898    if depth >= 512 {
899        return true;
900    }
901
902    match tag {
903        NbtTag::Compound(compound) => compound.values().any(|child| is_too_deep(child, depth + 1)),
904        NbtTag::List(list) => list_as_tags(list)
905            .iter()
906            .any(|child| is_too_deep(child, depth + 1)),
907        _ => false,
908    }
909}
910
911fn indexed_element(elements: &[NbtTag], index: i32) -> Option<NbtTag> {
912    let actual_index = if index < 0 {
913        elements.len().checked_add_signed(index as isize)?
914    } else {
915        usize::try_from(index).ok()?
916    };
917    elements.get(actual_index).cloned()
918}
919
920fn collection_elements(tag: &NbtTag) -> Vec<NbtTag> {
921    match tag {
922        NbtTag::List(list) => list_as_tags(list),
923        NbtTag::ByteArray(values) => values
924            .iter()
925            .map(|value| NbtTag::Byte(*value as i8))
926            .collect(),
927        NbtTag::IntArray(values) => values.iter().copied().map(NbtTag::Int).collect(),
928        NbtTag::LongArray(values) => values.iter().copied().map(NbtTag::Long).collect(),
929        _ => Vec::new(),
930    }
931}
932
933struct Parser<'a> {
934    input: &'a str,
935    cursor: usize,
936}
937
938impl<'a> Parser<'a> {
939    const fn new(input: &'a str) -> Self {
940        Self { input, cursor: 0 }
941    }
942
943    fn parse(&mut self) -> Result<NbtPath, NbtPathError> {
944        let start = self.cursor;
945        let mut nodes = Vec::new();
946        let mut first_node = true;
947
948        while self.can_read() && self.peek() != Some(' ') {
949            let node = self.parse_node(first_node)?;
950            nodes.push(NbtPathNodeEntry {
951                node,
952                original_end: self.cursor - start,
953            });
954            first_node = false;
955
956            if self.can_read() {
957                let Some(next) = self.peek() else {
958                    break;
959                };
960                if next != ' ' && next != '[' && next != '{' {
961                    self.expect_char('.')?;
962                }
963            }
964        }
965
966        if nodes.is_empty() {
967            return Err(self.error(NbtPathErrorKind::ExpectedPath));
968        }
969
970        Ok(NbtPath {
971            original: self.input[start..self.cursor].to_owned(),
972            nodes,
973        })
974    }
975
976    fn parse_node(&mut self, first_node: bool) -> Result<NbtPathNode, NbtPathError> {
977        match self.peek() {
978            Some('"' | '\'') => {
979                let name = self.parse_quoted_string()?;
980                self.read_object_node(name)
981            }
982            Some('[') => self.parse_element_node(),
983            Some('{') => {
984                if !first_node {
985                    return Err(self.error(NbtPathErrorKind::InvalidNode));
986                }
987                let pattern = self.parse_compound_pattern()?;
988                Ok(NbtPathNode::MatchRootObject(pattern))
989            }
990            Some(_) => {
991                let name = self.parse_unquoted_name()?;
992                self.read_object_node(name)
993            }
994            None => Err(self.error(NbtPathErrorKind::ExpectedPath)),
995        }
996    }
997
998    fn read_object_node(&mut self, name: String) -> Result<NbtPathNode, NbtPathError> {
999        if name.is_empty() {
1000            return Err(self.error(NbtPathErrorKind::ExpectedPath));
1001        }
1002        if self.peek() == Some('{') {
1003            let pattern = self.parse_compound_pattern()?;
1004            Ok(NbtPathNode::MatchObject { name, pattern })
1005        } else {
1006            Ok(NbtPathNode::CompoundChild(name))
1007        }
1008    }
1009
1010    fn parse_element_node(&mut self) -> Result<NbtPathNode, NbtPathError> {
1011        self.expect_char('[')?;
1012        match self.peek() {
1013            Some('{') => {
1014                let pattern = self.parse_compound_pattern()?;
1015                self.expect_char(']')?;
1016                Ok(NbtPathNode::MatchElement(pattern))
1017            }
1018            Some(']') => {
1019                self.read();
1020                Ok(NbtPathNode::AllElements)
1021            }
1022            _ => {
1023                let index = self.parse_i32()?;
1024                self.expect_char(']')?;
1025                Ok(NbtPathNode::IndexedElement(index))
1026            }
1027        }
1028    }
1029
1030    fn parse_compound_pattern(&mut self) -> Result<NbtCompound, NbtPathError> {
1031        let start = self.cursor;
1032        let (compound, consumed) =
1033            parse_snbt_compound_argument(&self.input[start..]).map_err(|error| {
1034                let cursor = error.cursor();
1035                NbtPathError::new(
1036                    start + cursor,
1037                    NbtPathErrorKind::InvalidSnbt(error.into_kind()),
1038                )
1039            })?;
1040        self.cursor += consumed;
1041        Ok(compound)
1042    }
1043
1044    fn parse_unquoted_name(&mut self) -> Result<String, NbtPathError> {
1045        let start = self.cursor;
1046        while self.peek().is_some_and(is_allowed_in_unquoted_name) {
1047            self.read();
1048        }
1049        if self.cursor == start {
1050            return Err(self.error(NbtPathErrorKind::InvalidNode));
1051        }
1052        Ok(self.input[start..self.cursor].to_owned())
1053    }
1054
1055    fn parse_quoted_string(&mut self) -> Result<String, NbtPathError> {
1056        let Some(terminator) = self.peek().filter(|ch| matches!(ch, '"' | '\'')) else {
1057            return Err(self.error(NbtPathErrorKind::ExpectedQuotedString));
1058        };
1059        self.read();
1060
1061        let mut value = String::new();
1062        while let Some(ch) = self.read() {
1063            match ch {
1064                ch if ch == terminator => return Ok(value),
1065                '\\' => {
1066                    let escape_cursor = self.cursor;
1067                    let escaped = self
1068                        .read()
1069                        .ok_or_else(|| self.error(NbtPathErrorKind::UnclosedQuotedString))?;
1070                    if escaped != terminator && escaped != '\\' {
1071                        return Err(Self::error_at(
1072                            escape_cursor,
1073                            NbtPathErrorKind::InvalidEscape(escaped),
1074                        ));
1075                    }
1076                    value.push(escaped);
1077                }
1078                _ => value.push(ch),
1079            }
1080        }
1081
1082        Err(self.error(NbtPathErrorKind::UnclosedQuotedString))
1083    }
1084
1085    fn parse_i32(&mut self) -> Result<i32, NbtPathError> {
1086        let start = self.cursor;
1087        while self
1088            .peek()
1089            .is_some_and(|ch| ch.is_ascii_digit() || matches!(ch, '.' | '-'))
1090        {
1091            self.read();
1092        }
1093        let value = &self.input[start..self.cursor];
1094        if value.is_empty() {
1095            return Err(Self::error_at(start, NbtPathErrorKind::ExpectedIndex));
1096        }
1097        if let Ok(value) = value.parse() {
1098            return Ok(value);
1099        }
1100        let value = value.to_owned();
1101        self.cursor = start;
1102        Err(Self::error_at(start, NbtPathErrorKind::InvalidIndex(value)))
1103    }
1104
1105    const fn can_read(&self) -> bool {
1106        self.cursor < self.input.len()
1107    }
1108
1109    fn peek(&self) -> Option<char> {
1110        self.input[self.cursor..].chars().next()
1111    }
1112
1113    fn read(&mut self) -> Option<char> {
1114        let ch = self.peek()?;
1115        self.cursor += ch.len_utf8();
1116        Some(ch)
1117    }
1118
1119    fn expect_char(&mut self, expected: char) -> Result<(), NbtPathError> {
1120        if self.peek() == Some(expected) {
1121            self.read();
1122            Ok(())
1123        } else {
1124            Err(self.error(NbtPathErrorKind::ExpectedSymbol(expected)))
1125        }
1126    }
1127
1128    const fn error(&self, kind: NbtPathErrorKind) -> NbtPathError {
1129        NbtPathError::new(self.cursor, kind)
1130    }
1131
1132    const fn error_at(cursor: usize, kind: NbtPathErrorKind) -> NbtPathError {
1133        NbtPathError::new(cursor, kind)
1134    }
1135}
1136
1137const fn is_allowed_in_unquoted_name(ch: char) -> bool {
1138    !matches!(ch, ' ' | '"' | '\'' | '[' | ']' | '.' | '{' | '}')
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143    use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
1144
1145    use super::*;
1146
1147    fn compound(entries: impl IntoIterator<Item = (&'static str, NbtTag)>) -> NbtTag {
1148        let mut compound = NbtCompound::new();
1149        for (key, tag) in entries {
1150            compound.insert(key, tag);
1151        }
1152        NbtTag::Compound(compound)
1153    }
1154
1155    fn list(entries: impl IntoIterator<Item = NbtTag>) -> NbtTag {
1156        NbtTag::List(NbtList::from(entries.into_iter().collect::<Vec<_>>()))
1157    }
1158
1159    #[test]
1160    fn parses_path_argument_without_consuming_separator() {
1161        let (path, cursor) = parse_nbt_path_argument("foo.bar run").expect("path parses");
1162
1163        assert_eq!(path.as_str(), "foo.bar");
1164        assert_eq!(cursor, 7);
1165    }
1166
1167    #[test]
1168    fn counts_compound_child_matches() {
1169        let path = parse_nbt_path("foo.bar").expect("path parses");
1170        let tag = compound([("foo", compound([("bar", NbtTag::Int(3))]))]);
1171
1172        assert_eq!(path.count_matching(&tag), 1);
1173        assert_eq!(path.get(&tag), vec![NbtTag::Int(3)]);
1174    }
1175
1176    #[test]
1177    fn counts_list_index_and_wildcard_matches() {
1178        let path = parse_nbt_path("items[1].id").expect("path parses");
1179        let wildcard = parse_nbt_path("items[].id").expect("path parses");
1180        let tag = compound([(
1181            "items",
1182            list([
1183                compound([("id", NbtTag::String("first".into()))]),
1184                compound([("id", NbtTag::String("second".into()))]),
1185            ]),
1186        )]);
1187
1188        assert_eq!(path.get(&tag), vec![NbtTag::String("second".into())]);
1189        assert_eq!(wildcard.count_matching(&tag), 2);
1190    }
1191
1192    #[test]
1193    fn negative_indices_select_from_end() {
1194        let path = parse_nbt_path("items[-1]").expect("path parses");
1195        let tag = compound([(
1196            "items",
1197            list([NbtTag::Int(1), NbtTag::Int(2), NbtTag::Int(3)]),
1198        )]);
1199
1200        assert_eq!(path.get(&tag), vec![NbtTag::Int(3)]);
1201    }
1202
1203    #[test]
1204    fn predicate_nodes_use_partial_compound_matching() {
1205        let path = parse_nbt_path("items[{id:\"minecraft:stone\"}].Count").expect("path parses");
1206        let tag = compound([(
1207            "items",
1208            list([
1209                compound([
1210                    ("id", NbtTag::String("minecraft:dirt".into())),
1211                    ("Count", NbtTag::Byte(1)),
1212                ]),
1213                compound([
1214                    ("id", NbtTag::String("minecraft:stone".into())),
1215                    ("Count", NbtTag::Byte(4)),
1216                    ("Slot", NbtTag::Byte(0)),
1217                ]),
1218            ]),
1219        )]);
1220
1221        assert_eq!(path.get(&tag), vec![NbtTag::Byte(4)]);
1222    }
1223
1224    #[test]
1225    fn root_predicate_matches_root_compound() {
1226        let path = parse_nbt_path("{id:\"minecraft:barrel\"}").expect("path parses");
1227        let tag = compound([
1228            ("id", NbtTag::String("minecraft:barrel".into())),
1229            ("x", NbtTag::Int(4)),
1230        ]);
1231
1232        assert_eq!(path.count_matching(&tag), 1);
1233    }
1234
1235    #[test]
1236    fn preserves_embedded_snbt_error_kind() {
1237        let error = parse_nbt_path("items[{id:}]").expect_err("invalid pattern should fail");
1238
1239        assert_eq!(
1240            error.kind(),
1241            &NbtPathErrorKind::InvalidSnbt(SnbtErrorKind::ExpectedValue)
1242        );
1243        assert_eq!(
1244            error.component(),
1245            TextComponent::from(&translations::SNBT_PARSER_EXPECTED_UNQUOTED_STRING)
1246        );
1247    }
1248
1249    #[test]
1250    fn quoted_key_errors_use_brigadier_cursors() {
1251        let unclosed = parse_nbt_path(r#""items"#).expect_err("unclosed key should fail");
1252        assert_eq!(unclosed.cursor(), r#""items"#.len());
1253        assert_eq!(unclosed.kind(), &NbtPathErrorKind::UnclosedQuotedString);
1254
1255        let invalid_escape =
1256            parse_nbt_path(r#""a\q""#).expect_err("invalid key escape should fail");
1257        assert_eq!(invalid_escape.cursor(), r#""a\"#.len());
1258        assert_eq!(invalid_escape.kind(), &NbtPathErrorKind::InvalidEscape('q'));
1259    }
1260
1261    #[test]
1262    fn index_errors_use_brigadier_integer_components() {
1263        let expected = parse_nbt_path("items[x]").expect_err("missing integer should fail");
1264        assert_eq!(expected.cursor(), "items[".len());
1265        assert_eq!(expected.kind(), &NbtPathErrorKind::ExpectedIndex);
1266        assert_eq!(
1267            expected.component(),
1268            TextComponent::from(&translations::PARSING_INT_EXPECTED)
1269        );
1270
1271        for value in ["-", "1.2", "999999999999999999999"] {
1272            let input = format!("items[{value}]");
1273            let invalid = parse_nbt_path(&input).expect_err("invalid integer should fail");
1274            assert_eq!(invalid.cursor(), "items[".len());
1275            assert_eq!(
1276                invalid.kind(),
1277                &NbtPathErrorKind::InvalidIndex(value.to_owned())
1278            );
1279            assert_eq!(
1280                invalid.component(),
1281                translations::PARSING_INT_INVALID
1282                    .message([value.to_owned()])
1283                    .component()
1284            );
1285        }
1286    }
1287
1288    #[test]
1289    fn set_creates_missing_compound_parents() {
1290        let path = parse_nbt_path("foo.bar").expect("path parses");
1291        let mut tag = compound([]);
1292
1293        assert_eq!(
1294            path.set(&mut tag, NbtTag::Int(7))
1295                .expect("path set should succeed"),
1296            1
1297        );
1298        assert_eq!(path.get(&tag), vec![NbtTag::Int(7)]);
1299    }
1300
1301    #[test]
1302    fn set_updates_all_list_elements() {
1303        let path = parse_nbt_path("items[].Count").expect("path parses");
1304        let mut tag = compound([(
1305            "items",
1306            list([
1307                compound([("Count", NbtTag::Byte(1))]),
1308                compound([("Count", NbtTag::Byte(2))]),
1309            ]),
1310        )]);
1311
1312        assert_eq!(
1313            path.set(&mut tag, NbtTag::Byte(4))
1314                .expect("path set should succeed"),
1315            2
1316        );
1317        assert_eq!(path.get(&tag), vec![NbtTag::Byte(4), NbtTag::Byte(4)]);
1318    }
1319
1320    #[test]
1321    fn set_updates_indexed_array_elements() {
1322        let path = parse_nbt_path("bytes[1]").expect("path parses");
1323        let mut tag = compound([("bytes", NbtTag::ByteArray(vec![1, 2, 3]))]);
1324
1325        assert_eq!(
1326            path.set(&mut tag, NbtTag::Byte(9))
1327                .expect("path set should succeed"),
1328            1
1329        );
1330        assert_eq!(path.get(&tag), vec![NbtTag::Byte(9)]);
1331    }
1332
1333    #[test]
1334    fn set_coerces_numeric_array_values_like_vanilla() {
1335        let mut tag = compound([
1336            ("bytes", NbtTag::ByteArray(vec![0])),
1337            ("ints", NbtTag::IntArray(vec![0, 0])),
1338            ("longs", NbtTag::LongArray(vec![0])),
1339        ]);
1340
1341        assert_eq!(
1342            parse_nbt_path("bytes[0]")
1343                .expect("path parses")
1344                .set(&mut tag, NbtTag::Short(258))
1345                .expect("path set should succeed"),
1346            1
1347        );
1348        assert_eq!(
1349            parse_nbt_path("ints[]")
1350                .expect("path parses")
1351                .set(&mut tag, NbtTag::Float(3.9))
1352                .expect("path set should succeed"),
1353            2
1354        );
1355        assert_eq!(
1356            parse_nbt_path("longs[0]")
1357                .expect("path parses")
1358                .set(&mut tag, NbtTag::Double(-1.2))
1359                .expect("path set should succeed"),
1360            1
1361        );
1362
1363        assert_eq!(
1364            parse_nbt_path("bytes[0]").expect("path parses").get(&tag),
1365            vec![NbtTag::Byte(2)]
1366        );
1367        assert_eq!(
1368            parse_nbt_path("ints[]").expect("path parses").get(&tag),
1369            vec![NbtTag::Int(3), NbtTag::Int(3)]
1370        );
1371        assert_eq!(
1372            parse_nbt_path("longs[0]").expect("path parses").get(&tag),
1373            vec![NbtTag::Long(-2)]
1374        );
1375    }
1376
1377    #[test]
1378    fn empty_array_wildcard_reports_vanilla_change_count_for_rejected_value() {
1379        let path = parse_nbt_path("bytes[]").expect("path parses");
1380        let mut tag = compound([("bytes", NbtTag::ByteArray(vec![]))]);
1381
1382        assert_eq!(
1383            path.set(&mut tag, NbtTag::String("not numeric".into()))
1384                .expect("path set should succeed"),
1385            1
1386        );
1387        assert_eq!(path.get(&tag), Vec::<NbtTag>::new());
1388    }
1389
1390    #[test]
1391    fn set_regular_list_elements_can_change_type_like_vanilla() {
1392        let path = parse_nbt_path("values[1]").expect("path parses");
1393        let all_values = parse_nbt_path("values[]").expect("path parses");
1394        let mut tag = compound([("values", list([NbtTag::Int(1), NbtTag::Int(2)]))]);
1395
1396        assert_eq!(
1397            path.set(&mut tag, NbtTag::String("two".into()))
1398                .expect("path set should succeed"),
1399            1
1400        );
1401        assert_eq!(
1402            all_values.get(&tag),
1403            vec![NbtTag::Int(1), NbtTag::String("two".into())]
1404        );
1405    }
1406
1407    #[test]
1408    fn predicate_parent_creates_match_in_non_compound_list() {
1409        let path = parse_nbt_path("items[{id:\"minecraft:stone\"}].Count").expect("path parses");
1410        let mut tag = compound([("items", list([NbtTag::String("existing".into())]))]);
1411
1412        assert_eq!(
1413            path.set(&mut tag, NbtTag::Byte(5))
1414                .expect("path set should succeed"),
1415            1
1416        );
1417        assert_eq!(path.get(&tag), vec![NbtTag::Byte(5)]);
1418        assert_eq!(
1419            parse_nbt_path("items[]").expect("path parses").get(&tag),
1420            vec![
1421                NbtTag::String("existing".into()),
1422                compound([
1423                    ("id", NbtTag::String("minecraft:stone".into())),
1424                    ("Count", NbtTag::Byte(5)),
1425                ]),
1426            ]
1427        );
1428    }
1429
1430    #[test]
1431    fn set_predicate_parent_creates_matching_compound() {
1432        let path = parse_nbt_path("items[{id:\"minecraft:stone\"}].Count").expect("path parses");
1433        let mut tag = compound([("items", list([]))]);
1434
1435        assert_eq!(
1436            path.set(&mut tag, NbtTag::Byte(5))
1437                .expect("path set should succeed"),
1438            1
1439        );
1440        assert_eq!(path.get(&tag), vec![NbtTag::Byte(5)]);
1441        assert_eq!(
1442            parse_nbt_path("items[0].id")
1443                .expect("id path parses")
1444                .get(&tag),
1445            vec![NbtTag::String("minecraft:stone".into())]
1446        );
1447    }
1448
1449    #[test]
1450    fn set_reports_missing_intermediate_path() {
1451        let path = parse_nbt_path("items[0].Count").expect("path parses");
1452        let mut tag = compound([("items", list([]))]);
1453
1454        assert_eq!(
1455            path.set(&mut tag, NbtTag::Byte(1)),
1456            Err(NbtPathMutationError::NothingFound("items[0]".to_owned()))
1457        );
1458    }
1459}