diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12647ba..771578e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,24 @@ jobs: - name: Unit tests run: cargo test --workspace + no-std: + name: Build without std + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + with: + targets: thumbv7em-none-eabihf + - uses: Swatinem/rust-cache@v2 + # A bare-metal target has no std, so any use of it, in the crate or a + # dependency, fails the build. + - name: Without std + run: cargo build -p barbacane-libinjection --no-default-features --target thumbv7em-none-eabihf + - name: Without std, with smallvec + run: cargo build -p barbacane-libinjection --no-default-features --features smallvec --target thumbv7em-none-eabihf + coverage: name: Library coverage runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d29ab3..efcd9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.1.3] - 2026-11-04 + +### Fixed +- The crate builds without the `std` feature. The SQLi tokenizer called + `std::str::from_utf8`, and `Vec`, `String` and `format!` were used without + importing them from `alloc`, so a `no_std` build failed + ([saarw/libinjectionrs#2](https://github.com/saarw/libinjectionrs/issues/2)). + Detection is unchanged. + +### Added +- A CI job builds the crate for a bare-metal target (`thumbv7em-none-eabihf`) + without `std`, with and without `smallvec`. + ## [0.1.2] - 2026-11-03 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 7247408..cf77fd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["libinjectionrs", "comparison-bin", "benches"] [workspace.package] -version = "0.1.2" +version = "0.1.3" edition = "2021" authors = ["William Saar "] license = "BSD-3-Clause" diff --git a/README.md b/README.md index 16333ce..44d922a 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,13 @@ A memory-safe Rust port of [libinjection](https://github.com/libinjection/libinj - XSS detection with context awareness - No `unsafe`, and lints that deny the common panic sources (see [Linting](#linting)) - Minimal heap allocations using `SmallVec` +- `no_std` support: with `default-features = false` the crate needs only `core` and `alloc` ## Agreement with the C library Correctness here means one thing: the same answer as the C library. That is measured, not asserted, by `comparison-bin/tests/differential.rs`, which links the C sources through the FFI harness and compares this port against them over libinjection's own corpus of ~163,000 inputs, on both verdicts and fingerprints. -**No known divergence from the C library remains.** SQLi fingerprints and XSS verdicts match exactly across the whole corpus (0 of 162,963 each), and the NUL-in-`$`-token edge, which the text corpus cannot reach, is matched too and held by a dedicated test. Any divergence outside this state fails the build. +**No known divergence from the C library remains.** SQLi fingerprints and XSS verdicts match exactly across the whole corpus (0 of 163,275 each), and the NUL-in-`$`-token edge, which the text corpus cannot reach, is matched too and held by a dedicated test. Any divergence outside this state fails the build. The fixes that got here, each following the C control flow rather than a particular input: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index decc7c9..9201547 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -80,9 +80,8 @@ impl XssDetector { - No panics in normal operation ### 4. Feature Flags -- `std` (default): Standard library support -- `smallvec`: Optimize small allocations -- `no_std`: Core-only operation with optional `alloc` +- `std` (default): implements `std::error::Error` for `Error`. Without it the crate is `no_std` and needs `alloc`. +- `smallvec` (default): stores tokens in a `SmallVec` instead of a `Vec` ### 5. Token Representation diff --git a/libinjectionrs/src/lib.rs b/libinjectionrs/src/lib.rs index 0dd4e63..d2d52a8 100644 --- a/libinjectionrs/src/lib.rs +++ b/libinjectionrs/src/lib.rs @@ -35,7 +35,6 @@ //! Most applications should **not** use these lower-level APIs unless they have //! specific requirements that the high-level functions cannot meet. -#[cfg(not(feature = "std"))] extern crate alloc; use core::fmt; diff --git a/libinjectionrs/src/sqli/blacklist.rs b/libinjectionrs/src/sqli/blacklist.rs index ff42c12..1254e8c 100644 --- a/libinjectionrs/src/sqli/blacklist.rs +++ b/libinjectionrs/src/sqli/blacklist.rs @@ -1,5 +1,7 @@ // SQL injection blacklist checker - matches C implementation exactly +use alloc::string::String; + use super::sqli_data; /// Check if a fingerprint is blacklisted diff --git a/libinjectionrs/src/sqli/mod.rs b/libinjectionrs/src/sqli/mod.rs index 9850062..708e1f4 100644 --- a/libinjectionrs/src/sqli/mod.rs +++ b/libinjectionrs/src/sqli/mod.rs @@ -1,3 +1,7 @@ +use alloc::format; +use alloc::string::{String, ToString}; +#[cfg(not(feature = "smallvec"))] +use alloc::vec::Vec; use core::ops::{Deref, BitOr, BitOrAssign}; #[cfg(feature = "smallvec")] diff --git a/libinjectionrs/src/sqli/tokenizer.rs b/libinjectionrs/src/sqli/tokenizer.rs index 39ddfdb..a9a943a 100644 --- a/libinjectionrs/src/sqli/tokenizer.rs +++ b/libinjectionrs/src/sqli/tokenizer.rs @@ -136,7 +136,7 @@ impl Token { let end = self.len.min(32); // For display purposes, show lossy conversion to handle 0xFF bytes // This won't affect tokenization logic, only debugging output - std::str::from_utf8(&self.val[..end]).unwrap_or("") + core::str::from_utf8(&self.val[..end]).unwrap_or("") } pub fn clear(&mut self) { @@ -482,7 +482,7 @@ impl<'a> SqliTokenizer<'a> { // Try 2-character operator lookup using the comprehensive table // Only attempt lookup if bytes form valid UTF-8, since operators are ASCII-only let two_char = &self.input[pos..pos + 2]; - if let Ok(two_char_str) = std::str::from_utf8(two_char) { + if let Ok(two_char_str) = core::str::from_utf8(two_char) { let token_type = self.lookup_word(two_char_str); if token_type != TokenType::None && token_type != TokenType::Bareword { // Found a 2-character operator in the lookup table @@ -920,7 +920,7 @@ impl<'a> SqliTokenizer<'a> { // form a valid keyword. Since libinjection keywords are ASCII-only, // if the slice contains any non-UTF8 bytes (like 0xFF), it can't be // a keyword anyway, so we can safely skip the lookup. - if let Ok(partial_word) = std::str::from_utf8(&word_slice[..i]) { + if let Ok(partial_word) = core::str::from_utf8(&word_slice[..i]) { let token_type = self.lookup_word(partial_word); if token_type != TokenType::None && token_type != TokenType::Bareword { self.current.clear(); @@ -935,7 +935,7 @@ impl<'a> SqliTokenizer<'a> { // Do full word lookup - only for valid UTF-8 sequences since keywords are ASCII-only if word_len < LIBINJECTION_SQLI_TOKEN_SIZE { - if let Ok(word_str) = std::str::from_utf8(word_slice) { + if let Ok(word_str) = core::str::from_utf8(word_slice) { let token_type = self.lookup_word(word_str); if token_type != TokenType::None { self.current.token_type = token_type; @@ -952,7 +952,7 @@ impl<'a> SqliTokenizer<'a> { let pos = self.parse_string_core(self.pos, CHAR_TICK, 1); // Check if backtick content is a keyword/function - only for valid UTF-8 - if let Ok(word_str) = std::str::from_utf8(&self.current.val[..self.current.len]) { + if let Ok(word_str) = core::str::from_utf8(&self.current.val[..self.current.len]) { let token_type = self.lookup_word(word_str); if token_type == TokenType::Function { self.current.token_type = TokenType::Function;