From be3faf0552d60c996aeb51016f93ef64a3dd2d6f Mon Sep 17 00:00:00 2001 From: Jonas Maier Date: Mon, 4 May 2026 23:57:57 +0200 Subject: escape test --- src/parse/mod.rs | 22 +++++++++++-- src/parse/test.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 02012dc..efef937 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -717,12 +717,12 @@ impl StringDelimiter { let first = s[0]; match self { StringDelimiter::None => { - if matches!(first, b' ' | b'$' | b'\\' | b'\'' | b'"') { + if matches!(first, b' ' | b'$' | b'\\' | b'\'' | b'"' | b'|' | b'{' | b'}') { out.push(b'\\'); } } StringDelimiter::Interp | StringDelimiter::InterpCustom(_) => { - if matches!(first, b'$' | b'\\' | b'"') { + if matches!(first, b'$' | b'\\' | b'"' | b'|') { out.push(b'\\'); } } @@ -750,6 +750,22 @@ impl StringDelimiter { } } + pub fn write_opening_delimiter(&self, out: &mut BString) { + match self { + StringDelimiter::None => (), + StringDelimiter::Interp => out.push(b'"'), + StringDelimiter::Strict => out.push(b'\''), + StringDelimiter::InterpCustom(delim) => { + out.push_all(&delim); + out.push_all(b"\"\"\""); + } + StringDelimiter::StrictCustom(delim) => { + out.push_all(&delim); + out.push_all(b"'''"); + } + } + } + pub fn write_closing_delimiter(&self, out: &mut BString) { match self { StringDelimiter::None => (), @@ -841,7 +857,7 @@ impl Parse for ExpString { let x = b.adv(); - if x == b'\\' { + if x == b'\\' && !delim.is_strict() { if let Some(x) = parse_escape_code(b)? { add_char(p, x); } diff --git a/src/parse/test.rs b/src/parse/test.rs index dd6fbca..01dea5d 100644 --- a/src/parse/test.rs +++ b/src/parse/test.rs @@ -342,3 +342,101 @@ fn backslash_joins_lines() { pipes([cmd([estr(b"echo"), estr(b"foobar")]),]) ) } + +fn combinations(choices: &[&[u8]], n: usize) -> impl Iterator> { + struct CombinationGenerator<'a> { + choices: &'a [&'a [u8]], + indices: Vec, + done: bool, + } + + impl<'a> Iterator for CombinationGenerator<'a> { + type Item = Vec; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + + // Build current combination + let mut out = Vec::new(); + for &i in &self.indices { + out.extend_from_slice(self.choices[i]); + } + + // Increment like a base-N counter + for pos in (0..self.indices.len()).rev() { + if self.indices[pos] + 1 < self.choices.len() { + self.indices[pos] += 1; + for j in pos + 1..self.indices.len() { + self.indices[j] = 0; + } + return Some(out); + } + } + + // Last combination reached + self.done = true; + Some(out) + } + } + + CombinationGenerator { + choices, + indices: vec![0; n], + done: n == 0 && choices.is_empty(), + } +} + +fn coerce(t: T) -> T { + t +} + +#[test] +fn test_escape_bruteforce() { + let words = [ + coerce::<&'static [u8]>(b"\""), + b"'", + b"x", + b"y", + b"z", + b"\\", + b"|", + b"$", + b"{", + b"}", + b"'''", + b"\"\"\"", + ]; + + let quotes = [ + StringDelimiter::None, + StringDelimiter::Interp, + StringDelimiter::Strict, + StringDelimiter::InterpCustom(b"x".into()), + StringDelimiter::StrictCustom(b"x".into()), + ]; + + for quote in quotes { + for phrase in combinations(&words[..], 5) { + let mut x = Vec::new(); + quote.write_opening_delimiter(&mut x); + quote.escape(&phrase, &mut x); + quote.write_closing_delimiter(&mut x); + let s = ExpString::parse(&mut Cursor::new(&x, ParseMode::Command)) + .map_err(|e| format!("{quote:?} escape {} failed: {e:?}", x.escape_ascii())) + .unwrap(); + assert_eq!(s.parts.len(), 1, "{}", x.escape_ascii()); + let s = s.parts[0].clone(); + let s = s.unwrap_boring(); + + let x = String::from_utf8(x).unwrap(); + let s = String::from_utf8(s).unwrap(); + let phrase = String::from_utf8(phrase).unwrap(); + assert_eq!( + phrase, s, + "escape/parse roundtrip in StringDelimiter::{quote:?} failed: {phrase} -> {x} -> {s}" + ); + } + } +} -- cgit v1.2.3