Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <william@saar.se>"]
license = "BSD-3-Clause"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
5 changes: 2 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion libinjectionrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions libinjectionrs/src/sqli/blacklist.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions libinjectionrs/src/sqli/mod.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
10 changes: 5 additions & 5 deletions libinjectionrs/src/sqli/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<binary>")
core::str::from_utf8(&self.val[..end]).unwrap_or("<binary>")
}

pub fn clear(&mut self) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Loading