cargo / anstyle / audit
cargo : anstyle @ 1.0.14
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

has-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

Audit of anstyle 1.0.14, a small #![no_std] Rust crate providing core types for ANSI text-styling escape codes, used as a vocabulary crate by clap and the anstream ecosystem. No runtime deps, no build script, no I/O, no network. One unsafe block (str::from_utf8_unchecked on a fixed-size buffer that only ever holds ASCII digits and &'static str bytes) is documented and sound. No findings. Safe to use.

Report

Subject

anstyle provides core types describing ANSI text styling escape codes for interoperability between Rust CLI crates. The published API is a small set of value types — Style, Color, AnsiColor, Ansi256Color, RgbColor, Effects, Reset — with const-friendly constructors and Display impls that emit the corresponding ANSI escape sequences. The crate is #![no_std] by default with an opt-in std feature (default-enabled) that adds write_to(&mut dyn std::io::Write) rendering helpers. The crate has no runtime dependencies. It is the foundation of the clap/anstream styling ecosystem.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -r. The crate is published from crates/anstyle of the rust-cli/anstyle workspace; the symlinked vcs/ directory points at that subdirectory. The six source files under src/ (lib.rs, color.rs, effect.rs, reset.rs, style.rs, macros.rs, ~1650 lines total) were read with full reads on lib.rs, the unsafe site, and the surrounding DisplayBuffer implementation, and targeted reads on the rest. Cargo.toml, README.md, and the published examples were reviewed. The upstream tests/testsuite.rs (excluded from the published include) was inspected for context.

Tools: openvet 0.6.0 for workspace and audit data management; diff (Apple) for the byte-level comparison; git (2.51) for the upstream checkout; grep/ripgrep for capability surveys.

Results

The comparison between the published crate contents and the upstream Git repository shows that the source files, README.md, and the LICENSE-* files match byte-for-byte. Manifest differences are limited to cargo's standard Cargo.toml normalisation plus the addition of .cargo_vcs_info.json, Cargo.lock, and the preserved Cargo.toml.orig. The upstream tests/ directory and CHANGELOG.md are not included in the published crate because the include glob in Cargo.toml covers only src/, Cargo.toml, Cargo.lock, LICENSE*, README.md, examples/, and build.rs (no build.rs actually exists, the include is permissive).

The crate ships no binary artefacts, no build.rs, no proc macros, and no install hooks, justifying has-binaries, has-build-exec, and has-install-exec. Inline unit tests live in the source files (#[test] functions in color.rs, effect.rs, reset.rs, style.rs — 8 in total), justifying has-unit-tests. The integration test at tests/testsuite.rs is part of the upstream repository but not included in the published .crate, justifying has-integration-tests = false at the package level. There is no fuzz/ directory and no property-test harness, justifying has-fuzz-tests and has-property-tests. The package contains no malicious code or deliberately harmful behaviour, justifying is-benign.

The codebase was reviewed for cryptographic libraries (none), network I/O (none), file I/O (none — std::io::Write is only used as a generic trait object in the optional write_to helper, and the examples/dump-style.rs example writes to stdout without touching the filesystem), process execution (none), environment-variable access (none), interpreters or JIT (none), and concurrency primitives (none). This justifies uses-crypto, uses-network, uses-filesystem, uses-exec, uses-environment, uses-jit, uses-interpreter, uses-concurrency, impl-crypto, impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, and impl-concurrency.

There is exactly one unsafe block in the entire crate, in src/color.rs:615-617. It calls core::str::from_utf8_unchecked(&self.buffer[0..self.len]) to produce a &str view into a fixed-size 19-byte buffer (DISPLAY_BUFFER_CAPACITY on line 568). The two methods that write into this buffer — write_str (copies &'static str bytes) and write_code (writes b'0' + digit ASCII digit bytes for codes up to three digits) — always produce valid UTF-8 and stay within the fixed bound. The block carries a // SAFETY: comment documenting the invariant. The Display impls that flow through this buffer are exercised by the inline unit tests. This justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal, and unsafe-tested.

No findings were recorded.

Conclusion

anstyle is a small, focused vocabulary crate with a const-friendly API and minimal surface. The single unsafe operation is local, well-documented, and trivially sound. The audit found no security, safety, or correctness defects. The package is benign and safe to use.

Findings

No findings.

Annotations(1)

src/color.rs

src/color.rs, line 568-625

const DISPLAY_BUFFER_CAPACITY: usize = 19;

#[derive(Copy, Clone, Default, Debug)]
struct DisplayBuffer {
    buffer: [u8; DISPLAY_BUFFER_CAPACITY],
    len: usize,
}

impl DisplayBuffer {
    #[must_use]
    #[inline(never)]
    fn write_str(mut self, part: &'static str) -> Self {
        for (i, b) in part.as_bytes().iter().enumerate() {
            self.buffer[self.len + i] = *b;
        }
        self.len += part.len();
        self
    }

    #[must_use]
    #[inline(never)]
    fn write_code(mut self, code: u8) -> Self {
        let c1: u8 = (code / 100) % 10;
        let c2: u8 = (code / 10) % 10;
        let c3: u8 = code % 10;

        let mut printed = false;
        if c1 != 0 {
            printed = true;
            self.buffer[self.len] = b'0' + c1;
            self.len += 1;
        }
        if c2 != 0 || printed {
            self.buffer[self.len] = b'0' + c2;
            self.len += 1;
        }
        // If we received a zero value we must still print a value.
        self.buffer[self.len] = b'0' + c3;
        self.len += 1;

        self
    }

    #[inline]
    fn as_str(&self) -> &str {
        // SAFETY: Only `&str` can be written to the buffer
        #[allow(unsafe_code)]
        unsafe {
            core::str::from_utf8_unchecked(&self.buffer[0..self.len])
        }
    }

    #[inline]
    #[cfg(feature = "std")]
    fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> {
        write.write_all(self.as_str().as_bytes())
    }
}

Single unsafe block in the crate: DisplayBuffer::as_str (lines 615-617) calls core::str::from_utf8_unchecked on self.buffer[0..self.len]. The buffer is only written by two methods: write_str (lines 579-585) which copies bytes from a &'static str parameter (already valid UTF-8), and write_code (lines 589-609) which writes ASCII digit bytes (b'0' + cN where cN < 10). Both producers yield valid UTF-8, so the unchecked cast is sound. The // SAFETY: comment on line 613 documents this invariant. The buffer is fixed-size (DISPLAY_BUFFER_CAPACITY = 19 on line 568), so the writes also stay in bounds — write_code's path emits at most 3 bytes and is gated by code: u8. Justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal.