Library usage guide
RVZSharp is a pure managed library (no native code) for .NET 8, .NET 9 and .NET 10.
It reads and writes Dolphin RVZ and WIA disc images, writes the legacy GCZ format,
reads the remaining legacy GameCube/Wii formats (CISO/WBI, WBFS, TGC, NFS), and exposes
every format through one interface that serves the original disc bytes.
Install-Package RVZSharp # Package Manager
dotnet add package RVZSharp # .NET CLI
All public types live in the RVZSharp assembly; the main namespaces are:
| Namespace | Contents |
|---|---|
RVZSharp.Blobs |
IBlobReader, Blob (factory), BlobType, per-format readers |
RVZSharp |
RvzReader, RvzWriter, WiaWriter, GczWriter, CisoWriter, WbfsWriter, TgcWriter, DiscHasher, BlobStream, RvzWriteOptions, GczWriteOptions |
RVZSharp.Models |
models: DiscInfo, DiscFileInfo, container structs (WiaFileHead, WiaDisc, WiaPartEntry, GroupEntry, HashExceptionEntry, DiscHashes), options records and enums (CompressionType, BlobType, DiscType, WiaRvzFormat) |
RVZSharp.Chunks |
ChunkDecoder, ExceptionListParser |
RVZSharp.Compression |
codec factories: CompressionCodecFactory, CompressionEncoderFactory (the vendored 7-Zip LZMA port is internal) |
RVZSharp.IO |
Adler32, SpanReader, SectionStream, NonDisposingStream |
RVZSharp.Packing |
RvzPackingDecoder, RvzPackingEncoder, LaggedFibonacciGenerator |
RVZSharp.Wii |
PartitionRegionBuilder, WiiHashCalculator, WiiVolume, WiiPartitionExtractor, PartitionReader |
RVZSharp.Files |
DiscFileSystem (FST parsing and file extraction; the DiscFileInfo tree nodes live in RVZSharp.Models) |
RVZSharp.Verification |
DiscVerifier and the VerificationReport/VerificationIssue/PartitionVerification/VerificationSeverity models |
Quickstart
using RVZSharp;
using RVZSharp.Blobs;
using RVZSharp.Models;
// 1. Open any disc image (format is auto-detected from the magic bytes).
using var blob = Blob.Open(@"C:\games\my-game.rvz");
// 2. Read the whole disc as ISO bytes.
byte[] iso = blob.ReadFully(); // fine for small images
// 3. Or convert it to RVZ with a different codec.
using var output = File.Create(@"C:\games\my-game-v2.rvz");
RvzWriter.Write(blob, output, new RvzWriteOptions
{
Compression = CompressionType.Zstd,
CompressionLevel = 5,
ChunkSize = 0x200000, // 2 MiB
});
That is the entire core API: open anything, read ISO bytes, write RVZ.
Opening disc images
Everything starts with Blob.Open, which sniffs the first four bytes and returns the right
reader:
| Magic bytes | Format | Reader type |
|---|---|---|
52 56 5A 01 (RVZ\x01) |
RVZ | RvzReader |
57 49 41 01 (WIA\x01) |
WIA | RvzReader |
43 49 53 4F (CISO) |
CISO/WBI | CisoBlob |
01 C0 0B B1 |
GCZ | GczBlob |
57 42 46 53 (WBFS) |
WBFS | WbfsBlob |
45 47 47 53 (EGGS) |
NFS | NfsBlob |
AE 0F 38 A2 |
TGC | TgcBlob |
| anything else | plain ISO | PlainBlob |
Overloads
// From a path (the reader owns and closes the file stream).
using IBlobReader blob = Blob.Open(@"C:\games\game.gcz");
// From an existing stream.
using var stream = File.OpenRead(@"C:\games\game.iso");
using IBlobReader blob = Blob.Open(stream, filePath: @"C:\games\game.iso", leaveOpen: false);
// NFS with an explicit AES key (bypasses the code/htk.bin lookup).
byte[] key = ReadKeyFromSomewhere(); // 16 bytes
using IBlobReader blob = Blob.Open(@"C:\content\hif_000000.nfs", key);
Notes:
Blob.Open(Stream, ...)requires a seekable stream (ArgumentExceptionotherwise).- The
filePathargument is consulted for NFS (locatingcode/htk.binand thehif_00000X.nfscontinuation files), for split WBFS images (game.wbfs+game.wbf1…parts are opened like Dolphin; the declared size is checked against the sum of the parts) and for split plain ISOs (game.part0.iso+game.part1.iso…, concatenated like Dolphin’sSplitPlainFileReader; a zero-size continuation falls back to the first part). NFS files must be namedhif_000000.nfsand live in a directory namedcontent; use the key overload to bypass the on-disk lookup. leaveOpencontrols whether disposing the reader also disposes the stream.- What
Blob.Openaccepts and rejects: a file that starts with a recognized container magic is always parsed as that container; a parse failure throws anRvzExceptionsubclass (RvzFormatExceptionfor structural damage,RvzUnsupportedExceptionfor newer versions,RvzHashMismatchExceptionfor failed integrity checks). Only files with no recognizable magic fall back toPlainBlob(an arbitrary file then opens successfully andReadAtserves its bytes — whether it is a real disc is a separate question the writer answers). CISO is validated lazily like Dolphin: a header with a plausible block size opens even if the payload is garbage (absent blocks decode to zeroes); the disc-header validation inRvzWriter.Writeis what keeps such garbage from being wrapped into an RVZ.
Is it a real disc?
Container sniffing only recognizes the file format; a plain file that is not a disc
opens as PlainBlob too. To ask “is this actually a GameCube/Wii disc?”, inspect the
decoded disc header (Wii magic at 0x18, GameCube magic at 0x1C — the same check
Dolphin’s TryCreateDisc and RvzWriter.Write perform):
using IBlobReader blob = Blob.Open(path);
if (!Blob.IsDisc(blob)) // false for arbitrary data
throw new InvalidDataException("not a GameCube/Wii disc");
DiscType type = Blob.GetDiscType(blob); // DiscType.Wii / DiscType.GameCube / DiscType.Unknown
Blob.GetDiscType / Blob.IsDisc read the decoded bytes, so they work for every input
format (plain ISO or any container). RvzWriter.Write rejects inputs that fail this check
with RvzFormatException before writing a byte.
Disc metadata
DiscInfo.TryRead(blob) reads the volume metadata from the decoded disc header (Dolphin:
VolumeDisc); DiscInfo.Read throws RvzFormatException for non-discs:
var info = DiscInfo.TryRead(blob);
if (info is not null)
{
Console.WriteLine($"{info.GameId} {info.MakerId} rev {info.Revision}");
Console.WriteLine($"{info.InternalName} ({info.Region}, {info.Country})");
if (info.TitleId is ulong titleId)
Console.WriteLine($"title 0x{titleId:X16}");
}
| Property | Meaning |
|---|---|
DiscType |
GameCube / Wii (from the header magic) |
GameId / MakerId |
6- and 2-character IDs; non-alphanumeric bytes become - |
Revision |
revision byte (header offset 7) |
InternalName |
name at 0x20 (CP1252, or Shift-JIS for NTSC-J) |
Region |
NTSC-J, NTSC-U, PAL, NTSC-K or Unknown |
Country |
Japan, USA, Europe, … (with Dolphin’s region fallback) |
CountryCode |
raw country byte (header offset 3) |
TitleId |
Wii title ID from the game partition’s ticket, or null |
As a Stream
BlobStream exposes any blob as a read-only, seekable Stream of any size:
using var blob = Blob.Open(path);
using var stream = new BlobStream(blob); // leaveOpen defaults to false
var reader = new BinaryReader(stream);
Scrubbing
ScrubbedBlob.Create(blob) wraps any disc image and zeroes the data of every non-game Wii
partition (update/channel) — the safe subset of Dolphin’s DiscScrubber that needs no
filesystem (FST) parser. It returns null for discs that cannot be scrubbed (non-Wii, or
no game partition). The CLI’s convert -s/--scrub uses it, and the writers expose the same
behavior through RvzWriteOptions.Scrub / GczWriteOptions.Scrub (ignored for GameCube
discs and Wii images without a game partition).
Lifetime
All readers implement IDisposable. Dispose them to release the underlying stream; the
readers themselves hold no other unmanaged resources.
The IBlobReader contract
public interface IBlobReader : IDisposable
{
BlobType Type { get; } // detected format
long Length { get; } // decoded ISO size in bytes
int BlockSize { get; } // natural block size, 0 when the format has none
int ReadAt(long position, Span<byte> buffer);
byte[] ReadFully(); // default implementation (streams via ReadAt, ≤ 2 GiB)
byte[] ReadFully(IProgress<double>? progress, CancellationToken cancellationToken = default);
long CopyTo(Stream destination, IProgress<double>? progress = null,
CancellationToken cancellationToken = default);
long CopyTo(Stream destination, IProgress<double>? progress, int maxThreads,
CancellationToken cancellationToken = default);
Task<byte[]> ReadFullyAsync(IProgress<double>? progress = null, CancellationToken cancellationToken = default);
Task<long> CopyToAsync(Stream destination, IProgress<double>? progress = null,
CancellationToken cancellationToken = default);
Task<long> CopyToAsync(Stream destination, IProgress<double>? progress, int maxThreads,
CancellationToken cancellationToken = default);
}
Every format — RVZ, GCZ, WBFS, a raw ISO — decodes to the same thing: the original disc image bytes, randomly accessible.
using var blob = Blob.Open(path);
// Random access: read one 0x8000-byte sector.
Span<byte> sector = stackalloc byte[0x8000];
blob.ReadAt(0x1234 * 0x8000, sector);
// Whole-image decode (images up to 2 GiB).
byte[] iso = blob.ReadFully();
ReadAtreturns the number of bytes actually read; it reads fewer bytes only at the end of the image. Reads are not required to be sequential.BlockSizeis the format’s natural block (GCZ: 0x4000, RVZ: chunk size, CISO: 0x8000, WBFS: cluster size, NFS: 0x8000, TGC/plain: 0).ReadFully()/ReadFully(progress, ct)are default interface methods: they work on every reader and are overridden where a faster path exists (RvzReader).CopyTostreams the whole image into anyStreamin 1 MiB blocks — no 2 GiB limit — reporting progress and observing cancellation.RvzReaderexposes the same method directly, so no cast is needed.
Streaming pattern (large images)
GameCube/Wii images are 0.5–9.4 GiB. To avoid holding the whole image in memory, stream it
with CopyTo:
using var blob = Blob.Open(path);
using var output = File.Create(@"C:\games\out.iso");
var progress = new Progress<double>(f => Console.Error.Write($"\r{f,6:P1}"));
blob.CopyTo(output, progress);
For explicit random-access control, the same loop can be written by hand with ReadAt
(1 MiB at a time, as CopyTo does internally).
Parallel and async decoding
CopyTo(..., maxThreads) decodes RVZ/WIA chunks on a worker pool (raw chunks and 64-sector
partition regions are independent units; only the short file reads are serialized, so
LZMA/LZMA2 decompression runs in parallel). Results are written in disc order, so the output
is byte-identical for any thread count. Other formats fall back to the sequential default
implementation. 0 uses the processor count, 1 forces sequential.
blob.CopyTo(output, progress, maxThreads: 0); // parallel where supported
The ...Async forms run the synchronous, CPU-bound decoders on the thread pool and return
a task; progress and cancellation behave exactly like the synchronous methods.
byte[] iso = await blob.ReadFullyAsync(progress, cancellationToken);
await blob.CopyToAsync(output, progress, maxThreads: 0, cancellationToken);
Reading RVZ / WIA in detail
RvzReader exposes the container metadata plus the decoded bytes:
using var file = File.OpenRead("game.rvz");
using var reader = RvzReader.Open(file, leaveOpen: true); // RVZ
using var reader = RvzReader.OpenWia(file, leaveOpen: true); // WIA
reader.IsWia; // true for WIA containers
reader.Length; // decoded ISO size
reader.BlockSize; // the chunk size
reader.FileHead; // WiaFileHead: magic, versions, SHA-1s, sizes
reader.Disc; // WiaDisc: disc type, codec, chunk size, table locations
reader.Partitions; // WiaPartEntry[]: key + two data ranges per partition
reader.RawDataEntries; // WiaRawDataEntry[]: raw data ranges
reader.GroupEntries; // GroupEntry[]: group table
Opening validates the whole container: magic, versions, the file-head and disc-struct
SHA-1s, the partition/raw/group table hashes, and structural rules (chunk size, group
coverage). A damaged file throws RvzHashMismatchException or RvzFormatException at
Open, never later.
WIA files (read-compatible version 0x00080000) support the PURGE codec and lack Zstd and
packing; RvzReader handles both formats transparently.
Reading the file system (FST)
DiscFileSystem parses the GameCube/Wii file system table (Dolphin: FileSystemGCWii) and
gives you the tree plus file data reads. GameCube discs open directly; Wii partitions open
through a decrypted PartitionReader view, so paths and offsets match Dolphin.
using RVZSharp.Files;
using RVZSharp.Wii;
// GameCube (or any disc whose data starts at offset 0):
using var fs = DiscFileSystem.Open(blob);
// Wii: pick a partition (WiiVolume.GetPartitions, type 0 = DATA/game):
using var fs = DiscFileSystem.Open(blob, WiiVolume.GetPartitions(blob)[0]);
var file = fs.Find("files/maps/foo.dat"); // case-insensitive, '/' separators
foreach (var child in fs.Root.Children) { } // Name, Path, IsDirectory, Size, Offset
using var output = File.Create("foo.dat");
fs.CopyFileTo(file, output); // streams the decrypted file bytes
Findaccepts a leading/and matches names case-insensitively (Dolphin behavior).- Entry offsets are partition-relative;
CopyFileToreads through the same decrypted view, so no manual sector math is needed. - Invalid file systems (no magic, truncated table, impossible entry ranges, no trailing NUL)
throw
RvzFormatExceptionatOpen. PartitionReader(public,IBlobReader) exposes any Wii partition’s decrypted bytes directly; it decrypts sectors on demand and is also whatWiiVolume.GetFstOffset/GetFstSizeuse internally.Partition.Keyis the plaintext partition key: for RVZ/WIA inputs it is the container’s authoritative partition-table key (which wins even when the disc ticket was re-signed), and for plain ISOs it is the ticket title key decrypted with the Wii common key (WiiVolume.GetTitleKey).
Writing RVZ
RvzWriter.Write(IBlobReader input, Stream output, RvzWriteOptions? options = null,
IProgress<double>? progress = null, CancellationToken cancellationToken = default);
The writer mirrors Dolphin’s converter:
- Input validation: the input must actually be a GameCube or Wii disc image — the
decoded bytes must carry the disc header magic (Wii
5D 1C 9E A3at offset0x18, GameCubeC2 33 9F 3Dat offset0x1C, matching Dolphin’sTryCreateDisc). Anything else throwsRvzFormatExceptionbefore a single byte is written, so arbitrary data can never be wrapped into an unusable RVZ. This also meansBlob.Open+RvzWriter.Writeis a complete “is this a real disc?” pipeline for any input format; to run the same check up front, callBlob.GetDiscType/Blob.IsDisc. - Wii discs (disc header magic + hash/encryption flags set) are stored with the partition optimization: partition data is written decrypted with SHA-1 hash exceptions, split at the FST end — this is what makes RVZ small.
- GameCube discs (and Wii discs without hashes/encryption) are stored as raw data.
- PRNG junk (the pseudo-random padding in Wii data) is detected and packed with a recovered Lagged-Fibonacci seed.
- All tables carry SHA-1 checksums; the output is fully self-describing and validated by any conforming reader (including Dolphin).
- Parallel compression: groups are packed and compressed on a bounded worker pool (one
encoder per worker, like Dolphin’s
MultithreadedCompressor) and appended in group order, so the output is byte-identical regardless ofMaxThreads. Only compressed groups are retained, so memory stays bounded by the thread count, not the disc size.
RvzWriteOptions
| Member | Default | Notes |
|---|---|---|
Compression |
Zstd |
None, Bzip2, Lzma, Lzma2, Zstd. Purge throws RvzUnsupportedException (use WiaWriter for PURGE). |
CompressionLevel |
5 |
Bzip2/Lzma/Lzma2: 1–9. Zstd: −131072..22 (negative levels = fast modes, 0 = default). Dolphin’s converter default. |
ChunkSize |
0x200000 |
Power of two between 0x8000 (32 KiB) and 0x200000 (2 MiB), or a multiple of 0x200000 above that (Dolphin’s rule). |
Packing |
true |
Set false to store junk literally (larger file, no packing overhead). |
Scrub |
false |
Zero the data of non-game Wii partitions before encoding (Dolphin’s DiscScrubber). No-op for GameCube discs. |
MaxThreads |
0 |
Compression/packing worker count; 0 uses the processor count. Output bytes do not depend on this value. |
var options = new RvzWriteOptions
{
Compression = CompressionType.Lzma2, // best ratio on compressible data
CompressionLevel = 9,
ChunkSize = 0x10000, // 64 KiB chunks
Packing = true,
};
The writer does not dispose the output stream; the caller owns it.
Progress and cancellation
Long conversions (multi-GiB images) can be monitored and canceled on both the write and the decode path:
using var cts = new CancellationTokenSource();
var progress = new Progress<double>(fraction =>
Console.Error.Write($"\r{fraction,6:P1}"));
try
{
// Encode: progress is the fraction of input bytes processed.
RvzWriter.Write(blob, output, options,
progress: progress, cancellationToken: cts.Token);
// Decode/stream: progress is the fraction of image bytes decoded.
blob.CopyTo(isoStream, progress, cts.Token);
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("\ncanceled.");
}
progressreceives a fraction in[0, 1], reported per read; the sequence is monotonic and ends at1.0.- Cancellation is observed between reads on both paths and throws
OperationCanceledException.
Verifying your output
DiscHasher computes the CRC-32, MD5 and SHA-1 of the decoded image in one streaming pass
(the same digests Dolphin’s volume verifier reports):
using var check = Blob.Open(outputPath);
DiscHashes hashes = DiscHasher.Compute(check, progress);
// Compare with the source image's hashes:
using var source = Blob.Open(inputPath);
var expected = DiscHasher.Compute(source);
if (!hashes.Sha1.AsSpan().SequenceEqual(expected.Sha1))
throw new InvalidDataException("round-trip mismatch");
For RVZ/WIA the container’s own integrity (all SHA-1s, structure rules) is already
validated by RvzReader.Open / RvzReader.OpenWia, so DiscHasher only needs to hash the
decoded bytes.
Verifying Wii partitions
DiscVerifier.Verify mirrors Dolphin’s verify tab: it enumerates the disc’s Wii partitions,
checks each partition header, TMD structure and H3 table, and walks every data sector’s
h0/h1/h2/h3 hash tree (decrypting sectors with the plaintext partition key derived by
WiiVolume.GetPartitions). Problems are reported per partition with a severity; only
VerificationSeverity.High problems (corrupt data) make a report invalid.
using RVZSharp.Verification;
VerificationReport report = DiscVerifier.Verify(blob, progress);
foreach (var partition in report.Partitions)
{
Console.WriteLine($"{partition.Name}: {partition.VerifiedBlocks} blocks, "
+ $"{partition.FailedBlocks} failed, tmd={partition.TmdValid}, h3={partition.H3TableValid}");
}
if (!report.IsValid)
{
foreach (var issue in report.Issues.Concat(report.Partitions.SelectMany(p => p.Issues)))
Console.Error.WriteLine($"[{issue.Severity}] {issue.Message}");
}
- GameCube discs have no hash trees and are always valid; a non-disc image is a single
Highissue. - The walk is CPU-bound (AES + SHA-1 per sector) but never materializes the image; it is sequential and observes cancellation between sectors.
RVZSharp.Cliexposes it asverify --partitions.
Writing WIA
WiaWriter shares the writer core with RvzWriter (Dolphin: ConvertToWIAOrRVZ):
using var input = Blob.Open(@"C:\games\game.iso");
using var output = File.Create(@"C:\games\game.wia");
WiaWriter.Write(input, output, new RvzWriteOptions
{
Compression = CompressionType.Lzma2, // None, Purge, Bzip2, Lzma, Lzma2
CompressionLevel = 5,
ChunkSize = 0x200000, // multiple of 2 MiB
});
CompressionType.Zstdis rejected (RvzUnsupportedException) — WIA predates it.CompressionType.Purgeis WIA-only and fully supported by the library (the CLI mirrors DolphinTool, whose-cchoices do not include it).RvzWriteOptions.Packingis RVZ-only and ignored for WIA.- The chunk size must be a multiple of 2 MiB (Dolphin’s
IsDiscImageBlockSizeValid). optionsdefaults toRvzWriteOptions.WiaDefault(LZMA2, level 5, 2 MiB chunks).- Input validation, progress and cancellation behave exactly like
RvzWriter.Write.
Writing GCZ
GczWriter mirrors Dolphin’s ConvertToGCZ: the image is split into blocks, each block is
deflated (level 9) and stored compressed unless that saves fewer than 10 bytes, and the block
table carries a per-block Adler-32 of the stored bytes. GCZ has no compression choice — it is
always zlib deflate.
using var input = Blob.Open(@"C:\games\game.iso");
using var output = File.Create(@"C:\games\game.gcz");
GczWriter.Write(input, output, new GczWriteOptions
{
BlockSize = 0x4000, // power of two; 16 KiB classic, Dolphin's GUI defaults to 128 KiB
MaxThreads = 0, // 0 = processor count; output is byte-identical for any value
});
- The output stream must be seekable: like Dolphin, the header and the block tables are
written after the block data (a
FileStreamorMemoryStreamworks; a network stream does not). - Input validation, progress and cancellation behave like
RvzWriter.Write; the Wii-disc GCZ-specific warning (“may not offer space advantages”) is a CLI concern. - Unlike WIA/RVZ, GCZ stores the image as-is (no partition decryption or packing): Wii discs compress poorly unless scrubbed first.
Writing CISO / WBFS / TGC
CisoWriter, WbfsWriter and TgcWriter complete the container matrix; all three accept
any input format (IBlobReader) or path and take an optional IProgress<double> and
cancellation token.
using var input = Blob.Open(@"C:\games\game.iso");
// CISO/WBI: a presence map + only the blocks that contain data.
CisoWriter.Write(input, ciso, new CisoWriteOptions
{
BlockSize = 0x200000, // power of two; the decoded image is BlockSize × 0x7FF8 bytes
Scrub = true, // zero non-game Wii partitions; all-zero blocks are not stored
});
// WBFS: Wii only. All-zero clusters share one zero-filled volume cluster.
WbfsWriter.Write(input, wbfs, new WbfsWriteOptions
{
BlockSize = 0x200000, // power of two ≥ 32 KiB; the u16 map caps the disc at 65535 clusters
});
// TGC: GameCube only. The ISO is stored verbatim after the 56-byte header.
TgcWriter.Write(input, tgc);
- CISO and WBFS output streams must be seekable (the map/table is written after the data); TGC output can be any stream.
- A CISO’s decoded length is always
BlockSize × 0x7FF8(the map capacity), so a small image decodes with a zero tail — exactly Dolphin’s semantics. - WBFS decodes to the fixed Wii double-layer size; clusters past the input are zero.
CisoWriteOptions.Scrub/WbfsWriteOptions.Scrubbehave like the RVZ/GCZScruboption; the CLI applies--scrubbefore calling the writers.
Compression codecs
Read side — decode any method found in a file:
ICompressionDecoder decoder = CompressionCodecFactory.Create(CompressionType.Lzma2);
using Stream stream = decoder.CreateDecompressor(file, props, inputSize, outputSize);
Write side — compress like the writer does:
var (encoder, props) = CompressionEncoderFactory.Create(CompressionType.Zstd, level: 5);
byte[] compressed = encoder.Compress(payload);
propsis the codec property blob stored in the disc struct’scompr_datafield (LZMA/LZMA2 dictionary settings; empty for the others).ICompressionEncoder.AddPrecedingData(...)is PURGE-specific (the exception lists that precede the compressed stream and are covered by its SHA-1 trailer).- Supported methods:
None,Purge(WIA),Bzip2,LZMA,LZMA2,Zstd(RVZ).
RVZ packing API
Packing is what makes RVZ files small: Wii junk is a Lagged-Fibonacci PRNG stream, so a 68-byte seed regenerates it. The writer detects junk and emits segment streams; the reader reconstructs it.
// Encode a chunk (returns the segment stream + its packed size).
var mainData = new List<byte>();
uint packedSize = 0;
RvzPackingEncoder.Pack(payload, dataOffset: 0, bytesPerChunk: payload.Length, chunks: 1,
allowJunkReuse: true, compression: true, mainData, ref packedSize);
// Decode a packed stream.
using var decoder = new RvzPackingDecoder(stream, dataOffset: 0);
decoder.Read(buffer, 0, buffer.Length);
LaggedFibonacciGenerator is public for advanced use — including seed recovery:
var (seed, bytesReconstructed) = LaggedFibonacciGenerator.GetSeed(
data, data.Length, dataOffset % LaggedFibonacciGenerator.BlockSize);
var lfg = new LaggedFibonacciGenerator();
lfg.SetSeed(seed);
lfg.ForwardBytes(dataOffset % LaggedFibonacciGenerator.BlockSize);
lfg.GetBytes(count, output);
In normal use you never touch these — they are needed only when implementing a custom container that embeds packed chunks.
Wii partition machinery
These types back the partition optimization:
WiiVolume— disc-header parsing, partition-table discovery, ticket handling and FST offsets, plusIsWiiDisc/HasWiiHashes/HasWiiEncryption.GetTitleKey(ticket)decrypts the 16-byte title key at ticket + 0x1BF with the console common key (IV = the title ID at 0x1DC; retail/Korean/RVT keys), exactly like Dolphin’sTicketReader::GetTitleKey.PartitionRegionBuilder(key)— rebuilds one encrypted 2 MiB region from decrypted payload + hash exceptions; used byRvzReader.ReadAt.Finish()returns the encrypted region bytes.WiiHashCalculator— SHA-1 hash-tree construction (h0/h1/h2) and hash-exception computation.WiiPartitionExtractor— writer side: reads encrypted regions from the input, decrypts them (AES-128-CBC), recomputes the hash tree, and diffs it against the original to produce the exception lists.PartitionReader— a decryptedIBlobReaderview of one partition (0 = data-area start); decrypts sectors on demand and powersDiscFileSystemand the FST offset reads.
foreach (var partition in WiiVolume.GetPartitions(blob))
{
Console.WriteLine($"partition @ 0x{partition.Offset:X}, type {partition.Type}, "
+ $"{partition.DataSize} data bytes, key {Convert.ToHexString(partition.Key)}");
}
Error handling
| Exception | Raised when |
|---|---|
RvzFormatException |
structural problems: truncated file, bad chunk size, unsupported method, missing disc coverage, decode stalls |
RvzHashMismatchException |
a SHA-1 (file head, disc struct, partition/raw/group tables) does not match |
RvzUnsupportedException |
a needed feature is not supported (e.g. PURGE inside RVZ, unknown codec) |
ArgumentException |
programmer errors: non-seekable stream, invalid chunk size |
OperationCanceledException |
RvzWriter.Write canceled via CancellationToken |
All Rvz*Exception types derive from RvzException (which derives from Exception), so
one catch covers the format-specific failures:
try
{
using var blob = Blob.Open(path);
byte[] iso = blob.ReadFully();
}
catch (RvzException e) // format + hash problems
{
Console.Error.WriteLine($"The image is damaged or unsupported: {e.Message}");
}
Hashing is verified eagerly: RVZ/WIA containers validate all checksums when opened, so
a corrupt file fails fast at Blob.Open, not halfway through your reads.
Recipes
Convert any file to RVZ with progress, then decode back:
using var blob = Blob.Open(inputPath);
using var output = File.Create(outputPath);
RvzWriter.Write(blob, output, new RvzWriteOptions { Compression = CompressionType.Zstd },
progress: new Progress<double>(p => Console.WriteLine($"{p:P1}")));
Read the game title and region without decoding the disc:
using var blob = Blob.Open(path);
Span<byte> header = stackalloc byte[0x80];
blob.ReadAt(0, header);
string gameId = System.Text.Encoding.ASCII.GetString(header[..6]);
List Wii partitions of any container:
using var blob = Blob.Open(path);
foreach (var partition in WiiVolume.GetPartitions(blob))
{
Console.WriteLine($"0x{partition.Offset:X8} type={partition.Type} "
+ $"{partition.DataSize} bytes");
}
Stream a 9.4 GiB WBFS to ISO without buffering: use the streaming pattern above —
WbfsBlob reports its fixed logical size and serves zero clusters without allocating them.
Thread safety
RvzReader.ReadAtis thread-safe: concurrent reads share decoded units through a bounded LRU cache (16 MiB,RvzReader.DefaultCacheSize) and only serialize the short file reads. Do not dispose the reader while another thread is reading.- The other
IBlobReaderimplementations read through a single stream and are not thread-safe; use one reader per thread or synchronize access. - The writers and the parallel decode path manage their own worker pools; progress reports are raised on the calling thread between groups/batches, and cancellation is observed at the same points.
Compatibility notes
- Target frameworks:
net8.0,net9.0,net10.0— the same assembly API on all three (enforced byEnablePackageValidationat pack time). - Dependencies:
LZMA-SDK(encoding),SharpZipLib(BZip2),ZstdSharp.Port(Zstandard) — all pure managed, all netstandard-compatible. - The RVZ/WIA container logic is derived from Dolphin; the library is
GPL-2.0-or-later (the same license Dolphin uses), so distributing or consuming it
follows the GPL. See
LICENSEandTHIRD-PARTY-NOTICES.mdin the package.