cargo / bit-set / audit
cargo : bit-set @ 0.8.0
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

datastructure-impl-boundsdatastructure-impl-correctdatastructure-impl-safedatastructure-impl-testedhas-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

bit-set 0.8.0 is a #![no_std] integer-set data structure built on top of bit-vec. Two unsafe blocks (storage_mut+index, set_len after truncating zero blocks); CI runs Miri with strict provenance. Three low-severity quality findings: missing SAFETY comments, stale RELEASES.md, and no property/fuzz tests.

Report

Subject

bit-set provides BitSet, a set of unsigned integers backed by a packed bit vector from the bit-vec crate. Storage scales with the maximum element, not the cardinality. The crate is #![no_std] by default; the std feature (default-on) and the serde feature (off-by-default) are the only build options. The lone dependency is bit-vec (same upstream, same author family).

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 single source file src/lib.rs (~1680 lines) was read in full, including the embedded test module, as was benches/bench.rs. Each unsafe block was analysed against the surrounding code to confirm its invariants. The Cargo.toml feature graph was reviewed, and the GitHub Actions CI was read to note which checks gate releases. Claims were emitted for all has-, uses-, and impl- categories. The test suite was not executed.

Results

The crate contents differ from the upstream Git tree only by Cargo's standard Cargo.toml normalisation and the auto-inserted Cargo.toml.orig and .cargo_vcs_info.json files; src/lib.rs and benches/bench.rs match byte-for-byte.

The crate ships no binary artefacts (justifying has-binaries), no build.rs and no proc-macros (justifying has-build-exec and has-install-exec). The source was reviewed for network, filesystem, environment, process-exec, JIT, interpreter, cryptographic and concurrency usage; none was found, justifying uses-network, uses-filesystem, uses-environment, uses-exec, uses-jit, uses-interpreter, uses-crypto and uses-concurrency. Correspondingly the crate does not implement any of those capabilities, justifying impl-crypto, impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-algorithm and impl-concurrency. The crate does implement a set data structure (BitSet), justifying impl-datastructure.

Two unsafe blocks were found, justifying uses-unsafe. Both call unsafe APIs on BitVec (storage_mut, set_len) and their invariants hold under review (unsafe-safe, unsafe-minimal); CI runs cargo miri test with -Zmiri-strict-provenance over the full test suite, justifying unsafe-tested. Neither block carries a // SAFETY: comment, justifying unsafe-documented = false (FINDING-1).

The crate has a 24-test embedded unit-test suite covering each public operation including a serde round-trip, justifying has-unit-tests, datastructure-impl-safe, datastructure-impl-correct, datastructure-impl-tested, and datastructure-impl-bounds (operations on BitSet are bounded by the storage length and never degrade adversarially). The crate has no integration, fuzz or property tests, justifying has-integration-tests, has-fuzz-tests and has-property-tests (FINDING-3).

No suspicious or obfuscated code was observed; the crate's behaviour matches its documentation, justifying is-benign.

Three low-severity quality findings were recorded: missing SAFETY comments on both unsafe blocks (FINDING-1), a stale RELEASES.md that documents a non-released 0.7.0 and a non-existent impl Display (FINDING-2), and the absence of property/fuzz tests for a public data-structure crate (FINDING-3).

Conclusion

bit-set is a small, single-purpose data-structure crate with a narrow attack surface. All findings are low-severity quality issues that do not affect runtime correctness or safety. The two unsafe blocks are minimal, their invariants hold, and they are exercised under Miri. The crate is safe to use as a transitive dependency.

Findings(3)

FINDING-1 quality low

Unsafe blocks lack SAFETY comments

The two unsafe blocks in src/lib.rs (lines 406-408 in other_op, lines 445-448 in shrink_to_fit) have no // SAFETY: comments explaining the invariants relied on when calling the unsafe BitVec APIs (storage_mut, set_len). The invariants do hold (justifying unsafe-safe), but the absence of documentation makes the code harder to review and to maintain safely under future modification, justifying unsafe-documented.

