#![allow(clippy::collapsible_if)]
use std::fmt;
use crate::cfg;
use crate::ssa::ir::{BlockId, SsaValue};
use crate::utils::snippet::truncate_at_char_boundary;
pub const MAX_EXPR_DEPTH: u32 = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Op {
Add,
Sub,
Mul,
Div,
Mod,
BitAnd,
BitOr,
BitXor,
LeftShift,
RightShift,
Eq,
NotEq,
Lt,
LtEq,
Gt,
GtEq,
}
impl From<cfg::BinOp> for Op {
fn from(b: cfg::BinOp) -> Self {
match b {
cfg::BinOp::Add => Op::Add,
cfg::BinOp::Sub => Op::Sub,
cfg::BinOp::Mul => Op::Mul,
cfg::BinOp::Div => Op::Div,
cfg::BinOp::Mod => Op::Mod,
cfg::BinOp::BitAnd => Op::BitAnd,
cfg::BinOp::BitOr => Op::BitOr,
cfg::BinOp::BitXor => Op::BitXor,
cfg::BinOp::LeftShift => Op::LeftShift,
cfg::BinOp::RightShift => Op::RightShift,
cfg::BinOp::Eq => Op::Eq,
cfg::BinOp::NotEq => Op::NotEq,
cfg::BinOp::Lt => Op::Lt,
cfg::BinOp::LtEq => Op::LtEq,
cfg::BinOp::Gt => Op::Gt,
cfg::BinOp::GtEq => Op::GtEq,
}
}
}
impl fmt::Display for Op {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Op::Add => write!(f, "+"),
Op::Sub => write!(f, "-"),
Op::Mul => write!(f, "*"),
Op::Div => write!(f, "/"),
Op::Mod => write!(f, "%"),
Op::BitAnd => write!(f, "&"),
Op::BitOr => write!(f, "|"),
Op::BitXor => write!(f, "^"),
Op::LeftShift => write!(f, "<<"),
Op::RightShift => write!(f, ">>"),
Op::Eq => write!(f, "=="),
Op::NotEq => write!(f, "!="),
Op::Lt => write!(f, "<"),
Op::LtEq => write!(f, "<="),
Op::Gt => write!(f, ">"),
Op::GtEq => write!(f, ">="),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SymbolicValue {
Concrete(i64),
ConcreteStr(String),
Symbol(SsaValue),
BinOp(Op, Box<SymbolicValue>, Box<SymbolicValue>),
Concat(Box<SymbolicValue>, Box<SymbolicValue>),
Call(String, Vec<SymbolicValue>),
Phi(Vec<(BlockId, SymbolicValue)>),
Substr(
Box<SymbolicValue>,
Box<SymbolicValue>,
Option<Box<SymbolicValue>>,
),
Replace(Box<SymbolicValue>, String, String),
ToLower(Box<SymbolicValue>),
ToUpper(Box<SymbolicValue>),
Trim(Box<SymbolicValue>),
StrLen(Box<SymbolicValue>),
Encode(super::strings::TransformKind, Box<SymbolicValue>),
Decode(super::strings::TransformKind, Box<SymbolicValue>),
Unknown,
}
impl SymbolicValue {
pub fn depth(&self) -> u32 {
match self {
SymbolicValue::Concrete(_)
| SymbolicValue::ConcreteStr(_)
| SymbolicValue::Symbol(_)
| SymbolicValue::Unknown => 0,
SymbolicValue::BinOp(_, l, r) | SymbolicValue::Concat(l, r) => {
1 + l.depth().max(r.depth())
}
SymbolicValue::Call(_, args) => 1 + args.iter().map(|a| a.depth()).max().unwrap_or(0),
SymbolicValue::Phi(operands) => {
1 + operands.iter().map(|(_, v)| v.depth()).max().unwrap_or(0)
}
SymbolicValue::ToLower(s)
| SymbolicValue::ToUpper(s)
| SymbolicValue::Trim(s)
| SymbolicValue::StrLen(s)
| SymbolicValue::Replace(s, _, _)
| SymbolicValue::Encode(_, s)
| SymbolicValue::Decode(_, s) => 1 + s.depth(),
SymbolicValue::Substr(s, start, end) => {
let max_child = s
.depth()
.max(start.depth())
.max(end.as_ref().map(|e| e.depth()).unwrap_or(0));
1 + max_child
}
}
}
pub fn is_concrete(&self) -> bool {
matches!(
self,
SymbolicValue::Concrete(_) | SymbolicValue::ConcreteStr(_)
)
}
pub fn as_concrete_int(&self) -> Option<i64> {
match self {
SymbolicValue::Concrete(n) => Some(*n),
_ => None,
}
}
pub fn as_concrete_str(&self) -> Option<&str> {
match self {
SymbolicValue::ConcreteStr(s) => Some(s),
_ => None,
}
}
}
pub fn mk_binop(op: Op, lhs: SymbolicValue, rhs: SymbolicValue) -> SymbolicValue {
if let (SymbolicValue::Concrete(a), SymbolicValue::Concrete(b)) = (&lhs, &rhs) {
let result = match op {
Op::Add => a.checked_add(*b),
Op::Sub => a.checked_sub(*b),
Op::Mul => a.checked_mul(*b),
Op::Div => {
if *b == 0 {
None
} else {
a.checked_div(*b)
}
}
Op::Mod => {
if *b == 0 {
None
} else {
a.checked_rem(*b)
}
}
Op::BitAnd => Some(*a & *b),
Op::BitOr => Some(*a | *b),
Op::BitXor => Some(*a ^ *b),
Op::LeftShift => {
if *b < 0 || *b > 63 {
None
} else {
a.checked_shl(*b as u32)
}
}
Op::RightShift => {
if *b < 0 || *b > 63 {
None
} else {
a.checked_shr(*b as u32)
}
}
Op::Eq => Some(if *a == *b { 1 } else { 0 }),
Op::NotEq => Some(if *a != *b { 1 } else { 0 }),
Op::Lt => Some(if *a < *b { 1 } else { 0 }),
Op::LtEq => Some(if *a <= *b { 1 } else { 0 }),
Op::Gt => Some(if *a > *b { 1 } else { 0 }),
Op::GtEq => Some(if *a >= *b { 1 } else { 0 }),
};
return match result {
Some(n) => SymbolicValue::Concrete(n),
None => SymbolicValue::Unknown,
};
}
let depth = 1 + lhs.depth().max(rhs.depth());
if depth > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::BinOp(op, Box::new(lhs), Box::new(rhs))
}
pub fn mk_concat(lhs: SymbolicValue, rhs: SymbolicValue) -> SymbolicValue {
if let (SymbolicValue::ConcreteStr(a), SymbolicValue::ConcreteStr(b)) = (&lhs, &rhs) {
return SymbolicValue::ConcreteStr(format!("{}{}", a, b));
}
let depth = 1 + lhs.depth().max(rhs.depth());
if depth > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Concat(Box::new(lhs), Box::new(rhs))
}
pub fn mk_call(name: String, args: Vec<SymbolicValue>) -> SymbolicValue {
let max_arg_depth = args.iter().map(|a| a.depth()).max().unwrap_or(0);
if 1 + max_arg_depth > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Call(name, args)
}
pub fn mk_phi(operands: Vec<(BlockId, SymbolicValue)>) -> SymbolicValue {
if operands.is_empty() {
return SymbolicValue::Unknown;
}
if operands.len() == 1 {
return operands.into_iter().next().unwrap().1;
}
if operands.windows(2).all(|w| w[0].1 == w[1].1) {
return operands.into_iter().next().unwrap().1;
}
let max_depth = operands.iter().map(|(_, v)| v.depth()).max().unwrap_or(0);
if 1 + max_depth > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Phi(operands)
}
pub fn mk_trim(s: SymbolicValue) -> SymbolicValue {
if let Some(result) = s.as_concrete_str().and_then(|cs| {
super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::Trim, cs)
}) {
return result;
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Trim(Box::new(s))
}
pub fn mk_to_lower(s: SymbolicValue) -> SymbolicValue {
if let Some(result) = s.as_concrete_str().and_then(|cs| {
super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::ToLower, cs)
}) {
return result;
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::ToLower(Box::new(s))
}
pub fn mk_to_upper(s: SymbolicValue) -> SymbolicValue {
if let Some(result) = s.as_concrete_str().and_then(|cs| {
super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::ToUpper, cs)
}) {
return result;
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::ToUpper(Box::new(s))
}
pub fn mk_replace(s: SymbolicValue, pattern: String, replacement: String) -> SymbolicValue {
if let Some(result) = s.as_concrete_str().and_then(|cs| {
super::strings::evaluate_string_op_concrete(
&super::strings::StringMethod::Replace {
pattern: pattern.clone(),
replacement: replacement.clone(),
},
cs,
)
}) {
return result;
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Replace(Box::new(s), pattern, replacement)
}
pub fn mk_substr(
s: SymbolicValue,
start: SymbolicValue,
end: Option<SymbolicValue>,
) -> SymbolicValue {
if let Some(cs) = s.as_concrete_str() {
if let Some(i) = start.as_concrete_int() {
let i = i.max(0) as usize;
match end.as_ref().and_then(|e| e.as_concrete_int()) {
Some(j) => {
let j = j.max(0) as usize;
let result = cs.get(i..j.min(cs.len())).unwrap_or("");
return SymbolicValue::ConcreteStr(result.to_owned());
}
None if end.is_none() => {
let result = cs.get(i..).unwrap_or("");
return SymbolicValue::ConcreteStr(result.to_owned());
}
_ => {} }
}
}
let max_child = s
.depth()
.max(start.depth())
.max(end.as_ref().map(|e| e.depth()).unwrap_or(0));
if 1 + max_child > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Substr(Box::new(s), Box::new(start), end.map(Box::new))
}
pub fn mk_strlen(s: SymbolicValue) -> SymbolicValue {
if let Some(result) = s.as_concrete_str().and_then(|cs| {
super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::StrLen, cs)
}) {
return result;
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::StrLen(Box::new(s))
}
pub fn mk_encode(kind: super::strings::TransformKind, s: SymbolicValue) -> SymbolicValue {
if let Some(cs) = s.as_concrete_str() {
if let Some(encoded) = super::strings::encode_concrete_for_witness(kind, cs) {
return SymbolicValue::ConcreteStr(encoded);
}
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Encode(kind, Box::new(s))
}
pub fn mk_decode(kind: super::strings::TransformKind, s: SymbolicValue) -> SymbolicValue {
if let Some(cs) = s.as_concrete_str() {
if let Some(decoded) = super::strings::decode_concrete_for_witness(kind, cs) {
return SymbolicValue::ConcreteStr(decoded);
}
}
if 1 + s.depth() > MAX_EXPR_DEPTH {
return SymbolicValue::Unknown;
}
SymbolicValue::Decode(kind, Box::new(s))
}
const MAX_DISPLAY_LEN: usize = 256;
const MAX_STR_DISPLAY_LEN: usize = 64;
impl fmt::Display for SymbolicValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = display_inner(self);
if s.len() > MAX_DISPLAY_LEN {
write!(f, "{}...", truncate_at_char_boundary(&s, MAX_DISPLAY_LEN))
} else {
write!(f, "{}", s)
}
}
}
fn display_inner(val: &SymbolicValue) -> String {
match val {
SymbolicValue::Concrete(n) => format!("{}", n),
SymbolicValue::ConcreteStr(s) => {
if s.len() > MAX_STR_DISPLAY_LEN {
format!(
"\"{}...\"",
truncate_at_char_boundary(s, MAX_STR_DISPLAY_LEN)
)
} else {
format!("\"{}\"", s)
}
}
SymbolicValue::Symbol(v) => format!("sym(v{})", v.0),
SymbolicValue::BinOp(op, l, r) => {
format!("({} {} {})", display_inner(l), op, display_inner(r))
}
SymbolicValue::Concat(l, r) => {
format!("({} ++ {})", display_inner(l), display_inner(r))
}
SymbolicValue::Call(name, args) => {
let arg_strs: Vec<String> = args.iter().map(display_inner).collect();
format!("{}({})", name, arg_strs.join(", "))
}
SymbolicValue::Phi(operands) => {
let parts: Vec<String> = operands
.iter()
.map(|(bid, v)| format!("B{}:{}", bid.0, display_inner(v)))
.collect();
format!("phi({})", parts.join(", "))
}
SymbolicValue::Trim(s) => format!("{}.trim()", display_inner(s)),
SymbolicValue::ToLower(s) => format!("{}.toLowerCase()", display_inner(s)),
SymbolicValue::ToUpper(s) => format!("{}.toUpperCase()", display_inner(s)),
SymbolicValue::Replace(s, pat, rep) => {
format!("{}.replace(\"{}\", \"{}\")", display_inner(s), pat, rep)
}
SymbolicValue::Substr(s, start, end) => match end {
Some(e) => format!(
"{}.substr({}, {})",
display_inner(s),
display_inner(start),
display_inner(e)
),
None => format!("{}.substr({})", display_inner(s), display_inner(start)),
},
SymbolicValue::StrLen(s) => format!("strlen({})", display_inner(s)),
SymbolicValue::Encode(kind, s) => {
format!("{}({})", kind.display_name(), display_inner(s))
}
SymbolicValue::Decode(kind, s) => {
format!("decode_{}({})", kind.display_name(), display_inner(s))
}
SymbolicValue::Unknown => "?".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn concrete_fold_add() {
assert_eq!(
mk_binop(
Op::Add,
SymbolicValue::Concrete(3),
SymbolicValue::Concrete(5)
),
SymbolicValue::Concrete(8)
);
}
#[test]
fn concrete_fold_sub() {
assert_eq!(
mk_binop(
Op::Sub,
SymbolicValue::Concrete(10),
SymbolicValue::Concrete(3)
),
SymbolicValue::Concrete(7)
);
}
#[test]
fn concrete_fold_mul() {
assert_eq!(
mk_binop(
Op::Mul,
SymbolicValue::Concrete(4),
SymbolicValue::Concrete(7)
),
SymbolicValue::Concrete(28)
);
}
#[test]
fn concrete_fold_div() {
assert_eq!(
mk_binop(
Op::Div,
SymbolicValue::Concrete(15),
SymbolicValue::Concrete(3)
),
SymbolicValue::Concrete(5)
);
}
#[test]
fn concrete_fold_mod() {
assert_eq!(
mk_binop(
Op::Mod,
SymbolicValue::Concrete(17),
SymbolicValue::Concrete(5)
),
SymbolicValue::Concrete(2)
);
}
#[test]
fn overflow_add() {
assert_eq!(
mk_binop(
Op::Add,
SymbolicValue::Concrete(i64::MAX),
SymbolicValue::Concrete(1)
),
SymbolicValue::Unknown
);
}
#[test]
fn overflow_sub() {
assert_eq!(
mk_binop(
Op::Sub,
SymbolicValue::Concrete(i64::MIN),
SymbolicValue::Concrete(1)
),
SymbolicValue::Unknown
);
}
#[test]
fn overflow_mul() {
assert_eq!(
mk_binop(
Op::Mul,
SymbolicValue::Concrete(i64::MAX),
SymbolicValue::Concrete(2)
),
SymbolicValue::Unknown
);
}
#[test]
fn div_by_zero() {
assert_eq!(
mk_binop(
Op::Div,
SymbolicValue::Concrete(10),
SymbolicValue::Concrete(0)
),
SymbolicValue::Unknown
);
}
#[test]
fn mod_by_zero() {
assert_eq!(
mk_binop(
Op::Mod,
SymbolicValue::Concrete(10),
SymbolicValue::Concrete(0)
),
SymbolicValue::Unknown
);
}
#[test]
fn min_mod_neg_one() {
assert_eq!(
mk_binop(
Op::Mod,
SymbolicValue::Concrete(i64::MIN),
SymbolicValue::Concrete(-1)
),
SymbolicValue::Unknown
);
}
#[test]
fn depth_bounding() {
let mut val = SymbolicValue::Symbol(SsaValue(0));
for _ in 0..MAX_EXPR_DEPTH {
val = mk_binop(Op::Add, val, SymbolicValue::Concrete(1));
}
assert_ne!(val, SymbolicValue::Unknown);
assert_eq!(val.depth(), MAX_EXPR_DEPTH);
val = mk_binop(Op::Add, val, SymbolicValue::Concrete(1));
assert_eq!(val, SymbolicValue::Unknown);
}
#[test]
fn concat_fold() {
assert_eq!(
mk_concat(
SymbolicValue::ConcreteStr("hello ".into()),
SymbolicValue::ConcreteStr("world".into()),
),
SymbolicValue::ConcreteStr("hello world".into())
);
}
#[test]
fn concat_no_int_coercion() {
let result = mk_concat(
SymbolicValue::ConcreteStr("val=".into()),
SymbolicValue::Concrete(42),
);
assert!(matches!(result, SymbolicValue::Concat(_, _)));
}
#[test]
fn concat_depth_bounding() {
let mut val = SymbolicValue::ConcreteStr("a".into());
for _ in 0..MAX_EXPR_DEPTH {
val = mk_concat(val, SymbolicValue::Symbol(SsaValue(0)));
}
assert_eq!(val.depth(), MAX_EXPR_DEPTH);
val = mk_concat(val, SymbolicValue::Symbol(SsaValue(0)));
assert_eq!(val, SymbolicValue::Unknown);
}
#[test]
fn phi_single_operand_unwrap() {
let v = SymbolicValue::Concrete(42);
assert_eq!(mk_phi(vec![(BlockId(0), v.clone())]), v);
}
#[test]
fn phi_all_same_fold() {
let v = SymbolicValue::Concrete(7);
assert_eq!(
mk_phi(vec![(BlockId(0), v.clone()), (BlockId(1), v.clone())]),
v
);
}
#[test]
fn phi_different_values() {
let result = mk_phi(vec![
(BlockId(0), SymbolicValue::Concrete(1)),
(BlockId(1), SymbolicValue::Concrete(2)),
]);
assert!(matches!(result, SymbolicValue::Phi(_)));
}
#[test]
fn phi_empty() {
assert_eq!(mk_phi(vec![]), SymbolicValue::Unknown);
}
#[test]
fn call_depth_bounding() {
let deep = {
let mut v = SymbolicValue::Symbol(SsaValue(0));
for _ in 0..MAX_EXPR_DEPTH {
v = mk_binop(Op::Add, v, SymbolicValue::Concrete(1));
}
v
};
let result = mk_call("f".into(), vec![deep]);
assert_eq!(result, SymbolicValue::Unknown);
}
#[test]
fn depth_leaf_nodes() {
assert_eq!(SymbolicValue::Concrete(0).depth(), 0);
assert_eq!(SymbolicValue::ConcreteStr("x".into()).depth(), 0);
assert_eq!(SymbolicValue::Symbol(SsaValue(0)).depth(), 0);
assert_eq!(SymbolicValue::Unknown.depth(), 0);
}
#[test]
fn depth_nested() {
let v = mk_binop(
Op::Add,
mk_binop(
Op::Mul,
SymbolicValue::Concrete(2),
SymbolicValue::Symbol(SsaValue(0)),
),
SymbolicValue::Concrete(1),
);
assert_eq!(v.depth(), 2);
}
#[test]
fn is_concrete_checks() {
assert!(SymbolicValue::Concrete(1).is_concrete());
assert!(SymbolicValue::ConcreteStr("x".into()).is_concrete());
assert!(!SymbolicValue::Symbol(SsaValue(0)).is_concrete());
assert!(!SymbolicValue::Unknown.is_concrete());
}
#[test]
fn as_concrete_int_checks() {
assert_eq!(SymbolicValue::Concrete(42).as_concrete_int(), Some(42));
assert_eq!(
SymbolicValue::ConcreteStr("x".into()).as_concrete_int(),
None
);
assert_eq!(SymbolicValue::Unknown.as_concrete_int(), None);
}
#[test]
fn as_concrete_str_checks() {
assert_eq!(
SymbolicValue::ConcreteStr("hi".into()).as_concrete_str(),
Some("hi")
);
assert_eq!(SymbolicValue::Concrete(1).as_concrete_str(), None);
}
#[test]
fn display_concrete() {
assert_eq!(format!("{}", SymbolicValue::Concrete(42)), "42");
}
#[test]
fn display_concrete_str() {
assert_eq!(
format!("{}", SymbolicValue::ConcreteStr("hello".into())),
"\"hello\""
);
}
#[test]
fn display_symbol() {
assert_eq!(format!("{}", SymbolicValue::Symbol(SsaValue(3))), "sym(v3)");
}
#[test]
fn display_binop() {
let v = mk_binop(
Op::Add,
SymbolicValue::Symbol(SsaValue(1)),
SymbolicValue::Concrete(2),
);
assert_eq!(format!("{}", v), "(sym(v1) + 2)");
}
#[test]
fn display_concat() {
let v = mk_concat(
SymbolicValue::ConcreteStr("SELECT ".into()),
SymbolicValue::Symbol(SsaValue(5)),
);
assert_eq!(format!("{}", v), "(\"SELECT \" ++ sym(v5))");
}
#[test]
fn display_call() {
let v = mk_call("parseInt".into(), vec![SymbolicValue::Symbol(SsaValue(2))]);
assert_eq!(format!("{}", v), "parseInt(sym(v2))");
}
#[test]
fn display_phi() {
let v = mk_phi(vec![
(BlockId(0), SymbolicValue::Concrete(1)),
(BlockId(1), SymbolicValue::Symbol(SsaValue(3))),
]);
assert_eq!(format!("{}", v), "phi(B0:1, B1:sym(v3))");
}
#[test]
fn display_unknown() {
assert_eq!(format!("{}", SymbolicValue::Unknown), "?");
}
#[test]
fn display_truncation() {
let mut v = SymbolicValue::Symbol(SsaValue(0));
for i in 1..30 {
v = mk_binop(Op::Add, v, SymbolicValue::Symbol(SsaValue(i)));
}
let s = format!("{}", v);
assert!(s.len() <= MAX_DISPLAY_LEN + 3); if s.len() > MAX_DISPLAY_LEN {
assert!(s.ends_with("..."));
}
}
#[test]
fn display_long_string_truncation() {
let long = "a".repeat(100);
let v = SymbolicValue::ConcreteStr(long);
let s = format!("{}", v);
assert!(s.contains("..."));
assert!(s.len() <= MAX_STR_DISPLAY_LEN + 6); }
#[test]
fn op_from_cfg_binop() {
assert_eq!(Op::from(cfg::BinOp::Add), Op::Add);
assert_eq!(Op::from(cfg::BinOp::Sub), Op::Sub);
assert_eq!(Op::from(cfg::BinOp::Mul), Op::Mul);
assert_eq!(Op::from(cfg::BinOp::Div), Op::Div);
assert_eq!(Op::from(cfg::BinOp::Mod), Op::Mod);
}
#[test]
fn op_from_cfg_binop_extended() {
assert_eq!(Op::from(cfg::BinOp::BitAnd), Op::BitAnd);
assert_eq!(Op::from(cfg::BinOp::BitOr), Op::BitOr);
assert_eq!(Op::from(cfg::BinOp::BitXor), Op::BitXor);
assert_eq!(Op::from(cfg::BinOp::LeftShift), Op::LeftShift);
assert_eq!(Op::from(cfg::BinOp::RightShift), Op::RightShift);
assert_eq!(Op::from(cfg::BinOp::Eq), Op::Eq);
assert_eq!(Op::from(cfg::BinOp::NotEq), Op::NotEq);
assert_eq!(Op::from(cfg::BinOp::Lt), Op::Lt);
assert_eq!(Op::from(cfg::BinOp::LtEq), Op::LtEq);
assert_eq!(Op::from(cfg::BinOp::Gt), Op::Gt);
assert_eq!(Op::from(cfg::BinOp::GtEq), Op::GtEq);
}
#[test]
fn display_bitwise_ops() {
assert_eq!(format!("{}", Op::BitAnd), "&");
assert_eq!(format!("{}", Op::BitOr), "|");
assert_eq!(format!("{}", Op::BitXor), "^");
assert_eq!(format!("{}", Op::LeftShift), "<<");
assert_eq!(format!("{}", Op::RightShift), ">>");
}
#[test]
fn display_comparison_ops() {
assert_eq!(format!("{}", Op::Eq), "==");
assert_eq!(format!("{}", Op::NotEq), "!=");
assert_eq!(format!("{}", Op::Lt), "<");
assert_eq!(format!("{}", Op::LtEq), "<=");
assert_eq!(format!("{}", Op::Gt), ">");
assert_eq!(format!("{}", Op::GtEq), ">=");
}
#[test]
fn concrete_fold_bit_and() {
assert_eq!(mk_binop(Op::BitAnd, c(0xFF), c(0x0F)), c(0x0F));
assert_eq!(mk_binop(Op::BitAnd, c(-1), c(0x07)), c(0x07));
}
#[test]
fn concrete_fold_bit_or() {
assert_eq!(mk_binop(Op::BitOr, c(0xF0), c(0x0F)), c(0xFF));
}
#[test]
fn concrete_fold_bit_xor() {
assert_eq!(mk_binop(Op::BitXor, c(0xFF), c(0x0F)), c(0xF0));
assert_eq!(mk_binop(Op::BitXor, c(42), c(42)), c(0));
}
#[test]
fn concrete_fold_left_shift() {
assert_eq!(mk_binop(Op::LeftShift, c(1), c(3)), c(8));
assert_eq!(mk_binop(Op::LeftShift, c(0x0F), c(4)), c(0xF0));
}
#[test]
fn concrete_fold_right_shift() {
assert_eq!(mk_binop(Op::RightShift, c(16), c(2)), c(4));
assert_eq!(mk_binop(Op::RightShift, c(0xFF), c(4)), c(0x0F));
}
#[test]
fn left_shift_negative_amount() {
assert_eq!(mk_binop(Op::LeftShift, c(1), c(-1)), SymbolicValue::Unknown);
}
#[test]
fn left_shift_amount_64() {
assert_eq!(mk_binop(Op::LeftShift, c(1), c(64)), SymbolicValue::Unknown);
}
#[test]
fn left_shift_amount_63() {
let result = mk_binop(Op::LeftShift, c(1), c(63));
assert_eq!(result, c(1i64 << 63));
}
#[test]
fn right_shift_negative_amount() {
assert_eq!(
mk_binop(Op::RightShift, c(1), c(-1)),
SymbolicValue::Unknown
);
}
#[test]
fn right_shift_amount_64() {
assert_eq!(
mk_binop(Op::RightShift, c(1), c(64)),
SymbolicValue::Unknown
);
}
#[test]
fn concrete_fold_eq_true() {
assert_eq!(mk_binop(Op::Eq, c(5), c(5)), c(1));
}
#[test]
fn concrete_fold_eq_false() {
assert_eq!(mk_binop(Op::Eq, c(5), c(3)), c(0));
}
#[test]
fn concrete_fold_neq() {
assert_eq!(mk_binop(Op::NotEq, c(5), c(3)), c(1));
assert_eq!(mk_binop(Op::NotEq, c(5), c(5)), c(0));
}
#[test]
fn concrete_fold_lt() {
assert_eq!(mk_binop(Op::Lt, c(3), c(5)), c(1));
assert_eq!(mk_binop(Op::Lt, c(5), c(3)), c(0));
assert_eq!(mk_binop(Op::Lt, c(5), c(5)), c(0));
}
#[test]
fn concrete_fold_lteq() {
assert_eq!(mk_binop(Op::LtEq, c(3), c(3)), c(1));
assert_eq!(mk_binop(Op::LtEq, c(4), c(3)), c(0));
}
#[test]
fn concrete_fold_gt() {
assert_eq!(mk_binop(Op::Gt, c(5), c(3)), c(1));
assert_eq!(mk_binop(Op::Gt, c(3), c(5)), c(0));
}
#[test]
fn concrete_fold_gteq() {
assert_eq!(mk_binop(Op::GtEq, c(3), c(3)), c(1));
assert_eq!(mk_binop(Op::GtEq, c(2), c(3)), c(0));
}
fn c(n: i64) -> SymbolicValue {
SymbolicValue::Concrete(n)
}
#[test]
fn mk_trim_concrete_fold() {
assert_eq!(
mk_trim(SymbolicValue::ConcreteStr(" hello ".into())),
SymbolicValue::ConcreteStr("hello".into())
);
}
#[test]
fn mk_trim_symbolic() {
let v = mk_trim(SymbolicValue::Symbol(SsaValue(0)));
assert!(matches!(v, SymbolicValue::Trim(_)));
assert_eq!(v.depth(), 1);
}
#[test]
fn mk_to_lower_concrete_fold() {
assert_eq!(
mk_to_lower(SymbolicValue::ConcreteStr("ABC".into())),
SymbolicValue::ConcreteStr("abc".into())
);
}
#[test]
fn mk_to_upper_concrete_fold() {
assert_eq!(
mk_to_upper(SymbolicValue::ConcreteStr("abc".into())),
SymbolicValue::ConcreteStr("ABC".into())
);
}
#[test]
fn mk_replace_concrete_fold() {
assert_eq!(
mk_replace(
SymbolicValue::ConcreteStr("a<script>b".into()),
"<script>".into(),
"".into(),
),
SymbolicValue::ConcreteStr("ab".into())
);
}
#[test]
fn mk_replace_symbolic() {
let v = mk_replace(
SymbolicValue::Symbol(SsaValue(0)),
"<".into(),
"<".into(),
);
assert!(matches!(v, SymbolicValue::Replace(_, _, _)));
assert_eq!(v.depth(), 1);
}
#[test]
fn mk_substr_concrete_fold() {
assert_eq!(
mk_substr(
SymbolicValue::ConcreteStr("hello world".into()),
SymbolicValue::Concrete(0),
Some(SymbolicValue::Concrete(5)),
),
SymbolicValue::ConcreteStr("hello".into())
);
}
#[test]
fn mk_substr_no_end() {
assert_eq!(
mk_substr(
SymbolicValue::ConcreteStr("hello world".into()),
SymbolicValue::Concrete(6),
None,
),
SymbolicValue::ConcreteStr("world".into())
);
}
#[test]
fn mk_strlen_concrete_fold() {
assert_eq!(
mk_strlen(SymbolicValue::ConcreteStr("hello".into())),
SymbolicValue::Concrete(5)
);
}
#[test]
fn mk_strlen_symbolic() {
let v = mk_strlen(SymbolicValue::Symbol(SsaValue(0)));
assert!(matches!(v, SymbolicValue::StrLen(_)));
assert_eq!(v.depth(), 1);
}
#[test]
fn string_ops_depth_bounding() {
let mut val = SymbolicValue::Symbol(SsaValue(0));
for _ in 0..MAX_EXPR_DEPTH {
val = mk_trim(val);
}
assert_eq!(val.depth(), MAX_EXPR_DEPTH);
val = mk_trim(val);
assert_eq!(val, SymbolicValue::Unknown);
}
#[test]
fn display_string_ops() {
let v = mk_trim(SymbolicValue::Symbol(SsaValue(1)));
assert_eq!(format!("{}", v), "sym(v1).trim()");
let v = mk_to_lower(SymbolicValue::Symbol(SsaValue(2)));
assert_eq!(format!("{}", v), "sym(v2).toLowerCase()");
let v = mk_to_upper(SymbolicValue::Symbol(SsaValue(3)));
assert_eq!(format!("{}", v), "sym(v3).toUpperCase()");
let v = mk_replace(
SymbolicValue::Symbol(SsaValue(4)),
"<".into(),
"<".into(),
);
assert_eq!(format!("{}", v), "sym(v4).replace(\"<\", \"<\")");
let v = mk_strlen(SymbolicValue::Symbol(SsaValue(5)));
assert_eq!(format!("{}", v), "strlen(sym(v5))");
let v = mk_substr(
SymbolicValue::Symbol(SsaValue(6)),
SymbolicValue::Concrete(0),
Some(SymbolicValue::Concrete(5)),
);
assert_eq!(format!("{}", v), "sym(v6).substr(0, 5)");
}
#[test]
fn mk_encode_concrete_folding() {
use super::super::strings::TransformKind;
let v = mk_encode(
TransformKind::HtmlEscape,
SymbolicValue::ConcreteStr("<b>".into()),
);
assert_eq!(v, SymbolicValue::ConcreteStr("<b>".into()));
}
#[test]
fn mk_encode_symbolic_preserves_structure() {
use super::super::strings::TransformKind;
let v = mk_encode(TransformKind::UrlEncode, SymbolicValue::Symbol(SsaValue(7)));
match v {
SymbolicValue::Encode(kind, inner) => {
assert_eq!(kind, TransformKind::UrlEncode);
assert_eq!(*inner, SymbolicValue::Symbol(SsaValue(7)));
}
other => panic!("expected Encode, got {:?}", other),
}
}
#[test]
fn mk_encode_depth_bounding() {
use super::super::strings::TransformKind;
let mut v = SymbolicValue::Symbol(SsaValue(0));
for _ in 0..MAX_EXPR_DEPTH {
v = SymbolicValue::Encode(TransformKind::HtmlEscape, Box::new(v));
}
let result = mk_encode(TransformKind::HtmlEscape, v);
assert_eq!(result, SymbolicValue::Unknown);
}
#[test]
fn mk_decode_concrete_folding() {
use super::super::strings::TransformKind;
let v = mk_decode(
TransformKind::Base64Decode,
SymbolicValue::ConcreteStr("aGVsbG8=".into()),
);
assert_eq!(v, SymbolicValue::ConcreteStr("hello".into()));
}
#[test]
fn mk_decode_url_concrete() {
use super::super::strings::TransformKind;
let v = mk_decode(
TransformKind::UrlDecode,
SymbolicValue::ConcreteStr("hello%20world".into()),
);
assert_eq!(v, SymbolicValue::ConcreteStr("hello world".into()));
}
#[test]
fn encode_display_format() {
use super::super::strings::TransformKind;
let v = mk_encode(
TransformKind::HtmlEscape,
SymbolicValue::Symbol(SsaValue(0)),
);
assert_eq!(format!("{}", v), "htmlEscape(sym(v0))");
let v = mk_decode(
TransformKind::Base64Decode,
SymbolicValue::Symbol(SsaValue(1)),
);
assert_eq!(format!("{}", v), "decode_base64Decode(sym(v1))");
}
#[test]
fn encode_depth() {
use super::super::strings::TransformKind;
let inner = SymbolicValue::Symbol(SsaValue(0));
let v = mk_encode(TransformKind::UrlEncode, inner);
assert_eq!(v.depth(), 1);
}
#[test]
fn binop_mixed_str_int_does_not_coerce() {
let v = mk_binop(
Op::Add,
SymbolicValue::ConcreteStr("price=".into()),
SymbolicValue::Concrete(42),
);
assert!(
matches!(v, SymbolicValue::BinOp(Op::Add, _, _)),
"mixed-type Add must produce a symbolic BinOp, not silently fold"
);
}
#[test]
fn phi_mixed_types_keeps_phi() {
let v = mk_phi(vec![
(BlockId(0), SymbolicValue::Concrete(7)),
(BlockId(1), SymbolicValue::ConcreteStr("x".into())),
]);
assert!(
matches!(v, SymbolicValue::Phi(_)),
"phi over mixed types must NOT fold to a single operand"
);
}
}