agent_first_data/document/
path.rs1use crate::document::{DocumentError, DocumentResult};
18
19pub fn parse_path(path: &str) -> DocumentResult<Vec<String>> {
20 if path.is_empty() {
21 return Err(DocumentError::EmptyPath);
22 }
23 let mut segments = Vec::new();
24 let mut segment = String::new();
25 let mut escaped = false;
26 for character in path.chars() {
27 if escaped {
28 match character {
29 '.' | '\\' => segment.push(character),
30 other => {
31 return Err(DocumentError::ParseError {
32 format: "path".to_string(),
33 detail: format!("invalid escape `\\{other}`"),
34 });
35 }
36 }
37 escaped = false;
38 } else {
39 match character {
40 '\\' => escaped = true,
41 '.' => segments.push(std::mem::take(&mut segment)),
42 other => segment.push(other),
43 }
44 }
45 }
46 if escaped {
47 return Err(DocumentError::ParseError {
48 format: "path".to_string(),
49 detail: "trailing path escape".to_string(),
50 });
51 }
52 segments.push(segment);
53 Ok(segments)
54}
55
56pub fn join_path(segments: &[String]) -> String {
57 segments
58 .iter()
59 .map(|segment| segment.replace('\\', "\\\\").replace('.', "\\."))
60 .collect::<Vec<_>>()
61 .join(".")
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
71 fn every_key_sequence_round_trips_through_its_spelling() {
72 let cases: &[&[&str]] = &[
73 &["packages", "", "version"],
74 &["packages", ""],
75 &["", "a"],
76 &["a", "", "", "b"],
77 &["plain"],
78 &["a.dotted.key", "b"],
79 &["back\\slash"],
80 &["node_modules/@esbuild/darwin-arm64"],
81 ];
82 for segments in cases {
83 let owned: Vec<String> = segments.iter().map(|s| (*s).to_string()).collect();
84 let spelling = join_path(&owned);
85 let parsed = parse_path(&spelling)
86 .unwrap_or_else(|error| panic!("{owned:?} spelled `{spelling}`: {error}"));
87 assert_eq!(parsed, owned, "spelled `{spelling}`");
88 }
89 }
90
91 #[test]
92 fn an_empty_segment_is_a_key_not_an_error() {
93 assert_eq!(
96 parse_path("packages..version").unwrap(),
97 vec!["packages".to_string(), String::new(), "version".to_string()]
98 );
99 assert_eq!(
100 parse_path("packages.").unwrap(),
101 vec!["packages".to_string(), String::new()]
102 );
103 assert_eq!(
104 parse_path(".leading").unwrap(),
105 vec![String::new(), "leading".to_string()]
106 );
107 }
108
109 #[test]
110 fn the_empty_string_still_names_no_path() {
111 assert!(matches!(parse_path(""), Err(DocumentError::EmptyPath)));
115 assert_eq!(join_path(&[String::new()]), "");
116 }
117
118 #[test]
119 fn malformed_escapes_are_still_refused() {
120 assert!(parse_path(r"trailing\").is_err());
121 assert!(parse_path(r"bad\qescape").is_err());
122 }
123}