Skip to main content

promql_parser/util/
string.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Internal utilities for strings.
16
17/// Escapes a string value for embedding in a PromQL double-quoted string literal.
18/// This is the inverse of `unquote_string` — it re-escapes backslashes and double quotes.
19pub fn escape_string(s: &str) -> String {
20    let mut result = String::with_capacity(s.len());
21    for c in s.chars() {
22        match c {
23            '\\' => result.push_str("\\\\"),
24            '"' => result.push_str("\\\""),
25            '\n' => result.push_str("\\n"),
26            '\r' => result.push_str("\\r"),
27            '\t' => result.push_str("\\t"),
28            _ => result.push(c),
29        }
30    }
31    result
32}
33
34/// This function is modified from original go version
35/// https://github.com/prometheus/prometheus/blob/v3.8.0/util/strutil/quote.go
36pub fn unquote_string(s: &str) -> Result<String, String> {
37    let n = s.len();
38    if n < 2 {
39        return Err("invalid syntax".to_string());
40    }
41
42    let bytes = s.as_bytes();
43    let quote = bytes[0];
44    if quote != bytes[n - 1] {
45        return Err("invalid syntax".to_string());
46    }
47
48    let inner = &s[1..n - 1];
49
50    if quote == b'`' {
51        if inner.contains('`') {
52            return Err("invalid syntax".to_string());
53        }
54        return Ok(inner.to_string());
55    }
56
57    if quote != b'"' && quote != b'\'' {
58        return Err("invalid syntax".to_string());
59    }
60
61    if inner.contains('\n') {
62        return Err("invalid syntax".to_string());
63    }
64
65    if !inner.contains('\\') && !inner.contains(quote as char) {
66        return Ok(inner.to_string());
67    }
68
69    let mut res = String::with_capacity(3 * inner.len() / 2);
70    let mut rest = inner;
71
72    while !rest.is_empty() {
73        let (c, tail) = unquote_char(rest, quote)?;
74        res.push(c);
75        rest = tail;
76    }
77
78    Ok(res)
79}
80
81fn unquote_char(s: &str, quote: u8) -> Result<(char, &str), String> {
82    let bytes = s.as_bytes();
83    let c = bytes[0];
84
85    // Easy cases
86    if c == quote && (quote == b'\'' || quote == b'"') {
87        return Err("invalid syntax".to_string());
88    }
89
90    if c < 0x80 {
91        if c != b'\\' {
92            return Ok((c as char, &s[1..]));
93        }
94    } else {
95        // Handle multi-byte UTF-8 character
96        let r = s.chars().next().unwrap();
97        return Ok((r, &s[r.len_utf8()..]));
98    }
99
100    // Hard case: backslash
101    if s.len() <= 1 {
102        return Err("invalid syntax".to_string());
103    }
104
105    let c = bytes[1];
106    let mut tail = &s[2..];
107
108    let value = match c {
109        b'a' => '\x07', // Alert/Bell
110        b'b' => '\x08', // Backspace
111        b'f' => '\x0c', // Form feed
112        b'n' => '\n',
113        b'r' => '\r',
114        b't' => '\t',
115        b'v' => '\x0b', // Vertical tab
116        b'x' | b'u' | b'U' => {
117            let n = match c {
118                b'x' => 2,
119                b'u' => 4,
120                b'U' => 8,
121                _ => unreachable!(),
122            };
123
124            if tail.len() < n {
125                return Err("invalid syntax".to_string());
126            }
127
128            let mut v: u32 = 0;
129            for i in 0..n {
130                let x = unhex(tail.as_bytes()[i])?;
131                v = (v << 4) | x;
132            }
133
134            tail = &tail[n..];
135
136            if c == b'x' {
137                std::char::from_u32(v).ok_or("invalid syntax")?
138            } else {
139                if v > 0x10FFFF {
140                    return Err("invalid syntax".to_string());
141                }
142                std::char::from_u32(v).ok_or("invalid syntax")?
143            }
144        }
145        b'0'..=b'7' => {
146            let mut v = (c - b'0') as u32;
147            if tail.len() < 2 {
148                return Err("invalid syntax".to_string());
149            }
150            for i in 0..2 {
151                let x = (tail.as_bytes()[i] as char)
152                    .to_digit(8)
153                    .ok_or("invalid syntax")?;
154                v = (v << 3) | x;
155            }
156            tail = &tail[2..];
157            if v > 255 {
158                return Err("invalid syntax".to_string());
159            }
160            std::char::from_u32(v).ok_or("invalid syntax")?
161        }
162        b'\\' => '\\',
163        b'\'' | b'"' => {
164            if c != quote {
165                return Err("invalid syntax".to_string());
166            }
167            c as char
168        }
169        _ => return Err("invalid syntax".to_string()),
170    };
171
172    Ok((value, tail))
173}
174
175fn unhex(b: u8) -> Result<u32, String> {
176    let c = b as char;
177    c.to_digit(16).ok_or_else(|| "invalid syntax".to_string())
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_unquote_string_basic() {
186        // Test simple double quotes
187        assert_eq!(unquote_string("\"hello\"").unwrap(), "hello");
188
189        // Test simple single quotes
190        assert_eq!(unquote_string("'hello'").unwrap(), "hello");
191
192        // Test backticks
193        assert_eq!(unquote_string("`hello`").unwrap(), "hello");
194    }
195
196    #[test]
197    fn test_unquote_string_empty() {
198        assert_eq!(unquote_string("\"\"").unwrap(), "");
199        assert_eq!(unquote_string("''").unwrap(), "");
200        assert_eq!(unquote_string("``").unwrap(), "");
201    }
202
203    #[test]
204    fn test_unquote_string_error_cases() {
205        // Too short
206        assert!(unquote_string("\"").is_err());
207        assert!(unquote_string("'").is_err());
208        assert!(unquote_string("`").is_err());
209
210        // Mismatched quotes
211        assert!(unquote_string("\"hello'").is_err());
212        assert!(unquote_string("'hello\"").is_err());
213        assert!(unquote_string("`hello\"").is_err());
214
215        // Invalid quote character
216        assert!(unquote_string("#hello#").is_err());
217        assert!(unquote_string("/hello/").is_err());
218
219        // Newlines in quoted strings
220        assert!(unquote_string("\"hello\nworld\"").is_err());
221        assert!(unquote_string("'hello\nworld'").is_err());
222
223        // Backticks with backticks inside
224        assert!(unquote_string("`hello`world`").is_err());
225    }
226
227    #[test]
228    fn test_unquote_string_escaped_characters() {
229        // Test various escape sequences
230        assert_eq!(unquote_string(r#""\a""#).unwrap(), "\x07");
231        assert_eq!(unquote_string(r#""\b""#).unwrap(), "\x08");
232        assert_eq!(unquote_string(r#""\f""#).unwrap(), "\x0c");
233        assert_eq!(unquote_string(r#""\n""#).unwrap(), "\n");
234        assert_eq!(unquote_string(r#""\r""#).unwrap(), "\r");
235        assert_eq!(unquote_string(r#""\t""#).unwrap(), "\t");
236        assert_eq!(unquote_string(r#""\v""#).unwrap(), "\x0b");
237
238        // Test escaped backslashes
239        assert_eq!(unquote_string(r#""\\""#).unwrap(), "\\");
240
241        // Test escaped quotes
242        assert_eq!(unquote_string(r#""\"""#).unwrap(), "\"");
243        assert_eq!(unquote_string(r#"'\''"#).unwrap(), "'");
244        assert_eq!(
245            unquote_string(r#""double-quoted raw string \" with escaped quote""#).unwrap(),
246            "double-quoted raw string \" with escaped quote"
247        );
248
249        // Mixed escape sequences
250        assert_eq!(unquote_string(r#""hello\nworld""#).unwrap(), "hello\nworld");
251        assert_eq!(unquote_string(r#""hello\tworld""#).unwrap(), "hello\tworld");
252    }
253
254    #[test]
255    fn test_unquote_string_hex_escapes() {
256        // Test \x hex escapes
257        assert_eq!(unquote_string(r#""\x41""#).unwrap(), "A");
258        assert_eq!(unquote_string(r#""\x61""#).unwrap(), "a");
259        assert_eq!(unquote_string(r#""\x20""#).unwrap(), " ");
260
261        // Test multiple hex escapes
262        assert_eq!(
263            unquote_string(r#""\x48\x65\x6c\x6c\x6f""#).unwrap(),
264            "Hello"
265        );
266
267        // Test invalid hex escapes
268        assert!(unquote_string(r#""\x""#).is_err()); // too short
269        assert!(unquote_string(r#""\x4""#).is_err()); // too short
270        assert!(unquote_string(r#""\x4G""#).is_err()); // invalid hex digit
271    }
272
273    #[test]
274    fn test_unquote_string_unicode_escapes() {
275        // Test \u unicode escapes (4 digits)
276        assert_eq!(unquote_string(r#""\u0041""#).unwrap(), "A");
277        assert_eq!(unquote_string(r#""\u0061""#).unwrap(), "a");
278        assert_eq!(unquote_string(r#""\u20AC""#).unwrap(), "€"); // Euro sign
279
280        // Test \U unicode escapes (8 digits)
281        assert_eq!(unquote_string(r#""\U00000041""#).unwrap(), "A");
282        assert_eq!(unquote_string(r#""\U00000061""#).unwrap(), "a");
283        assert_eq!(unquote_string(r#""\U000020AC""#).unwrap(), "€"); // Euro sign
284
285        // Test invalid unicode escapes
286        assert!(unquote_string(r#""\u""#).is_err()); // too short
287        assert!(unquote_string(r#""\u123""#).is_err()); // too short
288        assert!(unquote_string(r#""\U""#).is_err()); // too short
289        assert!(unquote_string(r#""\U1234567""#).is_err()); // too short
290        assert!(unquote_string(r#""\U11000000""#).is_err()); // beyond Unicode range
291    }
292
293    #[test]
294    fn test_unquote_string_octal_escapes() {
295        // Test octal escapes
296        assert_eq!(unquote_string(r#""\101""#).unwrap(), "A"); // 101 octal = 65 decimal = 'A'
297        assert_eq!(unquote_string(r#""\141""#).unwrap(), "a"); // 141 octal = 97 decimal = 'a'
298        assert_eq!(unquote_string(r#""\040""#).unwrap(), " "); // 040 octal = 32 decimal = space
299
300        // Test invalid octal escapes
301        assert!(unquote_string(r#""\1""#).is_err()); // too short
302        assert!(unquote_string(r#""\12""#).is_err()); // too short
303        assert!(unquote_string(r#""\400""#).is_err()); // 400 octal = 256 decimal > 255
304        assert!(unquote_string(r#""\8""#).is_err()); // invalid octal digit
305    }
306
307    #[test]
308    fn test_unquote_string_utf8_characters() {
309        // Test multi-byte UTF-8 characters
310        assert_eq!(unquote_string("\"café\"").unwrap(), "café");
311        assert_eq!(unquote_string("\"🦀\"").unwrap(), "🦀");
312        assert_eq!(unquote_string("\"こんにちは\"").unwrap(), "こんにちは");
313    }
314
315    #[test]
316    fn test_unquote_string_mixed_content() {
317        // Test strings with mixed content
318        assert_eq!(
319            unquote_string(r#""Hello, \u4e16\u754c!""#).unwrap(),
320            "Hello, 世界!"
321        );
322        assert_eq!(
323            unquote_string(r#""Line1\nLine2\tEnd""#).unwrap(),
324            "Line1\nLine2\tEnd"
325        );
326        assert_eq!(
327            unquote_string(r#""Path: C:\\\\Windows\\\\System32""#).unwrap(),
328            "Path: C:\\\\Windows\\\\System32"
329        );
330    }
331
332    #[test]
333    fn test_unquote_string_edge_cases() {
334        // Test quote character inside string without escape (should work if same as outer quote)
335        assert_eq!(unquote_string(r#"'It"s'"#).unwrap(), "It\"s");
336
337        // Test escaped quote that doesn't match outer quote (should fail)
338        assert!(unquote_string(r#""\'"'"#).is_err()); // trying to escape single quote in double quotes
339
340        // Test single quote with escaped single quote (should work)
341        assert_eq!(unquote_string(r#"'\''"#).unwrap(), "'");
342
343        // Test empty escape at end
344        assert!(unquote_string(r#""\""#).is_err());
345    }
346
347    #[test]
348    fn test_unquote_string_complex_escape_sequences() {
349        // Test complex combination of escape sequences
350        let complex = r#""Hello\x20World\n\u4e16\u754c\t\U0001F600""#;
351        let expected = "Hello World\n世界\t😀";
352        assert_eq!(unquote_string(complex).unwrap(), expected);
353    }
354
355    #[test]
356    fn test_unquote_string_backtick_edge_cases() {
357        // Test backticks with various content
358        assert_eq!(unquote_string("`hello world`").unwrap(), "hello world");
359        assert_eq!(unquote_string("`hello\nworld`").unwrap(), "hello\nworld"); // newlines allowed in backticks
360        assert_eq!(unquote_string("`hello\\nworld`").unwrap(), "hello\\nworld"); // backslashes treated literally
361
362        // Test nested backticks (should fail)
363        assert!(unquote_string("`hello`world`").is_err());
364        assert!(unquote_string("``hello`").is_err());
365    }
366
367    #[test]
368    fn test_escape_string() {
369        assert_eq!(escape_string("hello"), "hello");
370        assert_eq!(escape_string(r#"say "hi""#), r#"say \"hi\""#);
371        assert_eq!(escape_string("back\\slash"), "back\\\\slash");
372        assert_eq!(escape_string("new\nline"), "new\\nline");
373        assert_eq!(escape_string("tab\there"), "tab\\there");
374        assert_eq!(escape_string("cr\rhere"), "cr\\rhere");
375    }
376
377    #[test]
378    fn test_escape_unquote_roundtrip() {
379        // escape_string should produce output that unquote_string can reverse
380        let values = vec![
381            "hello",
382            "flagd\\.eval",
383            "a\\|b",
384            "C:\\\\Windows",
385            "say \"hi\"",
386        ];
387        for val in values {
388            let escaped = escape_string(val);
389            let quoted = format!("\"{}\"", escaped);
390            let unquoted = unquote_string(&quoted).unwrap();
391            assert_eq!(unquoted, val, "roundtrip failed for: {val:?}");
392        }
393    }
394}