FINDING-2 quality low

RELEASES.md is stale and incorrect

RELEASES.md is titled "Version 0.7.0 (not yet released)" while the crate ships as 0.8.0, and it claims impl Display is implemented even though only impl Debug exists in src/lib.rs (line 874). Users consulting the release notes will be misinformed about both the crate version and its public API.

FINDING-3 quality low

No property or fuzz tests for a public data-structure crate

The crate exposes BitSet set-algebra operations (union, intersection, difference, symmetric difference, subset, etc.) and is tested only by 24 hand-written unit tests in src/lib.rs. There are no property-based tests (e.g. proptest, quickcheck) or fuzz tests cross-checking the bit-set operations against a reference implementation such as HashSet<usize>. Coverage of edge cases (length mismatches between operands, very large indices, repeated grow/shrink cycles) is therefore narrow, justifying that datastructure-impl-tested only marginally holds. The CI does run cargo miri test with -Zmiri-strict-provenance, which mitigates the risk for the unsafe-related invariants.

Annotations(4)

.github/workflows/rust.yml

CI runs cargo miri test with MIRIFLAGS=-Zmiri-strict-provenance for both the default and serde feature sets, plus cargo clippy, cargo fmt --check, and cargo doc -Dwarnings. The Miri pass exercises the two unsafe blocks against Stacked-Borrows-style aliasing rules, justifying unsafe-tested.

Cargo.toml

Cargo.toml, line 56-62

[features]
default = ["std"]
serde = [
    "dep:serde",
    "bit-vec/serde",
]
std = ["bit-vec/std"]

Three features: default = ["std"], std (enables bit-vec/std), and serde (enables bit-vec/serde plus dep:serde). std is on by default but the crate is #![no_std] internally and pulls in extern crate std only when the feature is enabled. No feature pulls in network, filesystem, or unsafe capability.

RELEASES.md

Release notes are titled "Version 0.7.0 (not yet released)" but the crate is published as 0.8.0, and claim impl Display is implemented while only impl Debug exists in the source (FINDING-2).

src/lib.rs

src/lib.rs, line 379-410

    #[inline]
    fn other_op<F>(&mut self, other: &Self, mut f: F)
    where
        F: FnMut(B, B) -> B,
    {
        // Unwrap BitVecs
        let self_bit_vec = &mut self.bit_vec;
        let other_bit_vec = &other.bit_vec;

        let self_len = self_bit_vec.len();
        let other_len = other_bit_vec.len();

        // Expand the vector if necessary
        if self_len < other_len {
            self_bit_vec.grow(other_len - self_len, false);
        }

        // virtually pad other with 0's for equal lengths
        let other_words = {
            let (_, result) = match_words(self_bit_vec, other_bit_vec);
            result
        };

        // Apply values found in other
        for (i, w) in other_words {
            let old = self_bit_vec.storage()[i];
            let new = f(old, w);
            unsafe {
                self_bit_vec.storage_mut()[i] = new;
            }
        }
    }

other_op calls storage_mut()[i] = new inside an unsafe block (line 406-408). Justifies uses-unsafe. The block was grown to other_len immediately above (line 392-394) and the iterator other_words is bounded by max(self_len, other_len) storage entries, so the index i is always in range; this is a plain write to an already-allocated block, not a length mutation. The block is unsafe only because storage_mut() exposes the underlying Vec (supports unsafe-safe and unsafe-minimal). No SAFETY comment is present (FINDING-1, supports unsafe-documented).

src/lib.rs, line 432-450

    pub fn shrink_to_fit(&mut self) {
        let bit_vec = &mut self.bit_vec;
        // Obtain original length
        let old_len = bit_vec.storage().len();
        // Obtain coarse trailing zero length
        let n = bit_vec
            .storage()
            .iter()
            .rev()
            .take_while(|&&n| n == B::zero())
            .count();
        // Truncate away all empty trailing blocks, then shrink_to_fit
        let trunc_len = old_len - n;
        unsafe {
            bit_vec.storage_mut().truncate(trunc_len);
            bit_vec.set_len(trunc_len * B::bits());
        }
        bit_vec.shrink_to_fit();
    }

shrink_to_fit truncates the storage to drop trailing all-zero blocks, then calls set_len(trunc_len * B::bits()) to bring BitVec's bit-length into sync (lines 445-448). The discarded blocks were verified to be B::zero() by the preceding take_while (lines 437-442), so no set bits are dropped, justifying unsafe-safe and datastructure-impl-correct for this code path. No SAFETY comment is present (FINDING-1, supports unsafe-documented). trunc_len * B::bits() could overflow usize only for bit-sets of usize::MAX/B::bits() blocks, which would require an allocation far beyond practical memory.

src/lib.rs, line 88-118

#[allow(clippy::iter_skip_zero)]
// Take two BitVec's, and return iterators of their words, where the shorter one
// has been padded with 0's
fn match_words<'a, 'b, B: BitBlock>(
    a: &'a BitVec<B>,
    b: &'b BitVec<B>,
) -> (MatchWords<'a, B>, MatchWords<'b, B>) {
    let a_len = a.storage().len();
    let b_len = b.storage().len();

    // have to uselessly pretend to pad the longer one for type matching
    if a_len < b_len {
        (
            a.blocks()
                .enumerate()
                .chain(iter::repeat(B::zero()).enumerate().take(b_len).skip(a_len)),
            b.blocks()
                .enumerate()
                .chain(iter::repeat(B::zero()).enumerate().take(0).skip(0)),
        )
    } else {
        (
            a.blocks()
                .enumerate()
                .chain(iter::repeat(B::zero()).enumerate().take(0).skip(0)),
            b.blocks()
                .enumerate()
                .chain(iter::repeat(B::zero()).enumerate().take(a_len).skip(b_len)),
        )
    }
}

match_words virtually pads the shorter of two block iterators with zeros so that set-algebra operations behave correctly when operands have different storage lengths. For union/symmetric_difference the zero-padded blocks produce the longer operand's content; for intersection the extra blocks become zero; for difference, self blocks beyond other survive unchanged. Justifies datastructure-impl-correct for the in-place other_op-based methods.

src/lib.rs, line 120-123

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct BitSet<B = u32> {
    bit_vec: BitVec<B>,
}

BitSet<B = u32> wraps a single BitVec<B> field and derives serde::{Serialize, Deserialize} when the serde feature is enabled. The struct is the package's sole public data type, justifying impl-datastructure.

src/lib.rs, line 1105-1617

#[cfg(test)]
mod tests {
    use super::BitSet;
    use bit_vec::BitVec;
    use std::cmp::Ordering::{Equal, Greater, Less};
    use std::vec::Vec;
    use std::{format, vec};

    #[test]
    fn test_bit_set_show() {
        let mut s = BitSet::new();
        s.insert(1);
        s.insert(10);
        s.insert(50);
        s.insert(2);
        assert_eq!("{1, 2, 10, 50}", format!("{:?}", s));
    }

    #[test]
    fn test_bit_set_from_usizes() {
        let usizes = vec![0, 2, 2, 3];
        let a: BitSet = usizes.into_iter().collect();
        let mut b = BitSet::new();
        b.insert(0);
        b.insert(2);
        b.insert(3);
        assert_eq!(a, b);
    }

    #[test]
    fn test_bit_set_iterator() {
        let usizes = vec![0, 2, 2, 3];
        let bit_vec: BitSet = usizes.into_iter().collect();

        let idxs: Vec<_> = bit_vec.iter().collect();
        assert_eq!(idxs, [0, 2, 3]);
        assert_eq!(bit_vec.iter().count(), 3);

        let long: BitSet = (0..10000).filter(|&n| n % 2 == 0).collect();
        let real: Vec<_> = (0..10000 / 2).map(|x| x * 2).collect();

        let idxs: Vec<_> = long.iter().collect();
        assert_eq!(idxs, real);
        assert_eq!(long.iter().count(), real.len());
    }

    #[test]
    fn test_bit_set_frombit_vec_init() {
        let bools = [true, false];
        let lengths = [10, 64, 100];
        for &b in &bools {
            for &l in &lengths {
                let bitset = BitSet::from_bit_vec(BitVec::from_elem(l, b));
                assert_eq!(bitset.contains(1), b);
                assert_eq!(bitset.contains(l - 1), b);
                assert!(!bitset.contains(l));
            }
        }
    }

    #[test]
    fn test_bit_vec_masking() {
        let b = BitVec::from_elem(140, true);
        let mut bs = BitSet::from_bit_vec(b);
        assert!(bs.contains(139));
        assert!(!bs.contains(140));
        assert!(bs.insert(150));
        assert!(!bs.contains(140));
        assert!(!bs.contains(149));
        assert!(bs.contains(150));
        assert!(!bs.contains(151));
    }

    #[test]
    fn test_bit_set_basic() {
        let mut b = BitSet::new();
        assert!(b.insert(3));
        assert!(!b.insert(3));
        assert!(b.contains(3));
        assert!(b.insert(4));
        assert!(!b.insert(4));
        assert!(b.contains(3));
        assert!(b.insert(400));
        assert!(!b.insert(400));
        assert!(b.contains(400));
        assert_eq!(b.len(), 3);
    }

    #[test]
    fn test_bit_set_intersection() {
        let mut a = BitSet::new();
        let mut b = BitSet::new();

        assert!(a.insert(11));
        assert!(a.insert(1));
        assert!(a.insert(3));
        assert!(a.insert(77));
        assert!(a.insert(103));
        assert!(a.insert(5));

        assert!(b.insert(2));
        assert!(b.insert(11));
        assert!(b.insert(77));
        assert!(b.insert(5));
        assert!(b.insert(3));

        let expected = [3, 5, 11, 77];
        let actual: Vec<_> = a.intersection(&b).collect();
        assert_eq!(actual, expected);
        assert_eq!(a.intersection(&b).count(), expected.len());
    }

    #[test]
    fn test_bit_set_difference() {
        let mut a = BitSet::new();
        let mut b = BitSet::new();

        assert!(a.insert(1));
        assert!(a.insert(3));
        assert!(a.insert(5));
        assert!(a.insert(200));
        assert!(a.insert(500));

        assert!(b.insert(3));
        assert!(b.insert(200));

        let expected = [1, 5, 500];
        let actual: Vec<_> = a.difference(&b).collect();
        assert_eq!(actual, expected);
        assert_eq!(a.difference(&b).count(), expected.len());
    }

    #[test]
    fn test_bit_set_symmetric_difference() {
        let mut a = BitSet::new();
        let mut b = BitSet::new();

        assert!(a.insert(1));
        assert!(a.insert(3));
        assert!(a.insert(5));
        assert!(a.insert(9));
        assert!(a.insert(11));

        assert!(b.insert(3));
        assert!(b.insert(9));
        assert!(b.insert(14));
        assert!(b.insert(220));

        let expected = [1, 5, 11, 14, 220];
        let actual: Vec<_> = a.symmetric_difference(&b).collect();
        assert_eq!(actual, expected);
        assert_eq!(a.symmetric_difference(&b).count(), expected.len());
    }

    #[test]
    fn test_bit_set_union() {
        let mut a = BitSet::new();
        let mut b = BitSet::new();
        assert!(a.insert(1));
        assert!(a.insert(3));
        assert!(a.insert(5));
        assert!(a.insert(9));
        assert!(a.insert(11));
        assert!(a.insert(160));
        assert!(a.insert(19));
        assert!(a.insert(24));
        assert!(a.insert(200));

        assert!(b.insert(1));
        assert!(b.insert(5));
        assert!(b.insert(9));
        assert!(b.insert(13));
        assert!(b.insert(19));

        let expected = [1, 3, 5, 9, 11, 13, 19, 24, 160, 200];
        let actual: Vec<_> = a.union(&b).collect();
        assert_eq!(actual, expected);
        assert_eq!(a.union(&b).count(), expected.len());
    }

