A strictly read-only, cross-platform deleted-file recovery and filesystem-analysis tool β built entirely from scratch in Rust.
π Full documentation site β Β Β (source)
NullVeil reads disk images and live block devices and recovers data via two independent strategies:
- Signature carving (
carve) β filesystem-agnostic recovery of files from their on-disk byte signatures. Works on any source, any filesystem, any OS. The right tool when metadata is gone. - Per-filesystem undelete (
ls,recover) β reads the actual filesystem and recovers files by name and path. Cleanest where filesystem semantics allow it (FAT, NTFS); best-effort where they don't (ext, APFS).
Plus an interactive TUI (tui) with multi-select basket, search, and
hex-and-ASCII preview β and a mobile-recovery on-ramp (unwrap-ab)
that unpacks Android Backup files for triage.
Built as a learning project per a "from-scratch / the learning is the point" spec: every filesystem parser, every partition table parser, every byte-level structure is hand-rolled. No
fatfs,ntfs,gpt, or similar crates.
| If you want to⦠| Go here |
|---|---|
| Get running in 5 minutes | Quick start |
| Install on macOS / Linux / Windows | docs/src/install/ |
| Recover from a USB stick / external drive / SD card | External-storage tutorials |
| Recover from Android | Android tutorial |
| Recover from iPhone | iOS tutorial |
| Look up a CLI flag | CLI reference |
| Understand the read-only guarantee | Read-only by construction |
| Command | Purpose | Status |
|---|---|---|
info <source> |
Show partition table + detected filesystems | All platforms, all FS |
list-devices |
Enumerate live disks attached to this OS | macOS, Linux |
carve <source> --out <dir> |
Universal signature carving | All platforms |
ls <source> |
List directory entries | FAT, NTFS, ext, APFS (volume-list) |
ls --deleted <source> |
List deleted entries | FAT, NTFS, ext (rec_len gaps) |
recover <source> --out <dir> |
Per-filesystem undelete | FAT (byte-exact), NTFS (byte-exact), ext (best-effort) |
tui <source> |
Interactive terminal-UI browser | All platforms |
unwrap-ab <backup.ab> |
Unpack an Android Backup (mobile workflow) | All platforms |
- Disk image files (
.dd,.img,.raw) β every platform - Live raw devices:
- macOS β
/dev/rdiskNviaDKIOCGETBLOCKSIZE/DKIOCGETBLOCKCOUNT - Linux β
/dev/sdX,/dev/nvme*viaBLKSSZGET/BLKGETSIZE64(code written, untested locally) - Windows β recipe documented; implementation deferred (see
src/device/raw_windows.rs)
- macOS β
| Filesystem | Detection | List | Undelete | Notes |
|---|---|---|---|---|
| FAT12 / FAT16 / FAT32 | β | β | β byte-exact (contiguous) | Long filename (LFN) support; FAT32 + FAT12/16 root region |
| exFAT | β | β root dir | β byte-exact (root, contiguous) | UTF-16LE names, NoFatChain support; sub-directory recursion is a follow-up |
| NTFS | β | β | β byte-exact | MFT walker, USA fixup, resident + non-resident data, data-run decoding |
| ext2 / ext3 / ext4 | β | β | best-effort | Extents + legacy block pointers; deleted-name resurrection via rec_len gaps |
| APFS | β | β (volume list) | β | Container superblock + omap B+-tree walking; per-volume FS-tree deferred |
For deleted data on ext4 and APFS, where metadata is typically zeroed at
delete time, the principled fallback is carve (M1) β it doesn't care about
the filesystem and works on whatever bytes still exist on the volume.
CLI (clap)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Recovery orchestration Β· Export (out-dir only) Β· Audit log
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
Carving engine β Per-filesystem
(FS-agnostic, universal) β FAT Β· NTFS Β· ext Β· APFS
ββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββ
Filesystem trait + detection
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Partition layer (MBR / GPT / superfloppy)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
BlockReader trait (READ-ONLY β no write method on the trait)
backends: ImageFile β macOS dev β Linux dev β Windows dev
The BlockReader trait deliberately has no write method. No backend can
be asked to mutate a source through it. Defense-in-depth: backends also open
OS handles read-only (O_RDONLY / GENERIC_READ). Exporter::open refuses
output directories that:
- equal the source path,
- contain the source,
- live inside the source (when source is a directory), or
- (on Unix) live on a filesystem whose backing block device matches the source.
Every demo in this README ends by verifying shasum of the source is
unchanged after the run.
git clone <repo> # once you create one
cd NullVeil
cargo build --release
./target/release/nullveil --helpRequires Rust 1.95+ stable, edition 2024. Only direct dependencies:
clap, anyhow, thiserror, serde + serde_json, sha2, and
(Unix only) libc for raw-device ioctls.
$ nullveil info evidence.img
Use only on devices and systems you own or are authorized to analyze.
Source : evidence.img
Size : 64.0 MiB (67108864 bytes)
Sector : 512 bytes
Scheme : GPT
#1 LBA 40..131032 64.0 MiB [GPT 7C3457EF-0000-11AA-AA11-00306543ECAC]
filesystem: APFS, 4096-byte blocks, 64.0 MiB volume
$ nullveil list-devices
PATH KIND SIZE RAW PATH
/dev/disk0 disk ? /dev/rdisk0
/dev/disk0s1 partition ? /dev/rdisk0s1
...
/dev/disk4 disk 32.0 MiB /dev/rdisk4
System disks show ? for size when they're not user-readable
(they require root); a freshly attached USB / loopback shows its real size.
$ nullveil --log audit.jsonl carve evidence.img --out recovered --types jpg,png,pdf --verify
scanned 64.0 MiB / 200.0 MiB (32.0%), 5 carved, 325.3 KiB recovered
scanned 128.0 MiB / 200.0 MiB (64.0%), 11 carved, 520.3 KiB recovered
scanned 192.0 MiB / 200.0 MiB (96.0%), 16 carved, 846.1 KiB recovered
scanned 200.0 MiB / 200.0 MiB (100.0%), 16 carved, 846.1 KiB recovered
Carved : 16 file(s)
Bytes : 846.1 KiB
Dedupe : 2 duplicate(s) skipped
Out : recovered/
Manifest: recovered/manifest.json
Carving recognises JPEG, PNG, GIF, BMP, PDF, ZIP (incl. .docx/.xlsx/.jar), and MP4 (incl. .mov/.m4a). BMP and JPEG signatures include post-header validators that drop ~99% of random-data false positives.
--verify re-reads every written file and confirms its SHA-256 matches the
streaming hash. --types filters to specific extensions. --keep-duplicates
disables the default SHA-256 dedupe. The audit log is JSON-lines with
carve_start / carved / carve_end events.
Launch the interactive forensic browser:
nullveil tui evidence.img
# or, for a single-file recover shortcut on `r`:
nullveil tui evidence.img --out /tmp/recoveredBuilt on ratatui. Two-pane layout:
- Source view (default): partition list + selected-partition details
- Browser view (after
Enter): directory listing with a live/deleted toggle
Keybindings:
| Key | Action |
|---|---|
ββ / j k |
move cursor |
Enter / m |
mount partition (Source view) |
Enter |
descend into directory (Browser view) |
β« / Left / h |
ascend to parent directory |
Tab |
toggle Live β Deleted |
r |
recover highlighted file to --out dir |
Esc |
back to Source view (from Browser) |
? |
help overlay |
q |
quit |
The TUI inherits all the read-only safety guarantees from the engine β it
never holds a writable handle to a source. The r action streams the file
through the same Exporter+SHA-256-manifest pipeline as the CLI's recover
command, so a TUI recovery is forensically equivalent.
$ nullveil ls /tmp/fat32.img
KIND SIZE NAME PATH
file 512 KiB IMAGE.JPG /IMAGE.JPG
dir 0 B DOCS /DOCS
$ nullveil ls --deleted /tmp/fat32.img
file 128 KiB ?DOC.PDF /?DOC.PDF [deleted]
$ nullveil recover /tmp/fat32.img --out /tmp/out --include-deleted
Recovered: 2 file(s)
Bytes : 640.0 KiB
Out : /tmp/out
Manifest : /tmp/out/manifest.json
The leading ? is the byte we lost to FAT's 0xE5 delete marker β the
original glyph is gone. recover --include-deleted then performs
contiguous-allocation undelete (byte-exact for the common case of small,
non-fragmented files).
- Always recover to a different device than the source. NullVeil refuses
via
ExportError::SameAsSource, but the responsibility for picking a sane--outis yours. - Live raw devices typically require root (
sudo nullveil β¦) or full-disk-access on macOS. --logwrites a JSON-lines audit trail withts_ms, source identity, byte offsets read, per-file SHA-256 β keep this for chain-of-custody.- Forensic cardinal rule: treat the source as a witness. Don't write to
it; clone first if possible (
ddordc3dd). NullVeil never opens with write access; theBlockReadertrait has no write method, period.
- exFAT β detected, but full read is a separate filesystem from FAT (allocation bitmap, upcase table, fragmented allocation) and is left as a follow-up.
- NTFS β
list_dirwalks the MFT by parent_ref linear-scan; B+-tree index walking (the proper approach) is a follow-up. - ext β deleted-file data recovery is fundamentally best-effort because
the inode's block pointers are typically zeroed at delete time. Names
survive in directory
rec_lengaps; data needs the carver. - APFS β per-volume directory walking (FS-tree B+-tree with variable-length
keys and 13+ record types, multi-version COW, optional encryption) is a
whole milestone on its own;
nullveil carveis the practical recovery path today. - Windows live devices β recipe documented in source; needs a Windows
host + the
windows-syscrate to wire up. - Fragmented FAT undelete β currently contiguous-allocation only.
src/
lib.rs # Public engine API
main.rs # clap CLI
audit/ # JSON-lines audit log
carve/ # Universal carver + signature registry
mod.rs, signature.rs
device/ # BlockReader + backends
mod.rs, image.rs, raw_macos.rs, raw_linux.rs, raw_windows.rs, enumerate.rs
export/ # Streaming writer + manifest + dedupe + verify
mod.rs, manifest.rs
fs/ # Filesystem detection + per-FS impls
mod.rs
fat/ ntfs/ ext/ apfs/ # Each: detect + Filesystem trait impl
partition/ # MBR + GPT + superfloppy + CRC32
mod.rs, mbr.rs, gpt.rs, crc32.rs
tests/ # 27 integration tests across 8 e2e binaries
info_e2e.rs carve_e2e.rs
m3_device_e2e.rs m4_fat_e2e.rs m4_demo.rs
m5_ntfs_e2e.rs m6_ext_e2e.rs m7_apfs_e2e.rs
tools/ # Sample-image generator
gen-samples/
See CHANGELOG.md for the per-milestone log. The eight milestones, in order:
- M0 β
BlockReadertrait,ImageReader, MBR/GPT, filesystem detection,info - M1 β universal signature carving + SHA-256 manifest + audit log
- M2 β carving polish: progress, dedupe,
--verify,--quiet - M3 β live raw devices (macOS),
list-devices,--deviceflag, same-device write guard - M4 β FAT12/16/32 undelete with LFN reassembly +
ls/recoverCLI - M5 β NTFS undelete with MFT walker, USA fixup, attribute decoder, data runs
- M6 β ext2/3/4 read + best-effort deleted-name surfacing via
rec_lengaps - M7 β APFS container parser + object map B+-tree + volume superblock reader
116 tests across 89 unit + 27 e2e binaries, ~7,000 lines of Rust.
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.