agent_first_data/document/
path.rs1use crate::document::{DocumentError, DocumentResult};
24
25pub fn parse_path(path: &str) -> DocumentResult<Vec<String>> {
26 if path.is_empty() {
27 return Err(DocumentError::EmptyPath);
28 }
29 let mut segments = Vec::new();
30 let mut segment = String::new();
31 let mut escaped = false;
32 let mut wrote_bare_star = false;
35 let mut literal = false;
36 for character in path.chars() {
37 if escaped {
38 match character {
39 '.' | '\\' | '*' => {
40 segment.push(character);
41 literal = true;
42 }
43 other => {
44 return Err(DocumentError::PathSyntax {
45 detail: format!("invalid escape `\\{other}`"),
46 });
47 }
48 }
49 escaped = false;
50 } else {
51 match character {
52 '\\' => escaped = true,
53 '.' => {
54 if segment == "*" && !literal {
55 wrote_bare_star = true;
56 }
57 segments.push(std::mem::take(&mut segment));
58 literal = false;
59 }
60 other => segment.push(other),
61 }
62 }
63 }
64 if escaped {
65 return Err(DocumentError::PathSyntax {
66 detail: "trailing path escape".to_string(),
67 });
68 }
69 if segment == "*" && !literal {
70 wrote_bare_star = true;
71 }
72 segments.push(segment);
73 if wrote_bare_star {
78 return Err(DocumentError::PathSyntax {
79 detail: "a bare `*` segment is a pattern; write `\\*` for a literal star key, or use \
80 a command that expands patterns"
81 .to_string(),
82 });
83 }
84 Ok(segments)
85}
86
87pub fn join_path(segments: &[String]) -> String {
88 segments
89 .iter()
90 .map(|segment| {
91 segment
92 .replace('\\', "\\\\")
93 .replace('.', "\\.")
94 .replace('*', "\\*")
95 })
96 .collect::<Vec<_>>()
97 .join(".")
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum PatternSegment {
103 Key(String),
105 Wildcard,
107}
108
109pub fn parse_path_pattern(path: &str) -> DocumentResult<Vec<PatternSegment>> {
119 if path.is_empty() {
120 return Err(DocumentError::EmptyPath);
121 }
122 let mut segments = Vec::new();
123 let mut segment = String::new();
124 let mut escaped = false;
125 let mut literal = false;
126 for character in path.chars() {
127 if escaped {
128 match character {
129 '.' | '\\' | '*' => {
130 segment.push(character);
131 literal = true;
132 }
133 other => {
134 return Err(DocumentError::PathSyntax {
135 detail: format!("invalid escape `\\{other}`"),
136 });
137 }
138 }
139 escaped = false;
140 } else {
141 match character {
142 '\\' => escaped = true,
143 '.' => {
144 segments.push(finish_pattern_segment(
145 std::mem::take(&mut segment),
146 literal,
147 ));
148 literal = false;
149 }
150 other => segment.push(other),
151 }
152 }
153 }
154 if escaped {
155 return Err(DocumentError::PathSyntax {
156 detail: "trailing path escape".to_string(),
157 });
158 }
159 segments.push(finish_pattern_segment(segment, literal));
160 Ok(segments)
161}
162
163fn finish_pattern_segment(segment: String, literal: bool) -> PatternSegment {
164 if segment == "*" && !literal {
165 PatternSegment::Wildcard
166 } else {
167 PatternSegment::Key(segment)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
178 fn every_key_sequence_round_trips_through_its_spelling() {
179 let cases: &[&[&str]] = &[
180 &["packages", "", "version"],
181 &["packages", ""],
182 &["", "a"],
183 &["a", "", "", "b"],
184 &["plain"],
185 &["a.dotted.key", "b"],
186 &["back\\slash"],
187 &["node_modules/@esbuild/darwin-arm64"],
188 ];
189 for segments in cases {
190 let owned: Vec<String> = segments.iter().map(|s| (*s).to_string()).collect();
191 let spelling = join_path(&owned);
192 let parsed = parse_path(&spelling)
193 .unwrap_or_else(|error| panic!("{owned:?} spelled `{spelling}`: {error}"));
194 assert_eq!(parsed, owned, "spelled `{spelling}`");
195 }
196 }
197
198 #[test]
199 fn an_empty_segment_is_a_key_not_an_error() {
200 assert_eq!(
203 parse_path("packages..version").unwrap(),
204 vec!["packages".to_string(), String::new(), "version".to_string()]
205 );
206 assert_eq!(
207 parse_path("packages.").unwrap(),
208 vec!["packages".to_string(), String::new()]
209 );
210 assert_eq!(
211 parse_path(".leading").unwrap(),
212 vec![String::new(), "leading".to_string()]
213 );
214 }
215
216 #[test]
217 fn a_star_key_survives_the_wildcard_reservation() {
218 assert_eq!(join_path(&["*".to_string()]), r"\*");
222 assert_eq!(parse_path(r"\*").unwrap(), vec!["*".to_string()]);
223 assert_eq!(
224 parse_path(r"a.\*.b").unwrap(),
225 vec!["a".to_string(), "*".to_string(), "b".to_string()]
226 );
227 let segments = vec!["a".to_string(), "*".to_string()];
229 assert_eq!(parse_path(&join_path(&segments)).unwrap(), segments);
230 }
231
232 #[test]
233 fn a_bare_star_is_reserved_for_patterns() {
234 assert!(parse_path("*").is_err());
237 assert!(parse_path("package.*.name").is_err());
238 assert_eq!(
240 parse_path_pattern("package.*.name").unwrap(),
241 vec![
242 PatternSegment::Key("package".to_string()),
243 PatternSegment::Wildcard,
244 PatternSegment::Key("name".to_string()),
245 ]
246 );
247 assert_eq!(
249 parse_path_pattern(r"a.\*").unwrap(),
250 vec![
251 PatternSegment::Key("a".to_string()),
252 PatternSegment::Key("*".to_string()),
253 ]
254 );
255 }
256
257 #[test]
258 fn the_empty_string_still_names_no_path() {
259 assert!(matches!(parse_path(""), Err(DocumentError::EmptyPath)));
263 assert_eq!(join_path(&[String::new()]), "");
264 }
265
266 #[test]
267 fn malformed_escapes_are_still_refused() {
268 assert!(parse_path(r"trailing\").is_err());
269 assert!(parse_path(r"bad\qescape").is_err());
270 }
271}