    #[test]
    fn test_bit_set_subset() {
        let mut set1 = BitSet::new();
        let mut set2 = BitSet::new();

        assert!(set1.is_subset(&set2)); //  {}  {}
        set2.insert(100);
        assert!(set1.is_subset(&set2)); //  {}  { 1 }
        set2.insert(200);
        assert!(set1.is_subset(&set2)); //  {}  { 1, 2 }
        set1.insert(200);
        assert!(set1.is_subset(&set2)); //  { 2 }  { 1, 2 }
        set1.insert(300);
        assert!(!set1.is_subset(&set2)); // { 2, 3 }  { 1, 2 }
        set2.insert(300);
        assert!(set1.is_subset(&set2)); // { 2, 3 }  { 1, 2, 3 }
        set2.insert(400);
        assert!(set1.is_subset(&set2)); // { 2, 3 }  { 1, 2, 3, 4 }
        set2.remove(100);
        assert!(set1.is_subset(&set2)); // { 2, 3 }  { 2, 3, 4 }
        set2.remove(300);
        assert!(!set1.is_subset(&set2)); // { 2, 3 }  { 2, 4 }
        set1.remove(300);
        assert!(set1.is_subset(&set2)); // { 2 }  { 2, 4 }
    }

    #[test]
    fn test_bit_set_is_disjoint() {
        let a = BitSet::from_bytes(&[0b10100010]);
        let b = BitSet::from_bytes(&[0b01000000]);
        let c = BitSet::new();
        let d = BitSet::from_bytes(&[0b00110000]);

        assert!(!a.is_disjoint(&d));
        assert!(!d.is_disjoint(&a));

        assert!(a.is_disjoint(&b));
        assert!(a.is_disjoint(&c));
        assert!(b.is_disjoint(&a));
        assert!(b.is_disjoint(&c));
        assert!(c.is_disjoint(&a));
        assert!(c.is_disjoint(&b));
    }

    #[test]
    fn test_bit_set_union_with() {
        //a should grow to include larger elements
        let mut a = BitSet::new();
        a.insert(0);
        let mut b = BitSet::new();
        b.insert(5);
        let expected = BitSet::from_bytes(&[0b10000100]);
        a.union_with(&b);
        assert_eq!(a, expected);

        // Standard
        let mut a = BitSet::from_bytes(&[0b10100010]);
        let mut b = BitSet::from_bytes(&[0b01100010]);
        let c = a.clone();
        a.union_with(&b);
        b.union_with(&c);
        assert_eq!(a.len(), 4);
        assert_eq!(b.len(), 4);
    }

    #[test]
    fn test_bit_set_intersect_with() {
        // Explicitly 0'ed bits
        let mut a = BitSet::from_bytes(&[0b10100010]);
        let mut b = BitSet::from_bytes(&[0b00000000]);
        let c = a.clone();
        a.intersect_with(&b);
        b.intersect_with(&c);
        assert!(a.is_empty());
        assert!(b.is_empty());

        // Uninitialized bits should behave like 0's
        let mut a = BitSet::from_bytes(&[0b10100010]);
        let mut b = BitSet::new();
        let c = a.clone();
        a.intersect_with(&b);
        b.intersect_with(&c);
        assert!(a.is_empty());
        assert!(b.is_empty());

        // Standard
        let mut a = BitSet::from_bytes(&[0b10100010]);
        let mut b = BitSet::from_bytes(&[0b01100010]);
        let c = a.clone();
        a.intersect_with(&b);
        b.intersect_with(&c);
        assert_eq!(a.len(), 2);
        assert_eq!(b.len(), 2);
    }

