cargo / anstream / audit
cargo : anstream @ 1.0.0
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

environment-safefilesystem-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-safeparser-impl-testedunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

anstream 1.0.0 wraps stdout/stderr to transparently strip or translate ANSI escape codes based on terminal capabilities and environment variables. One unsafe block with a documented invariant and proptest coverage. No findings.

Report

Subject

anstream 1.0.0 is an I/O stream adapter library that transparently handles ANSI escape codes. It wraps stdout, stderr, or any std::io::Write implementation in one of three modes: pass-through (ANSI codes forwarded as-is), strip (escape codes removed, only printable text written), or wincon (Windows console API calls substitute for ANSI codes). Mode selection is automatic based on terminal capabilities and environment variables (NO_COLOR, CLICOLOR, CLICOLOR_FORCE, CI), or can be overridden by the caller via ColorChoice. The crate also exports print!, println!, eprint!, eprintln!, and panic! macro replacements that route through AutoStream. It is part of the anstyle ecosystem and carries an MIT OR Apache-2.0 dual licence.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. All Rust source files (~2562 LOC across 11 files in src/ and 2 examples) were read in full. The VCS checkout contains tests/ and benches/ directories excluded from the published crate; the sources themselves are not in scope for this audit but their absence from the crate is expected. Grep surveys were run for unsafe, FFI (extern "C"), network, filesystem, process, environment, cryptographic, and concurrency patterns using standard POSIX tools. The dependency list was cross-referenced against dependencies.json. openvet 0.6.0 was used for all claim, annotation, and dependency operations.

Results

The diff between published contents and VCS shows only cargo normalisation in Cargo.toml and the expected omission of tests/, benches/, and CHANGELOG.md; all source files are byte-for-byte identical. No binary artefacts are present, justifying has-binaries. No build.rs is present and the crate is not a proc macro, so has-build-exec and has-install-exec are false.

The crate contains a single unsafe site: the private from_utf8_unchecked function in src/adapter/strip.rs (lines 147-156), called from next_str (line 136). The function wraps std::str::from_utf8_unchecked and carries an explicit &'static str safety justification parameter. Under debug_assertions, it falls back to std::str::from_utf8(...).expect(...), making any invariant violation visible in tests. The invariant holds structurally: next_str only receives byte slices derived from &str input (whose UTF-8 validity is a type-system guarantee) and splits exclusively at positions confirmed to be printable or UTF-8 continuation bytes by the state machine. Justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal.

The ANSI escape sequence parser built on anstyle-parse's state machine and implemented in src/adapter/strip.rs and src/adapter/wincon.rs constitutes a parser, justifying impl-parser. Both the str-oriented and byte-oriented stripping paths are covered by proptest property tests that compare against a reference full-parser implementation (parser_strip) for arbitrary Unicode strings, justifying parser-impl-safe and parser-impl-tested. The property tests carry #[cfg_attr(miri, ignore)] annotations, consistent with the codebase having been run under Miri. unsafe-tested is further supported by this testing posture. parser-impl-correct was not evaluated: there is no published specification for ANSI escape code stripping to verify conformance against; the proptest suite validates equivalence with the reference anstyle-parse full-parser model rather than an external spec.

Environment variable access is confined to anstyle-query (optional, auto feature); the choice function in src/auto.rs reads NO_COLOR, CLICOLOR_FORCE, CLICOLOR, and CI via anstyle_query functions. No variable is written; the environment is not enumerated. Justifies uses-environment and environment-safe. std::fs::File is an accepted RawStream implementor (declared in src/stream.rs), meaning callers may wrap a file handle; the crate itself opens no files and performs no path operations, justifying uses-filesystem and filesystem-safe. No network operations, child processes, JIT, interpreter, or cryptographic operations are present anywhere in the source, justifying uses-network, uses-exec, uses-jit, uses-interpreter, uses-crypto, and impl-crypto as false. The crate does not implement concurrency primitives (impl-concurrency), algorithms (impl-algorithm), data structures (impl-datastructure), protocols (impl-protocol), an interpreter (impl-interpreter), or a JIT (impl-jit). No threads are spawned and no concurrency primitives are used by the crate itself, justifying uses-concurrency.

Unit tests are inlined in src/stream.rs, src/adapter/strip.rs, src/adapter/wincon.rs, and src/strip.rs, justifying has-unit-tests. Property tests using the proptest crate are present in src/adapter/strip.rs and src/adapter/wincon.rs, justifying has-property-tests. No dedicated integration test files or fuzz corpora are included in the published crate, so has-integration-tests and has-fuzz-tests are false. The crate contains no malicious code, obfuscated payloads, or unexpected network or filesystem behaviour, justifying is-benign.

Conclusion

The crate's source is clean and matches VCS byte-for-byte. The single unsafe block is small, documents its invariant with a descriptive string argument, and is backed by a debug-assertions fallback and a proptest suite. No I/O beyond writing to caller-supplied streams occurs at runtime; environment variable reads are limited to four well-known colour-control variables. No findings were recorded.

Findings

No findings.

Annotations(3)

src/adapter/strip.rs

