cargo / serde_test / audit
cargo : serde_test @ 1.0.177
PE Patrick Elsen signed 2026-06-02 published 2026-06-02

Claims

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

Summary

serde_test 1.0.177 is a test helper for the serde crate: assert_tokens/assert_ser_tokens/assert_de_tokens over a Token enum mirroring serde's data model. Pure safe Rust, no unsafe, no FFI, no I/O, no concurrency, one runtime dependency (serde). Source matches upstream byte-for-byte modulo cargo's normal Cargo.toml normalisation. One low-severity finding: no in-tree #[test] items — coverage is via doctests and the separate serde test suite.

Report

Subject

serde_test is a developer-facing test helper for the serde crate. It provides assert_tokens, assert_ser_tokens, assert_de_tokens, and the matching *_error variants that drive a custom serde Serializer/Deserializer pair over a slice of Token enum values. The Token enum (~30 variants) covers every shape that serde's data model can represent: primitives, strings, byte buffers, sequences, tuples, maps, structs, options, enums in all four representations, and a borrowed/non-borrowed/owned distinction for strings and bytes. The crate also exposes the Configure trait with Compact/Readable newtype wrappers for forcing is_human_readable on or off.

The intent is that authors of Serialize/Deserialize implementations write tests of the form "this value should serialize to exactly this sequence of Tokens" and the crate panics with a descriptive message at the first divergence.

Methodology

