diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 628444a86..3c387576b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -81,6 +81,7 @@ jobs: uses: ./.github/actions/setup-builder with: targets: 'thumbv6m-none-eabi' + - run: cargo test --release --no-default-features --test no_std_recursion - run: cargo check --no-default-features --target thumbv6m-none-eabi - run: cargo check --no-default-features --features visitor --target thumbv6m-none-eabi diff --git a/src/lib.rs b/src/lib.rs index e68d7f93e..ddf2fb74b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,7 +159,6 @@ // Allow proc-macros to find this crate extern crate self as sqlparser; -#[cfg(not(feature = "std"))] extern crate alloc; #[macro_use] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 953453a22..a4b4e1913 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -72,46 +72,25 @@ macro_rules! parser_err { mod alter; mod merge; -#[cfg(feature = "std")] -/// Implementation [`RecursionCounter`] if std is available mod recursion { - use std::cell::Cell; - use std::rc::Rc; + use alloc::rc::Rc; + use core::cell::Cell; use super::ParserError; - /// Tracks remaining recursion depth. This value is decremented on - /// each call to [`RecursionCounter::try_decrease()`], when it reaches 0 an error will - /// be returned. - /// - /// Note: Uses an [`std::rc::Rc`] and [`std::cell::Cell`] in order to satisfy the Rust - /// borrow checker so the automatic [`DepthGuard`] decrement a - /// reference to the counter. - /// - /// Note: when "recursive-protection" feature is enabled, this crate uses additional stack overflow protection - /// for some of its recursive methods. See [`recursive::recursive`] for more information. pub(crate) struct RecursionCounter { remaining_depth: Rc>, } impl RecursionCounter { - /// Creates a [`RecursionCounter`] with the specified maximum - /// depth pub fn new(remaining_depth: usize) -> Self { Self { remaining_depth: Rc::new(remaining_depth.into()), } } - /// Decreases the remaining depth by 1. - /// - /// Returns [`Err`] if the remaining depth falls to 0. - /// - /// Returns a [`DepthGuard`] which will adds 1 to the - /// remaining depth upon drop; pub fn try_decrease(&self) -> Result { let old_value = self.remaining_depth.get(); - // ran out of space if old_value == 0 { Err(ParserError::RecursionLimitExceeded) } else { @@ -121,7 +100,6 @@ mod recursion { } } - /// Guard that increases the remaining depth by 1 on drop pub struct DepthGuard { remaining_depth: Rc>, } @@ -131,33 +109,13 @@ mod recursion { Self { remaining_depth } } } + impl Drop for DepthGuard { fn drop(&mut self) { let old_value = self.remaining_depth.get(); - self.remaining_depth.set(old_value + 1); - } - } -} - -#[cfg(not(feature = "std"))] -mod recursion { - /// Implementation [`RecursionCounter`] if std is NOT available (and does not - /// guard against stack overflow). - /// - /// Has the same API as the std [`RecursionCounter`] implementation - /// but does not actually limit stack depth. - pub(crate) struct RecursionCounter {} - - impl RecursionCounter { - pub fn new(_remaining_depth: usize) -> Self { - Self {} - } - pub fn try_decrease(&self) -> Result { - Ok(DepthGuard {}) + self.remaining_depth.set(old_value.saturating_add(1)); } } - - pub struct DepthGuard {} } #[derive(PartialEq, Eq)] diff --git a/tests/no_std_recursion.rs b/tests/no_std_recursion.rs new file mode 100644 index 000000000..b4a733b69 --- /dev/null +++ b/tests/no_std_recursion.rs @@ -0,0 +1,112 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(not(feature = "std"))] + +use sqlparser::dialect::GenericDialect; +use sqlparser::parser::{Parser, ParserError}; + +#[test] +fn with_recursion_limit_applies_without_default_features() { + let dialect = GenericDialect {}; + let result = Parser::new(&dialect) + .with_recursion_limit(1) + .try_with_sql("SELECT * FROM foo WHERE (a OR (b OR (c OR d)))") + .unwrap() + .parse_statements(); + + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn default_recursion_limit_applies_without_default_features() { + let dialect = GenericDialect {}; + let sql = format!( + "SELECT * FROM t WHERE {}a = 1{}", + "(".repeat(200), + ")".repeat(200) + ); + + let result = Parser::parse_sql(&dialect, &sql); + + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn deeply_nested_not_returns_error_without_default_features() { + let dialect = GenericDialect {}; + let sql = format!("SELECT * FROM t WHERE {}a", "NOT ".repeat(1024)); + + let result = Parser::parse_sql(&dialect, &sql); + + assert!(result.is_err()); +} + +#[test] +fn valid_nested_queries_parse_without_default_features() { + let dialect = GenericDialect {}; + + let result = Parser::parse_sql(&dialect, "SELECT 1 + (2 + 3)"); + + assert!(result.is_ok()); +} + +#[test] +fn recursion_budget_restores_between_statements_without_default_features() { + let dialect = GenericDialect {}; + let statements = Parser::new(&dialect) + .with_recursion_limit(4) + .try_with_sql("SELECT 1; SELECT 2; SELECT 3") + .unwrap() + .parse_statements() + .unwrap(); + + assert_eq!(statements.len(), 3); +} + +#[test] +fn deeply_nested_intervals_hit_recursion_limit_without_default_features() { + let dialect = GenericDialect {}; + let sql = format!("SELECT {}1", "INTERVAL ".repeat(1000)); + + let result = Parser::parse_sql(&dialect, &sql); + + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn nested_queries_hit_recursion_limit_without_default_features() { + let dialect = GenericDialect {}; + let sql = format!( + "{}SELECT 1{}", + "SELECT 1 WHERE 1 IN (".repeat(100), + ")".repeat(100) + ); + + let result = Parser::parse_sql(&dialect, &sql); + + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn nested_table_factors_hit_recursion_limit_without_default_features() { + let dialect = GenericDialect {}; + let sql = format!("SELECT * FROM {}t{}", "(".repeat(100), ")".repeat(100)); + + let result = Parser::parse_sql(&dialect, &sql); + + assert_eq!(result, Err(ParserError::RecursionLimitExceeded)); +}