From 964c97200c36982d1ca659855f41ac71d12c03d6 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Wed, 3 Apr 2024 20:55:02 -0400 Subject: [PATCH] Move stream deserializers to new organization --- README.MD | 3 +- .../Deserializers/AACS.cs | 463 +++++- .../Deserializers/ArchiveDotOrg.cs | 16 +- .../Deserializers/AttractMode.cs | 121 +- .../Deserializers/BDPlus.cs | 86 +- .../Deserializers/BFPK.cs | 142 +- SabreTools.Serialization/Deserializers/BSP.cs | 231 ++- SabreTools.Serialization/Deserializers/CFB.cs | 385 ++++- SabreTools.Serialization/Deserializers/CIA.cs | 514 ++++++- .../Deserializers/Catalog.cs | 25 +- .../Deserializers/ClrMamePro.cs | 912 ++++++++++- .../Deserializers/CueSheet.cs | 604 +++++++- .../Deserializers/DosCenter.cs | 229 ++- .../Deserializers/EverdriveSMDB.cs | 77 +- SabreTools.Serialization/Deserializers/GCF.cs | 754 ++++++++- .../Deserializers/Hashfile.cs | 173 ++- SabreTools.Serialization/Deserializers/IRD.cs | 124 +- .../Deserializers/InstallShieldCabinet.cs | 799 +++++++++- .../Deserializers/JsonFile.cs | 40 +- .../Deserializers/LinearExecutable.cs | 1026 ++++++++++++- .../Deserializers/Listrom.cs | 256 +++- .../Deserializers/Listxml.cs | 16 +- .../Deserializers/Logiqx.cs | 16 +- SabreTools.Serialization/Deserializers/M1.cs | 16 +- .../Deserializers/MSDOS.cs | 166 +- .../Deserializers/MicrosoftCabinet.cs | 264 +++- .../Deserializers/MoPaQ.cs | 652 +++++++- .../Deserializers/N3DS.cs | 732 ++++++++- SabreTools.Serialization/Deserializers/NCF.cs | 523 ++++++- .../Deserializers/NewExecutable.cs | 501 +++++- .../Deserializers/Nitro.cs | 384 ++++- .../Deserializers/OfflineList.cs | 16 +- .../Deserializers/OpenMSX.cs | 16 +- SabreTools.Serialization/Deserializers/PAK.cs | 119 +- SabreTools.Serialization/Deserializers/PFF.cs | 204 ++- SabreTools.Serialization/Deserializers/PIC.cs | 222 ++- .../Deserializers/PlayJAudio.cs | 364 ++++- .../Deserializers/PlayJPlaylist.cs | 96 +- .../Deserializers/PortableExecutable.cs | 1354 ++++++++++++++++- .../Deserializers/Quantum.cs | 177 ++- .../Deserializers/RomCenter.cs | 228 ++- SabreTools.Serialization/Deserializers/SGA.cs | 739 ++++++++- .../Deserializers/SeparatedValue.cs | 124 +- .../Deserializers/SoftwareList.cs | 16 +- .../Deserializers/VBSP.cs | 122 +- SabreTools.Serialization/Deserializers/VPK.cs | 296 +++- SabreTools.Serialization/Deserializers/WAD.cs | 255 +++- SabreTools.Serialization/Deserializers/XZP.cs | 258 +++- .../Deserializers/XmlFile.cs | 39 +- .../Interfaces/IStreamSerializer.cs | 8 - .../Streams/AACS.Deserializer.cs | 450 ------ .../Streams/ArchiveDotOrg.Deserializer.cs | 12 - .../Streams/AttractMode.Deserializer.cs | 114 -- .../Streams/BDPlus.Deserializer.cs | 73 - .../Streams/BFPK.Deserializer.cs | 129 -- .../Streams/BSP.Deserializer.cs | 228 --- .../Streams/CFB.Deserializer.cs | 372 ----- .../Streams/CIA.Deserializer.cs | 511 ------- .../Streams/Catalog.Deserializer.cs | 19 - .../Streams/ClrMamePro.Deserializer.cs | 903 ----------- .../Streams/CueSheet.Deserializer.cs | 629 -------- .../Streams/DosCenter.Deserializer.cs | 222 --- .../Streams/EverdriveSMDB.Deserializer.cs | 70 - .../Streams/GCF.Deserializer.cs | 751 --------- .../Streams/HashFile.Deserializer.cs | 169 -- .../Streams/IRD.Deserializer.cs | 121 -- .../InstallShieldCabinet.Deserializer.cs | 787 ---------- .../Streams/JsonFile.Deserializer.cs | 40 - .../Streams/LinearExecutable.Deserializer.cs | 1013 ------------ .../Streams/Listrom.Deserializer.cs | 249 --- .../Streams/Listxml.Deserializer.cs | 12 - .../Streams/Logiqx.Deserializer.cs | 12 - .../Streams/M1.Deserializer.cs | 12 - .../Streams/MSDOS.Deserializer.cs | 153 -- .../Streams/MicrosoftCabinet.Deserializer.cs | 252 --- .../Streams/MoPaQ.Deserializer.cs | 639 -------- .../Streams/N3DS.Deserializer.cs | 719 --------- .../Streams/NCF.Deserialization.cs | 520 ------- .../Streams/NewExecutable.Deserializer.cs | 488 ------ .../Streams/Nitro.Deserializer.cs | 371 ----- .../Streams/OfflineList.Deserializer.cs | 12 - .../Streams/OpenMSX.Deserializer.cs | 12 - .../Streams/PAK.Deserializer.cs | 116 -- .../Streams/PFF.Deserializer.cs | 191 --- .../Streams/PIC.Deserializer.cs | 187 --- .../Streams/PlayJAudio.Deserializer.cs | 351 ----- .../Streams/PlayJPlaylist.Deserializer.cs | 83 - .../PortableExecutable.Deserializer.cs | 1341 ---------------- .../Streams/Quantum.Deserializer.cs | 164 -- .../Streams/RomCenter.Deserializer.cs | 221 --- .../Streams/SGA.Deserializer.cs | 736 --------- .../Streams/SeparatedValue.Deserializer.cs | 115 -- .../Streams/SoftwareList.Deserializer.cs | 12 - .../Streams/VBSP.Deserializer.cs | 119 -- .../Streams/VPK.Deserializer.cs | 293 ---- .../Streams/WAD.Deserializer.cs | 252 --- .../Streams/XZP.Deserializer.cs | 255 ---- .../Streams/XmlFile.Deserializer.cs | 40 - .../Wrappers/AACSMediaKeyBlock.cs | 2 +- .../Wrappers/BDPlusSVM.cs | 2 +- SabreTools.Serialization/Wrappers/BFPK.cs | 2 +- SabreTools.Serialization/Wrappers/BSP.cs | 2 +- SabreTools.Serialization/Wrappers/CFB.cs | 2 +- SabreTools.Serialization/Wrappers/CIA.cs | 2 +- SabreTools.Serialization/Wrappers/GCF.cs | 2 +- SabreTools.Serialization/Wrappers/IRD.cs | 2 +- .../Wrappers/InstallShieldCabinet.cs | 2 +- .../Wrappers/LinearExecutable.cs | 2 +- SabreTools.Serialization/Wrappers/MSDOS.cs | 2 +- .../Wrappers/MicrosoftCabinet.cs | 2 +- SabreTools.Serialization/Wrappers/N3DS.cs | 2 +- SabreTools.Serialization/Wrappers/NCF.cs | 2 +- .../Wrappers/NewExecutable.cs | 2 +- SabreTools.Serialization/Wrappers/Nitro.cs | 2 +- SabreTools.Serialization/Wrappers/PAK.cs | 2 +- SabreTools.Serialization/Wrappers/PFF.cs | 2 +- .../Wrappers/PlayJAudioFile.cs | 2 +- .../Wrappers/PortableExecutable.cs | 2 +- SabreTools.Serialization/Wrappers/Quantum.cs | 2 +- SabreTools.Serialization/Wrappers/SGA.cs | 2 +- SabreTools.Serialization/Wrappers/VBSP.cs | 2 +- SabreTools.Serialization/Wrappers/VPK.cs | 2 +- SabreTools.Serialization/Wrappers/WAD.cs | 2 +- SabreTools.Serialization/Wrappers/XZP.cs | 2 +- 124 files changed, 14690 insertions(+), 14805 deletions(-) delete mode 100644 SabreTools.Serialization/Streams/AACS.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/ArchiveDotOrg.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/AttractMode.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/BDPlus.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/BFPK.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/BSP.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/CFB.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/CIA.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/Catalog.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/ClrMamePro.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/CueSheet.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/DosCenter.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/EverdriveSMDB.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/GCF.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/HashFile.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/IRD.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/InstallShieldCabinet.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/JsonFile.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/LinearExecutable.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/Listrom.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/Listxml.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/Logiqx.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/M1.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/MSDOS.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/MicrosoftCabinet.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/MoPaQ.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/N3DS.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/NCF.Deserialization.cs delete mode 100644 SabreTools.Serialization/Streams/NewExecutable.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/Nitro.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/OfflineList.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/OpenMSX.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/PAK.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/PFF.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/PIC.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/PlayJAudio.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/PlayJPlaylist.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/PortableExecutable.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/Quantum.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/RomCenter.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/SGA.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/SeparatedValue.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/SoftwareList.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/VBSP.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/VPK.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/WAD.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/XZP.Deserializer.cs delete mode 100644 SabreTools.Serialization/Streams/XmlFile.Deserializer.cs diff --git a/README.MD b/README.MD index cabdcea4..4648f146 100644 --- a/README.MD +++ b/README.MD @@ -6,8 +6,9 @@ Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTo | Interface Name | Source Type | Destination Type | | --- | --- | --- | -| `IByteDeserializer` | `byte[]` | Model | +| `IByteDeserializer` | `byte[]?` | Model | | `IFileDeserializer` | `string?` Path | Model | +| `IStreamDeserializer` | `Stream?` | Model | ## `SabreTools.Serialization.CrossModel` diff --git a/SabreTools.Serialization/Deserializers/AACS.cs b/SabreTools.Serialization/Deserializers/AACS.cs index ccf9c069..7ac4b721 100644 --- a/SabreTools.Serialization/Deserializers/AACS.cs +++ b/SabreTools.Serialization/Deserializers/AACS.cs @@ -1,23 +1,29 @@ +using System; +using System.Collections.Generic; using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.AACS; using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Deserializers { public class AACS : - IByteDeserializer, - IFileDeserializer + IByteDeserializer, + IFileDeserializer, + IStreamDeserializer { #region IByteDeserializer /// - public static Models.AACS.MediaKeyBlock? DeserializeBytes(byte[]? data, int offset) + public static MediaKeyBlock? DeserializeBytes(byte[]? data, int offset) { var deserializer = new AACS(); return deserializer.Deserialize(data, offset); } /// - public Models.AACS.MediaKeyBlock? Deserialize(byte[]? data, int offset) + public MediaKeyBlock? Deserialize(byte[]? data, int offset) { // If the data is invalid if (data == null) @@ -29,7 +35,7 @@ namespace SabreTools.Serialization.Deserializers // Create a memory stream and parse that var dataStream = new MemoryStream(data, offset, data.Length - offset); - return Streams.AACS.DeserializeStream(dataStream); + return DeserializeStream(dataStream); } #endregion @@ -37,17 +43,458 @@ namespace SabreTools.Serialization.Deserializers #region IFileDeserializer /// - public static Models.AACS.MediaKeyBlock? DeserializeFile(string? path) + public static MediaKeyBlock? DeserializeFile(string? path) { var deserializer = new AACS(); return deserializer.Deserialize(path); } /// - public Models.AACS.MediaKeyBlock? Deserialize(string? path) + public MediaKeyBlock? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.AACS.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static MediaKeyBlock? DeserializeStream(Stream? data) + { + var deserializer = new AACS(); + return deserializer.Deserialize(data); + } + + /// + public MediaKeyBlock? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new media key block to fill + var mediaKeyBlock = new MediaKeyBlock(); + + #region Records + + // Create the records list + var records = new List(); + + // Try to parse the records + while (data.Position < data.Length) + { + // Try to parse the record + var record = ParseRecord(data); + if (record == null) + return null; + + // Add the record + records.Add(record); + + // If we have an end of media key block record + if (record.RecordType == RecordType.EndOfMediaKeyBlock) + break; + + // Align to the 4-byte boundary if we're not at the end + if (data.Position < data.Length) + { + while (data.Position < data.Length && (data.Position % 4) != 0) + _ = data.ReadByteValue(); + } + else + { + break; + } + } + + // Set the records + mediaKeyBlock.Records = records.ToArray(); + + #endregion + + return mediaKeyBlock; + } + + /// + /// Parse a Stream into a record + /// + /// Stream to parse + /// Filled record on success, null on error + private static Record? ParseRecord(Stream data) + { + // TODO: Use marshalling here instead of building + + // The first 4 bytes make up the type and length + byte[]? typeAndLength = data.ReadBytes(4); + if (typeAndLength == null) + return null; + + RecordType type = (RecordType)typeAndLength[0]; + + // Remove the first byte and parse as big-endian + typeAndLength[0] = 0x00; + Array.Reverse(typeAndLength); + uint length = BitConverter.ToUInt32(typeAndLength, 0); + + // Create a record based on the type + switch (type) + { + // Recognized record types + case RecordType.EndOfMediaKeyBlock: return ParseEndOfMediaKeyBlockRecord(data, type, length); + case RecordType.ExplicitSubsetDifference: return ParseExplicitSubsetDifferenceRecord(data, type, length); + case RecordType.MediaKeyData: return ParseMediaKeyDataRecord(data, type, length); + case RecordType.SubsetDifferenceIndex: return ParseSubsetDifferenceIndexRecord(data, type, length); + case RecordType.TypeAndVersion: return ParseTypeAndVersionRecord(data, type, length); + case RecordType.DriveRevocationList: return ParseDriveRevocationListRecord(data, type, length); + case RecordType.HostRevocationList: return ParseHostRevocationListRecord(data, type, length); + case RecordType.VerifyMediaKey: return ParseVerifyMediaKeyRecord(data, type, length); + case RecordType.Copyright: return ParseCopyrightRecord(data, type, length); + + // Unrecognized record type + default: + return null; + } + } + + /// + /// Parse a Stream into an end of media key block record + /// + /// Stream to parse + /// Filled end of media key block record on success, null on error + private static EndOfMediaKeyBlockRecord? ParseEndOfMediaKeyBlockRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.EndOfMediaKeyBlock) + return null; + + // TODO: Use marshalling here instead of building + var record = new EndOfMediaKeyBlockRecord(); + + record.RecordType = type; + record.RecordLength = length; + if (length > 4) + record.SignatureData = data.ReadBytes((int)(length - 4)); + + return record; + } + + /// + /// Parse a Stream into an explicit subset-difference record + /// + /// Stream to parse + /// Filled explicit subset-difference record on success, null on error + private static ExplicitSubsetDifferenceRecord? ParseExplicitSubsetDifferenceRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.ExplicitSubsetDifference) + return null; + + // TODO: Use marshalling here instead of building + var record = new ExplicitSubsetDifferenceRecord(); + + record.RecordType = type; + record.RecordLength = length; + + // Cache the current offset + long initialOffset = data.Position - 4; + + // Create the subset difference list + var subsetDifferences = new List(); + + // Try to parse the subset differences + while (data.Position < initialOffset + length - 5) + { + var subsetDifference = new SubsetDifference(); + + subsetDifference.Mask = data.ReadByteValue(); + subsetDifference.Number = data.ReadUInt32BigEndian(); + + subsetDifferences.Add(subsetDifference); + } + + // Set the subset differences + record.SubsetDifferences = subsetDifferences.ToArray(); + + // If there's any data left, discard it + if (data.Position < initialOffset + length) + _ = data.ReadBytes((int)(initialOffset + length - data.Position)); + + return record; + } + + /// + /// Parse a Stream into a media key data record + /// + /// Stream to parse + /// Filled media key data record on success, null on error + private static MediaKeyDataRecord? ParseMediaKeyDataRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.MediaKeyData) + return null; + + // TODO: Use marshalling here instead of building + var record = new MediaKeyDataRecord(); + + record.RecordType = type; + record.RecordLength = length; + + // Cache the current offset + long initialOffset = data.Position - 4; + + // Create the media key list + var mediaKeys = new List(); + + // Try to parse the media keys + while (data.Position < initialOffset + length) + { + byte[]? mediaKey = data.ReadBytes(0x10); + if (mediaKey != null) + mediaKeys.Add(mediaKey); + } + + // Set the media keys + record.MediaKeyData = mediaKeys.ToArray(); + + return record; + } + + /// + /// Parse a Stream into a subset-difference index record + /// + /// Stream to parse + /// Filled subset-difference index record on success, null on error + private static SubsetDifferenceIndexRecord? ParseSubsetDifferenceIndexRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.SubsetDifferenceIndex) + return null; + + // TODO: Use marshalling here instead of building + var record = new SubsetDifferenceIndexRecord(); + + record.RecordType = type; + record.RecordLength = length; + + // Cache the current offset + long initialOffset = data.Position - 4; + + record.Span = data.ReadUInt32BigEndian(); + + // Create the offset list + var offsets = new List(); + + // Try to parse the offsets + while (data.Position < initialOffset + length) + { + uint offset = data.ReadUInt32BigEndian(); + offsets.Add(offset); + } + + // Set the offsets + record.Offsets = offsets.ToArray(); + + return record; + } + + /// + /// Parse a Stream into a type and version record + /// + /// Stream to parse + /// Filled type and version record on success, null on error + private static TypeAndVersionRecord? ParseTypeAndVersionRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.TypeAndVersion) + return null; + + // TODO: Use marshalling here instead of building + var record = new TypeAndVersionRecord(); + + record.RecordType = type; + record.RecordLength = length; + record.MediaKeyBlockType = (MediaKeyBlockType)data.ReadUInt32BigEndian(); + record.VersionNumber = data.ReadUInt32BigEndian(); + + return record; + } + + /// + /// Parse a Stream into a drive revocation list record + /// + /// Stream to parse + /// Filled drive revocation list record on success, null on error + private static DriveRevocationListRecord? ParseDriveRevocationListRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.DriveRevocationList) + return null; + + // TODO: Use marshalling here instead of building + var record = new DriveRevocationListRecord(); + + record.RecordType = type; + record.RecordLength = length; + + // Cache the current offset + long initialOffset = data.Position - 4; + + record.TotalNumberOfEntries = data.ReadUInt32BigEndian(); + + // Create the signature blocks list + var blocks = new List(); + + // Try to parse the signature blocks + int entryCount = 0; + while (entryCount < record.TotalNumberOfEntries && data.Position < initialOffset + length) + { + var block = new DriveRevocationSignatureBlock(); + + block.NumberOfEntries = data.ReadUInt32BigEndian(); + block.EntryFields = new DriveRevocationListEntry[block.NumberOfEntries]; + for (int i = 0; i < block.EntryFields.Length; i++) + { + var entry = new DriveRevocationListEntry(); + + entry.Range = data.ReadUInt16BigEndian(); + entry.DriveID = data.ReadBytes(6); + + block.EntryFields[i] = entry; + entryCount++; + } + + blocks.Add(block); + + // If we have an empty block + if (block.NumberOfEntries == 0) + break; + } + + // Set the signature blocks + record.SignatureBlocks = blocks.ToArray(); + + // If there's any data left, discard it + if (data.Position < initialOffset + length) + _ = data.ReadBytes((int)(initialOffset + length - data.Position)); + + return record; + } + + /// + /// Parse a Stream into a host revocation list record + /// + /// Stream to parse + /// Filled host revocation list record on success, null on error + private static HostRevocationListRecord? ParseHostRevocationListRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.HostRevocationList) + return null; + + // TODO: Use marshalling here instead of building + var record = new HostRevocationListRecord(); + + record.RecordType = type; + record.RecordLength = length; + + // Cache the current offset + long initialOffset = data.Position - 4; + + record.TotalNumberOfEntries = data.ReadUInt32BigEndian(); + + // Create the signature blocks list + var blocks = new List(); + + // Try to parse the signature blocks + int entryCount = 0; + while (entryCount < record.TotalNumberOfEntries && data.Position < initialOffset + length) + { + var block = new HostRevocationSignatureBlock(); + + block.NumberOfEntries = data.ReadUInt32BigEndian(); + block.EntryFields = new HostRevocationListEntry[block.NumberOfEntries]; + for (int i = 0; i < block.EntryFields.Length; i++) + { + var entry = new HostRevocationListEntry(); + + entry.Range = data.ReadUInt16BigEndian(); + entry.HostID = data.ReadBytes(6); + + block.EntryFields[i] = entry; + entryCount++; + } + + blocks.Add(block); + + // If we have an empty block + if (block.NumberOfEntries == 0) + break; + } + + // Set the signature blocks + record.SignatureBlocks = blocks.ToArray(); + + // If there's any data left, discard it + if (data.Position < initialOffset + length) + _ = data.ReadBytes((int)(initialOffset + length - data.Position)); + + return record; + } + + /// + /// Parse a Stream into a verify media key record + /// + /// Stream to parse + /// Filled verify media key record on success, null on error + private static VerifyMediaKeyRecord? ParseVerifyMediaKeyRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.VerifyMediaKey) + return null; + + // TODO: Use marshalling here instead of building + var record = new VerifyMediaKeyRecord(); + + record.RecordType = type; + record.RecordLength = length; + record.CiphertextValue = data.ReadBytes(0x10); + + return record; + } + + /// + /// Parse a Stream into a copyright record + /// + /// Stream to parse + /// Filled copyright record on success, null on error + private static CopyrightRecord? ParseCopyrightRecord(Stream data, RecordType type, uint length) + { + // Verify we're calling the right parser + if (type != RecordType.Copyright) + return null; + + // TODO: Use marshalling here instead of building + var record = new CopyrightRecord(); + + record.RecordType = type; + record.RecordLength = length; + if (length > 4) + { + byte[]? copyright = data.ReadBytes((int)(length - 4)); + if (copyright != null) + record.Copyright = Encoding.ASCII.GetString(copyright).TrimEnd('\0'); + } + + return record; } #endregion diff --git a/SabreTools.Serialization/Deserializers/ArchiveDotOrg.cs b/SabreTools.Serialization/Deserializers/ArchiveDotOrg.cs index 6d07b33c..8c1dabfe 100644 --- a/SabreTools.Serialization/Deserializers/ArchiveDotOrg.cs +++ b/SabreTools.Serialization/Deserializers/ArchiveDotOrg.cs @@ -1,10 +1,11 @@ namespace SabreTools.Serialization.Deserializers { - public class ArchiveDotOrg : XmlFile + public class ArchiveDotOrg : + XmlFile { #region IFileDeserializer - /// + /// public static Models.ArchiveDotOrg.Files? DeserializeFile(string? path) { var deserializer = new ArchiveDotOrg(); @@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers } #endregion + + #region IStreamDeserializer + + /// + public static Models.ArchiveDotOrg.Files? DeserializeStream(System.IO.Stream? data) + { + var deserializer = new ArchiveDotOrg(); + return deserializer.Deserialize(data); + } + + #endregion } } \ No newline at end of file diff --git a/SabreTools.Serialization/Deserializers/AttractMode.cs b/SabreTools.Serialization/Deserializers/AttractMode.cs index 4a62dbc6..764e0515 100644 --- a/SabreTools.Serialization/Deserializers/AttractMode.cs +++ b/SabreTools.Serialization/Deserializers/AttractMode.cs @@ -1,23 +1,136 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using SabreTools.IO.Readers; +using SabreTools.Models.AttractMode; using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Deserializers { - public class AttractMode : IFileDeserializer + public class AttractMode : + IFileDeserializer, + IStreamDeserializer { #region IFileDeserializer /// - public static Models.AttractMode.MetadataFile? DeserializeFile(string? path) + public static MetadataFile? DeserializeFile(string? path) { var deserializer = new AttractMode(); return deserializer.Deserialize(path); } /// - public Models.AttractMode.MetadataFile? Deserialize(string? path) + public MetadataFile? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.AttractMode.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static MetadataFile? DeserializeStream(Stream? data) + { + var deserializer = new AttractMode(); + return deserializer.Deserialize(data); + } + + /// + public MetadataFile? Deserialize(Stream? data) + { + // If the stream is null + if (data == null) + return default; + + // Setup the reader and output + var reader = new SeparatedValueReader(data, Encoding.UTF8) + { + Separator = ';', + VerifyFieldCount = false, + }; + var dat = new MetadataFile(); + + // Read the header values first + if (!reader.ReadHeader() || reader.HeaderValues == null) + return null; + + dat.Header = reader.HeaderValues.ToArray(); + + // Loop through the rows and parse out values + var rows = new List(); + while (!reader.EndOfStream) + { + // If we have no next line + if (!reader.ReadNextLine() || reader.Line == null) + break; + + // Parse the line into a row + Row row; + if (reader.Line.Count < Serialization.AttractMode.HeaderWithRomnameCount) + { + row = new Row + { + Name = reader.Line[0], + Title = reader.Line[1], + Emulator = reader.Line[2], + CloneOf = reader.Line[3], + Year = reader.Line[4], + Manufacturer = reader.Line[5], + Category = reader.Line[6], + Players = reader.Line[7], + Rotation = reader.Line[8], + Control = reader.Line[9], + Status = reader.Line[10], + DisplayCount = reader.Line[11], + DisplayType = reader.Line[12], + AltRomname = reader.Line[13], + AltTitle = reader.Line[14], + Extra = reader.Line[15], + Buttons = reader.Line[16], + }; + + // If we have additional fields + if (reader.Line.Count > Serialization.AttractMode.HeaderWithoutRomnameCount) + row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.AttractMode.HeaderWithoutRomnameCount).ToArray(); + } + else + { + row = new Row + { + Name = reader.Line[0], + Title = reader.Line[1], + Emulator = reader.Line[2], + CloneOf = reader.Line[3], + Year = reader.Line[4], + Manufacturer = reader.Line[5], + Category = reader.Line[6], + Players = reader.Line[7], + Rotation = reader.Line[8], + Control = reader.Line[9], + Status = reader.Line[10], + DisplayCount = reader.Line[11], + DisplayType = reader.Line[12], + AltRomname = reader.Line[13], + AltTitle = reader.Line[14], + Extra = reader.Line[15], + Buttons = reader.Line[16], + }; + + // If we have additional fields + if (reader.Line.Count > Serialization.AttractMode.HeaderWithRomnameCount) + row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.AttractMode.HeaderWithRomnameCount).ToArray(); + } + + rows.Add(row); + } + + // Assign the rows to the Dat and return + dat.Row = rows.ToArray(); + return dat; } #endregion diff --git a/SabreTools.Serialization/Deserializers/BDPlus.cs b/SabreTools.Serialization/Deserializers/BDPlus.cs index f6e6cae3..1fa34bd6 100644 --- a/SabreTools.Serialization/Deserializers/BDPlus.cs +++ b/SabreTools.Serialization/Deserializers/BDPlus.cs @@ -1,23 +1,28 @@ using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.BDPlus; using SabreTools.Serialization.Interfaces; +using static SabreTools.Models.BDPlus.Constants; namespace SabreTools.Serialization.Deserializers { public class BDPlus : - IByteDeserializer, - IFileDeserializer + IByteDeserializer, + IFileDeserializer, + IStreamDeserializer { #region IByteDeserializer /// - public static Models.BDPlus.SVM? DeserializeBytes(byte[]? data, int offset) + public static SVM? DeserializeBytes(byte[]? data, int offset) { var deserializer = new BDPlus(); return deserializer.Deserialize(data, offset); } /// - public Models.BDPlus.SVM? Deserialize(byte[]? data, int offset) + public SVM? Deserialize(byte[]? data, int offset) { // If the data is invalid if (data == null) @@ -29,7 +34,7 @@ namespace SabreTools.Serialization.Deserializers // Create a memory stream and parse that var dataStream = new MemoryStream(data, offset, data.Length - offset); - return Streams.BDPlus.DeserializeStream(dataStream); + return DeserializeStream(dataStream); } #endregion @@ -37,17 +42,82 @@ namespace SabreTools.Serialization.Deserializers #region IFileDeserializer /// - public static Models.BDPlus.SVM? DeserializeFile(string? path) + public static SVM? DeserializeFile(string? path) { var deserializer = new BDPlus(); return deserializer.Deserialize(path); } /// - public Models.BDPlus.SVM? Deserialize(string? path) + public SVM? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.BDPlus.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static SVM? DeserializeStream(Stream? data) + { + var deserializer = new BDPlus(); + return deserializer.Deserialize(data); + } + + /// + public SVM? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Try to parse the SVM + return ParseSVMData(data); + } + + /// + /// Parse a Stream into an SVM + /// + /// Stream to parse + /// Filled SVM on success, null on error + private static SVM? ParseSVMData(Stream data) + { + // TODO: Use marshalling here instead of building + var svm = new SVM(); + + byte[]? signature = data.ReadBytes(8); + if (signature == null) + return null; + + svm.Signature = Encoding.ASCII.GetString(signature); + if (svm.Signature != SignatureString) + return null; + + svm.Unknown1 = data.ReadBytes(5); + svm.Year = data.ReadUInt16BigEndian(); + svm.Month = data.ReadByteValue(); + if (svm.Month < 1 || svm.Month > 12) + return null; + + svm.Day = data.ReadByteValue(); + if (svm.Day < 1 || svm.Day > 31) + return null; + + svm.Unknown2 = data.ReadBytes(4); + svm.Length = data.ReadUInt32(); + // if (svm.Length > 0) + // svm.Data = data.ReadBytes((int)svm.Length); + + return svm; } #endregion diff --git a/SabreTools.Serialization/Deserializers/BFPK.cs b/SabreTools.Serialization/Deserializers/BFPK.cs index abe6cb81..ea1bbb3c 100644 --- a/SabreTools.Serialization/Deserializers/BFPK.cs +++ b/SabreTools.Serialization/Deserializers/BFPK.cs @@ -1,23 +1,28 @@ using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.BFPK; using SabreTools.Serialization.Interfaces; +using static SabreTools.Models.BFPK.Constants; namespace SabreTools.Serialization.Deserializers { public class BFPK : - IByteDeserializer, - IFileDeserializer + IByteDeserializer, + IFileDeserializer, + IStreamDeserializer { #region IByteDeserializer /// - public static Models.BFPK.Archive? DeserializeBytes(byte[]? data, int offset) + public static Archive? DeserializeBytes(byte[]? data, int offset) { var deserializer = new BFPK(); return deserializer.Deserialize(data, offset); } /// - public Models.BFPK.Archive? Deserialize(byte[]? data, int offset) + public Archive? Deserialize(byte[]? data, int offset) { // If the data is invalid if (data == null) @@ -29,7 +34,7 @@ namespace SabreTools.Serialization.Deserializers // Create a memory stream and parse that var dataStream = new MemoryStream(data, offset, data.Length - offset); - return Streams.BFPK.DeserializeStream(dataStream); + return DeserializeStream(dataStream); } #endregion @@ -37,17 +42,138 @@ namespace SabreTools.Serialization.Deserializers #region IFileDeserializer /// - public static Models.BFPK.Archive? DeserializeFile(string? path) + public static Archive? DeserializeFile(string? path) { var deserializer = new BFPK(); return deserializer.Deserialize(path); } /// - public Models.BFPK.Archive? Deserialize(string? path) + public Archive? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.BFPK.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static Archive? DeserializeStream(Stream? data) + { + var deserializer = new BFPK(); + return deserializer.Deserialize(data); + } + + /// + public Archive? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new archive to fill + var archive = new Archive(); + + #region Header + + // Try to parse the header + var header = ParseHeader(data); + if (header == null) + return null; + + // Set the archive header + archive.Header = header; + + #endregion + + #region Files + + // If we have any files + if (header.Files > 0) + { + var files = new FileEntry[header.Files]; + + // Read all entries in turn + for (int i = 0; i < header.Files; i++) + { + var file = ParseFileEntry(data); + if (file == null) + return null; + + files[i] = file; + } + + // Set the files + archive.Files = files; + } + + #endregion + + return archive; + } + + /// + /// Parse a Stream into a header + /// + /// Stream to parse + /// Filled header on success, null on error + private static Header? ParseHeader(Stream data) + { + // TODO: Use marshalling here instead of building + Header header = new Header(); + + byte[]? magic = data.ReadBytes(4); + if (magic == null) + return null; + + header.Magic = Encoding.ASCII.GetString(magic); + if (header.Magic != SignatureString) + return null; + + header.Version = data.ReadInt32(); + header.Files = data.ReadInt32(); + + return header; + } + + /// + /// Parse a Stream into a file entry + /// + /// Stream to parse + /// Filled file entry on success, null on error + private static FileEntry ParseFileEntry(Stream data) + { + // TODO: Use marshalling here instead of building + FileEntry fileEntry = new FileEntry(); + + fileEntry.NameSize = data.ReadInt32(); + if (fileEntry.NameSize > 0) + { + byte[]? name = data.ReadBytes(fileEntry.NameSize); + if (name != null) + fileEntry.Name = Encoding.ASCII.GetString(name); + } + + fileEntry.UncompressedSize = data.ReadInt32(); + fileEntry.Offset = data.ReadInt32(); + if (fileEntry.Offset > 0) + { + long currentOffset = data.Position; + data.Seek(fileEntry.Offset, SeekOrigin.Begin); + fileEntry.CompressedSize = data.ReadInt32(); + data.Seek(currentOffset, SeekOrigin.Begin); + } + + return fileEntry; } #endregion diff --git a/SabreTools.Serialization/Deserializers/BSP.cs b/SabreTools.Serialization/Deserializers/BSP.cs index b55e94cd..2d56c89d 100644 --- a/SabreTools.Serialization/Deserializers/BSP.cs +++ b/SabreTools.Serialization/Deserializers/BSP.cs @@ -1,11 +1,17 @@ using System.IO; +using System.Linq; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.BSP; using SabreTools.Serialization.Interfaces; +using static SabreTools.Models.BSP.Constants; namespace SabreTools.Serialization.Deserializers { public class BSP : IByteDeserializer, - IFileDeserializer + IFileDeserializer, + IStreamDeserializer { #region IByteDeserializer @@ -29,7 +35,7 @@ namespace SabreTools.Serialization.Deserializers // Create a memory stream and parse that var dataStream = new MemoryStream(data, offset, data.Length - offset); - return Streams.BSP.DeserializeStream(dataStream); + return DeserializeStream(dataStream); } #endregion @@ -47,7 +53,226 @@ namespace SabreTools.Serialization.Deserializers public Models.BSP.File? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.BSP.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static Models.BSP.File? DeserializeStream(Stream? data) + { + var deserializer = new BSP(); + return deserializer.Deserialize(data); + } + + /// + public Models.BSP.File? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new Half-Life Level to fill + var file = new Models.BSP.File(); + + #region Header + + // Try to parse the header + var header = ParseHeader(data); + if (header == null) + return null; + + // Set the level header + file.Header = header; + + #endregion + + #region Lumps + + // Create the lump array + file.Lumps = new Lump[HL_BSP_LUMP_COUNT]; + + // Try to parse the lumps + for (int i = 0; i < HL_BSP_LUMP_COUNT; i++) + { + var lump = ParseLump(data); + file.Lumps[i] = lump; + } + + #endregion + + #region Texture header + + // Try to get the texture header lump + var textureDataLump = file.Lumps[HL_BSP_LUMP_TEXTUREDATA]; + if (textureDataLump == null || textureDataLump.Offset == 0 || textureDataLump.Length == 0) + return null; + + // Seek to the texture header + data.Seek(textureDataLump.Offset, SeekOrigin.Begin); + + // Try to parse the texture header + var textureHeader = ParseTextureHeader(data); + if (textureHeader == null) + return null; + + // Set the texture header + file.TextureHeader = textureHeader; + + #endregion + + #region Textures + + // Create the texture array + file.Textures = new Texture[textureHeader.TextureCount]; + + // Try to parse the textures + for (int i = 0; i < textureHeader.TextureCount; i++) + { + // Get the texture offset + int offset = (int)(textureHeader.Offsets![i] + file.Lumps[HL_BSP_LUMP_TEXTUREDATA]!.Offset); + if (offset < 0 || offset >= data.Length) + continue; + + // Seek to the texture + data.Seek(offset, SeekOrigin.Begin); + + var texture = ParseTexture(data); + file.Textures[i] = texture; + } + + #endregion + + return file; + } + + /// + /// Parse a Stream into a Half-Life Level header + /// + /// Stream to parse + /// Filled Half-Life Level header on success, null on error + private static Header? ParseHeader(Stream data) + { + // TODO: Use marshalling here instead of building + Header header = new Header(); + + // Only recognized versions are 29 and 30 + header.Version = data.ReadUInt32(); + if (header.Version != 29 && header.Version != 30) + return null; + + return header; + } + + /// + /// Parse a Stream into a lump + /// + /// Stream to parse + /// Filled lump on success, null on error + private static Lump ParseLump(Stream data) + { + // TODO: Use marshalling here instead of building + Lump lump = new Lump(); + + lump.Offset = data.ReadUInt32(); + lump.Length = data.ReadUInt32(); + + return lump; + } + + /// + /// Parse a Stream into a Half-Life Level texture header + /// + /// Stream to parse + /// Filled Half-Life Level texture header on success, null on error + private static TextureHeader ParseTextureHeader(Stream data) + { + // TODO: Use marshalling here instead of building + TextureHeader textureHeader = new TextureHeader(); + + textureHeader.TextureCount = data.ReadUInt32(); + + var offsets = new uint[textureHeader.TextureCount]; + + for (int i = 0; i < textureHeader.TextureCount; i++) + { + offsets[i] = data.ReadUInt32(); + if (data.Position >= data.Length) + break; + } + + textureHeader.Offsets = offsets; + + return textureHeader; + } + + /// + /// Parse a Stream into a texture + /// + /// Stream to parse + /// Mipmap level + /// Filled texture on success, null on error + private static Texture ParseTexture(Stream data, uint mipmap = 0) + { + // TODO: Use marshalling here instead of building + Texture texture = new Texture(); + + byte[]? name = data.ReadBytes(16)?.TakeWhile(c => c != '\0')?.ToArray(); + if (name != null) + texture.Name = Encoding.ASCII.GetString(name); + texture.Width = data.ReadUInt32(); + texture.Height = data.ReadUInt32(); + texture.Offsets = new uint[4]; + for (int i = 0; i < 4; i++) + { + texture.Offsets[i] = data.ReadUInt32(); + } + + // Get the size of the pixel data + uint pixelSize = 0; + for (int i = 0; i < HL_BSP_MIPMAP_COUNT; i++) + { + if (texture.Offsets[i] != 0) + { + pixelSize += (texture.Width >> i) * (texture.Height >> i); + } + } + + // If we have no pixel data + if (pixelSize == 0) + return texture; + + texture.TextureData = data.ReadBytes((int)pixelSize); + texture.PaletteSize = data.ReadUInt16(); + texture.PaletteData = data.ReadBytes((int)(texture.PaletteSize * 3)); + + // Adjust the dimensions based on mipmap level + switch (mipmap) + { + case 1: + texture.Width /= 2; + texture.Height /= 2; + break; + case 2: + texture.Width /= 4; + texture.Height /= 4; + break; + case 3: + texture.Width /= 8; + texture.Height /= 8; + break; + } + + return texture; } #endregion diff --git a/SabreTools.Serialization/Deserializers/CFB.cs b/SabreTools.Serialization/Deserializers/CFB.cs index e7535c45..fc3e6674 100644 --- a/SabreTools.Serialization/Deserializers/CFB.cs +++ b/SabreTools.Serialization/Deserializers/CFB.cs @@ -1,23 +1,30 @@ +using System; +using System.Collections.Generic; using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.CFB; using SabreTools.Serialization.Interfaces; +using static SabreTools.Models.CFB.Constants; namespace SabreTools.Serialization.Deserializers { public class CFB : - IByteDeserializer, - IFileDeserializer + IByteDeserializer, + IFileDeserializer, + IStreamDeserializer { #region IByteDeserializer /// - public static Models.CFB.Binary? DeserializeBytes(byte[]? data, int offset) + public static Binary? DeserializeBytes(byte[]? data, int offset) { var deserializer = new CFB(); return deserializer.Deserialize(data, offset); } /// - public Models.CFB.Binary? Deserialize(byte[]? data, int offset) + public Binary? Deserialize(byte[]? data, int offset) { // If the data is invalid if (data == null) @@ -29,7 +36,7 @@ namespace SabreTools.Serialization.Deserializers // Create a memory stream and parse that var dataStream = new MemoryStream(data, offset, data.Length - offset); - return Streams.CFB.DeserializeStream(dataStream); + return DeserializeStream(dataStream); } #endregion @@ -37,17 +44,379 @@ namespace SabreTools.Serialization.Deserializers #region IFileDeserializer /// - public static Models.CFB.Binary? DeserializeFile(string? path) + public static Binary? DeserializeFile(string? path) { var deserializer = new CFB(); return deserializer.Deserialize(path); } /// - public Models.CFB.Binary? Deserialize(string? path) + public Binary? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.CFB.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static Binary? DeserializeStream(Stream? data) + { + var deserializer = new CFB(); + return deserializer.Deserialize(data); + } + + /// + public Binary? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new binary to fill + var binary = new Binary(); + + #region Header + + // Try to parse the file header + var fileHeader = ParseFileHeader(data); + if (fileHeader == null) + return null; + + // Set the file header + binary.Header = fileHeader; + + #endregion + + #region DIFAT Sector Numbers + + // Create a DIFAT sector table + var difatSectors = new List(); + + // Add the sectors from the header + if (fileHeader.DIFAT != null) + difatSectors.AddRange(fileHeader.DIFAT); + + // Loop through and add the DIFAT sectors + var currentSector = (SectorNumber?)fileHeader.FirstDIFATSectorLocation; + for (int i = 0; i < fileHeader.NumberOfDIFATSectors; i++) + { + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var sectorNumbers = ParseSectorNumbers(data, fileHeader.SectorShift); + if (sectorNumbers == null) + return null; + + // Add the sector shifts + difatSectors.AddRange(sectorNumbers); + } + + // Get the next sector from the DIFAT + currentSector = difatSectors[i]; + } + + // Assign the DIFAT sectors table + binary.DIFATSectorNumbers = difatSectors.ToArray(); + + #endregion + + #region FAT Sector Numbers + + // Create a FAT sector table + var fatSectors = new List(); + + // Loop through and add the FAT sectors + currentSector = binary.DIFATSectorNumbers[0]; + for (int i = 0; i < fileHeader.NumberOfFATSectors; i++) + { + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var sectorNumbers = ParseSectorNumbers(data, fileHeader.SectorShift); + if (sectorNumbers == null) + return null; + + // Add the sector shifts + fatSectors.AddRange(sectorNumbers); + } + + // Get the next sector from the DIFAT + currentSector = binary.DIFATSectorNumbers[i]; + } + + // Assign the FAT sectors table + binary.FATSectorNumbers = fatSectors.ToArray(); + + #endregion + + #region Mini FAT Sector Numbers + + // Create a mini FAT sector table + var miniFatSectors = new List(); + + // Loop through and add the mini FAT sectors + currentSector = (SectorNumber)fileHeader.FirstMiniFATSectorLocation; + for (int i = 0; i < fileHeader.NumberOfMiniFATSectors; i++) + { + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var sectorNumbers = ParseSectorNumbers(data, fileHeader.SectorShift); + if (sectorNumbers == null) + return null; + + // Add the sector shifts + miniFatSectors.AddRange(sectorNumbers); + } + + // Get the next sector from the DIFAT + currentSector = binary.DIFATSectorNumbers[i]; + } + + // Assign the mini FAT sectors table + binary.MiniFATSectorNumbers = miniFatSectors.ToArray(); + + #endregion + + #region Directory Entries + + // Get the offset of the first directory sector + long firstDirectoryOffset = (long)(fileHeader.FirstDirectorySectorLocation * Math.Pow(2, fileHeader.SectorShift)); + if (firstDirectoryOffset < 0 || firstDirectoryOffset >= data.Length) + return null; + + // Seek to the first directory sector + data.Seek(firstDirectoryOffset, SeekOrigin.Begin); + + // Create a directory sector table + var directorySectors = new List(); + + // Get the number of directory sectors + uint directorySectorCount = 0; + switch (fileHeader.MajorVersion) + { + case 3: + directorySectorCount = int.MaxValue; + break; + case 4: + directorySectorCount = fileHeader.NumberOfDirectorySectors; + break; + } + + // Loop through and add the directory sectors + currentSector = (SectorNumber)fileHeader.FirstDirectorySectorLocation; + for (int i = 0; i < directorySectorCount; i++) + { + // If we have an end of chain + if (currentSector == SectorNumber.ENDOFCHAIN) + break; + + // If we have a free sector for a version 3 filie + if (directorySectorCount == int.MaxValue && currentSector == SectorNumber.FREESECT) + break; + + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var directoryEntries = ParseDirectoryEntries(data, fileHeader.SectorShift, fileHeader.MajorVersion); + if (directoryEntries == null) + return null; + + // Add the sector shifts + directorySectors.AddRange(directoryEntries); + } + + // Get the next sector from the DIFAT + currentSector = binary.DIFATSectorNumbers[i]; + } + + // Assign the Directory sectors table + binary.DirectoryEntries = directorySectors.ToArray(); + + #endregion + + return binary; + } + + /// + /// Parse a Stream into a file header + /// + /// Stream to parse + /// Filled file header on success, null on error + private static FileHeader? ParseFileHeader(Stream data) + { + // TODO: Use marshalling here instead of building + FileHeader header = new FileHeader(); + + header.Signature = data.ReadUInt64(); + if (header.Signature != SignatureUInt64) + return null; + + header.CLSID = data.ReadGuid(); + header.MinorVersion = data.ReadUInt16(); + header.MajorVersion = data.ReadUInt16(); + header.ByteOrder = data.ReadUInt16(); + if (header.ByteOrder != 0xFFFE) + return null; + + header.SectorShift = data.ReadUInt16(); + if (header.MajorVersion == 3 && header.SectorShift != 0x0009) + return null; + else if (header.MajorVersion == 4 && header.SectorShift != 0x000C) + return null; + + header.MiniSectorShift = data.ReadUInt16(); + header.Reserved = data.ReadBytes(6); + header.NumberOfDirectorySectors = data.ReadUInt32(); + if (header.MajorVersion == 3 && header.NumberOfDirectorySectors != 0) + return null; + + header.NumberOfFATSectors = data.ReadUInt32(); + header.FirstDirectorySectorLocation = data.ReadUInt32(); + header.TransactionSignatureNumber = data.ReadUInt32(); + header.MiniStreamCutoffSize = data.ReadUInt32(); + if (header.MiniStreamCutoffSize != 0x00001000) + return null; + + header.FirstMiniFATSectorLocation = data.ReadUInt32(); + header.NumberOfMiniFATSectors = data.ReadUInt32(); + header.FirstDIFATSectorLocation = data.ReadUInt32(); + header.NumberOfDIFATSectors = data.ReadUInt32(); + header.DIFAT = new SectorNumber?[109]; + for (int i = 0; i < header.DIFAT.Length; i++) + { + header.DIFAT[i] = (SectorNumber)data.ReadUInt32(); + } + + // Skip rest of sector for version 4 + if (header.MajorVersion == 4) + _ = data.ReadBytes(3584); + + return header; + } + + /// + /// Parse a Stream into a sector full of sector numbers + /// + /// Stream to parse + /// Sector shift from the header + /// Filled sector full of sector numbers on success, null on error + private static SectorNumber?[] ParseSectorNumbers(Stream data, ushort sectorShift) + { + // TODO: Use marshalling here instead of building + int sectorCount = (int)(Math.Pow(2, sectorShift) / sizeof(uint)); + var sectorNumbers = new SectorNumber?[sectorCount]; + + for (int i = 0; i < sectorNumbers.Length; i++) + { + sectorNumbers[i] = (SectorNumber)data.ReadUInt32(); + } + + return sectorNumbers; + } + + /// + /// Parse a Stream into a sector full of directory entries + /// + /// Stream to parse + /// Sector shift from the header + /// Major version from the header + /// Filled sector full of directory entries on success, null on error + private static DirectoryEntry[]? ParseDirectoryEntries(Stream data, ushort sectorShift, ushort majorVersion) + { + // TODO: Use marshalling here instead of building + const int directoryEntrySize = 64 + 2 + 1 + 1 + 4 + 4 + 4 + 16 + 4 + 8 + 8 + 4 + 8; + int sectorCount = (int)(Math.Pow(2, sectorShift) / directoryEntrySize); + DirectoryEntry[] directoryEntries = new DirectoryEntry[sectorCount]; + + for (int i = 0; i < directoryEntries.Length; i++) + { + var directoryEntry = ParseDirectoryEntry(data, majorVersion); + if (directoryEntry == null) + return null; + + directoryEntries[i] = directoryEntry; + } + + return directoryEntries; + } + + /// + /// Parse a Stream into a directory entry + /// + /// Stream to parse + /// Major version from the header + /// Filled directory entry on success, null on error + private static DirectoryEntry ParseDirectoryEntry(Stream data, ushort majorVersion) + { + // TODO: Use marshalling here instead of building + DirectoryEntry directoryEntry = new DirectoryEntry(); + + byte[]? name = data.ReadBytes(64); + if (name != null) + directoryEntry.Name = Encoding.Unicode.GetString(name).TrimEnd('\0'); + directoryEntry.NameLength = data.ReadUInt16(); + directoryEntry.ObjectType = (ObjectType)data.ReadByteValue(); + directoryEntry.ColorFlag = (ColorFlag)data.ReadByteValue(); + directoryEntry.LeftSiblingID = (StreamID)data.ReadUInt32(); + directoryEntry.RightSiblingID = (StreamID)data.ReadUInt32(); + directoryEntry.ChildID = (StreamID)data.ReadUInt32(); + directoryEntry.CLSID = data.ReadGuid(); + directoryEntry.StateBits = data.ReadUInt32(); + directoryEntry.CreationTime = data.ReadUInt64(); + directoryEntry.ModifiedTime = data.ReadUInt64(); + directoryEntry.StartingSectorLocation = data.ReadUInt32(); + directoryEntry.StreamSize = data.ReadUInt64(); + if (majorVersion == 3) + directoryEntry.StreamSize &= 0x0000FFFF; + + return directoryEntry; } #endregion diff --git a/SabreTools.Serialization/Deserializers/CIA.cs b/SabreTools.Serialization/Deserializers/CIA.cs index 4d3ae64d..d08c549c 100644 --- a/SabreTools.Serialization/Deserializers/CIA.cs +++ b/SabreTools.Serialization/Deserializers/CIA.cs @@ -1,11 +1,16 @@ +using System; using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.N3DS; using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Deserializers { public class CIA : IByteDeserializer, - IFileDeserializer + IFileDeserializer, + IStreamDeserializer { #region IByteDeserializer @@ -29,7 +34,7 @@ namespace SabreTools.Serialization.Deserializers // Create a memory stream and parse that var dataStream = new MemoryStream(data, offset, data.Length - offset); - return Streams.CIA.DeserializeStream(dataStream); + return DeserializeStream(dataStream); } #endregion @@ -47,7 +52,510 @@ namespace SabreTools.Serialization.Deserializers public Models.N3DS.CIA? Deserialize(string? path) { using var stream = PathProcessor.OpenStream(path); - return Streams.CIA.DeserializeStream(stream); + return DeserializeStream(stream); + } + + #endregion + + #region IStreamDeserializer + + /// + public static Models.N3DS.CIA? DeserializeStream(Stream? data) + { + var deserializer = new CIA(); + return deserializer.Deserialize(data); + } + + /// + public Models.N3DS.CIA? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new CIA archive to fill + var cia = new Models.N3DS.CIA(); + + #region CIA Header + + // Try to parse the header + var header = ParseCIAHeader(data); + if (header == null) + return null; + + // Set the CIA archive header + cia.Header = header; + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Certificate Chain + + // Create the certificate chain + cia.CertificateChain = new Certificate[3]; + + // Try to parse the certificates + for (int i = 0; i < 3; i++) + { + var certificate = ParseCertificate(data); + if (certificate == null) + return null; + + cia.CertificateChain[i] = certificate; + } + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Ticket + + // Try to parse the ticket + var ticket = ParseTicket(data); + if (ticket == null) + return null; + + // Set the ticket + cia.Ticket = ticket; + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Title Metadata + + // Try to parse the title metadata + var titleMetadata = ParseTitleMetadata(data); + if (titleMetadata == null) + return null; + + // Set the title metadata + cia.TMDFileData = titleMetadata; + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Content File Data + + // Create the partition table + cia.Partitions = new NCCHHeader[8]; + + // Iterate and build the partitions + for (int i = 0; i < 8; i++) + { + cia.Partitions[i] = N3DS.ParseNCCHHeader(data); + } + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Meta Data + + // If we have a meta data + if (header.MetaSize > 0) + { + // Try to parse the meta + var meta = ParseMetaData(data); + if (meta == null) + return null; + + // Set the meta + cia.MetaData = meta; + } + + #endregion + + return cia; + } + + /// + /// Parse a Stream into a CIA header + /// + /// Stream to parse + /// Filled CIA header on success, null on error + private static CIAHeader ParseCIAHeader(Stream data) + { + // TODO: Use marshalling here instead of building + CIAHeader ciaHeader = new CIAHeader(); + + ciaHeader.HeaderSize = data.ReadUInt32(); + ciaHeader.Type = data.ReadUInt16(); + ciaHeader.Version = data.ReadUInt16(); + ciaHeader.CertificateChainSize = data.ReadUInt32(); + ciaHeader.TicketSize = data.ReadUInt32(); + ciaHeader.TMDFileSize = data.ReadUInt32(); + ciaHeader.MetaSize = data.ReadUInt32(); + ciaHeader.ContentSize = data.ReadUInt64(); + ciaHeader.ContentIndex = data.ReadBytes(0x2000); + + return ciaHeader; + } + + /// + /// Parse a Stream into a certificate + /// + /// Stream to parse + /// Filled certificate on success, null on error + private static Certificate? ParseCertificate(Stream data) + { + // TODO: Use marshalling here instead of building + Certificate certificate = new Certificate(); + + certificate.SignatureType = (SignatureType)data.ReadUInt32(); + switch (certificate.SignatureType) + { + case SignatureType.RSA_4096_SHA1: + certificate.SignatureSize = 0x200; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA1: + certificate.SignatureSize = 0x100; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA1: + certificate.SignatureSize = 0x3C; + certificate.PaddingSize = 0x40; + break; + case SignatureType.RSA_4096_SHA256: + certificate.SignatureSize = 0x200; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA256: + certificate.SignatureSize = 0x100; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA256: + certificate.SignatureSize = 0x3C; + certificate.PaddingSize = 0x40; + break; + default: + return null; + } + + certificate.Signature = data.ReadBytes(certificate.SignatureSize); + certificate.Padding = data.ReadBytes(certificate.PaddingSize); + byte[]? issuer = data.ReadBytes(0x40); + if (issuer != null) + certificate.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0'); + certificate.KeyType = (PublicKeyType)data.ReadUInt32(); + byte[]? name = data.ReadBytes(0x40); + if (name != null) + certificate.Name = Encoding.ASCII.GetString(name).TrimEnd('\0'); + certificate.ExpirationTime = data.ReadUInt32(); + + switch (certificate.KeyType) + { + case PublicKeyType.RSA_4096: + certificate.RSAModulus = data.ReadBytes(0x200); + certificate.RSAPublicExponent = data.ReadUInt32(); + certificate.RSAPadding = data.ReadBytes(0x34); + break; + case PublicKeyType.RSA_2048: + certificate.RSAModulus = data.ReadBytes(0x100); + certificate.RSAPublicExponent = data.ReadUInt32(); + certificate.RSAPadding = data.ReadBytes(0x34); + break; + case PublicKeyType.EllipticCurve: + certificate.ECCPublicKey = data.ReadBytes(0x3C); + certificate.ECCPadding = data.ReadBytes(0x3C); + break; + default: + return null; + } + + return certificate; + } + + /// + /// Parse a Stream into a ticket + /// + /// Stream to parse + /// Indicates if the ticket is from CDN + /// Filled ticket on success, null on error + private static Ticket? ParseTicket(Stream data, bool fromCdn = false) + { + // TODO: Use marshalling here instead of building + Ticket ticket = new Ticket(); + + ticket.SignatureType = (SignatureType)data.ReadUInt32(); + switch (ticket.SignatureType) + { + case SignatureType.RSA_4096_SHA1: + ticket.SignatureSize = 0x200; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA1: + ticket.SignatureSize = 0x100; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA1: + ticket.SignatureSize = 0x3C; + ticket.PaddingSize = 0x40; + break; + case SignatureType.RSA_4096_SHA256: + ticket.SignatureSize = 0x200; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA256: + ticket.SignatureSize = 0x100; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA256: + ticket.SignatureSize = 0x3C; + ticket.PaddingSize = 0x40; + break; + default: + return null; + } + + ticket.Signature = data.ReadBytes(ticket.SignatureSize); + ticket.Padding = data.ReadBytes(ticket.PaddingSize); + byte[]? issuer = data.ReadBytes(0x40); + if (issuer != null) + ticket.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0'); + ticket.ECCPublicKey = data.ReadBytes(0x3C); + ticket.Version = data.ReadByteValue(); + ticket.CaCrlVersion = data.ReadByteValue(); + ticket.SignerCrlVersion = data.ReadByteValue(); + ticket.TitleKey = data.ReadBytes(0x10); + ticket.Reserved1 = data.ReadByteValue(); + ticket.TicketID = data.ReadUInt64(); + ticket.ConsoleID = data.ReadUInt32(); + ticket.TitleID = data.ReadUInt64(); + ticket.Reserved2 = data.ReadBytes(2); + ticket.TicketTitleVersion = data.ReadUInt16(); + ticket.Reserved3 = data.ReadBytes(8); + ticket.LicenseType = data.ReadByteValue(); + ticket.CommonKeyYIndex = data.ReadByteValue(); + ticket.Reserved4 = data.ReadBytes(0x2A); + ticket.eShopAccountID = data.ReadUInt32(); + ticket.Reserved5 = data.ReadByteValue(); + ticket.Audit = data.ReadByteValue(); + ticket.Reserved6 = data.ReadBytes(0x42); + ticket.Limits = new uint[0x10]; + for (int i = 0; i < ticket.Limits.Length; i++) + { + ticket.Limits[i] = data.ReadUInt32(); + } + + // Seek to the content index size + data.Seek(4, SeekOrigin.Current); + + // Read the size (big-endian) + byte[]? contentIndexSize = data.ReadBytes(4); + if (contentIndexSize != null) + { + Array.Reverse(contentIndexSize); + ticket.ContentIndexSize = BitConverter.ToUInt32(contentIndexSize, 0); + } + + // Seek back to the start of the content index + data.Seek(-8, SeekOrigin.Current); + + ticket.ContentIndex = data.ReadBytes((int)ticket.ContentIndexSize); + + // Certificates only exist in standalone CETK files + if (fromCdn) + { + ticket.CertificateChain = new Certificate[2]; + for (int i = 0; i < 2; i++) + { + var certificate = ParseCertificate(data); + if (certificate == null) + return null; + + ticket.CertificateChain[i] = certificate; + } + } + + return ticket; + } + + /// + /// Parse a Stream into a title metadata + /// + /// Stream to parse + /// Indicates if the ticket is from CDN + /// Filled title metadata on success, null on error + private static TitleMetadata? ParseTitleMetadata(Stream data, bool fromCdn = false) + { + // TODO: Use marshalling here instead of building + TitleMetadata titleMetadata = new TitleMetadata(); + + titleMetadata.SignatureType = (SignatureType)data.ReadUInt32(); + switch (titleMetadata.SignatureType) + { + case SignatureType.RSA_4096_SHA1: + titleMetadata.SignatureSize = 0x200; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA1: + titleMetadata.SignatureSize = 0x100; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA1: + titleMetadata.SignatureSize = 0x3C; + titleMetadata.PaddingSize = 0x40; + break; + case SignatureType.RSA_4096_SHA256: + titleMetadata.SignatureSize = 0x200; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA256: + titleMetadata.SignatureSize = 0x100; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA256: + titleMetadata.SignatureSize = 0x3C; + titleMetadata.PaddingSize = 0x40; + break; + default: + return null; + } + + titleMetadata.Signature = data.ReadBytes(titleMetadata.SignatureSize); + titleMetadata.Padding1 = data.ReadBytes(titleMetadata.PaddingSize); + byte[]? issuer = data.ReadBytes(0x40); + if (issuer != null) + titleMetadata.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0'); + titleMetadata.Version = data.ReadByteValue(); + titleMetadata.CaCrlVersion = data.ReadByteValue(); + titleMetadata.SignerCrlVersion = data.ReadByteValue(); + titleMetadata.Reserved1 = data.ReadByteValue(); + titleMetadata.SystemVersion = data.ReadUInt64(); + titleMetadata.TitleID = data.ReadUInt64(); + titleMetadata.TitleType = data.ReadUInt32(); + titleMetadata.GroupID = data.ReadUInt16(); + titleMetadata.SaveDataSize = data.ReadUInt32(); + titleMetadata.SRLPrivateSaveDataSize = data.ReadUInt32(); + titleMetadata.Reserved2 = data.ReadBytes(4); + titleMetadata.SRLFlag = data.ReadByteValue(); + titleMetadata.Reserved3 = data.ReadBytes(0x31); + titleMetadata.AccessRights = data.ReadUInt32(); + titleMetadata.TitleVersion = data.ReadUInt16(); + + // Read the content count (big-endian) + byte[]? contentCount = data.ReadBytes(2); + if (contentCount != null) + { + Array.Reverse(contentCount); + titleMetadata.ContentCount = BitConverter.ToUInt16(contentCount, 0); + } + + titleMetadata.BootContent = data.ReadUInt16(); + titleMetadata.Padding2 = data.ReadBytes(2); + titleMetadata.SHA256HashContentInfoRecords = data.ReadBytes(0x20); + titleMetadata.ContentInfoRecords = new ContentInfoRecord[64]; + for (int i = 0; i < 64; i++) + { + titleMetadata.ContentInfoRecords[i] = ParseContentInfoRecord(data); + } + titleMetadata.ContentChunkRecords = new ContentChunkRecord[titleMetadata.ContentCount]; + for (int i = 0; i < titleMetadata.ContentCount; i++) + { + titleMetadata.ContentChunkRecords[i] = ParseContentChunkRecord(data); + } + + // Certificates only exist in standalone TMD files + if (fromCdn) + { + titleMetadata.CertificateChain = new Certificate[2]; + for (int i = 0; i < 2; i++) + { + var certificate = ParseCertificate(data); + if (certificate == null) + return null; + + titleMetadata.CertificateChain[i] = certificate; + } + } + + return titleMetadata; + } + + /// + /// Parse a Stream into a content info record + /// + /// Stream to parse + /// Filled content info record on success, null on error + private static ContentInfoRecord ParseContentInfoRecord(Stream data) + { + // TODO: Use marshalling here instead of building + ContentInfoRecord contentInfoRecord = new ContentInfoRecord(); + + contentInfoRecord.ContentIndexOffset = data.ReadUInt16(); + contentInfoRecord.ContentCommandCount = data.ReadUInt16(); + contentInfoRecord.UnhashedContentRecordsSHA256Hash = data.ReadBytes(0x20); + + return contentInfoRecord; + } + + /// + /// Parse a Stream into a content chunk record + /// + /// Stream to parse + /// Filled content chunk record on success, null on error + private static ContentChunkRecord ParseContentChunkRecord(Stream data) + { + // TODO: Use marshalling here instead of building + ContentChunkRecord contentChunkRecord = new ContentChunkRecord(); + + contentChunkRecord.ContentId = data.ReadUInt32(); + contentChunkRecord.ContentIndex = (ContentIndex)data.ReadUInt16(); + contentChunkRecord.ContentType = (TMDContentType)data.ReadUInt16(); + contentChunkRecord.ContentSize = data.ReadUInt64(); + contentChunkRecord.SHA256Hash = data.ReadBytes(0x20); + + return contentChunkRecord; + } + + /// + /// Parse a Stream into a meta data + /// + /// Stream to parse + /// Filled meta data on success, null on error + private static MetaData ParseMetaData(Stream data) + { + // TODO: Use marshalling here instead of building + MetaData metaData = new MetaData(); + + metaData.TitleIDDependencyList = data.ReadBytes(0x180); + metaData.Reserved1 = data.ReadBytes(0x180); + metaData.CoreVersion = data.ReadUInt32(); + metaData.Reserved2 = data.ReadBytes(0xFC); + metaData.IconData = data.ReadBytes(0x36C0); + + return metaData; } #endregion diff --git a/SabreTools.Serialization/Deserializers/Catalog.cs b/SabreTools.Serialization/Deserializers/Catalog.cs index 5f1ebcac..54de815d 100644 --- a/SabreTools.Serialization/Deserializers/Catalog.cs +++ b/SabreTools.Serialization/Deserializers/Catalog.cs @@ -1,22 +1,39 @@ -using System.Text; +using System.IO; +using System.Text; namespace SabreTools.Serialization.Deserializers { - public class Catalog : JsonFile + public class Catalog : + JsonFile { #region IFileDeserializer - /// + /// public static Models.Xbox.Catalog? DeserializeFile(string? path) { var deserializer = new Catalog(); return deserializer.Deserialize(path); } - // Catalog.js file is a UTF-16 LE JSON + /// Catalog.js file is encoded as UTF-16 LE public override Models.Xbox.Catalog? Deserialize(string? path) => Deserialize(path, new UnicodeEncoding()); #endregion + + #region IStreamDeserializer + + /// + public static Models.Xbox.Catalog? DeserializeStream(Stream? data) + { + var deserializer = new Catalog(); + return deserializer.Deserialize(data); + } + + /// Catalog.js file is encoded as UTF-16 LE + public override Models.Xbox.Catalog? Deserialize(Stream? data) + => Deserialize(data, new UnicodeEncoding()); + + #endregion } } diff --git a/SabreTools.Serialization/Deserializers/ClrMamePro.cs b/SabreTools.Serialization/Deserializers/ClrMamePro.cs index 3a47ee62..db64c365 100644 --- a/SabreTools.Serialization/Deserializers/ClrMamePro.cs +++ b/SabreTools.Serialization/Deserializers/ClrMamePro.cs @@ -1,27 +1,929 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.IO.Readers; +using SabreTools.Models.ClrMamePro; using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Deserializers { - public class ClrMamePro : IFileDeserializer + public class ClrMamePro : + IFileDeserializer, + IStreamDeserializer { #region IFileDeserializer /// - public static Models.ClrMamePro.MetadataFile? DeserializeFile(string? path, bool quotes = true) + public static MetadataFile? DeserializeFile(string? path, bool quotes = true) { var deserializer = new ClrMamePro(); return deserializer.Deserialize(path, quotes); } /// - public Models.ClrMamePro.MetadataFile? Deserialize(string? path) + public MetadataFile? Deserialize(string? path) => Deserialize(path, true); /// - public Models.ClrMamePro.MetadataFile? Deserialize(string? path, bool quotes) + public MetadataFile? Deserialize(string? path, bool quotes) { using var stream = PathProcessor.OpenStream(path); - return Streams.ClrMamePro.DeserializeStream(stream, quotes); + return DeserializeStream(stream, quotes); + } + + #endregion + + #region IStreamDeserializer + + /// + public static MetadataFile? DeserializeStream(Stream? data, bool quotes = true) + { + var deserializer = new ClrMamePro(); + return deserializer.Deserialize(data, quotes); + } + + /// + public MetadataFile? Deserialize(Stream? data) + => Deserialize(data, true); + + /// + public MetadataFile? Deserialize(Stream? data, bool quotes) + { + // If the stream is null + if (data == null) + return default; + + // Setup the reader and output + var reader = new ClrMameProReader(data, Encoding.UTF8) { Quotes = quotes }; + var dat = new MetadataFile(); + + // Loop through and parse out the values + string? lastTopLevel = reader.TopLevel; + + GameBase? game = null; + var games = new List(); + var releases = new List(); + var biosSets = new List(); + var roms = new List(); + var disks = new List(); + var medias = new List(); + var samples = new List(); + var archives = new List(); + var chips = new List(); + var videos = new List