envvar_parser/
lib.rs

1use std::error::Error;
2use std::fmt::Debug;
3use std::result::Result;
4
5pub struct SyntaxError {
6    pos: usize,
7    msg: String,
8}
9
10impl std::fmt::Display for SyntaxError {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        write!(f, "Syntax error at {}: {}", self.pos, self.msg)
13    }
14}
15
16impl std::fmt::Debug for SyntaxError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "Syntax error at {}: {}", self.pos, self.msg)
19    }
20}
21
22impl Error for SyntaxError {}
23
24#[derive(Debug, Clone)]
25pub struct Span {
26    _start: usize,
27    _end: usize,
28}
29
30#[derive(Debug)]
31pub enum Token {
32    ROOT(Vec<Token>, Option<Span>),
33    Comment(String, Option<Span>),
34    Variable(String, String, Option<Span>),
35}
36
37impl Token {
38    pub fn create_root<T>(tokens: T) -> Token
39    where
40        T: IntoIterator<Item = Token>,
41    {
42        Token::ROOT(tokens.into_iter().collect(), None)
43    }
44
45    pub fn create_comment<T>(comment: T) -> Token
46    where
47        T: ToString,
48    {
49        Token::Comment(comment.to_string(), None)
50    }
51
52    pub fn create_variable<T, U>(name: T, value: U) -> Token
53    where
54        T: ToString,
55        U: ToString,
56    {
57        Token::Variable(name.to_string(), value.to_string(), None)
58    }
59
60    fn append(&mut self, token: Token) {
61        match self {
62            Token::ROOT(tokens, _) => tokens.push(token),
63            _ => {}
64        }
65    }
66
67    pub fn parse<C: AsRef<[u8]>>(content: C) -> Result<Token, Box<dyn Error>> {
68        let mut root_token = Token::ROOT(Vec::new(), None);
69        let content_ref = content.as_ref();
70
71        let mut key_characters: Vec<u8> = vec![];
72
73        key_characters.append(&mut (b'a'..b'z').collect::<Vec<u8>>());
74        key_characters.append(&mut (b'A'..b'Z').collect::<Vec<u8>>());
75        key_characters.append(&mut (b'0'..b'9').collect::<Vec<u8>>());
76        key_characters.push(b'_');
77
78        let mut pos_current = 0;
79        let mut acumulator_type = AcumulatorKind::ROOT;
80        let mut collector_variable_key = String::new();
81        let mut collector_variable_value = String::new();
82        let mut collector_comment = String::new();
83
84        loop {
85            if pos_current >= content_ref.len() {
86                break;
87            }
88            let char = content_ref[pos_current];
89            pos_current += 1;
90
91            match acumulator_type {
92                AcumulatorKind::ROOT => match char {
93                    b'#' => {
94                        acumulator_type = AcumulatorKind::COMMENT;
95                        continue;
96                    }
97                    b'\n' | b'\t' | b' ' | b'\r' => {
98                        continue;
99                    }
100                    char if key_characters.contains(&char) => {
101                        acumulator_type = AcumulatorKind::VariableKey;
102                        collector_variable_key = String::from_utf8(vec![char]).unwrap();
103                        continue;
104                    }
105                    _ => {
106                        return Err(Box::new(SyntaxError {
107                            pos: pos_current,
108                            msg: "Unexpected character".to_string(),
109                        }));
110                    }
111                },
112                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StartValue) => {
113                    match char {
114                        b' ' | b'\t' | b'\r' | b'\n' => {
115                            continue;
116                        }
117                        b'"' => {
118                            acumulator_type = AcumulatorKind::VariableValue(
119                                AcumulatorVariableValueKind::StringDoubleQuote,
120                            );
121                            collector_variable_value = String::new();
122                            continue;
123                        }
124                        b'\'' => {
125                            acumulator_type = AcumulatorKind::VariableValue(
126                                AcumulatorVariableValueKind::StringSimpleQuote,
127                            );
128                            collector_variable_value = String::new();
129                            continue;
130                        }
131                        char if char != b' ' => {
132                            acumulator_type = AcumulatorKind::VariableValue(
133                                AcumulatorVariableValueKind::StringWithoutQuotes,
134                            );
135                            collector_variable_value = String::from(char as char);
136                            continue;
137                        }
138                        _ => {
139                            return Err(Box::new(SyntaxError {
140                                pos: pos_current,
141                                msg: "Unexpected character".to_string(),
142                            }));
143                        }
144                    }
145                }
146                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StringWithoutQuotes) => {
147                    match char {
148                        b'\n' => {
149                            acumulator_type = AcumulatorKind::ROOT;
150                            root_token.append(Token::Variable(
151                                collector_variable_key.clone(),
152                                collector_variable_value.clone().trim().to_string(),
153                                None,
154                            ));
155                            continue;
156                        }
157                        b'#' => {
158                            acumulator_type = AcumulatorKind::COMMENT;
159                            root_token.append(Token::Variable(
160                                collector_variable_key.clone(),
161                                collector_variable_value.clone().trim().to_string(),
162                                None,
163                            ));
164                            continue;
165                        }
166                        _ => {
167                            collector_variable_value.push(char as char);
168                            continue;
169                        }
170                    }
171                }
172                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StringSimpleQuote) => {
173                    match char {
174                        b'\'' if content_ref[pos_current - 2] != b'\\' => {
175                            acumulator_type = AcumulatorKind::ROOT;
176                            root_token.append(Token::Variable(
177                                collector_variable_key.clone(),
178                                collector_variable_value.replace("\\'", "'").clone(),
179                                None,
180                            ));
181                            continue;
182                        }
183                        _ => {
184                            collector_variable_value.push(char as char);
185                            continue;
186                        }
187                    }
188                }
189                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StringDoubleQuote) => {
190                    match char {
191                        b'\"' if content_ref[pos_current - 2] != b'\\' => {
192                            acumulator_type = AcumulatorKind::ROOT;
193                            root_token.append(Token::Variable(
194                                collector_variable_key.clone(),
195                                collector_variable_value.replace("\\\"", "\"").clone(),
196                                None,
197                            ));
198                            continue;
199                        }
200                        _ => {
201                            collector_variable_value.push(char as char);
202                            continue;
203                        }
204                    }
205                }
206
207                AcumulatorKind::VariableEqual => match char {
208                    b' ' | b'\t' => {
209                        continue;
210                    }
211                    b'=' => {
212                        acumulator_type =
213                            AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StartValue);
214                        continue;
215                    }
216                    _ => {
217                        return Err(Box::new(SyntaxError {
218                            pos: pos_current,
219                            msg: "Unexpected character".to_string(),
220                        }));
221                    }
222                },
223                AcumulatorKind::VariableKey => match char {
224                    b' ' | b'\t' => {
225                        acumulator_type = AcumulatorKind::VariableEqual;
226                        continue;
227                    }
228                    b'=' => {
229                        acumulator_type =
230                            AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StartValue);
231                        continue;
232                    }
233                    char if key_characters.contains(&char) => {
234                        collector_variable_key.push(char as char);
235                        continue;
236                    }
237                    _ => {
238                        return Err(Box::new(SyntaxError {
239                            pos: pos_current,
240                            msg: "Unexpected character".to_string(),
241                        }));
242                    }
243                },
244                AcumulatorKind::COMMENT => match char {
245                    b'\n' => {
246                        acumulator_type = AcumulatorKind::ROOT;
247                        root_token.append(Token::Comment(
248                            collector_comment.clone().trim().to_string(),
249                            None,
250                        ));
251                        continue;
252                    }
253                    _ => {
254                        collector_comment.push(char as char);
255                        continue;
256                    }
257                },
258            }
259        }
260
261        Ok(root_token)
262    }
263
264    pub fn serialize(token: Token) -> Result<String, Box<String>> {
265        let tokens = match token {
266            Token::ROOT(tokens, _) => tokens,
267            _ => return Err(Box::new("Invalid token require a ROOT token".to_string())),
268        };
269
270        let mut serialized_string = String::new();
271
272        for token in tokens {
273            match token {
274                Token::Variable(key, value, _) => {
275                    let next_string = value.replace("'", "\\'");
276
277                    serialized_string.push_str(&format!("{}='{}'\n", key, next_string));
278                }
279                Token::Comment(comment, _) => {
280                    serialized_string.push_str(&format!("# {}\n", comment));
281                }
282                _ => {}
283            }
284        }
285
286        Ok(serialized_string)
287    }
288}
289
290#[derive(Debug)]
291pub struct EnvMap {}
292
293enum AcumulatorVariableValueKind {
294    StartValue,
295    StringDoubleQuote,
296    StringWithoutQuotes,
297    StringSimpleQuote,
298}
299
300enum AcumulatorKind {
301    ROOT,
302    COMMENT,
303    VariableKey,
304    VariableEqual,
305    VariableValue(AcumulatorVariableValueKind),
306}
307
308impl Default for AcumulatorKind {
309    fn default() -> Self {
310        Self::ROOT
311    }
312}
313
314#[cfg(test)]
315mod tests_env_map {
316    use super::*;
317
318    #[test]
319    fn parse_from_binary() {
320        let payload = b"
321            # Comment
322            key = value
323            KEY = 'val\\'ue' # inline comment
324            KEY2=\"VALUE2\"
325            KEY3 = ABC ASDF    # inline comment
326        ";
327
328        let envs = Token::parse(payload).unwrap();
329
330        match envs {
331            Token::ROOT(tokens, _) => {
332                match &tokens[0] {
333                    Token::Comment(comment, _) => assert_eq!(comment, "Comment"),
334                    _ => panic!("Unexpected token"),
335                }
336
337                match &tokens[1] {
338                    Token::Variable(key, value, _) => {
339                        assert_eq!(key, "key");
340                        assert_eq!(value, "value");
341                    }
342                    _ => panic!("Unexpected token"),
343                }
344
345                match &tokens[2] {
346                    Token::Variable(key, value, _) => {
347                        assert_eq!(key, "KEY");
348                        assert_eq!(value, "val'ue");
349                    }
350                    _ => panic!("Unexpected token"),
351                }
352
353                match &tokens[4] {
354                    Token::Variable(key, value, _) => {
355                        assert_eq!(key, "KEY2");
356                        assert_eq!(value, "VALUE2");
357                    }
358                    _ => panic!("Unexpected token"),
359                }
360
361                match &tokens[5] {
362                    Token::Variable(key, value, _) => {
363                        assert_eq!(key, "KEY3");
364                        assert_eq!(value, "ABC ASDF");
365                    }
366                    _ => panic!("Unexpected token"),
367                }
368            }
369            _ => panic!("Unexpected token"),
370        }
371    }
372
373    #[test]
374    fn serialize_to_binary() {
375        let payload = Token::create_root([
376            Token::create_comment("Comment"),
377            Token::create_variable("KEY1", "value"),
378            Token::create_variable("KEY2", "VALUE2"),
379            Token::create_variable("KEY3", "ABC ASDF"),
380        ]);
381
382        let body_payload = Token::serialize(payload).unwrap();
383
384        assert_eq!(
385            "# Comment\nKEY1='value'\nKEY2='VALUE2'\nKEY3='ABC ASDF'\n",
386            body_payload
387        );
388    }
389}