Skip to content

usage library

github-actions[bot] edited this page Oct 9, 2026 · 2 revisions

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 (ArgumentException otherwise).
  • The filePath argument is consulted for NFS (locating code/htk.bin and the hif_00000X.nfs continuation 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's SplitPlainFileReader; a zero-size continuation falls back to the first part). NFS files must be named hif_000000.nfs and live in a directory named content; use the key overload to bypass the on-disk lookup.
  • leaveOpen controls whether disposing the reader also disposes the stream.
  • What Blob.Open accepts and rejects: a file that starts with a recognized container magic is always parsed as that container; a parse failure throws an RvzException subclass (RvzFormatException for structural damage, RvzUnsupportedException for newer versions, RvzHashMismatchException for failed integrity checks). Only files with no recognizable magic fall back to PlainBlob (an arbitrary file then opens successfully and ReadAt serves 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 in RvzWriter.Write is 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();
  • ReadAt returns the number of bytes actually read; it reads fewer bytes only at the end of the image. Reads are not required to be sequential.
  • BlockSize is 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).
  • CopyTo streams the whole image into any Stream in 1 MiB blocks — no 2 GiB limit — reporting progress and observing cancellation. RvzReader exposes 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
  • Find accepts a leading / and matches names case-insensitively (Dolphin behavior).
  • Entry offsets are partition-relative; CopyFileTo reads 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 RvzFormatException at Open.
  • PartitionReader (public, IBlobReader) exposes any Wii partition's decrypted bytes directly; it decrypts sectors on demand and is also what WiiVolume.GetFstOffset/ GetFstSize use internally.
  • Partition.Key is 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 A3 at offset 0x18, GameCube C2 33 9F 3D at offset 0x1C, matching Dolphin's TryCreateDisc). Anything else throws RvzFormatException before a single byte is written, so arbitrary data can never be wrapped into an unusable RVZ. This also means Blob.Open + RvzWriter.Write is a complete "is this a real disc?" pipeline for any input format; to run the same check up front, call Blob.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 of MaxThreads. 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.");
}
  • progress receives a fraction in [0, 1], reported per read; the sequence is monotonic and ends at 1.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 High issue.
  • The walk is CPU-bound (AES + SHA-1 per sector) but never materializes the image; it is sequential and observes cancellation between sectors.
  • RVZSharp.Cli exposes it as verify --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.Zstd is rejected (RvzUnsupportedException) — WIA predates it.
  • CompressionType.Purge is WIA-only and fully supported by the library (the CLI mirrors DolphinTool, whose -c choices do not include it).
  • RvzWriteOptions.Packing is RVZ-only and ignored for WIA.
  • The chunk size must be a multiple of 2 MiB (Dolphin's IsDiscImageBlockSizeValid).
  • options defaults to RvzWriteOptions.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 FileStream or MemoryStream works; 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.Scrub behave like the RVZ/GCZ Scrub option; the CLI applies --scrub before 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);
  • props is the codec property blob stored in the disc struct's compr_data field (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, plus IsWiiDisc / 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's TicketReader::GetTitleKey.
  • PartitionRegionBuilder(key) — rebuilds one encrypted 2 MiB region from decrypted payload + hash exceptions; used by RvzReader.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 decrypted IBlobReader view of one partition (0 = data-area start); decrypts sectors on demand and powers DiscFileSystem and 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.ReadAt is 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 IBlobReader implementations 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 by EnablePackageValidation at 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 LICENSE and THIRD-PARTY-NOTICES.md in the package.

Clone this wiki locally