# wal-db v0.4.0 — Recovery hardening + typed records
**Recovery that survives anything, and records that can be typed.** v0.4.0 adds a
continuous fuzz harness that proves the recovery path never panics or
over-allocates on arbitrary bytes, a skip-bad-records policy for forensic partial
recovery, and an optional `pack-io` feature that lets records be typed values
instead of raw bytes. All additive — the default byte-record API is unchanged.
## What is wal-db?
A write-ahead log primitive for Rust storage engines — the durability substrate
under `lsm-db`, `txn-db`, `raft-io`, and Hive DB. The append path is lock-free,
concurrent commits coalesce into one fsync, durability is platform-correct, the
log can be striped across bounded segment files, and recovery is provable from a
torn write.
## What's new in 0.4.0
### Fuzz-hardened recovery
`fuzz/` adds a `cargo-fuzz` target that feeds arbitrary bytes to the recovery
path — open (which scans and truncates) and full iteration — under both recovery
policies, and asserts it never panics, over-allocates, or reads past the input.
The record-size cap means a crafted length prefix is rejected before any payload
allocation, so memory stays bounded no matter the input. Run it with:
```bash
cargo +nightly fuzz run recover
```
A representative local run executed **12 million inputs in 46 seconds with zero
crashes** and flat memory. CI runs it on every push.
### Recovery policies — `RecoveryPolicy`
`Wal::open` always truncates a torn tail so the append boundary is clean. For
corruption *inside* an already-recovered log — bit rot — a recovery policy now
controls how iteration reacts:
```rust
use wal_db::{RecoveryPolicy, Wal, WalConfig};
# fn main() -> Result<(), wal_db::WalError> {
# let dir = tempfile::tempdir().map_err(wal_db::WalError::from)?;
# let path = dir.path().join("app.wal");
let config = WalConfig::new().with_recovery_policy(RecoveryPolicy::SkipBadRecords);
let wal = Wal::open_with(&path, config)?;
for entry in wal.iter()? {
match entry {
Ok(record) => { /* use it */ }
Err(e) => eprintln!("skipped a damaged record: {e}"), // iteration continues
}
}
# Ok(())
# }
```
`StopAtFirstError` (the default) yields the first damage and stops.
`SkipBadRecords` surfaces each damaged record as an error — never silently — then
resumes at the next one. Skipping only works while a damaged record's length
prefix is intact enough to locate the next record; an unreadable length still
stops iteration, because there is no way to know where the next record begins.
### Typed records — the `pack-io` feature
By default a record is bytes. With the `pack-io` feature, a record can be any type
that derives `Serialize`/`Deserialize`:
```toml
[dependencies]
wal-db = { version = "0.4", features = ["pack-io"] }
```
```rust
use wal_db::{MemStore, Wal};
use wal_db::pack_io::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Event { id: u64, name: String }
# fn main() -> Result<(), wal_db::WalError> {
let wal = Wal::with_store(MemStore::new())?;
wal.append_typed(&Event { id: 1, name: "start".into() })?;
let event: Event = wal.iter()?.next().unwrap()?.decode()?;
assert_eq!(event, Event { id: 1, name: "start".into() });
# Ok(())
# }
```
`Wal::append_typed` serialises a value into one record; `Record::decode` reads it
back. The derives come from the re-exported `wal_db::pack_io`, so consumers do not
add the dependency themselves. A new `WalError::Encoding` variant reports a codec
failure. Everything here is opt-in: with the feature off, none of it exists and
the byte-record API is untouched.
## Breaking changes
**None.** The `pack-io` feature, `RecoveryPolicy`, the new config methods, and the
typed-record API are all additive, and `WalError::Encoding` is added under
`#[non_exhaustive]`. A log written by 0.3.x is read by 0.4.0 unchanged.
## Verification
Run on Windows x86_64 and Linux (WSL2 Ubuntu), Rust stable 1.95.x and MSRV
1.85.0; macOS is covered by the CI matrix:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo +1.85 clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo test # default features
RUSTFLAGS="--cfg loom" cargo test --test loom_wal
cargo +nightly fuzz run recover -- -max_total_time=60
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +1.85 build --all-features
cargo audit
cargo deny check
```
All green on both platforms. Counts at this tag:
- 50 unit tests by default, 52 with `--all-features` (typed-record tests)
- 10 integration tests (round-trip, segmented, torn-write property test,
cross-process durability)
- 2 loom model-check tests
- 1 fuzz target (12M+ inputs, no crashes)
- 22 doctests by default, 24 with `--all-features`
## What's next
- **0.5.0 — Feature complete + benchmarks.** LSN seeking (`Wal::iter_from`),
LSN-based truncation for compaction, an optional async-friendly wrapper, and a
recorded baseline benchmark suite. Feature freeze.
## Installation
```toml
[dependencies]
wal-db = "0.4"
# Typed records:
wal-db = { version = "0.4", features = ["pack-io"] }
```
MSRV: Rust 1.85.
## Documentation
- [README](https://github.com/jamesgober/wal-db/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/wal-db/blob/main/docs/API.md)
- [On-Disk Format](https://github.com/jamesgober/wal-db/blob/main/docs/ON_DISK_FORMAT.md)
- [CHANGELOG](https://github.com/jamesgober/wal-db/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.3.1...v0.4.0`](https://github.com/jamesgober/wal-db/compare/v0.3.1...v0.4.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/wal-db/blob/main/CHANGELOG.md#040---2026-06-05).