1pub 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
34pub 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 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 let r = s.chars().next().unwrap();
97 return Ok((r, &s[r.len_utf8()..]));
98 }
99
100 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', b'b' => '\x08', b'f' => '\x0c', b'n' => '\n',
113 b'r' => '\r',
114 b't' => '\t',
115 b'v' => '\x0b', 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 assert_eq!(unquote_string("\"hello\"").unwrap(), "hello");
188
189 assert_eq!(unquote_string("'hello'").unwrap(), "hello");
191
192 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 assert!(unquote_string("\"").is_err());
207 assert!(unquote_string("'").is_err());
208 assert!(unquote_string("`").is_err());
209
210 assert!(unquote_string("\"hello'").is_err());
212 assert!(unquote_string("'hello\"").is_err());
213 assert!(unquote_string("`hello\"").is_err());
214
215 assert!(unquote_string("#hello#").is_err());
217 assert!(unquote_string("/hello/").is_err());
218
219 assert!(unquote_string("\"hello\nworld\"").is_err());
221 assert!(unquote_string("'hello\nworld'").is_err());
222
223 assert!(unquote_string("`hello`world`").is_err());
225 }
226
227 #[test]
228 fn test_unquote_string_escaped_characters() {
229 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 assert_eq!(unquote_string(r#""\\""#).unwrap(), "\\");
240
241 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 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 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 assert_eq!(
263 unquote_string(r#""\x48\x65\x6c\x6c\x6f""#).unwrap(),
264 "Hello"
265 );
266
267 assert!(unquote_string(r#""\x""#).is_err()); assert!(unquote_string(r#""\x4""#).is_err()); assert!(unquote_string(r#""\x4G""#).is_err()); }
272
273 #[test]
274 fn test_unquote_string_unicode_escapes() {
275 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(), "€"); 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(), "€"); assert!(unquote_string(r#""\u""#).is_err()); assert!(unquote_string(r#""\u123""#).is_err()); assert!(unquote_string(r#""\U""#).is_err()); assert!(unquote_string(r#""\U1234567""#).is_err()); assert!(unquote_string(r#""\U11000000""#).is_err()); }
292
293 #[test]
294 fn test_unquote_string_octal_escapes() {
295 assert_eq!(unquote_string(r#""\101""#).unwrap(), "A"); assert_eq!(unquote_string(r#""\141""#).unwrap(), "a"); assert_eq!(unquote_string(r#""\040""#).unwrap(), " "); assert!(unquote_string(r#""\1""#).is_err()); assert!(unquote_string(r#""\12""#).is_err()); assert!(unquote_string(r#""\400""#).is_err()); assert!(unquote_string(r#""\8""#).is_err()); }
306
307 #[test]
308 fn test_unquote_string_utf8_characters() {
309 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 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 assert_eq!(unquote_string(r#"'It"s'"#).unwrap(), "It\"s");
336
337 assert!(unquote_string(r#""\'"'"#).is_err()); assert_eq!(unquote_string(r#"'\''"#).unwrap(), "'");
342
343 assert!(unquote_string(r#""\""#).is_err());
345 }
346
347 #[test]
348 fn test_unquote_string_complex_escape_sequences() {
349 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 assert_eq!(unquote_string("`hello world`").unwrap(), "hello world");
359 assert_eq!(unquote_string("`hello\nworld`").unwrap(), "hello\nworld"); assert_eq!(unquote_string("`hello\\nworld`").unwrap(), "hello\\nworld"); 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 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("ed).unwrap();
391 assert_eq!(unquoted, val, "roundtrip failed for: {val:?}");
392 }
393 }
394}