src/adapter/strip.rs, line 136-156

        let printable = unsafe {
            from_utf8_unchecked(
                printable,
                "`bytes` was validated as UTF-8, the parser preserves UTF-8 continuations",
            )
        };
        Some(printable)
    }
}

#[inline]
unsafe fn from_utf8_unchecked<'b>(bytes: &'b [u8], safety_justification: &'static str) -> &'b str {
    unsafe {
        if cfg!(debug_assertions) {
            // Catch problems more quickly when testing
            std::str::from_utf8(bytes).expect(safety_justification)
        } else {
            std::str::from_utf8_unchecked(bytes)
        }
    }
}

The single unsafe site in the crate is from_utf8_unchecked in src/adapter/strip.rs (lines 136-156). The function wraps std::str::from_utf8_unchecked and is only called after the parser has confirmed that the byte slice contains valid UTF-8 printable characters plus UTF-8 continuation bytes. The safety justification string is passed as a &'static str parameter and is checked via str::from_utf8 under debug assertions, making any violation visible in test runs. The invariant holds: input to this crate is always &str, which guarantees UTF-8 at the call site (next_str starts from a &str and only splits at byte boundaries that preserve UTF-8 validity). Justifies uses-unsafe, unsafe-safe, unsafe-documented, and unsafe-minimal.

src/adapter/strip.rs, line 114-144

#[inline]
fn next_str<'s>(bytes: &mut &'s [u8], state: &mut State) -> Option<&'s str> {
    let offset = bytes.iter().copied().position(|b| {
        let (next_state, action) = state_change(*state, b);
        if next_state != State::Anywhere {
            *state = next_state;
        }
        is_printable_bytes(action, b)
    });
    let (_, next) = bytes.split_at(offset.unwrap_or(bytes.len()));
    *bytes = next;
    *state = State::Ground;

    let offset = bytes.iter().copied().position(|b| {
        let (_next_state, action) = state_change(State::Ground, b);
        !(is_printable_bytes(action, b) || is_utf8_continuation(b))
    });
    let (printable, next) = bytes.split_at(offset.unwrap_or(bytes.len()));
    *bytes = next;
    if printable.is_empty() {
        None
    } else {
        let printable = unsafe {
            from_utf8_unchecked(
                printable,
                "`bytes` was validated as UTF-8, the parser preserves UTF-8 continuations",
            )
        };
        Some(printable)
    }
}

The ANSI escape code parser is implemented in src/adapter/strip.rs and src/adapter/wincon.rs. It consumes byte-by-byte state from anstyle_parse::state::state_change, then classifies bytes as printable or escape-sequence bytes. The next_str and next_bytes functions iterate input slices without allocating per-byte. The parser is exercised by proptest-based property tests (strip_str_no_escapes, strip_bytes_no_escapes, strip_char_no_escapes, strip_byte_no_escapes) that generate arbitrary Unicode strings and compare against a reference full-parser implementation. These tests use proptest and are annotated #[cfg_attr(miri, ignore)], indicating the test suite has been run under Miri (where it would catch UB in the unsafe block). Justifies impl-parser, parser-impl-safe, parser-impl-tested, and unsafe-tested.

src/auto.rs

src/auto.rs, line 198-223

fn choice(raw: &dyn RawStream) -> ColorChoice {
    let choice = ColorChoice::global();
    match choice {
        ColorChoice::Auto => {
            let clicolor = anstyle_query::clicolor();
            let clicolor_enabled = clicolor.unwrap_or(false);
            let clicolor_disabled = !clicolor.unwrap_or(true);
            if anstyle_query::no_color() {
                ColorChoice::Never
            } else if anstyle_query::clicolor_force() {
                ColorChoice::Always
            } else if clicolor_disabled {
                ColorChoice::Never
            } else if raw.is_terminal()
                && (anstyle_query::term_supports_color()
                    || clicolor_enabled
                    || anstyle_query::is_ci())
            {
                ColorChoice::Always
            } else {
                ColorChoice::Never
            }
        }
        ColorChoice::AlwaysAnsi | ColorChoice::Always | ColorChoice::Never => choice,
    }
}

Environment variable access is delegated to anstyle-query via the auto feature. The choice function in src/auto.rs (lines 198-223) calls anstyle_query::no_color(), anstyle_query::clicolor_force(), anstyle_query::clicolor(), and anstyle_query::is_ci(), which read NO_COLOR, CLICOLOR_FORCE, CLICOLOR, and CI respectively. These are well-documented, conventional environment variables used only to decide colour output mode. No environment variable is written or the environment enumerated. Justifies uses-environment and environment-safe.

src/stream.rs

src/stream.rs, line 119-124

impl IsTerminal for std::fs::File {
    #[inline]
    fn is_terminal(&self) -> bool {
        is_terminal_polyfill::IsTerminal::is_terminal(self)
    }
}

stream.rs implements RawStream and IsTerminal for std::fs::File, allowing a File handle to be used as a backing stream. The file is opened and managed by the caller; anstream itself performs no path operations, directory traversal, or file creation. Justifies uses-filesystem and filesystem-safe.