Skip to main content

lutra_sql/
string.rs

1use core::fmt;
2
3pub fn escape(string: &str, quote: char) -> EscapeQuotedString<'_> {
4    EscapeQuotedString { string, quote }
5}
6
7pub struct EscapeQuotedString<'a> {
8    string: &'a str,
9    quote: char,
10}
11
12impl fmt::Display for EscapeQuotedString<'_> {
13    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
14        // EscapeQuotedString doesn't know which mode of escape was
15        // chosen by the user. So this code must to correctly display
16        // strings without knowing if the strings are already escaped
17        // or not.
18        //
19        // If the quote symbol in the string is repeated twice, OR, if
20        // the quote symbol is after backslash, display all the chars
21        // without any escape. However, if the quote symbol is used
22        // just between usual chars, `fmt()` should display it twice."
23        //
24        // The following table has examples
25        //
26        // | original query | mode      | AST Node                                           | serialized   |
27        // | -------------  | --------- | -------------------------------------------------- | ------------ |
28        // | `"A""B""A"`    | no-escape | `DoubleQuotedString(String::from("A\"\"B\"\"A"))`  | `"A""B""A"`  |
29        // | `"A""B""A"`    | default   | `DoubleQuotedString(String::from("A\"B\"A"))`      | `"A""B""A"`  |
30        // | `"A\"B\"A"`    | no-escape | `DoubleQuotedString(String::from("A\\\"B\\\"A"))`  | `"A\"B\"A"`  |
31        // | `"A\"B\"A"`    | default   | `DoubleQuotedString(String::from("A\"B\"A"))`      | `"A""B""A"`  |
32        let quote = self.quote;
33        let mut previous_char = char::default();
34        let mut start_idx = 0;
35        let mut peekable_chars = self.string.char_indices().peekable();
36        while let Some(&(idx, ch)) = peekable_chars.peek() {
37            match ch {
38                char if char == quote => {
39                    if previous_char == '\\' {
40                        // the quote is already escaped with a backslash, skip
41                        peekable_chars.next();
42                        continue;
43                    }
44                    peekable_chars.next();
45                    match peekable_chars.peek() {
46                        Some((_, c)) if *c == quote => {
47                            // the quote is already escaped with another quote, skip
48                            peekable_chars.next();
49                        }
50                        _ => {
51                            // The quote is not escaped.
52                            // Including idx in the range, so the quote at idx will be printed twice:
53                            // in this call to write_str() and in the next one.
54                            f.write_str(&self.string[start_idx..=idx])?;
55                            start_idx = idx;
56                        }
57                    }
58                }
59                _ => {
60                    peekable_chars.next();
61                }
62            }
63            previous_char = ch;
64        }
65        f.write_str(&self.string[start_idx..])?;
66        Ok(())
67    }
68}