From 0013c93fa53dcf852e76be3158482f02084b4e9d Mon Sep 17 00:00:00 2001 From: "Levy A." Date: Wed, 19 Mar 2025 17:10:23 -0300 Subject: [PATCH] refactor --- fuzz/Cargo.lock | 15 +-- fuzz/Cargo.toml | 4 +- fuzz/fuzz_targets/expression.rs | 185 +++++++++++++++++++++++--------- 3 files changed, 147 insertions(+), 57 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 5712e1160..643d8496b 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -614,6 +614,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -642,7 +643,7 @@ dependencies = [ [[package]] name = "limbo_core" -version = "0.0.16" +version = "0.0.17" dependencies = [ "built", "cfg_block", @@ -679,14 +680,16 @@ dependencies = [ [[package]] name = "limbo_ext" -version = "0.0.16" +version = "0.0.17" dependencies = [ + "chrono", + "getrandom 0.3.1", "limbo_macros", ] [[package]] name = "limbo_macros" -version = "0.0.16" +version = "0.0.17" dependencies = [ "proc-macro2", "quote", @@ -695,7 +698,7 @@ dependencies = [ [[package]] name = "limbo_sqlite3_parser" -version = "0.0.16" +version = "0.0.17" dependencies = [ "bitflags", "cc", @@ -714,7 +717,7 @@ dependencies = [ [[package]] name = "limbo_time" -version = "0.0.16" +version = "0.0.17" dependencies = [ "chrono", "limbo_ext", @@ -726,7 +729,7 @@ dependencies = [ [[package]] name = "limbo_uuid" -version = "0.0.16" +version = "0.0.17" dependencies = [ "limbo_ext", "mimalloc", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e2e362d1a..ac411077e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "limbo-fuzz" version = "0.0.0" -authors = ["Automatically generated"] +authors = ["the Limbo authors"] publish = false edition = "2021" @@ -12,7 +12,7 @@ cargo-fuzz = true libfuzzer-sys = "0.4" arbitrary = { version = "1.4.1", features = ["derive"] } limbo_core = { path = "../core" } -rusqlite = "0.34.0" +rusqlite = { version = "0.34.0", features = ["bundled"] } # Prevent this from interfering with workspaces [workspace] diff --git a/fuzz/fuzz_targets/expression.rs b/fuzz/fuzz_targets/expression.rs index b8b116226..44338c634 100644 --- a/fuzz/fuzz_targets/expression.rs +++ b/fuzz/fuzz_targets/expression.rs @@ -1,31 +1,67 @@ #![no_main] use core::fmt; -use std::{num::NonZero, sync::Arc}; +use std::{error::Error, num::NonZero, sync::Arc}; use arbitrary::Arbitrary; -use libfuzzer_sys::fuzz_target; +use libfuzzer_sys::{fuzz_target, Corpus}; use limbo_core::{OwnedValue, IO as _}; -#[derive(Debug, Arbitrary)] -enum Binary { - Equal, - NotEqual, +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()) + } + } + }; } -impl fmt::Display for Binary { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Binary::Equal => "=", - Binary::NotEqual => "<>", - } - ) +str_enum! { + enum Binary { + Equal => "=", + Is => "IS", + NotEqual => "<>", + GreaterThan => ">", + GreaterThanOrEqual => ">=", + LessThan => "<", + LessThanOrEqual => "<=", + RightShift => ">>", + LeftShift => "<<", + BitwiseAnd => "&", + BitwiseOr => "|", + And => "AND", + Or => "OR", + Add => "+", + Subtract => "-", + Multiply => "*", + Divide => "/", + Mod => "%", + Concat => "||", } } -#[derive(Debug, Arbitrary, Clone)] +str_enum! { + enum Unary { + Not => "~", + Negative => "-", + Positive => "+", + } +} + +#[derive(Arbitrary, Debug, Clone)] enum Value { Null, Integer(i64), @@ -39,7 +75,13 @@ impl From for limbo_core::OwnedValue { match value { Value::Null => limbo_core::OwnedValue::Null, Value::Integer(v) => limbo_core::OwnedValue::Integer(v), - Value::Real(v) => limbo_core::OwnedValue::Float(v), + Value::Real(v) => { + if v.is_nan() { + limbo_core::OwnedValue::Null + } else { + limbo_core::OwnedValue::Float(v) + } + } Value::Text(v) => limbo_core::OwnedValue::from_text(&v), Value::Blob(v) => limbo_core::OwnedValue::from_blob(v.to_owned()), } @@ -78,48 +120,82 @@ impl rusqlite::types::FromSql for Value { enum Expr { Value(Value), Binary(Binary, Box, Box), + Unary(Unary, Box), } -fn to_sql(expr: &Expr) -> (String, Vec) { - match expr { - Expr::Value(value) => ("?".to_string(), vec![value.clone()]), - Expr::Binary(op, lhs, rhs) => { - let mut lhs = to_sql(lhs); - let mut rhs = to_sql(rhs); - lhs.1.append(&mut rhs.1); - (format!("({}) {op} ({})", lhs.0, rhs.0), lhs.1) +#[derive(Debug)] +struct Output { + query: String, + parameters: Vec, + 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) { - let (sql, params) = to_sql(&expr); - let sql = format!("select {}", &sql); +fn do_fuzz(expr: Expr) -> Result> { + let expr = expr.lower(); + let sql = format!("SELECT {}", expr.query); - let sqlite = rusqlite::Connection::open_in_memory().unwrap(); - let io = Arc::new(limbo_core::MemoryIO::new()); - let memory = limbo_core::Database::open_file(io.clone(), ":memory:", true).unwrap(); - let limbo = memory.connect().unwrap(); - - let expected = sqlite - .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| { - row.get::<_, Value>(0) - }) - .unwrap(); - - let mut stmt = limbo.prepare(sql).unwrap(); - for (idx, value) in params.into_iter().enumerate() { - stmt.bind_at(NonZero::new(idx + 1).unwrap(), value.into()) + // FIX: `limbo_core::translate::expr::translate_expr` causes a overflow if this is any higher. + if expr.depth > 153 { + return Ok(Corpus::Reject); } - let value = 'value: { + 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(limbo_core::MemoryIO::new()); + let db = limbo_core::Database::open_file(io.clone(), ":memory:", true)?; + 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 limbo_core::StepResult; - match stmt.step().unwrap() { - StepResult::IO => io.run_once().unwrap(), + match stmt.step()? { + StepResult::IO => io.run_once()?, StepResult::Row => { let row = stmt.row().unwrap(); - assert_eq!(row.count(), 1); + assert_eq!(row.count(), 1, "expr: {:?}", expr); break 'value row.get_value(0).clone(); } _ => unreachable!(), @@ -127,7 +203,18 @@ fn do_fuzz(expr: Expr) { } }; - assert_eq!(OwnedValue::from(expected), value); + assert_eq!( + OwnedValue::from(expected.clone()), + found.clone(), + "with expression {:?} {}", + expr, + match (expected, found) { + (Value::Real(a), OwnedValue::Float(b)) => format!("float diff: {:?}", (a - b).abs()), + _ => "".to_string(), + } + ); + + Ok(Corpus::Keep) } -fuzz_target!(|expr: Expr| do_fuzz(expr)); +fuzz_target!(|expr: Expr| -> Corpus { do_fuzz(expr).unwrap_or(Corpus::Keep) });