Output Formats Module (src/rbformats/)
July 18, 2026 · View on GitHub
Compress/decompress handlers for backup output formats, plus disk reconstruction for restore and VHD export.
Architecture
mod.rs— Orchestration functions (compress_partition,decompress_to_writer,reconstruct_disk_from_backup) and shared utilities (SplitWriter,write_zeros,is_all_zeros,output_path,file_name,CHUNK_SIZE).vhd.rs— VHD format: Fixed (build_vhd_footer,export_whole_disk_vhd,export_partition_vhd) and Dynamic/sparse (DynamicVhdReader,export_whole_disk_vhd_dynamic). Shared:vhd_chs_geometry,vhd_checksum,VHD_COOKIE.sparse.rs— Shared sparse-allocation bookkeeping (SparseAllocator,is_zero_unit) reused by dynamic VHD (and later QCOW2, VMDK sparse).qcow2.rs— QCOW2:Qcow2Reader(v2/v3, read + in-place allocate-on-write edit) andexport_qcow2(writes v3 withrefcount_order = 4, single-file, uncompressed clusters). Self-referential refcount-block sizing keepsqemu-img checkclean; the editor grows L2 tables + refcount blocks on demand and reuses free host clusters before extending the file.vmdk.rs— VMDK flat:VmdkFlatReader(parses the ASCII descriptor, concatenatesFLAT/ZEROextents frommonolithicFlatandtwoGbMaxExtentFlatlayouts),export_vmdk_flat(writes a single-extentmonolithicFlat:<base>.vmdkdescriptor +<base>-flat.vmdkraw extent), andopen_flat_extent_for_edit(resolves the single FLAT extent and returns itR+Wfor in-place edit; rejects multi-extent / ZERO / sparse layouts). Sparse VMDKs (KDMVmagic) detected by the flat reader are rejected with a precise message.vmdk_sparse.rs— VMDK sparse (monolithicSparse):VmdkSparseReaderparses the binaryKDMVSparseExtentHeader, loads the grain directory into RAM, lazily reads grain tables with a single-entry LRU (mirrorsQcow2Reader's L2 cache andChdReader's hunk cache), and servesRead + Seekover the virtual disk; unallocated grains read as zeros. Also implementsRead + Write + Seekallocate-on-write — writes into an unallocated grain append a fresh grain at EOF, set the GT entry on disk + in RAM, bumphost_end.export_vmdk_sparsestreams a whole-disk source into a single self-containedmonolithicSparsefile: header @ sector 0, embedded descriptor, GD, GTs, grain data; non-zero grains allocated on demand viaSparseAllocator, zero grains omitted. Compressed / marker (streamOptimized) variants are detected and rejected — they're not seekable.chd.rs— CHD (MAME) format via the in-processlibchdman-rscrate:compress_chd,compress_chd_dvd,ChdReader,CdCookedReader.zstd.rs— Zstd streaming compression:compress_zstd.raw.rs— Raw streaming with optional file splitting and sparse zero-skipping:stream_with_split.imz.rs— WinImage IMZ (.imz) read-only convert-from.materialize_imz_to_tempunwraps the ZIP-wrapped floppy image (.ima/.img) into a freshtempfile::TempDirand returns the temp path + guard. Password-protected entries (legacy ZipCrypto) are detected viaZipFile::encrypted()and rejected with a precise error. Wired throughgui::prepare_disk_image_pathso the GUI file-pick flow handles.imzuniformly with.adz/.hdz; downstream detection treats the materialized file as Raw.gho.rs— Norton Ghost (.gho/.ghs) read-only convert-from.materialize_gho_to_tempdecodes SECTOR-mode (raw sector-by-sector) backups — including multi-file span sets — to a raw disk image in a tempdir (None / zlib / Fast-LZ compression), wired throughgui::prepare_disk_image_pathalongside.imz/.adz/.hdz.discover_gho_span_set+SpanReaderhandle three corpus naming patterns: stem-prefix.GHSsiblings (with 8.3 truncation), hyphenated stem-prefix, and.GHO.NNNnumeric suffix; the user can pick the primary OR any span sibling and the whole chain decodes as one continuous stream. The container header, record-stream parser, and Fast-LZ decoder are clean-room ports from the MIT-licensednyarime/gho. File-aware ("truncated full backup") mode is supported viaGhoReader(FAT record-stream + NTFS GHPR reconstruction). Password-protected backups decrypt transparently — pass a password toGhoReader::open_with_password/materialize_gho_to_temp_with_password; the cipher was reverse-engineered fromghostexp.exe(seedocs/gho_password.md). File-aware and uncompressed-SECTOR images are encrypted and decrypt on read; compressed-SECTOR images are stored unencrypted by Ghost 11.5 (the password is still verified).
Available Formats
| Format | Extension | Compression | Splitting | Notes |
|---|---|---|---|---|
| Raw | .raw | None | Yes | Supports sparse zero-skipping |
| VHD (Fixed) | .vhd | None | No | Raw data + 512-byte footer |
| VHD (Dynamic) | .vhd | None | No | Sparse: BAT + per-block bitmap; all-zero blocks omitted |
| QCOW2 | .qcow2 | None | No | Read (v2 + v3) + write (v3 uncompressed). Sparse: zero clusters omitted. qemu-img check clean. |
| VMDK (Flat) | .vmdk | None | No | Read (monolithicFlat / twoGbMaxExtentFlat, FLAT + ZERO extents). Write: monolithicFlat — descriptor + sibling -flat.vmdk raw extent. |
| VMDK (Sparse) | .vmdk | None | No | Read + write + in-place edit (monolithicSparse, KDMV magic). Two-level grain map; zero grains omitted on write, allocate-on-write for edits. |
| WinImage IMZ | .imz | ZIP/Deflate | No | Read-only convert-from (decode-to-temp). Password-protected archives rejected with a clear error. |
| Norton Ghost | .gho / .ghs | None / zlib / Fast-LZ | Yes (auto-discover) | Read-only convert-from (decode-to-temp). SECTOR-mode (raw) only; file-aware reconstruction + password not yet supported. Multi-file span sets auto-discovered + concatenated. |
| Zstd | .zst | Zstd level 3 | Yes (post-hoc) | Good compression ratio |
| CHD | .chd | MAME CHD | Yes (post-hoc) | Native via libchdman-rs (no external tool) |
DMG (UDIF) reader — big-endian caveat
dmg.rs is a read-only source-container reader (not a backup output). It
decodes UDIF blocks of type raw/UDRW, zlib (UDZO), bzip2 (UDBZ), ADC (UDCO),
LZFSE (ULFO), and LZMA/xz (ULMO) into a Read + Seek virtual disk.
The two newest codecs differ in big-endian safety, which matters only if
rusty-backup is ever run on a big-endian host (e.g. a PowerPC G5 on Debian's
powerpc/ppc64 big-endian port — not the little-endian ppc64el). The
decode always runs on the machine executing rusty-backup, never on the vintage
box being imaged.
- ULMO / LZMA (
lzma-rs) — big-endian safe. The crate declares#![forbid(unsafe_code)]and reads every field through explicitbyteorderlittle/big tags, so decode is correct on any host endianness. - ULFO / LZFSE (
lzfse_rust) — big-endian unverified. The crate usesunsafenative-endian reads (read_unaligned::<u32>(),usize::from_ne_bytes) whose big-endian correctness is unproven and almost certainly untested upstream; ULFO decode could silently produce wrong bytes there. Verified only on little-endian (x86 / arm64 / ppc64el).
Both crates are left ungated today (every CI target is little-endian). If we
target a big-endian host, gate lzfse_rust behind a Cargo feature (so a
big-endian build can drop just that crate while keeping ULMO and the other DMG
codecs) and round-trip-test ULFO on that host against an hdiutil-produced
fixture before trusting it.
Other Apple source-container readers
These are read-only source-container readers (like dmg.rs), not backup
outputs. They decode a wrapped image into a Read + Seek flat disk that flows
through the normal partition / filesystem detection.
-
dmg_crypto.rs— encrypted disk images (encrcdsav2). AES-128/256 per-image encryption (Apple "FileVault 1", 10.5+ and still produced byhdiutil create -encryption). Passphrase →PBKDF2-HMAC-SHA1→ CBC unwrap of the volume-key blob (AES-192 on modern macOS, 3DES on older images; strip PKCS#7 +CKIE\0) → per-512-byte-blockAES-CBCwith anHMAC-SHA1(be32(n))IV. Verified end-to-end (payload round-trip) againsthdiutilAES-128 and AES-256 images. Wired into the password-carrying source path (source_reader::open_peeled_read_with_entry); password-less paths get a clear "provide the password" error instead of a cryptic MBR failure.Note — vintage relevance. Per-image encryption is uncommon in classic Mac preservation (it's a modern security feature; its heaviest real use is iOS firmware and personal vaults). It's supported here so an old encrypted
.dmga user still holds the passphrase for can be opened offline and cross-machine, with no Apple ID / Secure-Enclave dependency. The 3DES key-wrap path (the truly old images) is wired but has not been verified against a real vintage sample — modernhdiutilwraps with AES-192 even for AES-128 volumes, so we have no 3DES fixture yet. -
sparseimage.rs— Apple sparse images (.sparseimage, UDSP). A growable single file (Mac OS X 10.3+) storing only written 1 MiB bands via asprsband map; unallocated bands read as zeros. Verified byte-exact againsthdiutilimages. Handles up to ~1 GiB of written data (the in-header band map addresses ≤ 1008 physical bands); larger images use a band-index continuation scheme that returns a clear error rather than a misread. -
ndif.rs— NDIF / self-mounting images (.smi). The pre-UDIF Disk Copy 6 format (Mac OS 8/9): data-fork chunks described by abcemblock map in the resource fork. Delivered through a fork carrier (MacBinary.smi.bin, AppleDouble, or a native resource fork);model/source_readerdecodes the carrier and reconstructs the flat image (gated onbcem, so a plain.binraw image is never touched). Zero / raw / ADC chunks are decoded (verified against a real Disk First Aid 8.6.1 SMI); the RLE (0x81), LZH (0x82), KenCode (0x80), and StuffIt (0xF0) chunk codecs are recognized and reported with a clear per-codec error — they await real sample images that use them (KenCode is undocumented and needs reverse-engineering).
How to Add a New Output Format
-
Create
src/rbformats/myformat.rswith a compression function:pub(crate) fn compress_myformat( reader: &mut impl Read, output_base: &Path, split_size: Option<u64>, progress_cb: &mut impl FnMut(u64), cancel_check: &impl Fn() -> bool, ) -> Result<Vec<String>> -
Add a decompression path in
decompress_to_writer()inmod.rs(match oncompression_typestring from metadata). -
Add a variant to the
CompressionTypeenum inbackup/mod.rswithas_str()andfile_extension()methods. -
Add routing case in
compress_partition()inmod.rs. -
Add
pub mod myformat;torbformats/mod.rs.
Conventions
- All compression functions return
Vec<String>of output filenames (relative to the backup folder). - All functions accept
progress_cb(called with bytes read from source) andcancel_check(returnstrueto abort). - Shared utilities:
SplitWriterfor file splitting,write_zerosfor gap filling,output_pathfor consistent naming,CHUNK_SIZE(256 KB) for I/O buffers.