mod support {
pub mod parser_hardening;
}
use proptest::prelude::*;
use reddb_server::storage::query::parser::{self, ParseError, ParserLimits};
use support::parser_hardening::{
self as harness, assert_no_panic_on, corpus::geo_adversarial_inputs, geo_grammar,
HardenedParser,
};
pub struct GeoParser;
impl HardenedParser for GeoParser {
type Error = ParseError;
fn parse(input: &str) -> Result<(), Self::Error> {
parser::parse(input).map(|_| ())
}
fn parse_with_limits(input: &str, limits: ParserLimits) -> Result<(), Self::Error> {
let mut p = parser::Parser::with_limits(input, limits)?;
p.parse().map(|_| ())
}
}
#[test]
fn geo_parser_does_not_panic_on_adversarial_corpus() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
for (name, input) in geo_adversarial_inputs() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_no_panic_on::<GeoParser>(&input);
}));
if result.is_err() {
panic!("geo adversarial corpus entry {} panicked", name);
}
}
})
.expect("spawn corpus thread");
handle.join().expect("corpus thread panic");
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn proptest_radius_roundtrips(s in geo_grammar::radius_stmt()) {
harness::roundtrip_property::<GeoParser>(&s);
prop_assert!(
GeoParser::parse(&s).is_ok(),
"spatial radius did not parse: {}", s
);
}
#[test]
fn proptest_bbox_roundtrips(s in geo_grammar::bbox_stmt()) {
harness::roundtrip_property::<GeoParser>(&s);
prop_assert!(
GeoParser::parse(&s).is_ok(),
"spatial bbox did not parse: {}", s
);
}
#[test]
fn proptest_nearest_roundtrips(s in geo_grammar::nearest_stmt()) {
harness::roundtrip_property::<GeoParser>(&s);
prop_assert!(
GeoParser::parse(&s).is_ok(),
"spatial nearest did not parse: {}", s
);
}
#[test]
fn proptest_rtree_index_roundtrips(s in geo_grammar::rtree_index_stmt()) {
harness::roundtrip_property::<GeoParser>(&s);
prop_assert!(
GeoParser::parse(&s).is_ok(),
"rtree index did not parse: {}", s
);
}
#[test]
fn proptest_distance_fn_roundtrips(s in geo_grammar::distance_fn_stmt()) {
harness::roundtrip_property::<GeoParser>(&s);
prop_assert!(
GeoParser::parse(&s).is_ok(),
"distance fn did not parse: {}", s
);
}
#[test]
fn proptest_geo_arbitrary_suffix_no_panic(
prefix in prop_oneof![
Just("SEARCH SPATIAL RADIUS ".to_string()),
Just("SEARCH SPATIAL BBOX ".to_string()),
Just("SEARCH SPATIAL NEAREST ".to_string()),
],
suffix in ".{0,512}",
) {
let s = format!("{}{}", prefix, suffix);
harness::roundtrip_property::<GeoParser>(&s);
}
#[test]
fn proptest_geo_input_size_limit_enforced(len in 200usize..2000) {
let limits = ParserLimits {
max_input_bytes: 64,
..ParserLimits::default()
};
let suffix = "x".repeat(len);
let input = format!(
"SEARCH SPATIAL RADIUS 0.0 0.0 10.0 COLLECTION {} COLUMN col",
suffix
);
let r = GeoParser::parse_with_limits(&input, limits);
prop_assert!(r.is_err(), "oversized geo input must error");
}
}
use reddb_server::storage::query::ast::{IndexMethod, QueryExpr, SearchCommand};
fn parse_query(input: &str) -> QueryExpr {
parser::parse(input)
.unwrap_or_else(|e| panic!("expected ok for {input:?}, got error: {e}"))
.query
}
#[test]
fn happy_radius_paris_10km_parses() {
let q = parse_query(
"SEARCH SPATIAL RADIUS 48.8566 2.3522 10.0 COLLECTION sites COLUMN location LIMIT 50",
);
match q {
QueryExpr::SearchCommand(SearchCommand::SpatialRadius {
center_lat,
center_lon,
radius_km,
collection,
column,
limit,
..
}) => {
assert!((center_lat - 48.8566).abs() < 1e-9);
assert!((center_lon - 2.3522).abs() < 1e-9);
assert!((radius_km - 10.0).abs() < 1e-9);
assert_eq!(collection, "sites");
assert_eq!(column, "location");
assert_eq!(limit, 50);
}
other => panic!("expected SpatialRadius, got {other:?}"),
}
}
#[test]
fn happy_radius_default_limit_is_one_hundred() {
let q = parse_query("SEARCH SPATIAL RADIUS 0.0 0.0 1.5 COLLECTION sites COLUMN location");
match q {
QueryExpr::SearchCommand(SearchCommand::SpatialRadius { limit, .. }) => {
assert_eq!(limit, 100, "default limit pinned at 100");
}
other => panic!("expected SpatialRadius, got {other:?}"),
}
}
#[test]
fn happy_bbox_unit_square_parses() {
let q = parse_query(
"SEARCH SPATIAL BBOX 0.0 0.0 1.0 1.0 COLLECTION sites COLUMN location LIMIT 25",
);
match q {
QueryExpr::SearchCommand(SearchCommand::SpatialBbox {
min_lat,
min_lon,
max_lat,
max_lon,
collection,
column,
limit,
..
}) => {
assert_eq!(min_lat, 0.0);
assert_eq!(min_lon, 0.0);
assert_eq!(max_lat, 1.0);
assert_eq!(max_lon, 1.0);
assert_eq!(collection, "sites");
assert_eq!(column, "location");
assert_eq!(limit, 25);
}
other => panic!("expected SpatialBbox, got {other:?}"),
}
}
#[test]
fn happy_nearest_k_5_parses() {
let q =
parse_query("SEARCH SPATIAL NEAREST 40.7128 74.0060 K 5 COLLECTION sites COLUMN location");
match q {
QueryExpr::SearchCommand(SearchCommand::SpatialNearest {
lat,
lon,
k,
collection,
column,
..
}) => {
assert!((lat - 40.7128).abs() < 1e-9);
assert!((lon - 74.0060).abs() < 1e-9);
assert_eq!(k, 5);
assert_eq!(collection, "sites");
assert_eq!(column, "location");
}
other => panic!("expected SpatialNearest, got {other:?}"),
}
}
#[test]
fn happy_rtree_index_parses_with_method() {
let q = parse_query("CREATE INDEX gix_loc ON sites (location) USING RTREE");
match q {
QueryExpr::CreateIndex(ci) => {
assert_eq!(ci.name, "gix_loc");
assert_eq!(ci.table, "sites");
assert_eq!(ci.columns, vec!["location".to_string()]);
assert_eq!(ci.method, IndexMethod::RTree);
assert!(!ci.unique);
}
other => panic!("expected CreateIndex, got {other:?}"),
}
}
#[test]
fn happy_geo_distance_in_projection_parses() {
parse_query("SELECT GEO_DISTANCE(0.0, 0.0, 1.0, 1.0) FROM t");
}
#[test]
fn happy_haversine_in_projection_parses() {
parse_query("SELECT HAVERSINE(48.8566, 2.3522, 51.5074, 0.1278) FROM t");
}
#[test]
fn happy_vincenty_in_projection_parses() {
parse_query("SELECT VINCENTY(48.8566, 2.3522, 51.5074, 0.1278) FROM t");
}
#[test]
fn happy_radius_at_equator_parses() {
parse_query("SEARCH SPATIAL RADIUS 0.0 0.0 6371.0 COLLECTION sites COLUMN location");
}
#[test]
fn happy_nearest_at_origin_parses() {
parse_query("SEARCH SPATIAL NEAREST 0.0 0.0 K 1 COLLECTION sites COLUMN location");
}
#[test]
fn negative_latitude_parses() {
let r = parser::parse(
"SEARCH SPATIAL NEAREST -33.8688 151.2093 K 5 COLLECTION sites COLUMN location",
);
assert!(
r.is_ok(),
"negative lat should parse with unary-minus wired into the SPATIAL float position"
);
}
#[test]
fn lat_out_of_range_rejected() {
let r = parser::parse("SEARCH SPATIAL RADIUS 91.0 0.0 10.0 COLLECTION sites COLUMN location");
assert!(r.is_err(), "lat=91 should be rejected as out-of-range");
}
#[test]
fn nearest_k_zero_rejected() {
let r = parser::parse("SEARCH SPATIAL NEAREST 0.0 0.0 K 0 COLLECTION sites COLUMN location");
assert!(r.is_err(), "K=0 should be rejected as degenerate");
}
#[test]
fn radius_zero_rejected() {
let r = parser::parse("SEARCH SPATIAL RADIUS 0.0 0.0 0.0 COLLECTION sites COLUMN location");
assert!(r.is_err(), "radius=0 should be rejected as degenerate");
}