mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-07-07 20:45:01 +00:00
218 lines
6.2 KiB
Rust
218 lines
6.2 KiB
Rust
#![no_main]
|
|
use core::fmt;
|
|
use std::{error::Error, num::NonZero, sync::Arc};
|
|
|
|
use arbitrary::Arbitrary;
|
|
use libfuzzer_sys::{fuzz_target, Corpus};
|
|
use turso_core::{Value, IO as _};
|
|
|
|
macro_rules! str_enum {
|
|
($vis:vis enum $name:ident { $($variant:ident => $value:literal),*, }) => {
|
|
#[derive(PartialEq, Debug, Copy, Clone, Arbitrary)]
|
|
$vis enum $name {
|
|
$($variant),*
|
|
}
|
|
|
|
impl $name {
|
|
pub fn to_str(self) -> &'static str {
|
|
match self {
|
|
$($name::$variant => $value),*
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for $name {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.to_str())
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
str_enum! {
|
|
enum Binary {
|
|
// TODO: Not compatible yet
|
|
// Equal => "=",
|
|
// Is => "IS",
|
|
// Concat => "||",
|
|
// NotEqual => "<>",
|
|
// GreaterThan => ">",
|
|
// GreaterThanOrEqual => ">=",
|
|
// LessThan => "<",
|
|
// LessThanOrEqual => "<=",
|
|
RightShift => ">>",
|
|
LeftShift => "<<",
|
|
BitwiseAnd => "&",
|
|
BitwiseOr => "|",
|
|
And => "AND",
|
|
Or => "OR",
|
|
Add => "+",
|
|
Subtract => "-",
|
|
Multiply => "*",
|
|
Divide => "/",
|
|
Mod => "%",
|
|
}
|
|
}
|
|
|
|
str_enum! {
|
|
enum Unary {
|
|
Not => "NOT",
|
|
BitwiseNot => "~",
|
|
Negative => "-",
|
|
Positive => "+",
|
|
}
|
|
}
|
|
|
|
#[derive(Arbitrary, Debug, Clone)]
|
|
enum Value {
|
|
Null,
|
|
Integer(i64),
|
|
Real(f64),
|
|
Text(String),
|
|
Blob(Vec<u8>),
|
|
}
|
|
|
|
impl From<Value> for turso_core::Value {
|
|
fn from(value: Value) -> turso_core::Value {
|
|
match value {
|
|
Value::Null => turso_core::Value::Null,
|
|
Value::Integer(v) => turso_core::Value::Integer(v),
|
|
Value::Real(v) => {
|
|
if v.is_nan() {
|
|
turso_core::Value::Null
|
|
} else {
|
|
turso_core::Value::Float(v)
|
|
}
|
|
}
|
|
Value::Text(v) => turso_core::Value::from_text(&v),
|
|
Value::Blob(v) => turso_core::Value::from_blob(v.to_owned()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl rusqlite::ToSql for Value {
|
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
|
use rusqlite::types::ToSqlOutput;
|
|
|
|
Ok(match self {
|
|
Value::Null => ToSqlOutput::Owned(rusqlite::types::Value::Null),
|
|
Value::Integer(v) => ToSqlOutput::Owned(rusqlite::types::Value::Integer(*v)),
|
|
Value::Real(v) => ToSqlOutput::Owned(rusqlite::types::Value::Real(*v)),
|
|
Value::Text(v) => ToSqlOutput::Owned(rusqlite::types::Value::Text(v.to_owned())),
|
|
Value::Blob(v) => ToSqlOutput::Owned(rusqlite::types::Value::Blob(v.to_owned())),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl rusqlite::types::FromSql for Value {
|
|
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
|
Ok(match value {
|
|
rusqlite::types::ValueRef::Null => Value::Null,
|
|
rusqlite::types::ValueRef::Integer(v) => Value::Integer(v),
|
|
rusqlite::types::ValueRef::Real(v) => Value::Real(v),
|
|
rusqlite::types::ValueRef::Text(v) => {
|
|
Value::Text(String::from_utf8_lossy(v).to_string())
|
|
}
|
|
rusqlite::types::ValueRef::Blob(v) => Value::Blob(v.to_vec()),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Arbitrary)]
|
|
enum Expr {
|
|
Value(Value),
|
|
Binary(Binary, Box<Expr>, Box<Expr>),
|
|
Unary(Unary, Box<Expr>),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Output {
|
|
query: String,
|
|
parameters: Vec<Value>,
|
|
depth: usize,
|
|
}
|
|
|
|
impl Expr {
|
|
pub fn lower(&self) -> Output {
|
|
match self {
|
|
Expr::Value(value) => Output {
|
|
query: "?".to_string(),
|
|
parameters: vec![value.clone()],
|
|
depth: 0,
|
|
},
|
|
Expr::Unary(op, expr) => {
|
|
let expr = expr.lower();
|
|
Output {
|
|
query: format!("{op} ({})", expr.query),
|
|
parameters: expr.parameters,
|
|
depth: expr.depth + 1,
|
|
}
|
|
}
|
|
Expr::Binary(op, lhs, rhs) => {
|
|
let mut lhs = lhs.lower();
|
|
let mut rhs = rhs.lower();
|
|
Output {
|
|
query: format!("({}) {op} ({})", lhs.query, rhs.query),
|
|
parameters: {
|
|
lhs.parameters.append(&mut rhs.parameters);
|
|
lhs.parameters
|
|
},
|
|
depth: lhs.depth.max(rhs.depth) + 1,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn do_fuzz(expr: Expr) -> Result<Corpus, Box<dyn Error>> {
|
|
let expr = expr.lower();
|
|
let sql = format!("SELECT {}", expr.query);
|
|
|
|
// FIX: `turso_core::translate::expr::translate_expr` causes a overflow if this is any higher.
|
|
if expr.depth > 140 {
|
|
return Ok(Corpus::Reject);
|
|
}
|
|
|
|
let expected = {
|
|
let conn = rusqlite::Connection::open_in_memory()?;
|
|
conn.query_row(
|
|
&sql,
|
|
rusqlite::params_from_iter(expr.parameters.iter()),
|
|
|row| row.get::<_, Value>(0),
|
|
)?
|
|
};
|
|
|
|
let found = 'value: {
|
|
let io = Arc::new(turso_core::MemoryIO::new());
|
|
let db = turso_core::Database::open_file(io.clone(), ":memory:", false)?;
|
|
let conn = db.connect()?;
|
|
|
|
let mut stmt = conn.prepare(sql)?;
|
|
for (idx, value) in expr.parameters.iter().enumerate() {
|
|
stmt.bind_at(NonZero::new(idx + 1).unwrap(), value.clone().into())
|
|
}
|
|
loop {
|
|
use turso_core::StepResult;
|
|
match stmt.step()? {
|
|
StepResult::IO => stmt.run_once()?,
|
|
StepResult::Row => {
|
|
let row = stmt.row().unwrap();
|
|
assert_eq!(row.len(), 1, "expr: {:?}", expr);
|
|
break 'value row.get_value(0).clone();
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
};
|
|
|
|
assert_eq!(
|
|
Value::from(expected.clone()),
|
|
found.clone(),
|
|
"with expression {:?}",
|
|
expr,
|
|
);
|
|
|
|
Ok(Corpus::Keep)
|
|
}
|
|
|
|
fuzz_target!(|expr: Expr| -> Corpus { do_fuzz(expr).unwrap_or(Corpus::Keep) });
|