The published crate was downloaded by openvet audit new and unpacked into contents/; the upstream Git repository (https://github.com/serde-rs/test) was cloned and checked out at the commit recorded in .cargo_vcs_info.json into vcs/.

Tools used:

  • openvet audit (workspace creation, annotations, claims, findings, dependency narratives, report).
  • diff -r and diff -rq (GNU diffutils 3.x) to compare contents/ against vcs/.
  • grep to search for risky patterns (unsafe, extern, std::net, std::env, std::process, std::fs, std::thread, tokio, reqwest) and panic sites.
  • wc -l for line counts.
  • git log to confirm the published tag.

All seven source files were read in full (lib.rs, assert.rs, configure.rs, de.rs, error.rs, ser.rs, token.rs; ~3088 lines total). The upstream tree was enumerated to confirm absence of a tests/ directory. The CI workflow (.github/workflows/ci.yml) was read to understand what cargo test actually runs.

Results

The comparison between the published crate and the upstream Git commit shows that all source files match byte-for-byte. Cargo.toml differs only in cargo's standard normalisation (header banner, key reordering, key alphabetisation, inline-table to table-section expansion); Cargo.toml.orig preserves the original manifest. .cargo_vcs_info.json is cargo-generated.

The crate ships no binary artefacts (justifying has-binaries), no build.rs (justifying has-build-exec), no installer hook (justifying has-install-exec), and no proc-macro entry. A grep across the source tree for unsafe, extern, std::net, std::env, std::process, std::fs, std::thread, tokio, and reqwest returned a single hit: a use std::thread; line inside a doc comment in assert.rs that illustrates how a lock-poisoning error message would look. There is no actual unsafe code, FFI, networking, filesystem access, process execution, environment scraping, or threading. This is the basis for uses-unsafe, uses-network, uses-filesystem, uses-environment, uses-exec, uses-jit, uses-interpreter, uses-crypto, and uses-concurrency.

The crate does not implement a parser in the data-format sense (no byte- or character-stream parsing — the Deserializer walks a slice of Token enum values via split_first), nor any cryptographic primitive, interpreter, JIT, network protocol, general data structure, algorithm, or concurrency primitive. This is the basis for impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, impl-crypto, and impl-concurrency.

Panic sites were enumerated. Most are intentional: the public assert_* functions panic on mismatch (the crate's documented behaviour), and both Serializer::is_human_readable and Deserializer::is_human_readable panic to force test authors to opt into Configure. Two unreachable!() arms in ser.rs (lines 385 and 449) are guarded by the matching constructor in serialize_tuple_variant/serialize_struct_variant and cannot fire.

One low-severity finding was recorded. FINDING-1 documents that neither the crate nor its upstream repository contains #[test] items, a tests/ directory, or fuzz/property tests; coverage comes entirely from doctests run by CI, supplemented in practice by the separate serde repository's test suite. This is the basis for has-unit-tests = false, has-integration-tests = false, has-fuzz-tests = false, and has-property-tests = false.

The code is consistent with a serde-ecosystem test helper authored by the serde maintainers and contains no behaviour at odds with its documented purpose. This supports is-benign.

Conclusion

serde_test is a small, single-purpose, pure-safe-Rust test helper. It has no unsafe code, no I/O, no concurrency, and no native dependencies beyond serde itself. The one low-severity finding is structural (no in-tree non-doc tests), not behavioural. The crate is suitable for its stated role as a dev-dependencies-only test fixture.

Findings(1)

FINDING-1 quality low

No `#[test]` items or tests/ directory in the crate or upstream repository

Neither the published crate nor the upstream Git tree contains #[test] items or a tests/ or benches/ directory. The CI workflow (.github/workflows/ci.yml) runs cargo test --features serde/derive,serde/rc, which exercises only the doctests embedded in the source files. The doctests cover most of the public Token variants and the assert_* entry points, but the internal Deserializer/Serializer implementation logic — in particular the enum-handling branches in Deserializer::deserialize_any (src/de.rs:145-194) and the EnumMapVisitor (src/de.rs:570-642) — has no direct test coverage in this crate. In practice the crate is exercised by the serde repository's own test suite, which lives in a separate crate.

This is the basis for has-unit-tests = false, has-integration-tests = false, has-fuzz-tests = false, and has-property-tests = false.

Annotations(5)

src

Six Rust source files, ~3000 lines total, all reviewed in full. The crate implements serde's Serializer and Deserializer traits over a slice of Token enum values (defined in token.rs) to drive test-time assertion of Serialize/Deserialize impls. assert.rs holds the public assert_tokens/assert_ser_tokens/assert_de_tokens/*_error entry points; ser.rs and de.rs hold the serde-trait implementations; configure.rs wraps a value in Readable/Compact to override is_human_readable; error.rs is a string-wrapping Error type. No unsafe, no FFI, no I/O, no concurrency, no crypto.

src/assert.rs

All five public assertion functions are #[track_caller] and panic on mismatch (this is a test helper, so panicking is the intended UX). They drive a Serializer/Deserializer over the provided &[Token] slice and report the first divergence. assert_de_tokens also re-runs through deserialize_in_place (src/assert.rs:185-191) to catch broken in-place impls — the doc comment notes this is best-effort because a no-op in-place can pass.

src/configure.rs

Wraps a serde-serializable/deserializable value in Readable or Compact. Both wrappers forward every Serializer/Deserializer method to the inner serializer, only overriding is_human_readable to return true or false respectively. ~30 method implementations per direction; mechanical pass-throughs.

src/de.rs

src/de.rs, line 376-381

    fn is_human_readable(&self) -> bool {
        panic!(
            "Types which have different human-readable and compact representations \
             must explicitly mark their test cases with `serde_test::Configure`"
        );
    }

Same intentional panic as Serializer::is_human_readable (src/ser.rs:306-311) — forces use of Configure in tests.

src/de.rs, line 102-382

impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
    type Error = Error;

    forward_to_deserialize_any! {
        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
        bytes byte_buf unit seq map identifier ignored_any
    }

    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        let token = self.next_token()?;
        match token {
            Token::Bool(v) => visitor.visit_bool(v),
            Token::I8(v) => visitor.visit_i8(v),
            Token::I16(v) => visitor.visit_i16(v),
            Token::I32(v) => visitor.visit_i32(v),
            Token::I64(v) => visitor.visit_i64(v),
            Token::U8(v) => visitor.visit_u8(v),
            Token::U16(v) => visitor.visit_u16(v),
            Token::U32(v) => visitor.visit_u32(v),
            Token::U64(v) => visitor.visit_u64(v),
            Token::F32(v) => visitor.visit_f32(v),
            Token::F64(v) => visitor.visit_f64(v),
            Token::Char(v) => visitor.visit_char(v),
            Token::Str(v) => visitor.visit_str(v),
            Token::BorrowedStr(v) => visitor.visit_borrowed_str(v),
            Token::String(v) => visitor.visit_string(v.to_owned()),
            Token::Bytes(v) => visitor.visit_bytes(v),
            Token::BorrowedBytes(v) => visitor.visit_borrowed_bytes(v),
            Token::ByteBuf(v) => visitor.visit_byte_buf(v.to_vec()),
            Token::None => visitor.visit_none(),
            Token::Some => visitor.visit_some(self),
            Token::Unit | Token::UnitStruct { .. } => visitor.visit_unit(),
            Token::NewtypeStruct { .. } => visitor.visit_newtype_struct(self),
            Token::Seq { len } => self.visit_seq(len, Token::SeqEnd, visitor),
            Token::Tuple { len } => self.visit_seq(Some(len), Token::TupleEnd, visitor),
            Token::TupleStruct { len, .. } => {
                self.visit_seq(Some(len), Token::TupleStructEnd, visitor)
            }
            Token::Map { len } => self.visit_map(len, Token::MapEnd, visitor),
            Token::Struct { len, .. } => self.visit_map(Some(len), Token::StructEnd, visitor),
            Token::Enum { .. } => {
                let variant = self.next_token()?;
                let next = self.peek_token()?;
                match (variant, next) {
                    (Token::Str(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_str(variant)
                    }
                    (Token::BorrowedStr(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_borrowed_str(variant)
                    }
                    (Token::String(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_string(variant.to_string())
                    }
                    (Token::Bytes(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_bytes(variant)
                    }
                    (Token::BorrowedBytes(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_borrowed_bytes(variant)
                    }
                    (Token::ByteBuf(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_byte_buf(variant.to_vec())
                    }
                    (Token::U8(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_u8(variant)
                    }
                    (Token::U16(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_u16(variant)
                    }
                    (Token::U32(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_u32(variant)
                    }
                    (Token::U64(variant), Token::Unit) => {
                        self.next_token()?;
                        visitor.visit_u64(variant)
                    }
                    (variant, Token::Unit) => Err(unexpected(variant)),
                    (variant, _) => {
                        visitor.visit_map(EnumMapVisitor::new(self, variant, EnumFormat::Any))
                    }
                }
            }
            Token::UnitVariant { variant, .. } => visitor.visit_str(variant),
            Token::NewtypeVariant { variant, .. } => visitor.visit_map(EnumMapVisitor::new(
                self,
                Token::Str(variant),
                EnumFormat::Any,
            )),
            Token::TupleVariant { variant, .. } => visitor.visit_map(EnumMapVisitor::new(
                self,
                Token::Str(variant),
                EnumFormat::Seq,
            )),
            Token::StructVariant { variant, .. } => visitor.visit_map(EnumMapVisitor::new(
                self,
                Token::Str(variant),
                EnumFormat::Map,
            )),
            Token::SeqEnd
            | Token::TupleEnd
            | Token::TupleStructEnd
            | Token::MapEnd
            | Token::StructEnd
            | Token::TupleVariantEnd
            | Token::StructVariantEnd => Err(unexpected(token)),
        }
    }

    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::Unit | Token::None => {
                self.next_token()?;
                visitor.visit_none()
            }
            Token::Some => {
                self.next_token()?;
                visitor.visit_some(self)
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn deserialize_enum<V>(
        self,
        name: &'static str,
        _variants: &'static [&'static str],
        visitor: V,
    ) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::Enum { name: n } if name == n => {
                self.next_token()?;

                visitor.visit_enum(DeserializerEnumVisitor { de: self })
            }
            Token::UnitVariant { name: n, .. }
            | Token::NewtypeVariant { name: n, .. }
            | Token::TupleVariant { name: n, .. }
            | Token::StructVariant { name: n, .. }
                if name == n =>
            {
                visitor.visit_enum(DeserializerEnumVisitor { de: self })
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn deserialize_unit_struct<V>(self, name: &'static str, visitor: V) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::UnitStruct { .. } => {
                assert_next_token(self, Token::UnitStruct { name })?;
                visitor.visit_unit()
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn deserialize_newtype_struct<V>(
        self,
        name: &'static str,
        visitor: V,
    ) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::NewtypeStruct { .. } => {
                assert_next_token(self, Token::NewtypeStruct { name })?;
                visitor.visit_newtype_struct(self)
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::Unit | Token::UnitStruct { .. } => {
                self.next_token()?;
                visitor.visit_unit()
            }
            Token::Seq { .. } => {
                self.next_token()?;
                self.visit_seq(Some(len), Token::SeqEnd, visitor)
            }
            Token::Tuple { .. } => {
                self.next_token()?;
                self.visit_seq(Some(len), Token::TupleEnd, visitor)
            }
            Token::TupleStruct { .. } => {
                self.next_token()?;
                self.visit_seq(Some(len), Token::TupleStructEnd, visitor)
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn deserialize_tuple_struct<V>(
        self,
        name: &'static str,
        len: usize,
        visitor: V,
    ) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::Unit => {
                self.next_token()?;
                visitor.visit_unit()
            }
            Token::UnitStruct { .. } => {
                assert_next_token(self, Token::UnitStruct { name })?;
                visitor.visit_unit()
            }
            Token::Seq { .. } => {
                self.next_token()?;
                self.visit_seq(Some(len), Token::SeqEnd, visitor)
            }
            Token::Tuple { .. } => {
                self.next_token()?;
                self.visit_seq(Some(len), Token::TupleEnd, visitor)
            }
            Token::TupleStruct { len: n, .. } => {
                assert_next_token(self, Token::TupleStruct { name, len: n })?;
                self.visit_seq(Some(len), Token::TupleStructEnd, visitor)
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn deserialize_struct<V>(
        self,
        name: &'static str,
        fields: &'static [&'static str],
        visitor: V,
    ) -> Result<V::Value, Error>
    where
        V: Visitor<'de>,
    {
        match self.peek_token()? {
            Token::Struct { len: n, .. } => {
                assert_next_token(self, Token::Struct { name, len: n })?;
                self.visit_map(Some(fields.len()), Token::StructEnd, visitor)
            }
            Token::Map { .. } => {
                self.next_token()?;
                self.visit_map(Some(fields.len()), Token::MapEnd, visitor)
            }
            _ => self.deserialize_any(visitor),
        }
    }

    fn is_human_readable(&self) -> bool {
        panic!(
            "Types which have different human-readable and compact representations \
             must explicitly mark their test cases with `serde_test::Configure`"
        );
    }
}

Deserializer implementation walks the &[Token] slice and dispatches to the appropriate visitor.visit_* method. The enum-handling branches in deserialize_any (src/de.rs:145-194) implement the externally-tagged, internally-tagged-by-name, and explicit Enum formats. The Deserializer consumes its input via simple slice-front advancement (split_first), no parsing of bytes or characters.

src/ser.rs

src/ser.rs, line 306-311

    fn is_human_readable(&self) -> bool {
        panic!(
            "Types which have different human-readable and compact representations \
             must explicitly mark their test cases with `serde_test::Configure`"
        );
    }

is_human_readable panics by design: the crate forces test authors to mark their case as .compact() or .readable() via the Configure trait when the format would otherwise vary. Documented in the Configure doc comment.

src/ser.rs, line 385-385

            _ => unreachable!(),

unreachable!() arms in Variant::end and SerializeStructVariant::end (src/ser.rs:449) are guarded by the matching constructor in serialize_tuple_variant and serialize_struct_variant (src/ser.rs:258, 293) which assigns one of TupleVariantEnd/SeqEnd or StructVariantEnd/MapEnd. Cannot fire.