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