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 }