diff --git a/SabreTools.Data.Extensions/NintendoDiscExtensions.cs b/SabreTools.Data.Extensions/NintendoDiscExtensions.cs
new file mode 100644
index 00000000..f238808d
--- /dev/null
+++ b/SabreTools.Data.Extensions/NintendoDiscExtensions.cs
@@ -0,0 +1,30 @@
+using SabreTools.Data.Models.NintendoDisc;
+
+namespace SabreTools.Data.Extensions
+{
+ // TODO: Write tests for these
+ public static class NintendoDiscExtensions
+ {
+ ///
+ /// Get the platform associated with a disc header
+ ///
+ public static Platform GetPlatform(this DiscHeader header)
+ {
+ if (header.WiiMagic == Constants.WiiMagicWord)
+ return Platform.Wii;
+ else if (header.GCMagic == Constants.GCMagicWord)
+ return Platform.GameCube;
+ else if (header.GameId is not null && header.GameId.Length >= 1 && IsGameCubeTitleType(header.GameId[0]))
+ return Platform.GameCube;
+ else
+ return Platform.Unknown;
+ }
+
+ ///
+ /// Returns true if the GameId first character is a known GameCube title type prefix.
+ /// Used as a fallback when the GC magic word is absent from the disc image.
+ ///
+ public static bool IsGameCubeTitleType(this char c)
+ => c == 'G' || c == 'D' || c == 'R';
+ }
+}
diff --git a/SabreTools.Data.Models/GCZ/Archive.cs b/SabreTools.Data.Models/GCZ/Archive.cs
index c61a63ac..ff707fab 100644
--- a/SabreTools.Data.Models/GCZ/Archive.cs
+++ b/SabreTools.Data.Models/GCZ/Archive.cs
@@ -17,10 +17,10 @@ namespace SabreTools.Data.Models.GCZ
/// Each value encodes both the offset of the block within the compressed data section
/// and a compression flag in the top bit:
///
- /// - Top bit CLEAR → block is zlib/deflate-compressed at that offset.
- /// - Top bit SET → block is stored uncompressed at that offset.
+ /// - Top bit CLEAR -> block is zlib/deflate-compressed at that offset.
+ /// - Top bit SET -> block is stored uncompressed at that offset.
///
- /// Offset is value & ~UncompressedFlag.
+ /// Offset is value & ~UncompressedFlag.
///
public ulong[] BlockPointers { get; set; } = [];
diff --git a/SabreTools.Data.Models/MicrosoftCabinet/CFFILE.cs b/SabreTools.Data.Models/MicrosoftCabinet/CFFILE.cs
index c91566f7..58280ed6 100644
--- a/SabreTools.Data.Models/MicrosoftCabinet/CFFILE.cs
+++ b/SabreTools.Data.Models/MicrosoftCabinet/CFFILE.cs
@@ -30,7 +30,7 @@
public FolderIndex FolderIndex { get; set; }
///
- /// Date of this file, in the format ((year–1980) << 9)+(month << 5)+(day), where
+ /// Date of this file, in the format ((year-1980) << 9)+(month << 5)+(day), where
/// month={1..12} and day = { 1..31 }. This "date" is typically considered the "last modified" date in local
/// time, but the actual definition is application-defined.
///
diff --git a/SabreTools.Data.Models/NintendoDisc/Constants.cs b/SabreTools.Data.Models/NintendoDisc/Constants.cs
index 0550c902..d7dae3b2 100644
--- a/SabreTools.Data.Models/NintendoDisc/Constants.cs
+++ b/SabreTools.Data.Models/NintendoDisc/Constants.cs
@@ -11,47 +11,8 @@ namespace SabreTools.Data.Models.NintendoDisc
public const uint WiiMagicWord = 0x5D1C9EA3;
#endregion
-
- #region Disc header layout
-
- // Offsets within the 0x440-byte boot block.
- // Layout confirmed against Dolphin source (VolumeDisc.cpp / DiscUtils.h):
- // 0x000–0x003 Title code (4 chars, e.g. "GAFE")
- // 0x004–0x005 Maker code (2 chars, e.g. "01") — Dolphin Read(0x4, 2)
- // 0x006 Disc number — Dolphin GetDiscNumber() Read(6)
- // 0x007 Revision — Dolphin GetRevision() Read(7)
- // 0x008 Audio streaming
- // 0x009 Streaming buffer size
- // 0x00A–0x017 Unused (14 bytes)
- // 0x018 Wii magic (0x5D1C9EA3)
- // 0x01C GC magic (0xC2339F3D)
- // 0x020–0x07F Game title (0x60 bytes)
- // 0x080 Disable hash verification
- // 0x081 Disable disc encryption
- public const int TitleCodeOffset = 0x000;
- public const int TitleCodeLength = 4;
- public const int MakerCodeOffset = 0x004;
- public const int MakerCodeLength = 2;
- /// Full 6-char game ID = TitleCode[4] + MakerCode[2]
- public const int GameIdOffset = 0x000;
- public const int GameIdLength = 6;
- public const int DiscNumberOffset = 0x006;
- public const int DiscVersionOffset = 0x007;
- public const int AudioStreamingOffset = 0x008;
- public const int StreamingBufferSizeOffset = 0x009;
- public const int WiiMagicOffset = 0x018;
- public const int GCMagicOffset = 0x01C;
- public const int GameTitleOffset = 0x020;
- public const int GameTitleLength = 0x060;
- public const int DisableHashVerificationOffset = 0x080;
- public const int DisableDiscEncryptionOffset = 0x081;
- public const int DolOffsetField = 0x420;
- public const int FstOffsetField = 0x424;
- public const int FstSizeField = 0x428;
public const int DiscHeaderSize = 0x440;
- #endregion
-
#region BI2 data
public const int Bi2Address = 0x000440;
@@ -97,6 +58,7 @@ namespace SabreTools.Data.Models.NintendoDisc
public const int WiiBlockHeaderSize = 0x0400;
public const int WiiBlockDataSize = 0x7C00;
public const int WiiBlocksPerGroup = 64;
+ public const int WiiBlockHashSize = 0x0400;
public const int WiiGroupSize = WiiBlocksPerGroup * WiiBlockSize;
public const int WiiGroupDataSize = WiiBlocksPerGroup * WiiBlockDataSize;
diff --git a/SabreTools.Data.Models/NintendoDisc/DOLHeader.cs b/SabreTools.Data.Models/NintendoDisc/DOLHeader.cs
new file mode 100644
index 00000000..f6968db4
--- /dev/null
+++ b/SabreTools.Data.Models/NintendoDisc/DOLHeader.cs
@@ -0,0 +1,64 @@
+namespace SabreTools.Data.Models.NintendoDisc
+{
+ ///
+ public class DOLHeader
+ {
+ ///
+ /// Section offsets indicate where each section's data begins relative
+ /// to the start of this header. 0 for unused sections.
+ ///
+ ///
+ /// Big-endian
+ /// Indexes 0-6 are text sections.
+ /// Indexes 7-17 are data sections.
+ ///
+ public uint[] SectionOffsetTable { get; set; } = new uint[18];
+
+ ///
+ /// Section address indicates where each section should be copied to by
+ /// the loader as a virtual memory address. 0 for unused sections.
+ ///
+ ///
+ /// Big-endian
+ /// Indexes 0-6 are text sections.
+ /// Indexes 7-17 are data sections.
+ ///
+ public uint[] SectionAddressTable { get; set; } = new uint[18];
+
+ ///
+ /// Section lengths indicate the size in bytes of each section.
+ /// 0 for unused sections.
+ ///
+ ///
+ /// Big-endian
+ /// Indexes 0-6 are text sections.
+ /// Indexes 7-17 are data sections.
+ ///
+ public uint[] SectionLengthsTable { get; set; } = new uint[18];
+
+ ///
+ /// bss address indicates the start of the zero initialised (bss) range.
+ ///
+ /// Big-endian
+ public uint BSSAddress { get; set; }
+
+ ///
+ /// bss length indicates the size in bytes of the zero initialised (bss) range.
+ ///
+ /// Big-endian
+ public uint BSSLength { get; set; }
+
+ ///
+ /// Entry point indicates the virtual memory address of a function to run after
+ /// the DOL has been loaded in order to start the program.
+ /// This function should not return.
+ ///
+ /// Big-endian
+ public uint EntryPoint { get; set; }
+
+ ///
+ /// Padding
+ ///
+ public byte[] Padding { get; set; } = new byte[0x1C];
+ }
+}
diff --git a/SabreTools.Data.Models/NintendoDisc/Disc.cs b/SabreTools.Data.Models/NintendoDisc/Disc.cs
index ac4afc4b..5e3bfbcf 100644
--- a/SabreTools.Data.Models/NintendoDisc/Disc.cs
+++ b/SabreTools.Data.Models/NintendoDisc/Disc.cs
@@ -10,11 +10,6 @@ namespace SabreTools.Data.Models.NintendoDisc
///
public DiscHeader Header { get; set; } = new();
- ///
- /// Detected platform (GameCube or Wii)
- ///
- public Platform Platform { get; set; }
-
///
/// Wii partition table entries (Wii discs only)
///
diff --git a/SabreTools.Data.Models/NintendoDisc/DiscHeader.cs b/SabreTools.Data.Models/NintendoDisc/DiscHeader.cs
index c1dc58e8..02daeb38 100644
--- a/SabreTools.Data.Models/NintendoDisc/DiscHeader.cs
+++ b/SabreTools.Data.Models/NintendoDisc/DiscHeader.cs
@@ -12,12 +12,6 @@ namespace SabreTools.Data.Models.NintendoDisc
/// 6 bytes at offset 0x000
public string GameId { get; set; } = string.Empty;
- ///
- /// 2-character ASCII maker / publisher code (e.g. "01")
- ///
- /// Derived from GameId bytes at offset 0x004–0x005; not a separate on-disc field
- public string MakerCode { get; set; } = string.Empty;
-
///
/// Zero-based disc number for multi-disc games
///
@@ -38,6 +32,11 @@ namespace SabreTools.Data.Models.NintendoDisc
///
public byte StreamingBufferSize { get; set; }
+ ///
+ /// Unused 0x0E bytes (offsets 0x00A-0x017)
+ ///
+ public byte[] Padding00A { get; set; } = [];
+
///
/// Wii magic word at offset 0x018 (0x5D1C9EA3 for Wii discs, 0 for GameCube)
///
@@ -63,6 +62,11 @@ namespace SabreTools.Data.Models.NintendoDisc
///
public byte DisableDiscEncryption { get; set; }
+ ///
+ /// Unused padding until DOL/FST offset fields at 0x420
+ ///
+ public byte[] Padding082 { get; set; } = [];
+
///
/// Offset of the main DOL executable (no shift for GameCube; <<2 for Wii)
///
@@ -77,5 +81,10 @@ namespace SabreTools.Data.Models.NintendoDisc
/// Maximum size of the File System Table in bytes
///
public uint FstSize { get; set; }
+
+ ///
+ /// Remaining bytes to complete the 0x440 header
+ ///
+ public byte[] Padding42C { get; set; } = [];
}
}
diff --git a/SabreTools.Data.Models/NintendoDisc/FileSystemTable.cs b/SabreTools.Data.Models/NintendoDisc/FileSystemTable.cs
new file mode 100644
index 00000000..113b5378
--- /dev/null
+++ b/SabreTools.Data.Models/NintendoDisc/FileSystemTable.cs
@@ -0,0 +1,26 @@
+namespace SabreTools.Data.Models.NintendoDisc
+{
+ public class FileSystemTable
+ {
+ ///
+ /// 8 bytes of unknown data
+ ///
+ ///
+ /// Maps to and
+ /// but is unused?
+ ///
+ public byte[] Unknown { get; set; } = new byte[8];
+
+ ///
+ /// Number of entries in the table
+ ///
+ /// Big-endian
+ public uint EntryCount { get; set; }
+
+ ///
+ /// File system table entries
+ ///
+ /// Length given by
+ public FileSystemTableEntry[] Entries { get; set; } = [];
+ }
+}
diff --git a/SabreTools.Data.Models/NintendoDisc/FileSystemTableEntry.cs b/SabreTools.Data.Models/NintendoDisc/FileSystemTableEntry.cs
new file mode 100644
index 00000000..7dda17da
--- /dev/null
+++ b/SabreTools.Data.Models/NintendoDisc/FileSystemTableEntry.cs
@@ -0,0 +1,26 @@
+namespace SabreTools.Data.Models.NintendoDisc
+{
+ ///
+ /// File entry with start and end byte offsets on disc
+ ///
+ public class FileSystemTableEntry
+ {
+ ///
+ /// Offset to the entry name
+ ///
+ /// Big-endian, has high byte set to 0xFF if a directory entry
+ public uint NameOffset { get; set; }
+
+ ///
+ /// Offset to the start of the file
+ ///
+ /// Big-endian
+ public uint FileOffset { get; set; }
+
+ ///
+ /// File size
+ ///
+ /// Big-endian
+ public uint FileSize { get; set; }
+ }
+}
diff --git a/SabreTools.Data.Models/NintendoDisc/WiiPartitionGroup.cs b/SabreTools.Data.Models/NintendoDisc/WiiPartitionGroup.cs
new file mode 100644
index 00000000..4acaef00
--- /dev/null
+++ b/SabreTools.Data.Models/NintendoDisc/WiiPartitionGroup.cs
@@ -0,0 +1,23 @@
+namespace SabreTools.Data.Models.NintendoDisc
+{
+ public class WiiPartitionGroup
+ {
+ ///
+ /// Number of entries in the group
+ ///
+ /// Big-endian
+ public uint Count { get; set; }
+
+ ///
+ /// Offset to the start of the table
+ ///
+ /// Big-endian, requires left bit shift of 2 to get the real value
+ public uint Offset { get; set; }
+
+ ///
+ /// Entries for the group, stored at
+ ///
+ /// Number of entries determined by
+ public WiiPartitionTableEntry[] Entries { get; set; } = [];
+ }
+}
diff --git a/SabreTools.Data.Models/NintendoDisc/WiiPartitionTableEntry.cs b/SabreTools.Data.Models/NintendoDisc/WiiPartitionTableEntry.cs
index c463b110..20c847da 100644
--- a/SabreTools.Data.Models/NintendoDisc/WiiPartitionTableEntry.cs
+++ b/SabreTools.Data.Models/NintendoDisc/WiiPartitionTableEntry.cs
@@ -2,15 +2,15 @@ namespace SabreTools.Data.Models.NintendoDisc
{
///
/// A single entry in the Wii disc partition table.
- /// The table lives at 0x40000–0x4FFFF on the disc.
+ /// The table lives at 0x40000-0x4FFFF on the disc.
///
///
public sealed class WiiPartitionTableEntry
{
///
- /// Absolute byte offset of the partition on the disc.
- /// Stored on-disc as offset >> 2 (big-endian u32).
+ /// Absolute byte offset of the partition on the disc
///
+ /// Big-endian, requires left bit shift of 2 to get the real value
public long Offset { get; set; }
///
diff --git a/SabreTools.Data.Models/OLE/IndirectPropertyName.cs b/SabreTools.Data.Models/OLE/IndirectPropertyName.cs
index ef0080a2..fee9561a 100644
--- a/SabreTools.Data.Models/OLE/IndirectPropertyName.cs
+++ b/SabreTools.Data.Models/OLE/IndirectPropertyName.cs
@@ -6,7 +6,7 @@ namespace SabreTools.Data.Models.OLE
/// VT_STORAGE (0x0043), VT_STREAMED_OBJECT (0x0044), VT_STORED_OBJECT (0x0044), and
/// VT_VERSIONED_STREAM (0x0049). It MUST be represented as a CodePageString, and its value MUST
/// be derived from the property identifier of the property represented according to the following
- /// Augmented Backus–Naur Form (ABNF) [RFC4234] syntax.
+ /// Augmented Backus-Naur Form (ABNF) [RFC4234] syntax.
///
/// Indirectproperty = "prop" propertyIdentifier
///
diff --git a/SabreTools.Data.Models/WIA/PartitionDataEntry.cs b/SabreTools.Data.Models/WIA/PartitionDataEntry.cs
index d6d08785..18344bd1 100644
--- a/SabreTools.Data.Models/WIA/PartitionDataEntry.cs
+++ b/SabreTools.Data.Models/WIA/PartitionDataEntry.cs
@@ -6,16 +6,28 @@ namespace SabreTools.Data.Models.WIA
///
public sealed class PartitionDataEntry
{
- /// Zero-based index of the first sector covered by this range
+ ///
+ /// Zero-based index of the first sector covered by this range
+ ///
+ /// Big-endian
public uint FirstSector { get; set; }
- /// Number of sectors covered by this range
+ ///
+ /// Number of sectors covered by this range
+ ///
+ /// Big-endian
public uint NumberOfSectors { get; set; }
- /// Index into the group-entry array of the first group for this range
+ ///
+ /// Index into the group-entry array of the first group for this range
+ ///
+ /// Big-endian
public uint GroupIndex { get; set; }
- /// Number of groups covering this range
+ ///
+ /// Number of groups covering this range
+ ///
+ /// Big-endian
public uint NumberOfGroups { get; set; }
}
}
diff --git a/SabreTools.Data.Models/WIA/PartitionEntry.cs b/SabreTools.Data.Models/WIA/PartitionEntry.cs
index 6ce1461f..e204224b 100644
--- a/SabreTools.Data.Models/WIA/PartitionEntry.cs
+++ b/SabreTools.Data.Models/WIA/PartitionEntry.cs
@@ -12,10 +12,14 @@ namespace SabreTools.Data.Models.WIA
/// 16 bytes
public byte[] PartitionKey { get; set; } = new byte[16];
- /// First sector range for this partition (typically encrypted data)
+ ///
+ /// First sector range for this partition (typically encrypted data)
+ ///
public PartitionDataEntry DataEntry0 { get; set; } = new();
- /// Second sector range for this partition (typically decrypted/raw data)
+ ///
+ /// Second sector range for this partition (typically decrypted/raw data)
+ ///
public PartitionDataEntry DataEntry1 { get; set; } = new();
}
}
diff --git a/SabreTools.Data.Models/WIA/RawDataEntry.cs b/SabreTools.Data.Models/WIA/RawDataEntry.cs
index 8d6c845c..625b5695 100644
--- a/SabreTools.Data.Models/WIA/RawDataEntry.cs
+++ b/SabreTools.Data.Models/WIA/RawDataEntry.cs
@@ -6,16 +6,28 @@ namespace SabreTools.Data.Models.WIA
///
public sealed class RawDataEntry
{
- /// Byte offset of this region within the equivalent ISO image
+ ///
+ /// Byte offset of this region within the equivalent ISO image
+ ///
+ /// Big-endian
public ulong DataOffset { get; set; }
- /// Size of this region in bytes
+ ///
+ /// Size of this region in bytes
+ ///
+ /// Big-endian
public ulong DataSize { get; set; }
- /// Index into the group-entry array of the first group for this region
+ ///
+ /// Index into the group-entry array of the first group for this region
+ ///
+ /// Big-endian
public uint GroupIndex { get; set; }
- /// Number of groups covering this region
+ ///
+ /// Number of groups covering this region
+ ///
+ /// Big-endian
public uint NumberOfGroups { get; set; }
}
}
diff --git a/SabreTools.Data.Models/WIA/RvzGroupEntry.cs b/SabreTools.Data.Models/WIA/RvzGroupEntry.cs
index 44dfb96d..c3477dcd 100644
--- a/SabreTools.Data.Models/WIA/RvzGroupEntry.cs
+++ b/SabreTools.Data.Models/WIA/RvzGroupEntry.cs
@@ -8,19 +8,22 @@ namespace SabreTools.Data.Models.WIA
{
///
/// Actual byte offset of this group's data within the RVZ file.
- /// (On disk this value is stored as offset >> 2.)
+ /// (On disk this value is stored as offset >> 2.)
///
- public ulong DataOffset { get; set; }
+ /// Big-endian
+ public uint DataOffset { get; set; }
///
/// Total size of this group's data (compressed + any RVZ-pack section) in bytes
///
+ /// Big-endian
public uint DataSize { get; set; }
///
/// Size of the RVZ-packed (junk-stripped) portion within this group's data.
/// 0 means no RVZ packing was applied.
///
+ /// Big-endian
public uint RvzPackedSize { get; set; }
}
}
diff --git a/SabreTools.Data.Models/WIA/WiaGroupEntry.cs b/SabreTools.Data.Models/WIA/WiaGroupEntry.cs
index 43ffc901..13f874f7 100644
--- a/SabreTools.Data.Models/WIA/WiaGroupEntry.cs
+++ b/SabreTools.Data.Models/WIA/WiaGroupEntry.cs
@@ -8,13 +8,14 @@ namespace SabreTools.Data.Models.WIA
{
///
/// Actual byte offset of this group's data within the WIA file.
- /// (On disk this value is stored as offset >> 2.)
///
- public ulong DataOffset { get; set; }
+ /// Big-endian, requires left bit shift of 2 to get the real value
+ public uint DataOffset { get; set; }
///
/// Compressed size of this group's data in bytes (0 means group contains only zeroes)
///
+ /// Big-endian
public uint DataSize { get; set; }
}
}
diff --git a/SabreTools.Data.Models/WIA/WiaHeader1.cs b/SabreTools.Data.Models/WIA/WiaHeader1.cs
index 9e9703d2..e82ea598 100644
--- a/SabreTools.Data.Models/WIA/WiaHeader1.cs
+++ b/SabreTools.Data.Models/WIA/WiaHeader1.cs
@@ -15,16 +15,19 @@ namespace SabreTools.Data.Models.WIA
///
/// Format version (e.g. 0x01000000)
///
+ /// Big-endian
public uint Version { get; set; }
///
/// Minimum version required to read this file
///
+ /// Big-endian
public uint VersionCompatible { get; set; }
///
/// Size of WiaHeader2 in bytes
///
+ /// Big-endian
public uint Header2Size { get; set; }
///
@@ -36,11 +39,13 @@ namespace SabreTools.Data.Models.WIA
///
/// Total size of the equivalent uncompressed ISO image in bytes
///
+ /// Big-endian
public ulong IsoFileSize { get; set; }
///
/// Total size of this WIA / RVZ file in bytes
///
+ /// Big-endian
public ulong WiaFileSize { get; set; }
///
diff --git a/SabreTools.Data.Models/WIA/WiaHeader2.cs b/SabreTools.Data.Models/WIA/WiaHeader2.cs
index f97dbac3..d0ab8425 100644
--- a/SabreTools.Data.Models/WIA/WiaHeader2.cs
+++ b/SabreTools.Data.Models/WIA/WiaHeader2.cs
@@ -10,22 +10,26 @@ namespace SabreTools.Data.Models.WIA
///
/// Disc type: 1 = GameCube, 2 = Wii
///
+ /// Big-endian
public WiaDiscType DiscType { get; set; }
///
/// Compression algorithm applied to group data
///
+ /// Big-endian
public WiaRvzCompressionType CompressionType { get; set; }
///
- /// Informational compression level used when writing (1–9)
+ /// Informational compression level used when writing (1-9)
///
+ /// Big-endian
public int CompressionLevel { get; set; }
///
/// Group / chunk size in bytes.
/// WIA requires exactly 2 MiB; RVZ accepts powers of 2 between 32 KiB and 2 MiB.
///
+ /// Big-endian
public uint ChunkSize { get; set; }
///
@@ -37,16 +41,19 @@ namespace SabreTools.Data.Models.WIA
///
/// Number of PartitionEntry structures that follow the raw-data entries
///
+ /// Big-endian
public uint NumberOfPartitionEntries { get; set; }
///
/// Size of each PartitionEntry in bytes
///
+ /// Big-endian
public uint PartitionEntrySize { get; set; }
///
/// File offset of the PartitionEntry array
///
+ /// Big-endian
public ulong PartitionEntriesOffset { get; set; }
///
@@ -58,31 +65,37 @@ namespace SabreTools.Data.Models.WIA
///
/// Number of RawDataEntry structures
///
+ /// Big-endian
public uint NumberOfRawDataEntries { get; set; }
///
/// File offset of the RawDataEntry array
///
+ /// Big-endian
public ulong RawDataEntriesOffset { get; set; }
///
/// Total size in bytes of all RawDataEntry structures
///
+ /// Big-endian
public uint RawDataEntriesSize { get; set; }
///
/// Number of group entries (WiaGroupEntry or RvzGroupEntry)
///
+ /// Big-endian
public uint NumberOfGroupEntries { get; set; }
///
/// File offset of the group-entry array
///
+ /// Big-endian
public ulong GroupEntriesOffset { get; set; }
///
/// Total size in bytes of all group entries
///
+ /// Big-endian
public uint GroupEntriesSize { get; set; }
///
diff --git a/SabreTools.Serialization.Readers/GCZ.cs b/SabreTools.Serialization.Readers/GCZ.cs
index 804cfc2a..e628904c 100644
--- a/SabreTools.Serialization.Readers/GCZ.cs
+++ b/SabreTools.Serialization.Readers/GCZ.cs
@@ -1,6 +1,5 @@
using System.IO;
using SabreTools.Data.Models.GCZ;
-using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
#pragma warning disable IDE0017 // Simplify object initialization
@@ -31,6 +30,7 @@ namespace SabreTools.Serialization.Readers
return null;
// Validate block count — guard against absurdly large tables
+ // TODO: Determine if this block count is arbitrary
if (archive.Header.NumBlocks == 0 || archive.Header.NumBlocks > 0x100000)
return null;
@@ -38,15 +38,17 @@ namespace SabreTools.Serialization.Readers
// Read block pointer table (8 bytes per block)
archive.BlockPointers = new ulong[numBlocks];
- byte[] ptrBuf = data.ReadBytes(numBlocks * 8);
for (int i = 0; i < numBlocks; i++)
- archive.BlockPointers[i] = System.BitConverter.ToUInt64(ptrBuf, i * 8);
+ {
+ archive.BlockPointers[i] = data.ReadUInt64LittleEndian();
+ }
// Read block hash table (4 bytes per block, Adler-32)
archive.BlockHashes = new uint[numBlocks];
- byte[] hashBuf = data.ReadBytes(numBlocks * 4);
for (int i = 0; i < numBlocks; i++)
- archive.BlockHashes[i] = System.BitConverter.ToUInt32(hashBuf, i * 4);
+ {
+ archive.BlockHashes[i] = data.ReadUInt32LittleEndian();
+ }
return archive;
}
@@ -56,16 +58,23 @@ namespace SabreTools.Serialization.Readers
}
}
- private static GczHeader ParseGczHeader(Stream data)
+ ///
+ /// Parse a Stream into a GczHeader
+ ///
+ /// Stream to parse
+ /// Filled GczHeader on success, null on error
+ public static GczHeader ParseGczHeader(Stream data)
{
- var header = new GczHeader();
- header.MagicCookie = data.ReadUInt32LittleEndian();
- header.SubType = data.ReadUInt32LittleEndian();
- header.CompressedDataSize = data.ReadUInt64LittleEndian();
- header.DataSize = data.ReadUInt64LittleEndian();
- header.BlockSize = data.ReadUInt32LittleEndian();
- header.NumBlocks = data.ReadUInt32LittleEndian();
- return header;
+ var obj = new GczHeader();
+
+ obj.MagicCookie = data.ReadUInt32LittleEndian();
+ obj.SubType = data.ReadUInt32LittleEndian();
+ obj.CompressedDataSize = data.ReadUInt64LittleEndian();
+ obj.DataSize = data.ReadUInt64LittleEndian();
+ obj.BlockSize = data.ReadUInt32LittleEndian();
+ obj.NumBlocks = data.ReadUInt32LittleEndian();
+
+ return obj;
}
}
}
diff --git a/SabreTools.Serialization.Readers/NintendoDisc.cs b/SabreTools.Serialization.Readers/NintendoDisc.cs
index 83dce4a9..229dcf0c 100644
--- a/SabreTools.Serialization.Readers/NintendoDisc.cs
+++ b/SabreTools.Serialization.Readers/NintendoDisc.cs
@@ -1,5 +1,7 @@
+using System.Collections.Generic;
using System.IO;
using System.Text;
+using SabreTools.Data.Extensions;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
@@ -31,31 +33,27 @@ namespace SabreTools.Serialization.Readers
// Determine platform from magic words; fall back to GameId prefix for
// GC discs that omit the magic word (e.g. some redump/scene ISOs)
- if (disc.Header.WiiMagic == Constants.WiiMagicWord)
- disc.Platform = Platform.Wii;
- else if (disc.Header.GCMagic == Constants.GCMagicWord)
- disc.Platform = Platform.GameCube;
- else if (disc.Header.GameId != null && disc.Header.GameId.Length >= 1
- && IsGameCubeTitleType(disc.Header.GameId[0]))
- disc.Platform = Platform.GameCube;
- else
- disc.Platform = Platform.Unknown;
+ Platform platform = disc.Header.GetPlatform();
// Parse Wii-specific structures
- if (disc.Platform == Platform.Wii)
+ if (platform == Platform.Wii)
{
// Partition table starts at 0x40000
- long partTableEnd = initialOffset + Constants.WiiPartitionTableAddress
+ long partTableEnd = initialOffset
+ + Constants.WiiPartitionTableAddress
+ (Constants.WiiPartitionGroupCount * 8);
- if (data.Length >= partTableEnd)
+ if (partTableEnd < data.Length)
+ {
+ data.Seek(initialOffset + Constants.WiiPartitionTableAddress, SeekOrigin.Begin);
disc.PartitionTableEntries = ParsePartitionTable(data, initialOffset);
+ }
// Region data at 0x4E000
long regionEnd = initialOffset + Constants.WiiRegionDataAddress + Constants.WiiRegionDataSize;
- if (data.Length >= regionEnd)
+ if (regionEnd < data.Length)
{
data.Seek(initialOffset + Constants.WiiRegionDataAddress, SeekOrigin.Begin);
- disc.RegionData = ParseRegionData(data);
+ disc.RegionData = ParseWiiRegionData(data);
}
}
@@ -67,139 +65,215 @@ namespace SabreTools.Serialization.Readers
}
}
- #region Header parsing
-
///
- /// Parses just the disc header fields from the given stream without requiring
- /// the full 0x440-byte boot block. Requires at least 0x82 bytes (enough to
- /// reach AudioStreaming and StreamingBufferSize) to be useful; the DOL/FST
- /// fields will be zero when the stream is shorter than 0x42B bytes.
+ /// Parse a Stream into a DiscHeader
///
- public static DiscHeader? ParseDiscHeaderOnly(Stream? data)
+ /// Stream to parse
+ /// Filled DiscHeader on success, null on error
+ public static DiscHeader ParseDiscHeader(Stream data)
{
- if (data is null || !data.CanRead || data.Length - data.Position < 6)
- return null;
- try { return ParseDiscHeader(data); }
- catch { return null; }
- }
-
- private static DiscHeader ParseDiscHeader(Stream data)
- {
- var header = new DiscHeader();
+ var obj = new DiscHeader();
// 0x000: 4-char title code + 2-char maker code stored as one 6-byte GameId field
- byte[] gameIdBytes = data.ReadBytes(Constants.GameIdLength);
- header.GameId = Encoding.ASCII.GetString(gameIdBytes).TrimEnd('\0');
+ byte[] gameIdBytes = data.ReadBytes(6);
+ obj.GameId = Encoding.ASCII.GetString(gameIdBytes);
- // Maker code is the last 2 chars of the GameId (offsets 0x004–0x005).
- // Dolphin reads it with Read(0x4, 2) — there is no separate field at 0x006.
- header.MakerCode = header.GameId != null && header.GameId.Length >= 6
- ? header.GameId.Substring(4, 2)
- : string.Empty;
+ obj.DiscNumber = data.ReadByteValue();
+ obj.DiscVersion = data.ReadByteValue();
+ obj.AudioStreaming = data.ReadByteValue();
+ obj.StreamingBufferSize = data.ReadByteValue();
+ obj.Padding00A = data.ReadBytes(0x0E);
+ obj.WiiMagic = data.ReadUInt32BigEndian();
+ obj.GCMagic = data.ReadUInt32BigEndian();
- // 0x006: disc number, 0x007: revision (Dolphin GetDiscNumber/GetRevision)
- header.DiscNumber = data.ReadByteValue();
- header.DiscVersion = data.ReadByteValue();
- // 0x008: audio streaming, 0x009: streaming buffer size
- header.AudioStreaming = data.ReadByteValue();
- header.StreamingBufferSize = data.ReadByteValue();
+ byte[] titleBytes = data.ReadBytes(0x60);
+ obj.GameTitle = Encoding.ASCII.GetString(titleBytes);
- // Skip unused 0x0E bytes (offsets 0x00A–0x017)
- data.ReadBytes(0x0E);
+ obj.DisableHashVerification = data.ReadByteValue();
+ obj.DisableDiscEncryption = data.ReadByteValue();
+ obj.Padding082 = data.ReadBytes(0x39E);
+ obj.DolOffset = data.ReadUInt32BigEndian();
+ obj.FstOffset = data.ReadUInt32BigEndian();
+ obj.FstSize = data.ReadUInt32BigEndian();
+ obj.Padding42C = data.ReadBytes(0x14);
- header.WiiMagic = data.ReadUInt32BigEndian();
- header.GCMagic = data.ReadUInt32BigEndian();
-
- byte[] titleBytes = data.ReadBytes(Constants.GameTitleLength);
- header.GameTitle = Encoding.ASCII.GetString(titleBytes).TrimEnd('\0');
-
- header.DisableHashVerification = data.Position < data.Length ? data.ReadByteValue() : (byte)0;
- header.DisableDiscEncryption = data.Position < data.Length ? data.ReadByteValue() : (byte)0;
-
- // Skip to DOL/FST offset fields at 0x420.
- // Position so far: 6+1+1+1+1+14+4+4+96+1+1 = 130 = 0x82
- int skipToBootBlock = Constants.DolOffsetField - 0x82;
- if (data.Length - data.Position < skipToBootBlock + 12)
- return header;
-
- data.ReadBytes(skipToBootBlock);
-
- header.DolOffset = data.ReadUInt32BigEndian();
- header.FstOffset = data.ReadUInt32BigEndian();
- header.FstSize = data.ReadUInt32BigEndian();
-
- // Skip the remaining bytes to complete the 0x440 header
- // We are at 0x420 + 12 = 0x42C; need to reach 0x440
- data.ReadBytes(Constants.DiscHeaderSize - (Constants.DolOffsetField + 12));
-
- return header;
+ return obj;
}
- #endregion
-
- #region Wii partition table parsing
-
- private static WiiPartitionTableEntry[]? ParsePartitionTable(Stream data, long baseOffset)
- {
- data.Seek(baseOffset + Constants.WiiPartitionTableAddress, SeekOrigin.Begin);
-
- // Read 4 partition groups; each group has a count and a shifted offset
- var allEntries = new System.Collections.Generic.List();
-
- for (int g = 0; g < Constants.WiiPartitionGroupCount; g++)
- {
- uint count = data.ReadUInt32BigEndian();
- uint shiftedOffset = data.ReadUInt32BigEndian();
-
- if (count == 0)
- continue;
-
- long tableOffset = baseOffset + ((long)shiftedOffset << 2);
- long savedPosition = data.Position;
-
- if (tableOffset + ((long)count * 8) > data.Length)
- {
- data.Seek(savedPosition, SeekOrigin.Begin);
- continue;
- }
-
- data.Seek(tableOffset, SeekOrigin.Begin);
- for (uint i = 0; i < count; i++)
- {
- var entry = new WiiPartitionTableEntry();
- uint rawOffset = data.ReadUInt32BigEndian();
- entry.Offset = (long)rawOffset << 2;
- entry.Type = data.ReadUInt32BigEndian();
- allEntries.Add(entry);
- }
-
- data.Seek(savedPosition, SeekOrigin.Begin);
- }
-
- return allEntries.Count > 0 ? allEntries.ToArray() : null;
- }
-
- #endregion
-
- #region Wii region data parsing
-
- private static WiiRegionData ParseRegionData(Stream data)
- {
- var region = new WiiRegionData();
- region.RegionSetting = data.ReadUInt32BigEndian();
- region.AgeRatings = data.ReadBytes(16);
- return region;
- }
-
- #endregion
-
///
- /// Returns true if the GameId first character is a known GameCube title type prefix.
- /// Used as a fallback when the GC magic word is absent from the disc image.
+ /// Parse a byte array into a DOLHeader
///
- private static bool IsGameCubeTitleType(char c)
+ /// Byte array to parse
+ /// Filled DOLHeader on success, null on error
+ public static DOLHeader ParseDOLHeader(byte[] data, ref int offset)
{
- return c == 'G' || c == 'D' || c == 'R';
+ var obj = new DOLHeader();
+
+ obj.SectionOffsetTable = new uint[18];
+ for (int i = 0; i < obj.SectionOffsetTable.Length; i++)
+ {
+ obj.SectionOffsetTable[i] = data.ReadUInt32BigEndian(ref offset);
+ }
+
+ obj.SectionAddressTable = new uint[18];
+ for (int i = 0; i < obj.SectionAddressTable.Length; i++)
+ {
+ obj.SectionAddressTable[i] = data.ReadUInt32BigEndian(ref offset);
+ }
+
+ obj.SectionLengthsTable = new uint[18];
+ for (int i = 0; i < obj.SectionLengthsTable.Length; i++)
+ {
+ obj.SectionLengthsTable[i] = data.ReadUInt32BigEndian(ref offset);
+ }
+
+ obj.BSSAddress = data.ReadUInt32BigEndian(ref offset);
+ obj.BSSLength = data.ReadUInt32BigEndian(ref offset);
+ obj.EntryPoint = data.ReadUInt32BigEndian(ref offset);
+ obj.Padding = data.ReadBytes(ref offset, 0x1C);
+
+ return obj;
+ }
+
+ ///
+ /// Parse a byte array into a ParseFileSystemTable
+ ///
+ /// Byte array to parse
+ /// Filled ParseFileSystemTable on success, null on error
+ public static FileSystemTable? ParseFileSystemTable(byte[] data)
+ {
+ // Check that the root entry exists
+ if (data.Length < 12)
+ return null;
+
+ var obj = new FileSystemTable();
+
+ // Read the root entry first
+ int offset = 0;
+ obj.Unknown = data.ReadBytes(ref offset, 8);
+ obj.EntryCount = data.ReadUInt32BigEndian(ref offset);
+ if (obj.EntryCount < 1 || (obj.EntryCount * 12) > data.Length)
+ return null;
+
+ // Read all entries
+ offset = 0;
+ obj.Entries = new FileSystemTableEntry[obj.EntryCount];
+ for (int i = 0; i < obj.Entries.Length; i++)
+ {
+ obj.Entries[i] = ParseFileSystemTableEntry(data, ref offset);
+ }
+
+ return obj;
+ }
+
+ ///
+ /// Parse a byte array into a FileSystemTableEntry
+ ///
+ /// Byte array to parse
+ /// Filled FileSystemTableEntry on success, null on error
+ public static FileSystemTableEntry ParseFileSystemTableEntry(byte[] data, ref int offset)
+ {
+ var obj = new FileSystemTableEntry();
+
+ obj.NameOffset = data.ReadUInt32BigEndian(ref offset);
+ obj.FileOffset = data.ReadUInt32BigEndian(ref offset);
+ obj.FileSize = data.ReadUInt32BigEndian(ref offset);
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a partition table
+ ///
+ /// Stream to parse
+ /// Filled partition table on success, null on error
+ public static WiiPartitionTableEntry[]? ParsePartitionTable(Stream data, long initialOffset)
+ {
+ // Read 4 partition groups; each group has a count and a shifted offset
+ var allEntries = new List();
+
+ for (int i = 0; i < Constants.WiiPartitionGroupCount; i++)
+ {
+ var group = ParseWiiPartitionGroup(data, initialOffset);
+ if (group.Count == 0)
+ continue;
+
+ // TODO: Keep group entries separate
+ allEntries.AddRange(group.Entries);
+ }
+
+ return allEntries.Count > 0 ? [.. allEntries] : null;
+ }
+
+ ///
+ /// Parse a Stream into a WiiPartitionGroup
+ ///
+ /// Stream to parse
+ /// Initial offset into the stream
+ /// Filled WiiPartitionTableEntry on success, null on error
+ public static WiiPartitionGroup ParseWiiPartitionGroup(Stream data, long initialOffset)
+ {
+ var obj = new WiiPartitionGroup();
+
+ obj.Count = data.ReadUInt32BigEndian();
+ obj.Offset = data.ReadUInt32BigEndian();
+
+ // Empty groups should not attempt parsing
+ if (obj.Count == 0)
+ return obj;
+
+ // Determine the table offset
+ long tableOffset = initialOffset + (obj.Offset << 2);
+ if (tableOffset + (obj.Count * 8) > data.Length)
+ return obj;
+
+ // Seek to the table, if possible
+ long savedPosition = data.Position;
+ data.Seek(tableOffset, SeekOrigin.Begin);
+
+ // Parse the table
+ List entries = [];
+ for (uint i = 0; i < obj.Count; i++)
+ {
+ var entry = ParseWiiPartitionTableEntry(data);
+ entries.Add(entry);
+ }
+
+ // Set the entries and reset the stream
+ obj.Entries = [.. entries];
+ data.Seek(savedPosition, SeekOrigin.Begin);
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a WiiPartitionTableEntry
+ ///
+ /// Stream to parse
+ /// Filled WiiPartitionTableEntry on success, null on error
+ public static WiiPartitionTableEntry ParseWiiPartitionTableEntry(Stream data)
+ {
+ var obj = new WiiPartitionTableEntry();
+
+ obj.Offset = data.ReadUInt32BigEndian();
+ obj.Type = data.ReadUInt32BigEndian();
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a WiiRegionData
+ ///
+ /// Stream to parse
+ /// Filled WiiRegionData on success, null on error
+ public static WiiRegionData ParseWiiRegionData(Stream data)
+ {
+ var obj = new WiiRegionData();
+
+ obj.RegionSetting = data.ReadUInt32BigEndian();
+ obj.AgeRatings = data.ReadBytes(16);
+
+ return obj;
}
}
}
diff --git a/SabreTools.Serialization.Readers/WIA.cs b/SabreTools.Serialization.Readers/WIA.cs
index 3015ffb6..cff54346 100644
--- a/SabreTools.Serialization.Readers/WIA.cs
+++ b/SabreTools.Serialization.Readers/WIA.cs
@@ -26,44 +26,57 @@ namespace SabreTools.Serialization.Readers
var archive = new DiscImage();
// Parse Header1
- archive.Header1 = ParseHeader1(data);
-
- // Validate magic
+ archive.Header1 = ParseWiaHeader1(data);
if (archive.Header1.Magic != Constants.WiaMagic && archive.Header1.Magic != Constants.RvzMagic)
return null;
// Parse Header2
- archive.Header2 = ParseHeader2(data);
+ archive.Header2 = ParseWiaHeader2(data);
// Parse partition entries (Wii discs only)
- if (archive.Header2.NumberOfPartitionEntries > 0
- && archive.Header2.PartitionEntriesOffset > 0)
+ if (archive.Header2.NumberOfPartitionEntries > 0 && archive.Header2.PartitionEntriesOffset > 0)
{
data.Seek(initialOffset + (long)archive.Header2.PartitionEntriesOffset, SeekOrigin.Begin);
- archive.PartitionEntries = ParsePartitionEntries(
- data, (int)archive.Header2.NumberOfPartitionEntries);
+
+ archive.PartitionEntries = new PartitionEntry[archive.Header2.NumberOfPartitionEntries];
+ for (int i = 0; i < archive.PartitionEntries.Length; i++)
+ {
+ archive.PartitionEntries[i] = ParsePartitionEntry(data); ;
+ }
}
// Parse raw data entries
- if (archive.Header2.NumberOfRawDataEntries > 0
- && archive.Header2.RawDataEntriesOffset > 0)
+ if (archive.Header2.NumberOfRawDataEntries > 0 && archive.Header2.RawDataEntriesOffset > 0)
{
data.Seek(initialOffset + (long)archive.Header2.RawDataEntriesOffset, SeekOrigin.Begin);
- archive.RawDataEntries = ParseRawDataEntries(
- data, (int)archive.Header2.NumberOfRawDataEntries);
+
+ archive.RawDataEntries = new RawDataEntry[archive.Header2.NumberOfRawDataEntries];
+ for (int i = 0; i < archive.Header2.NumberOfRawDataEntries; i++)
+ {
+ archive.RawDataEntries[i] = ParseRawDataEntry(data);
+ }
}
// Parse group entries
- if (archive.Header2.NumberOfGroupEntries > 0
- && archive.Header2.GroupEntriesOffset > 0)
+ if (archive.Header2.NumberOfGroupEntries > 0 && archive.Header2.GroupEntriesOffset > 0)
{
data.Seek(initialOffset + (long)archive.Header2.GroupEntriesOffset, SeekOrigin.Begin);
if (archive.Header1.Magic == Constants.RvzMagic)
- archive.RvzGroupEntries = ParseRvzGroupEntries(
- data, (int)archive.Header2.NumberOfGroupEntries);
+ {
+ archive.RvzGroupEntries = new RvzGroupEntry[archive.Header2.NumberOfGroupEntries];
+ for (int i = 0; i < archive.Header2.NumberOfGroupEntries; i++)
+ {
+ archive.RvzGroupEntries[i] = ParseRvzGroupEntry(data);
+ }
+ }
else
- archive.GroupEntries = ParseWiaGroupEntries(
- data, (int)archive.Header2.NumberOfGroupEntries);
+ {
+ archive.GroupEntries = new WiaGroupEntry[archive.Header2.NumberOfGroupEntries];
+ for (int i = 0; i < archive.Header2.NumberOfGroupEntries; i++)
+ {
+ archive.GroupEntries[i] = ParseWiaGroupEntry(data); ;
+ }
+ }
}
return archive;
@@ -74,121 +87,184 @@ namespace SabreTools.Serialization.Readers
}
}
- #region Header parsing
-
- private static WiaHeader1 ParseHeader1(Stream data)
+ ///
+ /// Parse a Stream into a PartitionDataEntry
+ ///
+ /// Stream to parse
+ /// Filled PartitionDataEntry on success, null on error
+ public static PartitionDataEntry ParsePartitionDataEntry(Stream data)
{
- var h = new WiaHeader1();
- h.Magic = data.ReadUInt32LittleEndian();
- h.Version = data.ReadUInt32BigEndian();
- h.VersionCompatible = data.ReadUInt32BigEndian();
- h.Header2Size = data.ReadUInt32BigEndian();
- h.Header2Hash = data.ReadBytes(20);
- h.IsoFileSize = data.ReadUInt64BigEndian();
- h.WiaFileSize = data.ReadUInt64BigEndian();
- h.Header1Hash = data.ReadBytes(20);
- return h;
+ var obj = new PartitionDataEntry();
+
+ obj.FirstSector = data.ReadUInt32BigEndian();
+ obj.NumberOfSectors = data.ReadUInt32BigEndian();
+ obj.GroupIndex = data.ReadUInt32BigEndian();
+ obj.NumberOfGroups = data.ReadUInt32BigEndian();
+
+ return obj;
}
- private static WiaHeader2 ParseHeader2(Stream data)
+ ///
+ /// Parse a Stream into a PartitionEntry
+ ///
+ /// Stream to parse
+ /// Filled PartitionEntry on success, null on error
+ public static PartitionEntry ParsePartitionEntry(Stream data)
{
- var h = new WiaHeader2();
- h.DiscType = (WiaDiscType)data.ReadUInt32BigEndian();
- h.CompressionType = (WiaRvzCompressionType)data.ReadUInt32BigEndian();
- h.CompressionLevel = data.ReadInt32BigEndian();
- h.ChunkSize = data.ReadUInt32BigEndian();
- h.DiscHeader = data.ReadBytes(0x80);
- h.NumberOfPartitionEntries = data.ReadUInt32BigEndian();
- h.PartitionEntrySize = data.ReadUInt32BigEndian();
- h.PartitionEntriesOffset = data.ReadUInt64BigEndian();
- h.PartitionEntriesHash = data.ReadBytes(20);
- h.NumberOfRawDataEntries = data.ReadUInt32BigEndian();
- h.RawDataEntriesOffset = data.ReadUInt64BigEndian();
- h.RawDataEntriesSize = data.ReadUInt32BigEndian();
- h.NumberOfGroupEntries = data.ReadUInt32BigEndian();
- h.GroupEntriesOffset = data.ReadUInt64BigEndian();
- h.GroupEntriesSize = data.ReadUInt32BigEndian();
- h.CompressorDataSize = data.ReadByteValue();
- h.CompressorData = data.ReadBytes(7);
- return h;
+ var obj = new PartitionEntry();
+
+ obj.PartitionKey = data.ReadBytes(16);
+ obj.DataEntry0 = ParsePartitionDataEntry(data);
+ obj.DataEntry1 = ParsePartitionDataEntry(data);
+
+ return obj;
}
- #endregion
-
- #region Table parsing
-
- private static PartitionEntry[] ParsePartitionEntries(Stream data, int count)
+ ///
+ /// Parse a Stream into a RawDataEntry
+ ///
+ /// Stream to parse
+ /// Filled RawDataEntry on success, null on error
+ public static RawDataEntry ParseRawDataEntry(Stream data)
{
- var entries = new PartitionEntry[count];
- for (int i = 0; i < count; i++)
- {
- var e = new PartitionEntry();
- e.PartitionKey = data.ReadBytes(16);
- e.DataEntry0 = ParsePartitionDataEntry(data);
- e.DataEntry1 = ParsePartitionDataEntry(data);
- entries[i] = e;
- }
+ var obj = new RawDataEntry();
- return entries;
+ obj.DataOffset = data.ReadUInt64BigEndian();
+ obj.DataSize = data.ReadUInt64BigEndian();
+ obj.GroupIndex = data.ReadUInt32BigEndian();
+ obj.NumberOfGroups = data.ReadUInt32BigEndian();
+
+ return obj;
}
- private static PartitionDataEntry ParsePartitionDataEntry(Stream data)
+ ///
+ /// Parse a byte array into a RawDataEntry
+ ///
+ /// Byte array to parse
+ /// Filled RawDataEntry on success, null on error
+ public static RawDataEntry ParseRawDataEntry(byte[] data, ref int offset)
{
- var e = new PartitionDataEntry();
- e.FirstSector = data.ReadUInt32BigEndian();
- e.NumberOfSectors = data.ReadUInt32BigEndian();
- e.GroupIndex = data.ReadUInt32BigEndian();
- e.NumberOfGroups = data.ReadUInt32BigEndian();
- return e;
+ var obj = new RawDataEntry();
+
+ obj.DataOffset = data.ReadUInt64BigEndian(ref offset);
+ obj.DataSize = data.ReadUInt64BigEndian(ref offset);
+ obj.GroupIndex = data.ReadUInt32BigEndian(ref offset);
+ obj.NumberOfGroups = data.ReadUInt32BigEndian(ref offset);
+
+ return obj;
}
- private static RawDataEntry[] ParseRawDataEntries(Stream data, int count)
+ ///
+ /// Parse a Stream into a RvzGroupEntry
+ ///
+ /// Stream to parse
+ /// Filled RvzGroupEntry on success, null on error
+ public static RvzGroupEntry ParseRvzGroupEntry(Stream data)
{
- var entries = new RawDataEntry[count];
- for (int i = 0; i < count; i++)
- {
- var e = new RawDataEntry();
- e.DataOffset = data.ReadUInt64BigEndian();
- e.DataSize = data.ReadUInt64BigEndian();
- e.GroupIndex = data.ReadUInt32BigEndian();
- e.NumberOfGroups = data.ReadUInt32BigEndian();
- entries[i] = e;
- }
+ var obj = new RvzGroupEntry();
- return entries;
+ obj.DataOffset = data.ReadUInt32BigEndian();
+ obj.DataSize = data.ReadUInt32BigEndian();
+ obj.RvzPackedSize = data.ReadUInt32BigEndian();
+
+ return obj;
}
- private static WiaGroupEntry[] ParseWiaGroupEntries(Stream data, int count)
+ ///
+ /// Parse a byte array into a RvzGroupEntry
+ ///
+ /// Byte array to parse
+ /// Filled RvzGroupEntry on success, null on error
+ public static RvzGroupEntry ParseRvzGroupEntry(byte[] data, ref int offset)
{
- var entries = new WiaGroupEntry[count];
- for (int i = 0; i < count; i++)
- {
- var e = new WiaGroupEntry();
- // DataOffset stored as actual_offset >> 2
- e.DataOffset = (ulong)data.ReadUInt32BigEndian() << 2;
- e.DataSize = data.ReadUInt32BigEndian();
- entries[i] = e;
- }
+ var obj = new RvzGroupEntry();
- return entries;
+ obj.DataOffset = data.ReadUInt32BigEndian(ref offset);
+ obj.DataSize = data.ReadUInt32BigEndian(ref offset);
+ obj.RvzPackedSize = data.ReadUInt32BigEndian(ref offset);
+
+ return obj;
}
- private static RvzGroupEntry[] ParseRvzGroupEntries(Stream data, int count)
+ ///
+ /// Parse a Stream into a WiaGroupEntry
+ ///
+ /// Stream to parse
+ /// Filled WiaGroupEntry on success, null on error
+ public static WiaGroupEntry ParseWiaGroupEntry(Stream data)
{
- var entries = new RvzGroupEntry[count];
- for (int i = 0; i < count; i++)
- {
- var e = new RvzGroupEntry();
- // DataOffset stored as actual_offset >> 2
- e.DataOffset = (ulong)data.ReadUInt32BigEndian() << 2;
- e.DataSize = data.ReadUInt32BigEndian();
- e.RvzPackedSize = data.ReadUInt32BigEndian();
- entries[i] = e;
- }
+ var obj = new WiaGroupEntry();
- return entries;
+ obj.DataOffset = data.ReadUInt32BigEndian();
+ obj.DataSize = data.ReadUInt32BigEndian();
+
+ return obj;
}
- #endregion
+ ///
+ /// Parse a byte array into a WiaGroupEntry
+ ///
+ /// Byte array to parse
+ /// Filled WiaGroupEntry on success, null on error
+ public static WiaGroupEntry ParseWiaGroupEntry(byte[] data, ref int offset)
+ {
+ var obj = new WiaGroupEntry();
+
+ obj.DataOffset = data.ReadUInt32BigEndian(ref offset);
+ obj.DataSize = data.ReadUInt32BigEndian(ref offset);
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a WiaHeader1
+ ///
+ /// Stream to parse
+ /// Filled WiaHeader1 on success, null on error
+ public static WiaHeader1 ParseWiaHeader1(Stream data)
+ {
+ var obj = new WiaHeader1();
+
+ obj.Magic = data.ReadUInt32LittleEndian();
+ obj.Version = data.ReadUInt32BigEndian();
+ obj.VersionCompatible = data.ReadUInt32BigEndian();
+ obj.Header2Size = data.ReadUInt32BigEndian();
+ obj.Header2Hash = data.ReadBytes(20);
+ obj.IsoFileSize = data.ReadUInt64BigEndian();
+ obj.WiaFileSize = data.ReadUInt64BigEndian();
+ obj.Header1Hash = data.ReadBytes(20);
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a WiaHeader2
+ ///
+ /// Stream to parse
+ /// Filled WiaHeader2 on success, null on error
+ public static WiaHeader2 ParseWiaHeader2(Stream data)
+ {
+ var obj = new WiaHeader2();
+
+ obj.DiscType = (WiaDiscType)data.ReadUInt32BigEndian();
+ obj.CompressionType = (WiaRvzCompressionType)data.ReadUInt32BigEndian();
+ obj.CompressionLevel = data.ReadInt32BigEndian();
+ obj.ChunkSize = data.ReadUInt32BigEndian();
+ obj.DiscHeader = data.ReadBytes(0x80);
+ obj.NumberOfPartitionEntries = data.ReadUInt32BigEndian();
+ obj.PartitionEntrySize = data.ReadUInt32BigEndian();
+ obj.PartitionEntriesOffset = data.ReadUInt64BigEndian();
+ obj.PartitionEntriesHash = data.ReadBytes(20);
+ obj.NumberOfRawDataEntries = data.ReadUInt32BigEndian();
+ obj.RawDataEntriesOffset = data.ReadUInt64BigEndian();
+ obj.RawDataEntriesSize = data.ReadUInt32BigEndian();
+ obj.NumberOfGroupEntries = data.ReadUInt32BigEndian();
+ obj.GroupEntriesOffset = data.ReadUInt64BigEndian();
+ obj.GroupEntriesSize = data.ReadUInt32BigEndian();
+ obj.CompressorDataSize = data.ReadByteValue();
+ obj.CompressorData = data.ReadBytes(7);
+
+ return obj;
+ }
}
}
diff --git a/SabreTools.Serialization.Writers/GCZ.cs b/SabreTools.Serialization.Writers/GCZ.cs
index 2f90b496..41285de8 100644
--- a/SabreTools.Serialization.Writers/GCZ.cs
+++ b/SabreTools.Serialization.Writers/GCZ.cs
@@ -18,7 +18,7 @@ namespace SabreTools.Serialization.Writers
if (string.IsNullOrEmpty(path))
return false;
- if (obj is null || !ValidateArchive(obj))
+ if (obj is null || !ValidateDiscImage(obj))
return false;
using var fs = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
@@ -35,41 +35,56 @@ namespace SabreTools.Serialization.Writers
if (stream is null || !stream.CanWrite)
return false;
- if (obj is null || !ValidateArchive(obj))
+ if (obj is null || !ValidateDiscImage(obj))
return false;
// Header (32 bytes, little-endian)
- stream.WriteLittleEndian(obj.Header.MagicCookie);
- stream.WriteLittleEndian(obj.Header.SubType);
- stream.WriteLittleEndian(obj.Header.CompressedDataSize);
- stream.WriteLittleEndian(obj.Header.DataSize);
- stream.WriteLittleEndian(obj.Header.BlockSize);
- stream.WriteLittleEndian(obj.Header.NumBlocks);
+ WriteHeader(stream, obj.Header);
// Block pointer table (8 bytes per block, little-endian)
- foreach (ulong ptr in obj.BlockPointers)
- stream.WriteLittleEndian(ptr);
+ foreach (ulong pointer in obj.BlockPointers)
+ {
+ stream.WriteLittleEndian(pointer);
+ }
// Block hash table (4 bytes per block, little-endian)
foreach (uint hash in obj.BlockHashes)
+ {
stream.WriteLittleEndian(hash);
+ }
stream.Flush();
return true;
}
- private static bool ValidateArchive(DiscImage obj)
+ ///
+ /// Write GczHeader data to the stream
+ ///
+ /// Stream to write to
+ public static void WriteHeader(Stream stream, GczHeader obj)
+ {
+ stream.WriteLittleEndian(obj.MagicCookie);
+ stream.WriteLittleEndian(obj.SubType);
+ stream.WriteLittleEndian(obj.CompressedDataSize);
+ stream.WriteLittleEndian(obj.DataSize);
+ stream.WriteLittleEndian(obj.BlockSize);
+ stream.WriteLittleEndian(obj.NumBlocks);
+ }
+
+ ///
+ /// Validate that disc image is writable
+ ///
+ private static bool ValidateDiscImage(DiscImage obj)
{
- if (obj.Header is null)
- return false;
if (obj.Header.MagicCookie != Constants.MagicCookie)
return false;
if (obj.Header.NumBlocks == 0)
return false;
- if (obj.BlockPointers is null || obj.BlockPointers.Length != (int)obj.Header.NumBlocks)
+ if (obj.BlockPointers.Length != obj.Header.NumBlocks)
return false;
- if (obj.BlockHashes is null || obj.BlockHashes.Length != (int)obj.Header.NumBlocks)
+ if (obj.BlockHashes.Length != obj.Header.NumBlocks)
return false;
+
return true;
}
}
diff --git a/SabreTools.Serialization.Writers/WIA.cs b/SabreTools.Serialization.Writers/WIA.cs
index e32e430c..d6fcc765 100644
--- a/SabreTools.Serialization.Writers/WIA.cs
+++ b/SabreTools.Serialization.Writers/WIA.cs
@@ -1,6 +1,8 @@
+using System;
using System.IO;
using SabreTools.Data.Models.WIA;
using SabreTools.Numerics.Extensions;
+using static SabreTools.Data.Models.WIA.Constants;
namespace SabreTools.Serialization.Writers
{
@@ -18,7 +20,7 @@ namespace SabreTools.Serialization.Writers
if (string.IsNullOrEmpty(path))
return false;
- if (obj is null || !ValidateArchive(obj))
+ if (obj is null || !ValidateDiscImage(obj))
return false;
using var fs = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
@@ -35,121 +37,170 @@ namespace SabreTools.Serialization.Writers
if (stream is null || !stream.CanWrite)
return false;
- if (obj is null || !ValidateArchive(obj))
+ if (obj is null || !ValidateDiscImage(obj))
return false;
- WriteHeader1(stream, obj.Header1);
- WriteHeader2(stream, obj.Header2);
+ WriteWiaHeader1(stream, obj.Header1);
+ WriteWiaHeader2(stream, obj.Header2);
// Partition entries
- if (obj.PartitionEntries != null)
+ if (obj.PartitionEntries is not null)
{
foreach (var pe in obj.PartitionEntries)
+ {
WritePartitionEntry(stream, pe);
+ }
}
// Raw data entries
foreach (var re in obj.RawDataEntries)
+ {
WriteRawDataEntry(stream, re);
+ }
// Group entries
- if (obj.Header1.Magic == Constants.RvzMagic && obj.RvzGroupEntries != null)
+ if (obj.Header1.Magic == RvzMagic && obj.RvzGroupEntries is not null)
{
foreach (var ge in obj.RvzGroupEntries)
+ {
WriteRvzGroupEntry(stream, ge);
+ }
}
- else if (obj.Header1.Magic != Constants.RvzMagic && obj.GroupEntries != null)
+ else if (obj.Header1.Magic != RvzMagic && obj.GroupEntries is not null)
{
foreach (var ge in obj.GroupEntries)
+ {
WriteWiaGroupEntry(stream, ge);
+ }
}
stream.Flush();
return true;
}
- private static bool ValidateArchive(DiscImage obj)
+ ///
+ /// Write PartitionDataEntry data to the stream
+ ///
+ /// Stream to write to
+ public static void WritePartitionDataEntry(Stream stream, PartitionDataEntry obj)
{
- if (obj.Header1 is null || obj.Header2 is null)
- return false;
- if (obj.Header1.Magic != Constants.WiaMagic && obj.Header1.Magic != Constants.RvzMagic)
+ stream.WriteBigEndian(obj.FirstSector);
+ stream.WriteBigEndian(obj.NumberOfSectors);
+ stream.WriteBigEndian(obj.GroupIndex);
+ stream.WriteBigEndian(obj.NumberOfGroups);
+ stream.Flush();
+ }
+
+ ///
+ /// Write PartitionEntry data to the stream
+ ///
+ /// Stream to write to
+ public static void WritePartitionEntry(Stream stream, PartitionEntry obj)
+ {
+ stream.Write(obj.PartitionKey);
+ WritePartitionDataEntry(stream, obj.DataEntry0);
+ WritePartitionDataEntry(stream, obj.DataEntry1);
+ stream.Flush();
+ }
+
+ ///
+ /// Write RawDataEntry data to the stream
+ ///
+ /// Stream to write to
+ public static void WriteRawDataEntry(Stream stream, RawDataEntry obj)
+ {
+ stream.WriteBigEndian(obj.DataOffset);
+ stream.WriteBigEndian(obj.DataSize);
+ stream.WriteBigEndian(obj.GroupIndex);
+ stream.WriteBigEndian(obj.NumberOfGroups);
+ stream.Flush();
+ }
+
+ ///
+ /// Write RvzGroupEntry data to the stream
+ ///
+ /// Stream to write to
+ public static void WriteRvzGroupEntry(Stream stream, RvzGroupEntry obj)
+ {
+ stream.WriteBigEndian(obj.DataOffset);
+ stream.WriteBigEndian(obj.DataSize);
+ stream.WriteBigEndian(obj.RvzPackedSize);
+ stream.Flush();
+ }
+
+ ///
+ /// Write WiaGroupEntry data to the stream
+ ///
+ /// Stream to write to
+ public static void WriteWiaGroupEntry(Stream stream, WiaGroupEntry obj)
+ {
+ stream.WriteBigEndian(obj.DataOffset);
+ stream.WriteBigEndian(obj.DataSize);
+ stream.Flush();
+ }
+
+ ///
+ /// Write WiaHeader1 data to the stream
+ ///
+ /// Stream to write to
+ public static void WriteWiaHeader1(Stream stream, WiaHeader1 obj)
+ {
+ stream.WriteLittleEndian(obj.Magic);
+ stream.WriteBigEndian(obj.Version);
+ stream.WriteBigEndian(obj.VersionCompatible);
+ stream.WriteBigEndian(obj.Header2Size);
+ stream.Write(obj.Header2Hash);
+ stream.WriteBigEndian(obj.IsoFileSize);
+ stream.WriteBigEndian(obj.WiaFileSize);
+ stream.Write(obj.Header1Hash);
+ stream.Flush();
+ }
+
+ ///
+ /// Write WiaHeader2 data to the stream
+ ///
+ /// Stream to write to
+ public static void WriteWiaHeader2(Stream stream, WiaHeader2 obj)
+ {
+ stream.WriteBigEndian((uint)obj.DiscType);
+ stream.WriteBigEndian((uint)obj.CompressionType);
+ stream.WriteBigEndian(obj.CompressionLevel);
+ stream.WriteBigEndian(obj.ChunkSize);
+
+ byte[] dh = obj.DiscHeader;
+ stream.Write(dh, 0, Math.Min(dh.Length, DiscHeaderStoredSize));
+ if (dh.Length < DiscHeaderStoredSize)
+ stream.Write(new byte[DiscHeaderStoredSize - dh.Length], 0, DiscHeaderStoredSize - dh.Length);
+
+ stream.WriteBigEndian(obj.NumberOfPartitionEntries);
+ stream.WriteBigEndian(obj.PartitionEntrySize);
+ stream.WriteBigEndian(obj.PartitionEntriesOffset);
+ stream.Write(obj.PartitionEntriesHash);
+ stream.WriteBigEndian(obj.NumberOfRawDataEntries);
+ stream.WriteBigEndian(obj.RawDataEntriesOffset);
+ stream.WriteBigEndian(obj.RawDataEntriesSize);
+ stream.WriteBigEndian(obj.NumberOfGroupEntries);
+ stream.WriteBigEndian(obj.GroupEntriesOffset);
+ stream.WriteBigEndian(obj.GroupEntriesSize);
+ stream.WriteByte(obj.CompressorDataSize);
+
+ byte[] prop = obj.CompressorData;
+ stream.Write(prop, 0, Math.Min(prop.Length, 7));
+ if (prop.Length < 7)
+ stream.Write(new byte[7 - prop.Length], 0, 7 - prop.Length);
+
+ stream.Flush();
+ }
+
+ ///
+ /// Validate that disc image is writable
+ ///
+ private static bool ValidateDiscImage(DiscImage obj)
+ {
+ if (obj.Header1.Magic != WiaMagic && obj.Header1.Magic != RvzMagic)
return false;
+
return true;
}
-
- #region Write helpers
-
- private static void WriteHeader1(Stream s, WiaHeader1 h)
- {
- s.WriteLittleEndian(h.Magic);
- s.WriteBigEndian(h.Version);
- s.WriteBigEndian(h.VersionCompatible);
- s.WriteBigEndian(h.Header2Size);
- s.Write(h.Header2Hash, 0, 20);
- s.WriteBigEndian(h.IsoFileSize);
- s.WriteBigEndian(h.WiaFileSize);
- s.Write(h.Header1Hash, 0, 20);
- }
-
- private static void WriteHeader2(Stream s, WiaHeader2 h)
- {
- s.WriteBigEndian((uint)h.DiscType);
- s.WriteBigEndian((uint)h.CompressionType);
- s.WriteBigEndian(h.CompressionLevel);
- s.WriteBigEndian(h.ChunkSize);
- s.Write(h.DiscHeader, 0, 0x80);
- s.WriteBigEndian(h.NumberOfPartitionEntries);
- s.WriteBigEndian(h.PartitionEntrySize);
- s.WriteBigEndian(h.PartitionEntriesOffset);
- s.Write(h.PartitionEntriesHash, 0, 20);
- s.WriteBigEndian(h.NumberOfRawDataEntries);
- s.WriteBigEndian(h.RawDataEntriesOffset);
- s.WriteBigEndian(h.RawDataEntriesSize);
- s.WriteBigEndian(h.NumberOfGroupEntries);
- s.WriteBigEndian(h.GroupEntriesOffset);
- s.WriteBigEndian(h.GroupEntriesSize);
- s.WriteByte(h.CompressorDataSize);
- s.Write(h.CompressorData, 0, 7);
- }
-
- private static void WritePartitionDataEntry(Stream s, PartitionDataEntry e)
- {
- s.WriteBigEndian(e.FirstSector);
- s.WriteBigEndian(e.NumberOfSectors);
- s.WriteBigEndian(e.GroupIndex);
- s.WriteBigEndian(e.NumberOfGroups);
- }
-
- private static void WritePartitionEntry(Stream s, PartitionEntry e)
- {
- s.Write(e.PartitionKey, 0, 16);
- WritePartitionDataEntry(s, e.DataEntry0);
- WritePartitionDataEntry(s, e.DataEntry1);
- }
-
- private static void WriteRawDataEntry(Stream s, RawDataEntry e)
- {
- s.WriteBigEndian(e.DataOffset);
- s.WriteBigEndian(e.DataSize);
- s.WriteBigEndian(e.GroupIndex);
- s.WriteBigEndian(e.NumberOfGroups);
- }
-
- private static void WriteWiaGroupEntry(Stream s, WiaGroupEntry e)
- {
- // DataOffset stored as actual_offset >> 2
- s.WriteBigEndian((uint)(e.DataOffset >> 2));
- s.WriteBigEndian(e.DataSize);
- }
-
- private static void WriteRvzGroupEntry(Stream s, RvzGroupEntry e)
- {
- // DataOffset stored as actual_offset >> 2
- s.WriteBigEndian((uint)(e.DataOffset >> 2));
- s.WriteBigEndian(e.DataSize);
- s.WriteBigEndian(e.RvzPackedSize);
- }
-
- #endregion
}
}
diff --git a/SabreTools.Serialization.Writers/XDVDFS.cs b/SabreTools.Serialization.Writers/XDVDFS.cs
index fcb526f9..d54c5391 100644
--- a/SabreTools.Serialization.Writers/XDVDFS.cs
+++ b/SabreTools.Serialization.Writers/XDVDFS.cs
@@ -46,6 +46,9 @@ namespace SabreTools.Serialization.Writers
return true;
}
+ ///
+ /// Validate that Volume is writable
+ ///
public static bool ValidateVolume(Volume? obj)
{
// If the data is invalid
diff --git a/SabreTools.Serialization/SabreTools.Serialization.csproj b/SabreTools.Serialization/SabreTools.Serialization.csproj
index 4db86006..d2e241db 100644
--- a/SabreTools.Serialization/SabreTools.Serialization.csproj
+++ b/SabreTools.Serialization/SabreTools.Serialization.csproj
@@ -54,6 +54,7 @@
+
diff --git a/SabreTools.Wrappers.Test/GCZTests.cs b/SabreTools.Wrappers.Test/GCZTests.cs
new file mode 100644
index 00000000..8de0ee2d
--- /dev/null
+++ b/SabreTools.Wrappers.Test/GCZTests.cs
@@ -0,0 +1,60 @@
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace SabreTools.Wrappers.Test
+{
+ public class GCZTests
+ {
+ [Fact]
+ public void NullArray_Null()
+ {
+ byte[]? data = null;
+ int offset = 0;
+ var actual = GCZ.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyArray_Null()
+ {
+ byte[]? data = [];
+ int offset = 0;
+ var actual = GCZ.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidArray_Null()
+ {
+ byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)];
+ int offset = 0;
+ var actual = GCZ.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void NullStream_Null()
+ {
+ Stream? data = null;
+ var actual = GCZ.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyStream_Null()
+ {
+ Stream? data = new MemoryStream([]);
+ var actual = GCZ.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidStream_Null()
+ {
+ Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]);
+ var actual = GCZ.Create(data);
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/SabreTools.Wrappers.Test/NintendoDiscEncryptionTests.cs b/SabreTools.Wrappers.Test/NintendoDiscEncryptionTests.cs
deleted file mode 100644
index 1a302a2b..00000000
--- a/SabreTools.Wrappers.Test/NintendoDiscEncryptionTests.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Security.Cryptography;
-using Newtonsoft.Json;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace SabreTools.Wrappers.Test
-{
- [Collection("NintendoDisc")]
- public class NintendoDiscEncryptionTests
- {
- private readonly ITestOutputHelper _output;
-
- public NintendoDiscEncryptionTests(ITestOutputHelper output)
- {
- _output = output;
- }
- // -----------------------------------------------------------------------
- // DecryptTitleKey — no provider set
- // -----------------------------------------------------------------------
-
- [Fact]
- public void DecryptTitleKey_NoProvider_ReturnsNull()
- {
- NintendoDisc.CommonKeyProvider = null;
- Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[8], 0));
- }
-
- // -----------------------------------------------------------------------
- // DecryptTitleKey — argument guards
- // -----------------------------------------------------------------------
-
- [Fact]
- public void DecryptTitleKey_NullEncKey_ReturnsNull()
- {
- NintendoDisc.CommonKeyProvider = _ => new byte[16];
- try { Assert.Null(NintendoDisc.DecryptTitleKey(null!, new byte[8], 0)); }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- [Fact]
- public void DecryptTitleKey_WrongLengthEncKey_ReturnsNull()
- {
- NintendoDisc.CommonKeyProvider = _ => new byte[16];
- try { Assert.Null(NintendoDisc.DecryptTitleKey(new byte[8], new byte[8], 0)); }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- [Fact]
- public void DecryptTitleKey_NullTitleId_ReturnsNull()
- {
- NintendoDisc.CommonKeyProvider = _ => new byte[16];
- try { Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], null!, 0)); }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- [Fact]
- public void DecryptTitleKey_WrongLengthTitleId_ReturnsNull()
- {
- NintendoDisc.CommonKeyProvider = _ => new byte[16];
- try { Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[4], 0)); }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- // -----------------------------------------------------------------------
- // DecryptTitleKey — provider returns null for unknown index
- // -----------------------------------------------------------------------
-
- [Fact]
- public void DecryptTitleKey_UnknownIndex_ReturnsNull()
- {
- NintendoDisc.CommonKeyProvider = _ => null;
- try { Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[8], 0)); }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- // -----------------------------------------------------------------------
- // DecryptTitleKey — round-trip with injected key
- // -----------------------------------------------------------------------
-
- [Fact]
- public void DecryptTitleKey_WithInjectedKey_RoundTrips()
- {
- byte[] commonKey =
- {
- 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D,
- 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
- };
- byte[] plainTitleKey =
- {
- 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
- 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
- };
- byte[] titleId = { 0x00, 0x01, 0x00, 0x45, 0x52, 0x53, 0x42, 0x00 };
-
- byte[] iv = new byte[16];
- Array.Copy(titleId, 0, iv, 0, 8);
- byte[] encTitleKey = AesCbc.Encrypt(plainTitleKey, commonKey, iv)
- ?? throw new InvalidOperationException("AesCbc.Encrypt returned null");
-
- NintendoDisc.CommonKeyProvider = _ => commonKey;
- try
- {
- byte[]? decrypted = NintendoDisc.DecryptTitleKey(encTitleKey, titleId, 0);
- Assert.NotNull(decrypted);
- Assert.Equal(plainTitleKey, decrypted);
- }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- // -----------------------------------------------------------------------
- // Integration test — reads a real key file supplied by the user.
- //
- // Copy keys.json.example to keys.json and fill in the real key bytes.
- // The test is silently skipped when the file is absent OR when the loaded
- // keys do not hash to the expected SHA256 values hardcoded below, so CI
- // stays green without real keys in the repository.
- // -----------------------------------------------------------------------
-
- // SHA256(retail common key bytes)
- private const string RetailKeySha256 = "de38aeab4fe0c36d828a47e6fd315100e7ce234d3b00aa25e6ad6f5ff2824af8";
- // SHA256(Korean common key bytes)
- private const string KoreanKeySha256 = "b9f42ca27a1e178f0f14ebf1a05d486fa8db8d08875336c4e6e8dfae29f2901c";
-
- [Fact]
- public void LoadFromKeyFile_RealKeys_DecryptTitleKey_Succeeds()
- {
- string keyFile = Path.Combine(
- AppContext.BaseDirectory, "TestData", "NintendoDisc", "keys.json");
-
- _output.WriteLine($"Looking for key file: {keyFile}");
-
- if (!File.Exists(keyFile))
- {
- _output.WriteLine("Key file not found — test skipped.");
- return;
- }
-
- _output.WriteLine("Key file found. Parsing...");
- var provider = LoadKeyProvider(keyFile);
- NintendoDisc.CommonKeyProvider = provider;
- try
- {
- byte[]? retail = provider.Invoke(0);
- byte[]? korean = provider.Invoke(1);
-
- string retailHash = retail is null ? "(missing)" : Sha256Hex(retail);
- string koreanHash = korean is null ? "(missing)" : Sha256Hex(korean);
-
- _output.WriteLine($"retail (index 0) SHA256 : {retailHash}");
- _output.WriteLine($" expected : {RetailKeySha256}");
- _output.WriteLine($" match : {retailHash == RetailKeySha256}");
-
- _output.WriteLine($"korean (index 1) SHA256 : {koreanHash}");
- _output.WriteLine($" expected : {KoreanKeySha256}");
- _output.WriteLine($" match : {koreanHash == KoreanKeySha256}");
-
- if (retail is null || retailHash != RetailKeySha256)
- {
- _output.WriteLine("retail key did not match — integration assertions skipped.");
- return;
- }
- if (korean is null || koreanHash != KoreanKeySha256)
- {
- _output.WriteLine("korean key did not match — integration assertions skipped.");
- return;
- }
-
- _output.WriteLine("Both keys verified — running assertions.");
- Assert.Equal(16, retail.Length);
- Assert.Equal(16, korean.Length);
- _output.WriteLine("Assertions passed.");
- }
- finally { NintendoDisc.CommonKeyProvider = null; }
- }
-
- private static string Sha256Hex(byte[] data)
- {
- using var sha = SHA256.Create();
- return BitConverter.ToString(sha.ComputeHash(data)).Replace("-", string.Empty).ToLowerInvariant();
- }
-
- // -----------------------------------------------------------------------
- // Helper — parses the named JSON key file and returns a provider delegate.
- // Lives here in the test project; the library itself never does file I/O.
- // -----------------------------------------------------------------------
-
- ///
- /// Parses a named Wii common-key JSON file and returns a
- /// -compatible delegate.
- ///
- ///
- /// Expected file format:
- ///
- /// [
- /// { "name": "retail", "index": 0, "key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" },
- /// { "name": "korean", "index": 1, "key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
- /// ]
- ///
- /// Whitespace inside hex strings is ignored. Returns from the
- /// delegate for any index not present in the file.
- ///
- internal static Func LoadKeyProvider(string path)
- {
- string json = File.ReadAllText(path);
- var entries = JsonConvert.DeserializeObject>(json)
- ?? throw new FormatException("Key file could not be deserialized.");
-
- var map = new Dictionary();
- foreach (var entry in entries)
- {
- if (entry.Key is null)
- throw new FormatException($"Entry '{entry.Name}' is missing a key value.");
-
- string hex = entry.Key.Replace(" ", string.Empty).Replace("-", string.Empty);
- if (hex.Length != 32)
- throw new FormatException($"Entry '{entry.Name}' key must be 16 bytes (32 hex chars), got {hex.Length / 2}.");
-
- byte[] bytes = new byte[16];
- for (int i = 0; i < 16; i++)
- bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
-
- map[entry.Index] = bytes;
- }
-
- return index => map.TryGetValue(index, out byte[]? k) ? k : null;
- }
-
- private sealed class WiiKeyEntry
- {
- [JsonProperty("name")] public string? Name { get; set; }
- [JsonProperty("index")] public byte Index { get; set; }
- [JsonProperty("key")] public string? Key { get; set; }
- }
- }
-}
diff --git a/SabreTools.Wrappers.Test/NintendoDiscTests.cs b/SabreTools.Wrappers.Test/NintendoDiscTests.cs
new file mode 100644
index 00000000..633433af
--- /dev/null
+++ b/SabreTools.Wrappers.Test/NintendoDiscTests.cs
@@ -0,0 +1,94 @@
+using System;
+using Xunit;
+
+namespace SabreTools.Wrappers.Test
+{
+ [Collection("NintendoDisc")]
+ public class NintendoDiscTests
+ {
+ #region DecryptTitleKey
+
+ [Fact]
+ public void DecryptTitleKey_EmptyKeys_Null()
+ {
+ NintendoDisc.RetailCommonKey = [];
+ NintendoDisc.KoreanCommonKey = [];
+
+ Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[8], 0));
+ }
+
+ [Fact]
+ public void DecryptTitleKey_NullEncKey_Null()
+ {
+ NintendoDisc.RetailCommonKey = new byte[16];
+ NintendoDisc.KoreanCommonKey = new byte[16];
+
+ Assert.Null(NintendoDisc.DecryptTitleKey(null, new byte[8], 0));
+ }
+
+ [Fact]
+ public void DecryptTitleKey_WrongLengthEncKey_Null()
+ {
+ NintendoDisc.RetailCommonKey = new byte[16];
+ NintendoDisc.KoreanCommonKey = new byte[16];
+
+ Assert.Null(NintendoDisc.DecryptTitleKey(new byte[8], new byte[8], 0));
+ }
+
+ [Fact]
+ public void DecryptTitleKey_NullTitleId_Null()
+ {
+ NintendoDisc.RetailCommonKey = new byte[16];
+ NintendoDisc.KoreanCommonKey = new byte[16];
+
+ Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], null, 0));
+ }
+
+ [Fact]
+ public void DecryptTitleKey_WrongLengthTitleId_Null()
+ {
+ NintendoDisc.RetailCommonKey = new byte[16];
+ NintendoDisc.KoreanCommonKey = new byte[16];
+
+ Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[4], 0));
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(2)]
+ public void DecryptTitleKey_UnknownIndex_Null(int index)
+ {
+ Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[8], index));
+ }
+
+ [Fact]
+ public void DecryptTitleKey_WithInjectedKey_RoundTrips()
+ {
+ byte[] commonKey =
+ [
+ 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D,
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
+ ];
+ byte[] plainTitleKey =
+ [
+ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+ 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
+ ];
+ byte[] titleId = [0x00, 0x01, 0x00, 0x45, 0x52, 0x53, 0x42, 0x00];
+
+ byte[] iv = new byte[16];
+ Array.Copy(titleId, 0, iv, 0, 8);
+ byte[] encTitleKey = AesCbc.Encrypt(plainTitleKey, commonKey, iv)
+ ?? throw new InvalidOperationException("AesCbc.Encrypt returned null");
+
+ NintendoDisc.RetailCommonKey = commonKey;
+ NintendoDisc.KoreanCommonKey = commonKey;
+
+ byte[]? decrypted = NintendoDisc.DecryptTitleKey(encTitleKey, titleId, 0);
+ Assert.NotNull(decrypted);
+ Assert.Equal(plainTitleKey, decrypted);
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.Wrappers.Test/WIATests.cs b/SabreTools.Wrappers.Test/WIATests.cs
index 7d04ac9a..bdfef0be 100644
--- a/SabreTools.Wrappers.Test/WIATests.cs
+++ b/SabreTools.Wrappers.Test/WIATests.cs
@@ -3,15 +3,48 @@ using System.IO;
using System.Linq;
using SabreTools.Numerics.Extensions;
using Xunit;
+using static SabreTools.Data.Models.NintendoDisc.Constants;
namespace SabreTools.Wrappers.Test
{
[Collection("NintendoDisc")]
public class WIATests
{
- // -----------------------------------------------------------------------
- // WIA.Create null / invalid guards
- // -----------------------------------------------------------------------
+ ///
+ /// Arbitrary test-only common key — no relation to any real Wii key.
+ /// Used by both and .
+ ///
+ private static readonly byte[] TestCommonKey =
+ [
+ 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D,
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
+ ];
+
+ #region Constants
+
+ private const int HeaderAreaSize = 0x8000;
+
+ private const long IsoSize = Partition1Data + WiiGroupSize;
+
+ private const long Partition0Offset = 0x60000;
+
+ private const long Partition0Data = Partition0Offset + HeaderAreaSize;
+
+ private const long Partition1Offset = Partition0Data + WiiGroupSize;
+
+ private const long Partition1Data = Partition1Offset + HeaderAreaSize;
+
+ private const long PartitionListOffset = 0x50000;
+
+ private const long PartitionTableOffset = 0x40000;
+
+ #endregion
+
+ public WIATests()
+ {
+ NintendoDisc.RetailCommonKey = TestCommonKey;
+ NintendoDisc.KoreanCommonKey = TestCommonKey;
+ }
[Fact]
public void NullArray_Null()
@@ -64,66 +97,21 @@ namespace SabreTools.Wrappers.Test
Assert.Null(actual);
}
- // -----------------------------------------------------------------------
- // DumpIso guard
- // -----------------------------------------------------------------------
-
+ ///
+ /// Build the smallest valid WIA we can to get a non-null wrapper,
+ /// but for the guard test we only need to exercise the null-path branch.
+ /// We can create a real wrapper via the round-trip helper and then call
+ /// DumpIso with a null path — that must return false.
+ ///
[Fact]
public void DumpIso_NullPath_ReturnsFalse()
{
- // Build the smallest valid WIA we can to get a non-null wrapper,
- // but for the guard test we only need to exercise the null-path branch.
- // We can create a real wrapper via the round-trip helper and then call
- // DumpIso with a null path — that must return false.
var wia = BuildMinimalWiiWia();
+
Assert.NotNull(wia);
- Assert.False(wia!.DumpIso(null!));
+ Assert.False(wia!.DumpIso(null));
}
- // -----------------------------------------------------------------------
- // -----------------------------------------------------------------------
- // Helpers
- // -----------------------------------------------------------------------
-
- ///
- /// Builds a minimal synthetic Wii disc (one WiiGroup per partition) and returns a live
- /// wrapper backed by a .
- /// Returns null if any step fails.
- ///
- private static WIA? BuildMinimalWiiWia()
- {
- NintendoDisc.CommonKeyProvider = _ => TestCommonKey;
- try
- {
- byte[] iso = BuildMinimalWiiIso(TestCommonKey);
- var nd = NintendoDisc.Create(new MemoryStream(iso));
- if (nd is null) return null;
-
- var ms = new MemoryStream();
- bool ok = WIA.ConvertFromDiscToStream(nd, ms,
- isRvz: false,
- compressionType: Data.Models.WIA.WiaRvzCompressionType.None,
- compressionLevel: 5,
- chunkSize: Data.Models.WIA.Constants.DefaultChunkSize,
- out _);
- if (!ok) return null;
- ms.Position = 0;
- return WIA.Create(ms);
- }
- catch
- {
- return null;
- }
- finally
- {
- NintendoDisc.CommonKeyProvider = null;
- }
- }
-
- // -----------------------------------------------------------------------
- // Round-trip: Wii (partition crypto — encrypt → WIA → dump → decrypt)
- // -----------------------------------------------------------------------
-
///
/// Builds a synthetic Wii disc with 2 fake partitions (each 1 WiiGroup = 64 × 0x8000 bytes of
/// known plaintext encrypted with an arbitrary key), converts it to WIA (NONE compression),
@@ -134,10 +122,10 @@ namespace SabreTools.Wrappers.Test
/// This exercises both directions:
/// • WIA write path re-encrypts partition data correctly ()
/// • WIA read path () re-encrypts WIA decrypted groups back to
- /// ISO-layout AES-CBC blocks via GetCachedEncGroup / EncryptWiiGroup
+ /// ISO-layout AES-CBC blocks via GetCachedEncGroup / EncryptWiiGroup
///
/// Anti-bias: the final decryption uses — a single-block
- /// AES-CBC call that is completely independent of EncryptWiiGroup — so a symmetric bug
+ /// AES-CBC call that is completely independent of EncryptWiiGroup — so a symmetric bug
/// (broken encrypt paired with broken decrypt) would still fail the plaintext comparison.
/// The title key is encrypted via (BouncyCastle), while the
/// verification uses — a different code path.
@@ -145,9 +133,6 @@ namespace SabreTools.Wrappers.Test
[Fact]
public void Wii_WiaNoneRoundTrip_Succeeds()
{
- NintendoDisc.CommonKeyProvider = _ => TestCommonKey;
- try
- {
// ---- Build synthetic Wii ISO ----
byte[] iso = BuildMinimalWiiIso(TestCommonKey);
@@ -165,8 +150,7 @@ namespace SabreTools.Wrappers.Test
compressionLevel: 5,
chunkSize: Data.Models.WIA.Constants.DefaultChunkSize,
out Exception? writeEx);
- Assert.True(written,
- $"ConvertFromDiscToStream failed: {writeEx?.GetType().Name}: {writeEx?.Message}\n{writeEx?.StackTrace}");
+ Assert.True(written, $"ConvertFromDiscToStream failed: {writeEx?.GetType().Name}: {writeEx?.Message}\n{writeEx?.StackTrace}");
// ---- Decompress back to ISO ----
wiaMs.Position = 0;
@@ -181,58 +165,85 @@ namespace SabreTools.Wrappers.Test
byte[] dumpedIso = File.ReadAllBytes(tempIso);
- const int WiiBlockSize = 0x8000;
- const int WiiBlockDataSize = 0x7C00;
- const int WiiBlocksPerGroup = 64;
- const int WiiGroupSize = WiiBlocksPerGroup * WiiBlockSize;
- const int HeaderAreaSize = 0x8000;
- const long Partition0Offset = 0x60000;
- const long Partition0Data = Partition0Offset + HeaderAreaSize;
- const long Partition1Offset = Partition0Data + WiiGroupSize;
- const long Partition1Data = Partition1Offset + HeaderAreaSize;
-
- byte[] titleKey = new byte[16]
- {
+ byte[] titleKey =
+ [
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
- 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
- };
+ 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
+ ];
byte[] plain0 = new byte[WiiBlocksPerGroup * WiiBlockDataSize];
- for (int i = 0; i < plain0.Length; i++) plain0[i] = 0xAA;
+ for (int i = 0; i < plain0.Length; i++)
+ {
+ plain0[i] = 0xAA;
+ }
+
byte[] plain1 = new byte[WiiBlocksPerGroup * WiiBlockDataSize];
- for (int i = 0; i < plain1.Length; i++) plain1[i] = 0xBB;
+ for (int i = 0; i < plain1.Length; i++)
+ {
+ plain1[i] = 0xBB;
+ }
// ---- Anti-bias verification: decrypt each block using DecryptBlock only ----
- VerifyPartitionPlaintext(dumpedIso, Partition0Data, plain0, titleKey,
- WiiBlocksPerGroup, WiiBlockSize, WiiBlockDataSize, partitionLabel: "Partition 0");
+ VerifyPartitionPlaintext(dumpedIso,
+ Partition0Data,
+ plain0,
+ titleKey,
+ WiiBlocksPerGroup,
+ WiiBlockSize,
+ WiiBlockDataSize,
+ partitionLabel: "Partition 0");
- VerifyPartitionPlaintext(dumpedIso, Partition1Data, plain1, titleKey,
- WiiBlocksPerGroup, WiiBlockSize, WiiBlockDataSize, partitionLabel: "Partition 1");
+ VerifyPartitionPlaintext(dumpedIso,
+ Partition1Data,
+ plain1,
+ titleKey,
+ WiiBlocksPerGroup,
+ WiiBlockSize,
+ WiiBlockDataSize,
+ partitionLabel: "Partition 1");
}
finally
{
- if (File.Exists(tempIso)) File.Delete(tempIso);
- }
- }
- finally
- {
- NintendoDisc.CommonKeyProvider = null;
+ if (File.Exists(tempIso))
+ File.Delete(tempIso);
}
}
- // -----------------------------------------------------------------------
- // Wii test helpers
- // -----------------------------------------------------------------------
+ #region Wii test helpers
///
- /// Arbitrary test-only common key — no relation to any real Wii key.
- /// Used by both and .
+ /// Builds a minimal synthetic Wii disc (one WiiGroup per partition) and returns a live
+ /// wrapper backed by a .
+ /// Returns null if any step fails.
///
- private static readonly byte[] TestCommonKey =
+ private static WIA? BuildMinimalWiiWia()
{
- 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D,
- 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
- };
+ try
+ {
+ byte[] iso = BuildMinimalWiiIso(TestCommonKey);
+ var nd = NintendoDisc.Create(new MemoryStream(iso));
+ if (nd is null)
+ return null;
+
+ var ms = new MemoryStream();
+ bool ok = WIA.ConvertFromDiscToStream(nd, ms,
+ isRvz: false,
+ compressionType: Data.Models.WIA.WiaRvzCompressionType.None,
+ compressionLevel: 5,
+ chunkSize: Data.Models.WIA.Constants.DefaultChunkSize,
+ out _);
+
+ if (!ok)
+ return null;
+
+ ms.Position = 0;
+ return WIA.Create(ms);
+ }
+ catch
+ {
+ return null;
+ }
+ }
///
/// Builds a minimal synthetic Wii ISO with 2 partitions (1 WiiGroup each), encrypted
@@ -240,37 +251,29 @@ namespace SabreTools.Wrappers.Test
///
private static byte[] BuildMinimalWiiIso(byte[] commonKey)
{
- const int WiiBlockSize = 0x8000;
- const int WiiBlockDataSize = 0x7C00;
- const int WiiBlocksPerGroup = 64;
- const int WiiGroupDataSize = WiiBlocksPerGroup * WiiBlockDataSize;
- const int WiiGroupSize = WiiBlocksPerGroup * WiiBlockSize;
-
- byte[] titleKey = new byte[16]
- {
+ byte[] titleKey =
+ [
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
- };
- byte[] titleId = new byte[8] { 0x00, 0x01, 0x00, 0x45, 0x52, 0x53, 0x42, 0x00 };
+ ];
+ byte[] titleId = [0x00, 0x01, 0x00, 0x45, 0x52, 0x53, 0x42, 0x00];
byte[] encTitleKey = EncryptTitleKeyIndependent(titleKey, titleId, commonKey);
byte[] plain0 = new byte[WiiGroupDataSize];
- for (int i = 0; i < plain0.Length; i++) plain0[i] = 0xAA;
+ for (int i = 0; i < plain0.Length; i++)
+ {
+ plain0[i] = 0xAA;
+ }
+
byte[] plain1 = new byte[WiiGroupDataSize];
- for (int i = 0; i < plain1.Length; i++) plain1[i] = 0xBB;
+ for (int i = 0; i < plain1.Length; i++)
+ {
+ plain1[i] = 0xBB;
+ }
byte[] enc0 = WIA.EncryptWiiGroup(plain0, titleKey, WiiBlocksPerGroup);
byte[] enc1 = WIA.EncryptWiiGroup(plain1, titleKey, WiiBlocksPerGroup);
- const long PartitionTableOffset = 0x40000;
- const long PartitionListOffset = 0x50000;
- const long Partition0Offset = 0x60000;
- const int HeaderAreaSize = 0x8000; // data starts one full block after partition base
- const long Partition0Data = Partition0Offset + HeaderAreaSize;
- const long Partition1Offset = Partition0Data + WiiGroupSize;
- const long Partition1Data = Partition1Offset + HeaderAreaSize;
- const long IsoSize = Partition1Data + WiiGroupSize;
-
byte[] iso = new byte[IsoSize];
iso[0] = (byte)'R'; iso[1] = (byte)'S'; iso[2] = (byte)'B'; iso[3] = (byte)'E';
@@ -296,8 +299,19 @@ namespace SabreTools.Wrappers.Test
return iso;
}
- private static void WritePartitionHeader(byte[] iso, long partOffset,
- byte[] encTitleKey, byte[] titleId, byte ckIdx)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void WritePartitionHeader(byte[] iso,
+ long partOffset,
+ byte[] encTitleKey,
+ byte[] titleId,
+ byte ckIdx)
{
// Signature type 0x10001 at partOffset+0
int off = (int)partOffset;
@@ -325,12 +339,16 @@ namespace SabreTools.Wrappers.Test
///
/// Decrypts each block of one WII partition in the dumped ISO using only
/// (a single-block AES-CBC call that is
- /// completely independent of EncryptWiiGroup) and asserts the decrypted
+ /// completely independent of EncryptWiiGroup) and asserts the decrypted
/// block data matches the corresponding slice of .
///
- private static void VerifyPartitionPlaintext(byte[] iso, long dataStart,
- byte[] expectedPlaintext, byte[] titleKey,
- int blocksPerGroup, int blockSize, int blockDataSize,
+ private static void VerifyPartitionPlaintext(byte[] iso,
+ long dataStart,
+ byte[] expectedPlaintext,
+ byte[] titleKey,
+ int blocksPerGroup,
+ int blockSize,
+ int blockDataSize,
string partitionLabel)
{
for (int b = 0; b < blocksPerGroup; b++)
@@ -370,6 +388,6 @@ namespace SabreTools.Wrappers.Test
?? throw new InvalidOperationException("AesCbc.Encrypt returned null");
}
-
- }
- }
+ #endregion
+ }
+}
diff --git a/SabreTools.Wrappers/GCZ.Extraction.cs b/SabreTools.Wrappers/GCZ.Extraction.cs
index bafc2833..24c0bd44 100644
--- a/SabreTools.Wrappers/GCZ.Extraction.cs
+++ b/SabreTools.Wrappers/GCZ.Extraction.cs
@@ -1,3 +1,8 @@
+using System.IO;
+using SabreTools.IO.Compression.Deflate;
+using SabreTools.IO.Extensions;
+using static SabreTools.Data.Models.GCZ.Constants;
+
namespace SabreTools.Wrappers
{
public partial class GCZ : IExtractable
@@ -5,9 +10,87 @@ namespace SabreTools.Wrappers
///
public bool Extract(string outputDirectory, bool includeDebug)
{
- // Decompress GCZ to obtain the inner disc image, then delegate extraction.
var inner = GetInnerWrapper();
- return inner?.Extract(outputDirectory, includeDebug) ?? false;
+ if (inner is null)
+ return false;
+
+ return inner.Extract(outputDirectory, includeDebug);
}
+
+ #region Inner Wrapper
+
+ ///
+ /// Returns a NintendoDisc wrapper backed by a virtual stream that decompresses
+ /// GCZ blocks on demand, avoiding loading the entire ISO into memory.
+ ///
+ public NintendoDisc? GetInnerWrapper()
+ {
+ if (BlockPointers.Length == 0)
+ return null;
+ if (Header.DataSize == 0)
+ return null;
+
+ var stream = new GczVirtualStream(this);
+ return NintendoDisc.Create(stream);
+ }
+
+ ///
+ /// Decompresses a single GCZ block by index and returns its raw bytes.
+ /// Returns null on failure; returns a zero-filled block if the compressed size is zero.
+ ///
+ internal byte[]? DecompressBlock(int blockIndex)
+ {
+ if (blockIndex < 0 || blockIndex >= BlockPointers.Length)
+ return null;
+
+ ulong ptr = BlockPointers[blockIndex];
+ long blockFileOffset = DataOffset + (long)(ptr & ~UncompressedFlag);
+
+ ulong nextRaw = (blockIndex + 1 < BlockPointers.Length)
+ ? BlockPointers[blockIndex + 1] & ~UncompressedFlag
+ : CompressedDataSize;
+
+ int compSize = (int)(nextRaw - (ptr & ~UncompressedFlag));
+ if (compSize <= 0)
+ return new byte[BlockSize];
+
+ byte[] raw = ReadRangeFromSource(blockFileOffset, compSize);
+ if (raw.Length != compSize)
+ return null;
+
+ // Verify Adler-32 checksum on the compressed (raw) data before decompressing
+ if (BlockHashes is not null && blockIndex < BlockHashes.Length)
+ {
+ uint actual = Adler.Adler32(1, raw, 0, raw.Length);
+ if (actual != BlockHashes[blockIndex])
+ return null;
+ }
+
+ // If the data is raw, just return
+ if ((ptr & UncompressedFlag) != 0)
+ return raw;
+
+ // GCZ blocks are zlib-framed: 2-byte header + deflate data + 4-byte Adler-32 trailer.
+ // Strip the frame and feed raw deflate data to DeflateStream.
+ if (raw.Length < 6)
+ return null;
+
+ try
+ {
+ using var cs = new MemoryStream(raw, 2, raw.Length - 6);
+ using var ds = new DeflateStream(cs, CompressionMode.Decompress);
+ using var os = new MemoryStream();
+
+ ds.BlockCopy(os, blockSize: 4096);
+
+ return os.ToArray();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
}
}
diff --git a/SabreTools.Wrappers/GCZ.Printing.cs b/SabreTools.Wrappers/GCZ.Printing.cs
index 629125fe..ab08d42c 100644
--- a/SabreTools.Wrappers/GCZ.Printing.cs
+++ b/SabreTools.Wrappers/GCZ.Printing.cs
@@ -1,4 +1,6 @@
using System.Text;
+using SabreTools.Data.Models.GCZ;
+using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Text.Extensions;
namespace SabreTools.Wrappers
@@ -18,25 +20,41 @@ namespace SabreTools.Wrappers
{
builder.AppendLine("GCZ Information:");
builder.AppendLine("-------------------------");
- builder.AppendLine(Header.MagicCookie, "Magic Cookie");
- builder.AppendLine(Header.SubType, "Sub-Type");
- builder.AppendLine(Header.CompressedDataSize, "Compressed Data Size");
- builder.AppendLine(Header.DataSize, "Uncompressed Data Size");
- builder.AppendLine(Header.BlockSize, "Block Size");
- builder.AppendLine(Header.NumBlocks, "Block Count");
- builder.AppendLine();
- var discHeader = DiscHeader;
- if (discHeader is not null)
+ Print(builder, Header);
+ Print(builder, DiscHeader);
+ }
+
+ private static void Print(StringBuilder builder, GczHeader header)
+ {
+ builder.AppendLine(" Header:");
+ builder.AppendLine(" -------------------------");
+
+ builder.AppendLine(header.MagicCookie, "Magic Cookie");
+ builder.AppendLine(header.SubType, "Sub-Type");
+ builder.AppendLine(header.CompressedDataSize, "Compressed Data Size");
+ builder.AppendLine(header.DataSize, "Uncompressed Data Size");
+ builder.AppendLine(header.BlockSize, "Block Size");
+ builder.AppendLine(header.NumBlocks, "Block Count");
+ builder.AppendLine();
+ }
+
+ private static void Print(StringBuilder builder, DiscHeader? header)
+ {
+ builder.AppendLine(" Embedded Disc Header:");
+ builder.AppendLine(" -------------------------");
+ if (header is null)
{
- builder.AppendLine("Embedded Disc Header:");
- builder.AppendLine(discHeader.GameId, " Game ID");
- builder.AppendLine(discHeader.MakerCode, " Maker Code");
- builder.AppendLine(discHeader.DiscNumber, " Disc Number");
- builder.AppendLine(discHeader.DiscVersion, " Disc Version");
- builder.AppendLine(discHeader.GameTitle, " Game Title");
+ builder.AppendLine(" No embedded disc header");
builder.AppendLine();
+ return;
}
+
+ builder.AppendLine(header.GameId, " Game ID");
+ builder.AppendLine(header.DiscNumber, " Disc Number");
+ builder.AppendLine(header.DiscVersion, " Disc Version");
+ builder.AppendLine(header.GameTitle, " Game Title");
+ builder.AppendLine();
}
}
}
diff --git a/SabreTools.Wrappers/GCZ.Writing.cs b/SabreTools.Wrappers/GCZ.Writing.cs
index ad853388..d026fef2 100644
--- a/SabreTools.Wrappers/GCZ.Writing.cs
+++ b/SabreTools.Wrappers/GCZ.Writing.cs
@@ -1,45 +1,14 @@
using System;
-using SabreTools.IO.Compression.Deflate;
using System.IO;
using SabreTools.Data.Models.GCZ;
+using SabreTools.IO.Compression.Deflate;
+using SabreTools.IO.Extensions;
+using SabreTools.Numerics.Extensions;
namespace SabreTools.Wrappers
{
public partial class GCZ : IWritable
{
- ///
- /// Compress a NintendoDisc wrapper to a GCZ file at the given path.
- ///
- /// Decompressed disc image to compress.
- /// Destination file path.
- ///
- /// GCZ block size: 32 KiB, 64 KiB, or 128 KiB.
- /// Defaults to (32 KiB).
- ///
- /// True on success, false on failure.
- public static bool ConvertFromDisc(NintendoDisc source, string outputPath,
- uint blockSize = Constants.DefaultBlockSize)
- {
- if (source is null)
- return false;
- if (string.IsNullOrEmpty(outputPath))
- return false;
- if (blockSize != Constants.BlockSize32K &&
- blockSize != Constants.BlockSize64K &&
- blockSize != Constants.BlockSize128K)
- return false;
-
- try
- {
- using var fs = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
- return WriteGcz(source, fs, blockSize);
- }
- catch
- {
- return false;
- }
- }
-
///
public bool Write(string outputPath, bool includeDebug)
{
@@ -53,19 +22,44 @@ namespace SabreTools.Wrappers
outputPath = Path.GetFullPath(outputFilename);
}
- if (Model?.Header is null)
- {
- if (includeDebug) Console.WriteLine("Model was invalid, cannot write!");
- return false;
- }
-
var writer = new Serialization.Writers.GCZ { Debug = includeDebug };
return writer.SerializeFile(Model, outputPath);
}
- // -----------------------------------------------------------------------
- // Core GCZ compression pipeline (ISO → GCZ)
- // -----------------------------------------------------------------------
+ #region Core GCZ Compression Pipeline (ISO -> GCZ)
+
+ ///
+ /// Compress a NintendoDisc wrapper to a GCZ file at the given path.
+ ///
+ /// Decompressed disc image to compress.
+ /// Destination file path.
+ ///
+ /// GCZ block size: 32 KiB, 64 KiB, or 128 KiB.
+ /// Defaults to (32 KiB).
+ ///
+ /// True on success, false on failure.
+ public static bool ConvertFromDisc(NintendoDisc source, string outputPath, uint blockSize = Constants.DefaultBlockSize)
+ {
+ if (string.IsNullOrEmpty(outputPath))
+ return false;
+
+ if (blockSize != Constants.BlockSize32K
+ && blockSize != Constants.BlockSize64K
+ && blockSize != Constants.BlockSize128K)
+ {
+ return false;
+ }
+
+ try
+ {
+ using var fs = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
+ return WriteGcz(source, fs, blockSize);
+ }
+ catch
+ {
+ return false;
+ }
+ }
///
/// Write a GCZ image to from a decompressed disc source.
@@ -83,36 +77,36 @@ namespace SabreTools.Wrappers
long headerPos = destination.Position;
var header = new GczHeader
{
- MagicCookie = Constants.MagicCookie,
- SubType = 0,
+ MagicCookie = Constants.MagicCookie,
+ SubType = 0,
CompressedDataSize = 0,
- DataSize = (ulong)sourceSize,
- BlockSize = blockSize,
- NumBlocks = numBlocks,
+ DataSize = (ulong)sourceSize,
+ BlockSize = blockSize,
+ NumBlocks = numBlocks,
};
- WriteHeader(destination, header);
+ Serialization.Writers.GCZ.WriteHeader(destination, header);
// ---- Step 2: Reserve block-pointer table (8 bytes each) ----
long blockTablePos = destination.Position;
- var blockPointers = new ulong[numBlocks];
- destination.Position += (long)numBlocks * 8;
+ var blockPointers = new ulong[numBlocks];
+ destination.SeekIfPossible(numBlocks * 8, SeekOrigin.Current);
// ---- Step 3: Reserve block-hash table (4 bytes each) ----
var blockHashes = new uint[numBlocks];
- destination.Position += (long)numBlocks * 4;
+ destination.SeekIfPossible(numBlocks * 4, SeekOrigin.Current);
// ---- Step 4: Data section starts here ----
- long dataStartPos = destination.Position;
- var readBuf = new byte[blockSize];
- var compressBuf = new byte[(int)blockSize * 2];
+ long dataStartPos = destination.Position;
+ var readBuf = new byte[blockSize];
+ var compressBuf = new byte[(int)blockSize * 2];
- for (uint bi = 0; bi < numBlocks; bi++)
+ for (int i = 0; i < numBlocks; i++)
{
- long blockOffset = (long)bi * blockSize;
- int blockDataSize = (int)Math.Min(blockSize, sourceSize - blockOffset);
+ long blockOffset = i * blockSize;
+ int blockDataSize = (int)Math.Min(blockSize, sourceSize - blockOffset);
- byte[]? raw = source.ReadData(blockOffset, blockDataSize);
- if (raw is null || raw.Length != blockDataSize)
+ byte[] raw = source.ReadData(blockOffset, blockDataSize);
+ if (raw.Length != blockDataSize)
return false;
if (blockDataSize < readBuf.Length)
@@ -123,20 +117,18 @@ namespace SabreTools.Wrappers
// Record pointer as offset relative to data section start
ulong blockPointer = (ulong)(destination.Position - dataStartPos);
- int compressedSize;
- bool useCompression = TryCompressBlock(readBuf, blockDataSize, compressBuf, out compressedSize);
-
+ bool useCompression = TryCompressBlock(readBuf, blockDataSize, compressBuf, out int compressedSize);
if (useCompression)
{
- blockPointers[bi] = blockPointer;
+ blockPointers[i] = blockPointer;
destination.Write(compressBuf, 0, compressedSize);
- blockHashes[bi] = Adler.Adler32(1, compressBuf, 0, compressedSize);
+ blockHashes[i] = Adler.Adler32(1, compressBuf, 0, compressedSize);
}
else
{
- blockPointers[bi] = blockPointer | Constants.UncompressedFlag;
+ blockPointers[i] = blockPointer | Constants.UncompressedFlag;
destination.Write(readBuf, 0, blockDataSize);
- blockHashes[bi] = Adler.Adler32(1, readBuf, 0, blockDataSize);
+ blockHashes[i] = Adler.Adler32(1, readBuf, 0, blockDataSize);
}
}
@@ -145,26 +137,30 @@ namespace SabreTools.Wrappers
header.CompressedDataSize = (ulong)(finalEnd - dataStartPos);
// ---- Step 6: Write block-pointer table ----
- destination.Position = blockTablePos;
+ destination.SeekIfPossible(blockTablePos, SeekOrigin.Begin);
foreach (ulong ptr in blockPointers)
- WriteUInt64LE(destination, ptr);
+ {
+ destination.WriteLittleEndian(ptr);
+ }
// ---- Step 7: Write block-hash table ----
foreach (uint h in blockHashes)
- WriteUInt32LE(destination, h);
+ {
+ destination.WriteLittleEndian(h);
+ }
// ---- Step 8: Patch header ----
- destination.Position = headerPos;
- WriteHeader(destination, header);
+ destination.SeekIfPossible(headerPos, SeekOrigin.Begin);
+ Serialization.Writers.GCZ.WriteHeader(destination, header);
destination.Position = finalEnd;
destination.Flush();
return true;
}
- // -----------------------------------------------------------------------
- // Compression helpers
- // -----------------------------------------------------------------------
+ #endregion
+
+ #region Compression Helpers
///
/// Attempts to zlib-compress bytes of
@@ -172,57 +168,31 @@ namespace SabreTools.Wrappers
/// when the result is smaller than 97 % of the original (Dolphin's threshold).
/// GCZ uses the zlib framing: 2-byte header (0x78 0x9C) + deflate stream + 4-byte Adler-32 tail.
///
-private static bool TryCompressBlock(byte[] input, int inputSize, byte[] output, out int compressedSize)
-{
- using (var ms = new MemoryStream(output))
- {
- ms.WriteByte(0x78);
- ms.WriteByte(0x9C);
-
- using (var ds = new DeflateStream(ms, CompressionMode.Compress, leaveOpen: true))
+ private static bool TryCompressBlock(byte[] input, int inputSize, byte[] output, out int compressedSize)
{
- ds.Write(input, 0, inputSize);
+ using (var ms = new MemoryStream(output))
+ {
+ ms.WriteByte(0x78);
+ ms.WriteByte(0x9C);
+
+ using (var ds = new DeflateStream(ms, CompressionMode.Compress, leaveOpen: true))
+ {
+ ds.Write(input, 0, inputSize);
+ }
+
+ uint adler = Adler.Adler32(1, input, 0, inputSize);
+ ms.WriteByte((byte)(adler >> 24));
+ ms.WriteByte((byte)(adler >> 16));
+ ms.WriteByte((byte)(adler >> 8));
+ ms.WriteByte((byte)adler);
+
+ compressedSize = (int)ms.Position;
+ }
+
+ int threshold = inputSize * 97 / 100;
+ return compressedSize < threshold;
}
- uint adler = Adler.Adler32(1, input, 0, inputSize);
- ms.WriteByte((byte)(adler >> 24));
- ms.WriteByte((byte)(adler >> 16));
- ms.WriteByte((byte)(adler >> 8));
- ms.WriteByte((byte)adler);
-
- compressedSize = (int)ms.Position;
- }
-
- int threshold = inputSize * 97 / 100;
- return compressedSize < threshold;
-}
-
- // -----------------------------------------------------------------------
- // Little-endian binary write helpers
- // -----------------------------------------------------------------------
-
- private static void WriteHeader(Stream s, GczHeader h)
- {
- WriteUInt32LE(s, h.MagicCookie);
- WriteUInt32LE(s, h.SubType);
- WriteUInt64LE(s, h.CompressedDataSize);
- WriteUInt64LE(s, h.DataSize);
- WriteUInt32LE(s, h.BlockSize);
- WriteUInt32LE(s, h.NumBlocks);
- }
-
- private static void WriteUInt32LE(Stream s, uint v)
- {
- s.WriteByte((byte)v);
- s.WriteByte((byte)(v >> 8));
- s.WriteByte((byte)(v >> 16));
- s.WriteByte((byte)(v >> 24));
- }
-
- private static void WriteUInt64LE(Stream s, ulong v)
- {
- WriteUInt32LE(s, (uint)v);
- WriteUInt32LE(s, (uint)(v >> 32));
- }
+ #endregion
}
}
diff --git a/SabreTools.Wrappers/GCZ.cs b/SabreTools.Wrappers/GCZ.cs
index 7e28073f..7a3feba5 100644
--- a/SabreTools.Wrappers/GCZ.cs
+++ b/SabreTools.Wrappers/GCZ.cs
@@ -1,7 +1,9 @@
using System.IO;
+using System.IO.Compression;
using SabreTools.Data.Models.GCZ;
using SabreTools.Data.Models.NintendoDisc;
-using SabreTools.IO.Compression.Deflate;
+using SabreTools.IO.Extensions;
+using static SabreTools.Data.Models.GCZ.Constants;
namespace SabreTools.Wrappers
{
@@ -19,39 +21,24 @@ namespace SabreTools.Wrappers
///
public GczHeader Header => Model.Header;
- ///
- /// Total decompressed size of the disc image in bytes
- ///
- public ulong DataSize => Model.Header.DataSize;
+ ///
+ public ulong CompressedDataSize => Header.CompressedDataSize;
- ///
- /// Number of compressed blocks in this image
- ///
- public uint NumBlocks => Model.Header.NumBlocks;
+ ///
+ public ulong DataSize => Header.DataSize;
- ///
- /// Size of each uncompressed block in bytes
- ///
- public uint BlockSize => Model.Header.BlockSize;
+ ///
+ public uint NumBlocks => Header.NumBlocks;
- ///
- /// Block pointer table — top bit indicates uncompressed flag
- ///
+ ///
+ public uint BlockSize => Header.BlockSize;
+
+ ///
public ulong[] BlockPointers => Model.BlockPointers;
- ///
- /// Adler-32 hashes of each uncompressed block
- ///
+ ///
public uint[] BlockHashes => Model.BlockHashes;
- ///
- /// Byte offset within the GCZ file where the compressed block data begins.
- /// Computed as: HeaderSize + (NumBlocks * 8) + (NumBlocks * 4).
- ///
- private long DataOffset => Data.Models.GCZ.Constants.HeaderSize
- + ((long)Model.Header.NumBlocks * 8)
- + ((long)Model.Header.NumBlocks * 4);
-
///
/// Disc header parsed by decompressing the first block of the GCZ image.
///
@@ -59,16 +46,19 @@ namespace SabreTools.Wrappers
{
get
{
- if (_discHeaderCached)
- return _discHeader;
- _discHeader = ReadDiscHeader();
- _discHeaderCached = true;
- return _discHeader;
+ if (field is not null)
+ return field;
+
+ field = ReadDiscHeader();
+ return field;
}
}
- private DiscHeader? _discHeader;
- private bool _discHeaderCached;
+ ///
+ /// Byte offset within the GCZ file where the compressed block data begins.
+ /// Computed as: HeaderSize + (NumBlocks * 8) + (NumBlocks * 4).
+ ///
+ public long DataOffset => HeaderSize + (NumBlocks * 8) + (NumBlocks * 4);
#endregion
@@ -146,83 +136,7 @@ namespace SabreTools.Wrappers
#endregion
- #region Inner Wrapper
-
- ///
- /// Returns a NintendoDisc wrapper backed by a virtual stream that decompresses
- /// GCZ blocks on demand, avoiding loading the entire ISO into memory.
- ///
- public NintendoDisc? GetInnerWrapper()
- {
- if (Model.BlockPointers is null || Model.BlockPointers.Length == 0)
- return null;
-
- if (Model.Header.DataSize == 0)
- return null;
-
- var vStream = new GczVirtualStream(this);
- return NintendoDisc.Create(vStream);
- }
-
- ///
- /// Decompresses a single GCZ block by index and returns its raw bytes.
- /// Returns null on failure; returns a zero-filled block if the compressed size is zero.
- ///
- internal byte[]? DecompressBlock(int blockIndex)
- {
- const ulong UncompressedFlag = 0x8000000000000000UL;
-
- if (blockIndex < 0 || blockIndex >= Model.BlockPointers.Length)
- return null;
-
- ulong ptr = Model.BlockPointers[blockIndex];
- bool uncompressed = (ptr & UncompressedFlag) != 0;
- long blockFileOffset = DataOffset + (long)(ptr & ~UncompressedFlag);
-
- ulong nextRaw = (blockIndex + 1 < Model.BlockPointers.Length)
- ? Model.BlockPointers[blockIndex + 1] & ~UncompressedFlag
- : Model.Header.CompressedDataSize;
- int compSize = (int)(nextRaw - (ptr & ~UncompressedFlag));
-
- if (compSize <= 0)
- return new byte[Model.Header.BlockSize];
-
- byte[] raw = ReadRangeFromSource(blockFileOffset, compSize);
- if (raw is null || raw.Length != compSize)
- return null;
-
- // Verify Adler-32 checksum on the compressed (raw) data before decompressing
- if (Model.BlockHashes != null && blockIndex < Model.BlockHashes.Length)
- {
- uint actual = Adler.Adler32(1, raw, 0, raw.Length);
- if (actual != Model.BlockHashes[blockIndex])
- return null;
- }
-
- if (uncompressed)
- return raw;
-
- // GCZ blocks are zlib-framed: 2-byte header + deflate data + 4-byte Adler-32 trailer.
- // Strip the frame and feed raw deflate data to DeflateStream.
- if (raw.Length < 6)
- return null;
-
- try
- {
- using var cs = new MemoryStream(raw, 2, raw.Length - 6);
- using var ds = new DeflateStream(cs, CompressionMode.Decompress);
- using var os = new MemoryStream();
- byte[] buf = new byte[4096];
- int n;
- while ((n = ds.Read(buf, 0, buf.Length)) > 0)
- os.Write(buf, 0, n);
- return os.ToArray();
- }
- catch
- {
- return null;
- }
- }
+ #region Header
///
/// Decompresses just the first block of the GCZ image to read the disc header,
@@ -230,57 +144,54 @@ namespace SabreTools.Wrappers
///
private DiscHeader? ReadDiscHeader()
{
- const ulong UncompressedFlag = 0x8000000000000000UL;
-
- if (Model.BlockPointers is null || Model.BlockPointers.Length == 0)
+ if (BlockPointers is null || BlockPointers.Length == 0)
return null;
- ulong ptr = Model.BlockPointers[0];
- bool uncompressed = (ptr & UncompressedFlag) != 0;
- long blockFileOffset = DataOffset + (long)(ptr & ~UncompressedFlag);
+ ulong pointer = BlockPointers[0];
- ulong nextRaw = Model.BlockPointers.Length > 1
- ? Model.BlockPointers[1] & ~UncompressedFlag
- : Model.Header.CompressedDataSize;
- int compSize = (int)(nextRaw - (ptr & ~UncompressedFlag));
+ ulong nextRaw = BlockPointers.Length > 1
+ ? BlockPointers[1] & ~UncompressedFlag
+ : Header.CompressedDataSize;
+ int compSize = (int)(nextRaw - (pointer & ~UncompressedFlag));
if (compSize <= 0)
return null;
+ long blockFileOffset = DataOffset + (long)(pointer & ~UncompressedFlag);
byte[] raw = ReadRangeFromSource(blockFileOffset, compSize);
- if (raw is null || raw.Length != compSize)
+ if (raw.Length != compSize)
return null;
+ bool uncompressed = (pointer & UncompressedFlag) != 0;
if (uncompressed)
{
- using var ms2 = new MemoryStream(raw);
- var disc2 = new Serialization.Readers.NintendoDisc().Deserialize(ms2);
- return disc2?.Header;
+ var disc = new Serialization.Readers.NintendoDisc().Deserialize(raw, offset: 0);
+ return disc?.Header;
}
-
- if (raw.Length < 6)
- return null;
-
- byte[] block;
- try
+ else
{
- using var cs = new MemoryStream(raw, 2, raw.Length - 6);
- using var ds = new DeflateStream(cs, CompressionMode.Decompress);
- using var os = new MemoryStream();
- byte[] buf = new byte[4096];
- int n;
- while ((n = ds.Read(buf, 0, buf.Length)) > 0)
- os.Write(buf, 0, n);
- block = os.ToArray();
- }
- catch
- {
- return null;
- }
+ if (raw.Length < 6)
+ return null;
- using var ms = new MemoryStream(block);
- var disc = new Serialization.Readers.NintendoDisc().Deserialize(ms);
- return disc?.Header;
+ byte[] block;
+ try
+ {
+ using var cs = new MemoryStream(raw, 2, raw.Length - 6);
+ using var ds = new DeflateStream(cs, CompressionMode.Decompress);
+ using var os = new MemoryStream();
+
+ ds.BlockCopy(os, blockSize: 4096);
+
+ block = os.ToArray();
+ }
+ catch
+ {
+ return null;
+ }
+
+ var disc = new Serialization.Readers.NintendoDisc().Deserialize(block, offset: 0);
+ return disc?.Header;
+ }
}
#endregion
diff --git a/SabreTools.Wrappers/AesCbc.cs b/SabreTools.Wrappers/Helpers/AesCbc.cs
similarity index 85%
rename from SabreTools.Wrappers/AesCbc.cs
rename to SabreTools.Wrappers/Helpers/AesCbc.cs
index c756ed93..724e26a2 100644
--- a/SabreTools.Wrappers/AesCbc.cs
+++ b/SabreTools.Wrappers/Helpers/AesCbc.cs
@@ -2,19 +2,20 @@ using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
///
/// AES-128-CBC encrypt/decrypt helpers used by NintendoDisc and WIA/RVZ.
///
///
- /// Implemented directly via BouncyCastle because SabreTools.Security.Cryptography
- /// currently only exposes AES-CTR. When an AESCBC wrapper is added to that
+ /// Implemented directly via BouncyCastle because SabreTools.Security.Cryptography
+ /// currently only exposes AES-CTR. When an AESCBC wrapper is added to that
/// library, replace the bodies of and with
- /// the equivalent AESCBC.Decrypt / AESCBC.Encrypt calls and remove the
+ /// the equivalent AESCBC.Decrypt / AESCBC.Encrypt calls and remove the
/// BouncyCastle using directives from this file.
///
- internal static class AesCbc
+ public static class AesCbc
{
///
/// Decrypts with AES-128-CBC (no padding).
@@ -60,11 +61,16 @@ namespace SabreTools.Wrappers
}
}
+ ///
+ /// Create an AES/CBC cipher with a given key and initial value
+ ///
private static IBufferedCipher CreateCipher(bool forEncryption, byte[] key, byte[] iv)
{
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CBC/NoPadding");
+
cipher.Init(forEncryption, new ParametersWithIV(keyParam, iv));
+
return cipher;
}
}
diff --git a/SabreTools.Wrappers/GcFst.cs b/SabreTools.Wrappers/Helpers/FileSystemTableReader.cs
similarity index 60%
rename from SabreTools.Wrappers/GcFst.cs
rename to SabreTools.Wrappers/Helpers/FileSystemTableReader.cs
index 0474fa39..d313086f 100644
--- a/SabreTools.Wrappers/GcFst.cs
+++ b/SabreTools.Wrappers/Helpers/FileSystemTableReader.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using SabreTools.Numerics.Extensions;
namespace SabreTools.Wrappers
{
@@ -7,31 +6,33 @@ namespace SabreTools.Wrappers
/// Lightweight GameCube / Wii File-System Table (FST) reader used by
/// to distinguish real-file regions from junk.
///
- /// Mirrors Dolphin's FileSystemGCWii offset-to-file-info cache
- /// (m_offset_file_info_cache).
+ /// Mirrors Dolphin's FileSystemGCWii offset-to-file-info cache
+ /// (m_offset_file_info_cache).
///
- internal sealed class GcFst
+ public sealed class FileSystemTableReader
{
- private const int EntrySize = 12;
-
- /// File entry with start and end byte offsets on disc.
- internal struct FileEntry
+ ///
+ /// File entry with start and end byte offsets on disc
+ ///
+ public struct FileEntry
{
public long FileStart;
public long FileEnd;
}
- // Sorted ascending by FileEnd for O(log n) upper_bound queries.
+ ///
+ /// Sorted ascending by FileEnd for O(log n) upper_bound queries.
+ ///
private readonly List _files;
- private GcFst(List files)
+ private FileSystemTableReader(List files)
{
_files = files;
}
///
- /// Parses a raw FST binary blob and returns a ,
- /// or null if the data is too short or structurally invalid.
+ /// Parses a raw FST binary blob and returns a ,
+ /// or null if the data is too short or structurally invalid.
///
///
/// Raw FST bytes exactly as stored on disc (GameCube) or in decrypted
@@ -41,50 +42,50 @@ namespace SabreTools.Wrappers
/// Bit-shift to convert raw file-offset fields to byte addresses.
/// 0 for GameCube (direct bytes); 2 for Wii (offset × 4).
///
- public static GcFst? TryParse(byte[] fstData, int offsetShift)
+ public static FileSystemTableReader? TryParse(byte[] fstData, int offsetShift)
{
- if (fstData == null || fstData.Length < EntrySize)
+ // Read the file system table
+ var table = Serialization.Readers.NintendoDisc.ParseFileSystemTable(fstData);
+ if (table is null)
return null;
- // Root entry (index 0): FILE_SIZE field = total number of FST entries.
- int rootOffset = 8;
- uint totalEntries = fstData.ReadUInt32BigEndian(ref rootOffset);
- if (totalEntries < 1 || ((long)totalEntries * EntrySize) > fstData.Length)
- return null;
-
- var files = new List((int)(totalEntries - 1));
-
- for (uint i = 1; i < totalEntries; i++)
+ // Filter out the entries to non-empty files only
+ var filtered = new List();
+ foreach (var entry in table.Entries)
{
- int off = (int)(i * EntrySize);
- int nameOffPos = off;
- int fileOffPos = off + 4;
- int fileSizePos = off + 8;
- uint nameOffField = fstData.ReadUInt32BigEndian(ref nameOffPos);
- uint fileOffField = fstData.ReadUInt32BigEndian(ref fileOffPos);
- uint fileSizeField = fstData.ReadUInt32BigEndian(ref fileSizePos);
+ // Directory entry
+ if ((entry.NameOffset & 0xFF000000) != 0)
+ continue;
- if ((nameOffField & 0xFF000000u) != 0) continue; // directory entry
- if (fileSizeField == 0) continue; // empty file
+ // Empty file
+ if (entry.FileSize == 0)
+ continue;
- long fileStart = (long)fileOffField << offsetShift;
- long fileEnd = fileStart + fileSizeField;
- files.Add(new FileEntry { FileStart = fileStart, FileEnd = fileEnd });
+ long fileStart = entry.FileOffset << offsetShift;
+ long fileEnd = fileStart + entry.FileSize;
+
+ var fileEntry = new FileEntry
+ {
+ FileStart = fileStart,
+ FileEnd = fileEnd,
+ };
+ filtered.Add(fileEntry);
}
// Sort ascending by FileEnd so binary-search upper_bound works correctly.
- files.Sort(delegate(FileEntry a, FileEntry b)
+ filtered.Sort(delegate (FileEntry a, FileEntry b)
{
return a.FileEnd.CompareTo(b.FileEnd);
});
- return new GcFst(files);
+ return new FileSystemTableReader(filtered);
}
///
/// Returns the file entry whose byte range contains ,
- /// or null if no file does.
+ /// or null if no file does.
///
+ /// TODO: Determine how to use List.BinarySearch here
public FileEntry? FindFileInfo(long discOffset)
{
if (_files.Count == 0)
@@ -104,7 +105,7 @@ namespace SabreTools.Wrappers
if (lo >= _files.Count)
return null;
- FileEntry e = _files[lo];
+ var e = _files[lo];
if (e.FileStart <= discOffset)
return e;
@@ -113,7 +114,7 @@ namespace SabreTools.Wrappers
///
/// Returns the smallest FileEnd value strictly greater than
- /// , or null if there is none.
+ /// , or null if there is none.
///
public long? FindNextFileEnd(long discOffset)
{
@@ -135,7 +136,7 @@ namespace SabreTools.Wrappers
///
/// Returns the smallest FileStart value strictly greater than
- /// , or null if there is none.
+ /// , or null if there is none.
///
public long? FindNextFileStart(long discOffset)
{
@@ -166,6 +167,5 @@ namespace SabreTools.Wrappers
return best;
}
-
- }
- }
+ }
+}
diff --git a/SabreTools.Wrappers/GczVirtualStream.cs b/SabreTools.Wrappers/Helpers/GczVirtualStream.cs
similarity index 80%
rename from SabreTools.Wrappers/GczVirtualStream.cs
rename to SabreTools.Wrappers/Helpers/GczVirtualStream.cs
index f7c11348..e7156e87 100644
--- a/SabreTools.Wrappers/GczVirtualStream.cs
+++ b/SabreTools.Wrappers/Helpers/GczVirtualStream.cs
@@ -7,9 +7,16 @@ namespace SabreTools.Wrappers
/// A read-only seekable stream that decompresses GCZ blocks on demand.
/// Avoids loading the entire decompressed disc image into memory.
///
- internal sealed class GczVirtualStream : Stream
+ public sealed class GczVirtualStream : Stream
{
+ ///
+ /// GCZ wrapper used for reading
+ ///
private readonly GCZ _gcz;
+
+ ///
+ /// Virtual position within the stream
+ ///
private long _position;
// Single-block cache to avoid re-decompressing on adjacent reads within the same block.
@@ -21,10 +28,19 @@ namespace SabreTools.Wrappers
_gcz = gcz ?? throw new ArgumentNullException(nameof(gcz));
}
+ ///
public override bool CanRead => true;
+
+ ///
public override bool CanSeek => true;
+
+ ///
public override bool CanWrite => false;
+
+ ///
public override long Length => (long)_gcz.DataSize;
+
+ ///
public override long Position
{
get => _position;
@@ -32,10 +48,12 @@ namespace SabreTools.Wrappers
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
+
_position = value;
}
}
+ ///
public override int Read(byte[] buffer, int offset, int count)
{
if (buffer is null)
@@ -78,25 +96,15 @@ namespace SabreTools.Wrappers
return totalRead;
}
- private byte[]? GetBlock(int blockIndex)
- {
- if (_cachedBlockIndex == blockIndex)
- return _cachedBlock;
-
- byte[]? block = _gcz.DecompressBlock(blockIndex);
- _cachedBlockIndex = blockIndex;
- _cachedBlock = block;
- return block;
- }
-
+ ///
public override long Seek(long offset, SeekOrigin origin)
{
long newPos;
switch (origin)
{
- case SeekOrigin.Begin: newPos = offset; break;
+ case SeekOrigin.Begin: newPos = offset; break;
case SeekOrigin.Current: newPos = _position + offset; break;
- case SeekOrigin.End: newPos = Length + offset; break;
+ case SeekOrigin.End: newPos = Length + offset; break;
default: throw new ArgumentOutOfRangeException(nameof(origin));
}
@@ -107,10 +115,29 @@ namespace SabreTools.Wrappers
return _position;
}
+ ///
public override void Flush() { }
+ ///
public override void SetLength(long value) => throw new NotSupportedException();
+ ///
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+ ///
+ /// Decompress and retrieve a possibly-cached data block
+ ///
+ /// Block index to retrieve
+ /// Decompressed block data on success, null otherwise
+ private byte[]? GetBlock(int blockIndex)
+ {
+ if (_cachedBlockIndex == blockIndex)
+ return _cachedBlock;
+
+ byte[]? block = _gcz.DecompressBlock(blockIndex);
+ _cachedBlockIndex = blockIndex;
+ _cachedBlock = block;
+ return block;
+ }
}
}
diff --git a/SabreTools.Wrappers/LaggedFibonacciGenerator.cs b/SabreTools.Wrappers/Helpers/LaggedFibonacciGenerator.cs
similarity index 53%
rename from SabreTools.Wrappers/LaggedFibonacciGenerator.cs
rename to SabreTools.Wrappers/Helpers/LaggedFibonacciGenerator.cs
index 2813aa08..be733987 100644
--- a/SabreTools.Wrappers/LaggedFibonacciGenerator.cs
+++ b/SabreTools.Wrappers/Helpers/LaggedFibonacciGenerator.cs
@@ -1,72 +1,102 @@
using System;
+using SabreTools.Numerics.Extensions;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
///
/// Lagged Fibonacci Generator matching Dolphin's LaggedFibonacciGenerator exactly.
/// Used to regenerate Nintendo's deterministic "junk" padding data in disc images.
- /// RVZ format identifies junk regions and stores only a 68-byte seed (17 u32 words)
+ /// RVZ format identifies junk regions and stores only a 68-byte seed (17 uint words)
/// instead of the full data, enabling significant compression of padding areas.
///
- internal class LaggedFibonacciGenerator
+ public class LaggedFibonacciGenerator
{
+ ///
+ ///
+ ///
private const int LFG_K = 521;
- private const int LFG_J = 32;
-
- /// Size of the LFG output buffer in bytes (LFG_K * 4 = 2084).
- public const int BUFFER_BYTES = LFG_K * 4;
-
- /// Size of the seed in 32-bit words (68 bytes total).
- public const int SEED_SIZE = 17;
-
- private readonly uint[] m_buffer = new uint[LFG_K];
- private int m_position_bytes = 0;
///
- /// Initializes the generator from a 17-element u32 seed array.
- /// Each seed word is treated as a raw LE u32 from the file (Dolphin: reinterpret_cast then swap32).
+ ///
+ ///
+ private const int LFG_J = 32;
+
+ ///
+ /// Size of the LFG output buffer in bytes (LFG_K * 4 = 2084)
+ ///
+ public const int BUFFER_BYTES = LFG_K * 4;
+
+ ///
+ /// Size of the seed in 32-bit words (68 bytes total)
+ ///
+ public const int SEED_SIZE = 17;
+
+ ///
+ ///
+ ///
+ private readonly uint[] _buffer = new uint[LFG_K];
+
+ ///
+ ///
+ ///
+ private int _position = 0;
+
+ ///
+ /// Initializes the generator from a 17-element uint seed array.
+ /// Each seed word is treated as a raw LE uint from the file (Dolphin: reinterpret_cast then swap32).
///
public void SetSeed(uint[] seed)
{
if (seed == null || seed.Length < SEED_SIZE)
- throw new ArgumentException($"Seed must contain at least {SEED_SIZE} u32 values.", nameof(seed));
+ throw new ArgumentException($"Seed must contain at least {SEED_SIZE} uint values.", nameof(seed));
- m_position_bytes = 0;
+ // Reinterpret LE bytes as BE (Dolphin swap32)
+ _position = 0;
for (int i = 0; i < SEED_SIZE; i++)
- m_buffer[i] = SwapU32(seed[i]); // reinterpret LE bytes as BE (Dolphin swap32)
+ {
+ _buffer[i] = SwapU32(seed[i]);
+ }
+
Initialize(false);
}
///
- /// Initializes the generator from a 68-byte seed (17 BE u32 values as in the RVZ file).
- /// Matches Dolphin: m_buffer[i] = Common::swap32(seed + i * 4).
+ /// Initializes the generator from a 68-byte seed (17 BE uint values as in the RVZ file).
///
+ /// Matches Dolphin: m_buffer[i] = Common::swap32(seed + i * 4).
public void SetSeed(byte[] seedBytes)
{
if (seedBytes == null || seedBytes.Length < SEED_SIZE * 4)
throw new ArgumentException($"Seed must be {SEED_SIZE * 4} bytes.", nameof(seedBytes));
- m_position_bytes = 0;
+ _position = 0;
+ int offset = 0;
for (int i = 0; i < SEED_SIZE; i++)
- m_buffer[i] = ReadBigEndianU32(seedBytes, i * 4);
+ {
+ _buffer[i] = seedBytes.ReadUInt32BigEndian(ref offset);
+ }
+
Initialize(false);
}
///
/// Skips forward by bytes in the output stream.
- /// Matches Dolphin: LaggedFibonacciGenerator::Forward(size_t count).
///
+ /// Matches Dolphin: LaggedFibonacciGenerator::Forward(size_t count).
public void Forward(int count)
{
- m_position_bytes += count;
- while (m_position_bytes >= BUFFER_BYTES)
+ _position += count;
+ while (_position >= BUFFER_BYTES)
{
ForwardStep();
- m_position_bytes -= BUFFER_BYTES;
+ _position -= BUFFER_BYTES;
}
}
- /// Generates junk bytes and returns them.
+ ///
+ /// Generates junk bytes and returns them.
+ ///
public byte[] GetBytes(int count)
{
byte[] output = new byte[count];
@@ -78,20 +108,21 @@ namespace SabreTools.Wrappers
/// Generates junk bytes into starting at .
/// Matches Dolphin: LaggedFibonacciGenerator::GetBytes using memcpy pattern.
///
+ ///
public void GetBytes(int count, byte[] output, int outputOffset)
{
while (count > 0)
{
- int length = Math.Min(count, BUFFER_BYTES - m_position_bytes);
- Buffer.BlockCopy(m_buffer, m_position_bytes, output, outputOffset, length);
- m_position_bytes += length;
+ int length = Math.Min(count, BUFFER_BYTES - _position);
+ Buffer.BlockCopy(_buffer, _position, output, outputOffset, length);
+ _position += length;
count -= length;
outputOffset += length;
- if (m_position_bytes == BUFFER_BYTES)
+ if (_position == BUFFER_BYTES)
{
ForwardStep();
- m_position_bytes = 0;
+ _position = 0;
}
}
}
@@ -100,81 +131,98 @@ namespace SabreTools.Wrappers
/// Returns a single junk byte at the current position, advancing by one byte.
/// Matches Dolphin: LaggedFibonacciGenerator::GetByte.
///
+ ///
internal byte GetByte()
{
- int wordIdx = m_position_bytes / 4;
- int byteInWord = m_position_bytes % 4;
- byte result = (byte)(m_buffer[wordIdx] >> (byteInWord * 8)); // LE byte order
+ int wordIdx = _position / 4;
+ int byteInWord = _position % 4;
+ byte result = (byte)(_buffer[wordIdx] >> (byteInWord * 8)); // LE byte order
- m_position_bytes++;
- if (m_position_bytes == BUFFER_BYTES)
+ _position++;
+ if (_position == BUFFER_BYTES)
{
ForwardStep();
- m_position_bytes = 0;
+ _position = 0;
}
return result;
}
- // -------------------------------------------------------------------
- // Private forward/backward state steps
- // -------------------------------------------------------------------
+ #region Private forward/backward state steps
///
- /// Full buffer state step forward — Dolphin: Forward() (no args).
+ /// Full buffer state step forward
+ ///
+ ///
+ /// Dolphin: Forward() (no args).
/// for i in [0,J): buf[i] ^= buf[i + K - J] (= buf[i + 489])
/// for i in [J,K): buf[i] ^= buf[i - J] (= buf[i - 32])
- ///
+ ///
private void ForwardStep()
{
for (int i = 0; i < LFG_J; i++)
- m_buffer[i] ^= m_buffer[i + LFG_K - LFG_J];
+ {
+ _buffer[i] ^= _buffer[i + LFG_K - LFG_J];
+ }
+
for (int i = LFG_J; i < LFG_K; i++)
- m_buffer[i] ^= m_buffer[i - LFG_J];
+ {
+ _buffer[i] ^= _buffer[i - LFG_J];
+ }
}
///
/// Partial or full buffer state step backward — undoes ForwardStep.
- /// Dolphin: Backward(size_t start_word, size_t end_word).
///
+ /// Dolphin: Backward(size_t start_word, size_t end_word).
private void Backward(int startWord = 0, int endWord = LFG_K)
{
int loopEnd = Math.Max(LFG_J, startWord);
// Undo second loop of ForwardStep (reversed)
for (int i = Math.Min(endWord, LFG_K); i > loopEnd; i--)
- m_buffer[i - 1] ^= m_buffer[i - 1 - LFG_J];
+ {
+ _buffer[i - 1] ^= _buffer[i - 1 - LFG_J];
+ }
// Undo first loop of ForwardStep (reversed)
for (int i = Math.Min(endWord, LFG_J); i > startWord; i--)
- m_buffer[i - 1] ^= m_buffer[i - 1 + LFG_K - LFG_J];
+ {
+ _buffer[i - 1] ^= _buffer[i - 1 + LFG_K - LFG_J];
+ }
}
///
/// Recovers the original 17-word seed from the current buffer state and outputs it
- /// as LE u32 values into .
- /// Dolphin: Reinitialize(u32 seed_out[]).
+ /// as LE uint values into .
///
+ /// Dolphin: Reinitialize(uint seed_out[]).
private bool Reinitialize(uint[] seedOut)
{
for (int i = 0; i < 4; i++)
+ {
Backward();
+ }
// Swap all words back to big-endian representation
for (int i = 0; i < LFG_K; i++)
- m_buffer[i] = SwapU32(m_buffer[i]);
+ {
+ _buffer[i] = SwapU32(_buffer[i]);
+ }
// Reconstruct bits 16-17 for the first SEED_SIZE words
for (int i = 0; i < SEED_SIZE; i++)
{
- m_buffer[i] = (m_buffer[i] & 0xFF00FFFF)
- | ((m_buffer[i] << 2) & 0x00FC0000)
- | (((m_buffer[i + 16] ^ m_buffer[i + 15]) << 9) & 0x00030000);
+ _buffer[i] = (_buffer[i] & 0xFF00FFFF)
+ | ((_buffer[i] << 2) & 0x00FC0000)
+ | (((_buffer[i + 16] ^ _buffer[i + 15]) << 9) & 0x00030000);
}
- // Output seed as LE u32 values (swap32 converts BE→LE)
+ // Output seed as LE uint values (swap32 converts BE->LE)
for (int i = 0; i < SEED_SIZE; i++)
- seedOut[i] = SwapU32(m_buffer[i]);
+ {
+ seedOut[i] = SwapU32(_buffer[i]);
+ }
return Initialize(true);
}
@@ -183,91 +231,98 @@ namespace SabreTools.Wrappers
/// Fills m_buffer[SEED_SIZE..K-1] from the first SEED_SIZE words, applies the output
/// transform, and runs 4× ForwardStep. When is true,
/// verifies the data in m_buffer[SEED_SIZE..] matches the recurrence.
- /// Dolphin: Initialize(bool check_existing_data).
///
+ /// Dolphin: Initialize(bool check_existing_data).
private bool Initialize(bool checkExisting)
{
for (int i = SEED_SIZE; i < LFG_K; i++)
{
- uint calculated = (m_buffer[i - 17] << 23)
- ^ (m_buffer[i - 16] >> 9)
- ^ m_buffer[i - 1];
+ uint calculated = (_buffer[i - 17] << 23)
+ ^ (_buffer[i - 16] >> 9)
+ ^ _buffer[i - 1];
if (checkExisting)
{
- uint actual = (m_buffer[i] & 0xFF00FFFF) | ((m_buffer[i] << 2) & 0x00FC0000);
+ uint actual = (_buffer[i] & 0xFF00FFFF) | ((_buffer[i] << 2) & 0x00FC0000);
if ((calculated & 0xFFFCFFFF) != actual)
return false;
}
- m_buffer[i] = calculated;
+ _buffer[i] = calculated;
}
- // Output transform: each word → swap32((x & 0xFF00FFFF) | ((x >> 2) & 0x00FF0000))
+ // Output transform: each word -> swap32((x & 0xFF00FFFF) | ((x >> 2) & 0x00FF0000))
for (int i = 0; i < LFG_K; i++)
- m_buffer[i] = SwapU32((m_buffer[i] & 0xFF00FFFF) | ((m_buffer[i] >> 2) & 0x00FF0000));
+ {
+ _buffer[i] = SwapU32((_buffer[i] & 0xFF00FFFF) | ((_buffer[i] >> 2) & 0x00FF0000));
+ }
for (int i = 0; i < 4; i++)
+ {
ForwardStep();
+ }
return true;
}
- // -------------------------------------------------------------------
- // Static seed-recovery API (used by RvzPackDecompressor)
- // -------------------------------------------------------------------
+ #endregion
+
+ #region Static seed-recovery API (used by RvzPackDecompressor)
///
/// Attempts to recover a 17-word seed from disc data starting at
/// within .
/// is the number of bytes to match (up to the next 32 KiB boundary).
- /// is discOffset % 0x8000 — the offset within
+ /// is discOffset % 0x8000 — the offset within
/// the current LFG cycle.
- /// Returns the number of bytes that were successfully reconstructed (0 = not junk data).
- /// Matches Dolphin: LaggedFibonacciGenerator::GetSeed(u8*, size_t, size_t, u32[]).
///
+ /// the number of bytes that were successfully reconstructed (0 = not junk data).
+ /// Matches Dolphin: LaggedFibonacciGenerator::GetSeed(u8*, size_t, size_t, uint[]).
public static int GetSeed(byte[] data, int dataStart, int size, int dataOffsetMod, uint[] seedOut)
{
if (size <= 0 || dataStart < 0 || dataStart + size > data.Length)
return 0;
- // Skip any bytes before the next u32-aligned boundary
+ // Skip any bytes before the next uint-aligned boundary
int bytesToSkip = (4 - (dataOffsetMod % 4)) % 4;
if (bytesToSkip >= size)
return 0;
- int u32DataStart = dataStart + bytesToSkip;
- int u32Size = (size - bytesToSkip) / 4;
- int u32DataOffset = (dataOffsetMod + bytesToSkip) / 4;
+ int uintDataStart = dataStart + bytesToSkip;
+ int uintSize = (size - bytesToSkip) / 4;
+ int uintDataOffset = (dataOffsetMod + bytesToSkip) / 4;
- if (u32Size < LFG_K)
+ if (uintSize < LFG_K)
return 0;
- // Read disc bytes as LE u32 values (Dolphin: reinterpret_cast)
- uint[] u32Data = new uint[u32Size];
- for (int i = 0; i < u32Size; i++)
- u32Data[i] = ReadLittleEndianU32(data, u32DataStart + (i * 4));
+ // Read disc bytes as little-endian uint values (Dolphin: reinterpret_cast)
+ uint[] uintData = new uint[uintSize];
+ for (int i = 0; i < uintSize; i++)
+ {
+ uintData[i] = data.ReadUInt32LittleEndian(ref uintDataStart);
+ }
- LaggedFibonacciGenerator lfg = new LaggedFibonacciGenerator();
- if (!GetSeed_u32(u32Data, u32Size, u32DataOffset, lfg, seedOut))
+ var lfg = new LaggedFibonacciGenerator();
+ if (!GetSeed(uintData, uintSize, uintDataOffset, lfg, seedOut))
return 0;
// Set position to data_offset % BUFFER_BYTES and count matching bytes from data[dataStart]
- lfg.m_position_bytes = dataOffsetMod % BUFFER_BYTES;
+ lfg._position = dataOffsetMod % BUFFER_BYTES;
int reconstructed = 0;
for (int i = 0; i < size && lfg.GetByte() == data[dataStart + i]; i++)
+ {
reconstructed++;
+ }
return reconstructed;
}
///
- /// Inner u32-level seed recovery.
- /// Dolphin: GetSeed(const u32* data, size_t size, size_t data_offset, LFG*, u32[]).
+ /// Inner UInt32-level seed recovery.
///
- private static bool GetSeed_u32(uint[] data, int size, int dataOffset,
- LaggedFibonacciGenerator lfg, uint[] seedOut)
+ /// Dolphin: GetSeed(const uint* data, size_t size, size_t data_offset, LFG*, uint[]).
+ private static bool GetSeed(uint[] data, int size, int dataOffset, LaggedFibonacciGenerator lfg, uint[] seedOut)
{
if (size < LFG_K)
return false;
@@ -285,35 +340,38 @@ namespace SabreTools.Wrappers
int dataOffsetDivK = dataOffset / LFG_K;
// Rotate data into buffer so buffer[dataOffsetModK] = data[0]
- Array.Copy(data, 0, lfg.m_buffer, dataOffsetModK, LFG_K - dataOffsetModK);
+ Array.Copy(data, 0, lfg._buffer, dataOffsetModK, LFG_K - dataOffsetModK);
if (dataOffsetModK > 0)
- Array.Copy(data, LFG_K - dataOffsetModK, lfg.m_buffer, 0, dataOffsetModK);
+ Array.Copy(data, LFG_K - dataOffsetModK, lfg._buffer, 0, dataOffsetModK);
lfg.Backward(0, dataOffsetModK);
for (int i = 0; i < dataOffsetDivK; i++)
+ {
lfg.Backward();
+ }
if (!lfg.Reinitialize(seedOut))
return false;
for (int i = 0; i < dataOffsetDivK; i++)
+ {
lfg.ForwardStep();
+ }
return true;
}
- // -------------------------------------------------------------------
- // Endian helpers
- // -------------------------------------------------------------------
+ #endregion
- internal static uint ReadBigEndianU32(byte[] data, int offset) =>
- (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]);
+ #region Helpers
- private static uint ReadLittleEndianU32(byte[] data, int offset) =>
- (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
+ ///
+ /// Swap endinaness of a UInt32
+ ///
+ internal static uint SwapU32(uint value)
+ => (value << 24) | ((value << 8) & 0x00FF0000) | ((value >> 8) & 0x0000FF00) | (value >> 24);
- internal static uint SwapU32(uint value) =>
- (value << 24) | ((value << 8) & 0x00FF0000) | ((value >> 8) & 0x0000FF00) | (value >> 24);
+ #endregion
}
}
diff --git a/SabreTools.Wrappers/PurgeCompressor.cs b/SabreTools.Wrappers/Helpers/PurgeCompressor.cs
similarity index 83%
rename from SabreTools.Wrappers/PurgeCompressor.cs
rename to SabreTools.Wrappers/Helpers/PurgeCompressor.cs
index e844a559..b6fffd80 100644
--- a/SabreTools.Wrappers/PurgeCompressor.cs
+++ b/SabreTools.Wrappers/Helpers/PurgeCompressor.cs
@@ -1,7 +1,9 @@
using System;
using System.IO;
using SabreTools.Hashing;
+using SabreTools.Numerics.Extensions;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
///
@@ -20,6 +22,11 @@ namespace SabreTools.Wrappers
///
internal static class PurgeCompressor
{
+ ///
+ /// Zero-byte runs of this length or fewer are bridged
+ ///
+ private const int MaxGap = 8;
+
///
/// Compress [ ..
/// +) into PURGE format.
@@ -35,8 +42,6 @@ namespace SabreTools.Wrappers
/// PURGE-compressed byte array (segments + 20-byte SHA-1).
public static byte[] Compress(byte[] data, int offset, int count, byte[]? precedingBytes = null)
{
- const int MaxGap = 8; // zero-byte runs of this length or fewer are bridged
-
var output = new MemoryStream((count / 2) + 32);
int end = offset + count;
@@ -46,42 +51,46 @@ namespace SabreTools.Wrappers
{
// Skip leading zeros
while (pos < end && data[pos] == 0)
+ {
pos++;
+ }
if (pos >= end)
break;
// pos is now the start of a non-zero run (segment start)
int segStart = pos;
- int segEnd = pos;
+ int segEnd = pos;
// Extend the segment, bridging zero-gaps of <= MaxGap bytes
while (segEnd < end)
{
// advance through non-zero bytes
while (segEnd < end && data[segEnd] != 0)
+ {
segEnd++;
+ }
// peek ahead: count zero bytes
int zeroRun = 0;
while (segEnd + zeroRun < end && data[segEnd + zeroRun] == 0)
+ {
zeroRun++;
+ }
// If the gap is small enough (and there is more non-zero data after it),
// bridge the gap by including it in the segment.
if (zeroRun > 0 && zeroRun <= MaxGap && segEnd + zeroRun < end)
- {
segEnd += zeroRun; // include zeros in segment, keep scanning
- }
else
- {
break; // end of segment
- }
}
// Trim trailing zeros from segment end
while (segEnd > segStart && data[segEnd - 1] == 0)
+ {
segEnd--;
+ }
if (segEnd <= segStart)
{
@@ -90,11 +99,10 @@ namespace SabreTools.Wrappers
}
uint segOffset = (uint)(segStart - offset);
- uint segSize = (uint)(segEnd - segStart);
+ uint segSize = (uint)(segEnd - segStart);
- // Write {u32 offsetBE, u32 sizeBE, data[segSize]}
- WriteBeU32(output, segOffset);
- WriteBeU32(output, segSize);
+ output.WriteBigEndian(segOffset);
+ output.WriteBigEndian(segSize);
output.Write(data, segStart, (int)segSize);
pos = segEnd;
@@ -112,24 +120,23 @@ namespace SabreTools.Wrappers
return result;
}
+ ///
+ /// Get the SHA-1 hash of preceeding bytes and segment data
+ ///
+ /// Optional preceeding bytes
+ /// Segment data
+ /// SHA-1 hash of the data, all 0x00 on error
private static byte[] ComputeSha1(byte[]? precedingBytes, byte[] segments)
{
using var sha1 = new HashWrapper(HashType.SHA1);
- if (precedingBytes != null && precedingBytes.Length > 0)
+ if (precedingBytes is not null && precedingBytes.Length > 0)
sha1.Process(precedingBytes, 0, precedingBytes.Length);
sha1.Process(segments, 0, segments.Length);
sha1.Terminate();
- return sha1.CurrentHashBytes ?? new byte[20];
- }
- private static void WriteBeU32(Stream s, uint value)
- {
- s.WriteByte((byte)(value >> 24));
- s.WriteByte((byte)(value >> 16));
- s.WriteByte((byte)(value >> 8));
- s.WriteByte((byte)value);
+ return sha1.CurrentHashBytes ?? new byte[20];
}
}
}
diff --git a/SabreTools.Wrappers/PurgeDecompressor.cs b/SabreTools.Wrappers/Helpers/PurgeDecompressor.cs
similarity index 87%
rename from SabreTools.Wrappers/PurgeDecompressor.cs
rename to SabreTools.Wrappers/Helpers/PurgeDecompressor.cs
index 1f676f77..d7ce7ed1 100644
--- a/SabreTools.Wrappers/PurgeDecompressor.cs
+++ b/SabreTools.Wrappers/Helpers/PurgeDecompressor.cs
@@ -2,6 +2,7 @@ using System;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
///
@@ -21,6 +22,7 @@ namespace SabreTools.Wrappers
internal static class PurgeDecompressor
{
private const int SHA1_SIZE = 20;
+
private const int SEGMENT_HEADER_SIZE = 8; // u32 offset + u32 size
///
@@ -35,16 +37,18 @@ namespace SabreTools.Wrappers
/// exception-list section for Wii partition groups. Pass null for non-Wii groups.
///
///
- /// The decompressed byte array, or null if the data is malformed or the
+ /// The decompressed byte array, or null if the data is malformed or the
/// trailing SHA-1 does not match.
///
public static byte[]? Decompress(
- byte[] input, int inputOffset, int inputLength,
+ byte[] input,
+ int inputOffset,
+ int inputLength,
int decompressedSize,
byte[]? precedingBytes = null)
{
- if (input is null) throw new ArgumentNullException(nameof(input));
- if (inputLength < SHA1_SIZE) return null;
+ if (inputLength < SHA1_SIZE)
+ return null;
byte[] output = new byte[decompressedSize];
int pos = inputOffset;
@@ -52,7 +56,7 @@ namespace SabreTools.Wrappers
using (var sha1 = new HashWrapper(HashType.SHA1))
{
- if (precedingBytes != null && precedingBytes.Length > 0)
+ if (precedingBytes is not null && precedingBytes.Length > 0)
sha1.Process(precedingBytes, 0, precedingBytes.Length);
while (pos < dataEnd)
@@ -62,7 +66,7 @@ namespace SabreTools.Wrappers
int headerPos = pos;
uint segOffset = input.ReadUInt32BigEndian(ref headerPos);
- uint segSize = input.ReadUInt32BigEndian(ref headerPos);
+ uint segSize = input.ReadUInt32BigEndian(ref headerPos);
sha1.Process(input, pos, SEGMENT_HEADER_SIZE);
pos += SEGMENT_HEADER_SIZE;
@@ -84,7 +88,9 @@ namespace SabreTools.Wrappers
sha1.Terminate();
byte[]? computed = sha1.CurrentHashBytes;
- if (computed is null) return null;
+ if (computed is null)
+ return null;
+
for (int i = 0; i < SHA1_SIZE; i++)
{
if (computed[i] != input[dataEnd + i])
diff --git a/SabreTools.Wrappers/Helpers/RvzPackDecompressor.cs b/SabreTools.Wrappers/Helpers/RvzPackDecompressor.cs
new file mode 100644
index 00000000..79cfc30e
--- /dev/null
+++ b/SabreTools.Wrappers/Helpers/RvzPackDecompressor.cs
@@ -0,0 +1,143 @@
+using System;
+using SabreTools.Numerics.Extensions;
+
+// TODO: Move to IO
+namespace SabreTools.Wrappers
+{
+ ///
+ /// Decompressor for RVZ packed format.
+ /// RVZ uses run-length encoding to store real data and junk data efficiently:
+ /// - Real data: size (4 bytes) + data bytes
+ /// - Junk data: size with high bit set (4 bytes) + 68-byte seed -> regenerate using LFG
+ ///
+ internal class RvzPackDecompressor
+ {
+ ///
+ ///
+ ///
+ private readonly byte[] _packedData;
+
+ ///
+ ///
+ ///
+ private readonly uint _rvzPackedSize;
+
+ ///
+ ///
+ ///
+ private long _dataOffset;
+
+ ///
+ ///
+ ///
+ private readonly LaggedFibonacciGenerator _lfg;
+
+ ///
+ ///
+ ///
+ private int _inPosition = 0;
+
+ ///
+ ///
+ ///
+ private uint _currentSize = 0;
+
+ ///
+ ///
+ ///
+ private bool _currentIsJunk = false;
+
+ ///
+ /// Creates a new RVZ pack decompressor.
+ ///
+ /// The packed RVZ data
+ /// Expected size of packed data (for validation)
+ /// Offset in the virtual disc (for LFG alignment)
+ public RvzPackDecompressor(byte[] packedData, uint rvzPackedSize, long dataOffset)
+ {
+ _packedData = packedData;
+ _rvzPackedSize = rvzPackedSize;
+ _dataOffset = dataOffset;
+ _lfg = new LaggedFibonacciGenerator();
+ }
+
+ ///
+ /// Decompresses the packed data into the output buffer.
+ ///
+ /// Destination buffer
+ /// Offset in destination buffer
+ /// Number of bytes to decompress
+ /// Number of bytes actually decompressed
+ public int Decompress(byte[] output, int outputOffset, int count)
+ {
+ int totalWritten = 0;
+
+ while (totalWritten < count && !IsDone())
+ {
+ if (_currentSize == 0)
+ {
+ if (!ReadNextSegment())
+ break;
+ }
+
+ int bytesToWrite = Math.Min((int)_currentSize, count - totalWritten);
+ if (_currentIsJunk)
+ {
+ _lfg.GetBytes(bytesToWrite, output, outputOffset + totalWritten);
+ }
+ else
+ {
+ Array.Copy(_packedData, _inPosition, output, outputOffset + totalWritten, bytesToWrite);
+ _inPosition += bytesToWrite;
+ }
+
+ _currentSize -= (uint)bytesToWrite;
+ totalWritten += bytesToWrite;
+ _dataOffset += bytesToWrite;
+ }
+
+ return totalWritten;
+ }
+
+ ///
+ /// Checks if decompression is complete.
+ ///
+ public bool IsDone() => _currentSize == 0 && _inPosition >= _rvzPackedSize;
+
+ ///
+ /// Read the next segment of packed data
+ ///
+ /// True if the segment was read and cached, false otherwise
+ private bool ReadNextSegment()
+ {
+ if (_inPosition + 4 > _packedData.Length)
+ return false;
+
+ // Size field is big-endian u32; high bit signals junk data
+ uint sizeField = _packedData.ReadUInt32BigEndian(ref _inPosition);
+
+ _currentIsJunk = (sizeField & 0x80000000) != 0;
+ _currentSize = sizeField & 0x7FFFFFFF;
+
+ if (_currentIsJunk)
+ {
+ if (_inPosition + (LaggedFibonacciGenerator.SEED_SIZE * 4) > _packedData.Length)
+ return false;
+
+ byte[] seed = new byte[LaggedFibonacciGenerator.SEED_SIZE * 4];
+ Array.Copy(_packedData, _inPosition, seed, 0, seed.Length);
+ _inPosition += seed.Length;
+
+ _lfg.SetSeed(seed);
+
+ // Advance LFG to the correct position within the buffer.
+ // Dolphin: lfg.m_position_bytes = data_offset % (LFG_K * sizeof(u32))
+ int offsetInBuffer = (int)(_dataOffset % LaggedFibonacciGenerator.BUFFER_BYTES);
+ if (offsetInBuffer > 0)
+ _lfg.Forward(offsetInBuffer);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/SabreTools.Wrappers/RvzPackEncoder.cs b/SabreTools.Wrappers/Helpers/RvzPackEncoder.cs
similarity index 57%
rename from SabreTools.Wrappers/RvzPackEncoder.cs
rename to SabreTools.Wrappers/Helpers/RvzPackEncoder.cs
index 734b39bb..d4f2a833 100644
--- a/SabreTools.Wrappers/RvzPackEncoder.cs
+++ b/SabreTools.Wrappers/Helpers/RvzPackEncoder.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using SabreTools.Numerics.Extensions;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
///
@@ -10,7 +11,7 @@ namespace SabreTools.Wrappers
/// (Lagged Fibonacci Generator) junk regions with compact seed descriptors.
///
/// This is the exact inverse of and mirrors
- /// Dolphin's RVZPack() in WIABlob.cpp.
+ /// Dolphin's RVZPack() in WIABlob.cpp.
///
/// Two-phase algorithm:
///
@@ -22,34 +23,51 @@ namespace SabreTools.Wrappers
///
internal static class RvzPackEncoder
{
- // 17 u32s × 4 bytes = 68 bytes — minimum size to record a seed
+ ///
+ /// 17 u32s × 4 bytes = 68 bytes — minimum size to record a seed
+ ///
private const int SeedSizeBytes = LaggedFibonacciGenerator.SEED_SIZE * 4;
+ ///
+ ///
+ ///
private sealed class JunkRegion
{
- public long StartOffset;
+ public long StartOffset;
+
public uint[]? Seed;
}
- /// Result of packing a single chunk: compressed payload and its logical size.
+ ///
+ /// Result of packing a single chunk: compressed payload and its logical size.
+ ///
internal struct ChunkResult
{
- /// Packed payload, or null if the chunk contains no junk.
+ ///
+ /// Packed payload, or null if the chunk contains no junk.
+ ///
public byte[]? Packed;
- /// Number of bytes the decompressor needs to consume from .
+
+ ///
+ /// Number of bytes the decompressor needs to consume from .
+ ///
public uint RvzPackedSize;
}
- #region Public API
-
///
/// RVZ-pack a single chunk.
- /// Returns null if the chunk contains no junk (write raw instead).
+ /// Returns null if the chunk contains no junk (write raw instead).
/// is the number of bytes actually needed
/// by the decompressor (may be < packed.Length due to alignment).
///
- public static byte[]? Pack(byte[] data, int dataOffset, int size,
- long discDataOffset, out uint rvzPackedSize, GcFst? fst = null)
+ /// Source buffer
+ /// Start of data within
+ public static byte[]? Pack(byte[] data,
+ int dataOffset,
+ int size,
+ long discDataOffset,
+ out uint rvzPackedSize,
+ FileSystemTableReader? fst = null)
{
rvzPackedSize = 0;
if (size <= 0)
@@ -59,9 +77,9 @@ namespace SabreTools.Wrappers
if (junkInfo.Count == 0)
return null;
- ChunkResult r = EmitChunk(data, dataOffset, 0L, size, size, junkInfo);
- rvzPackedSize = r.RvzPackedSize;
- return r.Packed;
+ ChunkResult result = EmitChunk(data, dataOffset, 0L, size, junkInfo);
+ rvzPackedSize = result.RvzPackedSize;
+ return result.Packed;
}
///
@@ -69,101 +87,114 @@ namespace SabreTools.Wrappers
/// Performs one Phase-1 scan over the entire buffer, then calls
/// per chunk.
///
- /// Source buffer.
- /// Start of data within .
- /// Total number of bytes to process.
- /// Size of each individual chunk.
- /// Number of chunks.
- /// Disc-partition byte offset of the first byte.
- /// Optional FST for file-boundary optimisation.
+ /// Source buffer
+ /// Start of data within
+ /// Total number of bytes to process
+ /// Size of each individual chunk
+ /// Number of chunks
+ /// Disc-partition byte offset of the first byte
+ /// Optional FST for file-boundary optimisation
///
/// One per chunk;
- /// Packed == null means the chunk has no junk and should be written raw.
+ /// Packed == null means the chunk has no junk and should be written raw.
///
- public static ChunkResult[] PackGroup(
- byte[] data, int dataOffset, int totalSize,
- int bytesPerChunk, int numChunks,
- long discDataOffset, GcFst? fst = null)
+ public static ChunkResult[] PackGroup(byte[] data,
+ int dataOffset,
+ int totalSize,
+ int bytesPerChunk,
+ int numChunks,
+ long discDataOffset,
+ FileSystemTableReader? fst = null)
{
var junkInfo = ScanForJunk(data, dataOffset, totalSize, discDataOffset, fst);
var result = new ChunkResult[numChunks];
- for (int c = 0; c < numChunks; c++)
+ for (int i = 0; i < numChunks; i++)
{
- long chunkStart = (long)c * bytesPerChunk;
- long chunkEnd = Math.Min(chunkStart + bytesPerChunk, totalSize);
- result[c] = EmitChunk(data, dataOffset, chunkStart, chunkEnd, totalSize, junkInfo);
+ long chunkStart = (long)i * bytesPerChunk;
+ long chunkEnd = Math.Min(chunkStart + bytesPerChunk, totalSize);
+
+ result[i] = EmitChunk(data, dataOffset, chunkStart, chunkEnd, junkInfo);
}
return result;
}
- #endregion
-
- #region Phase 1 — scan buffer for junk regions
-
+ ///
+ /// Scan buffer for junk regions
+ ///
+ /// Source buffer
+ /// Start of data within
+ /// Total number of bytes to process
+ /// Disc-partition byte offset of the first byte
+ /// Optional FST for file-boundary optimisation
+ ///
private static SortedDictionary ScanForJunk(
- byte[] data, int dataOffset, int totalSize, long discDataOffset, GcFst? fst)
+ byte[] data,
+ int dataOffset,
+ int totalSize,
+ long discDataOffset,
+ FileSystemTableReader? fst)
{
var junkInfo = new SortedDictionary();
long position = 0;
- long dataOff = discDataOffset;
+ long dataOff = discDataOffset;
while (position < totalSize)
{
// Step 1: count and advance past leading zeros
long zeroes = 0;
- while ((position + zeroes) < totalSize &&
- data[dataOffset + position + zeroes] == 0)
+ while ((position + zeroes) < totalSize && data[dataOffset + position + zeroes] == 0)
+ {
zeroes++;
+ }
if (zeroes > SeedSizeBytes)
{
junkInfo[position + zeroes] = new JunkRegion
{
StartOffset = position,
- Seed = new uint[LaggedFibonacciGenerator.SEED_SIZE]
+ Seed = new uint[LaggedFibonacciGenerator.SEED_SIZE]
};
}
position += zeroes;
- dataOff += zeroes;
+ dataOff += zeroes;
if (position >= totalSize)
break;
// Step 2: compute aligned read window (next 0x8000 boundary)
long nextBoundary = AlignUp(dataOff + 1, 0x8000);
- long bytesToRead = Math.Min(nextBoundary - dataOff, totalSize - position);
- int dataOffMod = (int)(dataOff % 0x8000);
+ long bytesToRead = Math.Min(nextBoundary - dataOff, totalSize - position);
+ int dataOffMod = (int)(dataOff % 0x8000);
// Step 3: ALWAYS call GetSeed unconditionally — no FST pre-check
- var seed = new uint[LaggedFibonacciGenerator.SEED_SIZE];
- int reconstructed = LaggedFibonacciGenerator.GetSeed(
- data, (int)(dataOffset + position), (int)bytesToRead, dataOffMod, seed);
+ var seed = new uint[LaggedFibonacciGenerator.SEED_SIZE];
+ int reconstructed = LaggedFibonacciGenerator.GetSeed(data, (int)(dataOffset + position), (int)bytesToRead, dataOffMod, seed);
if (reconstructed > 0)
{
junkInfo[position + reconstructed] = new JunkRegion
{
StartOffset = position,
- Seed = seed
+ Seed = seed
};
}
// Step 4: FST skip AFTER GetSeed
- if (fst != null)
+ if (fst is not null)
{
long queryOff = dataOff + reconstructed;
- GcFst.FileEntry? fileInfo = fst.FindFileInfo(queryOff);
- if (fileInfo.HasValue)
+ FileSystemTableReader.FileEntry? fileInfo = fst.FindFileInfo(queryOff);
+ if (fileInfo is not null)
{
long fileEnd = fileInfo.Value.FileEnd;
if (fileEnd < (dataOff + bytesToRead))
{
position += fileEnd - dataOff;
- dataOff = fileEnd;
+ dataOff = fileEnd;
continue;
}
}
@@ -171,33 +202,40 @@ namespace SabreTools.Wrappers
// Step 5: normal advance by block window
position += bytesToRead;
- dataOff += bytesToRead;
+ dataOff += bytesToRead;
}
return junkInfo;
}
- #endregion
-
- #region Phase 2 — emit packed segments for a single chunk
-
+ ///
+ /// Emit packed segments for a single chunk
+ ///
+ /// Source buffer
+ /// Start of data within
+ ///
+ ///
+ ///
+ ///
private static ChunkResult EmitChunk(
- byte[] data, int dataOffset,
- long chunkStart, long chunkEnd, long totalSize,
+ byte[] data,
+ int dataOffset,
+ long chunkStart,
+ long chunkEnd,
SortedDictionary junkInfo)
{
- long currentOffset = chunkStart;
+ long currentOffset = chunkStart;
bool firstIteration = true;
- var output = new MemoryStream((int)(chunkEnd - chunkStart));
+ var output = new MemoryStream((int)(chunkEnd - chunkStart));
uint packedSize = 0;
while (currentOffset < chunkEnd)
{
- long remaining = chunkEnd - currentOffset;
- long nextJunkStart = chunkEnd;
- long nextJunkEnd = chunkEnd;
- uint[]? junkSeed = null;
+ long remaining = chunkEnd - currentOffset;
+ long nextJunkStart = chunkEnd;
+ long nextJunkEnd = chunkEnd;
+ uint[]? junkSeed = null;
if (remaining > SeedSizeBytes)
{
@@ -206,12 +244,11 @@ namespace SabreTools.Wrappers
// Dolphin Phase-2 condition:
// key > currentOffset + SEED_SIZE_BYTES AND
// startOffset + SEED_SIZE_BYTES < chunkEnd
- if ((kvp.Key > (currentOffset + SeedSizeBytes)) &&
- ((kvp.Value.StartOffset + SeedSizeBytes) < chunkEnd))
+ if ((kvp.Key > (currentOffset + SeedSizeBytes)) && ((kvp.Value.StartOffset + SeedSizeBytes) < chunkEnd))
{
nextJunkStart = Math.Max(currentOffset, kvp.Value.StartOffset);
- nextJunkEnd = Math.Min(chunkEnd, kvp.Key);
- junkSeed = kvp.Value.Seed;
+ nextJunkEnd = Math.Min(chunkEnd, kvp.Key);
+ junkSeed = kvp.Value.Seed;
break;
}
}
@@ -232,19 +269,21 @@ namespace SabreTools.Wrappers
{
output.WriteBigEndian((uint)nonJunkBytes);
output.Write(data, (int)(dataOffset + currentOffset), (int)nonJunkBytes);
- packedSize += 4 + (uint)nonJunkBytes;
+ packedSize += 4 + (uint)nonJunkBytes;
currentOffset += nonJunkBytes;
}
// Emit junk-seed segment
long junkBytes = nextJunkEnd - currentOffset;
- if (junkBytes > 0 && junkSeed != null)
+ if (junkBytes > 0 && junkSeed is not null)
{
output.WriteBigEndian(0x80000000u | (uint)junkBytes);
+
byte[] seedBytes = new byte[SeedSizeBytes];
Buffer.BlockCopy(junkSeed, 0, seedBytes, 0, SeedSizeBytes);
output.Write(seedBytes, 0, SeedSizeBytes);
- packedSize += 4 + (uint)SeedSizeBytes;
+
+ packedSize += 4 + (uint)SeedSizeBytes;
currentOffset += junkBytes;
}
@@ -255,10 +294,12 @@ namespace SabreTools.Wrappers
return new ChunkResult { Packed = output.ToArray(), RvzPackedSize = packedSize };
}
- #endregion
-
#region Helpers
+ ///
+ /// Align a value to a boundary
+ ///
+ /// TODO: Figure out how to use buffer alignment helpers here
private static long AlignUp(long value, long alignment) => (value + alignment - 1) & ~(alignment - 1);
#endregion
diff --git a/SabreTools.Wrappers/WiaRvzCompressionHelper.cs b/SabreTools.Wrappers/Helpers/WiaRvzCompressionHelper.cs
similarity index 58%
rename from SabreTools.Wrappers/WiaRvzCompressionHelper.cs
rename to SabreTools.Wrappers/Helpers/WiaRvzCompressionHelper.cs
index ecfb1f0b..b4504c21 100644
--- a/SabreTools.Wrappers/WiaRvzCompressionHelper.cs
+++ b/SabreTools.Wrappers/Helpers/WiaRvzCompressionHelper.cs
@@ -9,6 +9,7 @@ using SharpCompress.Compressors.ZStandard;
#endif
using SabreTools.Data.Models.WIA;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
///
@@ -17,10 +18,12 @@ namespace SabreTools.Wrappers
///
internal static class WiaRvzCompressionHelper
{
- // Dictionary sizes per compression level 1–9 (index 0 unused).
- // Mirrors Dolphin WIACompression.cpp dict_size choices.
+ ///
+ /// Dictionary sizes per compression level 1-9 (index 0 unused).
+ ///
+ /// Mirrors Dolphin WIACompression.cpp dict_size choices.
private static readonly int[] DictSizes =
- {
+ [
0, // 0: unused
1 << 16, // 1: 64 KiB
1 << 20, // 2: 1 MiB
@@ -31,29 +34,43 @@ namespace SabreTools.Wrappers
1 << 24, // 7: 16 MiB
1 << 25, // 8: 32 MiB
1 << 26, // 9: 64 MiB
- };
+ ];
- private static int GetDictSize(int level) =>
- DictSizes[Math.Max(1, Math.Min(9, level))];
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static int GetDictSize(int level)
+ => DictSizes[Math.Max(1, Math.Min(9, level))];
- // Returns the raw LZMA2 dict-size property byte for a given dictionary size.
+ ///
+ /// Returns the raw LZMA2 dict-size property byte for a given dictionary size.
+ ///
private static uint Lzma2DictSize(byte p) => (uint)((2 | (p & 1)) << ((p / 2) + 11));
+ ///
+ ///
+ ///
+ ///
+ ///
private static byte EncodeLzma2DictSize(uint d)
{
byte e = 0;
while (e < 40 && d > Lzma2DictSize(e))
+ {
e++;
+ }
+
return e;
}
///
- /// Fills the compressor-data bytes for WiaHeader2.CompressorData /
- /// WiaHeader2.CompressorDataSize.
+ /// Fills the compressor-data bytes for
+ /// and .
/// LZMA: 5 bytes. LZMA2: 1 byte. Others: 0 bytes.
///
- internal static void GetCompressorData(WiaRvzCompressionType type, int level,
- out byte[] propData, out byte propSize)
+ internal static void GetCompressorData(WiaRvzCompressionType type, int level, out byte[] propData, out byte propSize)
{
propData = new byte[7];
int dictSize = GetDictSize(level);
@@ -74,15 +91,27 @@ namespace SabreTools.Wrappers
propSize = 1;
break;
+ // All cases below default to 0
+ case WiaRvzCompressionType.None:
+ case WiaRvzCompressionType.Purge:
+ case WiaRvzCompressionType.Bzip2:
+ case WiaRvzCompressionType.Zstd:
default:
propSize = 0;
break;
}
}
- /// Compress using the specified algorithm.
- internal static byte[] Compress(WiaRvzCompressionType type, byte[] data, int offset,
- int length, int level, byte[] compressorData, byte compressorDataSize)
+ ///
+ /// Compress using the specified algorithm.
+ ///
+ internal static byte[] Compress(WiaRvzCompressionType type,
+ byte[] data,
+ int offset,
+ int length,
+ int level,
+ byte[] compressorData,
+ byte compressorDataSize)
{
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
switch (type)
@@ -95,6 +124,10 @@ namespace SabreTools.Wrappers
return CompressLzma(data, offset, length, level, isLzma2: true);
case WiaRvzCompressionType.Zstd:
return CompressZstd(data, offset, length, level);
+
+ // Do not use compression
+ case WiaRvzCompressionType.None:
+ case WiaRvzCompressionType.Purge:
default:
throw new ArgumentException($"Cannot compress type {type}", nameof(type));
}
@@ -103,29 +136,38 @@ namespace SabreTools.Wrappers
#endif
}
- /// Decompress using the specified algorithm.
- internal static byte[] Decompress(WiaRvzCompressionType type, byte[] data, int offset,
- int length, byte[] compressorData, byte compressorDataSize)
+ ///
+ /// Decompress using the specified algorithm.
+ ///
+ internal static byte[] Decompress(WiaRvzCompressionType type,
+ byte[] data,
+ int offset,
+ int length,
+ byte[] compressorData,
+ byte compressorDataSize)
{
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
switch (type)
{
case WiaRvzCompressionType.Bzip2:
return DecompressBzip2(data, offset, length);
+
case WiaRvzCompressionType.LZMA:
- {
- byte[] props = new byte[compressorDataSize];
- Array.Copy(compressorData, props, compressorDataSize);
- return DecompressLzma(data, offset, length, props, isLzma2: false);
- }
+ byte[] lzmaProps = new byte[compressorDataSize];
+ Array.Copy(compressorData, lzmaProps, compressorDataSize);
+ return DecompressLzma(data, offset, length, lzmaProps, isLzma2: false);
+
case WiaRvzCompressionType.LZMA2:
- {
- byte[] props = new byte[compressorDataSize];
- Array.Copy(compressorData, props, compressorDataSize);
- return DecompressLzma(data, offset, length, props, isLzma2: true);
- }
+ byte[] lzma2Props = new byte[compressorDataSize];
+ Array.Copy(compressorData, lzma2Props, compressorDataSize);
+ return DecompressLzma(data, offset, length, lzma2Props, isLzma2: true);
+
case WiaRvzCompressionType.Zstd:
return DecompressZstd(data, offset, length);
+
+ // Do not use compression
+ case WiaRvzCompressionType.None:
+ case WiaRvzCompressionType.Purge:
default:
throw new ArgumentException($"Cannot decompress type {type}", nameof(type));
}
@@ -135,7 +177,13 @@ namespace SabreTools.Wrappers
}
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
-
+ ///
+ /// Compress data using bzip2
+ ///
+ /// Source data
+ /// Offset into the source data
+ /// Length within the source data
+ /// Compressed data
private static byte[] CompressBzip2(byte[] data, int offset, int length)
{
using var outMs = new MemoryStream();
@@ -143,18 +191,37 @@ namespace SabreTools.Wrappers
{
bz2.Write(data, offset, length);
}
+
return outMs.ToArray();
}
+ ///
+ /// Decompress data using bzip2
+ ///
+ /// Source data
+ /// Offset into the source data
+ /// Length within the source data
+ /// Uncompressed data
private static byte[] DecompressBzip2(byte[] data, int offset, int length)
{
using var inMs = new MemoryStream(data, offset, length);
using var bz2 = BZip2Stream.Create(inMs, CompressionMode.Decompress, false, false);
using var outMs = new MemoryStream();
+
bz2.BlockCopy(outMs);
+
return outMs.ToArray();
}
+ ///
+ /// Compress data using LZMA/LZMA2
+ ///
+ /// Source data
+ /// Offset into the source data
+ /// Length within the source data
+ /// LZMA/LZMA2 level
+ /// Indicates if LZMA2 is used
+ /// Compressed data
private static byte[] CompressLzma(byte[] data, int offset, int length, int level, bool isLzma2)
{
int dictSize = GetDictSize(level);
@@ -163,37 +230,66 @@ namespace SabreTools.Wrappers
{
lzma.Write(data, offset, length);
}
+
return outMs.ToArray();
}
+ ///
+ /// Decompress data using LZMA/LZMA2
+ ///
+ /// Source data
+ /// Offset into the source data
+ /// Length within the source data
+ /// LZMA properties
+ /// Indicates if LZMA2 is used
+ /// Uncompressed data
private static byte[] DecompressLzma(byte[] data, int offset, int length, byte[] props, bool isLzma2)
{
using var inMs = new MemoryStream(data, offset, length);
using var lzma = LzmaStream.Create(props, inMs, length, -1, null, isLzma2, false);
using var outMs = new MemoryStream();
+
lzma.BlockCopy(outMs);
+
return outMs.ToArray();
}
+ ///
+ /// Compress data using Zstd
+ ///
+ /// Source data
+ /// Offset into the source data
+ /// Length within the source data
+ /// Zstd level
+ /// Compressed data
private static byte[] CompressZstd(byte[] data, int offset, int length, int level)
{
using var outMs = new MemoryStream();
- using (var zstd = new ZStandardStream(outMs, CompressionMode.Compress))
+ using (var zstd = new ZStandardStream(outMs, CompressionMode.Compress, level))
{
zstd.Write(data, offset, length);
}
+
return outMs.ToArray();
}
+ ///
+ /// Decompress data using Zstd
+ ///
+ /// Source data
+ /// Offset into the source data
+ /// Length within the source data
+ /// Uncompressed data
private static byte[] DecompressZstd(byte[] data, int offset, int length)
{
using var inMs = new MemoryStream(data, offset, length);
using var zstd = new ZStandardStream(inMs);
using var outMs = new MemoryStream();
+
zstd.BlockCopy(outMs);
+
return outMs.ToArray();
}
-
#endif
}
}
diff --git a/SabreTools.Wrappers/WiaVirtualStream.cs b/SabreTools.Wrappers/Helpers/WiaVirtualStream.cs
similarity index 86%
rename from SabreTools.Wrappers/WiaVirtualStream.cs
rename to SabreTools.Wrappers/Helpers/WiaVirtualStream.cs
index 73f4dacc..c45aa192 100644
--- a/SabreTools.Wrappers/WiaVirtualStream.cs
+++ b/SabreTools.Wrappers/Helpers/WiaVirtualStream.cs
@@ -10,6 +10,7 @@ namespace SabreTools.Wrappers
internal sealed class WiaVirtualStream : Stream
{
private readonly WIA _wia;
+
private long _position;
public WiaVirtualStream(WIA wia)
@@ -17,10 +18,19 @@ namespace SabreTools.Wrappers
_wia = wia ?? throw new ArgumentNullException(nameof(wia));
}
+ ///
public override bool CanRead => true;
+
+ ///
public override bool CanSeek => true;
+
+ ///
public override bool CanWrite => false;
+
+ ///
public override long Length => (long)_wia.IsoFileSize;
+
+ ///
public override long Position
{
get => _position;
@@ -28,10 +38,12 @@ namespace SabreTools.Wrappers
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
+
_position = value;
}
}
+ ///
public override int Read(byte[] buffer, int offset, int count)
{
if (buffer is null)
@@ -53,14 +65,15 @@ namespace SabreTools.Wrappers
return read;
}
+ ///
public override long Seek(long offset, SeekOrigin origin)
{
long newPos;
switch (origin)
{
- case SeekOrigin.Begin: newPos = offset; break;
+ case SeekOrigin.Begin: newPos = offset; break;
case SeekOrigin.Current: newPos = _position + offset; break;
- case SeekOrigin.End: newPos = Length + offset; break;
+ case SeekOrigin.End: newPos = Length + offset; break;
default: throw new ArgumentOutOfRangeException(nameof(origin));
}
@@ -71,10 +84,13 @@ namespace SabreTools.Wrappers
return _position;
}
+ ///
public override void Flush() { }
+ ///
public override void SetLength(long value) => throw new NotSupportedException();
+ ///
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}
diff --git a/SabreTools.Wrappers/NintendoDisc.Encryption.cs b/SabreTools.Wrappers/NintendoDisc.Encryption.cs
index 9b28bc3f..e75f54bb 100644
--- a/SabreTools.Wrappers/NintendoDisc.Encryption.cs
+++ b/SabreTools.Wrappers/NintendoDisc.Encryption.cs
@@ -1,44 +1,62 @@
+using System;
using SabreTools.Data.Models.NintendoDisc;
+using SabreTools.Hashing;
+// TODO: Move to IO
namespace SabreTools.Wrappers
{
public partial class NintendoDisc
{
- #region Wii Encryption / Decryption
+ ///
+ /// Retail common key
+ ///
+ public static byte[] KoreanCommonKey = [];
///
- /// Resolves a Wii common key by its ticket index (0 = retail, 1 = Korean).
- /// Must be set by the caller before invoking .
- /// If , or the delegate returns for a given
- /// index, decryption will return .
+ /// Korean common key SHA-256 hash
///
- public static System.Func? CommonKeyProvider { get; set; }
+ private const string KoreanCommonKeySHA256 = "b9f42ca27a1e178f0f14ebf1a05d486fa8db8d08875336c4e6e8dfae29f2901c";
+
+ ///
+ /// Retail common key
+ ///
+ public static byte[] RetailCommonKey = [];
+
+ ///
+ /// Retail common key SHA-256 hash
+ ///
+ private const string RetailCommonKeySHA256 = "de38aeab4fe0c36d828a47e6fd315100e7ce234d3b00aa25e6ad6f5ff2824af8";
///
/// Decrypt a Wii partition title key from the ticket data.
///
/// 16-byte encrypted title key from ticket offset 0x1BF
/// 8-byte title ID from ticket offset 0x1DC (big-endian)
- ///
- /// Common key index from ticket offset 0x1F1: 0 = retail, 1 = Korean
- ///
+ /// Common key index to use for decryption
/// Decrypted 16-byte title key, or null if no key is available for the given index
- public static byte[]? DecryptTitleKey(byte[] encryptedTitleKey, byte[] titleId, byte commonKeyIndex)
+ public static byte[]? DecryptTitleKey(byte[]? encryptedTitleKey, byte[]? titleId, int commonKeyIndex)
{
if (encryptedTitleKey is null || encryptedTitleKey.Length != 16)
return null;
if (titleId is null || titleId.Length != 8)
return null;
- byte[]? commonKey = CommonKeyProvider?.Invoke(commonKeyIndex);
- if (commonKey is null || commonKey.Length != 16)
+ byte[] commonKey;
+ if (commonKeyIndex == 0)
+ commonKey = RetailCommonKey;
+ else if (commonKeyIndex == 1)
+ commonKey = KoreanCommonKey;
+ else
+ return null;
+
+ if (commonKey.Length != 16)
return null;
// IV is the 8-byte title ID padded with zeros to 16 bytes
byte[] iv = new byte[16];
- System.Array.Copy(titleId, 0, iv, 0, 8);
+ Array.Copy(titleId, 0, iv, 0, 8);
- return DecryptAesCbc(encryptedTitleKey, commonKey, iv);
+ return AesCbc.Decrypt(encryptedTitleKey, commonKey, iv);
}
///
@@ -57,14 +75,39 @@ namespace SabreTools.Wrappers
if (iv is null || iv.Length != 16)
return null;
- return DecryptAesCbc(encryptedData, titleKey, iv);
+ return AesCbc.Decrypt(encryptedData, titleKey, iv);
}
- private static byte[]? DecryptAesCbc(byte[] data, byte[] key, byte[] iv)
+ ///
+ /// Validate the Korean common key based on hash and length
+ ///
+ /// Korean common key to validate
+ /// True if the key was valid, false otherwise
+ public static bool ValidateKoreanCommonKey(byte[]? commonKey)
{
- return AesCbc.Decrypt(data, key, iv);
+ // Ignore invalid values
+ if (commonKey is null || commonKey.Length != 16)
+ return false;
+
+ // Hash the key and compare
+ string? actualHash = HashTool.GetByteArrayHash(commonKey, HashType.SHA256);
+ return string.Equals(actualHash, KoreanCommonKeySHA256, StringComparison.OrdinalIgnoreCase);
}
- #endregion
+ ///
+ /// Validate the retail common key based on hash and length
+ ///
+ /// Retail common key to validate
+ /// True if the key was valid, false otherwise
+ public static bool ValidateRetailCommonKey(byte[]? commonKey)
+ {
+ // Ignore invalid values
+ if (commonKey is null || commonKey.Length != 16)
+ return false;
+
+ // Hash the key and compare
+ string? actualHash = HashTool.GetByteArrayHash(commonKey, HashType.SHA256);
+ return string.Equals(actualHash, RetailCommonKeySHA256, StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/SabreTools.Wrappers/NintendoDisc.Extraction.cs b/SabreTools.Wrappers/NintendoDisc.Extraction.cs
index 6d11d897..2f46779d 100644
--- a/SabreTools.Wrappers/NintendoDisc.Extraction.cs
+++ b/SabreTools.Wrappers/NintendoDisc.Extraction.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Text;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Numerics.Extensions;
+using SabreTools.Text.Extensions;
namespace SabreTools.Wrappers
{
@@ -15,11 +18,9 @@ namespace SabreTools.Wrappers
try
{
- Directory.CreateDirectory(outputDirectory);
-
- if (Model.Platform == Platform.GameCube)
+ if (Platform == Platform.GameCube)
return ExtractGameCube(outputDirectory);
- else if (Model.Platform == Platform.Wii)
+ else if (Platform == Platform.Wii)
return ExtractWii(outputDirectory);
return false;
@@ -28,52 +29,72 @@ namespace SabreTools.Wrappers
{
if (includeDebug)
Console.Error.WriteLine(ex);
+
return false;
}
}
+ #region Pre-decrypted reader override
+
+ ///
+ /// When set, calls this delegate instead of
+ /// performing AES-CBC decryption. Used by WIA/RVZ extraction, where partition data is
+ /// already stored decrypted and the encrypt-then-decrypt round-trip is unnecessary.
+ /// Signature: (absDataOffset, partitionDataOffset, length) -> decrypted bytes or null.
+ ///
+ internal Func? _preDecryptedReader;
+
+ #endregion
+
#region GameCube extraction
- private bool ExtractGameCube(string dest)
+ ///
+ /// Extract GameCube data from a disc
+ ///
+ /// Output directory to write to
+ /// True if the extraction was successful, false otherwise
+ private bool ExtractGameCube(string outputDirectory)
{
- string sysDir = Path.Combine(dest, "sys");
+ string sysDir = Path.Combine(outputDirectory, "sys");
Directory.CreateDirectory(sysDir);
- // sys/boot.bin (disc header, 0x000 – 0x43F)
+ // sys/boot.bin (disc header, 0x000 - 0x43F)
WriteRange(0, Constants.DiscHeaderSize, Path.Combine(sysDir, "boot.bin"));
- // sys/bi2.bin (0x440 – 0x243F)
+ // sys/bi2.bin (0x440 - 0x243F)
WriteRange(Constants.Bi2Address, Constants.Bi2Size, Path.Combine(sysDir, "bi2.bin"));
// sys/apploader.img
- WriteApploader(sysDir);
+ WriteGCApploader(sysDir);
// DOL offset stored without shift on GameCube
- long dolOffset = Model.Header.DolOffset;
+ long dolOffset = Header.DolOffset;
if (dolOffset > 0)
{
- byte[]? dolHeader = ReadDisc(dolOffset, 0xE0);
- if (dolHeader != null)
+ byte[] dolHeaderBytes = ReadRangeFromSource(dolOffset, 0xE0);
+ if (dolHeaderBytes.Length > 0)
{
+ int dolHeaderOffset = 0;
+ var dolHeader = Serialization.Readers.NintendoDisc.ParseDOLHeader(dolHeaderBytes, ref dolHeaderOffset);
+
int dolSize = GetDolSize(dolHeader);
WriteRange(dolOffset, dolSize, Path.Combine(sysDir, "main.dol"));
}
}
// FST offset stored without shift on GameCube
- long fstOffset = Model.Header.FstOffset;
- long fstSize = Model.Header.FstSize;
+ long fstOffset = Header.FstOffset;
+ long fstSize = Header.FstSize;
if (fstOffset > 0 && fstSize > 0)
{
- WriteRange(fstOffset, (int)Math.Min(fstSize, int.MaxValue),
- Path.Combine(sysDir, "fst.bin"));
+ WriteRange(fstOffset, (int)Math.Min(fstSize, int.MaxValue), Path.Combine(sysDir, "fst.bin"));
- byte[]? fstData = ReadDisc(fstOffset, (int)Math.Min(fstSize, int.MaxValue));
- if (fstData != null)
+ byte[] fstData = ReadRangeFromSource(fstOffset, (int)Math.Min(fstSize, int.MaxValue));
+ if (fstData.Length > 0)
{
- string filesDir = Path.Combine(dest, "files");
+ string filesDir = Path.Combine(outputDirectory, "files");
Directory.CreateDirectory(filesDir);
- ExtractFstFiles(fstData, offsetShift: 0, filesDir, ReadDisc);
+ ExtractFstFiles(fstData, offsetShift: 0, filesDir, ReadRangeFromSource);
}
}
@@ -84,28 +105,33 @@ namespace SabreTools.Wrappers
#region Wii extraction
- private bool ExtractWii(string dest)
+ ///
+ /// Extract Wii data from a disc
+ ///
+ /// Output directory to write to
+ /// True if the extraction was successful, false otherwise
+ private bool ExtractWii(string outputDirectory)
{
// Unencrypted disc header area
- string discDir = Path.Combine(dest, "disc");
+ string discDir = Path.Combine(outputDirectory, "disc");
Directory.CreateDirectory(discDir);
+
WriteRange(0, 0x100, Path.Combine(discDir, "header.bin"));
- WriteRange(Constants.WiiRegionDataAddress, Constants.WiiRegionDataSize,
- Path.Combine(discDir, "region.bin"));
+ WriteRange(Constants.WiiRegionDataAddress, Constants.WiiRegionDataSize, Path.Combine(discDir, "region.bin"));
if (Model.PartitionTableEntries is null)
return true;
- var typeCounters = new System.Collections.Generic.Dictionary();
+ var typeCounters = new Dictionary();
foreach (var pte in Model.PartitionTableEntries)
{
- long partOffset = pte.Offset;
+ long partOffset = pte.Offset << 2;
if (partOffset <= 0 || partOffset >= _dataSource.Length)
continue;
string partName = GetPartitionName(pte.Type, typeCounters);
- string partDir = Path.Combine(dest, partName);
+ string partDir = Path.Combine(outputDirectory, partName);
Directory.CreateDirectory(partDir);
ExtractWiiPartition(partOffset, partDir);
@@ -114,15 +140,22 @@ namespace SabreTools.Wrappers
return true;
}
- private void ExtractWiiPartition(long partOffset, string partDir)
+ ///
+ /// Extract a Wii partition
+ ///
+ /// Offset to the partition
+ /// Output directory to write to
+ private void ExtractWiiPartition(long partitionOffset, string outputDirectory)
{
// ticket.bin (unencrypted, 0x2A4 bytes at partition start)
- WriteRange(partOffset, Constants.WiiTicketSize, Path.Combine(partDir, "ticket.bin"));
+ WriteRange(partitionOffset, Constants.WiiTicketSize, Path.Combine(outputDirectory, "ticket.bin"));
- byte[]? ticketData = ReadDisc(partOffset, Constants.WiiTicketSize);
+ byte[]? ticketData = ReadRangeFromSource(partitionOffset, Constants.WiiTicketSize);
if (ticketData is null || ticketData.Length < Constants.TicketCommonKeyIndexOffset + 1)
return;
+ #region Title Key
+
// Decrypt title key
byte[] encTitleKey = new byte[16];
Array.Copy(ticketData, Constants.TicketEncryptedTitleKeyOffset, encTitleKey, 0, 16);
@@ -134,59 +167,76 @@ namespace SabreTools.Wrappers
if (titleKey is null)
return;
- // TMD
- byte[]? tmdSizeBytes = ReadDisc(partOffset + Constants.WiiTmdSizeAddress, 4);
+ #endregion
+
+ #region TMD
+
+ byte[] tmdSizeBytes = ReadRangeFromSource(partitionOffset + Constants.WiiTmdSizeAddress, 4);
int tmdSizePos = 0;
- uint tmdSize = tmdSizeBytes != null
+ uint tmdSize = tmdSizeBytes.Length > 0
? tmdSizeBytes.ReadUInt32BigEndian(ref tmdSizePos)
: 0;
- byte[]? tmdOffBytes = ReadDisc(partOffset + Constants.WiiTmdOffsetAddress, 4);
+
+ byte[] tmdOffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiTmdOffsetAddress, 4);
int tmdOffPos = 0;
- uint tmdOffShifted = tmdOffBytes != null
+ uint tmdOffShifted = tmdOffBytes.Length > 0
? tmdOffBytes.ReadUInt32BigEndian(ref tmdOffPos)
: 0;
+
long tmdOffset = (long)tmdOffShifted << 2;
if (tmdSize > 0 && tmdOffset > 0)
- WriteRange(partOffset + tmdOffset, (int)tmdSize, Path.Combine(partDir, "tmd.bin"));
+ WriteRange(partitionOffset + tmdOffset, (int)tmdSize, Path.Combine(outputDirectory, "tmd.bin"));
- // cert.bin
- byte[]? certSizeBytes = ReadDisc(partOffset + Constants.WiiCertSizeAddress, 4);
+ #endregion
+
+ #region cert.bin
+
+ byte[] certSizeBytes = ReadRangeFromSource(partitionOffset + Constants.WiiCertSizeAddress, 4);
int certSizePos = 0;
- uint certSize = certSizeBytes != null
+ uint certSize = certSizeBytes.Length > 0
? certSizeBytes.ReadUInt32BigEndian(ref certSizePos)
: 0;
- byte[]? certOffBytes = ReadDisc(partOffset + Constants.WiiCertOffsetAddress, 4);
+
+ byte[] certOffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiCertOffsetAddress, 4);
int certOffPos = 0;
- uint certOffShifted = certOffBytes != null
+ uint certOffShifted = certOffBytes.Length > 0
? certOffBytes.ReadUInt32BigEndian(ref certOffPos)
: 0;
+
long certOffset = (long)certOffShifted << 2;
if (certSize > 0 && certOffset > 0)
- WriteRange(partOffset + certOffset, (int)certSize, Path.Combine(partDir, "cert.bin"));
+ WriteRange(partitionOffset + certOffset, (int)certSize, Path.Combine(outputDirectory, "cert.bin"));
- // h3.bin
- byte[]? h3OffBytes = ReadDisc(partOffset + Constants.WiiH3OffsetAddress, 4);
+ #endregion
+
+ #region h3.bin
+
+ byte[] h3OffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiH3OffsetAddress, 4);
int h3OffPos = 0;
- uint h3OffShifted = h3OffBytes != null
+ uint h3OffShifted = h3OffBytes.Length > 0
? h3OffBytes.ReadUInt32BigEndian(ref h3OffPos)
: 0;
+
long h3Offset = (long)h3OffShifted << 2;
if (h3Offset > 0)
- WriteRange(partOffset + h3Offset, Constants.WiiH3Size, Path.Combine(partDir, "h3.bin"));
+ WriteRange(partitionOffset + h3Offset, Constants.WiiH3Size, Path.Combine(outputDirectory, "h3.bin"));
+
+ #endregion
// Encrypted partition data start
- byte[]? dataOffBytes = ReadDisc(partOffset + Constants.WiiDataOffsetAddress, 4);
+ byte[] dataOffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiDataOffsetAddress, 4);
int dataOffPos = 0;
- uint dataOffShifted = dataOffBytes != null
+ uint dataOffShifted = dataOffBytes.Length > 0
? dataOffBytes.ReadUInt32BigEndian(ref dataOffPos)
: 0;
+
long dataOffset = (long)dataOffShifted << 2;
if (dataOffset <= 0)
return;
- long absDataOffset = partOffset + dataOffset;
+ long absDataOffset = partitionOffset + dataOffset;
- string sysDir = Path.Combine(partDir, "sys");
+ string sysDir = Path.Combine(outputDirectory, "sys");
Directory.CreateDirectory(sysDir);
// Read boot block from decrypted partition (block 0, offset 0 within data)
@@ -197,57 +247,79 @@ namespace SabreTools.Wrappers
File.WriteAllBytes(Path.Combine(sysDir, "boot.bin"), bootBlock);
// bi2.bin
- byte[]? bi2 = ReadDecryptedPartitionRange(absDataOffset, titleKey,
- Constants.Bi2Address, Constants.Bi2Size);
- if (bi2 != null)
+ byte[]? bi2 = ReadDecryptedPartitionRange(absDataOffset, titleKey, Constants.Bi2Address, Constants.Bi2Size);
+ if (bi2 is not null)
File.WriteAllBytes(Path.Combine(sysDir, "bi2.bin"), bi2);
// apploader
WriteWiiApploader(absDataOffset, titleKey, sysDir);
- // DOL — stored offset is shifted <<2 in Wii partition
+ #region DOL
+
+ // Stored offset is shifted <<2 in Wii partition
int dolOffPos = 0x420;
uint dolOffShifted = bootBlock.ReadUInt32BigEndian(ref dolOffPos);
long dolOff = (long)dolOffShifted << 2;
if (dolOff > 0)
{
- byte[]? dolHdr = ReadDecryptedPartitionRange(absDataOffset, titleKey, dolOff, 0xE0);
- if (dolHdr != null)
+ byte[]? dolHeaderBytes = ReadDecryptedPartitionRange(absDataOffset, titleKey, dolOff, 0xE0);
+ if (dolHeaderBytes is not null)
{
- int dolSize = GetDolSize(dolHdr);
+ int dolHeaderOffset = 0;
+ var dolHeader = Serialization.Readers.NintendoDisc.ParseDOLHeader(dolHeaderBytes, ref dolHeaderOffset);
+
+ int dolSize = GetDolSize(dolHeader);
byte[]? dol = ReadDecryptedPartitionRange(absDataOffset, titleKey, dolOff, dolSize);
- if (dol != null)
+ if (dol is not null)
File.WriteAllBytes(Path.Combine(sysDir, "main.dol"), dol);
}
}
- // FST — stored offset shifted <<2 in Wii partition
+ #endregion
+
+ #region FST
+
+ // Stored offset shifted <<2 in Wii partition
int fstOffPos = 0x424;
- int fstSzPos = 0x428;
uint fstOffShifted = bootBlock.ReadUInt32BigEndian(ref fstOffPos);
- uint fstSzShifted = bootBlock.ReadUInt32BigEndian(ref fstSzPos);
long fstOff = (long)fstOffShifted << 2;
+
+ int fstSzPos = 0x428;
+ uint fstSzShifted = bootBlock.ReadUInt32BigEndian(ref fstSzPos);
long fstSize = (long)fstSzShifted << 2; // also stored >>2 on Wii
+
if (fstOff > 0 && fstSize > 0)
{
- byte[]? fstData = ReadDecryptedPartitionRange(absDataOffset, titleKey,
- fstOff, (int)Math.Min(fstSize, int.MaxValue));
- if (fstData != null)
+ byte[]? fstData = ReadDecryptedPartitionRange(absDataOffset, titleKey, fstOff, (int)Math.Min(fstSize, int.MaxValue));
+ if (fstData is not null)
{
File.WriteAllBytes(Path.Combine(sysDir, "fst.bin"), fstData);
- string filesDir = Path.Combine(partDir, "files");
+ string filesDir = Path.Combine(outputDirectory, "files");
Directory.CreateDirectory(filesDir);
- ExtractFstFiles(fstData, offsetShift: 2, filesDir,
+ ExtractFstFiles(fstData,
+ offsetShift: 2,
+ filesDir,
(offset, length) => ReadDecryptedPartitionRange(absDataOffset, titleKey, offset, length));
}
}
+
+ #endregion
}
#endregion
- #region FST extraction
+ #region FST Extraction
- private void ExtractFstFiles(byte[] fstData, int offsetShift, string filesDir,
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void ExtractFstFiles(byte[] fstData,
+ int offsetShift,
+ string filesDir,
Func readFunc)
{
if (fstData is null || fstData.Length < 12)
@@ -260,18 +332,21 @@ namespace SabreTools.Wrappers
return;
// String table immediately follows all entries
- long stringTableOffset = rootCount * 12;
+ int stringTableOffset = (int)(rootCount * 12);
- ExtractFstDirectory(fstData, 1, (int)rootCount, stringTableOffset,
- filesDir, offsetShift, readFunc);
+ ExtractFstDirectory(fstData, 1, (int)rootCount, stringTableOffset, filesDir, offsetShift, readFunc);
}
///
/// Recursively extracts FST entries [start, end) into .
/// Returns the index of the next entry after this directory.
///
- private int ExtractFstDirectory(byte[] fstData, int start, int end,
- long stringTableOffset, string currentDir, int offsetShift,
+ private int ExtractFstDirectory(byte[] fstData,
+ int start,
+ int end,
+ int stringTableOffset,
+ string currentDir,
+ int offsetShift,
Func readFunc)
{
int i = start;
@@ -283,14 +358,10 @@ namespace SabreTools.Wrappers
// Each FST entry is 12 bytes: [flags(1) | nameOff(3)] [fileOffRaw(4)] [fileSize(4)]
int fstEntryPos = fstBase;
- uint flagsAndNameOff = fstData.ReadUInt32BigEndian(ref fstEntryPos);
- byte flags = (byte)(flagsAndNameOff >> 24);
- bool isDir = (flags & 1) != 0;
- uint nameOff = flagsAndNameOff & 0x00FFFFFFu;
- uint fileOffRaw = fstData.ReadUInt32BigEndian(ref fstEntryPos);
- uint fileSize = fstData.ReadUInt32BigEndian(ref fstEntryPos);
+ var entry = Serialization.Readers.NintendoDisc.ParseFileSystemTableEntry(fstData, ref fstEntryPos);
- string name = ReadFstString(fstData, stringTableOffset + nameOff);
+ int nameOffset = stringTableOffset + (int)(entry.NameOffset & 0x00FFFFFF);
+ string? name = fstData.ReadNullTerminatedAnsiString(ref nameOffset);
if (string.IsNullOrEmpty(name))
{
i++;
@@ -298,19 +369,21 @@ namespace SabreTools.Wrappers
}
// Sanitize name: replace path separators and reject/flatten dot-segments
- name = name.Replace('/', '_').Replace('\\', '_');
+ name = name!.Replace('/', '_').Replace('\\', '_');
if (name == "." || name == "..")
name = "_";
+
name = name.TrimStart('.');
+ byte flags = (byte)(entry.NameOffset >> 24);
+ bool isDir = (flags & 1) != 0;
if (isDir)
{
// fileOffRaw = parent entry index; fileSize = last entry index in this dir
- int nextEntry = (int)fileSize;
+ int nextEntry = (int)entry.FileSize;
string subDir = Path.Combine(currentDir, name);
Directory.CreateDirectory(subDir);
- i = ExtractFstDirectory(fstData, i + 1, nextEntry, stringTableOffset,
- subDir, offsetShift, readFunc);
+ i = ExtractFstDirectory(fstData, i + 1, nextEntry, stringTableOffset, subDir, offsetShift, readFunc);
}
else
{
@@ -319,16 +392,16 @@ namespace SabreTools.Wrappers
if (!string.IsNullOrEmpty(outDir))
Directory.CreateDirectory(outDir);
- if (fileSize == 0)
+ if (entry.FileSize == 0)
{
// Zero-byte file — create empty
- File.WriteAllBytes(outPath, new byte[0]);
+ File.WriteAllBytes(outPath, []);
}
else
{
- long discOffset = (long)fileOffRaw << offsetShift;
- byte[]? fileData = readFunc(discOffset, (int)Math.Min(fileSize, int.MaxValue));
- if (fileData != null)
+ long discOffset = (long)entry.FileOffset << offsetShift;
+ byte[]? fileData = readFunc(discOffset, (int)Math.Min(entry.FileSize, int.MaxValue));
+ if (fileData is not null)
File.WriteAllBytes(outPath, fileData);
}
@@ -339,103 +412,155 @@ namespace SabreTools.Wrappers
return i;
}
- private static string ReadFstString(byte[] fstData, long offset)
- {
- if (offset < 0 || offset >= fstData.Length)
- return string.Empty;
-
- int start = (int)offset;
- int end = start;
- while (end < fstData.Length && fstData[end] != 0)
- end++;
-
- return System.Text.Encoding.ASCII.GetString(fstData, start, end - start);
- }
-
#endregion
- #region Apploader helpers
+ #region Apploader Writing
- private void WriteApploader(string sysDir)
+ ///
+ /// Write GameCube apploader.img to the output directory, if possible
+ ///
+ /// Output directory to write to
+ private bool WriteGCApploader(string outputDirectory)
{
- byte[]? hdr = ReadDisc(Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
- if (hdr is null) return;
+ byte[] header = ReadRangeFromSource(Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
+ if (header.Length == 0)
+ return false;
- int codeSizePos = Constants.ApploaderCodeSizeOffset;
- int trailerSizePos = Constants.ApploaderTrailerSizeOffset;
- uint codeSize = hdr.ReadUInt32BigEndian(ref codeSizePos);
- uint trailerSize = hdr.ReadUInt32BigEndian(ref trailerSizePos);
+ int index = Constants.ApploaderCodeSizeOffset;
+ uint codeSize = header.ReadUInt32BigEndian(ref index);
+
+ index = Constants.ApploaderTrailerSizeOffset;
+ uint trailerSize = header.ReadUInt32BigEndian(ref index);
int totalSize = Constants.ApploaderHeaderSize + (int)codeSize + (int)trailerSize;
- WriteRange(Constants.ApploaderAddress, totalSize, Path.Combine(sysDir, "apploader.img"));
+ WriteRange(Constants.ApploaderAddress, totalSize, Path.Combine(outputDirectory, "apploader.img"));
+ return true;
}
+ ///
+ /// Write Wii apploader.img to the output directory, if possible
+ ///
+ ///
+ ///
+ ///
private void WriteWiiApploader(long absDataOffset, byte[] titleKey, string sysDir)
{
- byte[]? hdr = ReadDecryptedPartitionRange(absDataOffset, titleKey,
- Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
- if (hdr is null) return;
+ byte[]? header = ReadDecryptedPartitionRange(absDataOffset, titleKey, Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
+ if (header is null)
+ return;
- int wiiCodeSizePos = Constants.ApploaderCodeSizeOffset;
- int wiiTrailerSizePos = Constants.ApploaderTrailerSizeOffset;
- uint codeSize = hdr.ReadUInt32BigEndian(ref wiiCodeSizePos);
- uint trailerSize = hdr.ReadUInt32BigEndian(ref wiiTrailerSizePos);
+ int index = Constants.ApploaderCodeSizeOffset;
+ uint codeSize = header.ReadUInt32BigEndian(ref index);
+
+ index = Constants.ApploaderTrailerSizeOffset;
+ uint trailerSize = header.ReadUInt32BigEndian(ref index);
int totalSize = Constants.ApploaderHeaderSize + (int)codeSize + (int)trailerSize;
- byte[]? apploader = ReadDecryptedPartitionRange(absDataOffset, titleKey,
- Constants.ApploaderAddress, totalSize);
- if (apploader != null)
+ byte[]? apploader = ReadDecryptedPartitionRange(absDataOffset, titleKey, Constants.ApploaderAddress, totalSize);
+ if (apploader is not null)
File.WriteAllBytes(Path.Combine(sysDir, "apploader.img"), apploader);
}
#endregion
- #region DOL size calculation
+ #region Helpers
- private static int GetDolSize(byte[] dolHeader)
+ ///
+ /// Total byte length of the raw disc image data
+ ///
+ internal long DataLength => _dataSource.Length;
+
+ ///
+ /// Read bytes from the disc image at .
+ ///
+ internal byte[] ReadData(long offset, int length) => ReadRangeFromSource(offset, length);
+
+ ///
+ /// Get the size of the DOL based on the header
+ ///
+ /// DOL header to retrieve the size for
+ /// The size of the DOL on success, 0 otherwise
+ private static int GetDolSize(DOLHeader dolHeader)
{
- // DOL header: 7 text section offsets (0x00), 11 data section offsets (0x1C),
- // 7 text sizes (0x90), 11 data sizes (0xAC), BSS offset (0xD8), BSS size (0xDC),
- // entry point (0xE0). Max (offset + size) over all sections gives the DOL size.
- if (dolHeader is null || dolHeader.Length < 0xE0)
+ if (dolHeader.SectionOffsetTable.Length != 18)
+ return 0;
+ if (dolHeader.SectionLengthsTable.Length != 18)
return 0;
int maxEnd = 0;
- // Text sections (7): offset table at 0x00, size table at 0x90
- for (int s = 0; s < 7; s++)
+
+ // Loop through the 18 offset and size entries
+ for (int i = 0; i < 18; i++)
{
- int offPos = s * 4;
- int szPos = 0x90 + (s * 4);
- int off = (int)dolHeader.ReadUInt32BigEndian(ref offPos);
- int sz = (int)dolHeader.ReadUInt32BigEndian(ref szPos);
- if (off > 0 && sz > 0) maxEnd = Math.Max(maxEnd, off + sz);
- }
- // Data sections (11): offset table at 0x1C, size table at 0xAC
- for (int s = 0; s < 11; s++)
- {
- int offPos = 0x1C + (s * 4);
- int szPos = 0xAC + (s * 4);
- int off = (int)dolHeader.ReadUInt32BigEndian(ref offPos);
- int sz = (int)dolHeader.ReadUInt32BigEndian(ref szPos);
- if (off > 0 && sz > 0) maxEnd = Math.Max(maxEnd, off + sz);
+ uint offset = dolHeader.SectionOffsetTable[i];
+ uint size = dolHeader.SectionLengthsTable[i];
+ if (offset > 0 && size > 0)
+ maxEnd = (int)Math.Max(maxEnd, offset + size);
}
return maxEnd;
}
- #endregion
+ ///
+ /// Get the name of a given partition type
+ ///
+ /// Parition type
+ /// Dictionary of counters used for global indexing
+ /// String representing the partition name
+ private static string GetPartitionName(uint type, Dictionary counters)
+ {
+ string code;
+ switch (type)
+ {
+ // GM + counter
+ case 0: code = "GM"; break;
- #region Wii partition block decryption helpers
+ // UP + counter
+ case 1: code = "UP"; break;
+
+ // CH + counter
+ case 2: code = "CH"; break;
+
+ // Unknown: if all 4 bytes are printable ASCII, use the raw 4-char string (no prefix, no counter).
+ // Otherwise fall back to P{globalIndex} — we use the cumulative counter sum as the index.
+ default:
+ byte b0 = (byte)(type >> 24),
+ b1 = (byte)(type >> 16),
+ b2 = (byte)(type >> 8),
+ b3 = (byte)type;
+
+ if (b0 >= 0x20 && b0 <= 0x7E
+ && b1 >= 0x20 && b1 <= 0x7E
+ && b2 >= 0x20 && b2 <= 0x7E
+ && b3 >= 0x20 && b3 <= 0x7E)
+ {
+ return Encoding.ASCII.GetString([b0, b1, b2, b3]);
+ }
+
+ // Non-printable: use global partition index (sum of all counter values so far)
+ int globalIndex = 0;
+ foreach (var v in counters.Values)
+ {
+ globalIndex += v;
+ }
+
+ return $"P{globalIndex}";
+ }
+
+ int index = counters.TryGetValue(type, out int cv) ? cv : 0;
+ counters[type] = index + 1;
+ return $"{code}{index}";
+ }
///
/// Reads bytes at within
/// the decrypted partition data, decrypting 0x8000-byte blocks as needed.
/// is the absolute ISO offset where the encrypted data begins.
///
- private byte[]? ReadDecryptedPartitionRange(long absDataOffset, byte[] titleKey,
- long partitionDataOffset, int length)
+ private byte[]? ReadDecryptedPartitionRange(long absDataOffset, byte[] titleKey, long partitionDataOffset, int length)
{
- if (length <= 0) return null;
+ if (length <= 0)
+ return null;
// WIA/RVZ fast path: data is already decrypted; skip the AES round-trip.
if (_preDecryptedReader is not null)
@@ -448,11 +573,11 @@ namespace SabreTools.Wrappers
{
long dataOff = partitionDataOffset + produced;
long blockNum = dataOff / Constants.WiiBlockDataSize;
- int offsetInBlock = (int)(dataOff % Constants.WiiBlockDataSize);
+ int offsetInBlock = (int)(dataOff % Constants.WiiBlockDataSize);
long encBlockOffset = absDataOffset + (blockNum * Constants.WiiBlockSize);
- byte[]? encBlock = ReadDisc(encBlockOffset, Constants.WiiBlockSize);
- if (encBlock is null || encBlock.Length < Constants.WiiBlockSize)
+ byte[] encBlock = ReadRangeFromSource(encBlockOffset, Constants.WiiBlockSize);
+ if (encBlock.Length < Constants.WiiBlockSize)
break;
// IV is at offset 0x3D0 of the raw (still-encrypted) block.
@@ -460,7 +585,7 @@ namespace SabreTools.Wrappers
byte[] iv = new byte[16];
Array.Copy(encBlock, 0x3D0, iv, 0, 16);
- // Decrypt the 0x7C00 data portion (bytes 0x400–0x7FFF of the raw block)
+ // Decrypt the 0x7C00 data portion (bytes 0x400-0x7FFF of the raw block)
byte[] encData = new byte[Constants.WiiBlockDataSize];
Array.Copy(encBlock, Constants.WiiBlockHeaderSize, encData, 0, Constants.WiiBlockDataSize);
@@ -476,64 +601,26 @@ namespace SabreTools.Wrappers
return produced == length ? result : null;
}
- #endregion
-
- #region Misc helpers
-
+ ///
+ /// Write a range from the underlying data source to a file
+ ///
+ /// Offset into the data source
+ /// Length of the data to read and write
+ /// Full path to the expected output file
private void WriteRange(long offset, int length, string filePath)
{
- if (length <= 0) return;
- byte[]? data = ReadDisc(offset, length);
- if (data is null) return;
- string? dir = Path.GetDirectoryName(filePath);
- if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
- File.WriteAllBytes(filePath, data);
- }
+ if (length <= 0)
+ return;
- private byte[]? ReadDisc(long offset, int length)
- {
- if (length <= 0 || offset < 0) return null;
byte[] data = ReadRangeFromSource(offset, length);
- return data.Length == length ? data : null;
- }
+ if (data.Length == 0)
+ return;
- /// Total byte length of the raw disc image data.
- internal long DataLength => _dataSource.Length;
+ string? dir = Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
- ///
- /// Read bytes from the disc image at .
- /// Returns null if the range is out of bounds or a short read occurs.
- ///
- internal byte[]? ReadData(long offset, int length) => ReadDisc(offset, length);
-
- private static string GetPartitionName(uint type,
- System.Collections.Generic.Dictionary counters)
- {
- // Matches DolphinIsoLib WiiDiscExtractor.PartitionFolderName exactly.
- // Known types: 0→GM+counter, 1→UP+counter, 2→CH+counter.
- // Unknown: if all 4 bytes are printable ASCII, use the raw 4-char string (no prefix, no counter).
- // Otherwise fall back to P{globalIndex} — we use the cumulative counter sum as the index.
- string code;
- switch (type)
- {
- case 0: code = "GM"; break;
- case 1: code = "UP"; break;
- case 2: code = "CH"; break;
- default:
- byte b0 = (byte)(type >> 24), b1 = (byte)(type >> 16),
- b2 = (byte)(type >> 8), b3 = (byte)type;
- if (b0 >= 0x20 && b0 <= 0x7E && b1 >= 0x20 && b1 <= 0x7E &&
- b2 >= 0x20 && b2 <= 0x7E && b3 >= 0x20 && b3 <= 0x7E)
- return System.Text.Encoding.ASCII.GetString(new byte[] { b0, b1, b2, b3 });
- // Non-printable: use global partition index (sum of all counter values so far)
- int globalIdx = 0;
- foreach (var v in counters.Values) globalIdx += v;
- return $"P{globalIdx}";
- }
-
- int idx = counters.TryGetValue(type, out int cv) ? cv : 0;
- counters[type] = idx + 1;
- return $"{code}{idx}";
+ File.WriteAllBytes(filePath, data);
}
#endregion
diff --git a/SabreTools.Wrappers/NintendoDisc.Printing.cs b/SabreTools.Wrappers/NintendoDisc.Printing.cs
index 72de4fc0..38edcc4d 100644
--- a/SabreTools.Wrappers/NintendoDisc.Printing.cs
+++ b/SabreTools.Wrappers/NintendoDisc.Printing.cs
@@ -1,4 +1,5 @@
using System.Text;
+using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Text.Extensions;
namespace SabreTools.Wrappers
@@ -19,44 +20,69 @@ namespace SabreTools.Wrappers
builder.AppendLine($"{Platform} Disc Image Information:");
builder.AppendLine("-------------------------");
- builder.AppendLine("Disc Header:");
- builder.AppendLine(Header.GameId, " Game ID");
- builder.AppendLine(Header.MakerCode, " Maker Code");
- builder.AppendLine(Header.DiscNumber, " Disc Number");
- builder.AppendLine(Header.DiscVersion, " Disc Version");
- builder.AppendLine(Header.AudioStreaming, " Audio Streaming");
- builder.AppendLine(Header.StreamingBufferSize, " Streaming Buffer Size");
- builder.AppendLine(Header.WiiMagic, " Wii Magic");
- builder.AppendLine(Header.GCMagic, " GC Magic");
- builder.AppendLine(Header.GameTitle, " Game Title");
- builder.AppendLine(Header.DisableHashVerification, " Disable Hash Verification");
- builder.AppendLine(Header.DisableDiscEncryption, " Disable Disc Encryption");
- builder.AppendLine(Header.DolOffset, " DOL Offset");
- builder.AppendLine(Header.FstOffset, " FST Offset");
- builder.AppendLine(Header.FstSize, " FST Size");
+ Print(builder, Header);
+ Print(builder, PartitionTableEntries);
+ Print(builder, RegionData);
+ }
+
+ private static void Print(StringBuilder builder, DiscHeader header)
+ {
+ builder.AppendLine(" Disc Header:");
+ builder.AppendLine(" -------------------------");
+
+ builder.AppendLine(header.GameId, " Game ID");
+ builder.AppendLine(header.DiscNumber, " Disc Number");
+ builder.AppendLine(header.DiscVersion, " Disc Version");
+ builder.AppendLine(header.AudioStreaming, " Audio Streaming");
+ builder.AppendLine(header.StreamingBufferSize, " Streaming Buffer Size");
+ builder.AppendLine(header.WiiMagic, " Wii Magic");
+ builder.AppendLine(header.GCMagic, " GC Magic");
+ builder.AppendLine(header.GameTitle, " Game Title");
+ builder.AppendLine(header.DisableHashVerification, " Disable Hash Verification");
+ builder.AppendLine(header.DisableDiscEncryption, " Disable Disc Encryption");
+ builder.AppendLine(header.DolOffset, " DOL Offset");
+ builder.AppendLine(header.FstOffset, " FST Offset");
+ builder.AppendLine(header.FstSize, " FST Size");
builder.AppendLine();
+ }
- if (PartitionTableEntries is { Length: > 0 })
+ private static void Print(StringBuilder builder, WiiPartitionTableEntry[]? entries)
+ {
+ builder.AppendLine(" Partition Table:");
+ builder.AppendLine(" -------------------------");
+ if (entries is null || entries.Length == 0)
{
- builder.AppendLine($"Partition Table ({PartitionTableEntries.Length} entries):");
- for (int i = 0; i < PartitionTableEntries.Length; i++)
- {
- var pt = PartitionTableEntries[i];
- builder.AppendLine($" Partition {i}:");
- builder.AppendLine(pt.Offset, " Offset");
- builder.AppendLine(pt.Type, " Type");
- }
-
+ builder.AppendLine(" No partition table entries");
builder.AppendLine();
+ return;
}
- if (RegionData is not null)
+ for (int i = 0; i < entries.Length; i++)
{
- builder.AppendLine("Region Data:");
- builder.AppendLine(RegionData.RegionSetting, " Region Setting");
- builder.AppendLine(RegionData.AgeRatings, " Age Ratings");
- builder.AppendLine();
+ var entry = entries[i];
+
+ builder.AppendLine($" Partition Table Entry {i}");
+ builder.AppendLine(entry.Offset, " Offset");
+ builder.AppendLine(entry.Type, " Type");
}
+
+ builder.AppendLine();
+ }
+
+ private static void Print(StringBuilder builder, WiiRegionData? regionData)
+ {
+ builder.AppendLine(" Region Data:");
+ builder.AppendLine(" -------------------------");
+ if (regionData is null)
+ {
+ builder.AppendLine(" No region data");
+ builder.AppendLine();
+ return;
+ }
+
+ builder.AppendLine(regionData.RegionSetting, " Region Setting");
+ builder.AppendLine(regionData.AgeRatings, " Age Ratings");
+ builder.AppendLine();
}
}
}
diff --git a/SabreTools.Wrappers/NintendoDisc.cs b/SabreTools.Wrappers/NintendoDisc.cs
index c33232af..7dd8cf5c 100644
--- a/SabreTools.Wrappers/NintendoDisc.cs
+++ b/SabreTools.Wrappers/NintendoDisc.cs
@@ -1,5 +1,5 @@
-using System;
using System.IO;
+using SabreTools.Data.Extensions;
using SabreTools.Data.Models.NintendoDisc;
namespace SabreTools.Wrappers
@@ -18,14 +18,19 @@ namespace SabreTools.Wrappers
///
public DiscHeader Header => Model.Header;
- ///
- public Platform Platform => Model.Platform;
+ ///
+ /// Detected platform (GameCube or Wii)
+ ///
+ public Platform Platform => Header.GetPlatform();
///
public string GameId => Model.Header.GameId;
- ///
- public string MakerCode => Model.Header.MakerCode;
+ ///
+ /// 2-character ASCII maker / publisher code (e.g. "01")
+ ///
+ public string MakerCode
+ => GameId.Length >= 6 ? GameId.Substring(4, 2) : string.Empty;
///
public string GameTitle => Model.Header.GameTitle;
@@ -44,18 +49,6 @@ namespace SabreTools.Wrappers
#endregion
- #region Pre-decrypted reader override
-
- ///
- /// When set, calls this delegate instead of
- /// performing AES-CBC decryption. Used by WIA/RVZ extraction, where partition data is
- /// already stored decrypted and the encrypt-then-decrypt round-trip is unnecessary.
- /// Signature: (absDataOffset, partitionDataOffset, length) → decrypted bytes or null.
- ///
- internal Func? _preDecryptedReader;
-
- #endregion
-
#region Constructors
///
diff --git a/SabreTools.Wrappers/RvzPackDecompressor.cs b/SabreTools.Wrappers/RvzPackDecompressor.cs
deleted file mode 100644
index 7b3d1aa1..00000000
--- a/SabreTools.Wrappers/RvzPackDecompressor.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System;
-using SabreTools.Numerics.Extensions;
-
-namespace SabreTools.Wrappers
-{
- ///
- /// Decompressor for RVZ packed format.
- /// RVZ uses run-length encoding to store real data and junk data efficiently:
- /// - Real data: size (4 bytes) + data bytes
- /// - Junk data: size with high bit set (4 bytes) + 68-byte seed → regenerate using LFG
- ///
- internal class RvzPackDecompressor
- {
- private readonly byte[] m_packed_data;
- private readonly uint m_rvz_packed_size;
- private long m_data_offset;
- private readonly LaggedFibonacciGenerator m_lfg;
-
- private int m_in_position = 0;
- private uint m_current_size = 0;
- private bool m_current_is_junk = false;
-
- ///
- /// Creates a new RVZ pack decompressor.
- ///
- /// The packed RVZ data
- /// Expected size of packed data (for validation)
- /// Offset in the virtual disc (for LFG alignment)
- public RvzPackDecompressor(byte[] packedData, uint rvzPackedSize, long dataOffset)
- {
- m_packed_data = packedData ?? throw new ArgumentNullException(nameof(packedData));
- m_rvz_packed_size = rvzPackedSize;
- m_data_offset = dataOffset;
- m_lfg = new LaggedFibonacciGenerator();
- }
-
- ///
- /// Decompresses the packed data into the output buffer.
- ///
- /// Destination buffer
- /// Offset in destination buffer
- /// Number of bytes to decompress
- /// Number of bytes actually decompressed
- public int Decompress(byte[] output, int outputOffset, int count)
- {
- int totalWritten = 0;
-
- while (totalWritten < count && !IsDone())
- {
- if (m_current_size == 0)
- {
- if (!ReadNextSegment())
- break;
- }
-
- int bytesToWrite = Math.Min((int)m_current_size, count - totalWritten);
-
- if (m_current_is_junk)
- {
- m_lfg.GetBytes(bytesToWrite, output, outputOffset + totalWritten);
- }
- else
- {
- Array.Copy(m_packed_data, m_in_position, output, outputOffset + totalWritten, bytesToWrite);
- m_in_position += bytesToWrite;
- }
-
- m_current_size -= (uint)bytesToWrite;
- totalWritten += bytesToWrite;
- m_data_offset += bytesToWrite;
- }
-
- return totalWritten;
- }
-
- ///
- /// Checks if decompression is complete.
- ///
- public bool IsDone() => m_current_size == 0 && m_in_position >= m_rvz_packed_size;
-
- private bool ReadNextSegment()
- {
- if (m_in_position + 4 > m_packed_data.Length)
- return false;
-
- // Size field is big-endian u32; high bit signals junk data
- uint sizeField = m_packed_data.ReadUInt32BigEndian(ref m_in_position);
-
- m_current_is_junk = (sizeField & 0x80000000) != 0;
- m_current_size = sizeField & 0x7FFFFFFF;
-
- if (m_current_is_junk)
- {
- if (m_in_position + (LaggedFibonacciGenerator.SEED_SIZE * 4) > m_packed_data.Length)
- return false;
-
- byte[] seed = new byte[LaggedFibonacciGenerator.SEED_SIZE * 4];
- Array.Copy(m_packed_data, m_in_position, seed, 0, seed.Length);
- m_in_position += seed.Length;
-
- m_lfg.SetSeed(seed);
-
- // Advance LFG to the correct position within the buffer.
- // Dolphin: lfg.m_position_bytes = data_offset % (LFG_K * sizeof(u32))
- int offsetInBuffer = (int)(m_data_offset % LaggedFibonacciGenerator.BUFFER_BYTES);
- if (offsetInBuffer > 0)
- m_lfg.Forward(offsetInBuffer);
- }
-
- return true;
- }
- }
-}
diff --git a/SabreTools.Wrappers/WIA.Extraction.cs b/SabreTools.Wrappers/WIA.Extraction.cs
index 4edd645b..d95d93e6 100644
--- a/SabreTools.Wrappers/WIA.Extraction.cs
+++ b/SabreTools.Wrappers/WIA.Extraction.cs
@@ -1,5 +1,3 @@
-using System.IO;
-
namespace SabreTools.Wrappers
{
public partial class WIA : IExtractable
@@ -7,7 +5,6 @@ namespace SabreTools.Wrappers
///
public bool Extract(string outputDirectory, bool includeDebug)
{
- // Decompress WIA/RVZ to obtain the inner disc image, then delegate extraction.
var inner = GetInnerWrapper();
return inner?.Extract(outputDirectory, includeDebug) ?? false;
}
diff --git a/SabreTools.Wrappers/WIA.Printing.cs b/SabreTools.Wrappers/WIA.Printing.cs
index ec79d682..13aff213 100644
--- a/SabreTools.Wrappers/WIA.Printing.cs
+++ b/SabreTools.Wrappers/WIA.Printing.cs
@@ -1,4 +1,6 @@
using System.Text;
+using SabreTools.Data.Models.NintendoDisc;
+using SabreTools.Data.Models.WIA;
using SabreTools.Text.Extensions;
namespace SabreTools.Wrappers
@@ -19,86 +21,125 @@ namespace SabreTools.Wrappers
string formatName = IsRvz ? "RVZ" : "WIA";
builder.AppendLine($"{formatName} Information:");
builder.AppendLine("-------------------------");
-
- builder.AppendLine("Header 1:");
- builder.AppendLine(Header1.Magic, " Magic");
- builder.AppendLine(Header1.Version, " Version");
- builder.AppendLine(Header1.VersionCompatible, " Version Compatible");
- builder.AppendLine(Header1.Header2Size, " Header 2 Size");
- builder.AppendLine(Header1.Header2Hash, " Header 2 Hash");
- builder.AppendLine(Header1.IsoFileSize, " ISO File Size");
- builder.AppendLine(Header1.WiaFileSize, " WIA File Size");
- builder.AppendLine(Header1.Header1Hash, " Header 1 Hash");
builder.AppendLine();
- builder.AppendLine("Header 2:");
- builder.AppendLine(Header2.DiscType.ToString(), " Disc Type");
- builder.AppendLine(Header2.CompressionType.ToString(), " Compression Type");
- builder.AppendLine(Header2.CompressionLevel, " Compression Level");
- builder.AppendLine(Header2.ChunkSize, " Chunk Size");
- builder.AppendLine(Header2.DiscHeader, " Disc Header");
- builder.AppendLine(Header2.NumberOfPartitionEntries, " Partition Entry Count");
- builder.AppendLine(Header2.PartitionEntrySize, " Partition Entry Size");
- builder.AppendLine(Header2.PartitionEntriesOffset, " Partition Entries Offset");
- builder.AppendLine(Header2.PartitionEntriesHash, " Partition Entries Hash");
- builder.AppendLine(Header2.NumberOfRawDataEntries, " Raw Data Entry Count");
- builder.AppendLine(Header2.RawDataEntriesOffset, " Raw Data Entries Offset");
- builder.AppendLine(Header2.RawDataEntriesSize, " Raw Data Entries Size");
- builder.AppendLine(Header2.NumberOfGroupEntries, " Group Entry Count");
- builder.AppendLine(Header2.GroupEntriesOffset, " Group Entries Offset");
- builder.AppendLine(Header2.GroupEntriesSize, " Group Entries Size");
- builder.AppendLine(Header2.CompressorDataSize, " Compressor Data Size");
- builder.AppendLine(Header2.CompressorData, " Compressor Data");
+ Print(builder, Header1);
+ Print(builder, Header2);
+ Print(builder, DiscHeader);
+ Print(builder, PartitionEntries);
+ Print(builder, RawDataEntries);
+ }
+
+ private static void Print(StringBuilder builder, WiaHeader1 header)
+ {
+ builder.AppendLine(" Header 1:");
+ builder.AppendLine(" -------------------------");
+ builder.AppendLine(header.Magic, " Magic");
+ builder.AppendLine(header.Version, " Version");
+ builder.AppendLine(header.VersionCompatible, " Version Compatible");
+ builder.AppendLine(header.Header2Size, " Header 2 Size");
+ builder.AppendLine(header.Header2Hash, " Header 2 Hash");
+ builder.AppendLine(header.IsoFileSize, " ISO File Size");
+ builder.AppendLine(header.WiaFileSize, " WIA File Size");
+ builder.AppendLine(header.Header1Hash, " Header 1 Hash");
builder.AppendLine();
+ }
- var discHeader = DiscHeader;
- if (discHeader is not null)
+ private static void Print(StringBuilder builder, WiaHeader2 header)
+ {
+ builder.AppendLine(" Header 2:");
+ builder.AppendLine(" -------------------------");
+ builder.AppendLine(header.DiscType.ToString(), " Disc Type");
+ builder.AppendLine(header.CompressionType.ToString(), " Compression Type");
+ builder.AppendLine(header.CompressionLevel, " Compression Level");
+ builder.AppendLine(header.ChunkSize, " Chunk Size");
+ builder.AppendLine(header.DiscHeader, " Disc Header");
+ builder.AppendLine(header.NumberOfPartitionEntries, " Partition Entry Count");
+ builder.AppendLine(header.PartitionEntrySize, " Partition Entry Size");
+ builder.AppendLine(header.PartitionEntriesOffset, " Partition Entries Offset");
+ builder.AppendLine(header.PartitionEntriesHash, " Partition Entries Hash");
+ builder.AppendLine(header.NumberOfRawDataEntries, " Raw Data Entry Count");
+ builder.AppendLine(header.RawDataEntriesOffset, " Raw Data Entries Offset");
+ builder.AppendLine(header.RawDataEntriesSize, " Raw Data Entries Size");
+ builder.AppendLine(header.NumberOfGroupEntries, " Group Entry Count");
+ builder.AppendLine(header.GroupEntriesOffset, " Group Entries Offset");
+ builder.AppendLine(header.GroupEntriesSize, " Group Entries Size");
+ builder.AppendLine(header.CompressorDataSize, " Compressor Data Size");
+ builder.AppendLine(header.CompressorData, " Compressor Data");
+ builder.AppendLine();
+ }
+
+ private static void Print(StringBuilder builder, DiscHeader? header)
+ {
+ builder.AppendLine(" Embedded Disc Header:");
+ builder.AppendLine(" -------------------------");
+ if (header is null)
{
- builder.AppendLine("Embedded Disc Header:");
- builder.AppendLine(discHeader.GameId, " Game ID");
- builder.AppendLine(discHeader.MakerCode, " Maker Code");
- builder.AppendLine(discHeader.DiscNumber, " Disc Number");
- builder.AppendLine(discHeader.DiscVersion, " Disc Version");
- builder.AppendLine(discHeader.GameTitle, " Game Title");
+ builder.AppendLine(" No embedded disc header");
builder.AppendLine();
+ return;
}
- if (PartitionEntries is { Length: > 0 })
- {
- builder.AppendLine($"Partition Entries ({PartitionEntries.Length}):");
- for (int i = 0; i < PartitionEntries.Length; i++)
- {
- var pe = PartitionEntries[i];
- builder.AppendLine($" Partition {i}:");
- builder.AppendLine(pe.PartitionKey, " Partition Key");
- builder.AppendLine(pe.DataEntry0.FirstSector, " Data Entry 0 First Sector");
- builder.AppendLine(pe.DataEntry0.NumberOfSectors, " Data Entry 0 Sector Count");
- builder.AppendLine(pe.DataEntry0.GroupIndex, " Data Entry 0 Group Index");
- builder.AppendLine(pe.DataEntry0.NumberOfGroups, " Data Entry 0 Group Count");
- builder.AppendLine(pe.DataEntry1.FirstSector, " Data Entry 1 First Sector");
- builder.AppendLine(pe.DataEntry1.NumberOfSectors, " Data Entry 1 Sector Count");
- builder.AppendLine(pe.DataEntry1.GroupIndex, " Data Entry 1 Group Index");
- builder.AppendLine(pe.DataEntry1.NumberOfGroups, " Data Entry 1 Group Count");
- }
+ builder.AppendLine(header.GameId, " Game ID");
+ builder.AppendLine(header.DiscNumber, " Disc Number");
+ builder.AppendLine(header.DiscVersion, " Disc Version");
+ builder.AppendLine(header.GameTitle, " Game Title");
+ builder.AppendLine();
+ }
+ private static void Print(StringBuilder builder, PartitionEntry[]? entries)
+ {
+ builder.AppendLine(" Partition Entry Table:");
+ builder.AppendLine(" -------------------------");
+ if (entries is null || entries.Length == 0)
+ {
+ builder.AppendLine(" No partition table entries");
builder.AppendLine();
+ return;
}
- if (RawDataEntries is { Length: > 0 })
+ for (int i = 0; i < entries.Length; i++)
{
- builder.AppendLine($"Raw Data Entries ({RawDataEntries.Length}):");
- for (int i = 0; i < RawDataEntries.Length; i++)
- {
- var rde = RawDataEntries[i];
- builder.AppendLine($" Raw Data Entry {i}:");
- builder.AppendLine(rde.DataOffset, " Data Offset");
- builder.AppendLine(rde.DataSize, " Data Size");
- builder.AppendLine(rde.GroupIndex, " Group Index");
- builder.AppendLine(rde.NumberOfGroups, " Group Count");
- }
+ var entry = entries[i];
- builder.AppendLine();
+ builder.AppendLine($" Partition Table Entry {i}:");
+ builder.AppendLine(entry.PartitionKey, " Partition Key");
+ builder.AppendLine(entry.DataEntry0.FirstSector, " Data Entry 0 First Sector");
+ builder.AppendLine(entry.DataEntry0.NumberOfSectors, " Data Entry 0 Sector Count");
+ builder.AppendLine(entry.DataEntry0.GroupIndex, " Data Entry 0 Group Index");
+ builder.AppendLine(entry.DataEntry0.NumberOfGroups, " Data Entry 0 Group Count");
+ builder.AppendLine(entry.DataEntry1.FirstSector, " Data Entry 1 First Sector");
+ builder.AppendLine(entry.DataEntry1.NumberOfSectors, " Data Entry 1 Sector Count");
+ builder.AppendLine(entry.DataEntry1.GroupIndex, " Data Entry 1 Group Index");
+ builder.AppendLine(entry.DataEntry1.NumberOfGroups, " Data Entry 1 Group Count");
}
+
+ builder.AppendLine();
+ }
+
+ private static void Print(StringBuilder builder, RawDataEntry[]? entries)
+ {
+ builder.AppendLine(" Raw Data Entry Table:");
+ builder.AppendLine(" -------------------------");
+ if (entries is null || entries.Length == 0)
+ {
+ builder.AppendLine(" No raw data table entries");
+ builder.AppendLine();
+ return;
+ }
+
+ for (int i = 0; i < entries.Length; i++)
+ {
+ var entry = entries[i];
+
+ builder.AppendLine($" Raw Data Table Entry {i}:");
+ builder.AppendLine(entry.DataOffset, " Data Offset");
+ builder.AppendLine(entry.DataSize, " Data Size");
+ builder.AppendLine(entry.GroupIndex, " Group Index");
+ builder.AppendLine(entry.NumberOfGroups, " Group Count");
+ }
+
+ builder.AppendLine();
}
}
}
diff --git a/SabreTools.Wrappers/WIA.Writing.cs b/SabreTools.Wrappers/WIA.Writing.cs
index 6161b65f..725435c1 100644
--- a/SabreTools.Wrappers/WIA.Writing.cs
+++ b/SabreTools.Wrappers/WIA.Writing.cs
@@ -4,35 +4,46 @@ using System.IO;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Threading.Tasks;
#endif
-using SabreTools.Hashing;
-using NdConstants = SabreTools.Data.Models.NintendoDisc.Constants;
-using WiaConst = SabreTools.Data.Models.WIA.Constants;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Data.Models.WIA;
+using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
+using static SabreTools.Data.Models.NintendoDisc.Constants;
+using static SabreTools.Data.Models.WIA.Constants;
namespace SabreTools.Wrappers
{
public partial class WIA : IWritable
{
- // -----------------------------------------------------------------------
- // Public entry points
- // -----------------------------------------------------------------------
+ ///
+ public bool Write(string outputPath, bool includeDebug)
+ {
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ string ext = IsRvz ? ".rvz" : ".wia";
+ string outputFilename = Filename is null
+ ? (Guid.NewGuid().ToString() + ext)
+ : (Filename + ".new");
+ outputPath = Path.GetFullPath(outputFilename);
+ }
+
+ var writer = new Serialization.Writers.WIA { Debug = includeDebug };
+ return writer.SerializeFile(Model, outputPath);
+ }
///
/// Compress a wrapper to a WIA or RVZ file.
///
- public static bool ConvertFromDisc(NintendoDisc source, string outputPath,
+ public static bool ConvertFromDisc(NintendoDisc source,
+ string outputPath,
bool isRvz = false,
WiaRvzCompressionType compressionType = WiaRvzCompressionType.None,
int compressionLevel = 5,
- uint chunkSize = WiaConst.DefaultChunkSize)
+ uint chunkSize = DefaultChunkSize)
{
- if (source is null)
- return false;
if (string.IsNullOrEmpty(outputPath))
return false;
- if (!isRvz && chunkSize != WiaConst.DefaultChunkSize)
+ if (!isRvz && chunkSize != DefaultChunkSize)
return false;
if (isRvz && compressionType == WiaRvzCompressionType.Purge)
return false;
@@ -40,8 +51,7 @@ namespace SabreTools.Wrappers
try
{
using var fs = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
- return WriteWiaRvz(source, fs, isRvz, compressionType,
- Math.Max(1, Math.Min(22, compressionLevel)), chunkSize);
+ return WriteWiaRvz(source, fs, isRvz, compressionType, Math.Max(1, Math.Min(22, compressionLevel)), chunkSize);
}
catch
{
@@ -55,7 +65,9 @@ namespace SabreTools.Wrappers
/// rather than silently swallowed.
///
/// Set to the exception thrown on failure, or null on success.
- public static bool ConvertFromDiscToStream(NintendoDisc source, Stream dest,
+ /// TODO: Determine why the exception can't just be thrown
+ public static bool ConvertFromDiscToStream(NintendoDisc source,
+ Stream dest,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
@@ -76,7 +88,7 @@ namespace SabreTools.Wrappers
return false;
}
- if (!isRvz && chunkSize != WiaConst.DefaultChunkSize)
+ if (!isRvz && chunkSize != DefaultChunkSize)
{
exception = new ArgumentException("WIA chunkSize must equal DefaultChunkSize");
return false;
@@ -90,8 +102,7 @@ namespace SabreTools.Wrappers
try
{
- return WriteWiaRvz(source, dest, isRvz, compressionType,
- Math.Max(1, Math.Min(22, compressionLevel)), chunkSize);
+ return WriteWiaRvz(source, dest, isRvz, compressionType, Math.Max(1, Math.Min(22, compressionLevel)), chunkSize);
}
catch (Exception ex)
{
@@ -100,118 +111,117 @@ namespace SabreTools.Wrappers
}
}
- ///
- public bool Write(string outputPath, bool includeDebug)
- {
- if (string.IsNullOrEmpty(outputPath))
- {
- string ext = IsRvz ? ".rvz" : ".wia";
- string outputFilename = Filename is null
- ? (Guid.NewGuid().ToString() + ext)
- : (Filename + ".new");
- outputPath = Path.GetFullPath(outputFilename);
- }
-
- if (Model?.Header1 is null || Model?.Header2 is null)
- {
- if (includeDebug) Console.WriteLine("Model was invalid, cannot write!");
- return false;
- }
-
- var writer = new Serialization.Writers.WIA { Debug = includeDebug };
- return writer.SerializeFile(Model, outputPath);
- }
-
- // -----------------------------------------------------------------------
- // Core pipeline
- // -----------------------------------------------------------------------
-
- private static bool WriteWiaRvz(NintendoDisc source, Stream dest,
- bool isRvz, WiaRvzCompressionType compressionType,
- int compressionLevel, uint chunkSize)
+ ///
+ /// Core pipeline for writing WIA and RVZ images
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool WriteWiaRvz(NintendoDisc source,
+ Stream dest,
+ bool isRvz,
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ uint chunkSize)
{
long isoSize = source.DataLength;
if (isoSize <= 0)
return false;
- byte[]? discHdr = source.ReadData(0, WiaConst.DiscHeaderStoredSize);
+ byte[]? discHdr = source.ReadData(0, DiscHeaderStoredSize);
if (discHdr is null)
return false;
Platform platform = DetectWiaPlatform(discHdr);
- if (platform == Platform.Unknown)
- return false;
+ return platform switch
+ {
+ Platform.GameCube => WriteGameCube(source, dest, isRvz, compressionType, compressionLevel, chunkSize, isoSize, discHdr),
+ Platform.Wii => WriteWii(source, dest, isRvz, compressionType, compressionLevel, chunkSize, isoSize, discHdr),
- if (platform == Platform.Wii)
- return WriteWii(source, dest, isRvz, compressionType, compressionLevel, chunkSize, isoSize, discHdr);
-
- return WriteGameCube(source, dest, isRvz, compressionType, compressionLevel, chunkSize, isoSize, discHdr);
+ // These should never happen
+ Platform.Unknown => false,
+ _ => false,
+ };
}
- // -----------------------------------------------------------------------
- // GameCube path
- // -----------------------------------------------------------------------
-
- private static bool WriteGameCube(NintendoDisc source, Stream dest,
- bool isRvz, WiaRvzCompressionType compressionType,
- int compressionLevel, uint chunkSize,
- long isoSize, byte[] discHdr)
+ ///
+ /// Write a GameCube WIA or RVZ image
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool WriteGameCube(NintendoDisc source,
+ Stream dest,
+ bool isRvz,
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ uint chunkSize,
+ long isoSize,
+ byte[] discHdr)
{
- const long rawDataStart = WiaConst.DiscHeaderStoredSize;
+ const long rawDataStart = DiscHeaderStoredSize;
long rawDataSize = isoSize - rawDataStart;
if (rawDataSize <= 0)
return false;
uint numGroups = (uint)((rawDataSize + chunkSize - 1) / chunkSize);
- int groupEntrySize = isRvz ? WiaConst.RvzGroupEntrySize : WiaConst.WiaGroupEntrySize;
+ int groupEntrySize = isRvz ? RvzGroupEntrySize : WiaGroupEntrySize;
- long headersBound = AlignWia(
- WiaConst.Header1Size + WiaConst.Header2Size +
- WiaConst.RawDataEntrySize + 0x100 +
- (numGroups * groupEntrySize),
- NdConstants.WiiBlockSize);
+ long headersBound = Align(
+ Header1Size + Header2Size + RawDataEntrySize + 0x100 + (numGroups * groupEntrySize),
+ WiiBlockSize);
dest.Write(new byte[headersBound], 0, (int)headersBound);
long bytesWritten = headersBound;
- var groupEntries = new WiaRvzGroupEntry[numGroups];
- var rawDedupMap = new Dictionary();
- GcFst? gcFst = isRvz ? BuildGcFst(source) : null;
+ var groupEntries = new RvzGroupEntry[numGroups];
+ var rawDedupMap = new Dictionary();
+ FileSystemTableReader? gcFst = isRvz ? BuildFileSystemTableReader(source) : null;
- WiaRvzCompressionHelper.GetCompressorData(compressionType, compressionLevel,
- out byte[] propData, out byte propSize);
+ WiaRvzCompressionHelper.GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
uint groupIdx = 0;
- long srcOff = rawDataStart;
+ long srcOff = rawDataStart;
long remaining = rawDataSize;
-int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
+ int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
while (remaining > 0)
{
- int thisBatch = (int)Math.Min(batchSize, (remaining + chunkSize - 1) / chunkSize);
- var work = new GcGroupWorkEntry[thisBatch];
+ int thisBatch = (int)Math.Min(batchSize, (remaining + chunkSize - 1) / chunkSize);
+ var work = new GcGroupWorkEntry[thisBatch];
int actualBatch = 0;
- for (int w = 0; w < thisBatch && remaining > 0; w++)
+ for (int i = 0; i < thisBatch && remaining > 0; i++)
{
int toRead = (int)Math.Min(chunkSize, remaining);
- byte[]? raw = source.ReadData(srcOff, toRead);
- if (raw is null) break;
+ byte[] raw = source.ReadData(srcOff, toRead);
+ if (raw.Length == 0)
+ break;
- var gi = work[w] = new GcGroupWorkEntry
+ var gi = work[i] = new GcGroupWorkEntry
{
- BytesRead = toRead,
+ BytesRead = toRead,
SourceOffset = srcOff,
};
- srcOff += toRead;
+ srcOff += toRead;
remaining -= toRead;
actualBatch++;
- gi.IsAllSame = IsAllSameWia(raw, toRead);
- gi.SameByte = raw[0];
+ gi.IsAllSame = IsAllSame(raw, toRead);
+ gi.SameByte = raw[0];
if (gi.IsAllSame)
{
@@ -229,10 +239,10 @@ int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
if (isRvz)
{
- byte[]? packed = RvzPackEncoder.Pack(raw, 0, toRead, srcOff - toRead,
- out gi.RvzPackedSize, gcFst);
+ byte[]? packed = RvzPackEncoder.Pack(raw, 0, toRead, srcOff - toRead, out gi.RvzPackedSize, gcFst);
gi.MainData = packed ?? raw;
- if (packed is null) gi.RvzPackedSize = 0;
+ if (packed is null)
+ gi.RvzPackedSize = 0;
}
else
{
@@ -253,16 +263,16 @@ int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
Parallel.For(0, actualBatch, w =>
{
var gi = work[w];
- if (gi.MainData != null && !gi.IsDedupHit)
+ if (gi.MainData is not null && !gi.IsDedupHit)
gi.CompressedData = WiaRvzCompressionHelper.Compress(ct, gi.MainData, 0, gi.MainData.Length, cl, pd, ps);
});
}
#endif
- for (int w = 0; w < actualBatch; w++)
+ for (int i = 0; i < actualBatch; i++)
{
- uint idx = groupIdx + (uint)w;
- var gi = work[w];
+ uint idx = groupIdx + (uint)i;
+ var gi = work[i];
if (gi.IsDedupHit)
{
@@ -273,20 +283,29 @@ int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
var dk = new WiaDedupKey2(0, gi.BytesRead);
if (!rawDedupMap.TryGetValue(dk, out var ze))
{
- ze = new WiaRvzGroupEntry((uint)(bytesWritten >> 2), 0, 0);
+ ze = new RvzGroupEntry
+ {
+ DataOffset = (uint)(bytesWritten >> 2),
+ DataSize = 0,
+ RvzPackedSize = 0
+ };
rawDedupMap[dk] = ze;
}
groupEntries[idx] = ze;
}
- else if (gi.MainData != null)
+ else if (gi.MainData is not null)
{
- uint groupOff = (uint)(bytesWritten >> 2);
- uint storedSz = WriteRawGroupData(dest, ref bytesWritten, gi,
- isRvz, compressionType, compressionLevel, propData, propSize);
- PadTo4Wia(dest, ref bytesWritten);
+ uint groupOff = (uint)(bytesWritten >> 2);
+ uint storedSz = WriteRawGroupData(dest, ref bytesWritten, gi, isRvz, compressionType);
+ PadToFourBytes(dest, ref bytesWritten);
- var entry = new WiaRvzGroupEntry(groupOff, storedSz, gi.RvzPackedSize);
+ var entry = new RvzGroupEntry
+ {
+ DataOffset = groupOff,
+ DataSize = storedSz,
+ RvzPackedSize = gi.RvzPackedSize
+ };
groupEntries[idx] = entry;
if (gi.IsAllSame && gi.SameByte != 0)
rawDedupMap[new WiaDedupKey2(gi.SameByte, gi.BytesRead)] = entry;
@@ -297,75 +316,100 @@ int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
}
// Write tables
- dest.Seek(WiaConst.Header1Size + WiaConst.Header2Size, SeekOrigin.Begin);
- long tablePos = WiaConst.Header1Size + WiaConst.Header2Size;
+ dest.Seek(Header1Size + Header2Size, SeekOrigin.Begin);
+ long tablePos = Header1Size + Header2Size;
ulong rawEntriesOffset = (ulong)tablePos;
- var rawEntry = new WiaRawDataEntry
+ var rawEntry = new RawDataEntry
{
- DataOffset = WiaConst.DiscHeaderStoredSize,
- DataSize = (ulong)rawDataSize,
- GroupIndex = 0,
+ DataOffset = DiscHeaderStoredSize,
+ DataSize = (ulong)rawDataSize,
+ GroupIndex = 0,
NumberOfGroups = numGroups,
};
- byte[] rawEntryBytes = SerializeRawDataEntry(rawEntry);
- byte[] rawEntryWritten = CompressTableDataWia(rawEntryBytes, compressionType, compressionLevel, propData, propSize);
+ byte[] rawEntryBytes = SerializeRawDataEntry(rawEntry);
+ byte[] rawEntryWritten = CompressTableDataWia(rawEntryBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(rawEntryWritten, 0, rawEntryWritten.Length);
tablePos += rawEntryWritten.Length;
- PadTableTo4Wia(dest, ref tablePos);
+ PadTableToFourBytes(dest, ref tablePos);
ulong groupEntriesOffset = (ulong)tablePos;
- byte[] groupEntryBytes = SerializeGroupEntries(groupEntries, numGroups, isRvz);
+ byte[] groupEntryBytes = SerializeGroupEntries(groupEntries, numGroups, isRvz);
byte[] groupEntryWritten = CompressTableDataWia(groupEntryBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(groupEntryWritten, 0, groupEntryWritten.Length);
tablePos += groupEntryWritten.Length;
-WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, compressionLevel, chunkSize,
- 0, (ulong)tablePos, new byte[20], // no partition entries
- 1u, rawEntriesOffset, (uint)rawEntryWritten.Length,
- numGroups, groupEntriesOffset, (uint)groupEntryWritten.Length,
- propData, propSize, isoSize, bytesWritten);
+ WriteWiaHeaders(dest,
+ discHdr,
+ isRvz,
+ WiaDiscType.GameCube,
+ compressionType,
+ compressionLevel,
+ chunkSize,
+ 0,
+ (ulong)tablePos,
+ new byte[20], // no partition entries
+ 1u,
+ rawEntriesOffset,
+ (uint)rawEntryWritten.Length,
+ numGroups,
+ groupEntriesOffset,
+ (uint)groupEntryWritten.Length,
+ propData,
+ propSize,
+ isoSize,
+ bytesWritten);
dest.Flush();
+
return true;
}
- // -----------------------------------------------------------------------
- // Wii path
- // -----------------------------------------------------------------------
-
- private static bool WriteWii(NintendoDisc source, Stream dest,
- bool isRvz, WiaRvzCompressionType compressionType,
- int compressionLevel, uint chunkSize,
- long isoSize, byte[] discHdr)
+ ///
+ /// Write a Wii WIA or RVZ image
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool WriteWii(NintendoDisc source,
+ Stream dest,
+ bool isRvz,
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ uint chunkSize,
+ long isoSize,
+ byte[] discHdr)
{
- var partitions = ReadWiiPartitions(source, isoSize);
- if (partitions is null) return false;
+ var partitions = ReadWiiPartitions(source);
+ if (partitions is null)
+ return false;
- var rawRegions = BuildRawRegions(source, partitions, isoSize);
+ var rawRegions = BuildRawRegions(partitions, isoSize);
- WiaRvzCompressionHelper.GetCompressorData(compressionType, compressionLevel,
- out byte[] propData, out byte propSize);
+ WiaRvzCompressionHelper.GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
- int groupEntrySize = isRvz ? WiaConst.RvzGroupEntrySize : WiaConst.WiaGroupEntrySize;
- uint totalGroups = CalcTotalGroups(partitions, rawRegions, chunkSize);
+ int groupEntrySize = isRvz ? RvzGroupEntrySize : WiaGroupEntrySize;
+ uint totalGroups = CalcTotalGroups(partitions, rawRegions, chunkSize);
- long headersBound = AlignWia(
- WiaConst.Header1Size + WiaConst.Header2Size +
- (partitions.Count * WiaConst.PartitionEntrySize) +
- (rawRegions.Count * WiaConst.RawDataEntrySize) + 0x100 +
- (totalGroups * groupEntrySize),
- NdConstants.WiiBlockSize);
+ long headersBound = Align(
+ Header1Size + Header2Size + (partitions.Count * PartitionEntrySize) + (rawRegions.Count * RawDataEntrySize) + 0x100 + (totalGroups * groupEntrySize),
+ WiiBlockSize);
dest.Write(new byte[headersBound], 0, (int)headersBound);
long bytesWritten = headersBound;
- var allGroups = new List();
+ var allGroups = new List();
uint currentGrpIdx = 0;
- uint lastValidOff = 0;
+ uint lastValidOff = 0;
- var dedupMap = new Dictionary();
- var decDedupMap = new Dictionary();
- var rawDedupMap = new Dictionary();
+ var dedupMap = new Dictionary();
+ var decDedupMap = new Dictionary();
+ var rawDedupMap = new Dictionary();
var wiaZeroDedup = new Dictionary();
var regions = BuildDiscRegions(partitions, rawRegions);
@@ -373,88 +417,150 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
{
if (region.IsPartition)
{
- ProcessWiiPartition(source, dest, region.PartitionInfo!,
- ref bytesWritten, allGroups, ref currentGrpIdx, ref lastValidOff,
- dedupMap, decDedupMap, wiaZeroDedup,
- isRvz, compressionType, compressionLevel, chunkSize, propData, propSize);
+ ProcessWiiPartition(source,
+ dest,
+ region.PartitionInfo!,
+ ref bytesWritten,
+ allGroups,
+ ref currentGrpIdx,
+ ref lastValidOff,
+ dedupMap,
+ decDedupMap,
+ wiaZeroDedup,
+ isRvz,
+ compressionType,
+ compressionLevel,
+ chunkSize,
+ propData,
+ propSize);
}
else
{
- ProcessRawRegion(source, dest, region.RawInfo!,
- ref bytesWritten, allGroups, ref currentGrpIdx, ref lastValidOff,
- rawDedupMap, isRvz, compressionType, compressionLevel, chunkSize, propData, propSize);
+ ProcessRawRegion(source,
+ dest,
+ region.RawInfo!,
+ ref bytesWritten,
+ allGroups,
+ ref currentGrpIdx,
+ ref lastValidOff,
+ rawDedupMap,
+ isRvz,
+ compressionType,
+ compressionLevel,
+ chunkSize,
+ propData,
+ propSize);
}
}
// Write tables
- dest.Seek(WiaConst.Header1Size + WiaConst.Header2Size, SeekOrigin.Begin);
- long tablePos = WiaConst.Header1Size + WiaConst.Header2Size;
+ dest.Seek(Header1Size + Header2Size, SeekOrigin.Begin);
+ long tablePos = Header1Size + Header2Size;
ulong partEntriesOffset = (ulong)tablePos;
byte[] partEntriesBytes = SerializePartitionEntries(dest, partitions);
tablePos += partEntriesBytes.Length;
- PadTableTo4Wia(dest, ref tablePos);
+ PadTableToFourBytes(dest, ref tablePos);
ulong rawEntriesOffset = (ulong)tablePos;
- byte[] rawEntryBytes = SerializeRawDataEntries(rawRegions);
+ byte[] rawEntryBytes = SerializeRawDataEntries(rawRegions);
byte[] rawEntryWritten = CompressTableDataWia(rawEntryBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(rawEntryWritten, 0, rawEntryWritten.Length);
tablePos += rawEntryWritten.Length;
- PadTableTo4Wia(dest, ref tablePos);
+ PadTableToFourBytes(dest, ref tablePos);
ulong groupEntriesOffset = (ulong)tablePos;
using (var gms = new MemoryStream())
{
foreach (var e in allGroups)
+ {
WriteGroupEntryWia(gms, e, isRvz);
- byte[] gBytes = gms.ToArray();
+ }
+
+ byte[] gBytes = gms.ToArray();
byte[] gWritten = CompressTableDataWia(gBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(gWritten, 0, gWritten.Length);
tablePos += gWritten.Length;
byte[] partHashData = ComputeSha1Wia(partEntriesBytes, 0, partEntriesBytes.Length);
- WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.Wii, compressionType, compressionLevel, chunkSize,
- (uint)partitions.Count, partEntriesOffset, partHashData,
- (uint)rawRegions.Count, rawEntriesOffset, (uint)rawEntryWritten.Length,
- (uint)allGroups.Count, groupEntriesOffset, (uint)gWritten.Length,
- propData, propSize, isoSize, bytesWritten);
+ WriteWiaHeaders(dest,
+ discHdr,
+ isRvz,
+ WiaDiscType.Wii,
+ compressionType,
+ compressionLevel,
+ chunkSize,
+ (uint)partitions.Count, partEntriesOffset,
+ partHashData,
+ (uint)rawRegions.Count,
+ rawEntriesOffset,
+ (uint)rawEntryWritten.Length,
+ (uint)allGroups.Count,
+ groupEntriesOffset,
+ (uint)gWritten.Length,
+ propData,
+ propSize,
+ isoSize,
+ bytesWritten);
}
dest.Flush();
return true;
}
- // -----------------------------------------------------------------------
- // Wii partition processing
- // -----------------------------------------------------------------------
-
- private static void ProcessWiiPartition(NintendoDisc source, Stream dest,
- WiiPartInfo part, ref long bytesWritten,
- List groupEntries, ref uint currentGrpIdx,
+ ///
+ /// Wii partition processing
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void ProcessWiiPartition(NintendoDisc source,
+ Stream dest,
+ WiiPartInfo part,
+ ref long bytesWritten,
+ List groupEntries,
+ ref uint currentGrpIdx,
ref uint lastValidOff,
Dictionary dedupMap,
Dictionary decDedupMap,
Dictionary wiaZeroDedup,
- bool isRvz, WiaRvzCompressionType compressionType,
- int compressionLevel, uint chunkSize,
- byte[] propData, byte propSize)
+ bool isRvz,
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ uint chunkSize,
+ byte[] propData,
+ byte propSize)
{
- long remaining = (long)part.DataSize;
- long srcOff = (long)part.DataStart;
+ long remaining = (long)part.DataSize;
+ long srcOff = (long)part.DataStart;
ulong partKeyHash = BitConverter.ToUInt64(part.TitleKey, 0)
^ BitConverter.ToUInt64(part.TitleKey, 8);
part.FirstGroupIndex = currentGrpIdx;
- int blocksPerChunk = (int)chunkSize / NdConstants.WiiBlockSize;
- int chunksPerGroup = NdConstants.WiiBlocksPerGroup / blocksPerChunk;
- int wiiGroupSize = NdConstants.WiiGroupSize;
+ int blocksPerChunk = (int)chunkSize / WiiBlockSize;
+ int chunksPerGroup = WiiBlocksPerGroup / blocksPerChunk;
+ int wiiGroupSize = WiiGroupSize;
int outerBatch = (chunksPerGroup == 1)
- ? Math.Max(Environment.ProcessorCount * 2, 16) : 1;
+ ? Math.Max(Environment.ProcessorCount * 2, 16)
+ : 1;
var batchItems = new WiiBatchItem[outerBatch];
- var flatWork = new List(outerBatch);
+ var flatWork = new List(outerBatch);
long regionDecOff = 0;
while (remaining > 0)
@@ -474,98 +580,102 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
SrcOffset = srcOff,
};
- srcOff += toRead;
+ srcOff += toRead;
remaining -= toRead;
actualBatch++;
- bool encAllSame = (chunksPerGroup == 1) && IsAllSameWia(encGroup, toRead);
+ bool encAllSame = (chunksPerGroup == 1) && IsAllSame(encGroup, toRead);
item.EncAllSame = encAllSame;
- item.DedupKey = new WiaDedupKey3(partKeyHash, encGroup[0], toRead);
+ item.DedupKey = new WiaDedupKey3(partKeyHash, encGroup[0], toRead);
if (encAllSame && dedupMap.TryGetValue(item.DedupKey, out var reused))
{
item.IsInterDedupHit = true;
- item.DedupResult = reused;
- regionDecOff += (long)(toRead / NdConstants.WiiBlockSize) * NdConstants.WiiBlockDataSize;
+ item.DedupResult = reused;
+ regionDecOff += (long)(toRead / WiiBlockSize) * WiiBlockDataSize;
continue;
}
- int numBlocks = toRead / NdConstants.WiiBlockSize;
+ int numBlocks = toRead / WiiBlockSize;
item.NumChunks = (numBlocks + blocksPerChunk - 1) / blocksPerChunk;
item.DecryptedAll = DecryptWiiGroup(encGroup, toRead, part.TitleKey);
- item.AllExceptions = GenerateHashExceptions(encGroup, toRead,
- item.DecryptedAll, part.TitleKey, numBlocks);
+ item.AllExceptions = GenerateHashExceptions(encGroup, item.DecryptedAll, part.TitleKey, numBlocks);
item.PartWork = new WiiChunkWork[item.NumChunks];
for (int c = 0; c < item.NumChunks; c++)
{
int cBlockStart = c * blocksPerChunk;
- int cBlockEnd = Math.Min(cBlockStart + blocksPerChunk, numBlocks);
+ int cBlockEnd = Math.Min(cBlockStart + blocksPerChunk, numBlocks);
int actualBlocks = cBlockEnd - cBlockStart;
- int decOff = cBlockStart * NdConstants.WiiBlockDataSize;
- int decLen = actualBlocks * NdConstants.WiiBlockDataSize;
+ int decOff = cBlockStart * WiiBlockDataSize;
+ int decLen = actualBlocks * WiiBlockDataSize;
byte[] procData = new byte[decLen];
- if (item.DecryptedAll != null && decLen > 0)
+ if (item.DecryptedAll is not null && decLen > 0)
Array.Copy(item.DecryptedAll, decOff, procData, 0, decLen);
var chunkEx = new List();
- if (item.AllExceptions != null)
+ if (item.AllExceptions is not null)
{
foreach (var ex in item.AllExceptions)
{
- int exBlock = ex.Offset / NdConstants.WiiBlockHeaderSize;
+ int exBlock = ex.Offset / WiiBlockHeaderSize;
if (exBlock >= cBlockStart && exBlock < cBlockEnd)
{
int localBlock = exBlock - cBlockStart;
- ushort localOff = (ushort)((localBlock * NdConstants.WiiBlockHeaderSize)
- + (ex.Offset % NdConstants.WiiBlockHeaderSize));
+ ushort localOff = (ushort)((localBlock * WiiBlockHeaderSize) + (ex.Offset % WiiBlockHeaderSize));
chunkEx.Add(new HashExceptionEntry { Offset = localOff, Hash = ex.Hash });
}
}
}
- bool isAllZeros = !isRvz && chunksPerGroup == 1
- && chunkEx.Count == 0 && procData.Length > 0
- && IsAllSameWia(procData, procData.Length) && procData[0] == 0;
+ bool isAllZeros = !isRvz
+ && chunksPerGroup == 1
+ && chunkEx.Count == 0
+ && procData.Length > 0
+ && IsAllSame(procData, procData.Length)
+ && procData[0] == 0;
- bool decAllSame = !isRvz && !isAllZeros && chunksPerGroup == 1
- && chunkEx.Count == 0 && procData.Length > 0
- && IsAllSameWia(procData, procData.Length);
+ bool decAllSame = !isRvz
+ && !isAllZeros
+ && chunksPerGroup == 1
+ && chunkEx.Count == 0
+ && procData.Length > 0
+ && IsAllSame(procData, procData.Length);
- var decDedupKey = new WiaDedupKey3(partKeyHash,
- procData.Length > 0 ? procData[0] : (byte)0, procData.Length);
+ var decDedupKey = new WiaDedupKey3(partKeyHash, procData.Length > 0 ? procData[0] : (byte)0, procData.Length);
var pw = item.PartWork[c] = new WiiChunkWork
{
- IsAllZeros = isAllZeros,
- DecAllSame = decAllSame,
+ IsAllZeros = isAllZeros,
+ DecAllSame = decAllSame,
DecDedupKey = decDedupKey,
};
- if (isAllZeros) continue;
+ if (isAllZeros)
+ continue;
if (decAllSame && decDedupMap.TryGetValue(decDedupKey, out var decReused))
{
- pw.IsDecDedupHit = true;
- pw.DecDedupOffset = decReused.Offset;
+ pw.IsDecDedupHit = true;
+ pw.DecDedupOffset = decReused.Offset;
pw.DecDedupDataSize = decReused.DataSize;
continue;
}
byte[] exListBytes = BuildExceptionList(chunkEx);
- int unpaddedExLen = 2 + (chunkEx.Count * 22);
+ int unpaddedExLen = 2 + (chunkEx.Count * 22);
byte[] mainData;
uint rvzPackedSize = 0;
if (isRvz)
{
- long baseDecOff = regionDecOff + ((long)c * (blocksPerChunk * NdConstants.WiiBlockDataSize));
- byte[]? packed = RvzPackEncoder.Pack(procData, 0, procData.Length,
- baseDecOff, out rvzPackedSize);
+ long baseDecOff = regionDecOff + ((long)c * (blocksPerChunk * WiiBlockDataSize));
+ byte[]? packed = RvzPackEncoder.Pack(procData, 0, procData.Length, baseDecOff, out rvzPackedSize);
mainData = packed ?? procData;
- if (packed is null) rvzPackedSize = 0;
+ if (packed is null)
+ rvzPackedSize = 0;
}
else
{
@@ -573,24 +683,24 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
}
pw.ExceptionListBytes = exListBytes;
- pw.UnpaddedExLen = unpaddedExLen;
- pw.MainDataBytes = mainData;
- pw.RvzPackedSize = rvzPackedSize;
+ pw.UnpaddedExLen = unpaddedExLen;
+ pw.MainDataBytes = mainData;
+ pw.RvzPackedSize = rvzPackedSize;
if (compressionType != WiaRvzCompressionType.None)
flatWork.Add(new WiaFlatWorkItem(b, c));
}
- regionDecOff += (long)numBlocks * NdConstants.WiiBlockDataSize;
+ regionDecOff += (long)numBlocks * WiiBlockDataSize;
}
- if (actualBatch == 0) break;
+ if (actualBatch == 0)
+ break;
- #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
+#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
// Phase 2: compress
if (flatWork.Count > 0)
{
- WiaRvzCompressionType ct = compressionType;
int cl = compressionLevel;
byte[] pd = propData;
byte ps = propSize;
@@ -598,14 +708,18 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
{
var fw = flatWork[idx];
var pw = batchItems[fw.BatchIndex].PartWork![fw.ChunkIndex];
- if (ct > WiaRvzCompressionType.Purge)
+ if (compressionType > WiaRvzCompressionType.Purge)
{
- byte[] toCompress = ConcatBytesWia(
- pw.ExceptionListBytes, 0, pw.UnpaddedExLen,
- pw.MainDataBytes, 0, pw.MainDataBytes.Length);
- pw.CompressedData = WiaRvzCompressionHelper.Compress(ct, toCompress, 0, toCompress.Length, cl, pd, ps);
+ byte[] toCompress = ConcatBytes(
+ pw.ExceptionListBytes,
+ 0,
+ pw.UnpaddedExLen,
+ pw.MainDataBytes,
+ 0,
+ pw.MainDataBytes.Length);
+ pw.CompressedData = WiaRvzCompressionHelper.Compress(compressionType, toCompress, 0, toCompress.Length, cl, pd, ps);
}
- else if (ct == WiaRvzCompressionType.Purge)
+ else if (compressionType == WiaRvzCompressionType.Purge)
{
pw.CompressedData = PurgeCompressor.Compress(pw.MainDataBytes, 0, pw.MainDataBytes.Length, pw.ExceptionListBytes);
}
@@ -621,10 +735,12 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
if (item.IsInterDedupHit)
{
lastValidOff = item.DedupResult.Offset;
- groupEntries.Add(new WiaRvzGroupEntry(
- item.DedupResult.Offset,
- item.DedupResult.DataSize,
- 0));
+ groupEntries.Add(new RvzGroupEntry
+ {
+ DataOffset = item.DedupResult.Offset,
+ DataSize = item.DedupResult.DataSize,
+ RvzPackedSize = 0
+ });
currentGrpIdx++;
continue;
}
@@ -642,30 +758,41 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
wiaZeroDedup[partKeyHash] = firstOff;
}
- groupEntries.Add(new WiaRvzGroupEntry(firstOff, 0, 0));
+ groupEntries.Add(new RvzGroupEntry
+ {
+ DataOffset = firstOff,
+ DataSize = 0,
+ RvzPackedSize = 0,
+ });
}
else if (pw.IsDecDedupHit)
{
- groupEntries.Add(new WiaRvzGroupEntry(
- pw.DecDedupOffset,
- pw.DecDedupDataSize,
- 0));
+ groupEntries.Add(new RvzGroupEntry
+ {
+ DataOffset = pw.DecDedupOffset,
+ DataSize = pw.DecDedupDataSize,
+ RvzPackedSize = 0,
+ });
}
else
{
- uint groupOff = (uint)(bytesWritten >> 2);
- lastValidOff = groupOff;
- uint storedSz = WriteWiiChunkData(dest, ref bytesWritten, pw, isRvz, compressionType);
+ uint groupOff = (uint)(bytesWritten >> 2);
+ lastValidOff = groupOff;
+ uint storedSz = WriteWiiChunkData(dest, ref bytesWritten, pw, isRvz, compressionType);
- groupEntries.Add(new WiaRvzGroupEntry(
- groupOff, storedSz, pw.RvzPackedSize));
+ groupEntries.Add(new RvzGroupEntry
+ {
+ DataOffset = groupOff,
+ DataSize = storedSz,
+ RvzPackedSize = pw.RvzPackedSize,
+ });
if (item.EncAllSame && c == 0)
dedupMap[item.DedupKey] = new WiaDedup2(groupOff, storedSz);
if (pw.DecAllSame && c == 0)
decDedupMap[pw.DecDedupKey] = new WiaDedup2(groupOff, storedSz);
- PadTo4Wia(dest, ref bytesWritten);
+ PadToFourBytes(dest, ref bytesWritten);
}
}
@@ -676,10 +803,22 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
part.NumberOfGroups = currentGrpIdx - part.FirstGroupIndex;
}
- private static uint WriteWiiChunkData(Stream dest, ref long bytesWritten,
- WiiChunkWork pw, bool isRvz, WiaRvzCompressionType compressionType)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static uint WriteWiiChunkData(Stream dest,
+ ref long bytesWritten,
+ WiiChunkWork pw,
+ bool isRvz,
+ WiaRvzCompressionType compressionType)
{
- if (pw.CompressedData != null)
+ if (pw.CompressedData is not null)
{
bool useC = !isRvz || pw.CompressedData.Length < pw.MainDataBytes.Length;
if (useC && compressionType > WiaRvzCompressionType.Purge)
@@ -708,34 +847,54 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
return (uint)(pw.ExceptionListBytes.Length + pw.MainDataBytes.Length);
}
- // -----------------------------------------------------------------------
- // Raw region processing
- // -----------------------------------------------------------------------
-
- private static void ProcessRawRegion(NintendoDisc source, Stream dest,
- RawRegionInfo raw, ref long bytesWritten,
- List groupEntries, ref uint currentGrpIdx,
+ ///
+ /// Raw region processing
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void ProcessRawRegion(NintendoDisc source,
+ Stream dest,
+ RawDataEntry raw,
+ ref long bytesWritten,
+ List groupEntries,
+ ref uint currentGrpIdx,
ref uint lastValidOff,
- Dictionary rawDedupMap,
- bool isRvz, WiaRvzCompressionType compressionType,
- int compressionLevel, uint chunkSize,
- byte[] propData, byte propSize)
+ Dictionary rawDedupMap,
+ bool isRvz,
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ uint chunkSize,
+ byte[] propData,
+ byte propSize)
{
- raw.FirstGroupIndex = currentGrpIdx;
+ raw.GroupIndex = currentGrpIdx;
- long skip = (long)raw.Offset % NdConstants.WiiBlockSize;
- long adjOffset = (long)raw.Offset - skip;
- long remaining = (long)raw.Size + skip;
- long srcOff = adjOffset;
+ long skip = (long)raw.DataOffset % WiiBlockSize;
+ long adjOffset = (long)raw.DataOffset - skip;
+ long remaining = (long)raw.DataSize + skip;
+ long srcOff = adjOffset;
while (remaining > 0)
{
int toRead = (int)Math.Min(chunkSize, remaining);
byte[]? data = source.ReadData(srcOff, toRead);
- if (data is null) break;
+ if (data is null)
+ break;
- bool isAllSame = IsAllSameWia(data, toRead);
- byte sameByte = data[0];
+ bool isAllSame = IsAllSame(data, toRead);
+ byte sameByte = data[0];
if (isAllSame)
{
@@ -744,18 +903,23 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
{
groupEntries.Add(cached);
currentGrpIdx++;
- srcOff += toRead;
+ srcOff += toRead;
remaining -= toRead;
continue;
}
if (sameByte == 0)
{
- var ze = new WiaRvzGroupEntry((uint)(bytesWritten >> 2), 0, 0);
+ var ze = new RvzGroupEntry
+ {
+ DataOffset = (uint)(bytesWritten >> 2),
+ DataSize = 0,
+ RvzPackedSize = 0,
+ };
rawDedupMap[dk] = ze;
groupEntries.Add(ze);
currentGrpIdx++;
- srcOff += toRead;
+ srcOff += toRead;
remaining -= toRead;
continue;
}
@@ -767,7 +931,8 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
{
byte[]? packed = RvzPackEncoder.Pack(data, 0, toRead, srcOff, out rvzPackedSize);
mainData = packed ?? data;
- if (packed is null) rvzPackedSize = 0;
+ if (packed is null)
+ rvzPackedSize = 0;
}
else
{
@@ -777,8 +942,7 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
byte[]? compressed = null;
if (compressionType > WiaRvzCompressionType.Purge)
{
- byte[] c2 = WiaRvzCompressionHelper.Compress(compressionType, mainData, 0,
- mainData.Length, compressionLevel, propData, propSize);
+ byte[] c2 = WiaRvzCompressionHelper.Compress(compressionType, mainData, 0, mainData.Length, compressionLevel, propData, propSize);
if (!isRvz || c2.Length < mainData.Length)
compressed = c2;
}
@@ -788,10 +952,10 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
}
uint groupOff = (uint)(bytesWritten >> 2);
- lastValidOff = groupOff;
+ lastValidOff = groupOff;
uint storedSz;
- if (compressed != null)
+ if (compressed is not null)
{
bool useC = !isRvz || compressed.Length < mainData.Length;
if (useC)
@@ -816,74 +980,96 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
storedSz = (uint)mainData.Length;
}
- PadTo4Wia(dest, ref bytesWritten);
+ PadToFourBytes(dest, ref bytesWritten);
- var entry = new WiaRvzGroupEntry(groupOff, storedSz, rvzPackedSize);
+ var entry = new RvzGroupEntry
+ {
+ DataOffset = groupOff,
+ DataSize = storedSz,
+ RvzPackedSize = rvzPackedSize,
+ };
groupEntries.Add(entry);
if (isAllSame && sameByte != 0)
rawDedupMap[new WiaDedupKey2(sameByte, toRead)] = entry;
currentGrpIdx++;
- srcOff += toRead;
+ srcOff += toRead;
remaining -= toRead;
}
- raw.NumberOfGroups = currentGrpIdx - raw.FirstGroupIndex;
+ raw.NumberOfGroups = currentGrpIdx - raw.GroupIndex;
}
- // -----------------------------------------------------------------------
- // Wii crypto helpers
- // -----------------------------------------------------------------------
+ #region Wii Crypto Helpers
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
private static byte[]? DecryptWiiGroup(byte[] encGroup, int bytesRead, byte[] titleKey)
{
- int numBlocks = bytesRead / NdConstants.WiiBlockSize;
- var result = new byte[numBlocks * NdConstants.WiiBlockDataSize];
+ int numBlocks = bytesRead / WiiBlockSize;
+ var result = new byte[numBlocks * WiiBlockDataSize];
for (int i = 0; i < numBlocks; i++)
{
- int off = i * NdConstants.WiiBlockSize;
+ int off = i * WiiBlockSize;
byte[] iv = new byte[16];
Array.Copy(encGroup, off + 0x3D0, iv, 0, 16);
- byte[] encData = new byte[NdConstants.WiiBlockDataSize];
- Array.Copy(encGroup, off + NdConstants.WiiBlockHeaderSize, encData, 0, NdConstants.WiiBlockDataSize);
+ byte[] encData = new byte[WiiBlockDataSize];
+ Array.Copy(encGroup, off + WiiBlockHeaderSize, encData, 0, WiiBlockDataSize);
byte[]? dec = NintendoDisc.DecryptBlock(encData, titleKey, iv);
- if (dec is null) return null;
+ if (dec is null)
+ return null;
- Array.Copy(dec, 0, result, i * NdConstants.WiiBlockDataSize, NdConstants.WiiBlockDataSize);
+ Array.Copy(dec, 0, result, i * WiiBlockDataSize, WiiBlockDataSize);
}
return result;
}
- private static List GenerateHashExceptions(
- byte[] encGroup, int bytesRead, byte[]? decryptedData, byte[] titleKey, int numBlocks)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static List GenerateHashExceptions(byte[] encGroup,
+ byte[]? decryptedData,
+ byte[] titleKey,
+ int numBlocks)
{
var exceptions = new List();
- if (decryptedData is null) return exceptions;
+ if (decryptedData is null)
+ return exceptions;
// Re-encrypt the decrypted data to obtain recomputed hashes
byte[] reEncGroup = EncryptWiiGroup(decryptedData, titleKey, numBlocks);
for (int blockIdx = 0; blockIdx < numBlocks; blockIdx++)
{
- int blockOff = blockIdx * NdConstants.WiiBlockSize;
+ int blockOff = blockIdx * WiiBlockSize;
- byte[] encHashBlock = new byte[NdConstants.WiiBlockHeaderSize];
- Array.Copy(encGroup, blockOff, encHashBlock, 0, NdConstants.WiiBlockHeaderSize);
+ byte[] encHashBlock = new byte[WiiBlockHeaderSize];
+ Array.Copy(encGroup, blockOff, encHashBlock, 0, WiiBlockHeaderSize);
- byte[] origHash = AesCbc.Decrypt(encHashBlock, titleKey, new byte[16]) ?? new byte[NdConstants.WiiBlockHeaderSize];
+ byte[] origHash = AesCbc.Decrypt(encHashBlock, titleKey, new byte[16]) ?? new byte[WiiBlockHeaderSize];
- byte[] reEncHashBlock = new byte[NdConstants.WiiBlockHeaderSize];
- Array.Copy(reEncGroup, blockOff, reEncHashBlock, 0, NdConstants.WiiBlockHeaderSize);
- byte[] recompHash = AesCbc.Decrypt(reEncHashBlock, titleKey, new byte[16]) ?? new byte[NdConstants.WiiBlockHeaderSize];
+ byte[] reEncHashBlock = new byte[WiiBlockHeaderSize];
+ Array.Copy(reEncGroup, blockOff, reEncHashBlock, 0, WiiBlockHeaderSize);
+ byte[] recompHash = AesCbc.Decrypt(reEncHashBlock, titleKey, new byte[16]) ?? new byte[WiiBlockHeaderSize];
- for (int off = 0; off < NdConstants.WiiBlockHeaderSize; off += 20)
+ for (int off = 0; off < WiiBlockHeaderSize; off += 20)
{
bool match = true;
- for (int j = 0; j < 20 && (off + j) < NdConstants.WiiBlockHeaderSize; j++)
+ for (int j = 0; j < 20 && (off + j) < WiiBlockHeaderSize; j++)
{
if (origHash[off + j] != recompHash[off + j])
{
@@ -895,11 +1081,11 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
if (!match)
{
byte[] hash = new byte[20];
- Array.Copy(origHash, off, hash, 0, Math.Min(20, NdConstants.WiiBlockHeaderSize - off));
+ Array.Copy(origHash, off, hash, 0, Math.Min(20, WiiBlockHeaderSize - off));
exceptions.Add(new HashExceptionEntry
{
- Offset = (ushort)((blockIdx * NdConstants.WiiBlockHeaderSize) + off),
- Hash = hash,
+ Offset = (ushort)((blockIdx * WiiBlockHeaderSize) + off),
+ Hash = hash,
});
}
}
@@ -908,34 +1094,44 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
return exceptions;
}
- private static List? ReadWiiPartitions(NintendoDisc source, long isoSize)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static List? ReadWiiPartitions(NintendoDisc source)
{
var result = new List();
- for (int group = 0; group < NdConstants.WiiPartitionGroupCount; group++)
+ for (int group = 0; group < WiiPartitionGroupCount; group++)
{
- byte[]? gEntry = source.ReadData(NdConstants.WiiPartitionTableAddress + (group * 8), 8);
- if (gEntry is null) continue;
+ byte[]? gEntry = source.ReadData(WiiPartitionTableAddress + (group * 8), 8);
+ if (gEntry is null)
+ continue;
int countPos = 0, offsetPos = 4;
- uint count = gEntry.ReadUInt32BigEndian(ref countPos);
+ uint count = gEntry.ReadUInt32BigEndian(ref countPos);
uint offset = gEntry.ReadUInt32BigEndian(ref offsetPos) << 2;
- if (count == 0 || offset == 0) continue;
+ if (count == 0 || offset == 0)
+ continue;
- for (int i = 0; i < (int)count; i++)
+ for (int i = 0; i < count; i++)
{
byte[]? pEntry = source.ReadData(offset + (i * 8), 8);
- if (pEntry is null) continue;
+ if (pEntry is null)
+ continue;
int partOffPos = 0;
long partOff = (long)pEntry.ReadUInt32BigEndian(ref partOffPos) << 2;
byte[]? sigType = source.ReadData(partOff, 4);
int sigTypePos = 0;
- if (sigType is null || sigType.ReadUInt32BigEndian(ref sigTypePos) != 0x10001U) continue;
+ if (sigType is null || sigType.ReadUInt32BigEndian(ref sigTypePos) != 0x10001U)
+ continue;
byte[]? hdr = source.ReadData(partOff, 0x2C0);
- if (hdr is null) continue;
+ if (hdr is null)
+ continue;
byte[] encKey = new byte[16];
Array.Copy(hdr, 0x1BF, encKey, 0, 16);
@@ -944,20 +1140,21 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
byte ckIdx = hdr[0x1F1];
byte[]? titleKey = NintendoDisc.DecryptTitleKey(encKey, titleId, ckIdx);
- if (titleKey is null) continue;
+ if (titleKey is null)
+ continue;
int dataOffPos = 0x2B8, dataSzPos = 0x2BC;
- ulong dataOff = (ulong)hdr.ReadUInt32BigEndian(ref dataOffPos) << 2;
+ ulong dataOff = (ulong)hdr.ReadUInt32BigEndian(ref dataOffPos) << 2;
ulong dataSize = (ulong)hdr.ReadUInt32BigEndian(ref dataSzPos) << 2;
result.Add(new WiiPartInfo
{
PartitionOffset = (ulong)partOff,
- TitleKey = titleKey,
- DataOffset = dataOff,
- DataSize = dataSize,
- DataStart = (ulong)partOff + dataOff,
- DataEnd = (ulong)partOff + dataOff + dataSize,
+ TitleKey = titleKey,
+ DataOffset = dataOff,
+ DataSize = dataSize,
+ DataStart = (ulong)partOff + dataOff,
+ DataEnd = (ulong)partOff + dataOff + dataSize,
});
}
}
@@ -965,145 +1162,212 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
return result.Count == 0 ? null : result;
}
- private static List BuildRawRegions(NintendoDisc source,
- List partitions, long isoSize)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static List BuildRawRegions(List partitions, long isoSize)
{
- var regions = new List();
+ var regions = new List();
partitions.Sort((a, b) => a.PartitionOffset.CompareTo(b.PartitionOffset));
- ulong cur = WiaConst.DiscHeaderStoredSize;
+ ulong cur = DiscHeaderStoredSize;
foreach (var p in partitions)
{
if (cur < p.PartitionOffset)
- regions.Add(new RawRegionInfo { Offset = cur, Size = p.PartitionOffset - cur });
- regions.Add(new RawRegionInfo { Offset = p.PartitionOffset, Size = p.DataOffset });
+ regions.Add(new RawDataEntry { DataOffset = cur, DataSize = p.PartitionOffset - cur });
+
+ regions.Add(new RawDataEntry { DataOffset = p.PartitionOffset, DataSize = p.DataOffset });
cur = p.DataEnd;
}
if (cur < (ulong)isoSize)
- regions.Add(new RawRegionInfo { Offset = cur, Size = (ulong)isoSize - cur });
+ regions.Add(new RawDataEntry { DataOffset = cur, DataSize = (ulong)isoSize - cur });
return regions;
}
- private static uint CalcTotalGroups(List partitions,
- List rawRegions, uint chunkSize)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static uint CalcTotalGroups(List partitions, List rawRegions, uint chunkSize)
{
uint total = 0;
foreach (var p in partitions)
+ {
total += (uint)((p.DataSize + chunkSize - 1) / chunkSize);
+ }
+
foreach (var r in rawRegions)
- total += (uint)((r.Size + chunkSize - 1) / chunkSize);
+ {
+ total += (uint)((r.DataSize + chunkSize - 1) / chunkSize);
+ }
+
return total;
}
- private static List BuildDiscRegions(List partitions,
- List rawRegions)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static List BuildDiscRegions(List partitions, List rawRegions)
{
var result = new List();
foreach (var p in partitions)
+ {
result.Add(new DiscRegionEntry { IsPartition = true, Offset = (long)p.DataStart, PartitionInfo = p });
+ }
+
foreach (var r in rawRegions)
- result.Add(new DiscRegionEntry { IsPartition = false, Offset = (long)r.Offset, RawInfo = r });
+ {
+ result.Add(new DiscRegionEntry { IsPartition = false, Offset = (long)r.DataOffset, RawInfo = r });
+ }
+
result.Sort((a, b) => a.Offset.CompareTo(b.Offset));
return result;
}
- // -----------------------------------------------------------------------
- // GcFst helper
- // -----------------------------------------------------------------------
+ #endregion
- private static GcFst? BuildGcFst(NintendoDisc source)
+ ///
+ /// GcFst helper
+ ///
+ ///
+ ///
+ private static FileSystemTableReader? BuildFileSystemTableReader(NintendoDisc source)
{
byte[]? hdr = source.ReadData(0x420, 12);
- if (hdr is null) return null;
+ if (hdr is null)
+ return null;
int fstOffPos = 4, fstSzPos = 8;
- uint fstOff = hdr.ReadUInt32BigEndian(ref fstOffPos);
+ uint fstOff = hdr.ReadUInt32BigEndian(ref fstOffPos);
uint fstSize = hdr.ReadUInt32BigEndian(ref fstSzPos);
- if (fstOff == 0 || fstSize == 0) return null;
+ if (fstOff == 0 || fstSize == 0)
+ return null;
byte[]? fstData = source.ReadData(fstOff, (int)fstSize);
- if (fstData is null) return null;
+ if (fstData is null)
+ return null;
- return GcFst.TryParse(fstData, offsetShift: 0);
+ return FileSystemTableReader.TryParse(fstData, offsetShift: 0);
}
- // -----------------------------------------------------------------------
- // Serialisation
- // -----------------------------------------------------------------------
+ #region Serialization - TODO: MOVE TO WRITER
- private static byte[] SerializeRawDataEntry(WiaRawDataEntry e)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] SerializeRawDataEntry(RawDataEntry obj)
{
using var ms = new MemoryStream();
- ms.WriteBigEndian(e.DataOffset);
- ms.WriteBigEndian(e.DataSize);
- ms.WriteBigEndian(e.GroupIndex);
- ms.WriteBigEndian(e.NumberOfGroups);
+ Serialization.Writers.WIA.WriteRawDataEntry(ms, obj);
return ms.ToArray();
}
- private static byte[] SerializeGroupEntries(WiaRvzGroupEntry[] entries, uint count, bool isRvz)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] SerializeGroupEntries(RvzGroupEntry[] entries, uint count, bool isRvz)
{
using var ms = new MemoryStream();
- for (uint i = 0; i < count && i < (uint)entries.Length; i++)
- WriteGroupEntryWia(ms, entries[i], isRvz);
+ for (int i = 0; i < count; i++)
+ {
+ var entry = entries[i];
+ WriteGroupEntryWia(ms, entry, isRvz);
+ }
+
return ms.ToArray();
}
- private static void WriteGroupEntryWia(Stream s, WiaRvzGroupEntry e, bool isRvz)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void WriteGroupEntryWia(Stream stream, RvzGroupEntry obj, bool isRvz)
{
- s.WriteBigEndian(e.DataOffset);
- s.WriteBigEndian(e.DataSize);
- if (isRvz) s.WriteBigEndian(e.RvzPackedSize);
+ if (isRvz)
+ {
+ Serialization.Writers.WIA.WriteRvzGroupEntry(stream, obj);
+ }
+ else
+ {
+ var wiaEntry = new WiaGroupEntry { DataOffset = obj.DataOffset, DataSize = obj.DataSize };
+ Serialization.Writers.WIA.WriteWiaGroupEntry(stream, wiaEntry);
+ }
}
- private static byte[] SerializePartitionEntries(Stream dest, List partitions)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] SerializePartitionEntries(Stream stream, List partitions)
{
using var ms = new MemoryStream();
- foreach (var p in partitions)
+ foreach (var partition in partitions)
{
// Write 16-byte key
- ms.Write(p.TitleKey, 0, 16);
- dest.Write(p.TitleKey, 0, 16);
+ ms.Write(partition.TitleKey, 0, 16);
+ stream.Write(partition.TitleKey, 0, 16);
// DataEntry0: all of the partition
- ms.WriteBigEndian((uint)(p.DataStart / NdConstants.WiiBlockSize));
- ms.WriteBigEndian((uint)(p.DataSize / NdConstants.WiiBlockSize));
- ms.WriteBigEndian(p.FirstGroupIndex);
- ms.WriteBigEndian(p.NumberOfGroups);
- dest.WriteBigEndian((uint)(p.DataStart / NdConstants.WiiBlockSize));
- dest.WriteBigEndian((uint)(p.DataSize / NdConstants.WiiBlockSize));
- dest.WriteBigEndian(p.FirstGroupIndex);
- dest.WriteBigEndian(p.NumberOfGroups);
+ ms.WriteBigEndian((uint)(partition.DataStart / WiiBlockSize));
+ ms.WriteBigEndian((uint)(partition.DataSize / WiiBlockSize));
+ ms.WriteBigEndian(partition.FirstGroupIndex);
+ ms.WriteBigEndian(partition.NumberOfGroups);
+ stream.WriteBigEndian((uint)(partition.DataStart / WiiBlockSize));
+ stream.WriteBigEndian((uint)(partition.DataSize / WiiBlockSize));
+ stream.WriteBigEndian(partition.FirstGroupIndex);
+ stream.WriteBigEndian(partition.NumberOfGroups);
// DataEntry1: zeros
- byte[] zeroPDE = new byte[WiaConst.PartitionDataEntrySize];
+ byte[] zeroPDE = new byte[PartitionDataEntrySize];
ms.Write(zeroPDE, 0, zeroPDE.Length);
- dest.Write(zeroPDE, 0, zeroPDE.Length);
+ stream.Write(zeroPDE, 0, zeroPDE.Length);
}
return ms.ToArray();
}
- private static byte[] SerializeRawDataEntries(List regions)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] SerializeRawDataEntries(List regions)
{
using var ms = new MemoryStream();
- foreach (var r in regions)
+ foreach (var region in regions)
{
- var e = new WiaRawDataEntry
- {
- DataOffset = r.Offset,
- DataSize = r.Size,
- GroupIndex = r.FirstGroupIndex,
- NumberOfGroups = r.NumberOfGroups,
- };
- ms.Write(SerializeRawDataEntry(e), 0, WiaConst.RawDataEntrySize);
+ ms.Write(SerializeRawDataEntry(region), 0, RawDataEntrySize);
}
return ms.ToArray();
}
+ ///
+ ///
+ ///
+ ///
+ ///
private static byte[] BuildExceptionList(List exceptions)
{
using var ms = new MemoryStream();
@@ -1112,6 +1376,7 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
ms.WriteByte((byte)(count >> 8));
ms.WriteByte((byte)count);
pos += 2;
+
foreach (var ex in exceptions)
{
ms.WriteByte((byte)(ex.Offset >> 8));
@@ -1120,147 +1385,195 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
pos += 22;
}
- while ((pos % 4) != 0) { ms.WriteByte(0); pos++; }
+ while ((pos % 4) != 0)
+ {
+ ms.WriteByte(0);
+ pos++;
+ }
return ms.ToArray();
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
private static byte[] CompressTableDataWia(byte[] data,
- WiaRvzCompressionType ct, int cl, byte[] propData, byte propSize)
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ byte[] propData,
+ byte propSize)
{
- if (ct == WiaRvzCompressionType.Purge)
+ if (compressionType == WiaRvzCompressionType.Purge)
return PurgeCompressor.Compress(data, 0, data.Length);
- if (ct > WiaRvzCompressionType.Purge)
- return WiaRvzCompressionHelper.Compress(ct, data, 0, data.Length, cl, propData, propSize);
+ if (compressionType > WiaRvzCompressionType.Purge)
+ return WiaRvzCompressionHelper.Compress(compressionType, data, 0, data.Length, compressionLevel, propData, propSize);
+
return data;
}
- // -----------------------------------------------------------------------
- // Header finalisation
- // -----------------------------------------------------------------------
+ #endregion
- private static void WriteWiaHeaders(Stream dest, byte[] discHdr,
- bool isRvz, WiaDiscType discType,
- WiaRvzCompressionType compressionType, int compressionLevel, uint chunkSize,
- uint numPartitions, ulong partEntriesOffset, byte[] partHash,
- uint numRawData, ulong rawEntriesOffset, uint rawEntriesSize,
- uint numGroups, ulong groupEntriesOffset, uint groupEntriesSize,
- byte[] propData, byte propSize,
- long isoSize, long fileSize)
+ #region Header Finalization
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void WriteWiaHeaders(Stream dest,
+ byte[] discHdr,
+ bool isRvz,
+ WiaDiscType discType,
+ WiaRvzCompressionType compressionType,
+ int compressionLevel,
+ uint chunkSize,
+ uint numPartitions,
+ ulong partEntriesOffset,
+ byte[] partHash,
+ uint numRawData,
+ ulong rawEntriesOffset,
+ uint rawEntriesSize,
+ uint numGroups,
+ ulong groupEntriesOffset,
+ uint groupEntriesSize,
+ byte[] propData,
+ byte propSize,
+ long isoSize,
+ long fileSize)
{
var header2 = new WiaHeader2
{
- DiscType = discType,
- CompressionType = compressionType,
- CompressionLevel = compressionLevel,
- ChunkSize = chunkSize,
- DiscHeader = discHdr,
+ DiscType = discType,
+ CompressionType = compressionType,
+ CompressionLevel = compressionLevel,
+ ChunkSize = chunkSize,
+ DiscHeader = discHdr,
NumberOfPartitionEntries = numPartitions,
- PartitionEntrySize = WiaConst.PartitionEntrySize,
- PartitionEntriesOffset = partEntriesOffset,
- PartitionEntriesHash = partHash,
- NumberOfRawDataEntries = numRawData,
- RawDataEntriesOffset = rawEntriesOffset,
- RawDataEntriesSize = rawEntriesSize,
- NumberOfGroupEntries = numGroups,
- GroupEntriesOffset = groupEntriesOffset,
- GroupEntriesSize = groupEntriesSize,
- CompressorDataSize = propSize,
- CompressorData = propData,
+ PartitionEntrySize = PartitionEntrySize,
+ PartitionEntriesOffset = partEntriesOffset,
+ PartitionEntriesHash = partHash,
+ NumberOfRawDataEntries = numRawData,
+ RawDataEntriesOffset = rawEntriesOffset,
+ RawDataEntriesSize = rawEntriesSize,
+ NumberOfGroupEntries = numGroups,
+ GroupEntriesOffset = groupEntriesOffset,
+ GroupEntriesSize = groupEntriesSize,
+ CompressorDataSize = propSize,
+ CompressorData = propData,
};
- byte[] h2Bytes = SerializeHeader2Wia(header2);
- byte[] h2Hash = ComputeSha1Wia(h2Bytes, 0, h2Bytes.Length);
+ byte[] h2Bytes = SerializeWiaHeader2(header2);
+ byte[] h2Hash = ComputeSha1Wia(h2Bytes, 0, h2Bytes.Length);
- uint magic = isRvz ? WiaConst.RvzMagic : WiaConst.WiaMagic;
- uint ver = isRvz ? WiaConst.RvzVersion : WiaConst.WiaVersion;
- uint verC = isRvz ? WiaConst.RvzVersionWriteCompatible : WiaConst.WiaVersionWriteCompatible;
+ uint magic = isRvz ? RvzMagic : WiaMagic;
+ uint ver = isRvz ? RvzVersion : WiaVersion;
+ uint verC = isRvz ? RvzVersionWriteCompatible : WiaVersionWriteCompatible;
var header1 = new WiaHeader1
{
- Magic = magic,
- Version = ver,
+ Magic = magic,
+ Version = ver,
VersionCompatible = verC,
- Header2Size = WiaConst.Header2Size,
- Header2Hash = h2Hash,
- IsoFileSize = (ulong)isoSize,
- WiaFileSize = (ulong)fileSize,
- Header1Hash = new byte[20],
+ Header2Size = Header2Size,
+ Header2Hash = h2Hash,
+ IsoFileSize = (ulong)isoSize,
+ WiaFileSize = (ulong)fileSize,
+ Header1Hash = new byte[20],
};
- byte[] h1Bytes = SerializeHeader1Wia(header1);
+ byte[] h1Bytes = SerializeWiaHeader1(header1);
byte[] h1Hashable = new byte[h1Bytes.Length - 20];
Array.Copy(h1Bytes, h1Hashable, h1Hashable.Length);
header1.Header1Hash = ComputeSha1Wia(h1Hashable, 0, h1Hashable.Length);
dest.Seek(0, SeekOrigin.Begin);
- dest.Write(SerializeHeader1Wia(header1), 0, WiaConst.Header1Size);
+ dest.Write(SerializeWiaHeader1(header1), 0, Header1Size);
dest.Write(h2Bytes, 0, h2Bytes.Length);
}
- private static byte[] SerializeHeader1Wia(WiaHeader1 h)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] SerializeWiaHeader1(WiaHeader1 obj)
{
using var ms = new MemoryStream();
- ms.WriteLittleEndian(h.Magic);
- ms.WriteBigEndian(h.Version);
- ms.WriteBigEndian(h.VersionCompatible);
- ms.WriteBigEndian(h.Header2Size);
- ms.Write(h.Header2Hash, 0, 20);
- ms.WriteBigEndian(h.IsoFileSize);
- ms.WriteBigEndian(h.WiaFileSize);
- ms.Write(h.Header1Hash, 0, 20);
+ Serialization.Writers.WIA.WriteWiaHeader1(ms, obj);
return ms.ToArray();
}
- private static byte[] SerializeHeader2Wia(WiaHeader2 h)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] SerializeWiaHeader2(WiaHeader2 obj)
{
using var ms = new MemoryStream();
- ms.WriteBigEndian((uint)h.DiscType);
- ms.WriteBigEndian((uint)h.CompressionType);
- ms.WriteBigEndian((uint)h.CompressionLevel);
- ms.WriteBigEndian(h.ChunkSize);
- byte[] dh = h.DiscHeader ?? new byte[WiaConst.DiscHeaderStoredSize];
- ms.Write(dh, 0, Math.Min(dh.Length, WiaConst.DiscHeaderStoredSize));
- if (dh.Length < WiaConst.DiscHeaderStoredSize)
- ms.Write(new byte[WiaConst.DiscHeaderStoredSize - dh.Length], 0,
- WiaConst.DiscHeaderStoredSize - dh.Length);
- ms.WriteBigEndian(h.NumberOfPartitionEntries);
- ms.WriteBigEndian(h.PartitionEntrySize);
- ms.WriteBigEndian(h.PartitionEntriesOffset);
- ms.Write(h.PartitionEntriesHash ?? new byte[20], 0, 20);
- ms.WriteBigEndian(h.NumberOfRawDataEntries);
- ms.WriteBigEndian(h.RawDataEntriesOffset);
- ms.WriteBigEndian(h.RawDataEntriesSize);
- ms.WriteBigEndian(h.NumberOfGroupEntries);
- ms.WriteBigEndian(h.GroupEntriesOffset);
- ms.WriteBigEndian(h.GroupEntriesSize);
- ms.WriteByte(h.CompressorDataSize);
- byte[] prop = h.CompressorData ?? new byte[7];
- ms.Write(prop, 0, Math.Min(prop.Length, 7));
- if (prop.Length < 7)
- ms.Write(new byte[7 - prop.Length], 0, 7 - prop.Length);
+ Serialization.Writers.WIA.WriteWiaHeader2(ms, obj);
return ms.ToArray();
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
private static byte[] ComputeSha1Wia(byte[] data, int offset, int count)
{
- if (count == 0) return new byte[20];
+ if (count == 0)
+ return new byte[20];
+
using var sha1 = new HashWrapper(HashType.SHA1);
sha1.Process(data, offset, count);
sha1.Terminate();
+
return sha1.CurrentHashBytes ?? new byte[20];
}
- // -----------------------------------------------------------------------
- // Platform detection
- // -----------------------------------------------------------------------
+ #endregion
+ #region Helpers
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// TODO: Can this be replaced by
private static Platform DetectWiaPlatform(byte[] header)
{
if (header.Length >= 0x1C)
{
uint wiiMagic = (uint)((header[0x18] << 24) | (header[0x19] << 16) | (header[0x1A] << 8) | header[0x1B]);
- if (wiiMagic == NdConstants.WiiMagicWord)
+ if (wiiMagic == WiiMagicWord)
return Platform.Wii;
}
@@ -1277,22 +1590,29 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
}
}
- if (valid) return Platform.GameCube;
+ if (valid)
+ return Platform.GameCube;
}
return Platform.Unknown;
}
- // -----------------------------------------------------------------------
- // Misc helpers
- // -----------------------------------------------------------------------
-
- private static uint WriteRawGroupData(Stream dest, ref long bytesWritten,
- GcGroupWorkEntry gi, bool isRvz,
- WiaRvzCompressionType compressionType, int compressionLevel,
- byte[] propData, byte propSize)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static uint WriteRawGroupData(Stream dest,
+ ref long bytesWritten,
+ GcGroupWorkEntry gi,
+ bool isRvz,
+ WiaRvzCompressionType compressionType)
{
- if (gi.CompressedData != null)
+ if (gi.CompressedData is not null)
{
bool useC = !isRvz || gi.CompressedData.Length < gi.MainData!.Length;
if (useC)
@@ -1305,7 +1625,7 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
}
}
- if (compressionType == WiaRvzCompressionType.Purge && gi.MainData != null)
+ if (compressionType == WiaRvzCompressionType.Purge && gi.MainData is not null)
{
byte[] comp = PurgeCompressor.Compress(gi.MainData, 0, gi.MainData.Length);
dest.Write(comp, 0, comp.Length);
@@ -1319,55 +1639,103 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
return (uint)data.Length;
}
- private static bool IsAllSameWia(byte[] data, int length)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool IsAllSame(byte[] data, int length)
{
- if (length == 0) return true;
+ if (length == 0)
+ return true;
+
byte first = data[0];
for (int i = 1; i < length; i++)
{
- if (data[i] != first) return false;
+ if (data[i] != first)
+ return false;
}
return true;
}
- #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
- private static byte[] ConcatBytesWia(byte[] a, int aOff, int aLen, byte[] b, int bOff, int bLen)
+#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static byte[] ConcatBytes(byte[] a, int aOff, int aLen, byte[] b, int bOff, int bLen)
{
var r = new byte[aLen + bLen];
- if (aLen > 0) Array.Copy(a, aOff, r, 0, aLen);
- if (bLen > 0) Array.Copy(b, bOff, r, aLen, bLen);
+
+ if (aLen > 0)
+ Array.Copy(a, aOff, r, 0, aLen);
+ if (bLen > 0)
+ Array.Copy(b, bOff, r, aLen, bLen);
+
return r;
}
#endif
- private static void PadTo4Wia(Stream s, ref long bytesWritten)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void PadToFourBytes(Stream stream, ref long bytesWritten)
{
int pad = (int)((-bytesWritten) & 3);
- if (pad > 0) { s.Write(new byte[pad], 0, pad); bytesWritten += pad; }
+ if (pad > 0)
+ {
+ stream.Write(new byte[pad], 0, pad);
+ bytesWritten += pad;
+ }
}
- private static void PadTableTo4Wia(Stream s, ref long tablePos)
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void PadTableToFourBytes(Stream stream, ref long tablePos)
{
long pad = (-tablePos) & 3;
- if (pad > 0) { s.Write(new byte[pad], 0, (int)pad); tablePos += pad; }
+ if (pad > 0)
+ {
+ stream.Write(new byte[pad], 0, (int)pad);
+ tablePos += pad;
+ }
}
- private static long AlignWia(long value, long align) => (value + align - 1) / align * align;
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static long Align(long value, long align)
+ => (value + align - 1) / align * align;
- // -----------------------------------------------------------------------
- // Inner work types
- // -----------------------------------------------------------------------
+ #endregion
+
+ #region Inner work types
// Key: (byte sameByte, int bytesRead) — replaces ValueTuple
private struct WiaDedupKey2 : IEquatable
{
public byte SameByte;
- public int BytesRead;
+ public int BytesRead;
public WiaDedupKey2(byte sameByte, int bytesRead)
{
- SameByte = sameByte;
+ SameByte = sameByte;
BytesRead = bytesRead;
}
@@ -1380,14 +1748,14 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
private struct WiaDedupKey3 : IEquatable
{
public ulong PartKeyHash;
- public byte SampleByte;
- public int BytesRead;
+ public byte SampleByte;
+ public int BytesRead;
public WiaDedupKey3(ulong pkh, byte sb, int br)
{
PartKeyHash = pkh;
- SampleByte = sb;
- BytesRead = br;
+ SampleByte = sb;
+ BytesRead = br;
}
public bool Equals(WiaDedupKey3 other) => PartKeyHash == other.PartKeyHash && SampleByte == other.SampleByte && BytesRead == other.BytesRead;
@@ -1396,115 +1764,79 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
}
// Value: (uint offset, uint dataSize) — replaces ValueTuple
- private struct WiaDedup2
+ private struct WiaDedup2(uint offset, uint dataSize)
{
- public uint Offset;
- public uint DataSize;
-
- public WiaDedup2(uint offset, uint dataSize) { Offset = offset; DataSize = dataSize; }
- }
-
- // Group entry holding DataOffset, DataSize, RvzPackedSize
- private struct WiaRvzGroupEntry
- {
- public uint DataOffset;
- public uint DataSize;
- public uint RvzPackedSize;
-
- public WiaRvzGroupEntry(uint dataOffset, uint dataSize, uint rvzPackedSize)
- {
- DataOffset = dataOffset;
- DataSize = dataSize;
- RvzPackedSize = rvzPackedSize;
- }
- }
-
- // Raw data entry local struct (avoids confusion with model RawDataEntry)
- private struct WiaRawDataEntry
- {
- public ulong DataOffset;
- public ulong DataSize;
- public uint GroupIndex;
- public uint NumberOfGroups;
+ public uint Offset = offset;
+ public uint DataSize = dataSize;
}
// Flat work item for Parallel.For — replaces (int b, int c) ValueTuple
- private struct WiaFlatWorkItem
+ private struct WiaFlatWorkItem(int batchIndex, int chunkIndex)
{
- public int BatchIndex;
- public int ChunkIndex;
-
- public WiaFlatWorkItem(int b, int c) { BatchIndex = b; ChunkIndex = c; }
+ public int BatchIndex = batchIndex;
+ public int ChunkIndex = chunkIndex;
}
private sealed class GcGroupWorkEntry
{
- public int BytesRead;
- public long SourceOffset;
- public bool IsAllSame;
- public byte SameByte;
- public bool IsDedupHit;
- public WiaRvzGroupEntry DedupEntry;
+ public int BytesRead;
+ public long SourceOffset;
+ public bool IsAllSame;
+ public byte SameByte;
+ public bool IsDedupHit;
+ public RvzGroupEntry DedupEntry = new();
public byte[]? MainData;
- public uint RvzPackedSize;
+ public uint RvzPackedSize;
public byte[]? CompressedData = null;
}
private sealed class WiiChunkWork
{
- public bool IsAllZeros;
- public bool IsDecDedupHit;
- public uint DecDedupOffset;
- public uint DecDedupDataSize;
- public byte[] ExceptionListBytes = new byte[0];
- public int UnpaddedExLen;
- public byte[] MainDataBytes = new byte[0];
- public uint RvzPackedSize;
+ public bool IsAllZeros;
+ public bool IsDecDedupHit;
+ public uint DecDedupOffset;
+ public uint DecDedupDataSize;
+ public byte[] ExceptionListBytes = [];
+ public int UnpaddedExLen;
+ public byte[] MainDataBytes = [];
+ public uint RvzPackedSize;
public byte[]? CompressedData = null;
- public bool DecAllSame;
+ public bool DecAllSame;
public WiaDedupKey3 DecDedupKey;
}
private sealed class WiiBatchItem
{
- public int BytesRead;
- public long SrcOffset;
- public bool IsInterDedupHit;
+ public int BytesRead;
+ public long SrcOffset;
+ public bool IsInterDedupHit;
public WiaDedup2 DedupResult;
- public bool EncAllSame;
+ public bool EncAllSame;
public WiaDedupKey3 DedupKey;
public byte[]? DecryptedAll;
public List? AllExceptions;
- public int NumChunks;
+ public int NumChunks;
public WiiChunkWork[]? PartWork;
}
private sealed class WiiPartInfo
{
public ulong PartitionOffset;
- public byte[] TitleKey = new byte[0];
+ public byte[] TitleKey = [];
public ulong DataOffset;
public ulong DataSize;
public ulong DataStart;
public ulong DataEnd;
- public uint FirstGroupIndex;
- public uint NumberOfGroups;
- }
-
- private sealed class RawRegionInfo
- {
- public ulong Offset;
- public ulong Size;
- public uint FirstGroupIndex;
- public uint NumberOfGroups;
+ public uint FirstGroupIndex;
+ public uint NumberOfGroups;
}
private sealed class DiscRegionEntry
{
- public bool IsPartition;
- public long Offset;
- public WiiPartInfo? PartitionInfo;
- public RawRegionInfo? RawInfo;
+ public bool IsPartition;
+ public long Offset;
+ public WiiPartInfo? PartitionInfo;
+ public RawDataEntry? RawInfo;
}
///
@@ -1512,8 +1844,8 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
/// Re-encrypts Wii partition groups on the fly via the virtual stream.
///
/// Destination ISO file path.
- /// true on success; false on any failure.
- public bool DumpIso(string outputPath)
+ /// true on success; false on any failure.
+ public bool DumpIso(string? outputPath)
{
if (string.IsNullOrEmpty(outputPath))
return false;
@@ -1537,6 +1869,7 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
int read = vStream.Read(buf, 0, toRead);
if (read <= 0)
break;
+
fs.Write(buf, 0, read);
remaining -= read;
}
@@ -1548,5 +1881,7 @@ WriteWiaHeaders(dest, discHdr, isRvz, WiaDiscType.GameCube, compressionType, com
return false;
}
}
+
+ #endregion
}
}
diff --git a/SabreTools.Wrappers/WIA.cs b/SabreTools.Wrappers/WIA.cs
index 947887dd..0056ce75 100644
--- a/SabreTools.Wrappers/WIA.cs
+++ b/SabreTools.Wrappers/WIA.cs
@@ -1,9 +1,10 @@
using System;
using System.IO;
-using SabreTools.Hashing;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Data.Models.WIA;
-using WiaConstants = SabreTools.Data.Models.WIA.Constants;
+using SabreTools.Hashing;
+using static SabreTools.Data.Models.NintendoDisc.Constants;
+using static SabreTools.Data.Models.WIA.Constants;
using WiaReader = SabreTools.Serialization.Readers.WIA;
namespace SabreTools.Wrappers
@@ -19,6 +20,9 @@ namespace SabreTools.Wrappers
#region Extension Properties
+ ///
+ public WiaGroupEntry[]? GroupEntries => Model.GroupEntries;
+
///
public WiaHeader1 Header1 => Model.Header1;
@@ -26,7 +30,7 @@ namespace SabreTools.Wrappers
public WiaHeader2 Header2 => Model.Header2;
/// True if this is an RVZ file; false if this is a WIA file.
- public bool IsRvz => Model.Header1.Magic == WiaConstants.RvzMagic;
+ public bool IsRvz => Header1.Magic == RvzMagic;
///
public PartitionEntry[]? PartitionEntries => Model.PartitionEntries;
@@ -34,10 +38,13 @@ namespace SabreTools.Wrappers
///
public RawDataEntry[] RawDataEntries => Model.RawDataEntries;
+ ///
+ public RvzGroupEntry[]? RvzGroupEntries => Model.RvzGroupEntries;
+
///
/// Total uncompressed ISO size in bytes
///
- public ulong IsoFileSize => Model.Header1.IsoFileSize;
+ public ulong IsoFileSize => Header1.IsoFileSize;
///
/// Disc header parsed from the 128-byte raw disc header stored in Header2.
@@ -46,19 +53,19 @@ namespace SabreTools.Wrappers
{
get
{
- if (_discHeader is not null)
- return _discHeader;
+ if (field is not null)
+ return field;
+
byte[]? raw = Header2.DiscHeader;
if (raw is null || raw.Length < 0x20)
return null;
+
using var ms = new MemoryStream(raw);
- _discHeader = Serialization.Readers.NintendoDisc.ParseDiscHeaderOnly(ms);
- return _discHeader;
+ field = Serialization.Readers.NintendoDisc.ParseDiscHeader(ms);
+ return field;
}
}
- private DiscHeader? _discHeader;
-
#endregion
#region Constructors
@@ -125,9 +132,11 @@ namespace SabreTools.Wrappers
if (model is null)
return null;
+#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
// The reader parsed the compressed table blobs as raw bytes.
// Re-read and decompress them here now that we have the compression parameters.
DecompressTables(model, data, currentOffset);
+#endif
return new WIA(model, data, currentOffset);
}
@@ -137,6 +146,7 @@ namespace SabreTools.Wrappers
}
}
+#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
///
/// Re-reads the partition entries, raw data entries, and group entries from the source
/// stream, decompresses them using the algorithm specified in Header2, and replaces the
@@ -144,7 +154,6 @@ namespace SabreTools.Wrappers
///
private static void DecompressTables(DiscImage model, Stream data, long baseOffset)
{
-#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
var comp = model.Header2.CompressionType;
// None / Purge tables are stored as plain big-endian structs — already parsed correctly.
@@ -155,13 +164,13 @@ namespace SabreTools.Wrappers
byte compDataSize = model.Header2.CompressorDataSize;
// --- Raw data entries (stored compressed) ---
- if (model.Header2.NumberOfRawDataEntries > 0 &&
- model.Header2.RawDataEntriesOffset > 0 &&
- model.Header2.RawDataEntriesSize > 0)
+ if (model.Header2.NumberOfRawDataEntries > 0
+ && model.Header2.RawDataEntriesOffset > 0
+ && model.Header2.RawDataEntriesSize > 0)
{
int count = (int)model.Header2.NumberOfRawDataEntries;
int compressedSize = (int)model.Header2.RawDataEntriesSize;
- int expectedSize = count * WiaConstants.RawDataEntrySize;
+ int expectedSize = count * RawDataEntrySize;
data.Seek(baseOffset + (long)model.Header2.RawDataEntriesOffset, SeekOrigin.Begin);
byte[] buf = new byte[compressedSize];
@@ -169,8 +178,7 @@ namespace SabreTools.Wrappers
if (read < compressedSize)
return;
- byte[] plain = WiaRvzCompressionHelper.Decompress(
- comp, buf, 0, compressedSize, compData, compDataSize);
+ byte[] plain = WiaRvzCompressionHelper.Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
if (plain is null || plain.Length < expectedSize)
return;
@@ -178,13 +186,13 @@ namespace SabreTools.Wrappers
}
// --- Group entries (stored compressed) ---
- if (model.Header2.NumberOfGroupEntries > 0 &&
- model.Header2.GroupEntriesOffset > 0 &&
- model.Header2.GroupEntriesSize > 0)
+ if (model.Header2.NumberOfGroupEntries > 0
+ && model.Header2.GroupEntriesOffset > 0
+ && model.Header2.GroupEntriesSize > 0)
{
int count = (int)model.Header2.NumberOfGroupEntries;
int compressedSize = (int)model.Header2.GroupEntriesSize;
- int entrySize = model.Header1.Magic == WiaConstants.RvzMagic ? WiaConstants.RvzGroupEntrySize : WiaConstants.WiaGroupEntrySize;
+ int entrySize = model.Header1.Magic == RvzMagic ? RvzGroupEntrySize : WiaGroupEntrySize;
int expectedSize = count * entrySize;
data.Seek(baseOffset + (long)model.Header2.GroupEntriesOffset, SeekOrigin.Begin);
@@ -193,85 +201,66 @@ namespace SabreTools.Wrappers
if (read < compressedSize)
return;
- byte[] plain = WiaRvzCompressionHelper.Decompress(
- comp, buf, 0, compressedSize, compData, compDataSize);
+ byte[] plain = WiaRvzCompressionHelper.Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
if (plain is null || plain.Length < expectedSize)
return;
- if (model.Header1.Magic == WiaConstants.RvzMagic)
+ if (model.Header1.Magic == RvzMagic)
model.RvzGroupEntries = ParseRvzGroupEntries(plain, count);
else
model.GroupEntries = ParseWiaGroupEntries(plain, count);
}
-#endif
}
-#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
- /// Parses raw data entries from a plain (already decompressed) byte array.
+ ///
+ /// Parses raw data entries from a plain (already decompressed) byte array.
+ ///
private static RawDataEntry[] ParseRawDataEntries(byte[] plain, int count)
{
var entries = new RawDataEntry[count];
+
+ int offset = 0;
for (int i = 0; i < count; i++)
{
- int o = i * WiaConstants.RawDataEntrySize;
- var e = new RawDataEntry();
- e.DataOffset = ReadUInt64BE(plain, o);
- e.DataSize = ReadUInt64BE(plain, o + 8);
- e.GroupIndex = ReadUInt32BE(plain, o + 16);
- e.NumberOfGroups = ReadUInt32BE(plain, o + 20);
- entries[i] = e;
+ entries[i] = WiaReader.ParseRawDataEntry(plain, ref offset);
}
return entries;
}
- /// Parses WIA group entries from a plain (already decompressed) byte array.
+ ///
+ /// Parses WIA group entries from a plain (already decompressed) byte array.
+ ///
private static WiaGroupEntry[] ParseWiaGroupEntries(byte[] plain, int count)
{
var entries = new WiaGroupEntry[count];
+
+ int offset = 0;
for (int i = 0; i < count; i++)
{
- int o = i * WiaConstants.WiaGroupEntrySize;
- var e = new WiaGroupEntry();
- e.DataOffset = (ulong)ReadUInt32BE(plain, o) << 2;
- e.DataSize = ReadUInt32BE(plain, o + 4);
- entries[i] = e;
+ entries[i] = WiaReader.ParseWiaGroupEntry(plain, ref offset);
}
return entries;
}
- /// Parses RVZ group entries from a plain (already decompressed) byte array.
+ ///
+ /// Parses RVZ group entries from a plain (already decompressed) byte array.
+ ///
private static RvzGroupEntry[] ParseRvzGroupEntries(byte[] plain, int count)
{
var entries = new RvzGroupEntry[count];
+
+ int offset = 0;
for (int i = 0; i < count; i++)
{
- int o = i * WiaConstants.RvzGroupEntrySize;
- var e = new RvzGroupEntry();
- e.DataOffset = (ulong)ReadUInt32BE(plain, o) << 2;
- e.DataSize = ReadUInt32BE(plain, o + 4);
- e.RvzPackedSize = ReadUInt32BE(plain, o + 8);
- entries[i] = e;
+ entries[i] = WiaReader.ParseRvzGroupEntry(plain, ref offset);
}
return entries;
}
#endif
-#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
- private static ulong ReadUInt64BE(byte[] b, int o)
- {
- return ((ulong)b[o] << 56) | ((ulong)b[o + 1] << 48) | ((ulong)b[o + 2] << 40) | ((ulong)b[o + 3] << 32)
- | ((ulong)b[o + 4] << 24) | ((ulong)b[o + 5] << 16) | ((ulong)b[o + 6] << 8) | b[o + 7];
- }
-
- private static uint ReadUInt32BE(byte[] b, int o)
- {
- return ((uint)b[o] << 24) | ((uint)b[o + 1] << 16) | ((uint)b[o + 2] << 8) | b[o + 3];
- }
-#endif
-
#endregion
#region Inner Wrapper
@@ -288,7 +277,7 @@ namespace SabreTools.Wrappers
///
public NintendoDisc? GetInnerWrapper()
{
- if (Model.Header1.IsoFileSize == 0)
+ if (Header1.IsoFileSize == 0)
return null;
var vStream = new WiaVirtualStream(this);
@@ -299,47 +288,43 @@ namespace SabreTools.Wrappers
// For Wii discs: WIA/RVZ stores partition data already decrypted.
// Wire a pre-decrypted reader so NintendoDisc.Extraction bypasses its
// AES-CBC decrypt pass and reads directly from our decompressed groups.
- if (Model.PartitionEntries is { Length: > 0 })
- disc._preDecryptedReader = BuildPreDecryptedReader();
+ if (PartitionEntries is not null && PartitionEntries.Length > 0)
+ disc._preDecryptedReader = PreDecryptedReader;
return disc;
}
///
- /// Builds the delegate used by .
+ /// Used by .
/// Matches (absolute ISO offset of the encrypted data
/// area) to the corresponding WIA by comparing it with
- /// de.FirstSector * 0x8000, then delegates to
+ /// de.FirstSector * 0x8000, then delegates to
/// .
///
- private Func BuildPreDecryptedReader()
+ private byte[]? PreDecryptedReader(long absDataOffset, long partitionDataOffset, int length)
{
- const int WiiBlockSize = 0x8000;
- return (absDataOffset, partitionDataOffset, length) =>
- {
- if (Model.PartitionEntries is null)
- return null;
-
- foreach (var pe in Model.PartitionEntries)
- {
- // The data area of this partition starts at de.FirstSector * 0x8000
- long deIsoStart = (long)pe.DataEntry0.FirstSector * WiiBlockSize;
- long deIsoEnd = deIsoStart + ((long)pe.DataEntry0.NumberOfSectors * WiiBlockSize);
-
- if (absDataOffset >= deIsoStart && absDataOffset < deIsoEnd)
- return ReadDecryptedPartitionBytes(pe, partitionDataOffset, length);
-
- if (pe.DataEntry1 is { NumberOfSectors: > 0 })
- {
- long de1Start = (long)pe.DataEntry1.FirstSector * WiiBlockSize;
- long de1End = de1Start + ((long)pe.DataEntry1.NumberOfSectors * WiiBlockSize);
- if (absDataOffset >= de1Start && absDataOffset < de1End)
- return ReadDecryptedPartitionBytes(pe, partitionDataOffset, length);
- }
- }
-
+ if (PartitionEntries is null)
return null;
- };
+
+ foreach (var entry in PartitionEntries)
+ {
+ // The data area of this partition starts at de.FirstSector * 0x8000
+ long deIsoStart = (long)entry.DataEntry0.FirstSector * WiiBlockSize;
+ long deIsoEnd = deIsoStart + ((long)entry.DataEntry0.NumberOfSectors * WiiBlockSize);
+
+ if (absDataOffset >= deIsoStart && absDataOffset < deIsoEnd)
+ return ReadDecryptedPartitionBytes(entry, partitionDataOffset, length);
+
+ if (entry.DataEntry1 is { NumberOfSectors: > 0 })
+ {
+ long de1Start = (long)entry.DataEntry1.FirstSector * WiiBlockSize;
+ long de1End = de1Start + ((long)entry.DataEntry1.NumberOfSectors * WiiBlockSize);
+ if (absDataOffset >= de1Start && absDataOffset < de1End)
+ return ReadDecryptedPartitionBytes(entry, partitionDataOffset, length);
+ }
+ }
+
+ return null;
}
///
@@ -349,7 +334,7 @@ namespace SabreTools.Wrappers
///
internal int ReadVirtual(long offset, byte[] buffer, int bufferOffset, int count)
{
- long isoSize = (long)Model.Header1.IsoFileSize;
+ long isoSize = (long)Header1.IsoFileSize;
if (offset >= isoSize || count <= 0)
return 0;
@@ -382,32 +367,34 @@ namespace SabreTools.Wrappers
private int ReadVirtualChunk(long pos, byte[] buffer, int bufferOffset, int count)
{
// 1. Disc header (first 0x80 bytes stored verbatim in Header2.DiscHeader)
- if (pos < WiaConstants.DiscHeaderStoredSize && Model.Header2.DiscHeader is { Length: > 0 })
+ if (pos < DiscHeaderStoredSize && Header2.DiscHeader.Length > 0)
{
- int available = (int)Math.Min(WiaConstants.DiscHeaderStoredSize - pos, count);
- int srcAvail = Math.Min(available, Model.Header2.DiscHeader.Length - (int)pos);
+ int available = (int)Math.Min(DiscHeaderStoredSize - pos, count);
+ int srcAvail = Math.Min(available, Header2.DiscHeader.Length - (int)pos);
if (srcAvail > 0)
- Array.Copy(Model.Header2.DiscHeader, (int)pos, buffer, bufferOffset, srcAvail);
+ Array.Copy(Header2.DiscHeader, (int)pos, buffer, bufferOffset, srcAvail);
+
if (available > srcAvail)
Array.Clear(buffer, bufferOffset + srcAvail, available - srcAvail);
+
return available;
}
- uint chunkSize = Model.Header2.ChunkSize;
- var comp = Model.Header2.CompressionType;
- byte[] compData = Model.Header2.CompressorData ?? new byte[7];
- byte compDataSize = Model.Header2.CompressorDataSize;
+ uint chunkSize = Header2.ChunkSize;
+ var comp = Header2.CompressionType;
+ byte[] compData = Header2.CompressorData;
+ byte compDataSize = Header2.CompressorDataSize;
// 2. Raw data entries (non-partition disc data)
- if (Model.RawDataEntries is { Length: > 0 })
+ if (RawDataEntries.Length > 0)
{
- foreach (var rde in Model.RawDataEntries)
+ foreach (var entry in RawDataEntries)
{
- if (rde.DataSize == 0 || rde.NumberOfGroups == 0)
+ if (entry.DataSize == 0 || entry.NumberOfGroups == 0)
continue;
- long rdeStart = (long)rde.DataOffset;
- long rdeEnd = rdeStart + (long)rde.DataSize;
+ long rdeStart = (long)entry.DataOffset;
+ long rdeEnd = rdeStart + (long)entry.DataSize;
if (pos < rdeStart || pos >= rdeEnd)
continue;
@@ -417,10 +404,10 @@ namespace SabreTools.Wrappers
uint g = (uint)(adjustedPos / chunkSize);
int offsetInGroup = (int)(adjustedPos % chunkSize);
- if (g >= rde.NumberOfGroups)
+ if (g >= entry.NumberOfGroups)
continue;
- uint groupFileIdx = rde.GroupIndex + g;
+ uint groupFileIdx = entry.GroupIndex + g;
byte[]? groupBytes = GetCachedRawGroup(groupFileIdx, comp, compData, compDataSize, chunkSize);
if (groupBytes is null)
return 0;
@@ -430,6 +417,7 @@ namespace SabreTools.Wrappers
return 0;
int remainingInEntry = (int)Math.Min(rdeEnd - pos, count);
+
// Also clamp to the end of this group
long groupIsoEnd = adjustedBase + ((long)(g + 1) * chunkSize);
int remainingInGroup = (int)Math.Min(groupIsoEnd - pos, remainingInEntry);
@@ -443,16 +431,35 @@ namespace SabreTools.Wrappers
}
// 3. Partition data entries (Wii encrypted partition data)
- if (Model.PartitionEntries is { Length: > 0 })
+ if (PartitionEntries is not null && PartitionEntries.Length > 0)
{
- foreach (var pe in Model.PartitionEntries)
+ foreach (var pe in PartitionEntries)
{
- int r = ReadPartitionChunk(pe.DataEntry0, pe.PartitionKey, pos,
- buffer, bufferOffset, count, comp, compData, compDataSize, chunkSize);
- if (r > 0) return r;
- r = ReadPartitionChunk(pe.DataEntry1, pe.PartitionKey, pos,
- buffer, bufferOffset, count, comp, compData, compDataSize, chunkSize);
- if (r > 0) return r;
+ int ret = ReadPartitionChunk(pe.DataEntry0,
+ pe.PartitionKey,
+ pos,
+ buffer,
+ bufferOffset,
+ count,
+ comp,
+ compData,
+ compDataSize,
+ chunkSize);
+ if (ret > 0)
+ return ret;
+
+ ret = ReadPartitionChunk(pe.DataEntry1,
+ pe.PartitionKey,
+ pos,
+ buffer,
+ bufferOffset,
+ count,
+ comp,
+ compData,
+ compDataSize,
+ chunkSize);
+ if (ret > 0)
+ return ret;
}
}
@@ -470,23 +477,21 @@ namespace SabreTools.Wrappers
if (length <= 0 || pe is null)
return null;
- const int WiiBlockSize = 0x8000;
- const int WiiBlockDataSize = 0x7C00;
+ uint chunkSize = Header2.ChunkSize;
+ var comp = Header2.CompressionType;
+ byte[] compData = Header2.CompressorData ?? new byte[7];
+ byte compDataSize = Header2.CompressorDataSize;
+ int blocksPerGroup = (int)(chunkSize / WiiBlockSize);
- uint chunkSize = Model.Header2.ChunkSize;
- var comp = Model.Header2.CompressionType;
- byte[] compData = Model.Header2.CompressorData ?? new byte[7];
- byte compDataSize = Model.Header2.CompressorDataSize;
- int blocksPerGroup = (int)(chunkSize / WiiBlockSize);
+ byte[] result = new byte[length];
+ int produced = 0;
- byte[] result = new byte[length];
- int produced = 0;
-
- // DataEntry0 covers [0 .. de0.NumberOfSectors * 0x7C00) in partition-data space.
- // DataEntry1 (if present) immediately follows.
+ // DataEntry0 covers [0 .. de0.NumberOfSectors * 0x7C00) in partition-data space
var de0 = pe.DataEntry0;
- var de1 = pe.DataEntry1;
long de0DataSize = (long)de0.NumberOfSectors * WiiBlockDataSize;
+
+ // DataEntry1 (if present) immediately follows
+ var de1 = pe.DataEntry1;
long de1DataSize = de1 is not null ? (long)de1.NumberOfSectors * WiiBlockDataSize : 0;
while (produced < length)
@@ -511,24 +516,29 @@ namespace SabreTools.Wrappers
break; // beyond available data
}
- long blockNum = deRelOff / WiiBlockDataSize;
- int offsetInBlock = (int)(deRelOff % WiiBlockDataSize);
+ long blockNum = deRelOff / WiiBlockDataSize;
+ int offsetInBlock = (int)(deRelOff % WiiBlockDataSize);
long groupRelative = blockNum / blocksPerGroup;
- int blockInGroup = (int)(blockNum % blocksPerGroup);
+ int blockInGroup = (int)(blockNum % blocksPerGroup);
if (groupRelative >= de.NumberOfGroups)
break;
- uint groupFileIdx = de.GroupIndex + (uint)groupRelative;
+ uint groupFileIdx = de.GroupIndex + (uint)groupRelative;
long dataOffsetForLfg = groupRelative * blocksPerGroup * WiiBlockDataSize;
- byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx, comp, compData, compDataSize,
- blocksPerGroup, WiiBlockDataSize, dataOffsetForLfg);
+ byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx,
+ comp,
+ compData,
+ compDataSize,
+ blocksPerGroup,
+ WiiBlockDataSize,
+ dataOffsetForLfg);
if (decrypted is null)
break;
- int offsetInGroup = (blockInGroup * WiiBlockDataSize) + offsetInBlock;
- int available = decrypted.Length - offsetInGroup;
+ int offsetInGroup = (blockInGroup * WiiBlockDataSize) + offsetInBlock;
+ int available = decrypted.Length - offsetInGroup;
if (available <= 0)
break;
@@ -545,17 +555,38 @@ namespace SabreTools.Wrappers
return null;
if (produced < length)
Array.Resize(ref result, produced);
+
return result;
}
- private int ReadPartitionChunk(PartitionDataEntry de, byte[] partitionKey, long pos,
- byte[] buffer, int bufferOffset, int count,
- WiaRvzCompressionType comp, byte[] compData, byte compDataSize, uint chunkSize)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private int ReadPartitionChunk(PartitionDataEntry de,
+ byte[] partitionKey,
+ long pos,
+ byte[] buffer,
+ int bufferOffset,
+ int count,
+ WiaRvzCompressionType comp,
+ byte[] compData,
+ byte compDataSize,
+ uint chunkSize)
{
if (de.NumberOfSectors == 0 || de.NumberOfGroups == 0)
return 0;
- const int WiiBlockSize = 0x8000;
if (chunkSize == 0)
return 0;
@@ -577,8 +608,7 @@ namespace SabreTools.Wrappers
return 0;
uint groupFileIdx = de.GroupIndex + (uint)groupNum;
- byte[]? encryptedGroup = GetCachedEncGroup(groupFileIdx, de, partitionKey,
- comp, compData, compDataSize, blocksPerGroup, chunkSize);
+ byte[]? encryptedGroup = GetCachedEncGroup(groupFileIdx, de, partitionKey, comp, compData, compDataSize, blocksPerGroup);
if (encryptedGroup is null)
return 0;
@@ -588,6 +618,7 @@ namespace SabreTools.Wrappers
return 0;
long remainingInEntry = isoDataEnd - pos;
+
// Stay within this group
long groupIsoEnd = isoDataStart + ((groupNum + 1) * blocksPerGroup * WiiBlockSize);
long remainingInGroup = groupIsoEnd - pos;
@@ -599,8 +630,20 @@ namespace SabreTools.Wrappers
return toCopy;
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
private byte[]? GetCachedRawGroup(uint groupFileIdx,
- WiaRvzCompressionType comp, byte[] compData, byte compDataSize, uint chunkSize)
+ WiaRvzCompressionType comp,
+ byte[] compData,
+ byte compDataSize,
+ uint chunkSize)
{
if (_cachedRawGroupIndex == groupFileIdx)
return _cachedRawGroup;
@@ -611,15 +654,30 @@ namespace SabreTools.Wrappers
return group;
}
- private byte[]? GetCachedEncGroup(uint groupFileIdx, PartitionDataEntry de, byte[] partitionKey,
- WiaRvzCompressionType comp, byte[] compData, byte compDataSize, int blocksPerGroup, uint chunkSize)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private byte[]? GetCachedEncGroup(uint groupFileIdx,
+ PartitionDataEntry de,
+ byte[] partitionKey,
+ WiaRvzCompressionType comp,
+ byte[] compData,
+ byte compDataSize,
+ int blocksPerGroup)
{
if (_cachedEncGroupIndex == groupFileIdx)
return _cachedEncGroup;
long dataOffsetForLfg = (groupFileIdx - de.GroupIndex) * blocksPerGroup * 0x7C00;
- byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx, comp, compData, compDataSize,
- blocksPerGroup, 0x7C00, dataOffsetForLfg);
+ byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx, comp, compData, compDataSize, blocksPerGroup, 0x7C00, dataOffsetForLfg);
if (decrypted is null)
return null;
@@ -631,75 +689,128 @@ namespace SabreTools.Wrappers
///
/// Reads and decompresses one raw (non-partition) group.
- /// Returns chunkSize bytes of raw ISO data, or null on failure.
+ /// Returns chunkSize bytes of raw ISO data, or null on failure.
///
- private byte[]? ReadGroupRaw(uint groupIdx, WiaRvzCompressionType comp,
- byte[] compressorData, byte compressorDataSize, uint chunkSize)
+ private byte[]? ReadGroupRaw(uint groupIdx,
+ WiaRvzCompressionType comp,
+ byte[] compressorData,
+ byte compressorDataSize,
+ uint chunkSize)
{
if (IsRvz)
{
- if (Model.RvzGroupEntries is null || groupIdx >= Model.RvzGroupEntries.Length)
+ if (RvzGroupEntries is null || groupIdx >= RvzGroupEntries.Length)
return null;
- var ge = Model.RvzGroupEntries[groupIdx];
+
+ var ge = RvzGroupEntries[groupIdx];
bool isRvzCompressed = (ge.DataSize & 0x80000000u) != 0;
uint dataSize = ge.DataSize & 0x7FFFFFFFu;
if (dataSize == 0)
return new byte[chunkSize];
- byte[] fileData = ReadRangeFromSource((long)ge.DataOffset, (int)dataSize);
- return DecompressGroupBytes(fileData, 0, (int)dataSize, comp,
- compressorData, compressorDataSize, (int)chunkSize, IsRvz, isRvzCompressed,
- ge.RvzPackedSize, groupIdx * chunkSize, false, chunkSize);
+
+ byte[] fileData = ReadRangeFromSource((long)ge.DataOffset << 2, (int)dataSize);
+ return DecompressGroupBytes(fileData,
+ 0,
+ (int)dataSize,
+ comp,
+ compressorData,
+ compressorDataSize,
+ (int)chunkSize,
+ IsRvz,
+ isRvzCompressed,
+ ge.RvzPackedSize,
+ groupIdx * chunkSize,
+ isWiiPartition: false,
+ chunkSize);
}
else
{
- if (Model.GroupEntries is null || groupIdx >= Model.GroupEntries.Length)
+ if (GroupEntries is null || groupIdx >= GroupEntries.Length)
return null;
- var ge = Model.GroupEntries[groupIdx];
+
+ var ge = GroupEntries[groupIdx];
if (ge.DataSize == 0)
return new byte[chunkSize];
- byte[] fileData = ReadRangeFromSource((long)ge.DataOffset, (int)ge.DataSize);
- return DecompressGroupBytes(fileData, 0, (int)ge.DataSize, comp,
- compressorData, compressorDataSize, (int)chunkSize, false, false,
- 0, 0L, false, chunkSize);
+
+ byte[] fileData = ReadRangeFromSource((long)ge.DataOffset << 2, (int)ge.DataSize);
+ return DecompressGroupBytes(fileData,
+ 0,
+ (int)ge.DataSize,
+ comp,
+ compressorData,
+ compressorDataSize,
+ (int)chunkSize,
+ IsRvz,
+ false,
+ 0,
+ 0,
+ false,
+ chunkSize);
}
}
///
/// Reads and decompresses a Wii partition group, returning the hash-stripped decrypted data.
///
- private byte[]? ReadDecryptedGroupData(uint groupIdx, WiaRvzCompressionType comp,
- byte[] compressorData, byte compressorDataSize, int blocksPerGroup, int blockDataSize,
+ private byte[]? ReadDecryptedGroupData(uint groupIdx,
+ WiaRvzCompressionType comp,
+ byte[] compressorData,
+ byte compressorDataSize,
+ int blocksPerGroup,
+ int blockDataSize,
long dataOffsetForLfg)
{
int decryptedGroupSize = blocksPerGroup * blockDataSize;
if (IsRvz)
{
- if (Model.RvzGroupEntries is null || groupIdx >= Model.RvzGroupEntries.Length)
+ if (RvzGroupEntries is null || groupIdx >= RvzGroupEntries.Length)
return null;
- var ge = Model.RvzGroupEntries[groupIdx];
+
+ var ge = RvzGroupEntries[groupIdx];
bool isRvzCompressed = (ge.DataSize & 0x80000000u) != 0;
uint dataSize = ge.DataSize & 0x7FFFFFFFu;
if (dataSize == 0)
return new byte[decryptedGroupSize];
- byte[] fileData = ReadRangeFromSource((long)ge.DataOffset, (int)dataSize);
- return DecompressGroupBytes(fileData, 0, (int)dataSize, comp,
- compressorData, compressorDataSize, decryptedGroupSize, IsRvz, isRvzCompressed,
- ge.RvzPackedSize, dataOffsetForLfg, true,
- Model.Header2.ChunkSize);
+
+ byte[] fileData = ReadRangeFromSource((long)ge.DataOffset << 2, (int)dataSize);
+ return DecompressGroupBytes(fileData,
+ 0,
+ (int)dataSize,
+ comp,
+ compressorData,
+ compressorDataSize,
+ decryptedGroupSize,
+ IsRvz,
+ isRvzCompressed,
+ ge.RvzPackedSize,
+ dataOffsetForLfg,
+ true,
+ Header2.ChunkSize);
}
else
{
- if (Model.GroupEntries is null || groupIdx >= Model.GroupEntries.Length)
+ if (GroupEntries is null || groupIdx >= GroupEntries.Length)
return null;
- var ge = Model.GroupEntries[groupIdx];
+
+ var ge = GroupEntries[groupIdx];
if (ge.DataSize == 0)
return new byte[decryptedGroupSize];
- byte[] fileData2 = ReadRangeFromSource((long)ge.DataOffset, (int)ge.DataSize);
- return DecompressGroupBytes(fileData2, 0, (int)ge.DataSize, comp,
- compressorData, compressorDataSize, decryptedGroupSize, false, false,
- 0, 0L, true,
- Model.Header2.ChunkSize);
+
+ byte[] fileData2 = ReadRangeFromSource((long)ge.DataOffset << 2, (int)ge.DataSize);
+ return DecompressGroupBytes(fileData2,
+ 0,
+ (int)ge.DataSize,
+ comp,
+ compressorData,
+ compressorDataSize,
+ decryptedGroupSize,
+ IsRvz,
+ false,
+ 0,
+ 0L,
+ true,
+ Header2.ChunkSize);
}
}
@@ -707,10 +818,18 @@ namespace SabreTools.Wrappers
/// Decompresses raw group bytes according to the WIA compression type and strips any
/// exception-list header, returning the plain data payload.
///
- private static byte[]? DecompressGroupBytes(byte[] fileData, int offset, int length,
- WiaRvzCompressionType comp, byte[] compressorData, byte compressorDataSize,
- int expectedSize, bool isRvz, bool isRvzCompressed,
- uint rvzPackedSize, long dataOffsetForLfg, bool isWiiPartition,
+ private static byte[]? DecompressGroupBytes(byte[] fileData,
+ int offset,
+ int length,
+ WiaRvzCompressionType comp,
+ byte[] compressorData,
+ byte compressorDataSize,
+ int expectedSize,
+ bool isRvz,
+ bool isRvzCompressed,
+ uint rvzPackedSize,
+ long dataOffsetForLfg,
+ bool isWiiPartition,
uint chunkSize = 2 * 1024 * 1024)
{
if (fileData is null || fileData.Length < length)
@@ -734,10 +853,10 @@ namespace SabreTools.Wrappers
// Exception list precedes the Purge payload; capture it for SHA-1, then decompress.
int purgeStart = isWiiPartition ? SkipExceptionLists(fileData, offset, length, chunkSize) : offset;
int exceptionLen = purgeStart - offset;
- byte[]? exceptionBytes = exceptionLen > 0
- ? new byte[exceptionLen] : null;
- if (exceptionBytes != null)
+ byte[]? exceptionBytes = exceptionLen > 0 ? new byte[exceptionLen] : null;
+ if (exceptionBytes is not null)
Array.Copy(fileData, offset, exceptionBytes, 0, exceptionLen);
+
int purgeLen = length - exceptionLen;
return PurgeDecompressor.Decompress(fileData, purgeStart, purgeLen, expectedSize, exceptionBytes);
}
@@ -785,6 +904,7 @@ namespace SabreTools.Wrappers
int bytesRead = rvzDecomp.Decompress(unpacked, 0, expectedSize);
if (bytesRead < expectedSize)
Array.Resize(ref unpacked, bytesRead);
+
return unpacked;
}
@@ -820,8 +940,10 @@ namespace SabreTools.Wrappers
{
ushort count = (ushort)((data[pos] << 8) | data[pos + 1]);
pos += 2;
+
// Each exception entry is 2 + 20 = 22 bytes
pos += count * 22;
+
// 4-byte alignment after last list
if (i == numLists - 1)
pos = (pos + 3) & ~3;
@@ -856,13 +978,10 @@ namespace SabreTools.Wrappers
///
internal static byte[] EncryptWiiGroup(byte[] decryptedData, byte[] key, int blocksPerGroup)
{
- const int WiiBlockSize = 0x8000;
- const int WiiBlockDataSize = 0x7C00;
- const int WiiBlockHashSize = 0x0400;
const int H0Count = 31;
const int H1Count = 8;
const int H2Count = 8;
- const int HashLen = 20;
+ const int HashLen = 20;
// --- Build H0 / H1 / H2 hash arrays ---
byte[][][] h0 = new byte[blocksPerGroup][][];
@@ -894,7 +1013,10 @@ namespace SabreTools.Wrappers
byte[] h0Concat = new byte[H0Count * HashLen];
for (int i = 0; i < H0Count; i++)
+ {
Array.Copy(h0[blockIdx][i], 0, h0Concat, i * HashLen, HashLen);
+ }
+
h1[g][s] = ComputeSha1(h0Concat, 0, h0Concat.Length);
}
}
@@ -906,7 +1028,10 @@ namespace SabreTools.Wrappers
int grp = Math.Min(i, h1.Length - 1);
byte[] h1Concat = new byte[H1Count * HashLen];
for (int s = 0; s < H1Count; s++)
+ {
Array.Copy(h1[grp][s], 0, h1Concat, s * HashLen, HashLen);
+ }
+
h2[i] = ComputeSha1(h1Concat, 0, h1Concat.Length);
}
@@ -919,7 +1044,11 @@ namespace SabreTools.Wrappers
int off = 0;
// H0 (31 * 20 = 0x26C)
- for (int i = 0; i < H0Count; i++) { Array.Copy(h0[b][i], 0, hashBlock, off, HashLen); off += HashLen; }
+ for (int i = 0; i < H0Count; i++)
+ {
+ Array.Copy(h0[b][i], 0, hashBlock, off, HashLen);
+ off += HashLen;
+ }
off += 0x14; // padding0
@@ -927,7 +1056,11 @@ namespace SabreTools.Wrappers
int h1Grp = b / H1Count;
if (h1Grp < h1.Length)
{
- for (int i = 0; i < H1Count; i++) { Array.Copy(h1[h1Grp][i], 0, hashBlock, off, HashLen); off += HashLen; }
+ for (int i = 0; i < H1Count; i++)
+ {
+ Array.Copy(h1[h1Grp][i], 0, hashBlock, off, HashLen);
+ off += HashLen;
+ }
}
else
{
@@ -964,16 +1097,19 @@ namespace SabreTools.Wrappers
return result;
}
-private static byte[] ComputeSha1(byte[] data, int offset, int count)
-{
- if (count == 0)
- return new byte[20];
+ ///
+ /// Get a segmented SHA-1 hash for input data
+ ///
+ private static byte[] ComputeSha1(byte[] data, int offset, int count)
+ {
+ if (count == 0)
+ return new byte[20];
- using var sha1 = new HashWrapper(HashType.SHA1);
- sha1.Process(data, offset, count);
- sha1.Terminate();
- return sha1.CurrentHashBytes ?? new byte[20];
-}
+ using var sha1 = new HashWrapper(HashType.SHA1);
+ sha1.Process(data, offset, count);
+ sha1.Terminate();
+ return sha1.CurrentHashBytes ?? new byte[20];
+ }
#endregion
}