    #[test]
    fn test_bit_set_difference_with() {
        // Explicitly 0'ed bits
        let mut a = BitSet::from_bytes(&[0b00000000]);
        let b = BitSet::from_bytes(&[0b10100010]);
        a.difference_with(&b);
        assert!(a.is_empty());

        // Uninitialized bits should behave like 0's
        let mut a = BitSet::new();
        let b = BitSet::from_bytes(&[0b11111111]);
        a.difference_with(&b);
        assert!(a.is_empty());

        // Standard
        let mut a = BitSet::from_bytes(&[0b10100010]);
        let mut b = BitSet::from_bytes(&[0b01100010]);
        let c = a.clone();
        a.difference_with(&b);
        b.difference_with(&c);
        assert_eq!(a.len(), 1);
        assert_eq!(b.len(), 1);
    }

    #[test]
    fn test_bit_set_symmetric_difference_with() {
        //a should grow to include larger elements
        let mut a = BitSet::new();
        a.insert(0);
        a.insert(1);
        let mut b = BitSet::new();
        b.insert(1);
        b.insert(5);
        let expected = BitSet::from_bytes(&[0b10000100]);
        a.symmetric_difference_with(&b);
        assert_eq!(a, expected);

        let mut a = BitSet::from_bytes(&[0b10100010]);
        let b = BitSet::new();
        let c = a.clone();
        a.symmetric_difference_with(&b);
        assert_eq!(a, c);

        // Standard
        let mut a = BitSet::from_bytes(&[0b11100010]);
        let mut b = BitSet::from_bytes(&[0b01101010]);
        let c = a.clone();
        a.symmetric_difference_with(&b);
        b.symmetric_difference_with(&c);
        assert_eq!(a.len(), 2);
        assert_eq!(b.len(), 2);
    }

    #[test]
    fn test_bit_set_eq() {
        let a = BitSet::from_bytes(&[0b10100010]);
        let b = BitSet::from_bytes(&[0b00000000]);
        let c = BitSet::new();

        assert!(a == a);
        assert!(a != b);
        assert!(a != c);
        assert!(b == b);
        assert!(b == c);
        assert!(c == c);
    }

    #[test]
    fn test_bit_set_cmp() {
        let a = BitSet::from_bytes(&[0b10100010]);
        let b = BitSet::from_bytes(&[0b00000000]);
        let c = BitSet::new();

        assert_eq!(a.cmp(&b), Greater);
        assert_eq!(a.cmp(&c), Greater);
        assert_eq!(b.cmp(&a), Less);
        assert_eq!(b.cmp(&c), Equal);
        assert_eq!(c.cmp(&a), Less);
        assert_eq!(c.cmp(&b), Equal);
    }

    #[test]
    fn test_bit_set_shrink_to_fit_new() {
        // There was a strange bug where we refused to truncate to 0
        // and this would end up actually growing the array in a way
        // that (safely corrupted the state).
        let mut a = BitSet::new();
        assert_eq!(a.len(), 0);
        assert_eq!(a.capacity(), 0);
        a.shrink_to_fit();
        assert_eq!(a.len(), 0);
        assert_eq!(a.capacity(), 0);
        assert!(!a.contains(1));
        a.insert(3);
        assert!(a.contains(3));
        assert_eq!(a.len(), 1);
        assert!(a.capacity() > 0);
        a.shrink_to_fit();
        assert!(a.contains(3));
        assert_eq!(a.len(), 1);
        assert!(a.capacity() > 0);
    }

