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 6af0fb776..6cc0c4aef 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -72,11 +72,13 @@ macro_rules! parser_err { mod alter; mod merge; -#[cfg(feature = "std")] -/// Implementation [`RecursionCounter`] if std is available +/// Implementation of [`RecursionCounter`]. +/// +/// Explicitly requires only `alloc` and `core`, so recursion is limited even +/// when the "std" feature is disabled. mod recursion { - use std::cell::Cell; - use std::rc::Rc; + use alloc::rc::Rc; + use core::cell::Cell; use super::ParserError; @@ -84,7 +86,7 @@ mod recursion { /// 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 + /// Note: Uses an [`alloc::rc::Rc`] and [`core::cell::Cell`] in order to satisfy the Rust /// borrow checker so the automatic [`DepthGuard`] decrement a /// reference to the counter. /// @@ -131,33 +133,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)] @@ -442,8 +424,12 @@ impl<'a> Parser<'a> { /// # } /// ``` /// - /// 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. + /// Note: Versions prior to `0.63.0` did not enforce any limit in builds + /// without the "std" feature. + /// + /// 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 fn with_recursion_limit(mut self, recursion_limit: usize) -> Self { self.recursion_counter = RecursionCounter::new(recursion_limit); self diff --git a/tests/no_std_recursion.rs b/tests/no_std_recursion.rs new file mode 100644 index 000000000..86cf5c0d6 --- /dev/null +++ b/tests/no_std_recursion.rs @@ -0,0 +1,115 @@ +// 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); + + // Note: Error is generic "expected end of statement" error rather than + // `RecursionLimitExceeded`. Either way, the important property is that deep + // nesting errors instead of overflowing the stack. + 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)); +}