aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJonas Maier <jonas@x77.dev>2026-05-04 23:57:57 +0200
committerJonas Maier <jonas@x77.dev>2026-05-04 23:57:57 +0200
commitbe3faf0552d60c996aeb51016f93ef64a3dd2d6f (patch)
tree0d770892864f1c66594494bee7f393663ad3441a /src
parente56e2d1f9206102068c402a0cadbe62284816586 (diff)
downloadpish-be3faf0552d60c996aeb51016f93ef64a3dd2d6f.tar.gz
escape test
Diffstat (limited to 'src')
-rw-r--r--src/parse/mod.rs22
-rw-r--r--src/parse/test.rs98
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<Item = Vec<u8>> {
+ struct CombinationGenerator<'a> {
+ choices: &'a [&'a [u8]],
+ indices: Vec<usize>,
+ done: bool,
+ }
+
+ impl<'a> Iterator for CombinationGenerator<'a> {
+ type Item = Vec<u8>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ 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 {
+ 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}"
+ );
+ }
+ }
+}