    #[test]
    fn test_bit_set_shrink_to_fit() {
        let mut a = BitSet::new();
        assert_eq!(a.len(), 0);
        assert_eq!(a.capacity(), 0);
        a.insert(259);
        a.insert(98);
        a.insert(3);
        assert_eq!(a.len(), 3);
        assert!(a.capacity() > 0);
        assert!(!a.contains(1));
        assert!(a.contains(259));
        assert!(a.contains(98));
        assert!(a.contains(3));

        a.shrink_to_fit();
        assert!(!a.contains(1));
        assert!(a.contains(259));
        assert!(a.contains(98));
        assert!(a.contains(3));
        assert_eq!(a.len(), 3);
        assert!(a.capacity() > 0);

        let old_cap = a.capacity();
        assert!(a.remove(259));
        a.shrink_to_fit();
        assert!(a.capacity() < old_cap, "{} {}", a.capacity(), old_cap);
        assert!(!a.contains(1));
        assert!(!a.contains(259));
        assert!(a.contains(98));
        assert!(a.contains(3));
        assert_eq!(a.len(), 2);

        let old_cap2 = a.capacity();
        a.clear();
        assert_eq!(a.capacity(), old_cap2);
        assert_eq!(a.len(), 0);
        assert!(!a.contains(1));
        assert!(!a.contains(259));
        assert!(!a.contains(98));
        assert!(!a.contains(3));

        a.insert(512);
        assert!(a.capacity() > 0);
        assert_eq!(a.len(), 1);
        assert!(a.contains(512));
        assert!(!a.contains(1));
        assert!(!a.contains(259));
        assert!(!a.contains(98));
        assert!(!a.contains(3));

        a.remove(512);
        a.shrink_to_fit();
        assert_eq!(a.capacity(), 0);
        assert_eq!(a.len(), 0);
        assert!(!a.contains(512));
        assert!(!a.contains(1));
        assert!(!a.contains(259));
        assert!(!a.contains(98));
        assert!(!a.contains(3));
        assert!(!a.contains(0));
    }

    #[test]
    fn test_bit_vec_remove() {
        let mut a = BitSet::new();

        assert!(a.insert(1));
        assert!(a.remove(1));

        assert!(a.insert(100));
        assert!(a.remove(100));

        assert!(a.insert(1000));
        assert!(a.remove(1000));
        a.shrink_to_fit();
    }

    #[test]
    fn test_bit_vec_clone() {
        let mut a = BitSet::new();

        assert!(a.insert(1));
        assert!(a.insert(100));
        assert!(a.insert(1000));

        let mut b = a.clone();

        assert!(a == b);

        assert!(b.remove(1));
        assert!(a.contains(1));

        assert!(a.remove(1000));
        assert!(b.contains(1000));
    }

    #[test]
    fn test_truncate() {
        let bytes = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];

        let mut s = BitSet::from_bytes(&bytes);
        s.truncate(5 * 8);

        assert_eq!(s, BitSet::from_bytes(&bytes[..5]));
        assert_eq!(s.len(), 5 * 8);
        s.truncate(4 * 8);
        assert_eq!(s, BitSet::from_bytes(&bytes[..4]));
        assert_eq!(s.len(), 4 * 8);
        // Truncating to a size > s.len() should be a noop
        s.truncate(5 * 8);
        assert_eq!(s, BitSet::from_bytes(&bytes[..4]));
        assert_eq!(s.len(), 4 * 8);
        s.truncate(8);
        assert_eq!(s, BitSet::from_bytes(&bytes[..1]));
        assert_eq!(s.len(), 8);
        s.truncate(0);
        assert_eq!(s, BitSet::from_bytes(&[]));
        assert_eq!(s.len(), 0);
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_serialization() {
        let bset: BitSet = BitSet::new();
        let serialized = serde_json::to_string(&bset).unwrap();
        let unserialized: BitSet = serde_json::from_str(&serialized).unwrap();
        assert_eq!(bset, unserialized);

        let elems: Vec<usize> = vec![11, 42, 100, 101];
        let bset: BitSet = elems.iter().map(|n| *n).collect();
        let serialized = serde_json::to_string(&bset).unwrap();
        let unserialized = serde_json::from_str(&serialized).unwrap();
        assert_eq!(bset, unserialized);
    }

Twenty-four unit tests gated on #[cfg(test)] cover insertion, removal, contains, length, equality, ordering, hashing, union/intersection/difference/symmetric difference (both iterator and in-place variants), subset/superset/disjoint, truncate, shrink_to_fit, clone, debug-formatting, and serde round-trip. Justifies has-unit-tests and partially supports datastructure-impl-tested. No property tests or fuzz tests are present (FINDING-3, supports has-property-tests = false and has-fuzz-tests = false).