#![cfg(kani)]
use crate::emitter::escape::{
escape_bytes_into, escape_bytes_len, escape_variable_bytes_into, is_safe_unquoted_bytes,
is_valid_shell_identifier_bytes,
};
use crate::kani_bounded::{any_bounded_bytes, any_bounded_identifier_bytes};
const N: usize = 4;
const OUT: usize = 5 * N + 2;
#[kani::proof]
#[kani::unwind(24)]
fn verify_escape_safety() {
let mut raw = [0u8; N];
let len = any_bounded_bytes::<N>(&mut raw);
let input = &raw[..len];
let mut out = [0u8; OUT];
let n = match escape_bytes_into(input, &mut out) {
Some(n) => n,
None => {
assert!(false, "5N+2 is always a large enough buffer");
return;
}
};
let escaped = &out[..n];
assert!(n >= 1);
assert!(n == escape_bytes_len(input));
assert!(
is_safe_unquoted_bytes(escaped)
|| (n >= 2 && escaped[0] == b'\'' && escaped[n - 1] == b'\'')
);
assert!(scan_word(escaped) == Some(QState::Unquoted));
if is_safe_unquoted_bytes(input) {
assert!(escaped == input);
}
}
#[kani::proof]
#[kani::unwind(24)]
fn verify_escape_roundtrip() {
let mut raw = [0u8; N];
let len = any_bounded_bytes::<N>(&mut raw);
let input = &raw[..len];
let mut out = [0u8; OUT];
let n = match escape_bytes_into(input, &mut out) {
Some(n) => n,
None => {
assert!(false, "5N+2 is always a large enough buffer");
return;
}
};
let mut back = [0u8; OUT];
let m = unescape_into(&out[..n], &mut back);
assert!(m == input.len());
assert!(&back[..m] == input);
}
#[kani::proof]
#[kani::unwind(24)]
fn verify_escape_buffer_contract() {
let mut raw = [0u8; N];
let len = any_bounded_bytes::<N>(&mut raw);
let input = &raw[..len];
let need = escape_bytes_len(input);
let mut buf = [0xAAu8; OUT];
assert!(escape_bytes_into(input, &mut buf[..need - 1]).is_none());
let mut i = 0;
while i < OUT {
assert!(buf[i] == 0xAA);
i += 1;
}
}
#[kani::proof]
#[kani::unwind(6)]
fn verify_variable_expansion_safety() {
let mut raw = [0u8; N];
let len = any_bounded_identifier_bytes::<N>(&mut raw);
let name = &raw[..len];
assert!(is_valid_shell_identifier_bytes(name));
let mut out = [0u8; N];
let n = match escape_variable_bytes_into(name, &mut out) {
Some(n) => n,
None => {
assert!(false, "ASCII identifier, buffer of exactly name.len()");
return;
}
};
assert!(n == len);
assert!(&out[..n] == name);
}
#[kani::proof]
#[kani::unwind(29)]
fn verify_injection_safety() {
let mut raw = [0u8; N];
let len = any_bounded_bytes::<N>(&mut raw);
let input = &raw[..len];
let mut ctx = [0u8; 5 + OUT];
ctx[..5].copy_from_slice(b"echo ");
let n = match escape_bytes_into(input, &mut ctx[5..]) {
Some(n) => n,
None => {
assert!(false, "5N+2 is always a large enough buffer");
return;
}
};
assert!(&ctx[..5] == b"echo ");
assert!(scan_word(&ctx[5..5 + n]) == Some(QState::Unquoted));
}
#[derive(PartialEq, Eq, Clone, Copy)]
enum QState {
Unquoted,
Single,
Double,
}
fn is_metachar(b: u8) -> bool {
matches!(
b,
b';' | b'&'
| b'|'
| b'`'
| b'$'
| b'('
| b')'
| b'<'
| b'>'
| b'\n'
| b' '
| b'\t'
| b'*'
| b'?'
| b'['
| b'{'
| b'~'
| b'#'
| b'!'
)
}
fn scan_word(s: &[u8]) -> Option<QState> {
let mut st = QState::Unquoted;
let mut i = 0;
while i < s.len() {
let c = s[i];
i += 1;
if st != QState::Single && c == b'\\' {
i += 1;
continue;
}
st = match st {
QState::Single => step_single(c),
QState::Double => step_double(c)?,
QState::Unquoted => step_unquoted(c)?,
};
}
Some(st)
}
fn step_single(c: u8) -> QState {
if c == b'\'' {
QState::Unquoted
} else {
QState::Single
}
}
fn step_double(c: u8) -> Option<QState> {
if c == b'"' {
Some(QState::Unquoted)
} else if c == b'$' || c == b'`' {
None
} else {
Some(QState::Double)
}
}
fn step_unquoted(c: u8) -> Option<QState> {
if c == b'\'' {
Some(QState::Single)
} else if c == b'"' {
Some(QState::Double)
} else if is_metachar(c) {
None
} else {
Some(QState::Unquoted)
}
}
fn unescape_into(inp: &[u8], out: &mut [u8]) -> usize {
if !(inp.len() >= 2 && inp[0] == b'\'' && inp[inp.len() - 1] == b'\'') {
out[..inp.len()].copy_from_slice(inp);
return inp.len();
}
let inner = &inp[1..inp.len() - 1];
let mut w = 0;
let mut i = 0;
while i < inner.len() {
if is_requote_at(inner, i) {
out[w] = b'\'';
w += 1;
i += 5;
} else {
out[w] = inner[i];
w += 1;
i += 1;
}
}
w
}
fn is_requote_at(inner: &[u8], i: usize) -> bool {
i + 5 <= inner.len()
&& inner[i] == b'\''
&& inner[i + 1] == b'"'
&& inner[i + 2] == b'\''
&& inner[i + 3] == b'"'
&& inner[i + 4] == b'\''
}