A Rust port of libbsd 0.12.2, published as two crates:
bsd— safe, idiomatic Rust implementations of the BSD libc extensions.bsd-abi— a C ABI-compatible replacement forlibbsd.so.0.
[dependencies]
bsd = { version = "0.1", default-features = false, features = ["vis"] }Fifteen modules: cstr, fmt, line, mem, mode, net, num, proc,
size, sort, symbols, time, tty, users and vis. Each is a Cargo
feature and all are enabled by default. libc is the only dependency, and is
optional.
use bsd::{mode::{Changes, Mode}, size::Format, vis::{Encoder, Flags}};
Mode(0o100_644).to_string(); // "-rw-r--r-- " (strmode)
Changes::parse("u+x,go-w")?.apply(0o644); // 0o744 (setmode/getmode)
Format::new(5).suffix("B").render(1_500_000)?; // "1 MB" (humanize_number)
Encoder::new(Flags::WHITE).encode(b"a b"); // br"a\040b" (strvis)Items take Rust types and Rust names rather than being transliterated, and each
one's documentation names the C function it replaces, so searching the docs for
strlcpy or humanize_number finds its counterpart.
Functionality the standard library already covers is not reimplemented:
| libbsd | Use instead |
|---|---|
the twelve endian conversions |
u16::from_be_bytes and its siblings |
heapsort, mergesort |
slice::sort_unstable, slice::sort |
the err family |
eprintln!, io::Error::last_os_error, process::exit |
funopen |
Read / Write / Seek |
queue.h, tree.h, bitstring.h |
VecDeque, BTreeMap, bitvec |
docs/spec/api.md lists every omission with its replacement, and the reasoning
behind each.
bsd-abi produces a shared object that a program already linked against libbsd
resolves to without relinking: the same 110 symbols under the same LIBBSD_*
version nodes, with soname libbsd.so.0. Alongside it, the packaging step
installs the same 28 headers, libbsd.a, libbsd-ctor.a and the three
pkg-config files.
crates/package.sh <dir> # writes lib/, include/ and lib/pkgconfig/ under <dir>As in the C, the exported set is chosen by configuration rather than probed.
Five profiles carry libbsd's per-platform selector tables — profile-glibc
(the default), profile-musl, profile-darwin, profile-solaris and
profile-aix — and individual abi-* features select symbols within a
profile. Cargo features are additive, so switching profile requires
--no-default-features.
The shared object is 469 KB stripped, against the C's 83 KB. The difference is
the Rust standard library rather than this port's code; docs/spec/abi.md
measures the same effect for static linking under
[spec:libbsd:req:abi.static-archive].
cargo test --workspace # unit, integration and differential tests
crates/package.sh <dir> # build the installable tree
crates/run-c-tests.sh # upstream's C suite against the packaged library
crates/feature-matrix.sh # build each bsd feature on its own
crates/profile-matrix.sh # build each ABI profile
crates/header-match.sh # compare installed prototypes to the Rust definitionsRequirements:
- A nightly toolchain, pinned in
rust-toolchain.toml.bsd-abineedsc_variadicfor its seventeen variadic entry points;bsdalone builds on stable. libbsd-dev, or the equivalent for your distribution, for the differential tests, which compare against the reference library at run time.cbindgen, forheader-match.sh.
header-match.sh covers a gap the other checks leave open: no stage of a normal
build compares a Rust extern "C" signature against the C prototype that
declares it, and upstream's suite catches a mismatch only for the 33 symbols it
calls. It takes the header's view from gcc -aux-info and the Rust view from
cbindgen and diffs them, covering 94 of the 107 distinct exported names; the
remaining 13 are exempt with a reason recorded for each.
| Path | Contents |
|---|---|
crates/bsd |
the safe Rust API |
crates/bsd-abi |
the C entry points, version script and packaging inputs |
crates/bsd-abi-ctor |
the setproctitle constructor, shipped as libbsd-ctor.a |
crates/difftest |
differential tests against the installed reference library |
docs/spec |
the port specification |
include/bsd |
upstream's headers, installed unmodified |
test |
upstream's C test suite, run unmodified |
The C implementation is not vendored. include/bsd/ and test/ are upstream
libbsd, used as they stand: the headers are what bsd-abi installs, and the
suite is compiled against the packaged library by crates/run-c-tests.sh.
docs/spec/port/ carries two rules for each C function — a def rule fixing
its shape and a sem rule stating its runtime behaviour — in enough detail to
reimplement it without consulting the C. docs/spec/abi.md governs the shared
object, docs/spec/api.md governs the safe API, and docs/spec/scope.md
records what is deliberately not translated. All 341 rules are covered by the
implementation.
Where the C has a defect, the rule says so and states what the port does with
it. Both crates fix them, bsd-abi included: a drop-in that reproduces
buffer overruns is not worth having. Each divergence is recorded in the rule
for the function it belongs to, and where the reference's defect is observable
at run time its test pins that too — so a repair upstream fails the test rather
than passing unnoticed.
What is corrected:
| Defect | Port |
|---|---|
strnvis and the bounded encoders write the terminator at index dlen, one past the buffer the caller sized |
dlen bounds the whole destination, as it does in OpenBSD |
unvis folds a numeric entity at eleven times the running value, so only single digits decode |
folds at ten:  is 12, not 13 |
unvis never leaves the number state at the ;, swallowing what follows |
returns to the ground state: 89 is \x07 then 89 |
stravis leaves the caller's pointer aimed at freed memory when it fails |
stores null |
humanize_number negates i64::MIN into itself and prints nonsense |
carries the magnitude unsigned |
inet_net_pton writes past the destination on the hexadecimal path, then wraps its unsigned counter and stops bounding the rest of the parse |
stops where the destination ends and reports EMSGSIZE |
nlist indexes the string table with an unvalidated st_name, so a malformed object faults the calling process |
the offset is checked against the table, and an out-of-range symbol is skipped |
pwcache returns a pointer into its own dead stack frame when both allocations fail |
unreachable: the path does not exist here |
arc4random's fork check compares the stored pid against 1, so a process running as pid 1 — every container init — fully reseeds on every call |
compares against the pid the state was seeded in |
arc4random leaves the caller's out-parameter holding MAP_FAILED when its first mapping fails |
null, which is what the caller tests for |
a typo (abi_reallocf=ues) drops reallocf from a musl build |
exported, as every other platform table does |
Two upstream tests assert the first of these — strnvis(str, 10, ...) writing
eleven bytes — so they fail against this port by design.
crates/run-c-tests.sh reports them in a diverged category and fails the run
if either starts passing.
- 110 versioned exports over 107 distinct names, matching the reference.
- 94 of those 107 prototypes verified against the installed headers.
- Upstream's C test suite: 24 pass, 2 skipped on the C build's own terms, and 2 diverging by specification — see above.
- 492 Rust tests.
BSD-3-Clause AND ISC AND MIT, matching the C it replaces; per-file provenance
is recorded in the spec rule for each symbol, and COPYING carries the licence
texts as upstream ships them.