darklua_core/nodes/expressions/
string_utils.rs

1use std::{fmt, iter::Peekable, str::CharIndices};
2
3#[derive(Debug, Clone)]
4enum StringErrorKind {
5    Invalid { message: String },
6    MalformedEscapeSequence { position: usize, message: String },
7}
8
9#[derive(Debug, Clone)]
10pub struct StringError {
11    kind: StringErrorKind,
12}
13
14impl fmt::Display for StringError {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match &self.kind {
17            StringErrorKind::Invalid { message } => {
18                write!(f, "invalid string: {}", message)
19            }
20            StringErrorKind::MalformedEscapeSequence { position, message } => {
21                write!(f, "malformed escape sequence at {}: {}", position, message)
22            }
23        }
24    }
25}
26
27impl StringError {
28    pub(crate) fn invalid(message: impl Into<String>) -> Self {
29        Self {
30            kind: StringErrorKind::Invalid {
31                message: message.into(),
32            },
33        }
34    }
35    pub(crate) fn malformed_escape_sequence(position: usize, message: impl Into<String>) -> Self {
36        Self {
37            kind: StringErrorKind::MalformedEscapeSequence {
38                position,
39                message: message.into(),
40            },
41        }
42    }
43}
44
45pub(crate) fn read_escaped_string(
46    chars: CharIndices,
47    reserve_size: Option<usize>,
48) -> Result<String, StringError> {
49    let mut chars = chars.peekable();
50
51    let mut value = String::new();
52    if let Some(reserve_size) = reserve_size {
53        value.reserve(reserve_size);
54    }
55
56    while let Some((position, char)) = chars.next() {
57        if char == '\\' {
58            if let Some((_, next_char)) = chars.next() {
59                match next_char {
60                    '\n' | '"' | '\'' | '\\' => value.push(next_char),
61                    'n' => value.push('\n'),
62                    't' => value.push('\t'),
63                    'a' => value.push('\u{7}'),
64                    'b' => value.push('\u{8}'),
65                    'v' => value.push('\u{B}'),
66                    'f' => value.push('\u{C}'),
67                    'r' => value.push('\r'),
68                    first_digit if first_digit.is_ascii_digit() => {
69                        let number = read_number(&mut chars, Some(first_digit), 10, 3);
70
71                        if number < 256 {
72                            value.push(number as u8 as char);
73                        } else {
74                            return Err(StringError::malformed_escape_sequence(
75                                position,
76                                "cannot escape ascii character greater than 256",
77                            ));
78                        }
79                    }
80                    'x' => {
81                        if let (Some(first_digit), Some(second_digit)) = (
82                            chars.next().map(|(_, c)| c).filter(char::is_ascii_hexdigit),
83                            chars.next().map(|(_, c)| c).filter(char::is_ascii_hexdigit),
84                        ) {
85                            let number = 16 * first_digit.to_digit(16).unwrap()
86                                + second_digit.to_digit(16).unwrap();
87
88                            if number < 256 {
89                                value.push(number as u8 as char);
90                            } else {
91                                return Err(StringError::malformed_escape_sequence(
92                                    position,
93                                    "cannot escape ascii character greater than 256",
94                                ));
95                            }
96                        } else {
97                            return Err(StringError::malformed_escape_sequence(
98                                position,
99                                "exactly two hexadecimal digit expected",
100                            ));
101                        }
102                    }
103                    'u' => {
104                        if !contains(&chars.next().map(|(_, c)| c), &'{') {
105                            return Err(StringError::malformed_escape_sequence(
106                                position,
107                                "expected opening curly brace",
108                            ));
109                        }
110
111                        let number = read_number(&mut chars, None, 16, 8);
112
113                        if !contains(&chars.next().map(|(_, c)| c), &'}') {
114                            return Err(StringError::malformed_escape_sequence(
115                                position,
116                                "expected closing curly brace",
117                            ));
118                        }
119
120                        if number > 0x10FFFF {
121                            return Err(StringError::malformed_escape_sequence(
122                                position,
123                                "invalid unicode value",
124                            ));
125                        }
126
127                        value.push(char::from_u32(number).expect("unable to convert u32 to char"));
128                    }
129                    'z' => {
130                        while chars
131                            .peek()
132                            .filter(|(_, char)| char.is_ascii_whitespace())
133                            .is_some()
134                        {
135                            chars.next();
136                        }
137                    }
138                    _ => {
139                        // an invalid escape does not error: it simply skips the backslash
140                        value.push(next_char);
141                    }
142                }
143            } else {
144                return Err(StringError::malformed_escape_sequence(
145                    position,
146                    "string ended after '\\'",
147                ));
148            }
149        } else {
150            value.push(char);
151        }
152    }
153
154    value.shrink_to_fit();
155
156    Ok(value)
157}
158
159fn read_number(
160    chars: &mut Peekable<CharIndices>,
161    first_digit: Option<char>,
162    radix: u32,
163    max: usize,
164) -> u32 {
165    let filter = match radix {
166        10 => char::is_ascii_digit,
167        16 => char::is_ascii_hexdigit,
168        _ => panic!("unsupported radix {}", radix),
169    };
170    let mut amount = first_digit
171        .map(|char| char.to_digit(radix).unwrap())
172        .unwrap_or(0);
173    let mut iteration_count: usize = first_digit.is_some().into();
174
175    while let Some(next_digit) = chars.peek().map(|(_, c)| *c).filter(filter) {
176        chars.next();
177
178        amount = amount * radix + next_digit.to_digit(radix).unwrap();
179        iteration_count += 1;
180
181        if iteration_count >= max {
182            break;
183        }
184    }
185
186    amount
187}
188
189#[inline]
190fn contains<T, U>(option: &Option<T>, x: &U) -> bool
191where
192    U: PartialEq<T>,
193{
194    match option {
195        Some(y) => x == y,
196        None => false,
197    }
198}