Architecture
┌────────────────────────────────────────────────────────────────────┐
│ CLI (RVZSharp.Cli) │
│ header │ verify │ convert │ extract — Blob.Open │
└───────────────┬────────────────────────────────────────────────────┘
│ IBlobReader
┌───────────────▼────────────────────────────────────────────────────┐
│ Library (RVZSharp) │
│ │
│ Blobs ── RvzReader ── Chunks/Compression/Packing/Wii (read path) │
│ Blobs ── RvzWriter/WiaWriter ── WiaRvzWriter core (write path) │
│ Blobs ── GczWriter / CisoWriter / WbfsWriter / TgcWriter (writers) │
│ Blobs ── DiscHasher / DiscVerifier ── Wii hash trees (verify) │
└────────────────────────────────────────────────────────────────────┘
Module map
| Module | Files | Responsibility |
|---|---|---|
Blobs/ |
Blob, BlobType, IBlobReader, PlainBlob, GczBlob, CisoBlob, WbfsBlob, TgcBlob, NfsBlob |
Format detection and per-format random-access decoding to ISO bytes |
Models/ |
DiscInfo, DiscFileInfo, WiaFileHead, WiaDisc, WiaPartEntry, WiaRawDataEntry, GroupEntry, HashExceptionEntry, DiscHashes, WiaRvzFormat, CompressionType |
Data models, container structs (RVZ/WIA), option records and enums |
Chunks/ |
ChunkDecoder, TableParser |
Group decompression, exception-list parsing, table loading |
Compression/ |
CompressionCodecFactory, CompressionEncoderFactory, ICompressionDecoder, ICompressionEncoder, codecs, Lzma/ (vendored 7-Zip decoder) |
Read-side decompression and write-side compression |
Packing/ |
RvzPackingDecoder, RvzPackingEncoder, LaggedFibonacciGenerator, LaggedFibonacciPrng |
RVZ junk packing: segment streams and PRNG seed recovery |
Wii/ |
PartitionRegionBuilder, WiiHashCalculator, WiiVolume, WiiPartitionExtractor, PartitionReader |
Wii partition encryption, hash tree, exceptions, ticket title-key decryption, decrypted partition views |
Files/ |
DiscFileSystem |
GameCube/Wii FST parsing, case-insensitive lookup, file data streaming |
Verification/ |
DiscVerifier |
Dolphin VolumeVerifier equivalent: partition headers, TMD/H3 tables and the h0/h1/h2/h3 hash tree walk |
IO/ |
LruCache, MultiPartStream, BlobCopy, Crc32, SectionStream, ParallelExecution |
Decoded-unit caching, split-file streams, copy/hash helpers and exception-preserving parallel loops |
Read path
IBlobReader (any format) canonical ISO bytes
│
▼
RvzReader.Open validates file head + disc struct hashes,
loads partition/raw/group tables (hash-checked)
│
▼
ReadAt(position) finds the data area covering `position`
│
├─ raw area ──► group by chunk ──► decompress ──► unpack segments (if packed)
│ │
└─ partition area ─► chunk payload (decrypted data) │
+ hash exceptions │
│ │
▼ ▼
PartitionRegionBuilder RvzPackingDecoder
(recompute h0/h1/h2, apply (LFG PRNG, seed +
exceptions, AES-128-CBC skip offset % 0x8000)
re-encrypt, zero-fill tail)
Key points:
- Raw areas are chunked by
chunk_size; partition areas by 2 MiB regions (64 sectors). - Groups carry
rvz_packed_size:0means “no packing headers”, anything else means the group data starts with a segment stream (see Compression & packing). - The reader keeps small caches (last raw chunk, last partition region) — cache keys include the segment index so split partitions cannot collide.
- Exception offsets are stored chunk-relative; the reader adds the chunk’s offset within its 2 MiB region before matching them to sectors.
Write path
IBlobReader (any format) Stream (RVZ/WIA out)
│ ▲
▼ │
WiiVolume detection ──► data areas in disc order │
(Wii magic, 0x60/0x61 flags, partition table) │
│ │
├─ raw area ──► chunk payload │
│ │ │
│ ▼ │
│ RvzPackingEncoder (junk scan, │
│ GetSeed, segment stream; RVZ) │
│ │ │
├─ partition area ─► WiiPartitionExtractor
│ (decrypt region, diff hash tree ──► exceptions,
│ split/merge into chunks; one exception list
│ per 2 MiB region a chunk covers)
│ │
│ ▼
│ pack (RVZ) + compress each group
│ (zero group when all-zero and no exceptions;
│ WIA always stores the codec output)
│ │
▼ ▼
tables (partition = plain, raw + group = compressed;
RVZ 12-byte / WIA 8-byte group entries)
layout iteration until the group-table size converges
│
▼
file head (SHA-1 over disc struct, sizes, head hash)
Key points:
- Partitions are split at the FST end (aligned up to 2 MiB): the area before it and the
area after it become two data entries, matching Dolphin’s
ConvertToWIAOrRVZ. - The first raw area starts at
0x80(the disc header is stored in the disc struct’sdisc_headerfield) and is read from sector-aligned offset 0; the raw table’s group count covers the grown read size so Dolphin-style readers that trustnumber_of_groupssee a consistent table. - Groups whose payload is all zeroes (and that have no exceptions) become zero groups (stored size 0) — this is what makes mostly-empty discs compress to kilobytes.
- The group table’s compressed size depends on the offsets inside it, so the writer iterates the layout until the table size converges (typically 2 iterations), then emits the file with all offsets aligned to 4 bytes.
GczWriteris independent of this pipeline (Dolphin:ConvertToGCZ): the image is read in power-of-two blocks, each block is deflated (stored raw when deflate does not fit or saves fewer than 10 bytes), the per-block Adler-32 covers the stored bytes, and the header/block tables are written last through a backward seek.CisoWriter,WbfsWriterandTgcWriterare independent too: CISO writes only non-zero blocks and fills the presence map with a backward seek; WBFS maps each disc cluster to a volume cluster and points every all-zero cluster at one shared zero cluster; TGC copies the GameCube ISO verbatim after a header carrying the relocated DOL/FST offsets.
Verification
DiscVerifier.Verify (Dolphin: VolumeVerifier) parses each partition header, validates
the TMD structure and the H3 table against the TMD content hash, then walks every data
sector: it decrypts the hash area (zero IV) and data (IV = ciphertext at 0x3D0) with the
plaintext partition key (WiiVolume.GetPartitions, which prefers the container’s key and
otherwise common-key decrypts the ticket’s title key), recomputes h0 from the 31 data
blocks, compares the embedded h1/h2 slots and the h3 entry, and reports every mismatch with
a severity. GameCube discs have no hash trees and are reported valid.
Design decisions
| Decision | Rationale |
|---|---|
| Dolphin C++ is the layout truth | References/dolphin-master/ — the RVZ/WIA formats were invented there; the Go reader and docs/WiaAndRvz.md are cross-checks |
| Canonical ISO view for all formats | one consumer (CLI, writer) works for every container |
GCZ uses BCL ZLibStream (read) and SharpZipLib Deflater (write) |
GCZ is deflate; SharpZipLib is already a dependency for bzip2, and Deflater can be reset per block like Dolphin’s deflateReset |
| Parallel decode serializes only file reads | RVZ/WIA chunks and partition regions are independent; workers read stored bytes under a lock (short I/O) and decompress from memory in parallel, then write in disc order — byte-identical output, near-linear speedup for LZMA/LZMA2 |
| FST offsets are read from the decrypted view | Dolphin’s GetFSTOffset is a partition-relative (decrypted) read; PartitionReader provides that view for the parser, the writer’s split point and extract |
| LZMA decoder is vendored 7-Zip | compact, self-contained, no external native code |
| LZMA-SDK for encoding | pure-managed public-domain encoder (runtime dependency, version 22.1.1) |
| Writer stores partitions decrypted + exceptions | the defining RVZ space optimization; identical to Dolphin |
| Junk packing is best-effort | GetSeed fails cleanly on non-PRNG data and the writer falls back to literal bytes — output stays valid |
| Chunk sizes per Dolphin | powers of two from 32 KiB to 2 MiB, or multiples of 2 MiB above that |
| PURGE only in WIA output | PURGE is a WIA-only method (WiaWriter); RVZ output rejects it and RVZ readers reject PURGE containers |
Format-version handling
WiaFileHead carries version (written 0x01000000) and version_compatible
(0x00030000 for RVZ, 0x00080000 for WIA). The reader accepts a file when
ImplementedVersion >= VersionCompatible, so newer readers can open older files and
vice versa.