cargo

Cargo.lock, version 3 and 4.

$ ./target/release/stranger scan fixtures/cargo-m.Cargo.lock

  cargo-m.Cargo.lock       708 packages   (34 direct · 674 transitive · 15 workspace)

  ⚠  VERSION DRIFT          70    same package at 2+ versions in one tree

  ·  INSTALL SCRIPTS        — no signal in this format
  ·  UNPINNED               — no signal in this format

  risk 46/100    8ms    third-party deps used to compute this: 0

ksni used to appear in that block. It is a real crate below the top 5,000 and it was a false positive — and it was four characters long, which turned out to be the whole story: a name that short is within two edits of something in every registry, so clause 2 was not filtering it at all. The length budget (distance::CHARS_PER_EDIT) gives a four-character name no edits, and ksni stops firing. False positives has the measurement.

The crates.io corpus's size is still a published number rather than an implementation detail, because that is the limit underneath: 5,000 names is the top of crates.io and not the whole of it, and a longer crate below the cut would still be reported.

Structurally this is the easiest of the three formats: an array of [[package]] tables, no install paths to reproduce, no nesting. What makes it non-trivial is that its dependency strings are not names.

The three shapes of a dependency string

Cargo writes the shortest form that is unambiguous and promotes only when it must:

"bytes"                                  name
"winapi 0.3.9"                           name version
"qux 1.0.0 (registry+https://…)"         name version source

The second appears when two entries share a name — cargo-m has five hashbrowns and five windows-sys. The third appears when two entries share a name and a version and differ only in origin.

Counted across all three fixtures, 5,689 dependency strings in total:

fixturebarename+versionname+version+source
cargo-s25180
cargo-m1,7235000
cargo-l2,6105970

The third shape is not exercised by any real fixture here. It is implemented and tested against a hand-written file, and that is the honest status: handled, unmeasured.

The second shape is very much exercised — 1,105 strings, a fifth of the corpus — and reading one as a bare name is not a near miss. All 22 of cargo-m's windows-sys edges carry a version and there are five entries they could mean, so dropping the version collapses all 22 onto one entry. Fourteen of them point at 0.61.2, so the luckiest possible guess still lands 8 edges on the wrong package, and the unluckiest lands 21. Silently, too — no parse error, just a graph with wrong edges, which corrupts the in-degree the detection rule leans on.

The invariant that makes the bare form safe is Cargo's, not this reader's — a bare name is written only when exactly one entry carries it. That was checked rather than assumed: across all three fixtures, zero bare names refer to a duplicated package and zero dependency strings of any shape fail to resolve.

No source means somebody in this repo wrote it

sourcewhat it is
registry+https://…crates.io, or another registry
git+https://…#reva git dependency — not first-party
absenta workspace member or a path = "…" dependency

There is nowhere to fetch a path dependency from, so Cargo writes no source key at all. That is the whole first-party test, and it is npm's link: true rule wearing different clothes. It matters for the same reason: cargo-l is a 944-entry workspace with 93 members, and if their edges counted as evidence, a hallucinated crate added to any one of those 93 Cargo.toml files would arrive with in-degree 1 and never be looked at. Those edges become roots instead.

A git dependency is deliberately not first-party. Somebody outside this repo wrote it, and bypassing crates.io is if anything more interesting — but it also means the crates.io corpus was never asked about it, which is why the name rules stay quiet on anything whose origin is not the registry the corpus samples. That fix removed two of cargo-m's three original findings.

What the format does not record

Build scripts. Cargo runs build.rs at compile time — the same arbitrary-code-execution shape npm's hasInstallScript flags — and Cargo.lock records nothing about it. install_script is false on every package, so install scripts never fires on a Rust tree. Inventing a proxy (the -sys suffix, say) would produce a confident wrong answer, which is worse than a blank. A real answer needs the .crate archive or the index's metadata, and both mean fetching.

dev-dependencies, and optional ones. Cargo.lock distinguishes neither. A feature-gated dependency that was resolved is written exactly like any other, so dev and optional are false on every package — a limitation, not a measurement. Splitting them out needs Cargo.toml, the workspace's, and feature unification, which is a resolver.

Checksums on v1 files. checksum on the package table is v2-and-later; v1 kept them in a [metadata] table keyed "checksum bytes 1.0.0 (registry+…)". This reader does not look there, so a v1 file reads as having no integrity anywhere. There is no v1 file in the corpus and Cargo has rewritten them on every cargo update since 2019.

has_integrity is otherwise exact. 93 of cargo-l's 944 entries have no checksum and all 93 are the workspace members. cargo-m has 34 without one: 15 workspace members and 19 git dependencies, which have a source and no checksum because a git revision is its own integrity claim.

What it refuses

Two shapes, both of which mean the file is not the thing its name says it is.

No [[package]] array at all:

$ mkdir -p /tmp/refuse-cargo
$ printf 'x = 1\n' > /tmp/refuse-cargo/Cargo.lock
$ ./target/release/stranger scan /tmp/refuse-cargo/Cargo.lock
stranger: /tmp/refuse-cargo/Cargo.lock: no [[package]] entries; this does not look like a Cargo.lock

Refusing beats the alternative for the same reason it does in the npm reader: a reader that looked for package, found nothing and carried on would report a clean project with zero dependencies, and a clean report on an unread file is the worst output an auditing tool can produce.

An entry with no name or no version:

$ printf '[[package]]\nversion = "1.0"\n' > /tmp/refuse-cargo/x.Cargo.lock
$ ./target/release/stranger scan /tmp/refuse-cargo/x.Cargo.lock
stranger: /tmp/refuse-cargo/x.Cargo.lock: [[package]] #1 has no `name`

The ordinal rather than a line number, and that is a real limit rather than an oversight: positions die at the end of toml::parse, which hands back a value tree with no spans in it. [[package]] #1 is still something you can count to. Threading spans through the whole Value type to improve one message was not worth what it would cost every other user of the parser.

Everything the TOML subset refuses is refused here too, and decisions documents that subset. The important half is that it refuses rather than guesses: Cargo.lock is written by one program, so anything outside the subset means either a cargo change worth knowing about or a corrupt file, and both deserve an error rather than a partial tree.

Which rules can fire

Only two. slopsquat on registry crates, and drift — every entry records an exact version so pinning has nothing to say, install_script is never set, and the trivial rule's name list is npm micro-packages.

$ ./target/release/stranger scan fixtures/cargo-l.Cargo.lock