Skip to main content

codex_patcher/toml/
query.rs

1use crate::toml::errors::TomlError;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct SectionPath {
6    parts: Vec<String>,
7}
8
9impl SectionPath {
10    pub fn new(parts: Vec<String>) -> Result<Self, TomlError> {
11        if parts.is_empty() {
12            return Err(TomlError::InvalidSectionPath {
13                input: "".to_string(),
14                message: "empty section path".to_string(),
15            });
16        }
17        Ok(Self { parts })
18    }
19
20    pub fn parse(input: &str) -> Result<Self, TomlError> {
21        let parts = parse_dotted_path(input)?;
22        if parts.is_empty() {
23            return Err(TomlError::InvalidSectionPath {
24                input: input.to_string(),
25                message: "empty section path".to_string(),
26            });
27        }
28        Ok(Self { parts })
29    }
30
31    pub fn parts(&self) -> &[String] {
32        &self.parts
33    }
34
35    pub fn as_string(&self) -> String {
36        self.parts.join(".")
37    }
38}
39
40impl fmt::Display for SectionPath {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}", self.as_string())
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct KeyPath {
48    parts: Vec<String>,
49}
50
51impl KeyPath {
52    pub fn parse(input: &str) -> Result<Self, TomlError> {
53        let parts = parse_dotted_path(input)?;
54        if parts.is_empty() {
55            return Err(TomlError::InvalidSectionPath {
56                input: input.to_string(),
57                message: "empty key path".to_string(),
58            });
59        }
60        Ok(Self { parts })
61    }
62
63    pub fn parts(&self) -> &[String] {
64        &self.parts
65    }
66
67    pub fn as_string(&self) -> String {
68        self.parts.join(".")
69    }
70}
71
72impl fmt::Display for KeyPath {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "{}", self.as_string())
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum TomlQuery {
80    Section { path: SectionPath },
81    Key { section: SectionPath, key: KeyPath },
82}
83
84fn parse_dotted_path(input: &str) -> Result<Vec<String>, TomlError> {
85    let mut parts = Vec::new();
86    let mut current = String::new();
87    let mut chars = input.chars().peekable();
88    let mut in_quotes = false;
89    let mut quote_char = '\0';
90    let mut quote_open_pos: usize = 0;
91    let mut char_idx: usize = 0;
92
93    while let Some(ch) = chars.next() {
94        if in_quotes {
95            if ch == quote_char {
96                in_quotes = false;
97                continue;
98            }
99
100            if quote_char == '"' && ch == '\\' {
101                if let Some(next) = chars.next() {
102                    let escaped = match next {
103                        '"' => '"',
104                        '\\' => '\\',
105                        'n' => '\n',
106                        't' => '\t',
107                        'r' => '\r',
108                        other => other,
109                    };
110                    current.push(escaped);
111                    continue;
112                }
113            }
114
115            current.push(ch);
116            continue;
117        }
118
119        match ch {
120            '.' => {
121                if current.is_empty() {
122                    return Err(TomlError::InvalidSectionPath {
123                        input: input.to_string(),
124                        message: "empty path segment".to_string(),
125                    });
126                }
127                parts.push(current.clone());
128                current.clear();
129            }
130            '"' | '\'' => {
131                if !current.is_empty() {
132                    return Err(TomlError::InvalidSectionPath {
133                        input: input.to_string(),
134                        message: "unexpected quote inside key".to_string(),
135                    });
136                }
137                in_quotes = true;
138                quote_char = ch;
139                quote_open_pos = char_idx;
140            }
141            ch if ch.is_whitespace() => {
142                return Err(TomlError::InvalidSectionPath {
143                    input: input.to_string(),
144                    message: "whitespace not allowed in key".to_string(),
145                });
146            }
147            other => current.push(other),
148        }
149        char_idx += 1;
150    }
151
152    if in_quotes {
153        return Err(TomlError::InvalidSectionPath {
154            input: input.to_string(),
155            message: format!(
156                "unterminated quoted key (opening {quote_char} at position {quote_open_pos})"
157            ),
158        });
159    }
160
161    if !current.is_empty() {
162        parts.push(current);
163    }
164
165    Ok(parts)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn parse_section_path_basic() {
174        let path = SectionPath::parse("profile.zack").unwrap();
175        assert_eq!(path.parts(), &["profile", "zack"]);
176    }
177
178    #[test]
179    fn parse_section_path_quoted() {
180        let path = SectionPath::parse("profile.\"zack.test\"").unwrap();
181        assert_eq!(path.parts(), &["profile", "zack.test"]);
182    }
183
184    #[test]
185    fn parse_key_path_dotted() {
186        let key = KeyPath::parse("target.x86_64").unwrap();
187        assert_eq!(key.parts(), &["target", "x86_64"]);
188    }
189}