-
Notifications
You must be signed in to change notification settings - Fork 0
usage library
RVZSharp is a pure managed library (no native code) for .NET 8, .NET 9 and .NET 10.
It reads and writes Dolphin RVZ disc images, reads WIA and the legacy GameCube/Wii
formats (GCZ, 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, RvzWriteOptions
|
RVZSharp.Models |
container structs: WiaFileHead, WiaDisc, WiaPartEntry, GroupEntry, CompressionType
|
RVZSharp.Chunks |
ChunkDecoder, HashExceptionEntry, 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
|
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.
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 |
// 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) and for split WBFS images (game.wbfs+game.wbf1…parts are opened like Dolphin; the declared size is checked against the sum of the parts). NFS files must be namedhif_000000.nfsand live in a directory namedcontent; use the key overload to bypass the on-disk lookup.
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.
-
leaveOpencontrols whether disposing the reader also disposes the stream. - Any file that is not a recognized container is opened as a plain ISO — opening arbitrary files succeeds, reads just serve the file bytes.
All readers implement IDisposable. Dispose them to release the underlying stream; the
readers themselves hold no other unmanaged resources.
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)
}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.
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()is a default interface method: it works on every reader and is overridden where a faster path exists (RvzReader).
GameCube/Wii images are 0.5–9.4 GiB. To avoid holding the whole image in memory, stream it:
using var blob = Blob.Open(path);
using var output = File.Create(@"C:\games\out.iso");
var buffer = new byte[1 << 20];
long position = 0;
while (position < blob.Length)
{
int read = blob.ReadAt(position, buffer);
if (read <= 0)
{
throw new IOException($"Decoding stopped at 0x{position:X}.");
}
output.Write(buffer, 0, read);
position += read;
}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 tableOpening 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.
RvzWriter.Write(IBlobReader input, Stream output, RvzWriteOptions? options = null,
IProgress<double>? progress = null, CancellationToken cancellationToken = default);The writer mirrors Dolphin's converter:
- 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).
| Member | Default | Notes |
|---|---|---|
Compression |
Zstd |
None, Bzip2, Lzma, Lzma2, Zstd. Purge throws RvzUnsupportedException (WIA-only). |
CompressionLevel |
3 |
Bzip2/Lzma/Lzma2: 1–9. Zstd: −131072..22 (negative levels = fast modes, 0 = 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). |
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.
Long conversions (multi-GiB images) can be monitored and canceled:
using var cts = new CancellationTokenSource();
var progress = new Progress<double>(fraction =>
Console.Error.Write($"\rconverting… {fraction,6:P1}"));
try
{
RvzWriter.Write(blob, output, options,
progress: progress, cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("\ncanceled.");
}-
progressreceives a fraction in[0, 1]of the input bytes processed, reported per group read; the sequence is monotonic and ends at1.0. - Cancellation is observed between reads and throws
OperationCanceledException.
Re-open what you wrote and compare hashes:
using var check = Blob.Open(outputPath);
using var sha1 = System.Security.Cryptography.SHA1.Create();
// stream check.ReadAt(...) through sha1 and compare with the source image's hashRead 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(decode only, WIA),Bzip2,LZMA,LZMA2,Zstd.
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.
These types back the partition optimization:
-
WiiVolume— disc-header parsing, partition-table discovery, ticket reading (title key at ticket + 0x1BF), FST offsets,IsWiiDisc/HasWiiHashes/HasWiiEncryption. -
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.
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)}");
}| 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.
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.
-
IBlobReaderinstances are not thread-safe:ReadAtmutates internal caches (RvzReadercaches the last raw chunk and partition region). Use one reader per thread, or synchronize access. -
RvzWriter.Writeis single-threaded by design; progress reports are raised on the calling thread.
- 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.