1use crate::snbt::{SNBT, fmt_snbt_compound};
2use minecraft_command_types_derive::HasMacro;
3use std::collections::BTreeMap;
4use std::fmt::{Display, Formatter};
5
6pub type SNBTCompound = BTreeMap<String, SNBT>;
7
8fn escape_nbt_path_key(name: &str) -> String {
9 let needs_quotes = name
10 .chars()
11 .any(|c| matches!(c, ' ' | '"' | '\'' | '[' | ']' | '.' | '{' | '}'));
12
13 if needs_quotes {
14 let escaped_content = name.replace('\\', "\\\\").replace('"', "\\\"");
15 format!("\"{}\"", escaped_content)
16 } else {
17 name.to_string()
18 }
19}
20
21#[derive(Debug, Clone, Eq, PartialEq, Hash, HasMacro)]
22pub enum NbtPathNode {
23 RootCompound(SNBTCompound),
24 Named(String, Option<SNBTCompound>),
25 Index(Option<SNBT>),
26}
27
28#[derive(Debug, Clone, Eq, PartialEq, Hash, HasMacro)]
29pub struct NbtPath(pub Vec<NbtPathNode>);
30
31impl Display for NbtPathNode {
32 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33 match self {
34 NbtPathNode::RootCompound(compound) => fmt_snbt_compound(f, compound),
35 NbtPathNode::Named(name, filter) => {
36 write!(f, "{}", escape_nbt_path_key(name))?;
37
38 if let Some(filter) = filter
39 && !filter.is_empty()
40 {
41 fmt_snbt_compound(f, filter)?;
42 }
43 Ok(())
44 }
45 NbtPathNode::Index(Some(snbt)) => write!(f, "[{}]", snbt),
46 NbtPathNode::Index(None) => write!(f, "[]"),
47 }
48 }
49}
50
51impl Display for NbtPath {
52 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53 let mut first = true;
54 for node in &self.0 {
55 if !first && !matches!(node, NbtPathNode::Index(_)) {
56 write!(f, ".")?;
57 }
58 first = false;
59 write!(f, "{}", node)?;
60 }
61 Ok(())
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::snbt::SNBT;
69 use std::collections::BTreeMap;
70
71 fn snbt_string(s: &str) -> SNBT {
72 SNBT::String(s.to_string())
73 }
74
75 fn compound(pairs: Vec<(&str, SNBT)>) -> SNBTCompound {
76 let mut map = BTreeMap::new();
77 for (k, v) in pairs {
78 map.insert(k.to_string(), v);
79 }
80 map
81 }
82
83 #[test]
84 fn test_example_1() {
85 let path = NbtPath(vec![
86 NbtPathNode::Named("foo".to_string(), None),
87 NbtPathNode::Named("bar".to_string(), None),
88 NbtPathNode::Index(Some(SNBT::Integer(0))),
89 NbtPathNode::Named("A [crazy name]!".to_string(), None),
90 NbtPathNode::Named("baz".to_string(), None),
91 ]);
92
93 assert_eq!(path.to_string(), r#"foo.bar[0]."A [crazy name]!".baz"#);
94 }
95
96 #[test]
97 fn test_example_2() {
98 let path = NbtPath(vec![
99 NbtPathNode::Named("Items".to_string(), None),
100 NbtPathNode::Index(Some(SNBT::Integer(1))),
101 NbtPathNode::Named("components".to_string(), None),
102 NbtPathNode::Named("minecraft:written_book_content".to_string(), None),
103 NbtPathNode::Named("pages".to_string(), None),
104 NbtPathNode::Index(Some(SNBT::Integer(3))),
105 NbtPathNode::Named("raw".to_string(), None),
106 ]);
107
108 assert_eq!(
109 path.to_string(),
110 r#"Items[1].components.minecraft:written_book_content.pages[3].raw"#
111 );
112 }
113
114 #[test]
115 fn test_root_compound_and_filters() {
116 let path = NbtPath(vec![NbtPathNode::RootCompound(compound(vec![(
117 "foo",
118 snbt_string("4.0f"),
119 )]))]);
120
121 assert_eq!(path.to_string(), r#"{"foo":"4.0f"}"#);
122
123 let path2 = NbtPath(vec![
124 NbtPathNode::Named(
125 "foo".to_string(),
126 Some(compound(vec![("bar", snbt_string("baz"))])),
127 ),
128 NbtPathNode::Named("bar".to_string(), None),
129 ]);
130
131 assert_eq!(path2.to_string(), r#"foo{"bar":"baz"}.bar"#);
132 }
133
134 #[test]
135 fn test_index_all() {
136 let path = NbtPath(vec![
137 NbtPathNode::Named("foo".to_string(), None),
138 NbtPathNode::Named("bar".to_string(), None),
139 NbtPathNode::Index(None),
140 NbtPathNode::Named("baz".to_string(), None),
141 ]);
142
143 assert_eq!(path.to_string(), r#"foo.bar[].baz"#);
144 }
145
146 #[test]
147 fn test_complex_escaping_with_new_rules() {
148 let path_with_quotes = NbtPath(vec![NbtPathNode::Named(
149 "key with \"quotes\"".to_string(),
150 None,
151 )]);
152 assert_eq!(path_with_quotes.to_string(), r#""key with \"quotes\"""#);
153
154 let path_with_dot = NbtPath(vec![NbtPathNode::Named("key.with.dot".to_string(), None)]);
155 assert_eq!(path_with_dot.to_string(), r#""key.with.dot""#);
156
157 let path_with_slash = NbtPath(vec![NbtPathNode::Named(
158 "key with \\ backslash".to_string(),
159 None,
160 )]);
161 assert_eq!(path_with_slash.to_string(), r#""key with \\ backslash""#);
162 }
163}