mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-24 15:55:05 +00:00
Move stream deserializers to new organization
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
@@ -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<Models.AACS.MediaKeyBlock>,
|
||||
IFileDeserializer<Models.AACS.MediaKeyBlock>
|
||||
IByteDeserializer<MediaKeyBlock>,
|
||||
IFileDeserializer<MediaKeyBlock>,
|
||||
IStreamDeserializer<MediaKeyBlock>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.AACS.MediaKeyBlock? DeserializeFile(string? path)
|
||||
public static MediaKeyBlock? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new AACS();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MediaKeyBlock? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new AACS();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<Record>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an end of media key block record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled end of media key block record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an explicit subset-difference record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled explicit subset-difference record on success, null on error</returns>
|
||||
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<SubsetDifference>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a media key data record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled media key data record on success, null on error</returns>
|
||||
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<byte[]>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a subset-difference index record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled subset-difference index record on success, null on error</returns>
|
||||
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<uint>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a type and version record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled type and version record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a drive revocation list record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled drive revocation list record on success, null on error</returns>
|
||||
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<DriveRevocationSignatureBlock>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a host revocation list record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled host revocation list record on success, null on error</returns>
|
||||
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<HostRevocationSignatureBlock>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a verify media key record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled verify media key record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a copyright record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled copyright record on success, null on error</returns>
|
||||
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
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class ArchiveDotOrg : XmlFile<Models.ArchiveDotOrg.Files>
|
||||
public class ArchiveDotOrg :
|
||||
XmlFile<Models.ArchiveDotOrg.Files>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.ArchiveDotOrg.Files? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new ArchiveDotOrg();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.ArchiveDotOrg.Files? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new ArchiveDotOrg();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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<Models.AttractMode.MetadataFile>
|
||||
public class AttractMode :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.AttractMode.MetadataFile? DeserializeFile(string? path)
|
||||
public static MetadataFile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new AttractMode();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new AttractMode();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<Row>();
|
||||
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
|
||||
|
||||
@@ -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<Models.BDPlus.SVM>,
|
||||
IFileDeserializer<Models.BDPlus.SVM>
|
||||
IByteDeserializer<SVM>,
|
||||
IFileDeserializer<SVM>,
|
||||
IStreamDeserializer<SVM>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.BDPlus.SVM? DeserializeFile(string? path)
|
||||
public static SVM? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new BDPlus();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static SVM? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new BDPlus();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SVM
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SVM on success, null on error</returns>
|
||||
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
|
||||
|
||||
@@ -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<Models.BFPK.Archive>,
|
||||
IFileDeserializer<Models.BFPK.Archive>
|
||||
IByteDeserializer<Archive>,
|
||||
IFileDeserializer<Archive>,
|
||||
IStreamDeserializer<Archive>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.BFPK.Archive? DeserializeFile(string? path)
|
||||
public static Archive? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new BFPK();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new BFPK();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled file entry on success, null on error</returns>
|
||||
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
|
||||
|
||||
@@ -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<Models.BSP.File>,
|
||||
IFileDeserializer<Models.BSP.File>
|
||||
IFileDeserializer<Models.BSP.File>,
|
||||
IStreamDeserializer<Models.BSP.File>
|
||||
{
|
||||
#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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.BSP.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new BSP();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Level header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Level header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a lump
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled lump on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Level texture header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Level texture header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a texture
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="mipmap">Mipmap level</param>
|
||||
/// <returns>Filled texture on success, null on error</returns>
|
||||
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
|
||||
|
||||
@@ -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<Models.CFB.Binary>,
|
||||
IFileDeserializer<Models.CFB.Binary>
|
||||
IByteDeserializer<Binary>,
|
||||
IFileDeserializer<Binary>,
|
||||
IStreamDeserializer<Binary>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.CFB.Binary? DeserializeFile(string? path)
|
||||
public static Binary? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new CFB();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Binary? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new CFB();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<SectorNumber?>();
|
||||
|
||||
// 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<SectorNumber?>();
|
||||
|
||||
// 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<SectorNumber?>();
|
||||
|
||||
// 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<DirectoryEntry>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled file header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a sector full of sector numbers
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="sectorShift">Sector shift from the header</param>
|
||||
/// <returns>Filled sector full of sector numbers on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a sector full of directory entries
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="sectorShift">Sector shift from the header</param>
|
||||
/// <param name="majorVersion">Major version from the header</param>
|
||||
/// <returns>Filled sector full of directory entries on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version from the header</param>
|
||||
/// <returns>Filled directory entry on success, null on error</returns>
|
||||
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
|
||||
|
||||
@@ -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<Models.N3DS.CIA>,
|
||||
IFileDeserializer<Models.N3DS.CIA>
|
||||
IFileDeserializer<Models.N3DS.CIA>,
|
||||
IStreamDeserializer<Models.N3DS.CIA>
|
||||
{
|
||||
#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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.N3DS.CIA? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new CIA();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a CIA header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled CIA header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a certificate
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled certificate on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a ticket
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="fromCdn">Indicates if the ticket is from CDN</param>
|
||||
/// <returns>Filled ticket on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a title metadata
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="fromCdn">Indicates if the ticket is from CDN</param>
|
||||
/// <returns>Filled title metadata on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a content info record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled content info record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a content chunk record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled content chunk record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a meta data
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled meta data on success, null on error</returns>
|
||||
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
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Catalog : JsonFile<Models.Xbox.Catalog>
|
||||
public class Catalog :
|
||||
JsonFile<Models.Xbox.Catalog>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
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
|
||||
/// <remarks>Catalog.js file is encoded as UTF-16 LE</remarks>
|
||||
public override Models.Xbox.Catalog? Deserialize(string? path)
|
||||
=> Deserialize(path, new UnicodeEncoding());
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.Xbox.Catalog? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Catalog();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <remarks>Catalog.js file is encoded as UTF-16 LE</remarks>
|
||||
public override Models.Xbox.Catalog? Deserialize(Stream? data)
|
||||
=> Deserialize(data, new UnicodeEncoding());
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Models.ClrMamePro.MetadataFile>
|
||||
public class ClrMamePro :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.ClrMamePro.MetadataFile? Deserialize(string? path)
|
||||
public MetadataFile? Deserialize(string? path)
|
||||
=> Deserialize(path, true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data, bool quotes = true)
|
||||
{
|
||||
var deserializer = new ClrMamePro();
|
||||
return deserializer.Deserialize(data, quotes);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, true);
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
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<GameBase>();
|
||||
var releases = new List<Release>();
|
||||
var biosSets = new List<BiosSet>();
|
||||
var roms = new List<Rom>();
|
||||
var disks = new List<Disk>();
|
||||
var medias = new List<Media>();
|
||||
var samples = new List<Sample>();
|
||||
var archives = new List<Archive>();
|
||||
var chips = new List<Chip>();
|
||||
var videos = new List<Video>();
|
||||
var dipSwitches = new List<DipSwitch>();
|
||||
|
||||
var additional = new List<string>();
|
||||
var headerAdditional = new List<string>();
|
||||
var gameAdditional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Ignore certain row types
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case CmpRowType.None:
|
||||
case CmpRowType.Comment:
|
||||
continue;
|
||||
case CmpRowType.EndTopLevel:
|
||||
switch (lastTopLevel)
|
||||
{
|
||||
case "doscenter":
|
||||
if (dat.ClrMamePro != null)
|
||||
dat.ClrMamePro.ADDITIONAL_ELEMENTS = [.. headerAdditional];
|
||||
|
||||
headerAdditional.Clear();
|
||||
break;
|
||||
case "game":
|
||||
case "machine":
|
||||
case "resource":
|
||||
case "set":
|
||||
if (game != null)
|
||||
{
|
||||
game.Release = [.. releases];
|
||||
game.BiosSet = [.. biosSets];
|
||||
game.Rom = [.. roms];
|
||||
game.Disk = [.. disks];
|
||||
game.Media = [.. medias];
|
||||
game.Sample = [.. samples];
|
||||
game.Archive = [.. archives];
|
||||
game.Chip = [.. chips];
|
||||
game.Video = [.. videos];
|
||||
game.DipSwitch = [.. dipSwitches];
|
||||
game.ADDITIONAL_ELEMENTS = [.. gameAdditional];
|
||||
|
||||
games.Add(game);
|
||||
game = null;
|
||||
}
|
||||
|
||||
releases.Clear();
|
||||
biosSets.Clear();
|
||||
roms.Clear();
|
||||
disks.Clear();
|
||||
medias.Clear();
|
||||
samples.Clear();
|
||||
archives.Clear();
|
||||
chips.Clear();
|
||||
videos.Clear();
|
||||
dipSwitches.Clear();
|
||||
gameAdditional.Clear();
|
||||
break;
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're at the root
|
||||
if (reader.RowType == CmpRowType.TopLevel)
|
||||
{
|
||||
lastTopLevel = reader.TopLevel;
|
||||
switch (reader.TopLevel)
|
||||
{
|
||||
case "clrmamepro":
|
||||
dat.ClrMamePro = new Models.ClrMamePro.ClrMamePro();
|
||||
break;
|
||||
case "game":
|
||||
game = new Game();
|
||||
break;
|
||||
case "machine":
|
||||
game = new Machine();
|
||||
break;
|
||||
case "resource":
|
||||
game = new Resource();
|
||||
break;
|
||||
case "set":
|
||||
game = new Set();
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in the doscenter block
|
||||
else if (reader.TopLevel == "clrmamepro"
|
||||
&& reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
dat.ClrMamePro ??= new Models.ClrMamePro.ClrMamePro();
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
dat.ClrMamePro.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
case "description":
|
||||
dat.ClrMamePro.Description = reader.Standalone?.Value;
|
||||
break;
|
||||
case "rootdir":
|
||||
dat.ClrMamePro.RootDir = reader.Standalone?.Value;
|
||||
break;
|
||||
case "category":
|
||||
dat.ClrMamePro.Category = reader.Standalone?.Value;
|
||||
break;
|
||||
case "version":
|
||||
dat.ClrMamePro.Version = reader.Standalone?.Value;
|
||||
break;
|
||||
case "date":
|
||||
dat.ClrMamePro.Date = reader.Standalone?.Value;
|
||||
break;
|
||||
case "author":
|
||||
dat.ClrMamePro.Author = reader.Standalone?.Value;
|
||||
break;
|
||||
case "homepage":
|
||||
dat.ClrMamePro.Homepage = reader.Standalone?.Value;
|
||||
break;
|
||||
case "url":
|
||||
dat.ClrMamePro.Url = reader.Standalone?.Value;
|
||||
break;
|
||||
case "comment":
|
||||
dat.ClrMamePro.Comment = reader.Standalone?.Value;
|
||||
break;
|
||||
case "header":
|
||||
dat.ClrMamePro.Header = reader.Standalone?.Value;
|
||||
break;
|
||||
case "type":
|
||||
dat.ClrMamePro.Type = reader.Standalone?.Value;
|
||||
break;
|
||||
case "forcemerging":
|
||||
dat.ClrMamePro.ForceMerging = reader.Standalone?.Value;
|
||||
break;
|
||||
case "forcezipping":
|
||||
dat.ClrMamePro.ForceZipping = reader.Standalone?.Value;
|
||||
break;
|
||||
case "forcepacking":
|
||||
dat.ClrMamePro.ForcePacking = reader.Standalone?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
headerAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a game, machine, resource, or set block
|
||||
else if ((reader.TopLevel == "game"
|
||||
|| reader.TopLevel == "machine"
|
||||
|| reader.TopLevel == "resource"
|
||||
|| reader.TopLevel == "set")
|
||||
&& reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
game ??= reader.TopLevel switch
|
||||
{
|
||||
"game" => new Game(),
|
||||
"machine" => new Machine(),
|
||||
"resource" => new Resource(),
|
||||
"set" => new Set(),
|
||||
_ => throw new FormatException($"Unknown top-level block: {reader.TopLevel}"),
|
||||
};
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
game.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
case "description":
|
||||
game.Description = reader.Standalone?.Value;
|
||||
break;
|
||||
case "year":
|
||||
game.Year = reader.Standalone?.Value;
|
||||
break;
|
||||
case "manufacturer":
|
||||
game.Manufacturer = reader.Standalone?.Value;
|
||||
break;
|
||||
case "category":
|
||||
game.Category = reader.Standalone?.Value;
|
||||
break;
|
||||
case "cloneof":
|
||||
game.CloneOf = reader.Standalone?.Value;
|
||||
break;
|
||||
case "romof":
|
||||
game.RomOf = reader.Standalone?.Value;
|
||||
break;
|
||||
case "sampleof":
|
||||
game.SampleOf = reader.Standalone?.Value;
|
||||
break;
|
||||
case "sample":
|
||||
var sample = new Sample
|
||||
{
|
||||
Name = reader.Standalone?.Value ?? string.Empty,
|
||||
ADDITIONAL_ELEMENTS = [],
|
||||
};
|
||||
samples.Add(sample);
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in an item block
|
||||
else if ((reader.TopLevel == "game"
|
||||
|| reader.TopLevel == "machine"
|
||||
|| reader.TopLevel == "resource"
|
||||
|| reader.TopLevel == "set")
|
||||
&& game != null
|
||||
&& reader.RowType == CmpRowType.Internal)
|
||||
{
|
||||
// Create the block
|
||||
switch (reader.InternalName)
|
||||
{
|
||||
case "release":
|
||||
var release = CreateRelease(reader);
|
||||
if (release != null)
|
||||
releases.Add(release);
|
||||
break;
|
||||
case "biosset":
|
||||
var biosSet = CreateBiosSet(reader);
|
||||
if (biosSet != null)
|
||||
biosSets.Add(biosSet);
|
||||
break;
|
||||
case "rom":
|
||||
var rom = CreateRom(reader);
|
||||
if (rom != null)
|
||||
roms.Add(rom);
|
||||
break;
|
||||
case "disk":
|
||||
var disk = CreateDisk(reader);
|
||||
if (disk != null)
|
||||
disks.Add(disk);
|
||||
break;
|
||||
case "media":
|
||||
var media = CreateMedia(reader);
|
||||
if (media != null)
|
||||
medias.Add(media);
|
||||
break;
|
||||
case "sample":
|
||||
var sample = CreateSample(reader);
|
||||
if (sample != null)
|
||||
samples.Add(sample);
|
||||
break;
|
||||
case "archive":
|
||||
var archive = CreateArchive(reader);
|
||||
if (archive != null)
|
||||
archives.Add(archive);
|
||||
break;
|
||||
case "chip":
|
||||
var chip = CreateChip(reader);
|
||||
if (chip != null)
|
||||
chips.Add(chip);
|
||||
break;
|
||||
case "video":
|
||||
var video = CreateVideo(reader);
|
||||
if (video != null)
|
||||
videos.Add(video);
|
||||
break;
|
||||
case "sound":
|
||||
var sound = CreateSound(reader);
|
||||
if (sound != null)
|
||||
game.Sound = sound;
|
||||
break;
|
||||
case "input":
|
||||
var input = CreateInput(reader);
|
||||
if (input != null)
|
||||
game.Input = input;
|
||||
break;
|
||||
case "dipswitch":
|
||||
var dipSwitch = CreateDipSwitch(reader);
|
||||
if (dipSwitch != null)
|
||||
dipSwitches.Add(dipSwitch);
|
||||
break;
|
||||
case "driver":
|
||||
var driver = CreateDriver(reader);
|
||||
if (driver != null)
|
||||
game.Driver = driver;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(reader.CurrentLine);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.Game = [.. games];
|
||||
dat.ADDITIONAL_ELEMENTS = [.. additional];
|
||||
return dat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Release object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Release object created from the reader context</returns>
|
||||
private static Release? CreateRelease(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var release = new Release();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
release.Name = kvp.Value;
|
||||
break;
|
||||
case "region":
|
||||
release.Region = kvp.Value;
|
||||
break;
|
||||
case "language":
|
||||
release.Language = kvp.Value;
|
||||
break;
|
||||
case "date":
|
||||
release.Date = kvp.Value;
|
||||
break;
|
||||
case "default":
|
||||
release.Default = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
release.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return release;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a BiosSet object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>BiosSet object created from the reader context</returns>
|
||||
private static BiosSet? CreateBiosSet(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var biosset = new BiosSet();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
biosset.Name = kvp.Value;
|
||||
break;
|
||||
case "description":
|
||||
biosset.Description = kvp.Value;
|
||||
break;
|
||||
case "default":
|
||||
biosset.Default = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
biosset.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return biosset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Rom object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Rom object created from the reader context</returns>
|
||||
private static Rom? CreateRom(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var rom = new Rom();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
rom.Name = kvp.Value;
|
||||
break;
|
||||
case "size":
|
||||
rom.Size = kvp.Value;
|
||||
break;
|
||||
case "crc":
|
||||
rom.CRC = kvp.Value;
|
||||
break;
|
||||
case "md5":
|
||||
rom.MD5 = kvp.Value;
|
||||
break;
|
||||
case "sha1":
|
||||
rom.SHA1 = kvp.Value;
|
||||
break;
|
||||
case "sha256":
|
||||
rom.SHA256 = kvp.Value;
|
||||
break;
|
||||
case "sha384":
|
||||
rom.SHA384 = kvp.Value;
|
||||
break;
|
||||
case "sha512":
|
||||
rom.SHA512 = kvp.Value;
|
||||
break;
|
||||
case "spamsum":
|
||||
rom.SpamSum = kvp.Value;
|
||||
break;
|
||||
case "xxh3_64":
|
||||
rom.xxHash364 = kvp.Value;
|
||||
break;
|
||||
case "xxh3_128":
|
||||
rom.xxHash3128 = kvp.Value;
|
||||
break;
|
||||
case "merge":
|
||||
rom.Merge = kvp.Value;
|
||||
break;
|
||||
case "status":
|
||||
rom.Status = kvp.Value;
|
||||
break;
|
||||
case "region":
|
||||
rom.Region = kvp.Value;
|
||||
break;
|
||||
case "flags":
|
||||
rom.Flags = kvp.Value;
|
||||
break;
|
||||
case "offs":
|
||||
rom.Offs = kvp.Value;
|
||||
break;
|
||||
case "serial":
|
||||
rom.Serial = kvp.Value;
|
||||
break;
|
||||
case "header":
|
||||
rom.Header = kvp.Value;
|
||||
break;
|
||||
case "date":
|
||||
rom.Date = kvp.Value;
|
||||
break;
|
||||
case "inverted":
|
||||
rom.Inverted = kvp.Value;
|
||||
break;
|
||||
case "mia":
|
||||
rom.MIA = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
rom.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return rom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Disk object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Disk object created from the reader context</returns>
|
||||
private static Disk? CreateDisk(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var disk = new Disk();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
disk.Name = kvp.Value;
|
||||
break;
|
||||
case "md5":
|
||||
disk.MD5 = kvp.Value;
|
||||
break;
|
||||
case "sha1":
|
||||
disk.SHA1 = kvp.Value;
|
||||
break;
|
||||
case "merge":
|
||||
disk.Merge = kvp.Value;
|
||||
break;
|
||||
case "status":
|
||||
disk.Status = kvp.Value;
|
||||
break;
|
||||
case "flags":
|
||||
disk.Flags = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
disk.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return disk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Media object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Media object created from the reader context</returns>
|
||||
private static Media? CreateMedia(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var media = new Media();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
media.Name = kvp.Value;
|
||||
break;
|
||||
case "md5":
|
||||
media.MD5 = kvp.Value;
|
||||
break;
|
||||
case "sha1":
|
||||
media.SHA1 = kvp.Value;
|
||||
break;
|
||||
case "sha256":
|
||||
media.SHA256 = kvp.Value;
|
||||
break;
|
||||
case "spamsum":
|
||||
media.SpamSum = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
media.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return media;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Sample object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Sample object created from the reader context</returns>
|
||||
private static Sample? CreateSample(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var sample = new Sample();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
sample.Name = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sample.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return sample;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Archive object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Archive object created from the reader context</returns>
|
||||
private static Archive? CreateArchive(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var archive = new Archive();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
archive.Name = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
archive.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Chip object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Chip object created from the reader context</returns>
|
||||
private static Chip? CreateChip(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var chip = new Chip();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "type":
|
||||
chip.Type = kvp.Value;
|
||||
break;
|
||||
case "name":
|
||||
chip.Name = kvp.Value;
|
||||
break;
|
||||
case "flags":
|
||||
chip.Flags = kvp.Value;
|
||||
break;
|
||||
case "clock":
|
||||
chip.Clock = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chip.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return chip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Video object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Video object created from the reader context</returns>
|
||||
private static Video? CreateVideo(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var video = new Video();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "screen":
|
||||
video.Screen = kvp.Value;
|
||||
break;
|
||||
case "orientation":
|
||||
video.Orientation = kvp.Value;
|
||||
break;
|
||||
case "x":
|
||||
video.X = kvp.Value;
|
||||
break;
|
||||
case "y":
|
||||
video.Y = kvp.Value;
|
||||
break;
|
||||
case "aspectx":
|
||||
video.AspectX = kvp.Value;
|
||||
break;
|
||||
case "aspecty":
|
||||
video.AspectY = kvp.Value;
|
||||
break;
|
||||
case "freq":
|
||||
video.Freq = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
video.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Sound object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Sound object created from the reader context</returns>
|
||||
private static Sound? CreateSound(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var sound = new Sound();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "channels":
|
||||
sound.Channels = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sound.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return sound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Input object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Input object created from the reader context</returns>
|
||||
private static Input? CreateInput(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var input = new Input();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "players":
|
||||
input.Players = kvp.Value;
|
||||
break;
|
||||
case "control":
|
||||
input.Control = kvp.Value;
|
||||
break;
|
||||
case "buttons":
|
||||
input.Buttons = kvp.Value;
|
||||
break;
|
||||
case "coins":
|
||||
input.Coins = kvp.Value;
|
||||
break;
|
||||
case "tilt":
|
||||
input.Tilt = kvp.Value;
|
||||
break;
|
||||
case "service":
|
||||
input.Service = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
input.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return input;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a DipSwitch object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>DipSwitch object created from the reader context</returns>
|
||||
private static DipSwitch? CreateDipSwitch(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var dipswitch = new DipSwitch();
|
||||
var entries = new List<string>();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
dipswitch.Name = kvp.Value;
|
||||
break;
|
||||
case "entry":
|
||||
entries.Add(kvp.Value);
|
||||
break;
|
||||
case "default":
|
||||
dipswitch.Default = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dipswitch.Entry = [.. entries];
|
||||
dipswitch.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return dipswitch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Driver object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Driver object created from the reader context</returns>
|
||||
private static Driver? CreateDriver(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var driver = new Driver();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "status":
|
||||
driver.Status = kvp.Value;
|
||||
break;
|
||||
case "color":
|
||||
driver.Color = kvp.Value;
|
||||
break;
|
||||
case "sound":
|
||||
driver.Sound = kvp.Value;
|
||||
break;
|
||||
case "palettesize":
|
||||
driver.PaletteSize = kvp.Value;
|
||||
break;
|
||||
case "blit":
|
||||
driver.Blit = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
driver.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return driver;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.CueSheets;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class CueSheet : IFileDeserializer<Models.CueSheets.CueSheet>
|
||||
public class CueSheet :
|
||||
IFileDeserializer<Models.CueSheets.CueSheet>,
|
||||
IStreamDeserializer<Models.CueSheets.CueSheet>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
@@ -17,9 +26,600 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.CueSheets.CueSheet? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.CueSheet.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.CueSheets.CueSheet? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new CueSheet();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.CueSheets.CueSheet? 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 cuesheet to fill
|
||||
var cueSheet = new Models.CueSheets.CueSheet();
|
||||
var cueFiles = new List<CueFile>();
|
||||
|
||||
// Read the next line from the input
|
||||
string? lastLine = null;
|
||||
while (true)
|
||||
{
|
||||
string? line = lastLine ?? data.ReadQuotedString();
|
||||
lastLine = null;
|
||||
|
||||
// If we have a null line, break from the loop
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
// If we have an empty line, we skip
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
|
||||
string[] splitLine = Regex
|
||||
.Matches(line, @"[^\s""]+|""[^""]*""")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
switch (splitLine[0])
|
||||
{
|
||||
// Read comments
|
||||
case "REM":
|
||||
// We ignore all comments for now
|
||||
break;
|
||||
|
||||
// Read MCN
|
||||
case "CATALOG":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"CATALOG line malformed: {line}");
|
||||
|
||||
cueSheet.Catalog = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read external CD-Text file path
|
||||
case "CDTEXTFILE":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"CDTEXTFILE line malformed: {line}");
|
||||
|
||||
cueSheet.CdTextFile = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced performer
|
||||
case "PERFORMER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"PERFORMER line malformed: {line}");
|
||||
|
||||
cueSheet.Performer = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced songwriter
|
||||
case "SONGWRITER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"SONGWRITER line malformed: {line}");
|
||||
|
||||
cueSheet.Songwriter = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced title
|
||||
case "TITLE":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"TITLE line malformed: {line}");
|
||||
|
||||
cueSheet.Title = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read file information
|
||||
case "FILE":
|
||||
if (splitLine.Length < 3)
|
||||
throw new FormatException($"FILE line malformed: {line}");
|
||||
|
||||
var file = CreateCueFile(splitLine[1], splitLine[2], data, out lastLine);
|
||||
if (file == default)
|
||||
throw new FormatException($"FILE line malformed: {line}");
|
||||
|
||||
cueFiles.Add(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cueSheet.Files = [.. cueFiles];
|
||||
return cueSheet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill a FILE from an array of lines
|
||||
/// </summary>
|
||||
/// <param name="fileName">File name to set</param>
|
||||
/// <param name="fileType">File type to set</param>
|
||||
/// <param name="data">Stream to pull from</param>
|
||||
private static CueFile? CreateCueFile(string fileName, string fileType, Stream data, out string? lastLine)
|
||||
{
|
||||
// Check the required parameters
|
||||
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (data.Position < 0 || data.Position >= data.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
// Create the holding objects
|
||||
lastLine = null;
|
||||
var cueFile = new CueFile();
|
||||
var cueTracks = new List<CueTrack>();
|
||||
|
||||
// Set the current fields
|
||||
cueFile.FileName = fileName.Trim('"');
|
||||
cueFile.FileType = GetFileType(fileType);
|
||||
|
||||
while (true)
|
||||
{
|
||||
string? line = lastLine ?? data.ReadQuotedString();
|
||||
lastLine = null;
|
||||
|
||||
// If we have a null line, break from the loop
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
// If we have an empty line, we skip
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
|
||||
string[] splitLine = Regex
|
||||
.Matches(line, @"[^\s""]+|""[^""]*""")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
switch (splitLine[0])
|
||||
{
|
||||
// Read comments
|
||||
case "REM":
|
||||
// We ignore all comments for now
|
||||
break;
|
||||
|
||||
// Read track information
|
||||
case "TRACK":
|
||||
if (splitLine.Length < 3)
|
||||
throw new FormatException($"TRACK line malformed: {line}");
|
||||
|
||||
var track = CreateCueTrack(splitLine[1], splitLine[2], data, out lastLine);
|
||||
if (track == default)
|
||||
throw new FormatException($"TRACK line malformed: {line}");
|
||||
|
||||
cueTracks.Add(track);
|
||||
break;
|
||||
|
||||
// Next file found, return
|
||||
case "FILE":
|
||||
lastLine = line;
|
||||
cueFile.Tracks = [.. cueTracks];
|
||||
return cueFile;
|
||||
|
||||
// Default means return
|
||||
default:
|
||||
lastLine = line;
|
||||
cueFile.Tracks = [.. cueTracks];
|
||||
return cueFile;
|
||||
}
|
||||
}
|
||||
|
||||
cueFile.Tracks = [.. cueTracks];
|
||||
return cueFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill a TRACK from an array of lines
|
||||
/// </summary>
|
||||
/// <param name="number">Number to set</param>
|
||||
/// <param name="dataType">Data type to set</param>
|
||||
/// <param name="data">Stream to pull from</param>
|
||||
private static CueTrack? CreateCueTrack(string number, string dataType, Stream data, out string? lastLine)
|
||||
{
|
||||
// Check the required parameters
|
||||
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (data.Position < 0 || data.Position >= data.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
// Set the current fields
|
||||
if (!int.TryParse(number, out int parsedNumber))
|
||||
throw new ArgumentException($"Number was not a number: {number}");
|
||||
else if (parsedNumber < 1 || parsedNumber > 99)
|
||||
throw new IndexOutOfRangeException($"Index must be between 1 and 99: {parsedNumber}");
|
||||
|
||||
// Create the holding objects
|
||||
lastLine = null;
|
||||
var cueTrack = new CueTrack();
|
||||
var cueIndices = new List<CueIndex>();
|
||||
|
||||
cueTrack.Number = parsedNumber;
|
||||
cueTrack.DataType = GetDataType(dataType);
|
||||
|
||||
while (true)
|
||||
{
|
||||
string? line = lastLine ?? data.ReadQuotedString();
|
||||
lastLine = null;
|
||||
|
||||
// If we have a null line, break from the loop
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
// If we have an empty line, we skip
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
|
||||
string[] splitLine = Regex
|
||||
.Matches(line, @"[^\s""]+|""[^""]*""")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
switch (splitLine[0])
|
||||
{
|
||||
// Read comments
|
||||
case "REM":
|
||||
// We ignore all comments for now
|
||||
break;
|
||||
|
||||
// Read flag information
|
||||
case "FLAGS":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"FLAGS line malformed: {line}");
|
||||
|
||||
cueTrack.Flags = GetFlags(splitLine);
|
||||
break;
|
||||
|
||||
// Read International Standard Recording Code
|
||||
case "ISRC":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"ISRC line malformed: {line}");
|
||||
|
||||
cueTrack.ISRC = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced performer
|
||||
case "PERFORMER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"PERFORMER line malformed: {line}");
|
||||
|
||||
cueTrack.Performer = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced songwriter
|
||||
case "SONGWRITER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"SONGWRITER line malformed: {line}");
|
||||
|
||||
cueTrack.Songwriter = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced title
|
||||
case "TITLE":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"TITLE line malformed: {line}");
|
||||
|
||||
cueTrack.Title = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read pregap information
|
||||
case "PREGAP":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"PREGAP line malformed: {line}");
|
||||
|
||||
var pregap = CreatePreGap(splitLine[1]);
|
||||
if (pregap == default)
|
||||
throw new FormatException($"PREGAP line malformed: {line}");
|
||||
|
||||
cueTrack.PreGap = pregap;
|
||||
break;
|
||||
|
||||
// Read index information
|
||||
case "INDEX":
|
||||
if (splitLine.Length < 3)
|
||||
throw new FormatException($"INDEX line malformed: {line}");
|
||||
|
||||
var index = CreateCueIndex(splitLine[1], splitLine[2]);
|
||||
if (index == default)
|
||||
throw new FormatException($"INDEX line malformed: {line}");
|
||||
|
||||
cueIndices.Add(index);
|
||||
break;
|
||||
|
||||
// Read postgap information
|
||||
case "POSTGAP":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"POSTGAP line malformed: {line}");
|
||||
|
||||
var postgap = CreatePostGap(splitLine[1]);
|
||||
if (postgap == default)
|
||||
throw new FormatException($"POSTGAP line malformed: {line}");
|
||||
|
||||
cueTrack.PostGap = postgap;
|
||||
break;
|
||||
|
||||
// Next track or file found, return
|
||||
case "TRACK":
|
||||
case "FILE":
|
||||
lastLine = line;
|
||||
cueTrack.Indices = [.. cueIndices];
|
||||
return cueTrack;
|
||||
|
||||
// Default means return
|
||||
default:
|
||||
lastLine = line;
|
||||
cueTrack.Indices = [.. cueIndices];
|
||||
return cueTrack;
|
||||
}
|
||||
}
|
||||
|
||||
cueTrack.Indices = [.. cueIndices];
|
||||
return cueTrack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a PREGAP from a mm:ss:ff length
|
||||
/// </summary>
|
||||
/// <param name="length">String to get length information from</param>
|
||||
private static PreGap CreatePreGap(string length)
|
||||
{
|
||||
// Ignore empty lines
|
||||
if (string.IsNullOrEmpty(length))
|
||||
throw new ArgumentException("Length was null or whitespace");
|
||||
|
||||
// Ignore lines that don't contain the correct information
|
||||
if (length!.Length != 8 || length.Count(c => c == ':') != 2)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Split the line
|
||||
string[] splitLength = length.Split(':');
|
||||
if (splitLength.Length != 3)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Parse the lengths
|
||||
int[] lengthSegments = new int[3];
|
||||
|
||||
// Minutes
|
||||
if (!int.TryParse(splitLength[0], out lengthSegments[0]))
|
||||
throw new FormatException($"Minutes segment was not a number: {splitLength[0]}");
|
||||
else if (lengthSegments[0] < 0)
|
||||
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
|
||||
|
||||
// Seconds
|
||||
if (!int.TryParse(splitLength[1], out lengthSegments[1]))
|
||||
throw new FormatException($"Seconds segment was not a number: {splitLength[1]}");
|
||||
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
|
||||
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
|
||||
|
||||
// Frames
|
||||
if (!int.TryParse(splitLength[2], out lengthSegments[2]))
|
||||
throw new FormatException($"Frames segment was not a number: {splitLength[2]}");
|
||||
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
|
||||
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
|
||||
|
||||
// Set the values
|
||||
var preGap = new PreGap
|
||||
{
|
||||
Minutes = lengthSegments[0],
|
||||
Seconds = lengthSegments[1],
|
||||
Frames = lengthSegments[2],
|
||||
};
|
||||
return preGap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill a INDEX from an array of lines
|
||||
/// </summary>
|
||||
/// <param name="index">Index to set</param>
|
||||
/// <param name="startTime">Start time to set</param>
|
||||
private static CueIndex CreateCueIndex(string index, string startTime)
|
||||
{
|
||||
// Set the current fields
|
||||
if (!int.TryParse(index, out int parsedIndex))
|
||||
throw new ArgumentException($"Index was not a number: {index}");
|
||||
else if (parsedIndex < 0 || parsedIndex > 99)
|
||||
throw new IndexOutOfRangeException($"Index must be between 0 and 99: {parsedIndex}");
|
||||
|
||||
// Ignore empty lines
|
||||
if (string.IsNullOrEmpty(startTime))
|
||||
throw new ArgumentException("Start time was null or whitespace");
|
||||
|
||||
// Ignore lines that don't contain the correct information
|
||||
if (startTime!.Length != 8 || startTime.Count(c => c == ':') != 2)
|
||||
throw new FormatException($"Start time was not in a recognized format: {startTime}");
|
||||
|
||||
// Split the line
|
||||
string[] splitTime = startTime.Split(':');
|
||||
if (splitTime.Length != 3)
|
||||
throw new FormatException($"Start time was not in a recognized format: {startTime}");
|
||||
|
||||
// Parse the lengths
|
||||
int[] lengthSegments = new int[3];
|
||||
|
||||
// Minutes
|
||||
if (!int.TryParse(splitTime[0], out lengthSegments[0]))
|
||||
throw new FormatException($"Minutes segment was not a number: {splitTime[0]}");
|
||||
else if (lengthSegments[0] < 0)
|
||||
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
|
||||
|
||||
// Seconds
|
||||
if (!int.TryParse(splitTime[1], out lengthSegments[1]))
|
||||
throw new FormatException($"Seconds segment was not a number: {splitTime[1]}");
|
||||
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
|
||||
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
|
||||
|
||||
// Frames
|
||||
if (!int.TryParse(splitTime[2], out lengthSegments[2]))
|
||||
throw new FormatException($"Frames segment was not a number: {splitTime[2]}");
|
||||
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
|
||||
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
|
||||
|
||||
// Set the values
|
||||
var cueIndex = new CueIndex
|
||||
{
|
||||
Index = parsedIndex,
|
||||
Minutes = lengthSegments[0],
|
||||
Seconds = lengthSegments[1],
|
||||
Frames = lengthSegments[2],
|
||||
};
|
||||
return cueIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a POSTGAP from a mm:ss:ff length
|
||||
/// </summary>
|
||||
/// <param name="length">String to get length information from</param>
|
||||
private static PostGap CreatePostGap(string length)
|
||||
{
|
||||
// Ignore empty lines
|
||||
if (string.IsNullOrEmpty(length))
|
||||
throw new ArgumentException("Length was null or whitespace");
|
||||
|
||||
// Ignore lines that don't contain the correct information
|
||||
if (length!.Length != 8 || length.Count(c => c == ':') != 2)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Split the line
|
||||
string[] splitLength = length.Split(':');
|
||||
if (splitLength.Length != 3)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Parse the lengths
|
||||
int[] lengthSegments = new int[3];
|
||||
|
||||
// Minutes
|
||||
if (!int.TryParse(splitLength[0], out lengthSegments[0]))
|
||||
throw new FormatException($"Minutes segment was not a number: {splitLength[0]}");
|
||||
else if (lengthSegments[0] < 0)
|
||||
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
|
||||
|
||||
// Seconds
|
||||
if (!int.TryParse(splitLength[1], out lengthSegments[1]))
|
||||
throw new FormatException($"Seconds segment was not a number: {splitLength[1]}");
|
||||
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
|
||||
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
|
||||
|
||||
// Frames
|
||||
if (!int.TryParse(splitLength[2], out lengthSegments[2]))
|
||||
throw new FormatException($"Frames segment was not a number: {splitLength[2]}");
|
||||
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
|
||||
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
|
||||
|
||||
// Set the values
|
||||
var postGap = new PostGap
|
||||
{
|
||||
Minutes = lengthSegments[0],
|
||||
Seconds = lengthSegments[1],
|
||||
Frames = lengthSegments[2],
|
||||
};
|
||||
return postGap;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Get the file type from a given string
|
||||
/// </summary>
|
||||
/// <param name="fileType">String to get value from</param>
|
||||
/// <returns>CueFileType, if possible</returns>
|
||||
private static CueFileType GetFileType(string? fileType)
|
||||
{
|
||||
return (fileType?.ToLowerInvariant()) switch
|
||||
{
|
||||
"binary" => CueFileType.BINARY,
|
||||
"motorola" => CueFileType.MOTOROLA,
|
||||
"aiff" => CueFileType.AIFF,
|
||||
"wave" => CueFileType.WAVE,
|
||||
"mp3" => CueFileType.MP3,
|
||||
_ => CueFileType.BINARY,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the data type from a given string
|
||||
/// </summary>
|
||||
/// <param name="dataType">String to get value from</param>
|
||||
/// <returns>CueTrackDataType, if possible (default AUDIO)</returns>
|
||||
private static CueTrackDataType GetDataType(string? dataType)
|
||||
{
|
||||
return (dataType?.ToLowerInvariant()) switch
|
||||
{
|
||||
"audio" => CueTrackDataType.AUDIO,
|
||||
"cdg" => CueTrackDataType.CDG,
|
||||
"mode1/2048" => CueTrackDataType.MODE1_2048,
|
||||
"mode1/2352" => CueTrackDataType.MODE1_2352,
|
||||
"mode2/2336" => CueTrackDataType.MODE2_2336,
|
||||
"mode2/2352" => CueTrackDataType.MODE2_2352,
|
||||
"cdi/2336" => CueTrackDataType.CDI_2336,
|
||||
"cdi/2352" => CueTrackDataType.CDI_2352,
|
||||
_ => CueTrackDataType.AUDIO,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the flag value for an array of strings
|
||||
/// </summary>
|
||||
/// <param name="flagStrings">Possible flags as strings</param>
|
||||
/// <returns>CueTrackFlag value representing the strings, if possible</returns>
|
||||
private static CueTrackFlag GetFlags(string?[]? flagStrings)
|
||||
{
|
||||
CueTrackFlag flag = 0;
|
||||
if (flagStrings == null)
|
||||
return flag;
|
||||
|
||||
foreach (string? flagString in flagStrings)
|
||||
{
|
||||
switch (flagString?.ToLowerInvariant())
|
||||
{
|
||||
case "flags":
|
||||
// No-op since this is the start of the line
|
||||
break;
|
||||
|
||||
case "dcp":
|
||||
flag |= CueTrackFlag.DCP;
|
||||
break;
|
||||
|
||||
case "4ch":
|
||||
flag |= CueTrackFlag.FourCH;
|
||||
break;
|
||||
|
||||
case "pre":
|
||||
flag |= CueTrackFlag.PRE;
|
||||
break;
|
||||
|
||||
case "scms":
|
||||
flag |= CueTrackFlag.SCMS;
|
||||
break;
|
||||
|
||||
case "data":
|
||||
flag |= CueTrackFlag.DATA;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return flag;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,244 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.DosCenter;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class DosCenter : IFileDeserializer<Models.DosCenter.MetadataFile>
|
||||
public class DosCenter :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.DosCenter.MetadataFile? DeserializeFile(string? path)
|
||||
public static MetadataFile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new DosCenter();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.DosCenter.MetadataFile? Deserialize(string? path)
|
||||
public MetadataFile? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.DosCenter.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new DosCenter();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new ClrMameProReader(data, Encoding.UTF8) { DosCenter = true };
|
||||
var dat = new MetadataFile();
|
||||
|
||||
// Loop through and parse out the values
|
||||
string? lastTopLevel = reader.TopLevel;
|
||||
|
||||
Game? game = null;
|
||||
var games = new List<Game>();
|
||||
var files = new List<Models.DosCenter.File>();
|
||||
|
||||
var additional = new List<string>();
|
||||
var headerAdditional = new List<string>();
|
||||
var gameAdditional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Ignore certain row types
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case CmpRowType.None:
|
||||
case CmpRowType.Comment:
|
||||
continue;
|
||||
case CmpRowType.EndTopLevel:
|
||||
switch (lastTopLevel)
|
||||
{
|
||||
case "doscenter":
|
||||
if (dat.DosCenter != null)
|
||||
dat.DosCenter.ADDITIONAL_ELEMENTS = headerAdditional.ToArray();
|
||||
|
||||
headerAdditional.Clear();
|
||||
break;
|
||||
case "game":
|
||||
if (game != null)
|
||||
{
|
||||
game.File = files.ToArray();
|
||||
game.ADDITIONAL_ELEMENTS = gameAdditional.ToArray();
|
||||
games.Add(game);
|
||||
}
|
||||
|
||||
game = null;
|
||||
files.Clear();
|
||||
gameAdditional.Clear();
|
||||
break;
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're at the root
|
||||
if (reader.RowType == CmpRowType.TopLevel)
|
||||
{
|
||||
lastTopLevel = reader.TopLevel;
|
||||
switch (reader.TopLevel)
|
||||
{
|
||||
case "doscenter":
|
||||
dat.DosCenter = new Models.DosCenter.DosCenter();
|
||||
break;
|
||||
case "game":
|
||||
game = new Game();
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in the doscenter block
|
||||
else if (reader.TopLevel == "doscenter" && reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
dat.DosCenter ??= new Models.DosCenter.DosCenter();
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name:":
|
||||
dat.DosCenter.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
case "description:":
|
||||
dat.DosCenter.Description = reader.Standalone?.Value;
|
||||
break;
|
||||
case "version:":
|
||||
dat.DosCenter.Version = reader.Standalone?.Value;
|
||||
break;
|
||||
case "date:":
|
||||
dat.DosCenter.Date = reader.Standalone?.Value;
|
||||
break;
|
||||
case "author:":
|
||||
dat.DosCenter.Author = reader.Standalone?.Value;
|
||||
break;
|
||||
case "homepage:":
|
||||
dat.DosCenter.Homepage = reader.Standalone?.Value;
|
||||
break;
|
||||
case "comment:":
|
||||
dat.DosCenter.Comment = reader.Standalone?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
headerAdditional.Add(item: reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a game block
|
||||
else if (reader.TopLevel == "game" && reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
game ??= new Game();
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
game.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(item: reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a file block
|
||||
else if (reader.TopLevel == "game" && reader.RowType == CmpRowType.Internal)
|
||||
{
|
||||
// If we have an unknown type, log it
|
||||
if (reader.InternalName != "file")
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(reader.CurrentLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the file and add to the list
|
||||
var file = CreateFile(reader);
|
||||
if (file != null)
|
||||
files.Add(file);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(item: reader.CurrentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.Game = games.ToArray();
|
||||
dat.ADDITIONAL_ELEMENTS = additional.ToArray();
|
||||
return dat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a File object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>File object created from the reader context</returns>
|
||||
private static Models.DosCenter.File? CreateFile(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var file = new Models.DosCenter.File();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
file.Name = kvp.Value;
|
||||
break;
|
||||
case "size":
|
||||
file.Size = kvp.Value;
|
||||
break;
|
||||
case "crc":
|
||||
file.CRC = kvp.Value;
|
||||
break;
|
||||
case "date":
|
||||
file.Date = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
itemAdditional.Add(item: reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
file.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return file;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.EverdriveSMDB;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class EverdriveSMDB : IFileDeserializer<Models.EverdriveSMDB.MetadataFile>
|
||||
public class EverdriveSMDB :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.EverdriveSMDB.MetadataFile? DeserializeFile(string? path)
|
||||
public static MetadataFile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new EverdriveSMDB();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.EverdriveSMDB.MetadataFile? Deserialize(string? path)
|
||||
public MetadataFile? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.EverdriveSMDB.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new EverdriveSMDB();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
Header = false,
|
||||
Separator = '\t',
|
||||
VerifyFieldCount = false,
|
||||
};
|
||||
var dat = new MetadataFile();
|
||||
|
||||
// Loop through the rows and parse out values
|
||||
var rows = new List<Row>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine() || reader.Line == null)
|
||||
break;
|
||||
|
||||
// Parse the line into a row
|
||||
var row = new Row
|
||||
{
|
||||
SHA256 = reader.Line[0],
|
||||
Name = reader.Line[1],
|
||||
SHA1 = reader.Line[2],
|
||||
MD5 = reader.Line[3],
|
||||
CRC32 = reader.Line[4],
|
||||
};
|
||||
|
||||
// If we have the size field
|
||||
if (reader.Line.Count > 5)
|
||||
row.Size = reader.Line[5];
|
||||
|
||||
// If we have additional fields
|
||||
if (reader.Line.Count > 6)
|
||||
row.ADDITIONAL_ELEMENTS = reader.Line.Skip(5).ToArray();
|
||||
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
// Assign the rows to the Dat and return
|
||||
dat.Row = rows.ToArray();
|
||||
return dat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.GCF;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class GCF :
|
||||
IByteDeserializer<Models.GCF.File>,
|
||||
IFileDeserializer<Models.GCF.File>
|
||||
IFileDeserializer<Models.GCF.File>,
|
||||
IStreamDeserializer<Models.GCF.File>
|
||||
{
|
||||
#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.GCF.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +52,750 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.GCF.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.GCF.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.GCF.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new GCF();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.GCF.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life Game Cache to fill
|
||||
var file = new Models.GCF.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entry Header
|
||||
|
||||
// Try to parse the block entry header
|
||||
var blockEntryHeader = ParseBlockEntryHeader(data);
|
||||
if (blockEntryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache block entry header
|
||||
file.BlockEntryHeader = blockEntryHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entries
|
||||
|
||||
// Create the block entry array
|
||||
file.BlockEntries = new BlockEntry[blockEntryHeader.BlockCount];
|
||||
|
||||
// Try to parse the block entries
|
||||
for (int i = 0; i < blockEntryHeader.BlockCount; i++)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
file.BlockEntries[i] = blockEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fragmentation Map Header
|
||||
|
||||
// Try to parse the fragmentation map header
|
||||
var fragmentationMapHeader = ParseFragmentationMapHeader(data);
|
||||
if (fragmentationMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache fragmentation map header
|
||||
file.FragmentationMapHeader = fragmentationMapHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fragmentation Maps
|
||||
|
||||
// Create the fragmentation map array
|
||||
file.FragmentationMaps = new FragmentationMap[fragmentationMapHeader.BlockCount];
|
||||
|
||||
// Try to parse the fragmentation maps
|
||||
for (int i = 0; i < fragmentationMapHeader.BlockCount; i++)
|
||||
{
|
||||
var fragmentationMap = ParseFragmentationMap(data);
|
||||
file.FragmentationMaps[i] = fragmentationMap;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entry Map Header
|
||||
|
||||
if (header.MinorVersion < 6)
|
||||
{
|
||||
// Try to parse the block entry map header
|
||||
var blockEntryMapHeader = ParseBlockEntryMapHeader(data);
|
||||
if (blockEntryMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache block entry map header
|
||||
file.BlockEntryMapHeader = blockEntryMapHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entry Maps
|
||||
|
||||
if (header.MinorVersion < 6)
|
||||
{
|
||||
// Create the block entry map array
|
||||
file.BlockEntryMaps = new BlockEntryMap[file.BlockEntryMapHeader!.BlockCount];
|
||||
|
||||
// Try to parse the block entry maps
|
||||
for (int i = 0; i < file.BlockEntryMapHeader.BlockCount; i++)
|
||||
{
|
||||
var blockEntryMap = ParseBlockEntryMap(data);
|
||||
file.BlockEntryMaps[i] = blockEntryMap;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Directory Header
|
||||
|
||||
// Try to parse the directory header
|
||||
var directoryHeader = ParseDirectoryHeader(data);
|
||||
if (directoryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache directory header
|
||||
file.DirectoryHeader = directoryHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Entries
|
||||
|
||||
// Create the directory entry array
|
||||
file.DirectoryEntries = new DirectoryEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.DirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Names
|
||||
|
||||
if (directoryHeader.NameSize > 0)
|
||||
{
|
||||
// Get the current offset for adjustment
|
||||
long directoryNamesStart = data.Position;
|
||||
|
||||
// Get the ending offset
|
||||
long directoryNamesEnd = data.Position + directoryHeader.NameSize;
|
||||
|
||||
// Create the string dictionary
|
||||
file.DirectoryNames = new Dictionary<long, string?>();
|
||||
|
||||
// Loop and read the null-terminated strings
|
||||
while (data.Position < directoryNamesEnd)
|
||||
{
|
||||
long nameOffset = data.Position - directoryNamesStart;
|
||||
string? directoryName = data.ReadString(Encoding.ASCII);
|
||||
if (data.Position > directoryNamesEnd)
|
||||
{
|
||||
data.Seek(-directoryName?.Length ?? 0, SeekOrigin.Current);
|
||||
byte[]? endingData = data.ReadBytes((int)(directoryNamesEnd - data.Position));
|
||||
if (endingData != null)
|
||||
directoryName = Encoding.ASCII.GetString(endingData);
|
||||
else
|
||||
directoryName = null;
|
||||
}
|
||||
|
||||
file.DirectoryNames[nameOffset] = directoryName;
|
||||
}
|
||||
|
||||
// Loop and assign to entries
|
||||
foreach (var directoryEntry in file.DirectoryEntries)
|
||||
{
|
||||
if (directoryEntry != null)
|
||||
directoryEntry.Name = file.DirectoryNames[directoryEntry.NameOffset];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 1 Entries
|
||||
|
||||
// Create the directory info 1 entry array
|
||||
file.DirectoryInfo1Entries = new DirectoryInfo1Entry[directoryHeader.Info1Count];
|
||||
|
||||
// Try to parse the directory info 1 entries
|
||||
for (int i = 0; i < directoryHeader.Info1Count; i++)
|
||||
{
|
||||
var directoryInfo1Entry = ParseDirectoryInfo1Entry(data);
|
||||
file.DirectoryInfo1Entries[i] = directoryInfo1Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 2 Entries
|
||||
|
||||
// Create the directory info 2 entry array
|
||||
file.DirectoryInfo2Entries = new DirectoryInfo2Entry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory info 2 entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryInfo2Entry = ParseDirectoryInfo2Entry(data);
|
||||
file.DirectoryInfo2Entries[i] = directoryInfo2Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Copy Entries
|
||||
|
||||
// Create the directory copy entry array
|
||||
file.DirectoryCopyEntries = new DirectoryCopyEntry[directoryHeader.CopyCount];
|
||||
|
||||
// Try to parse the directory copy entries
|
||||
for (int i = 0; i < directoryHeader.CopyCount; i++)
|
||||
{
|
||||
var directoryCopyEntry = ParseDirectoryCopyEntry(data);
|
||||
file.DirectoryCopyEntries[i] = directoryCopyEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Local Entries
|
||||
|
||||
// Create the directory local entry array
|
||||
file.DirectoryLocalEntries = new DirectoryLocalEntry[directoryHeader.LocalCount];
|
||||
|
||||
// Try to parse the directory local entries
|
||||
for (int i = 0; i < directoryHeader.LocalCount; i++)
|
||||
{
|
||||
var directoryLocalEntry = ParseDirectoryLocalEntry(data);
|
||||
file.DirectoryLocalEntries[i] = directoryLocalEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of directory section, just in case
|
||||
data.Seek(initialOffset + directoryHeader.DirectorySize, SeekOrigin.Begin);
|
||||
|
||||
#region Directory Map Header
|
||||
|
||||
if (header.MinorVersion >= 5)
|
||||
{
|
||||
// Try to parse the directory map header
|
||||
var directoryMapHeader = ParseDirectoryMapHeader(data);
|
||||
if (directoryMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache directory map header
|
||||
file.DirectoryMapHeader = directoryMapHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Map Entries
|
||||
|
||||
// Create the directory map entry array
|
||||
file.DirectoryMapEntries = new DirectoryMapEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory map entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryMapEntry = ParseDirectoryMapEntry(data);
|
||||
file.DirectoryMapEntries[i] = directoryMapEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Header
|
||||
|
||||
// Try to parse the checksum header
|
||||
var checksumHeader = ParseChecksumHeader(data);
|
||||
if (checksumHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum header
|
||||
file.ChecksumHeader = checksumHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Checksum Map Header
|
||||
|
||||
// Try to parse the checksum map header
|
||||
var checksumMapHeader = ParseChecksumMapHeader(data);
|
||||
if (checksumMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum map header
|
||||
file.ChecksumMapHeader = checksumMapHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Map Entries
|
||||
|
||||
// Create the checksum map entry array
|
||||
file.ChecksumMapEntries = new ChecksumMapEntry[checksumMapHeader.ItemCount];
|
||||
|
||||
// Try to parse the checksum map entries
|
||||
for (int i = 0; i < checksumMapHeader.ItemCount; i++)
|
||||
{
|
||||
var checksumMapEntry = ParseChecksumMapEntry(data);
|
||||
file.ChecksumMapEntries[i] = checksumMapEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Entries
|
||||
|
||||
// Create the checksum entry array
|
||||
file.ChecksumEntries = new ChecksumEntry[checksumMapHeader.ChecksumCount];
|
||||
|
||||
// Try to parse the checksum entries
|
||||
for (int i = 0; i < checksumMapHeader.ChecksumCount; i++)
|
||||
{
|
||||
var checksumEntry = ParseChecksumEntry(data);
|
||||
file.ChecksumEntries[i] = checksumEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of checksum section, just in case
|
||||
data.Seek(initialOffset + checksumHeader.ChecksumSize, SeekOrigin.Begin);
|
||||
|
||||
#region Data Block Header
|
||||
|
||||
// Try to parse the data block header
|
||||
var dataBlockHeader = ParseDataBlockHeader(data, header.MinorVersion);
|
||||
if (dataBlockHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache data block header
|
||||
file.DataBlockHeader = dataBlockHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.Dummy0 = data.ReadUInt32();
|
||||
if (header.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
header.MajorVersion = data.ReadUInt32();
|
||||
if (header.MajorVersion != 0x00000001)
|
||||
return null;
|
||||
|
||||
header.MinorVersion = data.ReadUInt32();
|
||||
if (header.MinorVersion != 3 && header.MinorVersion != 5 && header.MinorVersion != 6)
|
||||
return null;
|
||||
|
||||
header.CacheID = data.ReadUInt32();
|
||||
header.LastVersionPlayed = data.ReadUInt32();
|
||||
header.Dummy1 = data.ReadUInt32();
|
||||
header.Dummy2 = data.ReadUInt32();
|
||||
header.FileSize = data.ReadUInt32();
|
||||
header.BlockSize = data.ReadUInt32();
|
||||
header.BlockCount = data.ReadUInt32();
|
||||
header.Dummy3 = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry header on success, null on error</returns>
|
||||
private static BlockEntryHeader ParseBlockEntryHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntryHeader blockEntryHeader = new BlockEntryHeader();
|
||||
|
||||
blockEntryHeader.BlockCount = data.ReadUInt32();
|
||||
blockEntryHeader.BlocksUsed = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy0 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy1 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy2 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy3 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy4 = data.ReadUInt32();
|
||||
blockEntryHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return blockEntryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry on success, null on error</returns>
|
||||
private static BlockEntry ParseBlockEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntry blockEntry = new BlockEntry();
|
||||
|
||||
blockEntry.EntryFlags = data.ReadUInt32();
|
||||
blockEntry.FileDataOffset = data.ReadUInt32();
|
||||
blockEntry.FileDataSize = data.ReadUInt32();
|
||||
blockEntry.FirstDataBlockIndex = data.ReadUInt32();
|
||||
blockEntry.NextBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntry.PreviousBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return blockEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache fragmentation map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache fragmentation map header on success, null on error</returns>
|
||||
private static FragmentationMapHeader ParseFragmentationMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FragmentationMapHeader fragmentationMapHeader = new FragmentationMapHeader();
|
||||
|
||||
fragmentationMapHeader.BlockCount = data.ReadUInt32();
|
||||
fragmentationMapHeader.FirstUnusedEntry = data.ReadUInt32();
|
||||
fragmentationMapHeader.Terminator = data.ReadUInt32();
|
||||
fragmentationMapHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return fragmentationMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache fragmentation map
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache fragmentation map on success, null on error</returns>
|
||||
private static FragmentationMap ParseFragmentationMap(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FragmentationMap fragmentationMap = new FragmentationMap();
|
||||
|
||||
fragmentationMap.NextDataBlockIndex = data.ReadUInt32();
|
||||
|
||||
return fragmentationMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry map header on success, null on error</returns>
|
||||
private static BlockEntryMapHeader ParseBlockEntryMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntryMapHeader blockEntryMapHeader = new BlockEntryMapHeader();
|
||||
|
||||
blockEntryMapHeader.BlockCount = data.ReadUInt32();
|
||||
blockEntryMapHeader.FirstBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntryMapHeader.LastBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntryMapHeader.Dummy0 = data.ReadUInt32();
|
||||
blockEntryMapHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return blockEntryMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry map
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry map on success, null on error</returns>
|
||||
private static BlockEntryMap ParseBlockEntryMap(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntryMap blockEntryMap = new BlockEntryMap();
|
||||
|
||||
blockEntryMap.PreviousBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntryMap.NextBlockEntryIndex = data.ReadUInt32();
|
||||
|
||||
return blockEntryMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory header on success, null on error</returns>
|
||||
private static DirectoryHeader ParseDirectoryHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryHeader directoryHeader = new DirectoryHeader();
|
||||
|
||||
directoryHeader.Dummy0 = data.ReadUInt32();
|
||||
directoryHeader.CacheID = data.ReadUInt32();
|
||||
directoryHeader.LastVersionPlayed = data.ReadUInt32();
|
||||
directoryHeader.ItemCount = data.ReadUInt32();
|
||||
directoryHeader.FileCount = data.ReadUInt32();
|
||||
directoryHeader.Dummy1 = data.ReadUInt32();
|
||||
directoryHeader.DirectorySize = data.ReadUInt32();
|
||||
directoryHeader.NameSize = data.ReadUInt32();
|
||||
directoryHeader.Info1Count = data.ReadUInt32();
|
||||
directoryHeader.CopyCount = data.ReadUInt32();
|
||||
directoryHeader.LocalCount = data.ReadUInt32();
|
||||
directoryHeader.Dummy2 = data.ReadUInt32();
|
||||
directoryHeader.Dummy3 = data.ReadUInt32();
|
||||
directoryHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return directoryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.NameOffset = data.ReadUInt32();
|
||||
directoryEntry.ItemSize = data.ReadUInt32();
|
||||
directoryEntry.ChecksumIndex = data.ReadUInt32();
|
||||
directoryEntry.DirectoryFlags = (HL_GCF_FLAG)data.ReadUInt32();
|
||||
directoryEntry.ParentIndex = data.ReadUInt32();
|
||||
directoryEntry.NextIndex = data.ReadUInt32();
|
||||
directoryEntry.FirstIndex = data.ReadUInt32();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory info 1 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory info 1 entry on success, null on error</returns>
|
||||
private static DirectoryInfo1Entry ParseDirectoryInfo1Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo1Entry directoryInfo1Entry = new DirectoryInfo1Entry();
|
||||
|
||||
directoryInfo1Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo1Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory info 2 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory info 2 entry on success, null on error</returns>
|
||||
private static DirectoryInfo2Entry ParseDirectoryInfo2Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo2Entry directoryInfo2Entry = new DirectoryInfo2Entry();
|
||||
|
||||
directoryInfo2Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo2Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory copy entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory copy entry on success, null on error</returns>
|
||||
private static DirectoryCopyEntry ParseDirectoryCopyEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryCopyEntry directoryCopyEntry = new DirectoryCopyEntry();
|
||||
|
||||
directoryCopyEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryCopyEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory local entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory local entry on success, null on error</returns>
|
||||
private static DirectoryLocalEntry ParseDirectoryLocalEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryLocalEntry directoryLocalEntry = new DirectoryLocalEntry();
|
||||
|
||||
directoryLocalEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryLocalEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory map header on success, null on error</returns>
|
||||
private static DirectoryMapHeader? ParseDirectoryMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryMapHeader directoryMapHeader = new DirectoryMapHeader();
|
||||
|
||||
directoryMapHeader.Dummy0 = data.ReadUInt32();
|
||||
if (directoryMapHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
directoryMapHeader.Dummy1 = data.ReadUInt32();
|
||||
if (directoryMapHeader.Dummy1 != 0x00000000)
|
||||
return null;
|
||||
|
||||
return directoryMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory map entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory map entry on success, null on error</returns>
|
||||
private static DirectoryMapEntry ParseDirectoryMapEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryMapEntry directoryMapEntry = new DirectoryMapEntry();
|
||||
|
||||
directoryMapEntry.FirstBlockIndex = data.ReadUInt32();
|
||||
|
||||
return directoryMapEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum header on success, null on error</returns>
|
||||
private static ChecksumHeader? ParseChecksumHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumHeader checksumHeader = new ChecksumHeader();
|
||||
|
||||
checksumHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumHeader.ChecksumSize = data.ReadUInt32();
|
||||
|
||||
return checksumHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum map header on success, null on error</returns>
|
||||
private static ChecksumMapHeader? ParseChecksumMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapHeader checksumMapHeader = new ChecksumMapHeader();
|
||||
|
||||
checksumMapHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy0 != 0x14893721)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.Dummy1 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy1 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.ItemCount = data.ReadUInt32();
|
||||
checksumMapHeader.ChecksumCount = data.ReadUInt32();
|
||||
|
||||
return checksumMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum map entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum map entry on success, null on error</returns>
|
||||
private static ChecksumMapEntry ParseChecksumMapEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapEntry checksumMapEntry = new ChecksumMapEntry();
|
||||
|
||||
checksumMapEntry.ChecksumCount = data.ReadUInt32();
|
||||
checksumMapEntry.FirstChecksumIndex = data.ReadUInt32();
|
||||
|
||||
return checksumMapEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum entry on success, null on error</returns>
|
||||
private static ChecksumEntry ParseChecksumEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumEntry checksumEntry = new ChecksumEntry();
|
||||
|
||||
checksumEntry.Checksum = data.ReadUInt32();
|
||||
|
||||
return checksumEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache data block header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="minorVersion">Minor version field from the header</param>
|
||||
/// <returns>Filled Half-Life Game Cache data block header on success, null on error</returns>
|
||||
private static DataBlockHeader ParseDataBlockHeader(Stream data, uint minorVersion)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DataBlockHeader dataBlockHeader = new DataBlockHeader();
|
||||
|
||||
// In version 3 the DataBlockHeader is missing the LastVersionPlayed field.
|
||||
if (minorVersion >= 5)
|
||||
dataBlockHeader.LastVersionPlayed = data.ReadUInt32();
|
||||
|
||||
dataBlockHeader.BlockCount = data.ReadUInt32();
|
||||
dataBlockHeader.BlockSize = data.ReadUInt32();
|
||||
dataBlockHeader.FirstBlockOffset = data.ReadUInt32();
|
||||
dataBlockHeader.BlocksUsed = data.ReadUInt32();
|
||||
dataBlockHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return dataBlockHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.Models.Hashfile;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
// TODO: Replace use of Serialization.Hash with Hashing.HashType
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Hashfile : IFileDeserializer<Models.Hashfile.Hashfile>
|
||||
public class Hashfile :
|
||||
IFileDeserializer<Models.Hashfile.Hashfile>,
|
||||
IStreamDeserializer<Models.Hashfile.Hashfile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
@@ -21,9 +29,170 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.Hashfile.Hashfile? Deserialize(string? path, Hash hash)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.Hashfile.DeserializeStream(stream, hash);
|
||||
return DeserializeStream(stream, hash);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.Hashfile.Hashfile? DeserializeStream(Stream? data, Hash hash = Hash.CRC)
|
||||
{
|
||||
var deserializer = new Hashfile();
|
||||
return deserializer.Deserialize(data, hash);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Hashfile.Hashfile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, Hash.CRC);
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
public Models.Hashfile.Hashfile? Deserialize(Stream? data, Hash hash)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new StreamReader(data);
|
||||
var dat = new Models.Hashfile.Hashfile();
|
||||
var additional = new List<string>();
|
||||
|
||||
// Loop through the rows and parse out values
|
||||
var hashes = new List<object>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// Read and split the line
|
||||
string? line = reader.ReadLine();
|
||||
#if NETFRAMEWORK || NETCOREAPP3_1
|
||||
string[]? lineParts = line?.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
#else
|
||||
string[]? lineParts = line?.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
#endif
|
||||
if (lineParts == null)
|
||||
continue;
|
||||
|
||||
// Parse the line into a hash
|
||||
switch (hash)
|
||||
{
|
||||
case Hash.CRC:
|
||||
var sfv = new SFV
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Take(lineParts.Length - 1).ToArray()),
|
||||
Hash = lineParts[lineParts.Length - 1],
|
||||
#else
|
||||
File = string.Join(" ", lineParts[..^1]),
|
||||
Hash = lineParts[^1],
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sfv);
|
||||
break;
|
||||
case Hash.MD5:
|
||||
var md5 = new MD5
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(md5);
|
||||
break;
|
||||
case Hash.SHA1:
|
||||
var sha1 = new SHA1
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha1);
|
||||
break;
|
||||
case Hash.SHA256:
|
||||
var sha256 = new SHA256
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha256);
|
||||
break;
|
||||
case Hash.SHA384:
|
||||
var sha384 = new SHA384
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha384);
|
||||
break;
|
||||
case Hash.SHA512:
|
||||
var sha512 = new SHA512
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha512);
|
||||
break;
|
||||
case Hash.SpamSum:
|
||||
var spamSum = new SpamSum
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(spamSum);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the hashes to the hashfile and return
|
||||
switch (hash)
|
||||
{
|
||||
case Hash.CRC:
|
||||
dat.SFV = hashes.Cast<SFV>().ToArray();
|
||||
break;
|
||||
case Hash.MD5:
|
||||
dat.MD5 = hashes.Cast<MD5>().ToArray();
|
||||
break;
|
||||
case Hash.SHA1:
|
||||
dat.SHA1 = hashes.Cast<SHA1>().ToArray();
|
||||
break;
|
||||
case Hash.SHA256:
|
||||
dat.SHA256 = hashes.Cast<SHA256>().ToArray();
|
||||
break;
|
||||
case Hash.SHA384:
|
||||
dat.SHA384 = hashes.Cast<SHA384>().ToArray();
|
||||
break;
|
||||
case Hash.SHA512:
|
||||
dat.SHA512 = hashes.Cast<SHA512>().ToArray();
|
||||
break;
|
||||
case Hash.SpamSum:
|
||||
dat.SpamSum = hashes.Cast<SpamSum>().ToArray();
|
||||
break;
|
||||
}
|
||||
dat.ADDITIONAL_ELEMENTS = [.. additional];
|
||||
return dat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class IRD :
|
||||
IByteDeserializer<Models.IRD.File>,
|
||||
IFileDeserializer<Models.IRD.File>
|
||||
IFileDeserializer<Models.IRD.File>,
|
||||
IStreamDeserializer<Models.IRD.File>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
@@ -29,7 +32,7 @@ namespace SabreTools.Serialization.Deserializers
|
||||
|
||||
// Create a memory stream and parse that
|
||||
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
||||
return Streams.IRD.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +50,122 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.IRD.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.IRD.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.IRD.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new IRD();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.IRD.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 media key block to fill
|
||||
var ird = new Models.IRD.File();
|
||||
|
||||
ird.Magic = data.ReadBytes(4);
|
||||
if (ird.Magic == null)
|
||||
return null;
|
||||
|
||||
string magic = Encoding.ASCII.GetString(ird.Magic);
|
||||
if (magic != "3IRD")
|
||||
return null;
|
||||
|
||||
ird.Version = data.ReadByteValue();
|
||||
if (ird.Version < 6)
|
||||
return null;
|
||||
|
||||
var titleId = data.ReadBytes(9);
|
||||
if (titleId == null)
|
||||
return null;
|
||||
|
||||
ird.TitleID = Encoding.ASCII.GetString(titleId);
|
||||
|
||||
ird.TitleLength = data.ReadByteValue();
|
||||
var title = data.ReadBytes(ird.TitleLength);
|
||||
if (title == null)
|
||||
return null;
|
||||
|
||||
ird.Title = Encoding.ASCII.GetString(title);
|
||||
|
||||
var systemVersion = data.ReadBytes(4);
|
||||
if (systemVersion == null)
|
||||
return null;
|
||||
|
||||
ird.SystemVersion = Encoding.ASCII.GetString(systemVersion);
|
||||
|
||||
var gameVersion = data.ReadBytes(5);
|
||||
if (gameVersion == null)
|
||||
return null;
|
||||
|
||||
ird.GameVersion = Encoding.ASCII.GetString(gameVersion);
|
||||
|
||||
var appVersion = data.ReadBytes(5);
|
||||
if (appVersion == null)
|
||||
return null;
|
||||
|
||||
ird.AppVersion = Encoding.ASCII.GetString(appVersion);
|
||||
|
||||
if (ird.Version == 7)
|
||||
ird.UID = data.ReadUInt32();
|
||||
|
||||
ird.HeaderLength = data.ReadByteValue();
|
||||
ird.Header = data.ReadBytes((int)ird.HeaderLength);
|
||||
ird.FooterLength = data.ReadByteValue();
|
||||
ird.Footer = data.ReadBytes((int)ird.FooterLength);
|
||||
|
||||
ird.RegionCount = data.ReadByteValue();
|
||||
ird.RegionHashes = new byte[ird.RegionCount][];
|
||||
for (int i = 0; i < ird.RegionCount; i++)
|
||||
{
|
||||
ird.RegionHashes[i] = data.ReadBytes(16) ?? [];
|
||||
}
|
||||
|
||||
ird.FileCount = data.ReadByteValue();
|
||||
ird.FileKeys = new ulong[ird.FileCount];
|
||||
ird.FileHashes = new byte[ird.FileCount][];
|
||||
for (int i = 0; i < ird.FileCount; i++)
|
||||
{
|
||||
ird.FileKeys[i] = data.ReadUInt64();
|
||||
ird.FileHashes[i] = data.ReadBytes(16) ?? [];
|
||||
}
|
||||
|
||||
ird.ExtraConfig = data.ReadUInt16();
|
||||
ird.Attachments = data.ReadUInt16();
|
||||
|
||||
if (ird.Version >= 9)
|
||||
ird.PIC = data.ReadBytes(115);
|
||||
|
||||
ird.Data1Key = data.ReadBytes(16);
|
||||
ird.Data2Key = data.ReadBytes(16);
|
||||
|
||||
if (ird.Version < 9)
|
||||
ird.PIC = data.ReadBytes(115);
|
||||
|
||||
if (ird.Version > 7)
|
||||
ird.UID = data.ReadUInt32();
|
||||
|
||||
ird.CRC = data.ReadUInt32();
|
||||
|
||||
return ird;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.InstallShieldCabinet;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.InstallShieldCabinet.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
// TODO: Add multi-cabinet reading
|
||||
public class InstallShieldCabinet :
|
||||
IByteDeserializer<Models.InstallShieldCabinet.Cabinet>,
|
||||
IFileDeserializer<Models.InstallShieldCabinet.Cabinet>
|
||||
IByteDeserializer<Cabinet>,
|
||||
IFileDeserializer<Cabinet>,
|
||||
IStreamDeserializer<Cabinet>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.InstallShieldCabinet.Cabinet? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Cabinet? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new InstallShieldCabinet();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.InstallShieldCabinet.Cabinet? Deserialize(byte[]? data, int offset)
|
||||
public Cabinet? Deserialize(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null)
|
||||
@@ -30,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.InstallShieldCabinet.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -38,19 +44,796 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.InstallShieldCabinet.Cabinet? DeserializeFile(string? path)
|
||||
public static Cabinet? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new InstallShieldCabinet();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.InstallShieldCabinet.Cabinet? Deserialize(string? path)
|
||||
public Cabinet? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.InstallShieldCabinet.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Cabinet? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new InstallShieldCabinet();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cabinet? 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 cabinet to fill
|
||||
var cabinet = new Cabinet();
|
||||
|
||||
#region Common Header
|
||||
|
||||
// Try to parse the cabinet header
|
||||
var commonHeader = ParseCommonHeader(data);
|
||||
if (commonHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the cabinet header
|
||||
cabinet.CommonHeader = commonHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Volume Header
|
||||
|
||||
// Try to parse the volume header
|
||||
var volumeHeader = ParseVolumeHeader(data, GetMajorVersion(commonHeader));
|
||||
if (volumeHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the volume header
|
||||
cabinet.VolumeHeader = volumeHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Descriptor
|
||||
|
||||
// Get the descriptor offset
|
||||
uint descriptorOffset = commonHeader.DescriptorOffset;
|
||||
if (descriptorOffset < 0 || descriptorOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the descriptor
|
||||
data.Seek(descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the descriptor
|
||||
var descriptor = ParseDescriptor(data);
|
||||
if (descriptor == null)
|
||||
return null;
|
||||
|
||||
// Set the descriptor
|
||||
cabinet.Descriptor = descriptor;
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Descriptor Offsets
|
||||
|
||||
// Get the file table offset
|
||||
uint fileTableOffset = commonHeader.DescriptorOffset + descriptor.FileTableOffset;
|
||||
if (fileTableOffset < 0 || fileTableOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the file table
|
||||
data.Seek(fileTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the number of file table items
|
||||
uint fileTableItems;
|
||||
if (GetMajorVersion(commonHeader) <= 5)
|
||||
fileTableItems = descriptor.DirectoryCount + descriptor.FileCount;
|
||||
else
|
||||
fileTableItems = descriptor.DirectoryCount;
|
||||
|
||||
// Create and fill the file table
|
||||
cabinet.FileDescriptorOffsets = new uint[fileTableItems];
|
||||
for (int i = 0; i < cabinet.FileDescriptorOffsets.Length; i++)
|
||||
{
|
||||
cabinet.FileDescriptorOffsets[i] = data.ReadUInt32();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Descriptors
|
||||
|
||||
// Create and fill the directory descriptors
|
||||
cabinet.DirectoryNames = new string[descriptor.DirectoryCount];
|
||||
for (int i = 0; i < descriptor.DirectoryCount; i++)
|
||||
{
|
||||
// Get the directory descriptor offset
|
||||
uint offset = descriptorOffset
|
||||
+ descriptor.FileTableOffset
|
||||
+ cabinet.FileDescriptorOffsets[i];
|
||||
|
||||
// If we have an invalid offset
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the file descriptor offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the file descriptor
|
||||
string? directoryName = ParseDirectoryName(data, GetMajorVersion(commonHeader));
|
||||
if (directoryName != null)
|
||||
cabinet.DirectoryNames[i] = directoryName;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Descriptors
|
||||
|
||||
// Create and fill the file descriptors
|
||||
cabinet.FileDescriptors = new FileDescriptor[descriptor.FileCount];
|
||||
for (int i = 0; i < descriptor.FileCount; i++)
|
||||
{
|
||||
// Get the file descriptor offset
|
||||
uint offset;
|
||||
if (GetMajorVersion(commonHeader) <= 5)
|
||||
{
|
||||
offset = descriptorOffset
|
||||
+ descriptor.FileTableOffset
|
||||
+ cabinet.FileDescriptorOffsets[descriptor.DirectoryCount + i];
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = descriptorOffset
|
||||
+ descriptor.FileTableOffset
|
||||
+ descriptor.FileTableOffset2
|
||||
+ (uint)(i * 0x57);
|
||||
}
|
||||
|
||||
// If we have an invalid offset
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the file descriptor offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the file descriptor
|
||||
FileDescriptor fileDescriptor = ParseFileDescriptor(data, GetMajorVersion(commonHeader), descriptorOffset + descriptor.FileTableOffset);
|
||||
cabinet.FileDescriptors[i] = fileDescriptor;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Group Offsets
|
||||
|
||||
// Create and fill the file group offsets
|
||||
cabinet.FileGroupOffsets = new Dictionary<long, OffsetList?>();
|
||||
for (int i = 0; i < (descriptor.FileGroupOffsets?.Length ?? 0); i++)
|
||||
{
|
||||
// Get the file group offset
|
||||
uint offset = descriptor.FileGroupOffsets![i];
|
||||
if (offset == 0)
|
||||
continue;
|
||||
|
||||
// Adjust the file group offset
|
||||
offset += commonHeader.DescriptorOffset;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the file group offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
OffsetList offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.FileGroupOffsets[descriptor.FileGroupOffsets[i]] = offsetList;
|
||||
|
||||
// If we have a nonzero next offset
|
||||
uint nextOffset = offsetList.NextOffset;
|
||||
while (nextOffset != 0)
|
||||
{
|
||||
// Get the next offset to read
|
||||
uint internalOffset = nextOffset + commonHeader.DescriptorOffset;
|
||||
|
||||
// Seek to the file group offset
|
||||
data.Seek(internalOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.FileGroupOffsets[nextOffset] = offsetList;
|
||||
|
||||
// Set the next offset
|
||||
nextOffset = offsetList.NextOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Groups
|
||||
|
||||
// Create the file groups array
|
||||
cabinet.FileGroups = new FileGroup[cabinet.FileGroupOffsets.Count];
|
||||
|
||||
// Create and fill the file groups
|
||||
int fileGroupId = 0;
|
||||
foreach (var kvp in cabinet.FileGroupOffsets)
|
||||
{
|
||||
// Get the offset
|
||||
OffsetList? list = kvp.Value;
|
||||
if (list == null)
|
||||
{
|
||||
fileGroupId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have an invalid offset
|
||||
if (list.DescriptorOffset <= 0)
|
||||
{
|
||||
fileGroupId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/// Seek to the file group
|
||||
data.Seek(list.DescriptorOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the file group
|
||||
var fileGroup = ParseFileGroup(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
if (fileGroup == null)
|
||||
return null;
|
||||
|
||||
// Add the file group
|
||||
cabinet.FileGroups[fileGroupId++] = fileGroup;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Offsets
|
||||
|
||||
// Create and fill the component offsets
|
||||
cabinet.ComponentOffsets = new Dictionary<long, OffsetList?>();
|
||||
for (int i = 0; i < (descriptor.ComponentOffsets?.Length ?? 0); i++)
|
||||
{
|
||||
// Get the component offset
|
||||
uint offset = descriptor.ComponentOffsets![i];
|
||||
if (offset == 0)
|
||||
continue;
|
||||
|
||||
// Adjust the component offset
|
||||
offset += commonHeader.DescriptorOffset;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the component offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
OffsetList offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.ComponentOffsets[descriptor.ComponentOffsets[i]] = offsetList;
|
||||
|
||||
// If we have a nonzero next offset
|
||||
uint nextOffset = offsetList.NextOffset;
|
||||
while (nextOffset != 0)
|
||||
{
|
||||
// Get the next offset to read
|
||||
uint internalOffset = nextOffset + commonHeader.DescriptorOffset;
|
||||
|
||||
// Seek to the file group offset
|
||||
data.Seek(internalOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.ComponentOffsets[nextOffset] = offsetList;
|
||||
|
||||
// Set the next offset
|
||||
nextOffset = offsetList.NextOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Components
|
||||
|
||||
// Create the components array
|
||||
cabinet.Components = new Component[cabinet.ComponentOffsets.Count];
|
||||
|
||||
// Create and fill the components
|
||||
int componentId = 0;
|
||||
foreach (KeyValuePair<long, OffsetList?> kvp in cabinet.ComponentOffsets)
|
||||
{
|
||||
// Get the offset
|
||||
OffsetList? list = kvp.Value;
|
||||
if (list == null)
|
||||
{
|
||||
componentId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have an invalid offset
|
||||
if (list.DescriptorOffset <= 0)
|
||||
{
|
||||
componentId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seek to the component
|
||||
data.Seek(list.DescriptorOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the component
|
||||
var component = ParseComponent(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
if (component == null)
|
||||
return null;
|
||||
|
||||
// Add the component
|
||||
cabinet.Components[componentId++] = component;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// TODO: Parse setup types
|
||||
|
||||
return cabinet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a common header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled common header on success, null on error</returns>
|
||||
private static CommonHeader? ParseCommonHeader(Stream data)
|
||||
{
|
||||
CommonHeader commonHeader = new CommonHeader();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
commonHeader.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (commonHeader.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
commonHeader.Version = data.ReadUInt32();
|
||||
commonHeader.VolumeInfo = data.ReadUInt32();
|
||||
commonHeader.DescriptorOffset = data.ReadUInt32();
|
||||
commonHeader.DescriptorSize = data.ReadUInt32();
|
||||
|
||||
return commonHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a volume header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <returns>Filled volume header on success, null on error</returns>
|
||||
private static VolumeHeader ParseVolumeHeader(Stream data, int majorVersion)
|
||||
{
|
||||
VolumeHeader volumeHeader = new VolumeHeader();
|
||||
|
||||
// Read the descriptor based on version
|
||||
if (majorVersion <= 5)
|
||||
{
|
||||
volumeHeader.DataOffset = data.ReadUInt32();
|
||||
_ = data.ReadBytes(0x04); // Skip 0x04 bytes, unknown data?
|
||||
volumeHeader.FirstFileIndex = data.ReadUInt32();
|
||||
volumeHeader.LastFileIndex = data.ReadUInt32();
|
||||
volumeHeader.FirstFileOffset = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeCompressed = data.ReadUInt32();
|
||||
volumeHeader.LastFileOffset = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeCompressed = data.ReadUInt32();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Should standard and high values be combined?
|
||||
volumeHeader.DataOffset = data.ReadUInt32();
|
||||
volumeHeader.DataOffsetHigh = data.ReadUInt32();
|
||||
volumeHeader.FirstFileIndex = data.ReadUInt32();
|
||||
volumeHeader.LastFileIndex = data.ReadUInt32();
|
||||
volumeHeader.FirstFileOffset = data.ReadUInt32();
|
||||
volumeHeader.FirstFileOffsetHigh = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeExpandedHigh = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeCompressed = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeCompressedHigh = data.ReadUInt32();
|
||||
volumeHeader.LastFileOffset = data.ReadUInt32();
|
||||
volumeHeader.LastFileOffsetHigh = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeExpandedHigh = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeCompressed = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeCompressedHigh = data.ReadUInt32();
|
||||
}
|
||||
|
||||
return volumeHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a descriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled descriptor on success, null on error</returns>
|
||||
private static Descriptor ParseDescriptor(Stream data)
|
||||
{
|
||||
Descriptor descriptor = new Descriptor();
|
||||
|
||||
descriptor.StringsOffset = data.ReadUInt32();
|
||||
descriptor.Reserved0 = data.ReadBytes(4);
|
||||
descriptor.ComponentListOffset = data.ReadUInt32();
|
||||
descriptor.FileTableOffset = data.ReadUInt32();
|
||||
descriptor.Reserved1 = data.ReadBytes(4);
|
||||
descriptor.FileTableSize = data.ReadUInt32();
|
||||
descriptor.FileTableSize2 = data.ReadUInt32();
|
||||
descriptor.DirectoryCount = data.ReadUInt16();
|
||||
descriptor.Reserved2 = data.ReadBytes(4);
|
||||
descriptor.Reserved3 = data.ReadBytes(2);
|
||||
descriptor.Reserved4 = data.ReadBytes(4);
|
||||
descriptor.FileCount = data.ReadUInt32();
|
||||
descriptor.FileTableOffset2 = data.ReadUInt32();
|
||||
descriptor.ComponentTableInfoCount = data.ReadUInt16();
|
||||
descriptor.ComponentTableOffset = data.ReadUInt32();
|
||||
descriptor.Reserved5 = data.ReadBytes(4);
|
||||
descriptor.Reserved6 = data.ReadBytes(4);
|
||||
|
||||
descriptor.FileGroupOffsets = new uint[MAX_FILE_GROUP_COUNT];
|
||||
for (int i = 0; i < descriptor.FileGroupOffsets.Length; i++)
|
||||
{
|
||||
descriptor.FileGroupOffsets[i] = data.ReadUInt32();
|
||||
}
|
||||
|
||||
descriptor.ComponentOffsets = new uint[MAX_COMPONENT_COUNT];
|
||||
for (int i = 0; i < descriptor.ComponentOffsets.Length; i++)
|
||||
{
|
||||
descriptor.ComponentOffsets[i] = data.ReadUInt32();
|
||||
}
|
||||
|
||||
descriptor.SetupTypesOffset = data.ReadUInt32();
|
||||
descriptor.SetupTableOffset = data.ReadUInt32();
|
||||
descriptor.Reserved7 = data.ReadBytes(4);
|
||||
descriptor.Reserved8 = data.ReadBytes(4);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an offset list
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled offset list on success, null on error</returns>
|
||||
private static OffsetList ParseOffsetList(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
OffsetList offsetList = new OffsetList();
|
||||
|
||||
offsetList.NameOffset = data.ReadUInt32();
|
||||
offsetList.DescriptorOffset = data.ReadUInt32();
|
||||
offsetList.NextOffset = data.ReadUInt32();
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
// Seek to the name offset
|
||||
data.Seek(offsetList.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
offsetList.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
offsetList.Name = data.ReadString(Encoding.ASCII);
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentOffset, SeekOrigin.Begin);
|
||||
|
||||
return offsetList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file group
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled file group on success, null on error</returns>
|
||||
private static FileGroup ParseFileGroup(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
FileGroup fileGroup = new FileGroup();
|
||||
|
||||
fileGroup.NameOffset = data.ReadUInt32();
|
||||
|
||||
fileGroup.ExpandedSize = data.ReadUInt32();
|
||||
fileGroup.Reserved0 = data.ReadBytes(4);
|
||||
fileGroup.CompressedSize = data.ReadUInt32();
|
||||
fileGroup.Reserved1 = data.ReadBytes(4);
|
||||
fileGroup.Reserved2 = data.ReadBytes(2);
|
||||
fileGroup.Attribute1 = data.ReadUInt16();
|
||||
fileGroup.Attribute2 = data.ReadUInt16();
|
||||
|
||||
// TODO: Figure out what data lives in this area for V5 and below
|
||||
if (majorVersion <= 5)
|
||||
data.Seek(0x36, SeekOrigin.Current);
|
||||
|
||||
fileGroup.FirstFile = data.ReadUInt32();
|
||||
fileGroup.LastFile = data.ReadUInt32();
|
||||
fileGroup.UnknownOffset = data.ReadUInt32();
|
||||
fileGroup.Var4Offset = data.ReadUInt32();
|
||||
fileGroup.Var1Offset = data.ReadUInt32();
|
||||
fileGroup.HTTPLocationOffset = data.ReadUInt32();
|
||||
fileGroup.FTPLocationOffset = data.ReadUInt32();
|
||||
fileGroup.MiscOffset = data.ReadUInt32();
|
||||
fileGroup.Var2Offset = data.ReadUInt32();
|
||||
fileGroup.TargetDirectoryOffset = data.ReadUInt32();
|
||||
fileGroup.Reserved3 = data.ReadBytes(2);
|
||||
fileGroup.Reserved4 = data.ReadBytes(2);
|
||||
fileGroup.Reserved5 = data.ReadBytes(2);
|
||||
fileGroup.Reserved6 = data.ReadBytes(2);
|
||||
fileGroup.Reserved7 = data.ReadBytes(2);
|
||||
|
||||
// Cache the current position
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Read the name, if possible
|
||||
if (fileGroup.NameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(fileGroup.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
fileGroup.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
fileGroup.Name = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return fileGroup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a component
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled component on success, null on error</returns>
|
||||
private static Component ParseComponent(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
Component component = new Component();
|
||||
|
||||
component.IdentifierOffset = data.ReadUInt32();
|
||||
component.DescriptorOffset = data.ReadUInt32();
|
||||
component.DisplayNameOffset = data.ReadUInt32();
|
||||
component.Reserved0 = data.ReadBytes(2);
|
||||
component.ReservedOffset0 = data.ReadUInt32();
|
||||
component.ReservedOffset1 = data.ReadUInt32();
|
||||
component.ComponentIndex = data.ReadUInt16();
|
||||
component.NameOffset = data.ReadUInt32();
|
||||
component.ReservedOffset2 = data.ReadUInt32();
|
||||
component.ReservedOffset3 = data.ReadUInt32();
|
||||
component.ReservedOffset4 = data.ReadUInt32();
|
||||
component.Reserved1 = data.ReadBytes(32);
|
||||
component.CLSIDOffset = data.ReadUInt32();
|
||||
component.Reserved2 = data.ReadBytes(28);
|
||||
component.Reserved3 = data.ReadBytes(majorVersion <= 5 ? 2 : 1);
|
||||
component.DependsCount = data.ReadUInt16();
|
||||
component.DependsOffset = data.ReadUInt32();
|
||||
component.FileGroupCount = data.ReadUInt16();
|
||||
component.FileGroupNamesOffset = data.ReadUInt32();
|
||||
component.X3Count = data.ReadUInt16();
|
||||
component.X3Offset = data.ReadUInt32();
|
||||
component.SubComponentsCount = data.ReadUInt16();
|
||||
component.SubComponentsOffset = data.ReadUInt32();
|
||||
component.NextComponentOffset = data.ReadUInt32();
|
||||
component.ReservedOffset5 = data.ReadUInt32();
|
||||
component.ReservedOffset6 = data.ReadUInt32();
|
||||
component.ReservedOffset7 = data.ReadUInt32();
|
||||
component.ReservedOffset8 = data.ReadUInt32();
|
||||
|
||||
// Cache the current position
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Read the identifier, if possible
|
||||
if (component.IdentifierOffset != 0)
|
||||
{
|
||||
// Seek to the identifier
|
||||
data.Seek(component.IdentifierOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
component.Identifier = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
component.Identifier = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Read the display name, if possible
|
||||
if (component.DisplayNameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(component.DisplayNameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
component.DisplayName = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
component.DisplayName = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Read the name, if possible
|
||||
if (component.NameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(component.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
component.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
component.Name = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Read the CLSID, if possible
|
||||
if (component.CLSIDOffset != 0)
|
||||
{
|
||||
// Seek to the CLSID
|
||||
data.Seek(component.CLSIDOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the GUID
|
||||
component.CLSID = data.ReadGuid();
|
||||
}
|
||||
|
||||
// Read the file group names, if possible
|
||||
if (component.FileGroupCount != 0 && component.FileGroupNamesOffset != 0)
|
||||
{
|
||||
// Seek to the file group table offset
|
||||
data.Seek(component.FileGroupNamesOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the file group names table
|
||||
component.FileGroupNames = new string[component.FileGroupCount];
|
||||
for (int j = 0; j < component.FileGroupCount; j++)
|
||||
{
|
||||
// Get the name offset
|
||||
uint nameOffset = data.ReadUInt32();
|
||||
|
||||
// Cache the current offset
|
||||
long preNameOffset = data.Position;
|
||||
|
||||
// Seek to the name offset
|
||||
data.Seek(nameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
if (majorVersion >= 17)
|
||||
component.FileGroupNames[j] = data.ReadString(Encoding.Unicode) ?? string.Empty;
|
||||
else
|
||||
component.FileGroupNames[j] = data.ReadString(Encoding.ASCII) ?? string.Empty;
|
||||
|
||||
// Seek back to the original position
|
||||
data.Seek(preNameOffset, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a directory name
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <returns>Filled directory name on success, null on error</returns>
|
||||
private static string? ParseDirectoryName(Stream data, int majorVersion)
|
||||
{
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
return data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
return data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file descriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled file descriptor on success, null on error</returns>
|
||||
private static FileDescriptor ParseFileDescriptor(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
FileDescriptor fileDescriptor = new FileDescriptor();
|
||||
|
||||
// Read the descriptor based on version
|
||||
if (majorVersion <= 5)
|
||||
{
|
||||
fileDescriptor.Volume = 0xFFFF; // Set by the header index
|
||||
fileDescriptor.NameOffset = data.ReadUInt32();
|
||||
fileDescriptor.DirectoryIndex = data.ReadUInt32();
|
||||
fileDescriptor.Flags = (FileFlags)data.ReadUInt16();
|
||||
fileDescriptor.ExpandedSize = data.ReadUInt32();
|
||||
fileDescriptor.CompressedSize = data.ReadUInt32();
|
||||
_ = data.ReadBytes(0x14); // Skip 0x14 bytes, unknown data?
|
||||
fileDescriptor.DataOffset = data.ReadUInt32();
|
||||
|
||||
if (majorVersion == 5)
|
||||
fileDescriptor.MD5 = data.ReadBytes(0x10);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileDescriptor.Flags = (FileFlags)data.ReadUInt16();
|
||||
fileDescriptor.ExpandedSize = data.ReadUInt64();
|
||||
fileDescriptor.CompressedSize = data.ReadUInt64();
|
||||
fileDescriptor.DataOffset = data.ReadUInt64();
|
||||
fileDescriptor.MD5 = data.ReadBytes(0x10);
|
||||
_ = data.ReadBytes(0x10); // Skip 0x10 bytes, unknown data?
|
||||
fileDescriptor.NameOffset = data.ReadUInt32();
|
||||
fileDescriptor.DirectoryIndex = data.ReadUInt16();
|
||||
_ = data.ReadBytes(0x0C); // Skip 0x0C bytes, unknown data?
|
||||
fileDescriptor.LinkPrevious = data.ReadUInt32();
|
||||
fileDescriptor.LinkNext = data.ReadUInt32();
|
||||
fileDescriptor.LinkFlags = (LinkFlags)data.ReadByteValue();
|
||||
fileDescriptor.Volume = data.ReadUInt16();
|
||||
}
|
||||
|
||||
// Cache the current position
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Read the name, if possible
|
||||
if (fileDescriptor.NameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(fileDescriptor.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
fileDescriptor.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
fileDescriptor.Name = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Get the major version of the cabinet
|
||||
/// </summary>
|
||||
/// <remarks>This should live in the wrapper but is needed during parsing</remarks>
|
||||
private static int GetMajorVersion(CommonHeader commonHeader)
|
||||
{
|
||||
uint majorVersion = commonHeader.Version;
|
||||
if (majorVersion >> 24 == 1)
|
||||
{
|
||||
majorVersion = (majorVersion >> 12) & 0x0F;
|
||||
}
|
||||
else if (majorVersion >> 24 == 2 || majorVersion >> 24 == 4)
|
||||
{
|
||||
majorVersion = majorVersion & 0xFFFF;
|
||||
if (majorVersion != 0)
|
||||
majorVersion /= 100;
|
||||
}
|
||||
|
||||
return (int)majorVersion;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
@@ -7,7 +9,9 @@ namespace SabreTools.Serialization.Deserializers
|
||||
/// Base class for other JSON serializers
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class JsonFile<T> : IFileDeserializer<T>
|
||||
public class JsonFile<T> :
|
||||
IFileDeserializer<T>,
|
||||
IStreamDeserializer<T>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
@@ -25,7 +29,37 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public T? Deserialize(string? path, Encoding encoding)
|
||||
{
|
||||
using var data = PathProcessor.OpenStream(path);
|
||||
return new Streams.JsonFile<T>().Deserialize(data, encoding);
|
||||
return Deserialize(data, encoding);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual T? Deserialize(Stream? data)
|
||||
=> Deserialize(data, new UTF8Encoding(false));
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a Stream into <typeparamref name="T"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of object to deserialize to</typeparam>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="encoding">Text encoding to use</param>
|
||||
/// <returns>Filled object on success, null on error</returns>
|
||||
public T? Deserialize(Stream? data, Encoding encoding)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the serializer and the reader
|
||||
var serializer = JsonSerializer.Create();
|
||||
var streamReader = new StreamReader(data, encoding);
|
||||
var jsonReader = new JsonTextReader(streamReader);
|
||||
|
||||
// Perform the deserialization and return
|
||||
return serializer.Deserialize<T>(jsonReader);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,271 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.Models.Listrom;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Listrom : IFileDeserializer<Models.Listrom.MetadataFile>
|
||||
public class Listrom :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.Listrom.MetadataFile? DeserializeFile(string? path)
|
||||
public static MetadataFile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new Listrom();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Listrom.MetadataFile? Deserialize(string? path)
|
||||
public MetadataFile? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.Listrom.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Listrom();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new StreamReader(data, Encoding.UTF8);
|
||||
var dat = new MetadataFile();
|
||||
|
||||
Set? set = null;
|
||||
var sets = new List<Set>();
|
||||
var rows = new List<Row>();
|
||||
|
||||
var additional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// Read the line and don't split yet
|
||||
string? line = reader.ReadLine();
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
// If we have a set to process
|
||||
if (set != null)
|
||||
{
|
||||
set.Row = rows.ToArray();
|
||||
sets.Add(set);
|
||||
set = null;
|
||||
rows.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set lines are unique
|
||||
if (line.StartsWith("ROMs required for driver"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string driver = line.Substring("ROMs required for driver".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string driver = line["ROMs required for driver".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Driver = driver };
|
||||
continue;
|
||||
}
|
||||
else if (line.StartsWith("No ROMs required for driver"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string driver = line.Substring("No ROMs required for driver".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string driver = line["No ROMs required for driver".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Driver = driver };
|
||||
continue;
|
||||
}
|
||||
else if (line.StartsWith("ROMs required for device"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string device = line.Substring("ROMs required for device".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string device = line["ROMs required for device".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Device = device };
|
||||
continue;
|
||||
}
|
||||
else if (line.StartsWith("No ROMs required for device"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string device = line.Substring("No ROMs required for device".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string device = line["No ROMs required for device".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Device = device };
|
||||
continue;
|
||||
}
|
||||
else if (line.Equals("Name Size Checksum", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// No-op
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the line for the name iteratively
|
||||
#if NETFRAMEWORK || NETCOREAPP3_1
|
||||
string[] lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
#else
|
||||
string[] lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
#endif
|
||||
|
||||
// Read the name and set the rest of the line for processing
|
||||
string name = lineParts[0];
|
||||
#if NETFRAMEWORK
|
||||
string trimmedLine = line.Substring(name.Length);
|
||||
#else
|
||||
string trimmedLine = line[name.Length..];
|
||||
#endif
|
||||
if (trimmedLine == null)
|
||||
continue;
|
||||
|
||||
#if NETFRAMEWORK || NETCOREAPP3_1
|
||||
lineParts = trimmedLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
#else
|
||||
lineParts = trimmedLine.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
#endif
|
||||
|
||||
// The number of items in the row explains what type of row it is
|
||||
var row = new Row();
|
||||
switch (lineParts.Length)
|
||||
{
|
||||
// Normal CHD (Name, MD5/SHA1)
|
||||
case 1:
|
||||
row.Name = name;
|
||||
#if NETFRAMEWORK
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[0].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[0].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[0]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[0]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Normal ROM (Name, Size, CRC, MD5/SHA1)
|
||||
case 3 when line.Contains("CRC"):
|
||||
row.Name = name;
|
||||
row.Size = lineParts[0];
|
||||
#if NETFRAMEWORK
|
||||
row.CRC = lineParts[1].Substring("CRC".Length).Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[2].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[2].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
row.CRC = lineParts[1]["CRC".Length..].Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[2]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[2]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Bad CHD (Name, BAD, SHA1, BAD_DUMP)
|
||||
case 3 when line.Contains("BAD_DUMP"):
|
||||
row.Name = name;
|
||||
row.Bad = true;
|
||||
#if NETFRAMEWORK
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[1].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[1].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[1]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[1]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Nodump CHD (Name, NO GOOD DUMP KNOWN)
|
||||
case 4 when line.Contains("NO GOOD DUMP KNOWN"):
|
||||
row.Name = name;
|
||||
row.NoGoodDumpKnown = true;
|
||||
break;
|
||||
|
||||
// Bad ROM (Name, Size, BAD, CRC, MD5/SHA1, BAD_DUMP)
|
||||
case 5 when line.Contains("BAD_DUMP"):
|
||||
row.Name = name;
|
||||
row.Size = lineParts[0];
|
||||
row.Bad = true;
|
||||
#if NETFRAMEWORK
|
||||
row.CRC = lineParts[2].Substring("CRC".Length).Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[3].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[3].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
row.CRC = lineParts[2]["CRC".Length..].Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[3]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[3]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Nodump ROM (Name, Size, NO GOOD DUMP KNOWN)
|
||||
case 5 when line.Contains("NO GOOD DUMP KNOWN"):
|
||||
row.Name = name;
|
||||
row.Size = lineParts[0];
|
||||
row.NoGoodDumpKnown = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
row = null;
|
||||
additional.Add(line);
|
||||
break;
|
||||
}
|
||||
|
||||
if (row != null)
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
// If we have a set to process
|
||||
if (set != null)
|
||||
{
|
||||
set.Row = rows.ToArray();
|
||||
sets.Add(set);
|
||||
set = null;
|
||||
rows.Clear();
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.Set = sets.ToArray();
|
||||
dat.ADDITIONAL_ELEMENTS = additional.ToArray();
|
||||
return dat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Listxml : XmlFile<Models.Listxml.Mame>
|
||||
public class Listxml :
|
||||
XmlFile<Models.Listxml.Mame>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.Listxml.Mame? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new Listxml();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.Listxml.Mame? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new Listxml();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Logiqx : XmlFile<Models.Logiqx.Datafile>
|
||||
public class Logiqx :
|
||||
XmlFile<Models.Logiqx.Datafile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.Logiqx.Datafile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new Logiqx();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.Logiqx.Datafile? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new Logiqx();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class M1 : XmlFile<Models.Listxml.M1>
|
||||
public class M1 :
|
||||
XmlFile<Models.Listxml.M1>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.Listxml.M1? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new M1();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.Listxml.M1? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new M1();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.MSDOS;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.MSDOS.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class MSDOS :
|
||||
IByteDeserializer<Models.MSDOS.Executable>,
|
||||
IFileDeserializer<Models.MSDOS.Executable>
|
||||
IByteDeserializer<Executable>,
|
||||
IFileDeserializer<Executable>,
|
||||
IStreamDeserializer<Executable>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.MSDOS.Executable? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Executable? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new MSDOS();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.MSDOS.Executable? Deserialize(byte[]? data, int offset)
|
||||
public Executable? 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.MSDOS.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +42,162 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.MSDOS.Executable? DeserializeFile(string? path)
|
||||
public static Executable? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new MSDOS();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.MSDOS.Executable? Deserialize(string? path)
|
||||
public Executable? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.MSDOS.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Executable? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new MSDOS();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Executable? 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 executable to fill
|
||||
var executable = new Executable();
|
||||
|
||||
#region Executable Header
|
||||
|
||||
// Try to parse the executable header
|
||||
var executableHeader = ParseExecutableHeader(data);
|
||||
if (executableHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the executable header
|
||||
executable.Header = executableHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Relocation Table
|
||||
|
||||
// If the offset for the relocation table doesn't exist
|
||||
int tableAddress = initialOffset + executableHeader.RelocationTableAddr;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the relocation table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var relocationTable = ParseRelocationTable(data, executableHeader.RelocationItems);
|
||||
if (relocationTable == null)
|
||||
return null;
|
||||
|
||||
// Set the relocation table
|
||||
executable.RelocationTable = relocationTable;
|
||||
|
||||
#endregion
|
||||
|
||||
// Return the executable
|
||||
return executable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an MS-DOS executable header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled executable header on success, null on error</returns>
|
||||
private static ExecutableHeader? ParseExecutableHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new ExecutableHeader();
|
||||
|
||||
#region Standard Fields
|
||||
|
||||
byte[]? magic = data.ReadBytes(2);
|
||||
if (magic == null)
|
||||
return null;
|
||||
|
||||
header.Magic = Encoding.ASCII.GetString(magic);
|
||||
if (header.Magic != SignatureString)
|
||||
return null;
|
||||
|
||||
header.LastPageBytes = data.ReadUInt16();
|
||||
header.Pages = data.ReadUInt16();
|
||||
header.RelocationItems = data.ReadUInt16();
|
||||
header.HeaderParagraphSize = data.ReadUInt16();
|
||||
header.MinimumExtraParagraphs = data.ReadUInt16();
|
||||
header.MaximumExtraParagraphs = data.ReadUInt16();
|
||||
header.InitialSSValue = data.ReadUInt16();
|
||||
header.InitialSPValue = data.ReadUInt16();
|
||||
header.Checksum = data.ReadUInt16();
|
||||
header.InitialIPValue = data.ReadUInt16();
|
||||
header.InitialCSValue = data.ReadUInt16();
|
||||
header.RelocationTableAddr = data.ReadUInt16();
|
||||
header.OverlayNumber = data.ReadUInt16();
|
||||
|
||||
#endregion
|
||||
|
||||
// If we don't have enough data for PE extensions
|
||||
if (data.Position >= data.Length || data.Length - data.Position < 36)
|
||||
return header;
|
||||
|
||||
#region PE Extensions
|
||||
|
||||
header.Reserved1 = new ushort[4];
|
||||
for (int i = 0; i < header.Reserved1.Length; i++)
|
||||
{
|
||||
header.Reserved1[i] = data.ReadUInt16();
|
||||
}
|
||||
header.OEMIdentifier = data.ReadUInt16();
|
||||
header.OEMInformation = data.ReadUInt16();
|
||||
header.Reserved2 = new ushort[10];
|
||||
for (int i = 0; i < header.Reserved2.Length; i++)
|
||||
{
|
||||
header.Reserved2[i] = data.ReadUInt16();
|
||||
}
|
||||
header.NewExeHeaderAddr = data.ReadUInt32();
|
||||
|
||||
#endregion
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a relocation table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of relocation table entries to read</param>
|
||||
/// <returns>Filled relocation table on success, null on error</returns>
|
||||
private static RelocationEntry[] ParseRelocationTable(Stream data, int count)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var relocationTable = new RelocationEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = new RelocationEntry();
|
||||
entry.Offset = data.ReadUInt16();
|
||||
entry.Segment = data.ReadUInt16();
|
||||
relocationTable[i] = entry;
|
||||
}
|
||||
|
||||
return relocationTable;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.MicrosoftCabinet;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.MicrosoftCabinet.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
// TODO: Add multi-cabinet reading
|
||||
public class MicrosoftCabinet :
|
||||
IByteDeserializer<Models.MicrosoftCabinet.Cabinet>,
|
||||
IFileDeserializer<Models.MicrosoftCabinet.Cabinet>
|
||||
IByteDeserializer<Cabinet>,
|
||||
IFileDeserializer<Cabinet>,
|
||||
IStreamDeserializer<Cabinet>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.MicrosoftCabinet.Cabinet? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Cabinet? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new MicrosoftCabinet();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.MicrosoftCabinet.Cabinet? Deserialize(byte[]? data, int offset)
|
||||
public Cabinet? Deserialize(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null)
|
||||
@@ -30,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.MicrosoftCabinet.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -38,17 +43,260 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.MicrosoftCabinet.Cabinet? DeserializeFile(string? path)
|
||||
public static Cabinet? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new MicrosoftCabinet();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.MicrosoftCabinet.Cabinet? Deserialize(string? path)
|
||||
public Cabinet? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.MicrosoftCabinet.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Cabinet? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new MicrosoftCabinet();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cabinet? 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 cabinet to fill
|
||||
var cabinet = new Cabinet();
|
||||
|
||||
#region Cabinet Header
|
||||
|
||||
// Try to parse the cabinet header
|
||||
var cabinetHeader = ParseCabinetHeader(data);
|
||||
if (cabinetHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the cabinet header
|
||||
cabinet.Header = cabinetHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Folders
|
||||
|
||||
// Set the folder array
|
||||
cabinet.Folders = new CFFOLDER[cabinetHeader.FolderCount];
|
||||
|
||||
// Try to parse each folder, if we have any
|
||||
for (int i = 0; i < cabinetHeader.FolderCount; i++)
|
||||
{
|
||||
var folder = ParseFolder(data, cabinetHeader);
|
||||
if (folder == null)
|
||||
return null;
|
||||
|
||||
// Set the folder
|
||||
cabinet.Folders[i] = folder;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Files
|
||||
|
||||
// Get the files offset
|
||||
int filesOffset = (int)cabinetHeader.FilesOffset + initialOffset;
|
||||
if (filesOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the offset
|
||||
data.Seek(filesOffset, SeekOrigin.Begin);
|
||||
|
||||
// Set the file array
|
||||
cabinet.Files = new CFFILE[cabinetHeader.FileCount];
|
||||
|
||||
// Try to parse each file, if we have any
|
||||
for (int i = 0; i < cabinetHeader.FileCount; i++)
|
||||
{
|
||||
var file = ParseFile(data);
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
// Set the file
|
||||
cabinet.Files[i] = file;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return cabinet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a cabinet header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled cabinet header on success, null on error</returns>
|
||||
private static CFHEADER? ParseCabinetHeader(Stream data)
|
||||
{
|
||||
var header = new CFHEADER();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.Reserved1 = data.ReadUInt32();
|
||||
header.CabinetSize = data.ReadUInt32();
|
||||
header.Reserved2 = data.ReadUInt32();
|
||||
header.FilesOffset = data.ReadUInt32();
|
||||
header.Reserved3 = data.ReadUInt32();
|
||||
header.VersionMinor = data.ReadByteValue();
|
||||
header.VersionMajor = data.ReadByteValue();
|
||||
header.FolderCount = data.ReadUInt16();
|
||||
header.FileCount = data.ReadUInt16();
|
||||
header.Flags = (HeaderFlags)data.ReadUInt16();
|
||||
header.SetID = data.ReadUInt16();
|
||||
header.CabinetIndex = data.ReadUInt16();
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((header.Flags & HeaderFlags.RESERVE_PRESENT) != 0)
|
||||
#else
|
||||
if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT))
|
||||
#endif
|
||||
{
|
||||
header.HeaderReservedSize = data.ReadUInt16();
|
||||
if (header.HeaderReservedSize > 60_000)
|
||||
return null;
|
||||
|
||||
header.FolderReservedSize = data.ReadByteValue();
|
||||
header.DataReservedSize = data.ReadByteValue();
|
||||
|
||||
if (header.HeaderReservedSize > 0)
|
||||
header.ReservedData = data.ReadBytes(header.HeaderReservedSize);
|
||||
}
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((header.Flags & HeaderFlags.PREV_CABINET) != 0)
|
||||
#else
|
||||
if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET))
|
||||
#endif
|
||||
{
|
||||
header.CabinetPrev = data.ReadString(Encoding.ASCII);
|
||||
header.DiskPrev = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((header.Flags & HeaderFlags.NEXT_CABINET) != 0)
|
||||
#else
|
||||
if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET))
|
||||
#endif
|
||||
{
|
||||
header.CabinetNext = data.ReadString(Encoding.ASCII);
|
||||
header.DiskNext = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a folder
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="header">Cabinet header to get flags and sizes from</param>
|
||||
/// <returns>Filled folder on success, null on error</returns>
|
||||
private static CFFOLDER ParseFolder(Stream data, CFHEADER header)
|
||||
{
|
||||
var folder = new CFFOLDER();
|
||||
|
||||
folder.CabStartOffset = data.ReadUInt32();
|
||||
folder.DataCount = data.ReadUInt16();
|
||||
folder.CompressionType = (CompressionType)data.ReadUInt16();
|
||||
|
||||
if (header.FolderReservedSize > 0)
|
||||
folder.ReservedData = data.ReadBytes(header.FolderReservedSize);
|
||||
|
||||
if (folder.CabStartOffset > 0)
|
||||
{
|
||||
long currentPosition = data.Position;
|
||||
data.Seek(folder.CabStartOffset, SeekOrigin.Begin);
|
||||
|
||||
folder.DataBlocks = new CFDATA[folder.DataCount];
|
||||
for (int i = 0; i < folder.DataCount; i++)
|
||||
{
|
||||
CFDATA dataBlock = ParseDataBlock(data, header.DataReservedSize);
|
||||
folder.DataBlocks[i] = dataBlock;
|
||||
}
|
||||
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a data block
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="dataReservedSize">Reserved byte size for data blocks</param>
|
||||
/// <returns>Filled folder on success, null on error</returns>
|
||||
private static CFDATA ParseDataBlock(Stream data, byte dataReservedSize)
|
||||
{
|
||||
var dataBlock = new CFDATA();
|
||||
|
||||
dataBlock.Checksum = data.ReadUInt32();
|
||||
dataBlock.CompressedSize = data.ReadUInt16();
|
||||
dataBlock.UncompressedSize = data.ReadUInt16();
|
||||
|
||||
if (dataReservedSize > 0)
|
||||
dataBlock.ReservedData = data.ReadBytes(dataReservedSize);
|
||||
|
||||
if (dataBlock.CompressedSize > 0)
|
||||
dataBlock.CompressedData = data.ReadBytes(dataBlock.CompressedSize);
|
||||
|
||||
return dataBlock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled file on success, null on error</returns>
|
||||
private static CFFILE ParseFile(Stream data)
|
||||
{
|
||||
var file = new CFFILE();
|
||||
|
||||
file.FileSize = data.ReadUInt32();
|
||||
file.FolderStartOffset = data.ReadUInt32();
|
||||
file.FolderIndex = (FolderIndex)data.ReadUInt16();
|
||||
file.Date = data.ReadUInt16();
|
||||
file.Time = data.ReadUInt16();
|
||||
file.Attributes = (Models.MicrosoftCabinet.FileAttributes)data.ReadUInt16();
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((file.Attributes & Models.MicrosoftCabinet.FileAttributes.NAME_IS_UTF) != 0)
|
||||
#else
|
||||
if (file.Attributes.HasFlag(Models.MicrosoftCabinet.FileAttributes.NAME_IS_UTF))
|
||||
#endif
|
||||
file.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
file.Name = data.ReadString(Encoding.ASCII);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.MoPaQ;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.MoPaQ.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class MoPaQ :
|
||||
IByteDeserializer<Models.MoPaQ.Archive>,
|
||||
IFileDeserializer<Models.MoPaQ.Archive>
|
||||
IByteDeserializer<Archive>,
|
||||
IFileDeserializer<Archive>,
|
||||
IStreamDeserializer<Archive>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.MoPaQ.Archive? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Archive? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new MoPaQ();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.MoPaQ.Archive? Deserialize(byte[]? data, int offset)
|
||||
public Archive? 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.MoPaQ.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,19 +44,648 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.MoPaQ.Archive? DeserializeFile(string? path)
|
||||
public static Archive? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new MoPaQ();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.MoPaQ.Archive? Deserialize(string? path)
|
||||
public Archive? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.MoPaQ.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new MoPaQ();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 User Data
|
||||
|
||||
// Check for User Data
|
||||
uint possibleSignature = data.ReadUInt32();
|
||||
data.Seek(-4, SeekOrigin.Current);
|
||||
if (possibleSignature == 0x1B51504D)
|
||||
{
|
||||
// Save the current position for offset correction
|
||||
long basePtr = data.Position;
|
||||
|
||||
// Deserialize the user data, returning null if invalid
|
||||
var userData = ParseUserData(data);
|
||||
if (userData == null)
|
||||
return null;
|
||||
|
||||
// Set the user data
|
||||
archive.UserData = userData;
|
||||
|
||||
// Set the starting position according to the header offset
|
||||
data.Seek(basePtr + (int)archive.UserData.HeaderOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive Header
|
||||
|
||||
// Check for the Header
|
||||
possibleSignature = data.ReadUInt32();
|
||||
data.Seek(-4, SeekOrigin.Current);
|
||||
if (possibleSignature == 0x1A51504D)
|
||||
{
|
||||
// Try to parse the archive header
|
||||
var archiveHeader = ParseArchiveHeader(data);
|
||||
if (archiveHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the archive header
|
||||
archive.ArchiveHeader = archiveHeader;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hash Table
|
||||
|
||||
// TODO: The hash table has to be be decrypted before reading
|
||||
|
||||
// Version 1
|
||||
if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1)
|
||||
{
|
||||
// If we have a hash table
|
||||
long hashTableOffset = archive.ArchiveHeader.HashTablePosition;
|
||||
if (hashTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hashTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long hashTableEnd = hashTableOffset + archive.ArchiveHeader.HashTableSize;
|
||||
|
||||
// Read in the hash table
|
||||
var hashTable = new List<HashEntry>();
|
||||
|
||||
while (data.Position < hashTableEnd)
|
||||
{
|
||||
var hashEntry = ParseHashEntry(data);
|
||||
if (hashEntry == null)
|
||||
return null;
|
||||
|
||||
hashTable.Add(hashEntry);
|
||||
}
|
||||
|
||||
archive.HashTable = hashTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 2 and 3
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3)
|
||||
{
|
||||
// If we have a hash table
|
||||
long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition;
|
||||
if (hashTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hashTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long hashTableEnd = hashTableOffset + archive.ArchiveHeader.HashTableSize;
|
||||
|
||||
// Read in the hash table
|
||||
var hashTable = new List<HashEntry>();
|
||||
|
||||
while (data.Position < hashTableEnd)
|
||||
{
|
||||
var hashEntry = ParseHashEntry(data);
|
||||
if (hashEntry == null)
|
||||
return null;
|
||||
|
||||
hashTable.Add(hashEntry);
|
||||
}
|
||||
|
||||
archive.HashTable = hashTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 4
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4)
|
||||
{
|
||||
// If we have a hash table
|
||||
long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition;
|
||||
if (hashTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hashTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long hashTableEnd = hashTableOffset + (long)archive.ArchiveHeader.HashTableSizeLong;
|
||||
|
||||
// Read in the hash table
|
||||
var hashTable = new List<HashEntry>();
|
||||
|
||||
while (data.Position < hashTableEnd)
|
||||
{
|
||||
var hashEntry = ParseHashEntry(data);
|
||||
if (hashEntry == null)
|
||||
return null;
|
||||
|
||||
hashTable.Add(hashEntry);
|
||||
}
|
||||
|
||||
archive.HashTable = hashTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Table
|
||||
|
||||
// Version 1
|
||||
if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1)
|
||||
{
|
||||
// If we have a block table
|
||||
long blockTableOffset = archive.ArchiveHeader.BlockTablePosition;
|
||||
if (blockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(blockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long blockTableEnd = blockTableOffset + archive.ArchiveHeader.BlockTableSize;
|
||||
|
||||
// Read in the block table
|
||||
var blockTable = new List<BlockEntry>();
|
||||
|
||||
while (data.Position < blockTableEnd)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
if (blockEntry == null)
|
||||
return null;
|
||||
|
||||
blockTable.Add(blockEntry);
|
||||
}
|
||||
|
||||
archive.BlockTable = blockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 2 and 3
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3)
|
||||
{
|
||||
// If we have a block table
|
||||
long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition;
|
||||
if (blockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(blockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long blockTableEnd = blockTableOffset + archive.ArchiveHeader.BlockTableSize;
|
||||
|
||||
// Read in the block table
|
||||
var blockTable = new List<BlockEntry>();
|
||||
|
||||
while (data.Position < blockTableEnd)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
if (blockEntry == null)
|
||||
return null;
|
||||
|
||||
blockTable.Add(blockEntry);
|
||||
}
|
||||
|
||||
archive.BlockTable = blockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 4
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4)
|
||||
{
|
||||
// If we have a block table
|
||||
long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition;
|
||||
if (blockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(blockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long blockTableEnd = blockTableOffset + (long)archive.ArchiveHeader.BlockTableSizeLong;
|
||||
|
||||
// Read in the block table
|
||||
var blockTable = new List<BlockEntry>();
|
||||
|
||||
while (data.Position < blockTableEnd)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
if (blockEntry == null)
|
||||
return null;
|
||||
|
||||
blockTable.Add(blockEntry);
|
||||
}
|
||||
|
||||
archive.BlockTable = blockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hi-Block Table
|
||||
|
||||
// Version 2, 3, and 4
|
||||
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format2)
|
||||
{
|
||||
// If we have a hi-block table
|
||||
long hiBlockTableOffset = (long)archive.ArchiveHeader.HiBlockTablePosition;
|
||||
if (hiBlockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hiBlockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read in the hi-block table
|
||||
var hiBlockTable = new List<short>();
|
||||
|
||||
for (int i = 0; i < (archive.BlockTable?.Length ?? 0); i++)
|
||||
{
|
||||
short hiBlockEntry = data.ReadInt16();
|
||||
hiBlockTable.Add(hiBlockEntry);
|
||||
}
|
||||
|
||||
archive.HiBlockTable = hiBlockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BET Table
|
||||
|
||||
// Version 3 and 4
|
||||
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3)
|
||||
{
|
||||
// If we have a BET table
|
||||
long betTableOffset = (long)archive.ArchiveHeader.BetTablePosition;
|
||||
if (betTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(betTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read in the BET table
|
||||
var betTable = ParseBetTable(data);
|
||||
if (betTable != null)
|
||||
return null;
|
||||
|
||||
archive.BetTable = betTable;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HET Table
|
||||
|
||||
// Version 3 and 4
|
||||
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3)
|
||||
{
|
||||
// If we have a HET table
|
||||
long hetTableOffset = (long)archive.ArchiveHeader.HetTablePosition;
|
||||
if (hetTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hetTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read in the HET table
|
||||
var hetTable = ParseHetTable(data);
|
||||
if (hetTable != null)
|
||||
return null;
|
||||
|
||||
archive.HetTable = hetTable;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a archive header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled archive header on success, null on error</returns>
|
||||
private static ArchiveHeader? ParseArchiveHeader(Stream data)
|
||||
{
|
||||
ArchiveHeader archiveHeader = new ArchiveHeader();
|
||||
|
||||
// V1 - Common
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
archiveHeader.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (archiveHeader.Signature != ArchiveHeaderSignatureString)
|
||||
return null;
|
||||
|
||||
archiveHeader.HeaderSize = data.ReadUInt32();
|
||||
archiveHeader.ArchiveSize = data.ReadUInt32();
|
||||
archiveHeader.FormatVersion = (FormatVersion)data.ReadUInt16();
|
||||
archiveHeader.BlockSize = data.ReadUInt16();
|
||||
archiveHeader.HashTablePosition = data.ReadUInt32();
|
||||
archiveHeader.BlockTablePosition = data.ReadUInt32();
|
||||
archiveHeader.HashTableSize = data.ReadUInt32();
|
||||
archiveHeader.BlockTableSize = data.ReadUInt32();
|
||||
|
||||
// V2
|
||||
if (archiveHeader.FormatVersion >= FormatVersion.Format2)
|
||||
{
|
||||
archiveHeader.HiBlockTablePosition = data.ReadUInt64();
|
||||
archiveHeader.HashTablePositionHi = data.ReadUInt16();
|
||||
archiveHeader.BlockTablePositionHi = data.ReadUInt16();
|
||||
}
|
||||
|
||||
// V3
|
||||
if (archiveHeader.FormatVersion >= FormatVersion.Format3)
|
||||
{
|
||||
archiveHeader.ArchiveSizeLong = data.ReadUInt64();
|
||||
archiveHeader.BetTablePosition = data.ReadUInt64();
|
||||
archiveHeader.HetTablePosition = data.ReadUInt64();
|
||||
}
|
||||
|
||||
// V4
|
||||
if (archiveHeader.FormatVersion >= FormatVersion.Format4)
|
||||
{
|
||||
archiveHeader.HashTableSizeLong = data.ReadUInt64();
|
||||
archiveHeader.BlockTableSizeLong = data.ReadUInt64();
|
||||
archiveHeader.HiBlockTableSize = data.ReadUInt64();
|
||||
archiveHeader.HetTableSize = data.ReadUInt64();
|
||||
archiveHeader.BetTablesize = data.ReadUInt64();
|
||||
archiveHeader.RawChunkSize = data.ReadUInt32();
|
||||
|
||||
archiveHeader.BlockTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HashTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HiBlockTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.BetTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HetTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HetTableMD5 = data.ReadBytes(0x10);
|
||||
}
|
||||
|
||||
return archiveHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a user data object
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled user data on success, null on error</returns>
|
||||
private static UserData? ParseUserData(Stream data)
|
||||
{
|
||||
UserData userData = new UserData();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
userData.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (userData.Signature != UserDataSignatureString)
|
||||
return null;
|
||||
|
||||
userData.UserDataSize = data.ReadUInt32();
|
||||
userData.HeaderOffset = data.ReadUInt32();
|
||||
userData.UserDataHeaderSize = data.ReadUInt32();
|
||||
|
||||
return userData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a HET table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled HET table on success, null on error</returns>
|
||||
private static HetTable? ParseHetTable(Stream data)
|
||||
{
|
||||
HetTable hetTable = new HetTable();
|
||||
|
||||
// Common Headers
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
hetTable.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (hetTable.Signature != HetTableSignatureString)
|
||||
return null;
|
||||
|
||||
hetTable.Version = data.ReadUInt32();
|
||||
hetTable.DataSize = data.ReadUInt32();
|
||||
|
||||
// HET-Specific
|
||||
hetTable.TableSize = data.ReadUInt32();
|
||||
hetTable.MaxFileCount = data.ReadUInt32();
|
||||
hetTable.HashTableSize = data.ReadUInt32();
|
||||
hetTable.TotalIndexSize = data.ReadUInt32();
|
||||
hetTable.IndexSizeExtra = data.ReadUInt32();
|
||||
hetTable.IndexSize = data.ReadUInt32();
|
||||
hetTable.BlockTableSize = data.ReadUInt32();
|
||||
hetTable.HashTable = data.ReadBytes((int)hetTable.HashTableSize);
|
||||
|
||||
// TODO: Populate the file indexes array
|
||||
hetTable.FileIndexes = new byte[(int)hetTable.HashTableSize][];
|
||||
|
||||
return hetTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a BET table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled BET table on success, null on error</returns>
|
||||
private static BetTable? ParseBetTable(Stream data)
|
||||
{
|
||||
BetTable betTable = new BetTable();
|
||||
|
||||
// Common Headers
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
betTable.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (betTable.Signature != BetTableSignatureString)
|
||||
return null;
|
||||
|
||||
betTable.Version = data.ReadUInt32();
|
||||
betTable.DataSize = data.ReadUInt32();
|
||||
|
||||
// BET-Specific
|
||||
betTable.TableSize = data.ReadUInt32();
|
||||
betTable.FileCount = data.ReadUInt32();
|
||||
betTable.Unknown = data.ReadUInt32();
|
||||
betTable.TableEntrySize = data.ReadUInt32();
|
||||
|
||||
betTable.FilePositionBitIndex = data.ReadUInt32();
|
||||
betTable.FileSizeBitIndex = data.ReadUInt32();
|
||||
betTable.CompressedSizeBitIndex = data.ReadUInt32();
|
||||
betTable.FlagIndexBitIndex = data.ReadUInt32();
|
||||
betTable.UnknownBitIndex = data.ReadUInt32();
|
||||
|
||||
betTable.FilePositionBitCount = data.ReadUInt32();
|
||||
betTable.FileSizeBitCount = data.ReadUInt32();
|
||||
betTable.CompressedSizeBitCount = data.ReadUInt32();
|
||||
betTable.FlagIndexBitCount = data.ReadUInt32();
|
||||
betTable.UnknownBitCount = data.ReadUInt32();
|
||||
|
||||
betTable.TotalBetHashSize = data.ReadUInt32();
|
||||
betTable.BetHashSizeExtra = data.ReadUInt32();
|
||||
betTable.BetHashSize = data.ReadUInt32();
|
||||
betTable.BetHashArraySize = data.ReadUInt32();
|
||||
betTable.FlagCount = data.ReadUInt32();
|
||||
|
||||
betTable.FlagsArray = new uint[betTable.FlagCount];
|
||||
byte[]? flagsArray = data.ReadBytes((int)betTable.FlagCount * 4);
|
||||
if (flagsArray != null)
|
||||
Buffer.BlockCopy(flagsArray, 0, betTable.FlagsArray, 0, (int)betTable.FlagCount * 4);
|
||||
|
||||
// TODO: Populate the file table
|
||||
// TODO: Populate the hash table
|
||||
|
||||
return betTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a hash entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled hash entry on success, null on error</returns>
|
||||
private static HashEntry ParseHashEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
HashEntry hashEntry = new HashEntry();
|
||||
|
||||
hashEntry.NameHashPartA = data.ReadUInt32();
|
||||
hashEntry.NameHashPartB = data.ReadUInt32();
|
||||
hashEntry.Locale = (Locale)data.ReadUInt16();
|
||||
hashEntry.Platform = data.ReadUInt16();
|
||||
hashEntry.BlockIndex = data.ReadUInt32();
|
||||
|
||||
return hashEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a block entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled block entry on success, null on error</returns>
|
||||
private static BlockEntry ParseBlockEntry(Stream data)
|
||||
{
|
||||
BlockEntry blockEntry = new BlockEntry();
|
||||
|
||||
blockEntry.FilePosition = data.ReadUInt32();
|
||||
blockEntry.CompressedSize = data.ReadUInt32();
|
||||
blockEntry.UncompressedSize = data.ReadUInt32();
|
||||
blockEntry.Flags = (FileFlags)data.ReadUInt32();
|
||||
|
||||
return blockEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a patch info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled patch info on success, null on error</returns>
|
||||
private static PatchInfo ParsePatchInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
PatchInfo patchInfo = new PatchInfo();
|
||||
|
||||
patchInfo.Length = data.ReadUInt32();
|
||||
patchInfo.Flags = data.ReadUInt32();
|
||||
patchInfo.DataSize = data.ReadUInt32();
|
||||
patchInfo.MD5 = data.ReadBytes(0x10);
|
||||
|
||||
// TODO: Fill the sector offset table
|
||||
|
||||
return patchInfo;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Buffer for encryption and decryption
|
||||
/// </summary>
|
||||
private uint[] _stormBuffer = new uint[STORM_BUFFER_SIZE];
|
||||
|
||||
/// <summary>
|
||||
/// Prepare the encryption table
|
||||
/// </summary>
|
||||
private void PrepareCryptTable()
|
||||
{
|
||||
uint seed = 0x00100001;
|
||||
for (uint index1 = 0; index1 < 0x100; index1++)
|
||||
{
|
||||
for (uint index2 = index1, i = 0; i < 5; i++, index2 += 0x100)
|
||||
{
|
||||
seed = (seed * 125 + 3) % 0x2AAAAB;
|
||||
uint temp1 = (seed & 0xFFFF) << 0x10;
|
||||
|
||||
seed = (seed * 125 + 3) % 0x2AAAAB;
|
||||
uint temp2 = (seed & 0xFFFF);
|
||||
|
||||
_stormBuffer[index2] = (temp1 | temp2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt a single block of data
|
||||
/// </summary>
|
||||
private unsafe byte[] DecryptBlock(byte[] block, uint length, uint key)
|
||||
{
|
||||
uint seed = 0xEEEEEEEE;
|
||||
|
||||
uint[] castBlock = new uint[length / 4];
|
||||
Buffer.BlockCopy(block, 0, castBlock, 0, (int)length);
|
||||
int castBlockPtr = 0;
|
||||
|
||||
// Round to uints
|
||||
length >>= 2;
|
||||
|
||||
while (length-- > 0)
|
||||
{
|
||||
seed += _stormBuffer[MPQ_HASH_KEY2_MIX + (key & 0xFF)];
|
||||
uint ch = castBlock[castBlockPtr] ^ (key + seed);
|
||||
|
||||
key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B);
|
||||
seed = ch + seed + (seed << 5) + 3;
|
||||
castBlock[castBlockPtr++] = ch;
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(castBlock, 0, block, 0, (int)length);
|
||||
return block;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.N3DS;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.N3DS.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class N3DS :
|
||||
IByteDeserializer<Models.N3DS.Cart>,
|
||||
IFileDeserializer<Models.N3DS.Cart>
|
||||
IByteDeserializer<Cart>,
|
||||
IFileDeserializer<Cart>,
|
||||
IStreamDeserializer<Cart>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.N3DS.Cart? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Cart? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new N3DS();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.N3DS.Cart? Deserialize(byte[]? data, int offset)
|
||||
public Cart? 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.N3DS.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +43,727 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.N3DS.Cart? DeserializeFile(string? path)
|
||||
public static Cart? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new N3DS();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.N3DS.Cart? Deserialize(string? path)
|
||||
public Cart? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.N3DS.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Cart? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new N3DS();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cart? 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 cart image to fill
|
||||
var cart = new Cart();
|
||||
|
||||
#region NCSD Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseNCSDHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the cart image header
|
||||
cart.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Card Info Header
|
||||
|
||||
// Try to parse the card info header
|
||||
var cardInfoHeader = ParseCardInfoHeader(data);
|
||||
if (cardInfoHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the card info header
|
||||
cart.CardInfoHeader = cardInfoHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Development Card Info Header
|
||||
|
||||
// Try to parse the development card info header
|
||||
var developmentCardInfoHeader = ParseDevelopmentCardInfoHeader(data);
|
||||
if (developmentCardInfoHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the development card info header
|
||||
cart.DevelopmentCardInfoHeader = developmentCardInfoHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Partitions
|
||||
|
||||
// Create the partition table
|
||||
cart.Partitions = new NCCHHeader[8];
|
||||
|
||||
// Iterate and build the partitions
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
cart.Partitions[i] = ParseNCCHHeader(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the media unit size for further use
|
||||
long mediaUnitSize = 0;
|
||||
if (header.PartitionFlags != null)
|
||||
mediaUnitSize = (uint)(0x200 * Math.Pow(2, header.PartitionFlags[(int)NCSDFlags.MediaUnitSize]));
|
||||
|
||||
#region Extended Headers
|
||||
|
||||
// Create the extended header table
|
||||
cart.ExtendedHeaders = new NCCHExtendedHeader[8];
|
||||
|
||||
// Iterate and build the extended headers
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
// If we have an encrypted or invalid partition
|
||||
if (cart.Partitions[i]!.MagicID != NCCHMagicNumber)
|
||||
continue;
|
||||
|
||||
// If we have no partitions table
|
||||
if (cart.Header!.PartitionsTable == null)
|
||||
continue;
|
||||
|
||||
// Get the extended header offset
|
||||
long offset = (cart.Header.PartitionsTable[i]!.Offset * mediaUnitSize) + 0x200;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the extended header
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the extended header
|
||||
var extendedHeader = ParseNCCHExtendedHeader(data);
|
||||
if (extendedHeader != null)
|
||||
cart.ExtendedHeaders[i] = extendedHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ExeFS Headers
|
||||
|
||||
// Create the ExeFS header table
|
||||
cart.ExeFSHeaders = new ExeFSHeader[8];
|
||||
|
||||
// Iterate and build the ExeFS headers
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
// If we have an encrypted or invalid partition
|
||||
if (cart.Partitions[i]!.MagicID != NCCHMagicNumber)
|
||||
continue;
|
||||
|
||||
// If we have no partitions table
|
||||
if (cart.Header!.PartitionsTable == null)
|
||||
continue;
|
||||
|
||||
// Get the ExeFS header offset
|
||||
long offset = (cart.Header.PartitionsTable[i]!.Offset + cart.Partitions[i]!.ExeFSOffsetInMediaUnits) * mediaUnitSize;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the ExeFS header
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the ExeFS header
|
||||
cart.ExeFSHeaders[i] = ParseExeFSHeader(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RomFS Headers
|
||||
|
||||
// Create the RomFS header table
|
||||
cart.RomFSHeaders = new RomFSHeader[8];
|
||||
|
||||
// Iterate and build the RomFS headers
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
// If we have an encrypted or invalid partition
|
||||
if (cart.Partitions[i]!.MagicID != NCCHMagicNumber)
|
||||
continue;
|
||||
|
||||
// If we have no partitions table
|
||||
if (cart.Header!.PartitionsTable == null)
|
||||
continue;
|
||||
|
||||
// Get the RomFS header offset
|
||||
long offset = (cart.Header.PartitionsTable[i]!.Offset + cart.Partitions[i]!.RomFSOffsetInMediaUnits) * mediaUnitSize;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the RomFS header
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the RomFS header
|
||||
var romFsHeader = ParseRomFSHeader(data);
|
||||
if (romFsHeader != null)
|
||||
cart.RomFSHeaders[i] = romFsHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCSD header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled NCSD header on success, null on error</returns>
|
||||
private static NCSDHeader? ParseNCSDHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new NCSDHeader();
|
||||
|
||||
header.RSA2048Signature = data.ReadBytes(0x100);
|
||||
byte[]? magicNumber = data.ReadBytes(4);
|
||||
if (magicNumber == null)
|
||||
return null;
|
||||
|
||||
header.MagicNumber = Encoding.ASCII.GetString(magicNumber).TrimEnd('\0'); ;
|
||||
if (header.MagicNumber != NCSDMagicNumber)
|
||||
return null;
|
||||
|
||||
header.ImageSizeInMediaUnits = data.ReadUInt32();
|
||||
header.MediaId = data.ReadBytes(8);
|
||||
header.PartitionsFSType = (FilesystemType)data.ReadUInt64();
|
||||
header.PartitionsCryptType = data.ReadBytes(8);
|
||||
|
||||
header.PartitionsTable = new PartitionTableEntry[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
header.PartitionsTable[i] = ParsePartitionTableEntry(data);
|
||||
}
|
||||
|
||||
if (header.PartitionsFSType == FilesystemType.Normal || header.PartitionsFSType == FilesystemType.None)
|
||||
{
|
||||
header.ExheaderHash = data.ReadBytes(0x20);
|
||||
header.AdditionalHeaderSize = data.ReadUInt32();
|
||||
header.SectorZeroOffset = data.ReadUInt32();
|
||||
header.PartitionFlags = data.ReadBytes(8);
|
||||
|
||||
header.PartitionIdTable = new ulong[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
header.PartitionIdTable[i] = data.ReadUInt64();
|
||||
}
|
||||
|
||||
header.Reserved1 = data.ReadBytes(0x20);
|
||||
header.Reserved2 = data.ReadBytes(0x0E);
|
||||
header.FirmUpdateByte1 = data.ReadByteValue();
|
||||
header.FirmUpdateByte2 = data.ReadByteValue();
|
||||
}
|
||||
else if (header.PartitionsFSType == FilesystemType.FIRM)
|
||||
{
|
||||
header.Unknown = data.ReadBytes(0x5E);
|
||||
header.EncryptedMBR = data.ReadBytes(0x42);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a partition table entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled partition table entry on success, null on error</returns>
|
||||
private static PartitionTableEntry ParsePartitionTableEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var partitionTableEntry = new PartitionTableEntry();
|
||||
|
||||
partitionTableEntry.Offset = data.ReadUInt32();
|
||||
partitionTableEntry.Length = data.ReadUInt32();
|
||||
|
||||
return partitionTableEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a card info header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled card info header on success, null on error</returns>
|
||||
private static CardInfoHeader ParseCardInfoHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var cardInfoHeader = new CardInfoHeader();
|
||||
|
||||
cardInfoHeader.WritableAddressMediaUnits = data.ReadUInt32();
|
||||
cardInfoHeader.CardInfoBitmask = data.ReadUInt32();
|
||||
cardInfoHeader.Reserved1 = data.ReadBytes(0xF8);
|
||||
cardInfoHeader.FilledSize = data.ReadUInt32();
|
||||
cardInfoHeader.Reserved2 = data.ReadBytes(0x0C);
|
||||
cardInfoHeader.TitleVersion = data.ReadUInt16();
|
||||
cardInfoHeader.CardRevision = data.ReadUInt16();
|
||||
cardInfoHeader.Reserved3 = data.ReadBytes(0x0C);
|
||||
cardInfoHeader.CVerTitleID = data.ReadBytes(8);
|
||||
cardInfoHeader.CVerVersionNumber = data.ReadUInt16();
|
||||
cardInfoHeader.Reserved4 = data.ReadBytes(0xCD6);
|
||||
|
||||
return cardInfoHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a development card info header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled development card info header on success, null on error</returns>
|
||||
private static DevelopmentCardInfoHeader? ParseDevelopmentCardInfoHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var developmentCardInfoHeader = new DevelopmentCardInfoHeader();
|
||||
|
||||
developmentCardInfoHeader.InitialData = ParseInitialData(data);
|
||||
if (developmentCardInfoHeader.InitialData == null)
|
||||
return null;
|
||||
|
||||
developmentCardInfoHeader.CardDeviceReserved1 = data.ReadBytes(0x200);
|
||||
developmentCardInfoHeader.TitleKey = data.ReadBytes(0x10);
|
||||
developmentCardInfoHeader.CardDeviceReserved2 = data.ReadBytes(0x1BF0);
|
||||
|
||||
developmentCardInfoHeader.TestData = ParseTestData(data);
|
||||
if (developmentCardInfoHeader.TestData == null)
|
||||
return null;
|
||||
|
||||
return developmentCardInfoHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an initial data
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled initial data on success, null on error</returns>
|
||||
private static InitialData? ParseInitialData(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var initialData = new InitialData();
|
||||
|
||||
initialData.CardSeedKeyY = data.ReadBytes(0x10);
|
||||
initialData.EncryptedCardSeed = data.ReadBytes(0x10);
|
||||
initialData.CardSeedAESMAC = data.ReadBytes(0x10);
|
||||
initialData.CardSeedNonce = data.ReadBytes(0xC);
|
||||
initialData.Reserved = data.ReadBytes(0xC4);
|
||||
initialData.BackupHeader = ParseNCCHHeader(data, true);
|
||||
if (initialData.BackupHeader == null)
|
||||
return null;
|
||||
|
||||
return initialData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCCH header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="skipSignature">Indicates if the signature should be skipped</param>
|
||||
/// <returns>Filled NCCH header on success, null on error</returns>
|
||||
internal static NCCHHeader ParseNCCHHeader(Stream data, bool skipSignature = false)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new NCCHHeader();
|
||||
|
||||
if (!skipSignature)
|
||||
header.RSA2048Signature = data.ReadBytes(0x100);
|
||||
|
||||
byte[]? magicId = data.ReadBytes(4);
|
||||
if (magicId != null)
|
||||
header.MagicID = Encoding.ASCII.GetString(magicId).TrimEnd('\0');
|
||||
header.ContentSizeInMediaUnits = data.ReadUInt32();
|
||||
header.PartitionId = data.ReadUInt64();
|
||||
header.MakerCode = data.ReadUInt16();
|
||||
header.Version = data.ReadUInt16();
|
||||
header.VerificationHash = data.ReadUInt32();
|
||||
header.ProgramId = data.ReadBytes(8);
|
||||
header.Reserved1 = data.ReadBytes(0x10);
|
||||
header.LogoRegionHash = data.ReadBytes(0x20);
|
||||
byte[]? productCode = data.ReadBytes(0x10);
|
||||
if (productCode != null)
|
||||
header.ProductCode = Encoding.ASCII.GetString(productCode).TrimEnd('\0');
|
||||
header.ExtendedHeaderHash = data.ReadBytes(0x20);
|
||||
header.ExtendedHeaderSizeInBytes = data.ReadUInt32();
|
||||
header.Reserved2 = data.ReadBytes(4);
|
||||
header.Flags = ParseNCCHHeaderFlags(data);
|
||||
header.PlainRegionOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.PlainRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.LogoRegionOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.LogoRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.ExeFSOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.ExeFSSizeInMediaUnits = data.ReadUInt32();
|
||||
header.ExeFSHashRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.Reserved3 = data.ReadBytes(4);
|
||||
header.RomFSOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.RomFSSizeInMediaUnits = data.ReadUInt32();
|
||||
header.RomFSHashRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.Reserved4 = data.ReadBytes(4);
|
||||
header.ExeFSSuperblockHash = data.ReadBytes(0x20);
|
||||
header.RomFSSuperblockHash = data.ReadBytes(0x20);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCCH header flags
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled NCCH header flags on success, null on error</returns>
|
||||
private static NCCHHeaderFlags ParseNCCHHeaderFlags(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var headerFlags = new NCCHHeaderFlags();
|
||||
|
||||
headerFlags.Reserved0 = data.ReadByteValue();
|
||||
headerFlags.Reserved1 = data.ReadByteValue();
|
||||
headerFlags.Reserved2 = data.ReadByteValue();
|
||||
headerFlags.CryptoMethod = (CryptoMethod)data.ReadByteValue();
|
||||
headerFlags.ContentPlatform = (ContentPlatform)data.ReadByteValue();
|
||||
headerFlags.MediaPlatformIndex = (ContentType)data.ReadByteValue();
|
||||
headerFlags.ContentUnitSize = data.ReadByteValue();
|
||||
headerFlags.BitMasks = (BitMasks)data.ReadByteValue();
|
||||
|
||||
return headerFlags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an initial data
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled initial data on success, null on error</returns>
|
||||
private static TestData ParseTestData(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var testData = new TestData();
|
||||
|
||||
// TODO: Validate some of the values
|
||||
testData.Signature = data.ReadBytes(8);
|
||||
testData.AscendingByteSequence = data.ReadBytes(0x1F8);
|
||||
testData.DescendingByteSequence = data.ReadBytes(0x200);
|
||||
testData.Filled00 = data.ReadBytes(0x200);
|
||||
testData.FilledFF = data.ReadBytes(0x200);
|
||||
testData.Filled0F = data.ReadBytes(0x200);
|
||||
testData.FilledF0 = data.ReadBytes(0x200);
|
||||
testData.Filled55 = data.ReadBytes(0x200);
|
||||
testData.FilledAA = data.ReadBytes(0x1FF);
|
||||
testData.FinalByte = data.ReadByteValue();
|
||||
|
||||
return testData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCCH extended header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled NCCH extended header on success, null on error</returns>
|
||||
private static NCCHExtendedHeader? ParseNCCHExtendedHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var extendedHeader = new NCCHExtendedHeader();
|
||||
|
||||
extendedHeader.SCI = ParseSystemControlInfo(data);
|
||||
if (extendedHeader.SCI == null)
|
||||
return null;
|
||||
|
||||
extendedHeader.ACI = ParseAccessControlInfo(data);
|
||||
if (extendedHeader.ACI == null)
|
||||
return null;
|
||||
|
||||
extendedHeader.AccessDescSignature = data.ReadBytes(0x100);
|
||||
extendedHeader.NCCHHDRPublicKey = data.ReadBytes(0x100);
|
||||
|
||||
extendedHeader.ACIForLimitations = ParseAccessControlInfo(data);
|
||||
if (extendedHeader.ACI == null)
|
||||
return null;
|
||||
|
||||
return extendedHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a system control info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled system control info on success, null on error</returns>
|
||||
private static SystemControlInfo ParseSystemControlInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var systemControlInfo = new SystemControlInfo();
|
||||
|
||||
byte[]? applicationTitle = data.ReadBytes(8);
|
||||
if (applicationTitle != null)
|
||||
systemControlInfo.ApplicationTitle = Encoding.ASCII.GetString(applicationTitle).TrimEnd('\0');
|
||||
systemControlInfo.Reserved1 = data.ReadBytes(5);
|
||||
systemControlInfo.Flag = data.ReadByteValue();
|
||||
systemControlInfo.RemasterVersion = data.ReadUInt16();
|
||||
systemControlInfo.TextCodeSetInfo = ParseCodeSetInfo(data);
|
||||
systemControlInfo.StackSize = data.ReadUInt32();
|
||||
systemControlInfo.ReadOnlyCodeSetInfo = ParseCodeSetInfo(data);
|
||||
systemControlInfo.Reserved2 = data.ReadBytes(4);
|
||||
systemControlInfo.DataCodeSetInfo = ParseCodeSetInfo(data);
|
||||
systemControlInfo.BSSSize = data.ReadUInt32();
|
||||
systemControlInfo.DependencyModuleList = new ulong[48];
|
||||
for (int i = 0; i < 48; i++)
|
||||
{
|
||||
systemControlInfo.DependencyModuleList[i] = data.ReadUInt64();
|
||||
}
|
||||
systemControlInfo.SystemInfo = ParseSystemInfo(data);
|
||||
|
||||
return systemControlInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a code set info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled code set info on success, null on error</returns>
|
||||
private static CodeSetInfo ParseCodeSetInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var codeSetInfo = new CodeSetInfo();
|
||||
|
||||
codeSetInfo.Address = data.ReadUInt32();
|
||||
codeSetInfo.PhysicalRegionSizeInPages = data.ReadUInt32();
|
||||
codeSetInfo.SizeInBytes = data.ReadUInt32();
|
||||
|
||||
return codeSetInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a system info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled system info on success, null on error</returns>
|
||||
private static SystemInfo ParseSystemInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var systemInfo = new SystemInfo();
|
||||
|
||||
systemInfo.SaveDataSize = data.ReadUInt64();
|
||||
systemInfo.JumpID = data.ReadUInt64();
|
||||
systemInfo.Reserved = data.ReadBytes(0x30);
|
||||
|
||||
return systemInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an access control info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled access control info on success, null on error</returns>
|
||||
private static AccessControlInfo ParseAccessControlInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var accessControlInfo = new AccessControlInfo();
|
||||
|
||||
accessControlInfo.ARM11LocalSystemCapabilities = ParseARM11LocalSystemCapabilities(data);
|
||||
accessControlInfo.ARM11KernelCapabilities = ParseARM11KernelCapabilities(data);
|
||||
accessControlInfo.ARM9AccessControl = ParseARM9AccessControl(data);
|
||||
|
||||
return accessControlInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ARM11 local system capabilities
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ARM11 local system capabilities on success, null on error</returns>
|
||||
private static ARM11LocalSystemCapabilities ParseARM11LocalSystemCapabilities(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var arm11LocalSystemCapabilities = new ARM11LocalSystemCapabilities();
|
||||
|
||||
arm11LocalSystemCapabilities.ProgramID = data.ReadUInt64();
|
||||
arm11LocalSystemCapabilities.CoreVersion = data.ReadUInt32();
|
||||
arm11LocalSystemCapabilities.Flag1 = (ARM11LSCFlag1)data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.Flag2 = (ARM11LSCFlag2)data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.Flag0 = (ARM11LSCFlag0)data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.Priority = data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.ResourceLimitDescriptors = new ushort[16];
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
arm11LocalSystemCapabilities.ResourceLimitDescriptors[i] = data.ReadUInt16();
|
||||
}
|
||||
arm11LocalSystemCapabilities.StorageInfo = ParseStorageInfo(data);
|
||||
arm11LocalSystemCapabilities.ServiceAccessControl = new ulong[32];
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
arm11LocalSystemCapabilities.ServiceAccessControl[i] = data.ReadUInt64();
|
||||
}
|
||||
arm11LocalSystemCapabilities.ExtendedServiceAccessControl = new ulong[2];
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
arm11LocalSystemCapabilities.ExtendedServiceAccessControl[i] = data.ReadUInt64();
|
||||
}
|
||||
arm11LocalSystemCapabilities.Reserved = data.ReadBytes(0x0F);
|
||||
arm11LocalSystemCapabilities.ResourceLimitCategory = (ResourceLimitCategory)data.ReadByteValue();
|
||||
|
||||
return arm11LocalSystemCapabilities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a storage info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled storage info on success, null on error</returns>
|
||||
private static StorageInfo ParseStorageInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var storageInfo = new StorageInfo();
|
||||
|
||||
storageInfo.ExtdataID = data.ReadUInt64();
|
||||
storageInfo.SystemSavedataIDs = data.ReadBytes(8);
|
||||
storageInfo.StorageAccessibleUniqueIDs = data.ReadBytes(8);
|
||||
storageInfo.FileSystemAccessInfo = data.ReadBytes(7);
|
||||
storageInfo.OtherAttributes = (StorageInfoOtherAttributes)data.ReadByteValue();
|
||||
|
||||
return storageInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ARM11 kernel capabilities
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ARM11 kernel capabilities on success, null on error</returns>
|
||||
private static ARM11KernelCapabilities ParseARM11KernelCapabilities(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var arm11KernelCapabilities = new ARM11KernelCapabilities();
|
||||
|
||||
arm11KernelCapabilities.Descriptors = new uint[28];
|
||||
for (int i = 0; i < 28; i++)
|
||||
{
|
||||
arm11KernelCapabilities.Descriptors[i] = data.ReadUInt32();
|
||||
}
|
||||
arm11KernelCapabilities.Reserved = data.ReadBytes(0x10);
|
||||
|
||||
return arm11KernelCapabilities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ARM11 access control
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ARM11 access control on success, null on error</returns>
|
||||
private static ARM9AccessControl ParseARM9AccessControl(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var arm9AccessControl = new ARM9AccessControl();
|
||||
|
||||
arm9AccessControl.Descriptors = data.ReadBytes(15);
|
||||
arm9AccessControl.DescriptorVersion = data.ReadByteValue();
|
||||
|
||||
return arm9AccessControl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ExeFS header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ExeFS header on success, null on error</returns>
|
||||
private static ExeFSHeader ParseExeFSHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var exeFSHeader = new ExeFSHeader();
|
||||
|
||||
exeFSHeader.FileHeaders = new ExeFSFileHeader[10];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
exeFSHeader.FileHeaders[i] = ParseExeFSFileHeader(data);
|
||||
}
|
||||
exeFSHeader.Reserved = data.ReadBytes(0x20);
|
||||
exeFSHeader.FileHashes = new byte[10][];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
exeFSHeader.FileHashes[i] = data.ReadBytes(0x20) ?? [];
|
||||
}
|
||||
|
||||
return exeFSHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ExeFS file header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ExeFS file header on success, null on error</returns>
|
||||
private static ExeFSFileHeader ParseExeFSFileHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var exeFSFileHeader = new ExeFSFileHeader();
|
||||
|
||||
byte[]? fileName = data.ReadBytes(8);
|
||||
if (fileName != null)
|
||||
exeFSFileHeader.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
|
||||
exeFSFileHeader.FileOffset = data.ReadUInt32();
|
||||
exeFSFileHeader.FileSize = data.ReadUInt32();
|
||||
|
||||
return exeFSFileHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an RomFS header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled RomFS header on success, null on error</returns>
|
||||
private static RomFSHeader? ParseRomFSHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var romFSHeader = new RomFSHeader();
|
||||
|
||||
byte[]? magicString = data.ReadBytes(4);
|
||||
if (magicString == null)
|
||||
return null;
|
||||
|
||||
romFSHeader.MagicString = Encoding.ASCII.GetString(magicString).TrimEnd('\0');
|
||||
if (romFSHeader.MagicString != RomFSMagicNumber)
|
||||
return null;
|
||||
|
||||
romFSHeader.MagicNumber = data.ReadUInt32();
|
||||
if (romFSHeader.MagicNumber != RomFSSecondMagicNumber)
|
||||
return null;
|
||||
|
||||
romFSHeader.MasterHashSize = data.ReadUInt32();
|
||||
romFSHeader.Level1LogicalOffset = data.ReadUInt64();
|
||||
romFSHeader.Level1HashdataSize = data.ReadUInt64();
|
||||
romFSHeader.Level1BlockSizeLog2 = data.ReadUInt32();
|
||||
romFSHeader.Reserved1 = data.ReadBytes(4);
|
||||
romFSHeader.Level2LogicalOffset = data.ReadUInt64();
|
||||
romFSHeader.Level2HashdataSize = data.ReadUInt64();
|
||||
romFSHeader.Level2BlockSizeLog2 = data.ReadUInt32();
|
||||
romFSHeader.Reserved2 = data.ReadBytes(4);
|
||||
romFSHeader.Level3LogicalOffset = data.ReadUInt64();
|
||||
romFSHeader.Level3HashdataSize = data.ReadUInt64();
|
||||
romFSHeader.Level3BlockSizeLog2 = data.ReadUInt32();
|
||||
romFSHeader.Reserved3 = data.ReadBytes(4);
|
||||
romFSHeader.Reserved4 = data.ReadBytes(4);
|
||||
romFSHeader.OptionalInfoSize = data.ReadUInt32();
|
||||
|
||||
return romFSHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.NCF;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class NCF :
|
||||
IByteDeserializer<Models.NCF.File>,
|
||||
IFileDeserializer<Models.NCF.File>
|
||||
IFileDeserializer<Models.NCF.File>,
|
||||
IStreamDeserializer<Models.NCF.File>
|
||||
{
|
||||
#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.NCF.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +52,519 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.NCF.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.NCF.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.NCF.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new NCF();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.NCF.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life No Cache to fill
|
||||
var file = new Models.NCF.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the no cache header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Directory Header
|
||||
|
||||
// Try to parse the directory header
|
||||
var directoryHeader = ParseDirectoryHeader(data);
|
||||
if (directoryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache directory header
|
||||
file.DirectoryHeader = directoryHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Entries
|
||||
|
||||
// Create the directory entry array
|
||||
file.DirectoryEntries = new DirectoryEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.DirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Names
|
||||
|
||||
if (directoryHeader.NameSize > 0)
|
||||
{
|
||||
// Get the current offset for adjustment
|
||||
long directoryNamesStart = data.Position;
|
||||
|
||||
// Get the ending offset
|
||||
long directoryNamesEnd = data.Position + directoryHeader.NameSize;
|
||||
|
||||
// Create the string dictionary
|
||||
file.DirectoryNames = new Dictionary<long, string?>();
|
||||
|
||||
// Loop and read the null-terminated strings
|
||||
while (data.Position < directoryNamesEnd)
|
||||
{
|
||||
long nameOffset = data.Position - directoryNamesStart;
|
||||
string? directoryName = data.ReadString(Encoding.ASCII);
|
||||
if (data.Position > directoryNamesEnd)
|
||||
{
|
||||
data.Seek(-directoryName?.Length ?? 0, SeekOrigin.Current);
|
||||
byte[]? endingData = data.ReadBytes((int)(directoryNamesEnd - data.Position));
|
||||
if (endingData != null)
|
||||
directoryName = Encoding.ASCII.GetString(endingData);
|
||||
else
|
||||
directoryName = null;
|
||||
}
|
||||
|
||||
file.DirectoryNames[nameOffset] = directoryName;
|
||||
}
|
||||
|
||||
// Loop and assign to entries
|
||||
foreach (var directoryEntry in file.DirectoryEntries)
|
||||
{
|
||||
if (directoryEntry != null)
|
||||
directoryEntry.Name = file.DirectoryNames[directoryEntry.NameOffset];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 1 Entries
|
||||
|
||||
// Create the directory info 1 entry array
|
||||
file.DirectoryInfo1Entries = new DirectoryInfo1Entry[directoryHeader.Info1Count];
|
||||
|
||||
// Try to parse the directory info 1 entries
|
||||
for (int i = 0; i < directoryHeader.Info1Count; i++)
|
||||
{
|
||||
var directoryInfo1Entry = ParseDirectoryInfo1Entry(data);
|
||||
file.DirectoryInfo1Entries[i] = directoryInfo1Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 2 Entries
|
||||
|
||||
// Create the directory info 2 entry array
|
||||
file.DirectoryInfo2Entries = new DirectoryInfo2Entry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory info 2 entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryInfo2Entry = ParseDirectoryInfo2Entry(data);
|
||||
file.DirectoryInfo2Entries[i] = directoryInfo2Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Copy Entries
|
||||
|
||||
// Create the directory copy entry array
|
||||
file.DirectoryCopyEntries = new DirectoryCopyEntry[directoryHeader.CopyCount];
|
||||
|
||||
// Try to parse the directory copy entries
|
||||
for (int i = 0; i < directoryHeader.CopyCount; i++)
|
||||
{
|
||||
var directoryCopyEntry = ParseDirectoryCopyEntry(data);
|
||||
file.DirectoryCopyEntries[i] = directoryCopyEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Local Entries
|
||||
|
||||
// Create the directory local entry array
|
||||
file.DirectoryLocalEntries = new DirectoryLocalEntry[directoryHeader.LocalCount];
|
||||
|
||||
// Try to parse the directory local entries
|
||||
for (int i = 0; i < directoryHeader.LocalCount; i++)
|
||||
{
|
||||
var directoryLocalEntry = ParseDirectoryLocalEntry(data);
|
||||
file.DirectoryLocalEntries[i] = directoryLocalEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of directory section, just in case
|
||||
data.Seek(initialOffset + directoryHeader.DirectorySize, SeekOrigin.Begin);
|
||||
|
||||
#region Unknown Header
|
||||
|
||||
// Try to parse the unknown header
|
||||
var unknownHeader = ParseUnknownHeader(data);
|
||||
if (unknownHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache unknown header
|
||||
file.UnknownHeader = unknownHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Entries
|
||||
|
||||
// Create the unknown entry array
|
||||
file.UnknownEntries = new UnknownEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the unknown entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var unknownEntry = ParseUnknownEntry(data);
|
||||
file.UnknownEntries[i] = unknownEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Header
|
||||
|
||||
// Try to parse the checksum header
|
||||
var checksumHeader = ParseChecksumHeader(data);
|
||||
if (checksumHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum header
|
||||
file.ChecksumHeader = checksumHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Checksum Map Header
|
||||
|
||||
// Try to parse the checksum map header
|
||||
var checksumMapHeader = ParseChecksumMapHeader(data);
|
||||
if (checksumMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum map header
|
||||
file.ChecksumMapHeader = checksumMapHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Map Entries
|
||||
|
||||
// Create the checksum map entry array
|
||||
file.ChecksumMapEntries = new ChecksumMapEntry[checksumMapHeader.ItemCount];
|
||||
|
||||
// Try to parse the checksum map entries
|
||||
for (int i = 0; i < checksumMapHeader.ItemCount; i++)
|
||||
{
|
||||
var checksumMapEntry = ParseChecksumMapEntry(data);
|
||||
file.ChecksumMapEntries[i] = checksumMapEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Entries
|
||||
|
||||
// Create the checksum entry array
|
||||
file.ChecksumEntries = new ChecksumEntry[checksumMapHeader.ChecksumCount];
|
||||
|
||||
// Try to parse the checksum entries
|
||||
for (int i = 0; i < checksumMapHeader.ChecksumCount; i++)
|
||||
{
|
||||
var checksumEntry = ParseChecksumEntry(data);
|
||||
file.ChecksumEntries[i] = checksumEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of checksum section, just in case
|
||||
data.Seek(initialOffset + checksumHeader.ChecksumSize, SeekOrigin.Begin);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.Dummy0 = data.ReadUInt32();
|
||||
if (header.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
header.MajorVersion = data.ReadUInt32();
|
||||
if (header.MajorVersion != 0x00000002)
|
||||
return null;
|
||||
|
||||
header.MinorVersion = data.ReadUInt32();
|
||||
if (header.MinorVersion != 1)
|
||||
return null;
|
||||
|
||||
header.CacheID = data.ReadUInt32();
|
||||
header.LastVersionPlayed = data.ReadUInt32();
|
||||
header.Dummy1 = data.ReadUInt32();
|
||||
header.Dummy2 = data.ReadUInt32();
|
||||
header.FileSize = data.ReadUInt32();
|
||||
header.BlockSize = data.ReadUInt32();
|
||||
header.BlockCount = data.ReadUInt32();
|
||||
header.Dummy3 = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory header on success, null on error</returns>
|
||||
private static DirectoryHeader? ParseDirectoryHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryHeader directoryHeader = new DirectoryHeader();
|
||||
|
||||
directoryHeader.Dummy0 = data.ReadUInt32();
|
||||
if (directoryHeader.Dummy0 != 0x00000004)
|
||||
return null;
|
||||
|
||||
directoryHeader.CacheID = data.ReadUInt32();
|
||||
directoryHeader.LastVersionPlayed = data.ReadUInt32();
|
||||
directoryHeader.ItemCount = data.ReadUInt32();
|
||||
directoryHeader.FileCount = data.ReadUInt32();
|
||||
directoryHeader.ChecksumDataLength = data.ReadUInt32();
|
||||
directoryHeader.DirectorySize = data.ReadUInt32();
|
||||
directoryHeader.NameSize = data.ReadUInt32();
|
||||
directoryHeader.Info1Count = data.ReadUInt32();
|
||||
directoryHeader.CopyCount = data.ReadUInt32();
|
||||
directoryHeader.LocalCount = data.ReadUInt32();
|
||||
directoryHeader.Dummy1 = data.ReadUInt32();
|
||||
directoryHeader.Dummy2 = data.ReadUInt32();
|
||||
directoryHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return directoryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.NameOffset = data.ReadUInt32();
|
||||
directoryEntry.ItemSize = data.ReadUInt32();
|
||||
directoryEntry.ChecksumIndex = data.ReadUInt32();
|
||||
directoryEntry.DirectoryFlags = (HL_NCF_FLAG)data.ReadUInt32();
|
||||
directoryEntry.ParentIndex = data.ReadUInt32();
|
||||
directoryEntry.NextIndex = data.ReadUInt32();
|
||||
directoryEntry.FirstIndex = data.ReadUInt32();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory info 1 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory info 1 entry on success, null on error</returns>
|
||||
private static DirectoryInfo1Entry ParseDirectoryInfo1Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo1Entry directoryInfo1Entry = new DirectoryInfo1Entry();
|
||||
|
||||
directoryInfo1Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo1Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory info 2 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory info 2 entry on success, null on error</returns>
|
||||
private static DirectoryInfo2Entry ParseDirectoryInfo2Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo2Entry directoryInfo2Entry = new DirectoryInfo2Entry();
|
||||
|
||||
directoryInfo2Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo2Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory copy entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory copy entry on success, null on error</returns>
|
||||
private static DirectoryCopyEntry ParseDirectoryCopyEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryCopyEntry directoryCopyEntry = new DirectoryCopyEntry();
|
||||
|
||||
directoryCopyEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryCopyEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory local entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory local entry on success, null on error</returns>
|
||||
private static DirectoryLocalEntry ParseDirectoryLocalEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryLocalEntry directoryLocalEntry = new DirectoryLocalEntry();
|
||||
|
||||
directoryLocalEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryLocalEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache unknown header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache unknown header on success, null on error</returns>
|
||||
private static UnknownHeader? ParseUnknownHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownHeader unknownHeader = new UnknownHeader();
|
||||
|
||||
unknownHeader.Dummy0 = data.ReadUInt32();
|
||||
if (unknownHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
unknownHeader.Dummy1 = data.ReadUInt32();
|
||||
if (unknownHeader.Dummy1 != 0x00000000)
|
||||
return null;
|
||||
|
||||
return unknownHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache unknown entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cacheunknown entry on success, null on error</returns>
|
||||
private static UnknownEntry ParseUnknownEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownEntry unknownEntry = new UnknownEntry();
|
||||
|
||||
unknownEntry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return unknownEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum header on success, null on error</returns>
|
||||
private static ChecksumHeader? ParseChecksumHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumHeader checksumHeader = new ChecksumHeader();
|
||||
|
||||
checksumHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumHeader.ChecksumSize = data.ReadUInt32();
|
||||
|
||||
return checksumHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum map header on success, null on error</returns>
|
||||
private static ChecksumMapHeader? ParseChecksumMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapHeader checksumMapHeader = new ChecksumMapHeader();
|
||||
|
||||
checksumMapHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy0 != 0x14893721)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.Dummy1 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy1 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.ItemCount = data.ReadUInt32();
|
||||
checksumMapHeader.ChecksumCount = data.ReadUInt32();
|
||||
|
||||
return checksumMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum map entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum map entry on success, null on error</returns>
|
||||
private static ChecksumMapEntry ParseChecksumMapEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapEntry checksumMapEntry = new ChecksumMapEntry();
|
||||
|
||||
checksumMapEntry.ChecksumCount = data.ReadUInt32();
|
||||
checksumMapEntry.FirstChecksumIndex = data.ReadUInt32();
|
||||
|
||||
return checksumMapEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum entry on success, null on error</returns>
|
||||
private static ChecksumEntry ParseChecksumEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumEntry checksumEntry = new ChecksumEntry();
|
||||
|
||||
checksumEntry.Checksum = data.ReadUInt32();
|
||||
|
||||
return checksumEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.NewExecutable;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.NewExecutable.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class NewExecutable :
|
||||
IByteDeserializer<Models.NewExecutable.Executable>,
|
||||
IFileDeserializer<Models.NewExecutable.Executable>
|
||||
IByteDeserializer<Executable>,
|
||||
IFileDeserializer<Executable>,
|
||||
IStreamDeserializer<Executable>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.NewExecutable.Executable? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Executable? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new NewExecutable();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.NewExecutable.Executable? Deserialize(byte[]? data, int offset)
|
||||
public Executable? 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.NewExecutable.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +44,495 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.NewExecutable.Executable? DeserializeFile(string? path)
|
||||
public static Executable? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new NewExecutable();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.NewExecutable.Executable? Deserialize(string? path)
|
||||
public Executable? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.NewExecutable.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Executable? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new NewExecutable();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Executable? 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 executable to fill
|
||||
var executable = new Executable();
|
||||
|
||||
#region MS-DOS Stub
|
||||
|
||||
// Parse the MS-DOS stub
|
||||
var stub = new MSDOS().Deserialize(data);
|
||||
if (stub?.Header == null || stub.Header.NewExeHeaderAddr == 0)
|
||||
return null;
|
||||
|
||||
// Set the MS-DOS stub
|
||||
executable.Stub = stub;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Executable Header
|
||||
|
||||
// Try to parse the executable header
|
||||
data.Seek(initialOffset + stub.Header.NewExeHeaderAddr, SeekOrigin.Begin);
|
||||
var executableHeader = ParseExecutableHeader(data);
|
||||
if (executableHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the executable header
|
||||
executable.Header = executableHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Segment Table
|
||||
|
||||
// If the offset for the segment table doesn't exist
|
||||
int tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.SegmentTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the segment table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var segmentTable = ParseSegmentTable(data, executableHeader.FileSegmentCount);
|
||||
if (segmentTable == null)
|
||||
return null;
|
||||
|
||||
// Set the segment table
|
||||
executable.SegmentTable = segmentTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resource Table
|
||||
|
||||
// If the offset for the segment table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.SegmentTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the resource table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var resourceTable = ParseResourceTable(data, executableHeader.ResourceEntriesCount);
|
||||
if (resourceTable == null)
|
||||
return null;
|
||||
|
||||
// Set the resource table
|
||||
executable.ResourceTable = resourceTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resident-Name Table
|
||||
|
||||
// If the offset for the resident-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ResidentNameTableOffset;
|
||||
int endOffset = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ModuleReferenceTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the resident-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var residentNameTable = ParseResidentNameTable(data, endOffset);
|
||||
if (residentNameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the resident-name table
|
||||
executable.ResidentNameTable = residentNameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Module-Reference Table
|
||||
|
||||
// If the offset for the module-reference table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ModuleReferenceTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the module-reference table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var moduleReferenceTable = ParseModuleReferenceTable(data, executableHeader.ModuleReferenceTableSize);
|
||||
if (moduleReferenceTable == null)
|
||||
return null;
|
||||
|
||||
// Set the module-reference table
|
||||
executable.ModuleReferenceTable = moduleReferenceTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Imported-Name Table
|
||||
|
||||
// If the offset for the imported-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ImportedNamesTableOffset;
|
||||
endOffset = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.EntryTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the imported-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var importedNameTable = ParseImportedNameTable(data, endOffset);
|
||||
if (importedNameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the imported-name table
|
||||
executable.ImportedNameTable = importedNameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entry Table
|
||||
|
||||
// If the offset for the imported-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.EntryTableOffset;
|
||||
endOffset = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.EntryTableOffset
|
||||
+ executableHeader.EntryTableSize;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the imported-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var entryTable = ParseEntryTable(data, endOffset);
|
||||
if (entryTable == null)
|
||||
return null;
|
||||
|
||||
// Set the entry table
|
||||
executable.EntryTable = entryTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nonresident-Name Table
|
||||
|
||||
// If the offset for the nonresident-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)executableHeader.NonResidentNamesTableOffset;
|
||||
endOffset = initialOffset
|
||||
+ (int)executableHeader.NonResidentNamesTableOffset
|
||||
+ executableHeader.NonResidentNameTableSize;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the nonresident-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var nonResidentNameTable = ParseNonResidentNameTable(data, endOffset);
|
||||
if (nonResidentNameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the nonresident-name table
|
||||
executable.NonResidentNameTable = nonResidentNameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
return executable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a New Executable header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled executable header on success, null on error</returns>
|
||||
public static ExecutableHeader? ParseExecutableHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new ExecutableHeader();
|
||||
|
||||
byte[]? magic = data.ReadBytes(2);
|
||||
if (magic == null)
|
||||
return null;
|
||||
|
||||
header.Magic = Encoding.ASCII.GetString(magic);
|
||||
if (header.Magic != SignatureString)
|
||||
return null;
|
||||
|
||||
header.LinkerVersion = data.ReadByteValue();
|
||||
header.LinkerRevision = data.ReadByteValue();
|
||||
header.EntryTableOffset = data.ReadUInt16();
|
||||
header.EntryTableSize = data.ReadUInt16();
|
||||
header.CrcChecksum = data.ReadUInt32();
|
||||
header.FlagWord = (HeaderFlag)data.ReadUInt16();
|
||||
header.AutomaticDataSegmentNumber = data.ReadUInt16();
|
||||
header.InitialHeapAlloc = data.ReadUInt16();
|
||||
header.InitialStackAlloc = data.ReadUInt16();
|
||||
header.InitialCSIPSetting = data.ReadUInt32();
|
||||
header.InitialSSSPSetting = data.ReadUInt32();
|
||||
header.FileSegmentCount = data.ReadUInt16();
|
||||
header.ModuleReferenceTableSize = data.ReadUInt16();
|
||||
header.NonResidentNameTableSize = data.ReadUInt16();
|
||||
header.SegmentTableOffset = data.ReadUInt16();
|
||||
header.ResourceTableOffset = data.ReadUInt16();
|
||||
header.ResidentNameTableOffset = data.ReadUInt16();
|
||||
header.ModuleReferenceTableOffset = data.ReadUInt16();
|
||||
header.ImportedNamesTableOffset = data.ReadUInt16();
|
||||
header.NonResidentNamesTableOffset = data.ReadUInt32();
|
||||
header.MovableEntriesCount = data.ReadUInt16();
|
||||
header.SegmentAlignmentShiftCount = data.ReadUInt16();
|
||||
header.ResourceEntriesCount = data.ReadUInt16();
|
||||
header.TargetOperatingSystem = (OperatingSystem)data.ReadByteValue();
|
||||
header.AdditionalFlags = (OS2Flag)data.ReadByteValue();
|
||||
header.ReturnThunkOffset = data.ReadUInt16();
|
||||
header.SegmentReferenceThunkOffset = data.ReadUInt16();
|
||||
header.MinCodeSwapAreaSize = data.ReadUInt16();
|
||||
header.WindowsSDKRevision = data.ReadByteValue();
|
||||
header.WindowsSDKVersion = data.ReadByteValue();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a segment table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of segment table entries to read</param>
|
||||
/// <returns>Filled segment table on success, null on error</returns>
|
||||
public static SegmentTableEntry[] ParseSegmentTable(Stream data, int count)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var segmentTable = new SegmentTableEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = new SegmentTableEntry();
|
||||
entry.Offset = data.ReadUInt16();
|
||||
entry.Length = data.ReadUInt16();
|
||||
entry.FlagWord = (SegmentTableEntryFlag)data.ReadUInt16();
|
||||
entry.MinimumAllocationSize = data.ReadUInt16();
|
||||
segmentTable[i] = entry;
|
||||
}
|
||||
|
||||
return segmentTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a resource table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of resource table entries to read</param>
|
||||
/// <returns>Filled resource table on success, null on error</returns>
|
||||
public static ResourceTable ParseResourceTable(Stream data, int count)
|
||||
{
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// TODO: Use marshalling here instead of building
|
||||
var resourceTable = new ResourceTable();
|
||||
|
||||
resourceTable.AlignmentShiftCount = data.ReadUInt16();
|
||||
resourceTable.ResourceTypes = new ResourceTypeInformationEntry[count];
|
||||
for (int i = 0; i < resourceTable.ResourceTypes.Length; i++)
|
||||
{
|
||||
var entry = new ResourceTypeInformationEntry();
|
||||
entry.TypeID = data.ReadUInt16();
|
||||
entry.ResourceCount = data.ReadUInt16();
|
||||
entry.Reserved = data.ReadUInt32();
|
||||
entry.Resources = new ResourceTypeResourceEntry[entry.ResourceCount];
|
||||
for (int j = 0; j < entry.ResourceCount; j++)
|
||||
{
|
||||
// TODO: Should we read and store the resource data?
|
||||
var resource = new ResourceTypeResourceEntry();
|
||||
resource.Offset = data.ReadUInt16();
|
||||
resource.Length = data.ReadUInt16();
|
||||
resource.FlagWord = (ResourceTypeResourceFlag)data.ReadUInt16();
|
||||
resource.ResourceID = data.ReadUInt16();
|
||||
resource.Reserved = data.ReadUInt32();
|
||||
entry.Resources[j] = resource;
|
||||
}
|
||||
resourceTable.ResourceTypes[i] = entry;
|
||||
}
|
||||
|
||||
// Get the full list of unique string offsets
|
||||
var stringOffsets = resourceTable.ResourceTypes
|
||||
.Where(rt => rt != null)
|
||||
.Where(rt => rt!.IsIntegerType() == false)
|
||||
.Select(rt => rt!.TypeID)
|
||||
.Union(resourceTable.ResourceTypes
|
||||
.Where(rt => rt != null)
|
||||
.SelectMany(rt => rt!.Resources ?? [])
|
||||
.Where(r => r!.IsIntegerType() == false)
|
||||
.Select(r => r!.ResourceID))
|
||||
.Distinct()
|
||||
.OrderBy(o => o)
|
||||
.ToList();
|
||||
|
||||
// Populate the type and name string dictionary
|
||||
resourceTable.TypeAndNameStrings = [];
|
||||
for (int i = 0; i < stringOffsets.Count; i++)
|
||||
{
|
||||
int stringOffset = (int)(stringOffsets[i] + initialOffset);
|
||||
data.Seek(stringOffset, SeekOrigin.Begin);
|
||||
var str = new ResourceTypeAndNameString();
|
||||
str.Length = data.ReadByteValue();
|
||||
str.Text = data.ReadBytes(str.Length);
|
||||
resourceTable.TypeAndNameStrings[stringOffsets[i]] = str;
|
||||
}
|
||||
|
||||
return resourceTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a resident-name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the resident-name table</param>
|
||||
/// <returns>Filled resident-name table on success, null on error</returns>
|
||||
public static ResidentNameTableEntry[] ParseResidentNameTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var residentNameTable = new List<ResidentNameTableEntry>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
var entry = new ResidentNameTableEntry();
|
||||
entry.Length = data.ReadByteValue();
|
||||
entry.NameString = data.ReadBytes(entry.Length);
|
||||
entry.OrdinalNumber = data.ReadUInt16();
|
||||
residentNameTable.Add(entry);
|
||||
}
|
||||
|
||||
return [.. residentNameTable];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a module-reference table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of module-reference table entries to read</param>
|
||||
/// <returns>Filled module-reference table on success, null on error</returns>
|
||||
public static ModuleReferenceTableEntry[] ParseModuleReferenceTable(Stream data, int count)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var moduleReferenceTable = new ModuleReferenceTableEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = new ModuleReferenceTableEntry();
|
||||
entry.Offset = data.ReadUInt16();
|
||||
moduleReferenceTable[i] = entry;
|
||||
}
|
||||
|
||||
return moduleReferenceTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an imported-name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the imported-name table</param>
|
||||
/// <returns>Filled imported-name table on success, null on error</returns>
|
||||
public static Dictionary<ushort, ImportedNameTableEntry?> ParseImportedNameTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var importedNameTable = new Dictionary<ushort, ImportedNameTableEntry?>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
ushort currentOffset = (ushort)data.Position;
|
||||
var entry = new ImportedNameTableEntry();
|
||||
entry.Length = data.ReadByteValue();
|
||||
entry.NameString = data.ReadBytes(entry.Length);
|
||||
importedNameTable[currentOffset] = entry;
|
||||
}
|
||||
|
||||
return importedNameTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an entry table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the entry table</param>
|
||||
/// <returns>Filled entry table on success, null on error</returns>
|
||||
public static EntryTableBundle[] ParseEntryTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var entryTable = new List<EntryTableBundle>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
var entry = new EntryTableBundle();
|
||||
entry.EntryCount = data.ReadByteValue();
|
||||
entry.SegmentIndicator = data.ReadByteValue();
|
||||
switch (entry.GetEntryType())
|
||||
{
|
||||
case SegmentEntryType.Unused:
|
||||
break;
|
||||
|
||||
case SegmentEntryType.FixedSegment:
|
||||
entry.FixedFlagWord = (FixedSegmentEntryFlag)data.ReadByteValue();
|
||||
entry.FixedOffset = data.ReadUInt16();
|
||||
break;
|
||||
|
||||
case SegmentEntryType.MoveableSegment:
|
||||
entry.MoveableFlagWord = (MoveableSegmentEntryFlag)data.ReadByteValue();
|
||||
entry.MoveableReserved = data.ReadUInt16();
|
||||
entry.MoveableSegmentNumber = data.ReadByteValue();
|
||||
entry.MoveableOffset = data.ReadUInt16();
|
||||
break;
|
||||
}
|
||||
entryTable.Add(entry);
|
||||
}
|
||||
|
||||
return [.. entryTable];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a nonresident-name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the nonresident-name table</param>
|
||||
/// <returns>Filled nonresident-name table on success, null on error</returns>
|
||||
public static NonResidentNameTableEntry[] ParseNonResidentNameTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var residentNameTable = new List<NonResidentNameTableEntry>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
var entry = new NonResidentNameTableEntry();
|
||||
entry.Length = data.ReadByteValue();
|
||||
entry.NameString = data.ReadBytes(entry.Length);
|
||||
entry.OrdinalNumber = data.ReadUInt16();
|
||||
residentNameTable.Add(entry);
|
||||
}
|
||||
|
||||
return [.. residentNameTable];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.Nitro;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Nitro :
|
||||
IByteDeserializer<Models.Nitro.Cart>,
|
||||
IFileDeserializer<Models.Nitro.Cart>
|
||||
IByteDeserializer<Cart>,
|
||||
IFileDeserializer<Cart>,
|
||||
IStreamDeserializer<Cart>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.Nitro.Cart? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Cart? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new Nitro();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Nitro.Cart? Deserialize(byte[]? data, int offset)
|
||||
public Cart? 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.Nitro.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +42,380 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.Nitro.Cart? DeserializeFile(string? path)
|
||||
public static Cart? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new Nitro();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Nitro.Cart? Deserialize(string? path)
|
||||
public Cart? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.Nitro.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Cart? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Nitro();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cart? 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 cart image to fill
|
||||
var cart = new Cart();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseCommonHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the cart image header
|
||||
cart.CommonHeader = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended DSi Header
|
||||
|
||||
// If we have a DSi-compatible cartridge
|
||||
if (header.UnitCode == Unitcode.NDSPlusDSi || header.UnitCode == Unitcode.DSi)
|
||||
{
|
||||
var extendedDSiHeader = ParseExtendedDSiHeader(data);
|
||||
if (extendedDSiHeader == null)
|
||||
return null;
|
||||
|
||||
cart.ExtendedDSiHeader = extendedDSiHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Secure Area
|
||||
|
||||
// Try to get the secure area offset
|
||||
long secureAreaOffset = 0x4000;
|
||||
if (secureAreaOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the secure area
|
||||
data.Seek(secureAreaOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the secure area without processing
|
||||
cart.SecureArea = data.ReadBytes(0x800);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Name Table
|
||||
|
||||
// Try to get the name table offset
|
||||
long nameTableOffset = header.FileNameTableOffset;
|
||||
if (nameTableOffset < 0 || nameTableOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the name table
|
||||
data.Seek(nameTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the name table
|
||||
var nameTable = ParseNameTable(data);
|
||||
if (nameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the name table
|
||||
cart.NameTable = nameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Allocation Table
|
||||
|
||||
// Try to get the file allocation table offset
|
||||
long fileAllocationTableOffset = header.FileAllocationTableOffset;
|
||||
if (fileAllocationTableOffset < 0 || fileAllocationTableOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the file allocation table
|
||||
data.Seek(fileAllocationTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the file allocation table
|
||||
var fileAllocationTable = new List<FileAllocationTableEntry>();
|
||||
|
||||
// Try to parse the file allocation table
|
||||
while (data.Position - fileAllocationTableOffset < header.FileAllocationTableLength)
|
||||
{
|
||||
var entry = ParseFileAllocationTableEntry(data);
|
||||
fileAllocationTable.Add(entry);
|
||||
}
|
||||
|
||||
// Set the file allocation table
|
||||
cart.FileAllocationTable = fileAllocationTable.ToArray();
|
||||
|
||||
#endregion
|
||||
|
||||
// TODO: Read and optionally parse out the other areas
|
||||
// Look for offsets and lengths in the header pieces
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a common header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled common header on success, null on error</returns>
|
||||
private static CommonHeader ParseCommonHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
CommonHeader commonHeader = new CommonHeader();
|
||||
|
||||
byte[]? gameTitle = data.ReadBytes(12);
|
||||
if (gameTitle != null)
|
||||
commonHeader.GameTitle = Encoding.ASCII.GetString(gameTitle).TrimEnd('\0');
|
||||
commonHeader.GameCode = data.ReadUInt32();
|
||||
byte[]? makerCode = data.ReadBytes(2);
|
||||
if (makerCode != null)
|
||||
commonHeader.MakerCode = Encoding.ASCII.GetString(bytes: makerCode).TrimEnd('\0');
|
||||
commonHeader.UnitCode = (Unitcode)data.ReadByteValue();
|
||||
commonHeader.EncryptionSeedSelect = data.ReadByteValue();
|
||||
commonHeader.DeviceCapacity = data.ReadByteValue();
|
||||
commonHeader.Reserved1 = data.ReadBytes(7);
|
||||
commonHeader.GameRevision = data.ReadUInt16();
|
||||
commonHeader.RomVersion = data.ReadByteValue();
|
||||
commonHeader.InternalFlags = data.ReadByteValue();
|
||||
commonHeader.ARM9RomOffset = data.ReadUInt32();
|
||||
commonHeader.ARM9EntryAddress = data.ReadUInt32();
|
||||
commonHeader.ARM9LoadAddress = data.ReadUInt32();
|
||||
commonHeader.ARM9Size = data.ReadUInt32();
|
||||
commonHeader.ARM7RomOffset = data.ReadUInt32();
|
||||
commonHeader.ARM7EntryAddress = data.ReadUInt32();
|
||||
commonHeader.ARM7LoadAddress = data.ReadUInt32();
|
||||
commonHeader.ARM7Size = data.ReadUInt32();
|
||||
commonHeader.FileNameTableOffset = data.ReadUInt32();
|
||||
commonHeader.FileNameTableLength = data.ReadUInt32();
|
||||
commonHeader.FileAllocationTableOffset = data.ReadUInt32();
|
||||
commonHeader.FileAllocationTableLength = data.ReadUInt32();
|
||||
commonHeader.ARM9OverlayOffset = data.ReadUInt32();
|
||||
commonHeader.ARM9OverlayLength = data.ReadUInt32();
|
||||
commonHeader.ARM7OverlayOffset = data.ReadUInt32();
|
||||
commonHeader.ARM7OverlayLength = data.ReadUInt32();
|
||||
commonHeader.NormalCardControlRegisterSettings = data.ReadUInt32();
|
||||
commonHeader.SecureCardControlRegisterSettings = data.ReadUInt32();
|
||||
commonHeader.IconBannerOffset = data.ReadUInt32();
|
||||
commonHeader.SecureAreaCRC = data.ReadUInt16();
|
||||
commonHeader.SecureTransferTimeout = data.ReadUInt16();
|
||||
commonHeader.ARM9Autoload = data.ReadUInt32();
|
||||
commonHeader.ARM7Autoload = data.ReadUInt32();
|
||||
commonHeader.SecureDisable = data.ReadBytes(8);
|
||||
commonHeader.NTRRegionRomSize = data.ReadUInt32();
|
||||
commonHeader.HeaderSize = data.ReadUInt32();
|
||||
commonHeader.Reserved2 = data.ReadBytes(56);
|
||||
commonHeader.NintendoLogo = data.ReadBytes(156);
|
||||
commonHeader.NintendoLogoCRC = data.ReadUInt16();
|
||||
commonHeader.HeaderCRC = data.ReadUInt16();
|
||||
commonHeader.DebuggerReserved = data.ReadBytes(0x20);
|
||||
|
||||
return commonHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an extended DSi header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled extended DSi header on success, null on error</returns>
|
||||
private static ExtendedDSiHeader ParseExtendedDSiHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ExtendedDSiHeader extendedDSiHeader = new ExtendedDSiHeader();
|
||||
|
||||
extendedDSiHeader.GlobalMBK15Settings = new uint[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
extendedDSiHeader.GlobalMBK15Settings[i] = data.ReadUInt32();
|
||||
}
|
||||
extendedDSiHeader.LocalMBK68SettingsARM9 = new uint[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
extendedDSiHeader.LocalMBK68SettingsARM9[i] = data.ReadUInt32();
|
||||
}
|
||||
extendedDSiHeader.LocalMBK68SettingsARM7 = new uint[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
extendedDSiHeader.LocalMBK68SettingsARM7[i] = data.ReadUInt32();
|
||||
}
|
||||
extendedDSiHeader.GlobalMBK9Setting = data.ReadUInt32();
|
||||
extendedDSiHeader.RegionFlags = data.ReadUInt32();
|
||||
extendedDSiHeader.AccessControl = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7SCFGEXTMask = data.ReadUInt32();
|
||||
extendedDSiHeader.ReservedFlags = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM9iRomOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.Reserved3 = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM9iLoadAddress = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM9iSize = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7iRomOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.Reserved4 = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7iLoadAddress = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7iSize = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestNTRRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestNTRRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestTWLRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestTWLRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestSectorHashtableRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestSectorHashtableRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestBlockHashtableRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestBlockHashtableRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestSectorSize = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestBlockSectorCount = data.ReadUInt32();
|
||||
extendedDSiHeader.IconBannerSize = data.ReadUInt32();
|
||||
extendedDSiHeader.Unknown1 = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea1Offset = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea1Size = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea2Offset = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea2Size = data.ReadUInt32();
|
||||
extendedDSiHeader.TitleID = data.ReadBytes(8);
|
||||
extendedDSiHeader.DSiWarePublicSavSize = data.ReadUInt32();
|
||||
extendedDSiHeader.DSiWarePrivateSavSize = data.ReadUInt32();
|
||||
extendedDSiHeader.ReservedZero = data.ReadBytes(176);
|
||||
extendedDSiHeader.Unknown2 = data.ReadBytes(0x10);
|
||||
extendedDSiHeader.ARM9WithSecureAreaSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.ARM7SHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.DigestMasterSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.BannerSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.ARM9iDecryptedSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.ARM7iDecryptedSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.Reserved5 = data.ReadBytes(40);
|
||||
extendedDSiHeader.ARM9NoSecureAreaSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.Reserved6 = data.ReadBytes(2636);
|
||||
extendedDSiHeader.ReservedAndUnchecked = data.ReadBytes(0x180);
|
||||
extendedDSiHeader.RSASignature = data.ReadBytes(0x80);
|
||||
|
||||
return extendedDSiHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled name table on success, null on error</returns>
|
||||
private static NameTable ParseNameTable(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
NameTable nameTable = new NameTable();
|
||||
|
||||
// Create a variable-length table
|
||||
var folderAllocationTable = new List<FolderAllocationTableEntry>();
|
||||
int entryCount = int.MaxValue;
|
||||
while (entryCount > 0)
|
||||
{
|
||||
var entry = ParseFolderAllocationTableEntry(data);
|
||||
folderAllocationTable.Add(entry);
|
||||
|
||||
// If we have the root entry
|
||||
if (entryCount == int.MaxValue)
|
||||
entryCount = (entry.Unknown << 8) | entry.ParentFolderIndex;
|
||||
|
||||
// Decrement the entry count
|
||||
entryCount--;
|
||||
}
|
||||
|
||||
// Assign the folder allocation table
|
||||
nameTable.FolderAllocationTable = folderAllocationTable.ToArray();
|
||||
|
||||
// Create a variable-length table
|
||||
var nameList = new List<NameListEntry>();
|
||||
while (true)
|
||||
{
|
||||
var entry = ParseNameListEntry(data);
|
||||
if (entry == null)
|
||||
break;
|
||||
|
||||
nameList.Add(entry);
|
||||
}
|
||||
|
||||
// Assign the name list
|
||||
nameTable.NameList = nameList.ToArray();
|
||||
|
||||
return nameTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a folder allocation table entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled folder allocation table entry on success, null on error</returns>
|
||||
private static FolderAllocationTableEntry ParseFolderAllocationTableEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FolderAllocationTableEntry entry = new FolderAllocationTableEntry();
|
||||
|
||||
entry.StartOffset = data.ReadUInt32();
|
||||
entry.FirstFileIndex = data.ReadUInt16();
|
||||
entry.ParentFolderIndex = data.ReadByteValue();
|
||||
entry.Unknown = data.ReadByteValue();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a name list entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled name list entry on success, null on error</returns>
|
||||
private static NameListEntry? ParseNameListEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
NameListEntry entry = new NameListEntry();
|
||||
|
||||
byte flagAndSize = data.ReadByteValue();
|
||||
if (flagAndSize == 0xFF)
|
||||
return null;
|
||||
|
||||
entry.Folder = (flagAndSize & 0x80) != 0;
|
||||
|
||||
byte size = (byte)(flagAndSize & ~0x80);
|
||||
if (size > 0)
|
||||
{
|
||||
byte[]? name = data.ReadBytes(size);
|
||||
if (name != null)
|
||||
entry.Name = Encoding.UTF8.GetString(name);
|
||||
}
|
||||
|
||||
if (entry.Folder)
|
||||
entry.Index = data.ReadUInt16();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a name list entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled name list entry on success, null on error</returns>
|
||||
private static FileAllocationTableEntry ParseFileAllocationTableEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FileAllocationTableEntry entry = new FileAllocationTableEntry();
|
||||
|
||||
entry.StartOffset = data.ReadUInt32();
|
||||
entry.EndOffset = data.ReadUInt32();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class OfflineList : XmlFile<Models.OfflineList.Dat>
|
||||
public class OfflineList :
|
||||
XmlFile<Models.OfflineList.Dat>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.OfflineList.Dat? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new OfflineList();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.OfflineList.Dat? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new OfflineList();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class OpenMSX : XmlFile<Models.OpenMSX.SoftwareDb>
|
||||
public class OpenMSX :
|
||||
XmlFile<Models.OpenMSX.SoftwareDb>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.OpenMSX.SoftwareDb? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new OpenMSX();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.OpenMSX.SoftwareDb? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new OpenMSX();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PAK;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PAK.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class PAK :
|
||||
IByteDeserializer<Models.PAK.File>,
|
||||
IFileDeserializer<Models.PAK.File>
|
||||
IFileDeserializer<Models.PAK.File>,
|
||||
IStreamDeserializer<Models.PAK.File>
|
||||
{
|
||||
#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.PAK.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +52,115 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.PAK.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.PAK.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.PAK.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PAK();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PAK.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life Package to fill
|
||||
var file = new Models.PAK.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Items
|
||||
|
||||
// Get the directory items offset
|
||||
uint directoryItemsOffset = header.DirectoryOffset;
|
||||
if (directoryItemsOffset < 0 || directoryItemsOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the directory items
|
||||
data.Seek(directoryItemsOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the directory item array
|
||||
file.DirectoryItems = new DirectoryItem[header.DirectoryLength / 64];
|
||||
|
||||
// Try to parse the directory items
|
||||
for (int i = 0; i < file.DirectoryItems.Length; i++)
|
||||
{
|
||||
var directoryItem = ParseDirectoryItem(data);
|
||||
file.DirectoryItems[i] = directoryItem;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Package header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Package header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.DirectoryOffset = data.ReadUInt32();
|
||||
header.DirectoryLength = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Package directory item
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Package directory item on success, null on error</returns>
|
||||
private static DirectoryItem ParseDirectoryItem(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryItem directoryItem = new DirectoryItem();
|
||||
|
||||
byte[]? itemName = data.ReadBytes(56);
|
||||
if (itemName != null)
|
||||
directoryItem.ItemName = Encoding.ASCII.GetString(itemName).TrimEnd('\0');
|
||||
directoryItem.ItemOffset = data.ReadUInt32();
|
||||
directoryItem.ItemLength = data.ReadUInt32();
|
||||
|
||||
return directoryItem;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PFF;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PFF.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class PFF :
|
||||
IByteDeserializer<Models.PFF.Archive>,
|
||||
IFileDeserializer<Models.PFF.Archive>
|
||||
IByteDeserializer<Archive>,
|
||||
IFileDeserializer<Archive>,
|
||||
IStreamDeserializer<Archive>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.PFF.Archive? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Archive? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new PFF();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PFF.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.PFF.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +42,200 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.PFF.Archive? DeserializeFile(string? path)
|
||||
public static Archive? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new PFF();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PFF.Archive? Deserialize(string? path)
|
||||
public Archive? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.PFF.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PFF();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 Segments
|
||||
|
||||
// Get the segments
|
||||
long offset = header.FileListOffset;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the segments
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create the segments array
|
||||
archive.Segments = new Segment[header.NumberOfFiles];
|
||||
|
||||
// Read all segments in turn
|
||||
for (int i = 0; i < header.NumberOfFiles; i++)
|
||||
{
|
||||
var file = ParseSegment(data, header.FileSegmentSize);
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
archive.Segments[i] = file;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Footer
|
||||
|
||||
// Get the footer offset
|
||||
offset = header.FileListOffset + (header.FileSegmentSize * header.NumberOfFiles);
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the footer
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the footer
|
||||
var footer = ParseFooter(data);
|
||||
if (footer == null)
|
||||
return null;
|
||||
|
||||
// Set the archive footer
|
||||
archive.Footer = footer;
|
||||
|
||||
#endregion
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.HeaderSize = data.ReadUInt32();
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
header.NumberOfFiles = data.ReadUInt32();
|
||||
header.FileSegmentSize = data.ReadUInt32();
|
||||
switch (header.Signature)
|
||||
{
|
||||
case Version0SignatureString:
|
||||
if (header.FileSegmentSize != Version0HSegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
case Version2SignatureString:
|
||||
if (header.FileSegmentSize != Version2SegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
// Version 3 can sometimes have Version 2 segment sizes
|
||||
case Version3SignatureString:
|
||||
if (header.FileSegmentSize != Version2SegmentSize && header.FileSegmentSize != Version3SegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
case Version4SignatureString:
|
||||
if (header.FileSegmentSize != Version4SegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
header.FileListOffset = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a footer
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled footer on success, null on error</returns>
|
||||
private static Footer ParseFooter(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Footer footer = new Footer();
|
||||
|
||||
footer.SystemIP = data.ReadUInt32();
|
||||
footer.Reserved = data.ReadUInt32();
|
||||
byte[]? kingTag = data.ReadBytes(4);
|
||||
if (kingTag != null)
|
||||
footer.KingTag = Encoding.ASCII.GetString(kingTag);
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="segmentSize">PFF segment size</param>
|
||||
/// <returns>Filled file entry on success, null on error</returns>
|
||||
private static Segment ParseSegment(Stream data, uint segmentSize)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Segment segment = new Segment();
|
||||
|
||||
segment.Deleted = data.ReadUInt32();
|
||||
segment.FileLocation = data.ReadUInt32();
|
||||
segment.FileSize = data.ReadUInt32();
|
||||
segment.PackedDate = data.ReadUInt32();
|
||||
byte[]? fileName = data.ReadBytes(0x10);
|
||||
if (fileName != null)
|
||||
segment.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
|
||||
if (segmentSize > Version2SegmentSize)
|
||||
segment.ModifiedDate = data.ReadUInt32();
|
||||
if (segmentSize > Version3SegmentSize)
|
||||
segment.CompressionLevel = data.ReadUInt32();
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,237 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PIC;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PIC.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class PIC : IFileDeserializer<Models.PIC.DiscInformation>
|
||||
public class PIC :
|
||||
IByteDeserializer<DiscInformation>,
|
||||
IFileDeserializer<DiscInformation>,
|
||||
IStreamDeserializer<DiscInformation>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static DiscInformation? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new PIC();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DiscInformation? Deserialize(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
// If the offset is out of bounds
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Create a memory stream and parse that
|
||||
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.PIC.DiscInformation? DeserializeFile(string? path)
|
||||
public static DiscInformation? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new PIC();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PIC.DiscInformation? Deserialize(string? path)
|
||||
public DiscInformation? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.PIC.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static DiscInformation? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PIC();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DiscInformation? 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;
|
||||
|
||||
var di = new DiscInformation();
|
||||
|
||||
// Read the initial disc information
|
||||
di.DataStructureLength = data.ReadUInt16BigEndian();
|
||||
di.Reserved0 = data.ReadByteValue();
|
||||
di.Reserved1 = data.ReadByteValue();
|
||||
|
||||
// Create a list for the units
|
||||
var diUnits = new List<DiscInformationUnit>();
|
||||
|
||||
// Loop and read all available units
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
var unit = ParseDiscInformationUnit(data);
|
||||
if (unit == null)
|
||||
continue;
|
||||
|
||||
diUnits.Add(unit);
|
||||
}
|
||||
|
||||
// Assign the units and return
|
||||
di.Units = diUnits.ToArray();
|
||||
return di;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit on success, null on error</returns>
|
||||
private static DiscInformationUnit? ParseDiscInformationUnit(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var unit = new DiscInformationUnit();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseDiscInformationUnitHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the information unit header
|
||||
unit.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Body
|
||||
|
||||
// Try to parse the body
|
||||
var body = ParseDiscInformationUnitBody(data);
|
||||
if (body == null)
|
||||
return null;
|
||||
|
||||
// Set the information unit body
|
||||
unit.Body = body;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trailer
|
||||
|
||||
if (unit.Body.DiscTypeIdentifier == DiscTypeIdentifierReWritable || unit.Body.DiscTypeIdentifier == DiscTypeIdentifierRecordable)
|
||||
{
|
||||
// Try to parse the trailer
|
||||
var trailer = ParseDiscInformationUnitTrailer(data);
|
||||
if (trailer == null)
|
||||
return null;
|
||||
|
||||
// Set the information unit trailer
|
||||
unit.Trailer = trailer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return unit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit header on success, null on error</returns>
|
||||
private static DiscInformationUnitHeader? ParseDiscInformationUnitHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new DiscInformationUnitHeader();
|
||||
|
||||
// We only accept Disc Information units, not Emergency Brake or other
|
||||
byte[]? dic = data.ReadBytes(2);
|
||||
if (dic == null)
|
||||
return null;
|
||||
|
||||
header.DiscInformationIdentifier = Encoding.ASCII.GetString(dic);
|
||||
if (header.DiscInformationIdentifier != "DI")
|
||||
return null;
|
||||
|
||||
header.DiscInformationFormat = data.ReadByteValue();
|
||||
header.NumberOfUnitsInBlock = data.ReadByteValue();
|
||||
header.Reserved0 = data.ReadByteValue();
|
||||
header.SequenceNumber = data.ReadByteValue();
|
||||
header.BytesInUse = data.ReadByteValue();
|
||||
header.Reserved1 = data.ReadByteValue();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit body
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit body on success, null on error</returns>
|
||||
private static DiscInformationUnitBody? ParseDiscInformationUnitBody(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var body = new DiscInformationUnitBody();
|
||||
|
||||
byte[]? dti = data.ReadBytes(3);
|
||||
if (dti == null)
|
||||
return null;
|
||||
|
||||
body.DiscTypeIdentifier = Encoding.ASCII.GetString(dti);
|
||||
body.DiscSizeClassVersion = data.ReadByteValue();
|
||||
switch (body.DiscTypeIdentifier)
|
||||
{
|
||||
case DiscTypeIdentifierROM:
|
||||
case DiscTypeIdentifierROMUltra:
|
||||
case DiscTypeIdentifierXGD4:
|
||||
body.FormatDependentContents = data.ReadBytes(52);
|
||||
break;
|
||||
case DiscTypeIdentifierReWritable:
|
||||
case DiscTypeIdentifierRecordable:
|
||||
body.FormatDependentContents = data.ReadBytes(100);
|
||||
break;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit trailer
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit trailer on success, null on error</returns>
|
||||
private static DiscInformationUnitTrailer ParseDiscInformationUnitTrailer(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var trailer = new DiscInformationUnitTrailer();
|
||||
|
||||
trailer.DiscManufacturerID = data.ReadBytes(6);
|
||||
trailer.MediaTypeID = data.ReadBytes(3);
|
||||
trailer.TimeStamp = data.ReadUInt16();
|
||||
trailer.ProductRevisionNumber = data.ReadByteValue();
|
||||
|
||||
return trailer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PlayJ;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PlayJ.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class PlayJAudio :
|
||||
IByteDeserializer<Models.PlayJ.AudioFile>,
|
||||
IFileDeserializer<Models.PlayJ.AudioFile>
|
||||
IByteDeserializer<AudioFile>,
|
||||
IFileDeserializer<AudioFile>,
|
||||
IStreamDeserializer<AudioFile>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.PlayJ.AudioFile? DeserializeBytes(byte[]? data, int offset)
|
||||
public static AudioFile? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new PlayJAudio();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PlayJ.AudioFile? Deserialize(byte[]? data, int offset)
|
||||
public AudioFile? 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.PlayJAudio.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +42,360 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.PlayJ.AudioFile? DeserializeFile(string? path)
|
||||
public static AudioFile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new PlayJAudio();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PlayJ.AudioFile? Deserialize(string? path)
|
||||
public AudioFile? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.PlayJAudio.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static AudioFile? DeserializeStream(Stream? data, long adjust = 0)
|
||||
{
|
||||
var deserializer = new PlayJAudio();
|
||||
return deserializer.Deserialize(data, adjust);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioFile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, 0);
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
/// <param name="adjust">Offset to adjust all seeking by</param>
|
||||
public AudioFile? Deserialize(Stream? data, long adjust)
|
||||
{
|
||||
// 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 audio file to fill
|
||||
var audioFile = new AudioFile();
|
||||
|
||||
#region Audio Header
|
||||
|
||||
// Try to parse the audio header
|
||||
var audioHeader = ParseAudioHeader(data);
|
||||
if (audioHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the audio header
|
||||
audioFile.Header = audioHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Block 1
|
||||
|
||||
uint unknownOffset1 = (audioHeader.Version == 0x00000000)
|
||||
? (audioHeader as AudioHeaderV1)?.UnknownOffset1 ?? 0
|
||||
: ((audioHeader as AudioHeaderV2)?.UnknownOffset1 ?? 0) + 0x54;
|
||||
|
||||
// If we have an unknown block 1 offset
|
||||
if (unknownOffset1 > 0)
|
||||
{
|
||||
// Get the unknown block 1 offset
|
||||
long offset = unknownOffset1 + adjust;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the unknown block 1
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Try to parse the unknown block 1
|
||||
var unknownBlock1 = ParseUnknownBlock1(data);
|
||||
if (unknownBlock1 == null)
|
||||
return null;
|
||||
|
||||
// Set the unknown block 1
|
||||
audioFile.UnknownBlock1 = unknownBlock1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region V1 Only
|
||||
|
||||
// If we have a V1 file
|
||||
if (audioHeader.Version == 0x00000000)
|
||||
{
|
||||
#region Unknown Value 2
|
||||
|
||||
// Get the V1 unknown offset 2
|
||||
uint? unknownOffset2 = (audioHeader as AudioHeaderV1)?.UnknownOffset2;
|
||||
|
||||
// If we have an unknown value 2 offset
|
||||
if (unknownOffset2 != null && unknownOffset2 > 0)
|
||||
{
|
||||
// Get the unknown value 2 offset
|
||||
long offset = unknownOffset2.Value + adjust;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the unknown value 2
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Set the unknown value 2
|
||||
audioFile.UnknownValue2 = data.ReadUInt32();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Block 3
|
||||
|
||||
// Get the V1 unknown offset 3
|
||||
uint? unknownOffset3 = (audioHeader as AudioHeaderV1)?.UnknownOffset3;
|
||||
|
||||
// If we have an unknown block 3 offset
|
||||
if (unknownOffset3 != null && unknownOffset3 > 0)
|
||||
{
|
||||
// Get the unknown block 3 offset
|
||||
long offset = unknownOffset3.Value + adjust;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the unknown block 3
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Try to parse the unknown block 3
|
||||
var unknownBlock3 = ParseUnknownBlock3(data);
|
||||
if (unknownBlock3 == null)
|
||||
return null;
|
||||
|
||||
// Set the unknown block 3
|
||||
audioFile.UnknownBlock3 = unknownBlock3;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region V2 Only
|
||||
|
||||
// If we have a V2 file
|
||||
if (audioHeader.Version == 0x0000000A)
|
||||
{
|
||||
#region Data Files Count
|
||||
|
||||
// Set the data files count
|
||||
audioFile.DataFilesCount = data.ReadUInt32();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Files
|
||||
|
||||
// Create the data files array
|
||||
audioFile.DataFiles = new DataFile[audioFile.DataFilesCount];
|
||||
|
||||
// Try to parse the data files
|
||||
for (int i = 0; i < audioFile.DataFiles.Length; i++)
|
||||
{
|
||||
var dataFile = ParseDataFile(data);
|
||||
if (dataFile == null)
|
||||
return null;
|
||||
|
||||
audioFile.DataFiles[i] = dataFile;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return audioFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an audio header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled audio header on success, null on error</returns>
|
||||
private static AudioHeader? ParseAudioHeader(Stream data)
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// TODO: Use marshalling here instead of building
|
||||
AudioHeader audioHeader;
|
||||
|
||||
// Get the common header pieces
|
||||
uint signature = data.ReadUInt32();
|
||||
if (signature != SignatureUInt32)
|
||||
return null;
|
||||
|
||||
uint version = data.ReadUInt32();
|
||||
|
||||
// Build the header according to version
|
||||
uint unknownOffset1;
|
||||
switch (version)
|
||||
{
|
||||
// Version 1
|
||||
case 0x00000000:
|
||||
AudioHeaderV1 v1 = new AudioHeaderV1();
|
||||
|
||||
v1.Signature = signature;
|
||||
v1.Version = version;
|
||||
v1.TrackID = data.ReadUInt32();
|
||||
v1.UnknownOffset1 = data.ReadUInt32();
|
||||
v1.UnknownOffset2 = data.ReadUInt32();
|
||||
v1.UnknownOffset3 = data.ReadUInt32();
|
||||
v1.Unknown1 = data.ReadUInt32();
|
||||
v1.Unknown2 = data.ReadUInt32();
|
||||
v1.Year = data.ReadUInt32();
|
||||
v1.TrackNumber = data.ReadByteValue();
|
||||
v1.Subgenre = (Subgenre)data.ReadByteValue();
|
||||
v1.Duration = data.ReadUInt32();
|
||||
|
||||
audioHeader = v1;
|
||||
unknownOffset1 = v1.UnknownOffset1;
|
||||
break;
|
||||
|
||||
// Version 2
|
||||
case 0x0000000A:
|
||||
AudioHeaderV2 v2 = new AudioHeaderV2();
|
||||
|
||||
v2.Signature = signature;
|
||||
v2.Version = version;
|
||||
v2.Unknown1 = data.ReadUInt32();
|
||||
v2.Unknown2 = data.ReadUInt32();
|
||||
v2.Unknown3 = data.ReadUInt32();
|
||||
v2.Unknown4 = data.ReadUInt32();
|
||||
v2.Unknown5 = data.ReadUInt32();
|
||||
v2.Unknown6 = data.ReadUInt32();
|
||||
v2.UnknownOffset1 = data.ReadUInt32();
|
||||
v2.Unknown7 = data.ReadUInt32();
|
||||
v2.Unknown8 = data.ReadUInt32();
|
||||
v2.Unknown9 = data.ReadUInt32();
|
||||
v2.UnknownOffset2 = data.ReadUInt32();
|
||||
v2.Unknown10 = data.ReadUInt32();
|
||||
v2.Unknown11 = data.ReadUInt32();
|
||||
v2.Unknown12 = data.ReadUInt32();
|
||||
v2.Unknown13 = data.ReadUInt32();
|
||||
v2.Unknown14 = data.ReadUInt32();
|
||||
v2.Unknown15 = data.ReadUInt32();
|
||||
v2.Unknown16 = data.ReadUInt32();
|
||||
v2.Unknown17 = data.ReadUInt32();
|
||||
v2.TrackID = data.ReadUInt32();
|
||||
v2.Year = data.ReadUInt32();
|
||||
v2.TrackNumber = data.ReadUInt32();
|
||||
v2.Unknown18 = data.ReadUInt32();
|
||||
|
||||
audioHeader = v2;
|
||||
unknownOffset1 = v2.UnknownOffset1 + 0x54;
|
||||
break;
|
||||
|
||||
// No other version are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
audioHeader.TrackLength = data.ReadUInt16();
|
||||
byte[]? track = data.ReadBytes(audioHeader.TrackLength);
|
||||
if (track != null)
|
||||
audioHeader.Track = Encoding.ASCII.GetString(track);
|
||||
|
||||
audioHeader.ArtistLength = data.ReadUInt16();
|
||||
byte[]? artist = data.ReadBytes(audioHeader.ArtistLength);
|
||||
if (artist != null)
|
||||
audioHeader.Artist = Encoding.ASCII.GetString(artist);
|
||||
|
||||
audioHeader.AlbumLength = data.ReadUInt16();
|
||||
byte[]? album = data.ReadBytes(audioHeader.AlbumLength);
|
||||
if (album != null)
|
||||
audioHeader.Album = Encoding.ASCII.GetString(album);
|
||||
|
||||
audioHeader.WriterLength = data.ReadUInt16();
|
||||
byte[]? writer = data.ReadBytes(audioHeader.WriterLength);
|
||||
if (writer != null)
|
||||
audioHeader.Writer = Encoding.ASCII.GetString(writer);
|
||||
|
||||
audioHeader.PublisherLength = data.ReadUInt16();
|
||||
byte[]? publisher = data.ReadBytes(audioHeader.PublisherLength);
|
||||
if (publisher != null)
|
||||
audioHeader.Publisher = Encoding.ASCII.GetString(publisher);
|
||||
|
||||
audioHeader.LabelLength = data.ReadUInt16();
|
||||
byte[]? label = data.ReadBytes(audioHeader.LabelLength);
|
||||
if (label != null)
|
||||
audioHeader.Label = Encoding.ASCII.GetString(label);
|
||||
|
||||
if (data.Position - initialOffset < unknownOffset1)
|
||||
{
|
||||
audioHeader.CommentsLength = data.ReadUInt16();
|
||||
byte[]? comments = data.ReadBytes(audioHeader.CommentsLength);
|
||||
if (comments != null)
|
||||
audioHeader.Comments = Encoding.ASCII.GetString(comments);
|
||||
}
|
||||
|
||||
return audioHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an unknown block 1
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled unknown block 1 on success, null on error</returns>
|
||||
private static UnknownBlock1 ParseUnknownBlock1(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownBlock1 unknownBlock1 = new UnknownBlock1();
|
||||
|
||||
unknownBlock1.Length = data.ReadUInt32();
|
||||
unknownBlock1.Data = data.ReadBytes((int)unknownBlock1.Length);
|
||||
|
||||
return unknownBlock1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an unknown block 3
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled unknown block 3 on success, null on error</returns>
|
||||
private static UnknownBlock3 ParseUnknownBlock3(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownBlock3 unknownBlock3 = new UnknownBlock3();
|
||||
|
||||
// No-op because we don't even know the length
|
||||
|
||||
return unknownBlock3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a data file
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled data file on success, null on error</returns>
|
||||
private static DataFile ParseDataFile(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DataFile dataFile = new DataFile();
|
||||
|
||||
dataFile.FileNameLength = data.ReadUInt16();
|
||||
byte[]? fileName = data.ReadBytes(dataFile.FileNameLength);
|
||||
if (fileName != null)
|
||||
dataFile.FileName = Encoding.ASCII.GetString(fileName);
|
||||
|
||||
dataFile.DataLength = data.ReadUInt32();
|
||||
dataFile.Data = data.ReadBytes((int)dataFile.DataLength);
|
||||
|
||||
return dataFile;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
using System.IO;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PlayJ;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class PlayJPlaylist :
|
||||
IByteDeserializer<Models.PlayJ.Playlist>,
|
||||
IFileDeserializer<Models.PlayJ.Playlist>
|
||||
IByteDeserializer<Playlist>,
|
||||
IFileDeserializer<Playlist>,
|
||||
IStreamDeserializer<Playlist>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.PlayJ.Playlist? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Playlist? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new PlayJPlaylist();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PlayJ.Playlist? Deserialize(byte[]? data, int offset)
|
||||
public Playlist? Deserialize(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null)
|
||||
@@ -29,7 +32,7 @@ namespace SabreTools.Serialization.Deserializers
|
||||
|
||||
// Create a memory stream and parse that
|
||||
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
||||
return Streams.PlayJPlaylist.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +40,94 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.PlayJ.Playlist? DeserializeFile(string? path)
|
||||
public static Playlist? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new PlayJPlaylist();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PlayJ.Playlist? Deserialize(string? path)
|
||||
public Playlist? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.PlayJPlaylist.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Playlist? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PlayJPlaylist();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Playlist? 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 playlist to fill
|
||||
var playlist = new Playlist();
|
||||
|
||||
#region Playlist Header
|
||||
|
||||
// Try to parse the playlist header
|
||||
var playlistHeader = ParsePlaylistHeader(data);
|
||||
if (playlistHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the playlist header
|
||||
playlist.Header = playlistHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Audio Files
|
||||
|
||||
// Create the audio files array
|
||||
playlist.AudioFiles = new AudioFile[playlistHeader.TrackCount];
|
||||
|
||||
// Try to parse the audio files
|
||||
for (int i = 0; i < playlist.AudioFiles.Length; i++)
|
||||
{
|
||||
long currentOffset = data.Position;
|
||||
var entryHeader = PlayJAudio.DeserializeStream(data, currentOffset);
|
||||
if (entryHeader == null)
|
||||
return null;
|
||||
|
||||
playlist.AudioFiles[i] = entryHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a playlist header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled playlist header on success, null on error</returns>
|
||||
private static PlaylistHeader ParsePlaylistHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
PlaylistHeader playlistHeader = new PlaylistHeader();
|
||||
|
||||
playlistHeader.TrackCount = data.ReadUInt32();
|
||||
playlistHeader.Data = data.ReadBytes(52);
|
||||
|
||||
return playlistHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,28 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.Quantum;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.Quantum.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class Quantum :
|
||||
IByteDeserializer<Models.Quantum.Archive>,
|
||||
IFileDeserializer<Models.Quantum.Archive>
|
||||
IByteDeserializer<Archive>,
|
||||
IFileDeserializer<Archive>,
|
||||
IStreamDeserializer<Archive>
|
||||
{
|
||||
#region IByteDeserializer
|
||||
|
||||
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
|
||||
public static Models.Quantum.Archive? DeserializeBytes(byte[]? data, int offset)
|
||||
public static Archive? DeserializeBytes(byte[]? data, int offset)
|
||||
{
|
||||
var deserializer = new Quantum();
|
||||
return deserializer.Deserialize(data, offset);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Quantum.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.Quantum.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -37,17 +42,173 @@ namespace SabreTools.Serialization.Deserializers
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.Quantum.Archive? DeserializeFile(string? path)
|
||||
public static Archive? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new Quantum();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Quantum.Archive? Deserialize(string? path)
|
||||
public Archive? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.Quantum.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Quantum();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 File List
|
||||
|
||||
// If we have any files
|
||||
if (header.FileCount > 0)
|
||||
{
|
||||
var fileDescriptors = new FileDescriptor[header.FileCount];
|
||||
|
||||
// Read all entries in turn
|
||||
for (int i = 0; i < header.FileCount; i++)
|
||||
{
|
||||
var file = ParseFileDescriptor(data, header.MinorVersion);
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
fileDescriptors[i] = file;
|
||||
}
|
||||
|
||||
// Set the file list
|
||||
archive.FileList = fileDescriptors;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the compressed data offset
|
||||
archive.CompressedDataOffset = data.Position;
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(2);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.MajorVersion = data.ReadByteValue();
|
||||
header.MinorVersion = data.ReadByteValue();
|
||||
header.FileCount = data.ReadUInt16();
|
||||
header.TableSize = data.ReadByteValue();
|
||||
header.CompressionFlags = data.ReadByteValue();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file descriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="minorVersion">Minor version of the archive</param>
|
||||
/// <returns>Filled file descriptor on success, null on error</returns>
|
||||
private static FileDescriptor ParseFileDescriptor(Stream data, byte minorVersion)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FileDescriptor fileDescriptor = new FileDescriptor();
|
||||
|
||||
fileDescriptor.FileNameSize = ReadVariableLength(data);
|
||||
if (fileDescriptor.FileNameSize > 0)
|
||||
{
|
||||
byte[]? fileName = data.ReadBytes(fileDescriptor.FileNameSize);
|
||||
if (fileName != null)
|
||||
fileDescriptor.FileName = Encoding.ASCII.GetString(fileName);
|
||||
}
|
||||
|
||||
fileDescriptor.CommentFieldSize = ReadVariableLength(data);
|
||||
if (fileDescriptor.CommentFieldSize > 0)
|
||||
{
|
||||
byte[]? commentField = data.ReadBytes(fileDescriptor.CommentFieldSize);
|
||||
if (commentField != null)
|
||||
fileDescriptor.CommentField = Encoding.ASCII.GetString(commentField);
|
||||
}
|
||||
|
||||
fileDescriptor.ExpandedFileSize = data.ReadUInt32();
|
||||
fileDescriptor.FileTime = data.ReadUInt16();
|
||||
fileDescriptor.FileDate = data.ReadUInt16();
|
||||
|
||||
// Hack for unknown format data
|
||||
if (minorVersion == 22)
|
||||
fileDescriptor.Unknown = data.ReadUInt16();
|
||||
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a variable-length size prefix
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Variable-length size prefix</returns>
|
||||
/// <remarks>
|
||||
/// Strings are prefixed with their length. If the length is less than 128
|
||||
/// then it is stored directly in one byte. If it is greater than 127 then
|
||||
/// the high bit of the first byte is set to 1 and the remaining fifteen bits
|
||||
/// contain the actual length in big-endian format.
|
||||
/// </remarks>
|
||||
private static int ReadVariableLength(Stream data)
|
||||
{
|
||||
byte b0 = data.ReadByteValue();
|
||||
if (b0 < 0x7F)
|
||||
return b0;
|
||||
|
||||
b0 &= 0x7F;
|
||||
byte b1 = data.ReadByteValue();
|
||||
return (b0 << 8) | b1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,23 +1,243 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.RomCenter;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class RomCenter : IFileDeserializer<Models.RomCenter.MetadataFile>
|
||||
public class RomCenter :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.RomCenter.MetadataFile? DeserializeFile(string? path)
|
||||
public static MetadataFile? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new RomCenter();
|
||||
return deserializer.Deserialize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.RomCenter.MetadataFile? Deserialize(string? path)
|
||||
public MetadataFile? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.RomCenter.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new RomCenter();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new IniReader(data, Encoding.UTF8)
|
||||
{
|
||||
ValidateRows = false,
|
||||
};
|
||||
var dat = new MetadataFile();
|
||||
|
||||
// Loop through and parse out the values
|
||||
var roms = new List<Rom>();
|
||||
var additional = new List<string>();
|
||||
var creditsAdditional = new List<string>();
|
||||
var datAdditional = new List<string>();
|
||||
var emulatorAdditional = new List<string>();
|
||||
var gamesAdditional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Ignore certain row types
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case IniRowType.None:
|
||||
case IniRowType.Comment:
|
||||
continue;
|
||||
case IniRowType.SectionHeader:
|
||||
switch (reader.Section?.ToLowerInvariant())
|
||||
{
|
||||
case "credits":
|
||||
dat.Credits ??= new Credits();
|
||||
break;
|
||||
case "dat":
|
||||
dat.Dat ??= new Dat();
|
||||
break;
|
||||
case "emulator":
|
||||
dat.Emulator ??= new Emulator();
|
||||
break;
|
||||
case "games":
|
||||
dat.Games ??= new Games();
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in credits
|
||||
if (reader.Section?.ToLowerInvariant() == "credits")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Credits ??= new Credits();
|
||||
|
||||
switch (reader.KeyValuePair?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "author":
|
||||
dat.Credits.Author = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "version":
|
||||
dat.Credits.Version = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "email":
|
||||
dat.Credits.Email = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "homepage":
|
||||
dat.Credits.Homepage = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "url":
|
||||
dat.Credits.Url = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "date":
|
||||
dat.Credits.Date = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "comment":
|
||||
dat.Credits.Comment = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
creditsAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in dat
|
||||
else if (reader.Section?.ToLowerInvariant() == "dat")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Dat ??= new Dat();
|
||||
|
||||
switch (reader.KeyValuePair?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "version":
|
||||
dat.Dat.Version = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "plugin":
|
||||
dat.Dat.Plugin = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "split":
|
||||
dat.Dat.Split = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "merge":
|
||||
dat.Dat.Merge = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
datAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in emulator
|
||||
else if (reader.Section?.ToLowerInvariant() == "emulator")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Emulator ??= new Emulator();
|
||||
|
||||
switch (reader.KeyValuePair?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "refname":
|
||||
dat.Emulator.RefName = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "version":
|
||||
dat.Emulator.Version = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
emulatorAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in games
|
||||
else if (reader.Section?.ToLowerInvariant() == "games")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Games ??= new Games();
|
||||
|
||||
// If the line doesn't contain the delimiter
|
||||
if (!(reader.CurrentLine?.Contains('¬') ?? false))
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
gamesAdditional.Add(reader.CurrentLine);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, separate out the line
|
||||
string[] splitLine = reader.CurrentLine.Split('¬');
|
||||
var rom = new Rom
|
||||
{
|
||||
// EMPTY = splitLine[0]
|
||||
ParentName = splitLine[1],
|
||||
ParentDescription = splitLine[2],
|
||||
GameName = splitLine[3],
|
||||
GameDescription = splitLine[4],
|
||||
RomName = splitLine[5],
|
||||
RomCRC = splitLine[6],
|
||||
RomSize = splitLine[7],
|
||||
RomOf = splitLine[8],
|
||||
MergeName = splitLine[9],
|
||||
// EMPTY = splitLine[10]
|
||||
};
|
||||
|
||||
if (splitLine.Length > 11)
|
||||
rom.ADDITIONAL_ELEMENTS = splitLine.Skip(11).ToArray();
|
||||
|
||||
roms.Add(rom);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.ADDITIONAL_ELEMENTS = additional.Where(s => s != null).ToArray();
|
||||
if (dat.Credits != null)
|
||||
dat.Credits.ADDITIONAL_ELEMENTS = creditsAdditional.Where(s => s != null).ToArray();
|
||||
if (dat.Dat != null)
|
||||
dat.Dat.ADDITIONAL_ELEMENTS = datAdditional.Where(s => s != null).ToArray();
|
||||
if (dat.Emulator != null)
|
||||
dat.Emulator.ADDITIONAL_ELEMENTS = emulatorAdditional.Where(s => s != null).ToArray();
|
||||
if (dat.Games != null)
|
||||
{
|
||||
dat.Games.Rom = roms.ToArray();
|
||||
dat.Games.ADDITIONAL_ELEMENTS = gamesAdditional.Where(s => s != null).Select(s => s).ToArray();
|
||||
}
|
||||
return dat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.SGA;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.SGA.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class SGA :
|
||||
IByteDeserializer<Models.SGA.File>,
|
||||
IFileDeserializer<Models.SGA.File>
|
||||
IFileDeserializer<Models.SGA.File>,
|
||||
IStreamDeserializer<Models.SGA.File>
|
||||
{
|
||||
#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.SGA.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +53,734 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.SGA.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.SGA.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.SGA.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new SGA();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.SGA.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new SGA to fill
|
||||
var file = new Models.SGA.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the SGA header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory
|
||||
|
||||
// Try to parse the directory
|
||||
var directory = ParseDirectory(data, header.MajorVersion);
|
||||
if (directory == null)
|
||||
return null;
|
||||
|
||||
// Set the SGA directory
|
||||
file.Directory = directory;
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
byte[]? signatureBytes = data.ReadBytes(8);
|
||||
if (signatureBytes == null)
|
||||
return null;
|
||||
|
||||
string signature = Encoding.ASCII.GetString(signatureBytes);
|
||||
if (signature != SignatureString)
|
||||
return null;
|
||||
|
||||
ushort majorVersion = data.ReadUInt16();
|
||||
ushort minorVersion = data.ReadUInt16();
|
||||
if (minorVersion != 0)
|
||||
return null;
|
||||
|
||||
switch (majorVersion)
|
||||
{
|
||||
// Versions 4 and 5 share the same header
|
||||
case 4:
|
||||
case 5:
|
||||
Header4 header4 = new Header4();
|
||||
|
||||
header4.Signature = signature;
|
||||
header4.MajorVersion = majorVersion;
|
||||
header4.MinorVersion = minorVersion;
|
||||
header4.FileMD5 = data.ReadBytes(0x10);
|
||||
byte[]? header4Name = data.ReadBytes(count: 128);
|
||||
if (header4Name != null)
|
||||
header4.Name = Encoding.Unicode.GetString(header4Name).TrimEnd('\0');
|
||||
header4.HeaderMD5 = data.ReadBytes(0x10);
|
||||
header4.HeaderLength = data.ReadUInt32();
|
||||
header4.FileDataOffset = data.ReadUInt32();
|
||||
header4.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return header4;
|
||||
|
||||
// Versions 6 and 7 share the same header
|
||||
case 6:
|
||||
case 7:
|
||||
Header6 header6 = new Header6();
|
||||
|
||||
header6.Signature = signature;
|
||||
header6.MajorVersion = majorVersion;
|
||||
header6.MinorVersion = minorVersion;
|
||||
byte[]? header6Name = data.ReadBytes(count: 128);
|
||||
if (header6Name != null)
|
||||
header6.Name = Encoding.Unicode.GetString(header6Name).TrimEnd('\0');
|
||||
header6.HeaderLength = data.ReadUInt32();
|
||||
header6.FileDataOffset = data.ReadUInt32();
|
||||
header6.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return header6;
|
||||
|
||||
// No other major versions are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA directory on success, null on error</returns>
|
||||
private static Models.SGA.Directory? ParseDirectory(Stream data, ushort majorVersion)
|
||||
{
|
||||
#region Directory
|
||||
|
||||
// Create the appropriate type of directory
|
||||
Models.SGA.Directory directory;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: directory = new Directory4(); break;
|
||||
case 5: directory = new Directory5(); break;
|
||||
case 6: directory = new Directory6(); break;
|
||||
case 7: directory = new Directory7(); break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
#region Directory Header
|
||||
|
||||
// Try to parse the directory header
|
||||
var directoryHeader = ParseDirectoryHeader(data, majorVersion);
|
||||
if (directoryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the directory header
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.DirectoryHeader = directoryHeader as DirectoryHeader4; break;
|
||||
case 5: (directory as Directory5)!.DirectoryHeader = directoryHeader as DirectoryHeader5; break;
|
||||
case 6: (directory as Directory6)!.DirectoryHeader = directoryHeader as DirectoryHeader5; break;
|
||||
case 7: (directory as Directory7)!.DirectoryHeader = directoryHeader as DirectoryHeader7; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sections
|
||||
|
||||
// Get the sections offset
|
||||
long sectionOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sectionOffset = (directoryHeader as DirectoryHeader4)!.SectionOffset; break;
|
||||
case 5:
|
||||
case 6: sectionOffset = (directoryHeader as DirectoryHeader5)!.SectionOffset; break;
|
||||
case 7: sectionOffset = (directoryHeader as DirectoryHeader7)!.SectionOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the sections offset based on the directory
|
||||
sectionOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (sectionOffset < 0 || sectionOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the sections
|
||||
data.Seek(sectionOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the section count
|
||||
uint sectionCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sectionCount = (directoryHeader as DirectoryHeader4)!.SectionCount; break;
|
||||
case 5:
|
||||
case 6: sectionCount = (directoryHeader as DirectoryHeader5)!.SectionCount; break;
|
||||
case 7: sectionCount = (directoryHeader as DirectoryHeader7)!.SectionCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Create the sections array
|
||||
object[] sections;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sections = new Section4[sectionCount]; break;
|
||||
case 5:
|
||||
case 6:
|
||||
case 7: sections = new Section5[sectionCount]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Try to parse the sections
|
||||
for (int i = 0; i < sections.Length; i++)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sections[i] = ParseSection4(data); break;
|
||||
case 5:
|
||||
case 6:
|
||||
case 7: sections[i] = ParseSection5(data); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the sections
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Sections = sections as Section4[]; break;
|
||||
case 5: (directory as Directory5)!.Sections = sections as Section5[]; break;
|
||||
case 6: (directory as Directory6)!.Sections = sections as Section5[]; break;
|
||||
case 7: (directory as Directory7)!.Sections = sections as Section5[]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Folders
|
||||
|
||||
// Get the folders offset
|
||||
long folderOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folderOffset = (directoryHeader as DirectoryHeader4)!.FolderOffset; break;
|
||||
case 5: folderOffset = (directoryHeader as DirectoryHeader5)!.FolderOffset; break;
|
||||
case 6: folderOffset = (directoryHeader as DirectoryHeader5)!.FolderOffset; break;
|
||||
case 7: folderOffset = (directoryHeader as DirectoryHeader7)!.FolderOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the folders offset based on the directory
|
||||
folderOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (folderOffset < 0 || folderOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the folders
|
||||
data.Seek(folderOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the folder count
|
||||
uint folderCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folderCount = (directoryHeader as DirectoryHeader4)!.FolderCount; break;
|
||||
case 5: folderCount = (directoryHeader as DirectoryHeader5)!.FolderCount; break;
|
||||
case 6: folderCount = (directoryHeader as DirectoryHeader5)!.FolderCount; break;
|
||||
case 7: folderCount = (directoryHeader as DirectoryHeader7)!.FolderCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Create the folders array
|
||||
object[] folders;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folders = new Folder4[folderCount]; break;
|
||||
case 5: folders = new Folder5[folderCount]; break;
|
||||
case 6: folders = new Folder5[folderCount]; break;
|
||||
case 7: folders = new Folder5[folderCount]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Try to parse the folders
|
||||
for (int i = 0; i < folders.Length; i++)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folders[i] = ParseFolder4(data); break;
|
||||
case 5: folders[i] = ParseFolder5(data); break;
|
||||
case 6: folders[i] = ParseFolder5(data); break;
|
||||
case 7: folders[i] = ParseFolder5(data); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the folders
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Folders = folders as Folder4[]; break;
|
||||
case 5: (directory as Directory5)!.Folders = folders as Folder5[]; break;
|
||||
case 6: (directory as Directory6)!.Folders = folders as Folder5[]; break;
|
||||
case 7: (directory as Directory7)!.Folders = folders as Folder5[]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Files
|
||||
|
||||
// Get the files offset
|
||||
long fileOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: fileOffset = (directoryHeader as DirectoryHeader4)!.FileOffset; break;
|
||||
case 5: fileOffset = (directoryHeader as DirectoryHeader5)!.FileOffset; break;
|
||||
case 6: fileOffset = (directoryHeader as DirectoryHeader5)!.FileOffset; break;
|
||||
case 7: fileOffset = (directoryHeader as DirectoryHeader7)!.FileOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the files offset based on the directory
|
||||
fileOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (fileOffset < 0 || fileOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the files
|
||||
data.Seek(fileOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the file count
|
||||
uint fileCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: fileCount = (directoryHeader as DirectoryHeader4)!.FileCount; break;
|
||||
case 5: fileCount = (directoryHeader as DirectoryHeader5)!.FileCount; break;
|
||||
case 6: fileCount = (directoryHeader as DirectoryHeader5)!.FileCount; break;
|
||||
case 7: fileCount = (directoryHeader as DirectoryHeader7)!.FileCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Create the files array
|
||||
object[] files;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: files = new File4[fileCount]; break;
|
||||
case 5: files = new File4[fileCount]; break;
|
||||
case 6: files = new File6[fileCount]; break;
|
||||
case 7: files = new File7[fileCount]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Try to parse the files
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: files[i] = ParseFile4(data); break;
|
||||
case 5: files[i] = ParseFile4(data); break;
|
||||
case 6: files[i] = ParseFile6(data); break;
|
||||
case 7: files[i] = ParseFile7(data); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the files
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Files = files as File4[]; break;
|
||||
case 5: (directory as Directory5)!.Files = files as File4[]; break;
|
||||
case 6: (directory as Directory6)!.Files = files as File6[]; break;
|
||||
case 7: (directory as Directory7)!.Files = files as File7[]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Table
|
||||
|
||||
// Get the string table offset
|
||||
long stringTableOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: stringTableOffset = (directoryHeader as DirectoryHeader4)!.StringTableOffset; break;
|
||||
case 5: stringTableOffset = (directoryHeader as DirectoryHeader5)!.StringTableOffset; break;
|
||||
case 6: stringTableOffset = (directoryHeader as DirectoryHeader5)!.StringTableOffset; break;
|
||||
case 7: stringTableOffset = (directoryHeader as DirectoryHeader7)!.StringTableOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the string table offset based on the directory
|
||||
stringTableOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (stringTableOffset < 0 || stringTableOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the string table
|
||||
data.Seek(stringTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the string table count
|
||||
uint stringCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: stringCount = (directoryHeader as DirectoryHeader4)!.StringTableCount; break;
|
||||
case 5: stringCount = (directoryHeader as DirectoryHeader5)!.StringTableCount; break;
|
||||
case 6: stringCount = (directoryHeader as DirectoryHeader5)!.StringTableCount; break;
|
||||
case 7: stringCount = (directoryHeader as DirectoryHeader7)!.StringTableCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// TODO: Are these strings actually indexed by number and not position?
|
||||
// TODO: If indexed by position, I think it needs to be adjusted by start of table
|
||||
|
||||
// Create the strings dictionary
|
||||
Dictionary<long, string?> strings = new Dictionary<long, string?>((int)stringCount);
|
||||
|
||||
// Get the current position to adjust the offsets
|
||||
long stringTableStart = data.Position;
|
||||
|
||||
// Try to parse the strings
|
||||
for (int i = 0; i < stringCount; i++)
|
||||
{
|
||||
long currentPosition = data.Position - stringTableStart;
|
||||
strings[currentPosition] = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Assign the files
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.StringTable = strings; break;
|
||||
case 5: (directory as Directory5)!.StringTable = strings; break;
|
||||
case 6: (directory as Directory6)!.StringTable = strings; break;
|
||||
case 7: (directory as Directory7)!.StringTable = strings; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Loop through all folders to assign names
|
||||
for (int i = 0; i < folderCount; i++)
|
||||
{
|
||||
uint nameOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: nameOffset = (directory as Directory4)!.Folders![i]!.NameOffset; break;
|
||||
case 5: nameOffset = (directory as Directory5)!.Folders![i]!.NameOffset; break;
|
||||
case 6: nameOffset = (directory as Directory6)!.Folders![i]!.NameOffset; break;
|
||||
case 7: nameOffset = (directory as Directory7)!.Folders![i]!.NameOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
case 5: (directory as Directory5)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
case 6: (directory as Directory6)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
case 7: (directory as Directory7)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through all files to assign names
|
||||
for (int i = 0; i < fileCount; i++)
|
||||
{
|
||||
uint nameOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: nameOffset = (directory as Directory4)!.Files![i]!.NameOffset; break;
|
||||
case 5: nameOffset = (directory as Directory5)!.Files![i]!.NameOffset; break;
|
||||
case 6: nameOffset = (directory as Directory6)!.Files![i]!.NameOffset; break;
|
||||
case 7: nameOffset = (directory as Directory7)!.Files![i]!.NameOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
case 5: (directory as Directory5)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
case 6: (directory as Directory6)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
case 7: (directory as Directory7)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA directory header on success, null on error</returns>
|
||||
private static object? ParseDirectoryHeader(Stream data, ushort majorVersion)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: return ParseDirectory4Header(data);
|
||||
case 5: return ParseDirectory5Header(data);
|
||||
case 6: return ParseDirectory5Header(data);
|
||||
case 7: return ParseDirectory7Header(data);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA directory header version 4 on success, null on error</returns>
|
||||
private static DirectoryHeader4 ParseDirectory4Header(Stream data)
|
||||
{
|
||||
DirectoryHeader4 directoryHeader4 = new DirectoryHeader4();
|
||||
|
||||
directoryHeader4.SectionOffset = data.ReadUInt32();
|
||||
directoryHeader4.SectionCount = data.ReadUInt16();
|
||||
directoryHeader4.FolderOffset = data.ReadUInt32();
|
||||
directoryHeader4.FolderCount = data.ReadUInt16();
|
||||
directoryHeader4.FileOffset = data.ReadUInt32();
|
||||
directoryHeader4.FileCount = data.ReadUInt16();
|
||||
directoryHeader4.StringTableOffset = data.ReadUInt32();
|
||||
directoryHeader4.StringTableCount = data.ReadUInt16();
|
||||
|
||||
return directoryHeader4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header version 5
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA directory header version 5 on success, null on error</returns>
|
||||
private static DirectoryHeader5 ParseDirectory5Header(Stream data)
|
||||
{
|
||||
DirectoryHeader5 directoryHeader5 = new DirectoryHeader5();
|
||||
|
||||
directoryHeader5.SectionOffset = data.ReadUInt32();
|
||||
directoryHeader5.SectionCount = data.ReadUInt32();
|
||||
directoryHeader5.FolderOffset = data.ReadUInt32();
|
||||
directoryHeader5.FolderCount = data.ReadUInt32();
|
||||
directoryHeader5.FileOffset = data.ReadUInt32();
|
||||
directoryHeader5.FileCount = data.ReadUInt32();
|
||||
directoryHeader5.StringTableOffset = data.ReadUInt32();
|
||||
directoryHeader5.StringTableCount = data.ReadUInt32();
|
||||
|
||||
return directoryHeader5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header version 7
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA directory header version 7 on success, null on error</returns>
|
||||
private static DirectoryHeader7 ParseDirectory7Header(Stream data)
|
||||
{
|
||||
DirectoryHeader7 directoryHeader7 = new DirectoryHeader7();
|
||||
|
||||
directoryHeader7.SectionOffset = data.ReadUInt32();
|
||||
directoryHeader7.SectionCount = data.ReadUInt32();
|
||||
directoryHeader7.FolderOffset = data.ReadUInt32();
|
||||
directoryHeader7.FolderCount = data.ReadUInt32();
|
||||
directoryHeader7.FileOffset = data.ReadUInt32();
|
||||
directoryHeader7.FileCount = data.ReadUInt32();
|
||||
directoryHeader7.StringTableOffset = data.ReadUInt32();
|
||||
directoryHeader7.StringTableCount = data.ReadUInt32();
|
||||
directoryHeader7.HashTableOffset = data.ReadUInt32();
|
||||
directoryHeader7.BlockSize = data.ReadUInt32();
|
||||
|
||||
return directoryHeader7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA section version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA section version 4 on success, null on error</returns>
|
||||
private static Section4 ParseSection4(Stream data)
|
||||
{
|
||||
Section4 section4 = new Section4();
|
||||
|
||||
byte[]? section4Alias = data.ReadBytes(64);
|
||||
if (section4Alias != null)
|
||||
section4.Alias = Encoding.ASCII.GetString(section4Alias).TrimEnd('\0');
|
||||
byte[]? section4Name = data.ReadBytes(64);
|
||||
if (section4Name != null)
|
||||
section4.Name = Encoding.ASCII.GetString(section4Name).TrimEnd('\0');
|
||||
section4.FolderStartIndex = data.ReadUInt16();
|
||||
section4.FolderEndIndex = data.ReadUInt16();
|
||||
section4.FileStartIndex = data.ReadUInt16();
|
||||
section4.FileEndIndex = data.ReadUInt16();
|
||||
section4.FolderRootIndex = data.ReadUInt16();
|
||||
|
||||
return section4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA section version 5
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA section version 5 on success, null on error</returns>
|
||||
private static Section5 ParseSection5(Stream data)
|
||||
{
|
||||
Section5 section5 = new Section5();
|
||||
|
||||
byte[]? section5Alias = data.ReadBytes(64);
|
||||
if (section5Alias != null)
|
||||
section5.Alias = Encoding.ASCII.GetString(section5Alias).TrimEnd('\0');
|
||||
byte[]? section5Name = data.ReadBytes(64);
|
||||
if (section5Name != null)
|
||||
section5.Name = Encoding.ASCII.GetString(section5Name).TrimEnd('\0');
|
||||
section5.FolderStartIndex = data.ReadUInt32();
|
||||
section5.FolderEndIndex = data.ReadUInt32();
|
||||
section5.FileStartIndex = data.ReadUInt32();
|
||||
section5.FileEndIndex = data.ReadUInt32();
|
||||
section5.FolderRootIndex = data.ReadUInt32();
|
||||
|
||||
return section5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA folder version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA folder version 4 on success, null on error</returns>
|
||||
private static Folder4 ParseFolder4(Stream data)
|
||||
{
|
||||
Folder4 folder4 = new Folder4();
|
||||
|
||||
folder4.NameOffset = data.ReadUInt32();
|
||||
folder4.Name = null; // Read from string table
|
||||
folder4.FolderStartIndex = data.ReadUInt16();
|
||||
folder4.FolderEndIndex = data.ReadUInt16();
|
||||
folder4.FileStartIndex = data.ReadUInt16();
|
||||
folder4.FileEndIndex = data.ReadUInt16();
|
||||
|
||||
return folder4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA folder version 5
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA folder version 5 on success, null on error</returns>
|
||||
private static Folder5 ParseFolder5(Stream data)
|
||||
{
|
||||
Folder5 folder5 = new Folder5();
|
||||
|
||||
folder5.NameOffset = data.ReadUInt32();
|
||||
folder5.Name = null; // Read from string table
|
||||
folder5.FolderStartIndex = data.ReadUInt32();
|
||||
folder5.FolderEndIndex = data.ReadUInt32();
|
||||
folder5.FileStartIndex = data.ReadUInt32();
|
||||
folder5.FileEndIndex = data.ReadUInt32();
|
||||
|
||||
return folder5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA file version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA file version 4 on success, null on error</returns>
|
||||
private static File4 ParseFile4(Stream data)
|
||||
{
|
||||
File4 file4 = new File4();
|
||||
|
||||
file4.NameOffset = data.ReadUInt32();
|
||||
file4.Name = null; // Read from string table
|
||||
file4.Offset = data.ReadUInt32();
|
||||
file4.SizeOnDisk = data.ReadUInt32();
|
||||
file4.Size = data.ReadUInt32();
|
||||
file4.TimeModified = data.ReadUInt32();
|
||||
file4.Dummy0 = data.ReadByteValue();
|
||||
file4.Type = data.ReadByteValue();
|
||||
|
||||
return file4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA file version 6
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA file version 6 on success, null on error</returns>
|
||||
private static File6 ParseFile6(Stream data)
|
||||
{
|
||||
File6 file6 = new File6();
|
||||
|
||||
file6.NameOffset = data.ReadUInt32();
|
||||
file6.Name = null; // Read from string table
|
||||
file6.Offset = data.ReadUInt32();
|
||||
file6.SizeOnDisk = data.ReadUInt32();
|
||||
file6.Size = data.ReadUInt32();
|
||||
file6.TimeModified = data.ReadUInt32();
|
||||
file6.Dummy0 = data.ReadByteValue();
|
||||
file6.Type = data.ReadByteValue();
|
||||
file6.CRC32 = data.ReadUInt32();
|
||||
|
||||
return file6;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA file version 7
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA file version 7 on success, null on error</returns>
|
||||
private static File7 ParseFile7(Stream data)
|
||||
{
|
||||
File7 file7 = new File7();
|
||||
|
||||
file7.NameOffset = data.ReadUInt32();
|
||||
file7.Name = null; // Read from string table
|
||||
file7.Offset = data.ReadUInt32();
|
||||
file7.SizeOnDisk = data.ReadUInt32();
|
||||
file7.Size = data.ReadUInt32();
|
||||
file7.TimeModified = data.ReadUInt32();
|
||||
file7.Dummy0 = data.ReadByteValue();
|
||||
file7.Type = data.ReadByteValue();
|
||||
file7.CRC32 = data.ReadUInt32();
|
||||
file7.HashOffset = data.ReadUInt32();
|
||||
|
||||
return file7;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,27 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.SeparatedValue;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class SeparatedValue : IFileDeserializer<Models.SeparatedValue.MetadataFile>
|
||||
public class SeparatedValue :
|
||||
IFileDeserializer<MetadataFile>,
|
||||
IStreamDeserializer<MetadataFile>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.SeparatedValue.MetadataFile? DeserializeFile(string? path, char delim = ',')
|
||||
public static MetadataFile? DeserializeFile(string? path, char delim = ',')
|
||||
{
|
||||
var deserializer = new SeparatedValue();
|
||||
return deserializer.Deserialize(path, delim);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.SeparatedValue.MetadataFile? Deserialize(string? path)
|
||||
public MetadataFile? Deserialize(string? path)
|
||||
=> Deserialize(path, ',');
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.SeparatedValue.MetadataFile? Deserialize(string? path, char delim)
|
||||
public MetadataFile? Deserialize(string? path, char delim)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.SeparatedValue.DeserializeStream(stream, delim);
|
||||
return DeserializeStream(stream, delim);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data, char delim = ',')
|
||||
{
|
||||
var deserializer = new SeparatedValue();
|
||||
return deserializer.Deserialize(data, delim);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, ',');
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
public MetadataFile? Deserialize(Stream? data, char delim)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new SeparatedValueReader(data, Encoding.UTF8)
|
||||
{
|
||||
Header = true,
|
||||
Separator = delim,
|
||||
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<Row>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine() || reader.Line == null)
|
||||
break;
|
||||
|
||||
// Parse the line into a row
|
||||
Row? row = null;
|
||||
if (reader.Line.Count < Serialization.SeparatedValue.HeaderWithExtendedHashesCount)
|
||||
{
|
||||
row = new Row
|
||||
{
|
||||
FileName = reader.Line[0],
|
||||
InternalName = reader.Line[1],
|
||||
Description = reader.Line[2],
|
||||
GameName = reader.Line[3],
|
||||
GameDescription = reader.Line[4],
|
||||
Type = reader.Line[5],
|
||||
RomName = reader.Line[6],
|
||||
DiskName = reader.Line[7],
|
||||
Size = reader.Line[8],
|
||||
CRC = reader.Line[9],
|
||||
MD5 = reader.Line[10],
|
||||
SHA1 = reader.Line[11],
|
||||
SHA256 = reader.Line[12],
|
||||
Status = reader.Line[13],
|
||||
};
|
||||
|
||||
// If we have additional fields
|
||||
if (reader.Line.Count > Serialization.SeparatedValue.HeaderWithoutExtendedHashesCount)
|
||||
row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.SeparatedValue.HeaderWithoutExtendedHashesCount).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
row = new Row
|
||||
{
|
||||
FileName = reader.Line[0],
|
||||
InternalName = reader.Line[1],
|
||||
Description = reader.Line[2],
|
||||
GameName = reader.Line[3],
|
||||
GameDescription = reader.Line[4],
|
||||
Type = reader.Line[5],
|
||||
RomName = reader.Line[6],
|
||||
DiskName = reader.Line[7],
|
||||
Size = reader.Line[8],
|
||||
CRC = reader.Line[9],
|
||||
MD5 = reader.Line[10],
|
||||
SHA1 = reader.Line[11],
|
||||
SHA256 = reader.Line[12],
|
||||
SHA384 = reader.Line[13],
|
||||
SHA512 = reader.Line[14],
|
||||
SpamSum = reader.Line[15],
|
||||
Status = reader.Line[16],
|
||||
};
|
||||
|
||||
// If we have additional fields
|
||||
if (reader.Line.Count > Serialization.SeparatedValue.HeaderWithExtendedHashesCount)
|
||||
row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.SeparatedValue.HeaderWithExtendedHashesCount).ToArray();
|
||||
}
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
// Assign the rows to the Dat and return
|
||||
dat.Row = rows.ToArray();
|
||||
return dat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class SoftwareList : XmlFile<Models.SoftwareList.SoftwareList>
|
||||
public class SoftwareList :
|
||||
XmlFile<Models.SoftwareList.SoftwareList>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
|
||||
/// <inheritdoc cref="Interfaces.IFileDeserializer.Deserialize(string?)"/>
|
||||
public static Models.SoftwareList.SoftwareList? DeserializeFile(string? path)
|
||||
{
|
||||
var deserializer = new SoftwareList();
|
||||
@@ -12,5 +13,16 @@ namespace SabreTools.Serialization.Deserializers
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="Interfaces.IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.SoftwareList.SoftwareList? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new SoftwareList();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.VBSP;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.VBSP.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class VBSP :
|
||||
IByteDeserializer<Models.VBSP.File>,
|
||||
IFileDeserializer<Models.VBSP.File>
|
||||
IFileDeserializer<Models.VBSP.File>,
|
||||
IStreamDeserializer<Models.VBSP.File>
|
||||
{
|
||||
#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.VBSP.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +52,118 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.VBSP.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.VBSP.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.VBSP.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new VBSP();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.VBSP.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life 2 Level to fill
|
||||
var file = new Models.VBSP.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life 2 Level header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life 2 Level header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.Version = data.ReadInt32();
|
||||
if ((header.Version < 19 || header.Version > 22) && header.Version != 0x00040014)
|
||||
return null;
|
||||
|
||||
header.Lumps = new Lump[HL_VBSP_LUMP_COUNT];
|
||||
for (int i = 0; i < HL_VBSP_LUMP_COUNT; i++)
|
||||
{
|
||||
header.Lumps[i] = ParseLump(data, header.Version);
|
||||
}
|
||||
|
||||
header.MapRevision = data.ReadInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life 2 Level lump
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="version">VBSP version</param>
|
||||
/// <returns>Filled Half-Life 2 Level lump on success, null on error</returns>
|
||||
private static Lump ParseLump(Stream data, int version)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Lump lump = new Lump();
|
||||
|
||||
lump.Offset = data.ReadUInt32();
|
||||
lump.Length = data.ReadUInt32();
|
||||
lump.Version = data.ReadUInt32();
|
||||
lump.FourCC = new char[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
lump.FourCC[i] = (char)data.ReadByte();
|
||||
}
|
||||
|
||||
// This block was commented out because test VBSPs with header
|
||||
// version 21 had the values in the "right" order already and
|
||||
// were causing decompression issues
|
||||
|
||||
//if (version >= 21 && version != 0x00040014)
|
||||
//{
|
||||
// uint temp = lump.Version;
|
||||
// lump.Version = lump.Offset;
|
||||
// lump.Offset = lump.Length;
|
||||
// lump.Length = temp;
|
||||
//}
|
||||
|
||||
return lump;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.VPK;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.VPK.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class VPK :
|
||||
IByteDeserializer<Models.VPK.File>,
|
||||
IFileDeserializer<Models.VPK.File>
|
||||
IFileDeserializer<Models.VPK.File>,
|
||||
IStreamDeserializer<Models.VPK.File>
|
||||
{
|
||||
#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.VPK.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +53,291 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.VPK.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.VPK.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.VPK.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new VPK();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.VPK.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Valve Package to fill
|
||||
var file = new Models.VPK.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
// The original version had no signature.
|
||||
var header = ParseHeader(data);
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended Header
|
||||
|
||||
if (header?.Version == 2)
|
||||
{
|
||||
// Try to parse the extended header
|
||||
var extendedHeader = ParseExtendedHeader(data);
|
||||
if (extendedHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the package extended header
|
||||
file.ExtendedHeader = extendedHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Items
|
||||
|
||||
// Create the directory items tree
|
||||
var directoryItems = ParseDirectoryItemTree(data);
|
||||
|
||||
// Set the directory items
|
||||
file.DirectoryItems = directoryItems;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive Hashes
|
||||
|
||||
if (header?.Version == 2 && file.ExtendedHeader != null && file.ExtendedHeader.ArchiveHashLength > 0)
|
||||
{
|
||||
// Create the archive hashes list
|
||||
var archiveHashes = new List<ArchiveHash>();
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
// Try to parse the directory items
|
||||
while (data.Position < initialOffset + file.ExtendedHeader.ArchiveHashLength)
|
||||
{
|
||||
var archiveHash = ParseArchiveHash(data);
|
||||
archiveHashes.Add(archiveHash);
|
||||
}
|
||||
|
||||
file.ArchiveHashes = archiveHashes.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.Signature = data.ReadUInt32();
|
||||
if (header.Signature != SignatureUInt32)
|
||||
return null;
|
||||
|
||||
header.Version = data.ReadUInt32();
|
||||
if (header.Version > 2)
|
||||
return null;
|
||||
|
||||
header.DirectoryLength = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package extended header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package extended header on success, null on error</returns>
|
||||
private static ExtendedHeader ParseExtendedHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ExtendedHeader extendedHeader = new ExtendedHeader();
|
||||
|
||||
extendedHeader.Dummy0 = data.ReadUInt32();
|
||||
extendedHeader.ArchiveHashLength = data.ReadUInt32();
|
||||
extendedHeader.ExtraLength = data.ReadUInt32();
|
||||
extendedHeader.Dummy1 = data.ReadUInt32();
|
||||
|
||||
return extendedHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package archive hash
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package archive hash on success, null on error</returns>
|
||||
private static ArchiveHash ParseArchiveHash(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ArchiveHash archiveHash = new ArchiveHash();
|
||||
|
||||
archiveHash.ArchiveIndex = data.ReadUInt32();
|
||||
archiveHash.ArchiveOffset = data.ReadUInt32();
|
||||
archiveHash.Length = data.ReadUInt32();
|
||||
archiveHash.Hash = data.ReadBytes(0x10);
|
||||
|
||||
return archiveHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package directory item tree
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package directory item tree on success, null on error</returns>
|
||||
private static DirectoryItem[] ParseDirectoryItemTree(Stream data)
|
||||
{
|
||||
// Create the directory items list
|
||||
var directoryItems = new List<DirectoryItem>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the extension
|
||||
string? extensionString = data.ReadString(Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(extensionString))
|
||||
break;
|
||||
|
||||
// Sanitize the extension
|
||||
for (int i = 0; i < 0x20; i++)
|
||||
{
|
||||
extensionString = extensionString!.Replace($"{(char)i}", string.Empty);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the path
|
||||
string? pathString = data.ReadString(Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(pathString))
|
||||
break;
|
||||
|
||||
// Sanitize the path
|
||||
for (int i = 0; i < 0x20; i++)
|
||||
{
|
||||
pathString = pathString!.Replace($"{(char)i}", string.Empty);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the name
|
||||
string? nameString = data.ReadString(Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(nameString))
|
||||
break;
|
||||
|
||||
// Sanitize the name
|
||||
for (int i = 0; i < 0x20; i++)
|
||||
{
|
||||
nameString = nameString!.Replace($"{(char)i}", string.Empty);
|
||||
}
|
||||
|
||||
// Get the directory item
|
||||
var directoryItem = ParseDirectoryItem(data, extensionString!, pathString!, nameString!);
|
||||
|
||||
// Add the directory item
|
||||
directoryItems.Add(directoryItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directoryItems.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package directory item
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package directory item on success, null on error</returns>
|
||||
private static DirectoryItem ParseDirectoryItem(Stream data, string extension, string path, string name)
|
||||
{
|
||||
DirectoryItem directoryItem = new DirectoryItem();
|
||||
|
||||
directoryItem.Extension = extension;
|
||||
directoryItem.Path = path;
|
||||
directoryItem.Name = name;
|
||||
|
||||
// Get the directory entry
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
|
||||
// Set the directory entry
|
||||
directoryItem.DirectoryEntry = directoryEntry;
|
||||
|
||||
// Get the preload data pointer
|
||||
long preloadDataPointer = -1; int preloadDataLength = -1;
|
||||
if (directoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE && directoryEntry.EntryLength > 0)
|
||||
{
|
||||
preloadDataPointer = directoryEntry.EntryOffset;
|
||||
preloadDataLength = (int)directoryEntry.EntryLength;
|
||||
}
|
||||
else if (directoryEntry.PreloadBytes > 0)
|
||||
{
|
||||
preloadDataPointer = data.Position;
|
||||
preloadDataLength = directoryEntry.PreloadBytes;
|
||||
}
|
||||
|
||||
// If we had a valid preload data pointer
|
||||
byte[]? preloadData = null;
|
||||
if (preloadDataPointer >= 0 && preloadDataLength > 0)
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Seek to the preload data offset
|
||||
data.Seek(preloadDataPointer, SeekOrigin.Begin);
|
||||
|
||||
// Read the preload data
|
||||
preloadData = data.ReadBytes(preloadDataLength);
|
||||
|
||||
// Seek back to the original offset
|
||||
data.Seek(initialOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Set the preload data
|
||||
directoryItem.PreloadData = preloadData;
|
||||
|
||||
return directoryItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.CRC = data.ReadUInt32();
|
||||
directoryEntry.PreloadBytes = data.ReadUInt16();
|
||||
directoryEntry.ArchiveIndex = data.ReadUInt16();
|
||||
directoryEntry.EntryOffset = data.ReadUInt32();
|
||||
directoryEntry.EntryLength = data.ReadUInt32();
|
||||
directoryEntry.Dummy0 = data.ReadUInt16();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.WAD;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.WAD.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class WAD :
|
||||
IByteDeserializer<Models.WAD.File>,
|
||||
IFileDeserializer<Models.WAD.File>
|
||||
IFileDeserializer<Models.WAD.File>,
|
||||
IStreamDeserializer<Models.WAD.File>
|
||||
{
|
||||
#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.WAD.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +52,251 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.WAD.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.WAD.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.WAD.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new WAD();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.WAD.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life Texture Package to fill
|
||||
var file = new Models.WAD.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lumps
|
||||
|
||||
// Get the lump offset
|
||||
uint lumpOffset = header.LumpOffset;
|
||||
if (lumpOffset < 0 || lumpOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the lump offset
|
||||
data.Seek(lumpOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the lump array
|
||||
file.Lumps = new Lump[header.LumpCount];
|
||||
for (int i = 0; i < header.LumpCount; i++)
|
||||
{
|
||||
var lump = ParseLump(data);
|
||||
file.Lumps[i] = lump;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lump Infos
|
||||
|
||||
// Create the lump info array
|
||||
file.LumpInfos = new LumpInfo?[header.LumpCount];
|
||||
for (int i = 0; i < header.LumpCount; i++)
|
||||
{
|
||||
var lump = file.Lumps[i];
|
||||
if (lump == null)
|
||||
{
|
||||
file.LumpInfos[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lump.Compression != 0)
|
||||
{
|
||||
file.LumpInfos[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the lump info offset
|
||||
uint lumpInfoOffset = lump.Offset;
|
||||
if (lumpInfoOffset < 0 || lumpInfoOffset >= data.Length)
|
||||
{
|
||||
file.LumpInfos[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seek to the lump info offset
|
||||
data.Seek(lumpInfoOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the lump info -- TODO: Do we ever set the mipmap level?
|
||||
var lumpInfo = ParseLumpInfo(data, lump.Type);
|
||||
file.LumpInfos[i] = lumpInfo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Texture Package header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Texture Package header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.LumpCount = data.ReadUInt32();
|
||||
header.LumpOffset = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Texture Package lump
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Texture Package lump on success, null on error</returns>
|
||||
private static Lump ParseLump(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Lump lump = new Lump();
|
||||
|
||||
lump.Offset = data.ReadUInt32();
|
||||
lump.DiskLength = data.ReadUInt32();
|
||||
lump.Length = data.ReadUInt32();
|
||||
lump.Type = data.ReadByteValue();
|
||||
lump.Compression = data.ReadByteValue();
|
||||
lump.Padding0 = data.ReadByteValue();
|
||||
lump.Padding1 = data.ReadByteValue();
|
||||
byte[]? name = data.ReadBytes(16);
|
||||
if (name != null)
|
||||
lump.Name = Encoding.ASCII.GetString(name).TrimEnd('\0');
|
||||
|
||||
return lump;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Texture Package lump info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="type">Lump type</param>
|
||||
/// <param name="mipmap">Mipmap level</param>
|
||||
/// <returns>Filled Half-Life Texture Package lump info on success, null on error</returns>
|
||||
private static LumpInfo? ParseLumpInfo(Stream data, byte type, uint mipmap = 0)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
LumpInfo lumpInfo = new LumpInfo();
|
||||
|
||||
// Cache the initial offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Type 0x42 has no name, type 0x43 does. Are these flags?
|
||||
if (type == 0x42)
|
||||
{
|
||||
if (mipmap > 0)
|
||||
return null;
|
||||
|
||||
lumpInfo.Width = data.ReadUInt32();
|
||||
lumpInfo.Height = data.ReadUInt32();
|
||||
lumpInfo.PixelData = data.ReadBytes((int)(lumpInfo.Width * lumpInfo.Height));
|
||||
lumpInfo.PaletteSize = data.ReadUInt16();
|
||||
}
|
||||
else if (type == 0x43)
|
||||
{
|
||||
if (mipmap > 3)
|
||||
return null;
|
||||
|
||||
byte[]? name = data.ReadBytes(16);
|
||||
if (name != null)
|
||||
lumpInfo.Name = Encoding.ASCII.GetString(name);
|
||||
lumpInfo.Width = data.ReadUInt32();
|
||||
lumpInfo.Height = data.ReadUInt32();
|
||||
lumpInfo.PixelOffset = data.ReadUInt32();
|
||||
_ = data.ReadBytes(12); // Unknown data
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
// Seek to the pixel data
|
||||
data.Seek(initialOffset + lumpInfo.PixelOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the pixel data
|
||||
lumpInfo.PixelData = data.ReadBytes((int)(lumpInfo.Width * lumpInfo.Height));
|
||||
|
||||
// Seek back to the offset
|
||||
data.Seek(currentOffset, SeekOrigin.Begin);
|
||||
|
||||
uint pixelSize = lumpInfo.Width * lumpInfo.Height;
|
||||
|
||||
// Mipmap data -- TODO: How do we determine this during initial parsing?
|
||||
switch (mipmap)
|
||||
{
|
||||
case 1: _ = data.ReadBytes((int)pixelSize); break;
|
||||
case 2: _ = data.ReadBytes((int)(pixelSize + (pixelSize / 4))); break;
|
||||
case 3: _ = data.ReadBytes((int)(pixelSize + (pixelSize / 4) + (pixelSize / 16))); break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
_ = data.ReadBytes((int)(pixelSize + (pixelSize / 4) + (pixelSize / 16) + (pixelSize / 64))); // Pixel data
|
||||
lumpInfo.PaletteSize = data.ReadUInt16();
|
||||
lumpInfo.PaletteData = data.ReadBytes((int)lumpInfo.PaletteSize * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Adjust based on mipmap level
|
||||
switch (mipmap)
|
||||
{
|
||||
case 1:
|
||||
lumpInfo.Width /= 2;
|
||||
lumpInfo.Height /= 2;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
lumpInfo.Width /= 4;
|
||||
lumpInfo.Height /= 4;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
lumpInfo.Width /= 8;
|
||||
lumpInfo.Height /= 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return lumpInfo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.XZP;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.XZP.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
{
|
||||
public class XZP :
|
||||
IByteDeserializer<Models.XZP.File>,
|
||||
IFileDeserializer<Models.XZP.File>
|
||||
IFileDeserializer<Models.XZP.File>,
|
||||
IStreamDeserializer<Models.XZP.File>
|
||||
{
|
||||
#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.XZP.DeserializeStream(dataStream);
|
||||
return DeserializeStream(dataStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -47,7 +52,254 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public Models.XZP.File? Deserialize(string? path)
|
||||
{
|
||||
using var stream = PathProcessor.OpenStream(path);
|
||||
return Streams.XZP.DeserializeStream(stream);
|
||||
return DeserializeStream(stream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
|
||||
public static Models.XZP.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new XZP();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.XZP.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new XBox Package File to fill
|
||||
var file = new Models.XZP.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Entries
|
||||
|
||||
// Create the directory entry array
|
||||
file.DirectoryEntries = new DirectoryEntry[header.DirectoryEntryCount];
|
||||
|
||||
// Try to parse the directory entries
|
||||
for (int i = 0; i < header.DirectoryEntryCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.DirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preload Directory Entries
|
||||
|
||||
if (header.PreloadBytes > 0)
|
||||
{
|
||||
// Create the preload directory entry array
|
||||
file.PreloadDirectoryEntries = new DirectoryEntry[header.PreloadDirectoryEntryCount];
|
||||
|
||||
// Try to parse the preload directory entries
|
||||
for (int i = 0; i < header.PreloadDirectoryEntryCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.PreloadDirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preload Directory Mappings
|
||||
|
||||
if (header.PreloadBytes > 0)
|
||||
{
|
||||
// Create the preload directory mapping array
|
||||
file.PreloadDirectoryMappings = new DirectoryMapping[header.PreloadDirectoryEntryCount];
|
||||
|
||||
// Try to parse the preload directory mappings
|
||||
for (int i = 0; i < header.PreloadDirectoryEntryCount; i++)
|
||||
{
|
||||
var directoryMapping = ParseDirectoryMapping(data);
|
||||
file.PreloadDirectoryMappings[i] = directoryMapping;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Items
|
||||
|
||||
if (header.DirectoryItemCount > 0)
|
||||
{
|
||||
// Get the directory item offset
|
||||
uint directoryItemOffset = header.DirectoryItemOffset;
|
||||
if (directoryItemOffset < 0 || directoryItemOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the directory items
|
||||
data.Seek(directoryItemOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the directory item array
|
||||
file.DirectoryItems = new DirectoryItem[header.DirectoryItemCount];
|
||||
|
||||
// Try to parse the directory items
|
||||
for (int i = 0; i < header.DirectoryItemCount; i++)
|
||||
{
|
||||
var directoryItem = ParseDirectoryItem(data);
|
||||
file.DirectoryItems[i] = directoryItem;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Footer
|
||||
|
||||
// Seek to the footer
|
||||
data.Seek(-8, SeekOrigin.End);
|
||||
|
||||
// Try to parse the footer
|
||||
var footer = ParseFooter(data);
|
||||
if (footer == null)
|
||||
return null;
|
||||
|
||||
// Set the package footer
|
||||
file.Footer = footer;
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != HeaderSignatureString)
|
||||
return null;
|
||||
|
||||
header.Version = data.ReadUInt32();
|
||||
if (header.Version != 6)
|
||||
return null;
|
||||
|
||||
header.PreloadDirectoryEntryCount = data.ReadUInt32();
|
||||
header.DirectoryEntryCount = data.ReadUInt32();
|
||||
header.PreloadBytes = data.ReadUInt32();
|
||||
header.HeaderLength = data.ReadUInt32();
|
||||
header.DirectoryItemCount = data.ReadUInt32();
|
||||
header.DirectoryItemOffset = data.ReadUInt32();
|
||||
header.DirectoryItemLength = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.FileNameCRC = data.ReadUInt32();
|
||||
directoryEntry.EntryLength = data.ReadUInt32();
|
||||
directoryEntry.EntryOffset = data.ReadUInt32();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File directory mapping
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File directory mapping on success, null on error</returns>
|
||||
private static DirectoryMapping ParseDirectoryMapping(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryMapping directoryMapping = new DirectoryMapping();
|
||||
|
||||
directoryMapping.PreloadDirectoryEntryIndex = data.ReadUInt16();
|
||||
|
||||
return directoryMapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File directory item
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File directory item on success, null on error</returns>
|
||||
private static DirectoryItem ParseDirectoryItem(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryItem directoryItem = new DirectoryItem();
|
||||
|
||||
directoryItem.FileNameCRC = data.ReadUInt32();
|
||||
directoryItem.NameOffset = data.ReadUInt32();
|
||||
directoryItem.TimeCreated = data.ReadUInt32();
|
||||
|
||||
// Cache the current offset
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Seek to the name offset
|
||||
data.Seek(directoryItem.NameOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the name
|
||||
directoryItem.Name = data.ReadString(Encoding.ASCII);
|
||||
|
||||
// Seek back to the right position
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return directoryItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File footer
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File footer on success, null on error</returns>
|
||||
private static Footer? ParseFooter(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Footer footer = new Footer();
|
||||
|
||||
footer.FileLength = data.ReadUInt32();
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
footer.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (footer.Signature != FooterSignatureString)
|
||||
return null;
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Deserializers
|
||||
@@ -6,7 +10,9 @@ namespace SabreTools.Serialization.Deserializers
|
||||
/// Base class for other XML deserializers
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class XmlFile<T> : IFileDeserializer<T>
|
||||
public class XmlFile<T> :
|
||||
IFileDeserializer<T>,
|
||||
IStreamDeserializer<T>
|
||||
{
|
||||
#region IFileDeserializer
|
||||
|
||||
@@ -14,7 +20,36 @@ namespace SabreTools.Serialization.Deserializers
|
||||
public T? Deserialize(string? path)
|
||||
{
|
||||
using var data = PathProcessor.OpenStream(path);
|
||||
return new Streams.XmlFile<T>().Deserialize(data);
|
||||
return Deserialize(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IStreamDeserializer
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the serializer and the reader
|
||||
var serializer = new XmlSerializer(typeof(T));
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
#endif
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
};
|
||||
var streamReader = new StreamReader(data);
|
||||
var xmlReader = XmlReader.Create(streamReader, settings);
|
||||
|
||||
// Perform the deserialization and return
|
||||
return (T?)serializer.Deserialize(xmlReader);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -5,14 +5,6 @@ namespace SabreTools.Serialization.Interfaces
|
||||
/// </summary>
|
||||
public interface IStreamSerializer<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserialize a Stream into <typeparamref name="T"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of object to deserialize to</typeparam>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled object on success, null on error</returns>
|
||||
T? Deserialize(System.IO.Stream? data);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize a <typeparamref name="T"/> into a Stream
|
||||
/// </summary>
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class AACS : IStreamSerializer<MediaKeyBlock>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MediaKeyBlock? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new AACS();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<Record>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an end of media key block record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled end of media key block record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an explicit subset-difference record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled explicit subset-difference record on success, null on error</returns>
|
||||
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<SubsetDifference>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a media key data record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled media key data record on success, null on error</returns>
|
||||
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<byte[]>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a subset-difference index record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled subset-difference index record on success, null on error</returns>
|
||||
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<uint>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a type and version record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled type and version record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a drive revocation list record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled drive revocation list record on success, null on error</returns>
|
||||
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<DriveRevocationSignatureBlock>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a host revocation list record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled host revocation list record on success, null on error</returns>
|
||||
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<HostRevocationSignatureBlock>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a verify media key record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled verify media key record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a copyright record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled copyright record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class ArchiveDotOrg : XmlFile<Models.ArchiveDotOrg.Files>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.ArchiveDotOrg.Files? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new ArchiveDotOrg();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class AttractMode : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new AttractMode();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<Row>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class BDPlus : IStreamSerializer<SVM>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static SVM? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new BDPlus();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SVM
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SVM on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class BFPK : IStreamSerializer<Archive>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new BFPK();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled file entry on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class BSP : IStreamSerializer<Models.BSP.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.BSP.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new BSP();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Level header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Level header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a lump
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled lump on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Level texture header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Level texture header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a texture
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="mipmap">Mipmap level</param>
|
||||
/// <returns>Filled texture on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class CFB : IStreamSerializer<Binary>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Binary? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new CFB();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<SectorNumber?>();
|
||||
|
||||
// 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<SectorNumber?>();
|
||||
|
||||
// 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<SectorNumber?>();
|
||||
|
||||
// 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<DirectoryEntry>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled file header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a sector full of sector numbers
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="sectorShift">Sector shift from the header</param>
|
||||
/// <returns>Filled sector full of sector numbers on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a sector full of directory entries
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="sectorShift">Sector shift from the header</param>
|
||||
/// <param name="majorVersion">Major version from the header</param>
|
||||
/// <returns>Filled sector full of directory entries on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version from the header</param>
|
||||
/// <returns>Filled directory entry on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.N3DS;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class CIA : IStreamSerializer<Models.N3DS.CIA>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.N3DS.CIA? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new CIA();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a CIA header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled CIA header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a certificate
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled certificate on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a ticket
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="fromCdn">Indicates if the ticket is from CDN</param>
|
||||
/// <returns>Filled ticket on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a title metadata
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="fromCdn">Indicates if the ticket is from CDN</param>
|
||||
/// <returns>Filled title metadata on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a content info record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled content info record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a content chunk record
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled content chunk record on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a meta data
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled meta data on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Catalog : JsonFile<Models.Xbox.Catalog>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.Xbox.Catalog? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Catalog();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
// Catalog JSON is encoded as UTF-16 LE
|
||||
public override Models.Xbox.Catalog? Deserialize(Stream? data)
|
||||
=> Deserialize(data, new UnicodeEncoding());
|
||||
}
|
||||
}
|
||||
@@ -1,903 +0,0 @@
|
||||
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.Streams
|
||||
{
|
||||
public partial class ClrMamePro : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data, bool quotes = true)
|
||||
{
|
||||
var deserializer = new ClrMamePro();
|
||||
return deserializer.Deserialize(data, quotes);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, true);
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
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<GameBase>();
|
||||
var releases = new List<Release>();
|
||||
var biosSets = new List<BiosSet>();
|
||||
var roms = new List<Rom>();
|
||||
var disks = new List<Disk>();
|
||||
var medias = new List<Media>();
|
||||
var samples = new List<Sample>();
|
||||
var archives = new List<Archive>();
|
||||
var chips = new List<Chip>();
|
||||
var videos = new List<Video>();
|
||||
var dipSwitches = new List<DipSwitch>();
|
||||
|
||||
var additional = new List<string>();
|
||||
var headerAdditional = new List<string>();
|
||||
var gameAdditional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Ignore certain row types
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case CmpRowType.None:
|
||||
case CmpRowType.Comment:
|
||||
continue;
|
||||
case CmpRowType.EndTopLevel:
|
||||
switch (lastTopLevel)
|
||||
{
|
||||
case "doscenter":
|
||||
if (dat.ClrMamePro != null)
|
||||
dat.ClrMamePro.ADDITIONAL_ELEMENTS = [.. headerAdditional];
|
||||
|
||||
headerAdditional.Clear();
|
||||
break;
|
||||
case "game":
|
||||
case "machine":
|
||||
case "resource":
|
||||
case "set":
|
||||
if (game != null)
|
||||
{
|
||||
game.Release = [.. releases];
|
||||
game.BiosSet = [.. biosSets];
|
||||
game.Rom = [.. roms];
|
||||
game.Disk = [.. disks];
|
||||
game.Media = [.. medias];
|
||||
game.Sample = [.. samples];
|
||||
game.Archive = [.. archives];
|
||||
game.Chip = [.. chips];
|
||||
game.Video = [.. videos];
|
||||
game.DipSwitch = [.. dipSwitches];
|
||||
game.ADDITIONAL_ELEMENTS = [.. gameAdditional];
|
||||
|
||||
games.Add(game);
|
||||
game = null;
|
||||
}
|
||||
|
||||
releases.Clear();
|
||||
biosSets.Clear();
|
||||
roms.Clear();
|
||||
disks.Clear();
|
||||
medias.Clear();
|
||||
samples.Clear();
|
||||
archives.Clear();
|
||||
chips.Clear();
|
||||
videos.Clear();
|
||||
dipSwitches.Clear();
|
||||
gameAdditional.Clear();
|
||||
break;
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're at the root
|
||||
if (reader.RowType == CmpRowType.TopLevel)
|
||||
{
|
||||
lastTopLevel = reader.TopLevel;
|
||||
switch (reader.TopLevel)
|
||||
{
|
||||
case "clrmamepro":
|
||||
dat.ClrMamePro = new Models.ClrMamePro.ClrMamePro();
|
||||
break;
|
||||
case "game":
|
||||
game = new Game();
|
||||
break;
|
||||
case "machine":
|
||||
game = new Machine();
|
||||
break;
|
||||
case "resource":
|
||||
game = new Resource();
|
||||
break;
|
||||
case "set":
|
||||
game = new Set();
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in the doscenter block
|
||||
else if (reader.TopLevel == "clrmamepro"
|
||||
&& reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
dat.ClrMamePro ??= new Models.ClrMamePro.ClrMamePro();
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
dat.ClrMamePro.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
case "description":
|
||||
dat.ClrMamePro.Description = reader.Standalone?.Value;
|
||||
break;
|
||||
case "rootdir":
|
||||
dat.ClrMamePro.RootDir = reader.Standalone?.Value;
|
||||
break;
|
||||
case "category":
|
||||
dat.ClrMamePro.Category = reader.Standalone?.Value;
|
||||
break;
|
||||
case "version":
|
||||
dat.ClrMamePro.Version = reader.Standalone?.Value;
|
||||
break;
|
||||
case "date":
|
||||
dat.ClrMamePro.Date = reader.Standalone?.Value;
|
||||
break;
|
||||
case "author":
|
||||
dat.ClrMamePro.Author = reader.Standalone?.Value;
|
||||
break;
|
||||
case "homepage":
|
||||
dat.ClrMamePro.Homepage = reader.Standalone?.Value;
|
||||
break;
|
||||
case "url":
|
||||
dat.ClrMamePro.Url = reader.Standalone?.Value;
|
||||
break;
|
||||
case "comment":
|
||||
dat.ClrMamePro.Comment = reader.Standalone?.Value;
|
||||
break;
|
||||
case "header":
|
||||
dat.ClrMamePro.Header = reader.Standalone?.Value;
|
||||
break;
|
||||
case "type":
|
||||
dat.ClrMamePro.Type = reader.Standalone?.Value;
|
||||
break;
|
||||
case "forcemerging":
|
||||
dat.ClrMamePro.ForceMerging = reader.Standalone?.Value;
|
||||
break;
|
||||
case "forcezipping":
|
||||
dat.ClrMamePro.ForceZipping = reader.Standalone?.Value;
|
||||
break;
|
||||
case "forcepacking":
|
||||
dat.ClrMamePro.ForcePacking = reader.Standalone?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
headerAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a game, machine, resource, or set block
|
||||
else if ((reader.TopLevel == "game"
|
||||
|| reader.TopLevel == "machine"
|
||||
|| reader.TopLevel == "resource"
|
||||
|| reader.TopLevel == "set")
|
||||
&& reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
game ??= reader.TopLevel switch
|
||||
{
|
||||
"game" => new Game(),
|
||||
"machine" => new Machine(),
|
||||
"resource" => new Resource(),
|
||||
"set" => new Set(),
|
||||
_ => throw new FormatException($"Unknown top-level block: {reader.TopLevel}"),
|
||||
};
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
game.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
case "description":
|
||||
game.Description = reader.Standalone?.Value;
|
||||
break;
|
||||
case "year":
|
||||
game.Year = reader.Standalone?.Value;
|
||||
break;
|
||||
case "manufacturer":
|
||||
game.Manufacturer = reader.Standalone?.Value;
|
||||
break;
|
||||
case "category":
|
||||
game.Category = reader.Standalone?.Value;
|
||||
break;
|
||||
case "cloneof":
|
||||
game.CloneOf = reader.Standalone?.Value;
|
||||
break;
|
||||
case "romof":
|
||||
game.RomOf = reader.Standalone?.Value;
|
||||
break;
|
||||
case "sampleof":
|
||||
game.SampleOf = reader.Standalone?.Value;
|
||||
break;
|
||||
case "sample":
|
||||
var sample = new Sample
|
||||
{
|
||||
Name = reader.Standalone?.Value ?? string.Empty,
|
||||
ADDITIONAL_ELEMENTS = [],
|
||||
};
|
||||
samples.Add(sample);
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in an item block
|
||||
else if ((reader.TopLevel == "game"
|
||||
|| reader.TopLevel == "machine"
|
||||
|| reader.TopLevel == "resource"
|
||||
|| reader.TopLevel == "set")
|
||||
&& game != null
|
||||
&& reader.RowType == CmpRowType.Internal)
|
||||
{
|
||||
// Create the block
|
||||
switch (reader.InternalName)
|
||||
{
|
||||
case "release":
|
||||
var release = CreateRelease(reader);
|
||||
if (release != null)
|
||||
releases.Add(release);
|
||||
break;
|
||||
case "biosset":
|
||||
var biosSet = CreateBiosSet(reader);
|
||||
if (biosSet != null)
|
||||
biosSets.Add(biosSet);
|
||||
break;
|
||||
case "rom":
|
||||
var rom = CreateRom(reader);
|
||||
if (rom != null)
|
||||
roms.Add(rom);
|
||||
break;
|
||||
case "disk":
|
||||
var disk = CreateDisk(reader);
|
||||
if (disk != null)
|
||||
disks.Add(disk);
|
||||
break;
|
||||
case "media":
|
||||
var media = CreateMedia(reader);
|
||||
if (media != null)
|
||||
medias.Add(media);
|
||||
break;
|
||||
case "sample":
|
||||
var sample = CreateSample(reader);
|
||||
if (sample != null)
|
||||
samples.Add(sample);
|
||||
break;
|
||||
case "archive":
|
||||
var archive = CreateArchive(reader);
|
||||
if (archive != null)
|
||||
archives.Add(archive);
|
||||
break;
|
||||
case "chip":
|
||||
var chip = CreateChip(reader);
|
||||
if (chip != null)
|
||||
chips.Add(chip);
|
||||
break;
|
||||
case "video":
|
||||
var video = CreateVideo(reader);
|
||||
if (video != null)
|
||||
videos.Add(video);
|
||||
break;
|
||||
case "sound":
|
||||
var sound = CreateSound(reader);
|
||||
if (sound != null)
|
||||
game.Sound = sound;
|
||||
break;
|
||||
case "input":
|
||||
var input = CreateInput(reader);
|
||||
if (input != null)
|
||||
game.Input = input;
|
||||
break;
|
||||
case "dipswitch":
|
||||
var dipSwitch = CreateDipSwitch(reader);
|
||||
if (dipSwitch != null)
|
||||
dipSwitches.Add(dipSwitch);
|
||||
break;
|
||||
case "driver":
|
||||
var driver = CreateDriver(reader);
|
||||
if (driver != null)
|
||||
game.Driver = driver;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(reader.CurrentLine);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.Game = [.. games];
|
||||
dat.ADDITIONAL_ELEMENTS = [.. additional];
|
||||
return dat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Release object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Release object created from the reader context</returns>
|
||||
private static Release? CreateRelease(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var release = new Release();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
release.Name = kvp.Value;
|
||||
break;
|
||||
case "region":
|
||||
release.Region = kvp.Value;
|
||||
break;
|
||||
case "language":
|
||||
release.Language = kvp.Value;
|
||||
break;
|
||||
case "date":
|
||||
release.Date = kvp.Value;
|
||||
break;
|
||||
case "default":
|
||||
release.Default = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
release.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return release;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a BiosSet object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>BiosSet object created from the reader context</returns>
|
||||
private static BiosSet? CreateBiosSet(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var biosset = new BiosSet();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
biosset.Name = kvp.Value;
|
||||
break;
|
||||
case "description":
|
||||
biosset.Description = kvp.Value;
|
||||
break;
|
||||
case "default":
|
||||
biosset.Default = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
biosset.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return biosset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Rom object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Rom object created from the reader context</returns>
|
||||
private static Rom? CreateRom(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var rom = new Rom();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
rom.Name = kvp.Value;
|
||||
break;
|
||||
case "size":
|
||||
rom.Size = kvp.Value;
|
||||
break;
|
||||
case "crc":
|
||||
rom.CRC = kvp.Value;
|
||||
break;
|
||||
case "md5":
|
||||
rom.MD5 = kvp.Value;
|
||||
break;
|
||||
case "sha1":
|
||||
rom.SHA1 = kvp.Value;
|
||||
break;
|
||||
case "sha256":
|
||||
rom.SHA256 = kvp.Value;
|
||||
break;
|
||||
case "sha384":
|
||||
rom.SHA384 = kvp.Value;
|
||||
break;
|
||||
case "sha512":
|
||||
rom.SHA512 = kvp.Value;
|
||||
break;
|
||||
case "spamsum":
|
||||
rom.SpamSum = kvp.Value;
|
||||
break;
|
||||
case "xxh3_64":
|
||||
rom.xxHash364 = kvp.Value;
|
||||
break;
|
||||
case "xxh3_128":
|
||||
rom.xxHash3128 = kvp.Value;
|
||||
break;
|
||||
case "merge":
|
||||
rom.Merge = kvp.Value;
|
||||
break;
|
||||
case "status":
|
||||
rom.Status = kvp.Value;
|
||||
break;
|
||||
case "region":
|
||||
rom.Region = kvp.Value;
|
||||
break;
|
||||
case "flags":
|
||||
rom.Flags = kvp.Value;
|
||||
break;
|
||||
case "offs":
|
||||
rom.Offs = kvp.Value;
|
||||
break;
|
||||
case "serial":
|
||||
rom.Serial = kvp.Value;
|
||||
break;
|
||||
case "header":
|
||||
rom.Header = kvp.Value;
|
||||
break;
|
||||
case "date":
|
||||
rom.Date = kvp.Value;
|
||||
break;
|
||||
case "inverted":
|
||||
rom.Inverted = kvp.Value;
|
||||
break;
|
||||
case "mia":
|
||||
rom.MIA = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
rom.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return rom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Disk object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Disk object created from the reader context</returns>
|
||||
private static Disk? CreateDisk(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var disk = new Disk();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
disk.Name = kvp.Value;
|
||||
break;
|
||||
case "md5":
|
||||
disk.MD5 = kvp.Value;
|
||||
break;
|
||||
case "sha1":
|
||||
disk.SHA1 = kvp.Value;
|
||||
break;
|
||||
case "merge":
|
||||
disk.Merge = kvp.Value;
|
||||
break;
|
||||
case "status":
|
||||
disk.Status = kvp.Value;
|
||||
break;
|
||||
case "flags":
|
||||
disk.Flags = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
disk.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return disk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Media object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Media object created from the reader context</returns>
|
||||
private static Media? CreateMedia(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var media = new Media();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
media.Name = kvp.Value;
|
||||
break;
|
||||
case "md5":
|
||||
media.MD5 = kvp.Value;
|
||||
break;
|
||||
case "sha1":
|
||||
media.SHA1 = kvp.Value;
|
||||
break;
|
||||
case "sha256":
|
||||
media.SHA256 = kvp.Value;
|
||||
break;
|
||||
case "spamsum":
|
||||
media.SpamSum = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
media.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return media;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Sample object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Sample object created from the reader context</returns>
|
||||
private static Sample? CreateSample(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var sample = new Sample();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
sample.Name = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sample.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return sample;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Archive object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Archive object created from the reader context</returns>
|
||||
private static Archive? CreateArchive(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var archive = new Archive();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
archive.Name = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
archive.ADDITIONAL_ELEMENTS = [.. itemAdditional];
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Chip object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Chip object created from the reader context</returns>
|
||||
private static Chip? CreateChip(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var chip = new Chip();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "type":
|
||||
chip.Type = kvp.Value;
|
||||
break;
|
||||
case "name":
|
||||
chip.Name = kvp.Value;
|
||||
break;
|
||||
case "flags":
|
||||
chip.Flags = kvp.Value;
|
||||
break;
|
||||
case "clock":
|
||||
chip.Clock = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chip.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return chip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Video object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Video object created from the reader context</returns>
|
||||
private static Video? CreateVideo(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var video = new Video();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "screen":
|
||||
video.Screen = kvp.Value;
|
||||
break;
|
||||
case "orientation":
|
||||
video.Orientation = kvp.Value;
|
||||
break;
|
||||
case "x":
|
||||
video.X = kvp.Value;
|
||||
break;
|
||||
case "y":
|
||||
video.Y = kvp.Value;
|
||||
break;
|
||||
case "aspectx":
|
||||
video.AspectX = kvp.Value;
|
||||
break;
|
||||
case "aspecty":
|
||||
video.AspectY = kvp.Value;
|
||||
break;
|
||||
case "freq":
|
||||
video.Freq = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
video.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Sound object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Sound object created from the reader context</returns>
|
||||
private static Sound? CreateSound(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var sound = new Sound();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "channels":
|
||||
sound.Channels = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sound.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return sound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Input object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Input object created from the reader context</returns>
|
||||
private static Input? CreateInput(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var input = new Input();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "players":
|
||||
input.Players = kvp.Value;
|
||||
break;
|
||||
case "control":
|
||||
input.Control = kvp.Value;
|
||||
break;
|
||||
case "buttons":
|
||||
input.Buttons = kvp.Value;
|
||||
break;
|
||||
case "coins":
|
||||
input.Coins = kvp.Value;
|
||||
break;
|
||||
case "tilt":
|
||||
input.Tilt = kvp.Value;
|
||||
break;
|
||||
case "service":
|
||||
input.Service = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
input.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return input;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a DipSwitch object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>DipSwitch object created from the reader context</returns>
|
||||
private static DipSwitch? CreateDipSwitch(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var dipswitch = new DipSwitch();
|
||||
var entries = new List<string>();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
dipswitch.Name = kvp.Value;
|
||||
break;
|
||||
case "entry":
|
||||
entries.Add(kvp.Value);
|
||||
break;
|
||||
case "default":
|
||||
dipswitch.Default = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dipswitch.Entry = [.. entries];
|
||||
dipswitch.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return dipswitch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Driver object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>Driver object created from the reader context</returns>
|
||||
private static Driver? CreateDriver(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var driver = new Driver();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "status":
|
||||
driver.Status = kvp.Value;
|
||||
break;
|
||||
case "color":
|
||||
driver.Color = kvp.Value;
|
||||
break;
|
||||
case "sound":
|
||||
driver.Sound = kvp.Value;
|
||||
break;
|
||||
case "palettesize":
|
||||
driver.PaletteSize = kvp.Value;
|
||||
break;
|
||||
case "blit":
|
||||
driver.Blit = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
itemAdditional.Add($"{kvp.Key}: {kvp.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
driver.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,629 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.CueSheets;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class CueSheet : IStreamSerializer<Models.CueSheets.CueSheet>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.CueSheets.CueSheet? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new CueSheet();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.CueSheets.CueSheet? 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 cuesheet to fill
|
||||
var cueSheet = new Models.CueSheets.CueSheet();
|
||||
var cueFiles = new List<CueFile>();
|
||||
|
||||
// Read the next line from the input
|
||||
string? lastLine = null;
|
||||
while (true)
|
||||
{
|
||||
string? line = lastLine ?? data.ReadQuotedString();
|
||||
lastLine = null;
|
||||
|
||||
// If we have a null line, break from the loop
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
// If we have an empty line, we skip
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
|
||||
string[] splitLine = Regex
|
||||
.Matches(line, @"[^\s""]+|""[^""]*""")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
switch (splitLine[0])
|
||||
{
|
||||
// Read comments
|
||||
case "REM":
|
||||
// We ignore all comments for now
|
||||
break;
|
||||
|
||||
// Read MCN
|
||||
case "CATALOG":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"CATALOG line malformed: {line}");
|
||||
|
||||
cueSheet.Catalog = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read external CD-Text file path
|
||||
case "CDTEXTFILE":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"CDTEXTFILE line malformed: {line}");
|
||||
|
||||
cueSheet.CdTextFile = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced performer
|
||||
case "PERFORMER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"PERFORMER line malformed: {line}");
|
||||
|
||||
cueSheet.Performer = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced songwriter
|
||||
case "SONGWRITER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"SONGWRITER line malformed: {line}");
|
||||
|
||||
cueSheet.Songwriter = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced title
|
||||
case "TITLE":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"TITLE line malformed: {line}");
|
||||
|
||||
cueSheet.Title = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read file information
|
||||
case "FILE":
|
||||
if (splitLine.Length < 3)
|
||||
throw new FormatException($"FILE line malformed: {line}");
|
||||
|
||||
var file = CreateCueFile(splitLine[1], splitLine[2], data, out lastLine);
|
||||
if (file == default)
|
||||
throw new FormatException($"FILE line malformed: {line}");
|
||||
|
||||
cueFiles.Add(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cueSheet.Files = [.. cueFiles];
|
||||
return cueSheet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill a FILE from an array of lines
|
||||
/// </summary>
|
||||
/// <param name="fileName">File name to set</param>
|
||||
/// <param name="fileType">File type to set</param>
|
||||
/// <param name="data">Stream to pull from</param>
|
||||
private static CueFile? CreateCueFile(string fileName, string fileType, Stream data, out string? lastLine)
|
||||
{
|
||||
// Check the required parameters
|
||||
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (data.Position < 0 || data.Position >= data.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
// Create the holding objects
|
||||
lastLine = null;
|
||||
var cueFile = new CueFile();
|
||||
var cueTracks = new List<CueTrack>();
|
||||
|
||||
// Set the current fields
|
||||
cueFile.FileName = fileName.Trim('"');
|
||||
cueFile.FileType = GetFileType(fileType);
|
||||
|
||||
while (true)
|
||||
{
|
||||
string? line = lastLine ?? data.ReadQuotedString();
|
||||
lastLine = null;
|
||||
|
||||
// If we have a null line, break from the loop
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
// If we have an empty line, we skip
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
|
||||
string[] splitLine = Regex
|
||||
.Matches(line, @"[^\s""]+|""[^""]*""")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
switch (splitLine[0])
|
||||
{
|
||||
// Read comments
|
||||
case "REM":
|
||||
// We ignore all comments for now
|
||||
break;
|
||||
|
||||
// Read track information
|
||||
case "TRACK":
|
||||
if (splitLine.Length < 3)
|
||||
throw new FormatException($"TRACK line malformed: {line}");
|
||||
|
||||
var track = CreateCueTrack(splitLine[1], splitLine[2], data, out lastLine);
|
||||
if (track == default)
|
||||
throw new FormatException($"TRACK line malformed: {line}");
|
||||
|
||||
cueTracks.Add(track);
|
||||
break;
|
||||
|
||||
// Next file found, return
|
||||
case "FILE":
|
||||
lastLine = line;
|
||||
cueFile.Tracks = [.. cueTracks];
|
||||
return cueFile;
|
||||
|
||||
// Default means return
|
||||
default:
|
||||
lastLine = line;
|
||||
cueFile.Tracks = [.. cueTracks];
|
||||
return cueFile;
|
||||
}
|
||||
}
|
||||
|
||||
cueFile.Tracks = [.. cueTracks];
|
||||
return cueFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill a TRACK from an array of lines
|
||||
/// </summary>
|
||||
/// <param name="number">Number to set</param>
|
||||
/// <param name="dataType">Data type to set</param>
|
||||
/// <param name="data">Stream to pull from</param>
|
||||
private static CueTrack? CreateCueTrack(string number, string dataType, Stream data, out string? lastLine)
|
||||
{
|
||||
// Check the required parameters
|
||||
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (data.Position < 0 || data.Position >= data.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
// Set the current fields
|
||||
if (!int.TryParse(number, out int parsedNumber))
|
||||
throw new ArgumentException($"Number was not a number: {number}");
|
||||
else if (parsedNumber < 1 || parsedNumber > 99)
|
||||
throw new IndexOutOfRangeException($"Index must be between 1 and 99: {parsedNumber}");
|
||||
|
||||
// Create the holding objects
|
||||
lastLine = null;
|
||||
var cueTrack = new CueTrack();
|
||||
var cueIndices = new List<CueIndex>();
|
||||
|
||||
cueTrack.Number = parsedNumber;
|
||||
cueTrack.DataType = GetDataType(dataType);
|
||||
|
||||
while (true)
|
||||
{
|
||||
string? line = lastLine ?? data.ReadQuotedString();
|
||||
lastLine = null;
|
||||
|
||||
// If we have a null line, break from the loop
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
// If we have an empty line, we skip
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
|
||||
string[] splitLine = Regex
|
||||
.Matches(line, @"[^\s""]+|""[^""]*""")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
switch (splitLine[0])
|
||||
{
|
||||
// Read comments
|
||||
case "REM":
|
||||
// We ignore all comments for now
|
||||
break;
|
||||
|
||||
// Read flag information
|
||||
case "FLAGS":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"FLAGS line malformed: {line}");
|
||||
|
||||
cueTrack.Flags = GetFlags(splitLine);
|
||||
break;
|
||||
|
||||
// Read International Standard Recording Code
|
||||
case "ISRC":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"ISRC line malformed: {line}");
|
||||
|
||||
cueTrack.ISRC = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced performer
|
||||
case "PERFORMER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"PERFORMER line malformed: {line}");
|
||||
|
||||
cueTrack.Performer = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced songwriter
|
||||
case "SONGWRITER":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"SONGWRITER line malformed: {line}");
|
||||
|
||||
cueTrack.Songwriter = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read CD-Text enhanced title
|
||||
case "TITLE":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"TITLE line malformed: {line}");
|
||||
|
||||
cueTrack.Title = splitLine[1].Trim('"');
|
||||
break;
|
||||
|
||||
// Read pregap information
|
||||
case "PREGAP":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"PREGAP line malformed: {line}");
|
||||
|
||||
var pregap = CreatePreGap(splitLine[1]);
|
||||
if (pregap == default)
|
||||
throw new FormatException($"PREGAP line malformed: {line}");
|
||||
|
||||
cueTrack.PreGap = pregap;
|
||||
break;
|
||||
|
||||
// Read index information
|
||||
case "INDEX":
|
||||
if (splitLine.Length < 3)
|
||||
throw new FormatException($"INDEX line malformed: {line}");
|
||||
|
||||
var index = CreateCueIndex(splitLine[1], splitLine[2]);
|
||||
if (index == default)
|
||||
throw new FormatException($"INDEX line malformed: {line}");
|
||||
|
||||
cueIndices.Add(index);
|
||||
break;
|
||||
|
||||
// Read postgap information
|
||||
case "POSTGAP":
|
||||
if (splitLine.Length < 2)
|
||||
throw new FormatException($"POSTGAP line malformed: {line}");
|
||||
|
||||
var postgap = CreatePostGap(splitLine[1]);
|
||||
if (postgap == default)
|
||||
throw new FormatException($"POSTGAP line malformed: {line}");
|
||||
|
||||
cueTrack.PostGap = postgap;
|
||||
break;
|
||||
|
||||
// Next track or file found, return
|
||||
case "TRACK":
|
||||
case "FILE":
|
||||
lastLine = line;
|
||||
cueTrack.Indices = [.. cueIndices];
|
||||
return cueTrack;
|
||||
|
||||
// Default means return
|
||||
default:
|
||||
lastLine = line;
|
||||
cueTrack.Indices = [.. cueIndices];
|
||||
return cueTrack;
|
||||
}
|
||||
}
|
||||
|
||||
cueTrack.Indices = [.. cueIndices];
|
||||
return cueTrack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a PREGAP from a mm:ss:ff length
|
||||
/// </summary>
|
||||
/// <param name="length">String to get length information from</param>
|
||||
private static PreGap CreatePreGap(string length)
|
||||
{
|
||||
// Ignore empty lines
|
||||
if (string.IsNullOrEmpty(length))
|
||||
throw new ArgumentException("Length was null or whitespace");
|
||||
|
||||
// Ignore lines that don't contain the correct information
|
||||
if (length!.Length != 8 || length.Count(c => c == ':') != 2)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Split the line
|
||||
string[] splitLength = length.Split(':');
|
||||
if (splitLength.Length != 3)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Parse the lengths
|
||||
int[] lengthSegments = new int[3];
|
||||
|
||||
// Minutes
|
||||
if (!int.TryParse(splitLength[0], out lengthSegments[0]))
|
||||
throw new FormatException($"Minutes segment was not a number: {splitLength[0]}");
|
||||
else if (lengthSegments[0] < 0)
|
||||
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
|
||||
|
||||
// Seconds
|
||||
if (!int.TryParse(splitLength[1], out lengthSegments[1]))
|
||||
throw new FormatException($"Seconds segment was not a number: {splitLength[1]}");
|
||||
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
|
||||
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
|
||||
|
||||
// Frames
|
||||
if (!int.TryParse(splitLength[2], out lengthSegments[2]))
|
||||
throw new FormatException($"Frames segment was not a number: {splitLength[2]}");
|
||||
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
|
||||
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
|
||||
|
||||
// Set the values
|
||||
var preGap = new PreGap
|
||||
{
|
||||
Minutes = lengthSegments[0],
|
||||
Seconds = lengthSegments[1],
|
||||
Frames = lengthSegments[2],
|
||||
};
|
||||
return preGap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill a INDEX from an array of lines
|
||||
/// </summary>
|
||||
/// <param name="index">Index to set</param>
|
||||
/// <param name="startTime">Start time to set</param>
|
||||
private static CueIndex CreateCueIndex(string index, string startTime)
|
||||
{
|
||||
// Set the current fields
|
||||
if (!int.TryParse(index, out int parsedIndex))
|
||||
throw new ArgumentException($"Index was not a number: {index}");
|
||||
else if (parsedIndex < 0 || parsedIndex > 99)
|
||||
throw new IndexOutOfRangeException($"Index must be between 0 and 99: {parsedIndex}");
|
||||
|
||||
// Ignore empty lines
|
||||
if (string.IsNullOrEmpty(startTime))
|
||||
throw new ArgumentException("Start time was null or whitespace");
|
||||
|
||||
// Ignore lines that don't contain the correct information
|
||||
if (startTime!.Length != 8 || startTime.Count(c => c == ':') != 2)
|
||||
throw new FormatException($"Start time was not in a recognized format: {startTime}");
|
||||
|
||||
// Split the line
|
||||
string[] splitTime = startTime.Split(':');
|
||||
if (splitTime.Length != 3)
|
||||
throw new FormatException($"Start time was not in a recognized format: {startTime}");
|
||||
|
||||
// Parse the lengths
|
||||
int[] lengthSegments = new int[3];
|
||||
|
||||
// Minutes
|
||||
if (!int.TryParse(splitTime[0], out lengthSegments[0]))
|
||||
throw new FormatException($"Minutes segment was not a number: {splitTime[0]}");
|
||||
else if (lengthSegments[0] < 0)
|
||||
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
|
||||
|
||||
// Seconds
|
||||
if (!int.TryParse(splitTime[1], out lengthSegments[1]))
|
||||
throw new FormatException($"Seconds segment was not a number: {splitTime[1]}");
|
||||
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
|
||||
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
|
||||
|
||||
// Frames
|
||||
if (!int.TryParse(splitTime[2], out lengthSegments[2]))
|
||||
throw new FormatException($"Frames segment was not a number: {splitTime[2]}");
|
||||
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
|
||||
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
|
||||
|
||||
// Set the values
|
||||
var cueIndex = new CueIndex
|
||||
{
|
||||
Index = parsedIndex,
|
||||
Minutes = lengthSegments[0],
|
||||
Seconds = lengthSegments[1],
|
||||
Frames = lengthSegments[2],
|
||||
};
|
||||
return cueIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a POSTGAP from a mm:ss:ff length
|
||||
/// </summary>
|
||||
/// <param name="length">String to get length information from</param>
|
||||
private static PostGap CreatePostGap(string length)
|
||||
{
|
||||
// Ignore empty lines
|
||||
if (string.IsNullOrEmpty(length))
|
||||
throw new ArgumentException("Length was null or whitespace");
|
||||
|
||||
// Ignore lines that don't contain the correct information
|
||||
if (length!.Length != 8 || length.Count(c => c == ':') != 2)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Split the line
|
||||
string[] splitLength = length.Split(':');
|
||||
if (splitLength.Length != 3)
|
||||
throw new FormatException($"Length was not in a recognized format: {length}");
|
||||
|
||||
// Parse the lengths
|
||||
int[] lengthSegments = new int[3];
|
||||
|
||||
// Minutes
|
||||
if (!int.TryParse(splitLength[0], out lengthSegments[0]))
|
||||
throw new FormatException($"Minutes segment was not a number: {splitLength[0]}");
|
||||
else if (lengthSegments[0] < 0)
|
||||
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
|
||||
|
||||
// Seconds
|
||||
if (!int.TryParse(splitLength[1], out lengthSegments[1]))
|
||||
throw new FormatException($"Seconds segment was not a number: {splitLength[1]}");
|
||||
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
|
||||
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
|
||||
|
||||
// Frames
|
||||
if (!int.TryParse(splitLength[2], out lengthSegments[2]))
|
||||
throw new FormatException($"Frames segment was not a number: {splitLength[2]}");
|
||||
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
|
||||
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
|
||||
|
||||
// Set the values
|
||||
var postGap = new PostGap
|
||||
{
|
||||
Minutes = lengthSegments[0],
|
||||
Seconds = lengthSegments[1],
|
||||
Frames = lengthSegments[2],
|
||||
};
|
||||
return postGap;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Get the file type from a given string
|
||||
/// </summary>
|
||||
/// <param name="fileType">String to get value from</param>
|
||||
/// <returns>CueFileType, if possible</returns>
|
||||
private static CueFileType GetFileType(string? fileType)
|
||||
{
|
||||
switch (fileType?.ToLowerInvariant())
|
||||
{
|
||||
case "binary":
|
||||
return CueFileType.BINARY;
|
||||
|
||||
case "motorola":
|
||||
return CueFileType.MOTOROLA;
|
||||
|
||||
case "aiff":
|
||||
return CueFileType.AIFF;
|
||||
|
||||
case "wave":
|
||||
return CueFileType.WAVE;
|
||||
|
||||
case "mp3":
|
||||
return CueFileType.MP3;
|
||||
|
||||
default:
|
||||
return CueFileType.BINARY;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the data type from a given string
|
||||
/// </summary>
|
||||
/// <param name="dataType">String to get value from</param>
|
||||
/// <returns>CueTrackDataType, if possible (default AUDIO)</returns>
|
||||
private static CueTrackDataType GetDataType(string? dataType)
|
||||
{
|
||||
switch (dataType?.ToLowerInvariant())
|
||||
{
|
||||
case "audio":
|
||||
return CueTrackDataType.AUDIO;
|
||||
|
||||
case "cdg":
|
||||
return CueTrackDataType.CDG;
|
||||
|
||||
case "mode1/2048":
|
||||
return CueTrackDataType.MODE1_2048;
|
||||
|
||||
case "mode1/2352":
|
||||
return CueTrackDataType.MODE1_2352;
|
||||
|
||||
case "mode2/2336":
|
||||
return CueTrackDataType.MODE2_2336;
|
||||
|
||||
case "mode2/2352":
|
||||
return CueTrackDataType.MODE2_2352;
|
||||
|
||||
case "cdi/2336":
|
||||
return CueTrackDataType.CDI_2336;
|
||||
|
||||
case "cdi/2352":
|
||||
return CueTrackDataType.CDI_2352;
|
||||
|
||||
default:
|
||||
return CueTrackDataType.AUDIO;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the flag value for an array of strings
|
||||
/// </summary>
|
||||
/// <param name="flagStrings">Possible flags as strings</param>
|
||||
/// <returns>CueTrackFlag value representing the strings, if possible</returns>
|
||||
private static CueTrackFlag GetFlags(string?[]? flagStrings)
|
||||
{
|
||||
CueTrackFlag flag = 0;
|
||||
if (flagStrings == null)
|
||||
return flag;
|
||||
|
||||
foreach (string? flagString in flagStrings)
|
||||
{
|
||||
switch (flagString?.ToLowerInvariant())
|
||||
{
|
||||
case "flags":
|
||||
// No-op since this is the start of the line
|
||||
break;
|
||||
|
||||
case "dcp":
|
||||
flag |= CueTrackFlag.DCP;
|
||||
break;
|
||||
|
||||
case "4ch":
|
||||
flag |= CueTrackFlag.FourCH;
|
||||
break;
|
||||
|
||||
case "pre":
|
||||
flag |= CueTrackFlag.PRE;
|
||||
break;
|
||||
|
||||
case "scms":
|
||||
flag |= CueTrackFlag.SCMS;
|
||||
break;
|
||||
|
||||
case "data":
|
||||
flag |= CueTrackFlag.DATA;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return flag;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.DosCenter;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class DosCenter : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new DosCenter();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new ClrMameProReader(data, Encoding.UTF8) { DosCenter = true };
|
||||
var dat = new MetadataFile();
|
||||
|
||||
// Loop through and parse out the values
|
||||
string? lastTopLevel = reader.TopLevel;
|
||||
|
||||
Game? game = null;
|
||||
var games = new List<Game>();
|
||||
var files = new List<Models.DosCenter.File>();
|
||||
|
||||
var additional = new List<string>();
|
||||
var headerAdditional = new List<string>();
|
||||
var gameAdditional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Ignore certain row types
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case CmpRowType.None:
|
||||
case CmpRowType.Comment:
|
||||
continue;
|
||||
case CmpRowType.EndTopLevel:
|
||||
switch (lastTopLevel)
|
||||
{
|
||||
case "doscenter":
|
||||
if (dat.DosCenter != null)
|
||||
dat.DosCenter.ADDITIONAL_ELEMENTS = headerAdditional.ToArray();
|
||||
|
||||
headerAdditional.Clear();
|
||||
break;
|
||||
case "game":
|
||||
if (game != null)
|
||||
{
|
||||
game.File = files.ToArray();
|
||||
game.ADDITIONAL_ELEMENTS = gameAdditional.ToArray();
|
||||
games.Add(game);
|
||||
}
|
||||
|
||||
game = null;
|
||||
files.Clear();
|
||||
gameAdditional.Clear();
|
||||
break;
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're at the root
|
||||
if (reader.RowType == CmpRowType.TopLevel)
|
||||
{
|
||||
lastTopLevel = reader.TopLevel;
|
||||
switch (reader.TopLevel)
|
||||
{
|
||||
case "doscenter":
|
||||
dat.DosCenter = new Models.DosCenter.DosCenter();
|
||||
break;
|
||||
case "game":
|
||||
game = new Game();
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in the doscenter block
|
||||
else if (reader.TopLevel == "doscenter" && reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
dat.DosCenter ??= new Models.DosCenter.DosCenter();
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name:":
|
||||
dat.DosCenter.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
case "description:":
|
||||
dat.DosCenter.Description = reader.Standalone?.Value;
|
||||
break;
|
||||
case "version:":
|
||||
dat.DosCenter.Version = reader.Standalone?.Value;
|
||||
break;
|
||||
case "date:":
|
||||
dat.DosCenter.Date = reader.Standalone?.Value;
|
||||
break;
|
||||
case "author:":
|
||||
dat.DosCenter.Author = reader.Standalone?.Value;
|
||||
break;
|
||||
case "homepage:":
|
||||
dat.DosCenter.Homepage = reader.Standalone?.Value;
|
||||
break;
|
||||
case "comment:":
|
||||
dat.DosCenter.Comment = reader.Standalone?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
headerAdditional.Add(item: reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a game block
|
||||
else if (reader.TopLevel == "game" && reader.RowType == CmpRowType.Standalone)
|
||||
{
|
||||
// Create the block if we haven't already
|
||||
game ??= new Game();
|
||||
|
||||
switch (reader.Standalone?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
game.Name = reader.Standalone?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(item: reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a file block
|
||||
else if (reader.TopLevel == "game" && reader.RowType == CmpRowType.Internal)
|
||||
{
|
||||
// If we have an unknown type, log it
|
||||
if (reader.InternalName != "file")
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
gameAdditional.Add(reader.CurrentLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the file and add to the list
|
||||
var file = CreateFile(reader);
|
||||
if (file != null)
|
||||
files.Add(file);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(item: reader.CurrentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.Game = games.ToArray();
|
||||
dat.ADDITIONAL_ELEMENTS = additional.ToArray();
|
||||
return dat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a File object from the current reader context
|
||||
/// </summary>
|
||||
/// <param name="reader">ClrMameProReader representing the metadata file</param>
|
||||
/// <returns>File object created from the reader context</returns>
|
||||
private static Models.DosCenter.File? CreateFile(ClrMameProReader reader)
|
||||
{
|
||||
if (reader.Internal == null)
|
||||
return null;
|
||||
|
||||
var itemAdditional = new List<string>();
|
||||
var file = new Models.DosCenter.File();
|
||||
foreach (var kvp in reader.Internal)
|
||||
{
|
||||
switch (kvp.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "name":
|
||||
file.Name = kvp.Value;
|
||||
break;
|
||||
case "size":
|
||||
file.Size = kvp.Value;
|
||||
break;
|
||||
case "crc":
|
||||
file.CRC = kvp.Value;
|
||||
break;
|
||||
case "date":
|
||||
file.Date = kvp.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
itemAdditional.Add(item: reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
file.ADDITIONAL_ELEMENTS = itemAdditional.ToArray();
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.EverdriveSMDB;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class EverdriveSMDB : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new EverdriveSMDB();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
Header = false,
|
||||
Separator = '\t',
|
||||
VerifyFieldCount = false,
|
||||
};
|
||||
var dat = new MetadataFile();
|
||||
|
||||
// Loop through the rows and parse out values
|
||||
var rows = new List<Row>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine() || reader.Line == null)
|
||||
break;
|
||||
|
||||
// Parse the line into a row
|
||||
var row = new Row
|
||||
{
|
||||
SHA256 = reader.Line[0],
|
||||
Name = reader.Line[1],
|
||||
SHA1 = reader.Line[2],
|
||||
MD5 = reader.Line[3],
|
||||
CRC32 = reader.Line[4],
|
||||
};
|
||||
|
||||
// If we have the size field
|
||||
if (reader.Line.Count > 5)
|
||||
row.Size = reader.Line[5];
|
||||
|
||||
// If we have additional fields
|
||||
if (reader.Line.Count > 6)
|
||||
row.ADDITIONAL_ELEMENTS = reader.Line.Skip(5).ToArray();
|
||||
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
// Assign the rows to the Dat and return
|
||||
dat.Row = rows.ToArray();
|
||||
return dat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,751 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.GCF;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class GCF : IStreamSerializer<Models.GCF.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.GCF.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new GCF();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.GCF.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life Game Cache to fill
|
||||
var file = new Models.GCF.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entry Header
|
||||
|
||||
// Try to parse the block entry header
|
||||
var blockEntryHeader = ParseBlockEntryHeader(data);
|
||||
if (blockEntryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache block entry header
|
||||
file.BlockEntryHeader = blockEntryHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entries
|
||||
|
||||
// Create the block entry array
|
||||
file.BlockEntries = new BlockEntry[blockEntryHeader.BlockCount];
|
||||
|
||||
// Try to parse the block entries
|
||||
for (int i = 0; i < blockEntryHeader.BlockCount; i++)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
file.BlockEntries[i] = blockEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fragmentation Map Header
|
||||
|
||||
// Try to parse the fragmentation map header
|
||||
var fragmentationMapHeader = ParseFragmentationMapHeader(data);
|
||||
if (fragmentationMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache fragmentation map header
|
||||
file.FragmentationMapHeader = fragmentationMapHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fragmentation Maps
|
||||
|
||||
// Create the fragmentation map array
|
||||
file.FragmentationMaps = new FragmentationMap[fragmentationMapHeader.BlockCount];
|
||||
|
||||
// Try to parse the fragmentation maps
|
||||
for (int i = 0; i < fragmentationMapHeader.BlockCount; i++)
|
||||
{
|
||||
var fragmentationMap = ParseFragmentationMap(data);
|
||||
file.FragmentationMaps[i] = fragmentationMap;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entry Map Header
|
||||
|
||||
if (header.MinorVersion < 6)
|
||||
{
|
||||
// Try to parse the block entry map header
|
||||
var blockEntryMapHeader = ParseBlockEntryMapHeader(data);
|
||||
if (blockEntryMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache block entry map header
|
||||
file.BlockEntryMapHeader = blockEntryMapHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Entry Maps
|
||||
|
||||
if (header.MinorVersion < 6)
|
||||
{
|
||||
// Create the block entry map array
|
||||
file.BlockEntryMaps = new BlockEntryMap[file.BlockEntryMapHeader!.BlockCount];
|
||||
|
||||
// Try to parse the block entry maps
|
||||
for (int i = 0; i < file.BlockEntryMapHeader.BlockCount; i++)
|
||||
{
|
||||
var blockEntryMap = ParseBlockEntryMap(data);
|
||||
file.BlockEntryMaps[i] = blockEntryMap;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Directory Header
|
||||
|
||||
// Try to parse the directory header
|
||||
var directoryHeader = ParseDirectoryHeader(data);
|
||||
if (directoryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache directory header
|
||||
file.DirectoryHeader = directoryHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Entries
|
||||
|
||||
// Create the directory entry array
|
||||
file.DirectoryEntries = new DirectoryEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.DirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Names
|
||||
|
||||
if (directoryHeader.NameSize > 0)
|
||||
{
|
||||
// Get the current offset for adjustment
|
||||
long directoryNamesStart = data.Position;
|
||||
|
||||
// Get the ending offset
|
||||
long directoryNamesEnd = data.Position + directoryHeader.NameSize;
|
||||
|
||||
// Create the string dictionary
|
||||
file.DirectoryNames = new Dictionary<long, string?>();
|
||||
|
||||
// Loop and read the null-terminated strings
|
||||
while (data.Position < directoryNamesEnd)
|
||||
{
|
||||
long nameOffset = data.Position - directoryNamesStart;
|
||||
string? directoryName = data.ReadString(Encoding.ASCII);
|
||||
if (data.Position > directoryNamesEnd)
|
||||
{
|
||||
data.Seek(-directoryName?.Length ?? 0, SeekOrigin.Current);
|
||||
byte[]? endingData = data.ReadBytes((int)(directoryNamesEnd - data.Position));
|
||||
if (endingData != null)
|
||||
directoryName = Encoding.ASCII.GetString(endingData);
|
||||
else
|
||||
directoryName = null;
|
||||
}
|
||||
|
||||
file.DirectoryNames[nameOffset] = directoryName;
|
||||
}
|
||||
|
||||
// Loop and assign to entries
|
||||
foreach (var directoryEntry in file.DirectoryEntries)
|
||||
{
|
||||
if (directoryEntry != null)
|
||||
directoryEntry.Name = file.DirectoryNames[directoryEntry.NameOffset];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 1 Entries
|
||||
|
||||
// Create the directory info 1 entry array
|
||||
file.DirectoryInfo1Entries = new DirectoryInfo1Entry[directoryHeader.Info1Count];
|
||||
|
||||
// Try to parse the directory info 1 entries
|
||||
for (int i = 0; i < directoryHeader.Info1Count; i++)
|
||||
{
|
||||
var directoryInfo1Entry = ParseDirectoryInfo1Entry(data);
|
||||
file.DirectoryInfo1Entries[i] = directoryInfo1Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 2 Entries
|
||||
|
||||
// Create the directory info 2 entry array
|
||||
file.DirectoryInfo2Entries = new DirectoryInfo2Entry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory info 2 entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryInfo2Entry = ParseDirectoryInfo2Entry(data);
|
||||
file.DirectoryInfo2Entries[i] = directoryInfo2Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Copy Entries
|
||||
|
||||
// Create the directory copy entry array
|
||||
file.DirectoryCopyEntries = new DirectoryCopyEntry[directoryHeader.CopyCount];
|
||||
|
||||
// Try to parse the directory copy entries
|
||||
for (int i = 0; i < directoryHeader.CopyCount; i++)
|
||||
{
|
||||
var directoryCopyEntry = ParseDirectoryCopyEntry(data);
|
||||
file.DirectoryCopyEntries[i] = directoryCopyEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Local Entries
|
||||
|
||||
// Create the directory local entry array
|
||||
file.DirectoryLocalEntries = new DirectoryLocalEntry[directoryHeader.LocalCount];
|
||||
|
||||
// Try to parse the directory local entries
|
||||
for (int i = 0; i < directoryHeader.LocalCount; i++)
|
||||
{
|
||||
var directoryLocalEntry = ParseDirectoryLocalEntry(data);
|
||||
file.DirectoryLocalEntries[i] = directoryLocalEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of directory section, just in case
|
||||
data.Seek(initialOffset + directoryHeader.DirectorySize, SeekOrigin.Begin);
|
||||
|
||||
#region Directory Map Header
|
||||
|
||||
if (header.MinorVersion >= 5)
|
||||
{
|
||||
// Try to parse the directory map header
|
||||
var directoryMapHeader = ParseDirectoryMapHeader(data);
|
||||
if (directoryMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache directory map header
|
||||
file.DirectoryMapHeader = directoryMapHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Map Entries
|
||||
|
||||
// Create the directory map entry array
|
||||
file.DirectoryMapEntries = new DirectoryMapEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory map entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryMapEntry = ParseDirectoryMapEntry(data);
|
||||
file.DirectoryMapEntries[i] = directoryMapEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Header
|
||||
|
||||
// Try to parse the checksum header
|
||||
var checksumHeader = ParseChecksumHeader(data);
|
||||
if (checksumHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum header
|
||||
file.ChecksumHeader = checksumHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Checksum Map Header
|
||||
|
||||
// Try to parse the checksum map header
|
||||
var checksumMapHeader = ParseChecksumMapHeader(data);
|
||||
if (checksumMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum map header
|
||||
file.ChecksumMapHeader = checksumMapHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Map Entries
|
||||
|
||||
// Create the checksum map entry array
|
||||
file.ChecksumMapEntries = new ChecksumMapEntry[checksumMapHeader.ItemCount];
|
||||
|
||||
// Try to parse the checksum map entries
|
||||
for (int i = 0; i < checksumMapHeader.ItemCount; i++)
|
||||
{
|
||||
var checksumMapEntry = ParseChecksumMapEntry(data);
|
||||
file.ChecksumMapEntries[i] = checksumMapEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Entries
|
||||
|
||||
// Create the checksum entry array
|
||||
file.ChecksumEntries = new ChecksumEntry[checksumMapHeader.ChecksumCount];
|
||||
|
||||
// Try to parse the checksum entries
|
||||
for (int i = 0; i < checksumMapHeader.ChecksumCount; i++)
|
||||
{
|
||||
var checksumEntry = ParseChecksumEntry(data);
|
||||
file.ChecksumEntries[i] = checksumEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of checksum section, just in case
|
||||
data.Seek(initialOffset + checksumHeader.ChecksumSize, SeekOrigin.Begin);
|
||||
|
||||
#region Data Block Header
|
||||
|
||||
// Try to parse the data block header
|
||||
var dataBlockHeader = ParseDataBlockHeader(data, header.MinorVersion);
|
||||
if (dataBlockHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache data block header
|
||||
file.DataBlockHeader = dataBlockHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.Dummy0 = data.ReadUInt32();
|
||||
if (header.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
header.MajorVersion = data.ReadUInt32();
|
||||
if (header.MajorVersion != 0x00000001)
|
||||
return null;
|
||||
|
||||
header.MinorVersion = data.ReadUInt32();
|
||||
if (header.MinorVersion != 3 && header.MinorVersion != 5 && header.MinorVersion != 6)
|
||||
return null;
|
||||
|
||||
header.CacheID = data.ReadUInt32();
|
||||
header.LastVersionPlayed = data.ReadUInt32();
|
||||
header.Dummy1 = data.ReadUInt32();
|
||||
header.Dummy2 = data.ReadUInt32();
|
||||
header.FileSize = data.ReadUInt32();
|
||||
header.BlockSize = data.ReadUInt32();
|
||||
header.BlockCount = data.ReadUInt32();
|
||||
header.Dummy3 = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry header on success, null on error</returns>
|
||||
private static BlockEntryHeader ParseBlockEntryHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntryHeader blockEntryHeader = new BlockEntryHeader();
|
||||
|
||||
blockEntryHeader.BlockCount = data.ReadUInt32();
|
||||
blockEntryHeader.BlocksUsed = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy0 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy1 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy2 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy3 = data.ReadUInt32();
|
||||
blockEntryHeader.Dummy4 = data.ReadUInt32();
|
||||
blockEntryHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return blockEntryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry on success, null on error</returns>
|
||||
private static BlockEntry ParseBlockEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntry blockEntry = new BlockEntry();
|
||||
|
||||
blockEntry.EntryFlags = data.ReadUInt32();
|
||||
blockEntry.FileDataOffset = data.ReadUInt32();
|
||||
blockEntry.FileDataSize = data.ReadUInt32();
|
||||
blockEntry.FirstDataBlockIndex = data.ReadUInt32();
|
||||
blockEntry.NextBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntry.PreviousBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return blockEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache fragmentation map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache fragmentation map header on success, null on error</returns>
|
||||
private static FragmentationMapHeader ParseFragmentationMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FragmentationMapHeader fragmentationMapHeader = new FragmentationMapHeader();
|
||||
|
||||
fragmentationMapHeader.BlockCount = data.ReadUInt32();
|
||||
fragmentationMapHeader.FirstUnusedEntry = data.ReadUInt32();
|
||||
fragmentationMapHeader.Terminator = data.ReadUInt32();
|
||||
fragmentationMapHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return fragmentationMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache fragmentation map
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache fragmentation map on success, null on error</returns>
|
||||
private static FragmentationMap ParseFragmentationMap(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FragmentationMap fragmentationMap = new FragmentationMap();
|
||||
|
||||
fragmentationMap.NextDataBlockIndex = data.ReadUInt32();
|
||||
|
||||
return fragmentationMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry map header on success, null on error</returns>
|
||||
private static BlockEntryMapHeader ParseBlockEntryMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntryMapHeader blockEntryMapHeader = new BlockEntryMapHeader();
|
||||
|
||||
blockEntryMapHeader.BlockCount = data.ReadUInt32();
|
||||
blockEntryMapHeader.FirstBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntryMapHeader.LastBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntryMapHeader.Dummy0 = data.ReadUInt32();
|
||||
blockEntryMapHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return blockEntryMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache block entry map
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache block entry map on success, null on error</returns>
|
||||
private static BlockEntryMap ParseBlockEntryMap(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
BlockEntryMap blockEntryMap = new BlockEntryMap();
|
||||
|
||||
blockEntryMap.PreviousBlockEntryIndex = data.ReadUInt32();
|
||||
blockEntryMap.NextBlockEntryIndex = data.ReadUInt32();
|
||||
|
||||
return blockEntryMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory header on success, null on error</returns>
|
||||
private static DirectoryHeader ParseDirectoryHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryHeader directoryHeader = new DirectoryHeader();
|
||||
|
||||
directoryHeader.Dummy0 = data.ReadUInt32();
|
||||
directoryHeader.CacheID = data.ReadUInt32();
|
||||
directoryHeader.LastVersionPlayed = data.ReadUInt32();
|
||||
directoryHeader.ItemCount = data.ReadUInt32();
|
||||
directoryHeader.FileCount = data.ReadUInt32();
|
||||
directoryHeader.Dummy1 = data.ReadUInt32();
|
||||
directoryHeader.DirectorySize = data.ReadUInt32();
|
||||
directoryHeader.NameSize = data.ReadUInt32();
|
||||
directoryHeader.Info1Count = data.ReadUInt32();
|
||||
directoryHeader.CopyCount = data.ReadUInt32();
|
||||
directoryHeader.LocalCount = data.ReadUInt32();
|
||||
directoryHeader.Dummy2 = data.ReadUInt32();
|
||||
directoryHeader.Dummy3 = data.ReadUInt32();
|
||||
directoryHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return directoryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.NameOffset = data.ReadUInt32();
|
||||
directoryEntry.ItemSize = data.ReadUInt32();
|
||||
directoryEntry.ChecksumIndex = data.ReadUInt32();
|
||||
directoryEntry.DirectoryFlags = (HL_GCF_FLAG)data.ReadUInt32();
|
||||
directoryEntry.ParentIndex = data.ReadUInt32();
|
||||
directoryEntry.NextIndex = data.ReadUInt32();
|
||||
directoryEntry.FirstIndex = data.ReadUInt32();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory info 1 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory info 1 entry on success, null on error</returns>
|
||||
private static DirectoryInfo1Entry ParseDirectoryInfo1Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo1Entry directoryInfo1Entry = new DirectoryInfo1Entry();
|
||||
|
||||
directoryInfo1Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo1Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory info 2 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory info 2 entry on success, null on error</returns>
|
||||
private static DirectoryInfo2Entry ParseDirectoryInfo2Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo2Entry directoryInfo2Entry = new DirectoryInfo2Entry();
|
||||
|
||||
directoryInfo2Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo2Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory copy entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory copy entry on success, null on error</returns>
|
||||
private static DirectoryCopyEntry ParseDirectoryCopyEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryCopyEntry directoryCopyEntry = new DirectoryCopyEntry();
|
||||
|
||||
directoryCopyEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryCopyEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory local entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory local entry on success, null on error</returns>
|
||||
private static DirectoryLocalEntry ParseDirectoryLocalEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryLocalEntry directoryLocalEntry = new DirectoryLocalEntry();
|
||||
|
||||
directoryLocalEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryLocalEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory map header on success, null on error</returns>
|
||||
private static DirectoryMapHeader? ParseDirectoryMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryMapHeader directoryMapHeader = new DirectoryMapHeader();
|
||||
|
||||
directoryMapHeader.Dummy0 = data.ReadUInt32();
|
||||
if (directoryMapHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
directoryMapHeader.Dummy1 = data.ReadUInt32();
|
||||
if (directoryMapHeader.Dummy1 != 0x00000000)
|
||||
return null;
|
||||
|
||||
return directoryMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache directory map entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache directory map entry on success, null on error</returns>
|
||||
private static DirectoryMapEntry ParseDirectoryMapEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryMapEntry directoryMapEntry = new DirectoryMapEntry();
|
||||
|
||||
directoryMapEntry.FirstBlockIndex = data.ReadUInt32();
|
||||
|
||||
return directoryMapEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum header on success, null on error</returns>
|
||||
private static ChecksumHeader? ParseChecksumHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumHeader checksumHeader = new ChecksumHeader();
|
||||
|
||||
checksumHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumHeader.ChecksumSize = data.ReadUInt32();
|
||||
|
||||
return checksumHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum map header on success, null on error</returns>
|
||||
private static ChecksumMapHeader? ParseChecksumMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapHeader checksumMapHeader = new ChecksumMapHeader();
|
||||
|
||||
checksumMapHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy0 != 0x14893721)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.Dummy1 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy1 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.ItemCount = data.ReadUInt32();
|
||||
checksumMapHeader.ChecksumCount = data.ReadUInt32();
|
||||
|
||||
return checksumMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum map entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum map entry on success, null on error</returns>
|
||||
private static ChecksumMapEntry ParseChecksumMapEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapEntry checksumMapEntry = new ChecksumMapEntry();
|
||||
|
||||
checksumMapEntry.ChecksumCount = data.ReadUInt32();
|
||||
checksumMapEntry.FirstChecksumIndex = data.ReadUInt32();
|
||||
|
||||
return checksumMapEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache checksum entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Game Cache checksum entry on success, null on error</returns>
|
||||
private static ChecksumEntry ParseChecksumEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumEntry checksumEntry = new ChecksumEntry();
|
||||
|
||||
checksumEntry.Checksum = data.ReadUInt32();
|
||||
|
||||
return checksumEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Game Cache data block header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="minorVersion">Minor version field from the header</param>
|
||||
/// <returns>Filled Half-Life Game Cache data block header on success, null on error</returns>
|
||||
private static DataBlockHeader ParseDataBlockHeader(Stream data, uint minorVersion)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DataBlockHeader dataBlockHeader = new DataBlockHeader();
|
||||
|
||||
// In version 3 the DataBlockHeader is missing the LastVersionPlayed field.
|
||||
if (minorVersion >= 5)
|
||||
dataBlockHeader.LastVersionPlayed = data.ReadUInt32();
|
||||
|
||||
dataBlockHeader.BlockCount = data.ReadUInt32();
|
||||
dataBlockHeader.BlockSize = data.ReadUInt32();
|
||||
dataBlockHeader.FirstBlockOffset = data.ReadUInt32();
|
||||
dataBlockHeader.BlocksUsed = data.ReadUInt32();
|
||||
dataBlockHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return dataBlockHeader;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.Models.Hashfile;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Hashfile : IStreamSerializer<Models.Hashfile.Hashfile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.Hashfile.Hashfile? DeserializeStream(Stream? data, Hash hash = Hash.CRC)
|
||||
{
|
||||
var deserializer = new Hashfile();
|
||||
return deserializer.Deserialize(data, hash);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.Hashfile.Hashfile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, Hash.CRC);
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
public Models.Hashfile.Hashfile? Deserialize(Stream? data, Hash hash)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new StreamReader(data);
|
||||
var dat = new Models.Hashfile.Hashfile();
|
||||
var additional = new List<string>();
|
||||
|
||||
// Loop through the rows and parse out values
|
||||
var hashes = new List<object>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// Read and split the line
|
||||
string? line = reader.ReadLine();
|
||||
#if NETFRAMEWORK || NETCOREAPP3_1
|
||||
string[]? lineParts = line?.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
#else
|
||||
string[]? lineParts = line?.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
#endif
|
||||
if (lineParts == null)
|
||||
continue;
|
||||
|
||||
// Parse the line into a hash
|
||||
switch (hash)
|
||||
{
|
||||
case Hash.CRC:
|
||||
var sfv = new SFV
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Take(lineParts.Length - 1).ToArray()),
|
||||
Hash = lineParts[lineParts.Length - 1],
|
||||
#else
|
||||
File = string.Join(" ", lineParts[..^1]),
|
||||
Hash = lineParts[^1],
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sfv);
|
||||
break;
|
||||
case Hash.MD5:
|
||||
var md5 = new MD5
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(md5);
|
||||
break;
|
||||
case Hash.SHA1:
|
||||
var sha1 = new SHA1
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha1);
|
||||
break;
|
||||
case Hash.SHA256:
|
||||
var sha256 = new SHA256
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha256);
|
||||
break;
|
||||
case Hash.SHA384:
|
||||
var sha384 = new SHA384
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha384);
|
||||
break;
|
||||
case Hash.SHA512:
|
||||
var sha512 = new SHA512
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(sha512);
|
||||
break;
|
||||
case Hash.SpamSum:
|
||||
var spamSum = new SpamSum
|
||||
{
|
||||
Hash = lineParts[0],
|
||||
#if NETFRAMEWORK
|
||||
File = string.Join(" ", lineParts.Skip(1).ToArray()),
|
||||
#else
|
||||
File = string.Join(" ", lineParts[1..]),
|
||||
#endif
|
||||
};
|
||||
hashes.Add(spamSum);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the hashes to the hashfile and return
|
||||
switch (hash)
|
||||
{
|
||||
case Hash.CRC:
|
||||
dat.SFV = hashes.Cast<SFV>().ToArray();
|
||||
break;
|
||||
case Hash.MD5:
|
||||
dat.MD5 = hashes.Cast<MD5>().ToArray();
|
||||
break;
|
||||
case Hash.SHA1:
|
||||
dat.SHA1 = hashes.Cast<SHA1>().ToArray();
|
||||
break;
|
||||
case Hash.SHA256:
|
||||
dat.SHA256 = hashes.Cast<SHA256>().ToArray();
|
||||
break;
|
||||
case Hash.SHA384:
|
||||
dat.SHA384 = hashes.Cast<SHA384>().ToArray();
|
||||
break;
|
||||
case Hash.SHA512:
|
||||
dat.SHA512 = hashes.Cast<SHA512>().ToArray();
|
||||
break;
|
||||
case Hash.SpamSum:
|
||||
dat.SpamSum = hashes.Cast<SpamSum>().ToArray();
|
||||
break;
|
||||
}
|
||||
dat.ADDITIONAL_ELEMENTS = [.. additional];
|
||||
return dat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class IRD : IStreamSerializer<Models.IRD.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.IRD.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new IRD();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.IRD.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 media key block to fill
|
||||
var ird = new Models.IRD.File();
|
||||
|
||||
ird.Magic = data.ReadBytes(4);
|
||||
if (ird.Magic == null)
|
||||
return null;
|
||||
|
||||
string magic = Encoding.ASCII.GetString(ird.Magic);
|
||||
if (magic != "3IRD")
|
||||
return null;
|
||||
|
||||
ird.Version = data.ReadByteValue();
|
||||
if (ird.Version < 6)
|
||||
return null;
|
||||
|
||||
var titleId = data.ReadBytes(9);
|
||||
if (titleId == null)
|
||||
return null;
|
||||
|
||||
ird.TitleID = Encoding.ASCII.GetString(titleId);
|
||||
|
||||
ird.TitleLength = data.ReadByteValue();
|
||||
var title = data.ReadBytes(ird.TitleLength);
|
||||
if (title == null)
|
||||
return null;
|
||||
|
||||
ird.Title = Encoding.ASCII.GetString(title);
|
||||
|
||||
var systemVersion = data.ReadBytes(4);
|
||||
if (systemVersion == null)
|
||||
return null;
|
||||
|
||||
ird.SystemVersion = Encoding.ASCII.GetString(systemVersion);
|
||||
|
||||
var gameVersion = data.ReadBytes(5);
|
||||
if (gameVersion == null)
|
||||
return null;
|
||||
|
||||
ird.GameVersion = Encoding.ASCII.GetString(gameVersion);
|
||||
|
||||
var appVersion = data.ReadBytes(5);
|
||||
if (appVersion == null)
|
||||
return null;
|
||||
|
||||
ird.AppVersion = Encoding.ASCII.GetString(appVersion);
|
||||
|
||||
if (ird.Version == 7)
|
||||
ird.UID = data.ReadUInt32();
|
||||
|
||||
ird.HeaderLength = data.ReadByteValue();
|
||||
ird.Header = data.ReadBytes((int)ird.HeaderLength);
|
||||
ird.FooterLength = data.ReadByteValue();
|
||||
ird.Footer = data.ReadBytes((int)ird.FooterLength);
|
||||
|
||||
ird.RegionCount = data.ReadByteValue();
|
||||
ird.RegionHashes = new byte[ird.RegionCount][];
|
||||
for (int i = 0; i < ird.RegionCount; i++)
|
||||
{
|
||||
ird.RegionHashes[i] = data.ReadBytes(16) ?? [];
|
||||
}
|
||||
|
||||
ird.FileCount = data.ReadByteValue();
|
||||
ird.FileKeys = new ulong[ird.FileCount];
|
||||
ird.FileHashes = new byte[ird.FileCount][];
|
||||
for (int i = 0; i < ird.FileCount; i++)
|
||||
{
|
||||
ird.FileKeys[i] = data.ReadUInt64();
|
||||
ird.FileHashes[i] = data.ReadBytes(16) ?? [];
|
||||
}
|
||||
|
||||
ird.ExtraConfig = data.ReadUInt16();
|
||||
ird.Attachments = data.ReadUInt16();
|
||||
|
||||
if (ird.Version >= 9)
|
||||
ird.PIC = data.ReadBytes(115);
|
||||
|
||||
ird.Data1Key = data.ReadBytes(16);
|
||||
ird.Data2Key = data.ReadBytes(16);
|
||||
|
||||
if (ird.Version < 9)
|
||||
ird.PIC = data.ReadBytes(115);
|
||||
|
||||
if (ird.Version > 7)
|
||||
ird.UID = data.ReadUInt32();
|
||||
|
||||
ird.CRC = data.ReadUInt32();
|
||||
|
||||
return ird;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,787 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.InstallShieldCabinet;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.InstallShieldCabinet.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
// TODO: Add multi-cabinet reading
|
||||
public partial class InstallShieldCabinet : IStreamSerializer<Cabinet>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Cabinet? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new InstallShieldCabinet();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cabinet? 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 cabinet to fill
|
||||
var cabinet = new Cabinet();
|
||||
|
||||
#region Common Header
|
||||
|
||||
// Try to parse the cabinet header
|
||||
var commonHeader = ParseCommonHeader(data);
|
||||
if (commonHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the cabinet header
|
||||
cabinet.CommonHeader = commonHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Volume Header
|
||||
|
||||
// Try to parse the volume header
|
||||
var volumeHeader = ParseVolumeHeader(data, GetMajorVersion(commonHeader));
|
||||
if (volumeHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the volume header
|
||||
cabinet.VolumeHeader = volumeHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Descriptor
|
||||
|
||||
// Get the descriptor offset
|
||||
uint descriptorOffset = commonHeader.DescriptorOffset;
|
||||
if (descriptorOffset < 0 || descriptorOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the descriptor
|
||||
data.Seek(descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the descriptor
|
||||
var descriptor = ParseDescriptor(data);
|
||||
if (descriptor == null)
|
||||
return null;
|
||||
|
||||
// Set the descriptor
|
||||
cabinet.Descriptor = descriptor;
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Descriptor Offsets
|
||||
|
||||
// Get the file table offset
|
||||
uint fileTableOffset = commonHeader.DescriptorOffset + descriptor.FileTableOffset;
|
||||
if (fileTableOffset < 0 || fileTableOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the file table
|
||||
data.Seek(fileTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the number of file table items
|
||||
uint fileTableItems;
|
||||
if (GetMajorVersion(commonHeader) <= 5)
|
||||
fileTableItems = descriptor.DirectoryCount + descriptor.FileCount;
|
||||
else
|
||||
fileTableItems = descriptor.DirectoryCount;
|
||||
|
||||
// Create and fill the file table
|
||||
cabinet.FileDescriptorOffsets = new uint[fileTableItems];
|
||||
for (int i = 0; i < cabinet.FileDescriptorOffsets.Length; i++)
|
||||
{
|
||||
cabinet.FileDescriptorOffsets[i] = data.ReadUInt32();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Descriptors
|
||||
|
||||
// Create and fill the directory descriptors
|
||||
cabinet.DirectoryNames = new string[descriptor.DirectoryCount];
|
||||
for (int i = 0; i < descriptor.DirectoryCount; i++)
|
||||
{
|
||||
// Get the directory descriptor offset
|
||||
uint offset = descriptorOffset
|
||||
+ descriptor.FileTableOffset
|
||||
+ cabinet.FileDescriptorOffsets[i];
|
||||
|
||||
// If we have an invalid offset
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the file descriptor offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the file descriptor
|
||||
string? directoryName = ParseDirectoryName(data, GetMajorVersion(commonHeader));
|
||||
if (directoryName != null)
|
||||
cabinet.DirectoryNames[i] = directoryName;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Descriptors
|
||||
|
||||
// Create and fill the file descriptors
|
||||
cabinet.FileDescriptors = new FileDescriptor[descriptor.FileCount];
|
||||
for (int i = 0; i < descriptor.FileCount; i++)
|
||||
{
|
||||
// Get the file descriptor offset
|
||||
uint offset;
|
||||
if (GetMajorVersion(commonHeader) <= 5)
|
||||
{
|
||||
offset = descriptorOffset
|
||||
+ descriptor.FileTableOffset
|
||||
+ cabinet.FileDescriptorOffsets[descriptor.DirectoryCount + i];
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = descriptorOffset
|
||||
+ descriptor.FileTableOffset
|
||||
+ descriptor.FileTableOffset2
|
||||
+ (uint)(i * 0x57);
|
||||
}
|
||||
|
||||
// If we have an invalid offset
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the file descriptor offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the file descriptor
|
||||
FileDescriptor fileDescriptor = ParseFileDescriptor(data, GetMajorVersion(commonHeader), descriptorOffset + descriptor.FileTableOffset);
|
||||
cabinet.FileDescriptors[i] = fileDescriptor;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Group Offsets
|
||||
|
||||
// Create and fill the file group offsets
|
||||
cabinet.FileGroupOffsets = new Dictionary<long, OffsetList?>();
|
||||
for (int i = 0; i < (descriptor.FileGroupOffsets?.Length ?? 0); i++)
|
||||
{
|
||||
// Get the file group offset
|
||||
uint offset = descriptor.FileGroupOffsets![i];
|
||||
if (offset == 0)
|
||||
continue;
|
||||
|
||||
// Adjust the file group offset
|
||||
offset += commonHeader.DescriptorOffset;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the file group offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
OffsetList offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.FileGroupOffsets[descriptor.FileGroupOffsets[i]] = offsetList;
|
||||
|
||||
// If we have a nonzero next offset
|
||||
uint nextOffset = offsetList.NextOffset;
|
||||
while (nextOffset != 0)
|
||||
{
|
||||
// Get the next offset to read
|
||||
uint internalOffset = nextOffset + commonHeader.DescriptorOffset;
|
||||
|
||||
// Seek to the file group offset
|
||||
data.Seek(internalOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.FileGroupOffsets[nextOffset] = offsetList;
|
||||
|
||||
// Set the next offset
|
||||
nextOffset = offsetList.NextOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Groups
|
||||
|
||||
// Create the file groups array
|
||||
cabinet.FileGroups = new FileGroup[cabinet.FileGroupOffsets.Count];
|
||||
|
||||
// Create and fill the file groups
|
||||
int fileGroupId = 0;
|
||||
foreach (var kvp in cabinet.FileGroupOffsets)
|
||||
{
|
||||
// Get the offset
|
||||
OffsetList? list = kvp.Value;
|
||||
if (list == null)
|
||||
{
|
||||
fileGroupId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have an invalid offset
|
||||
if (list.DescriptorOffset <= 0)
|
||||
{
|
||||
fileGroupId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/// Seek to the file group
|
||||
data.Seek(list.DescriptorOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the file group
|
||||
var fileGroup = ParseFileGroup(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
if (fileGroup == null)
|
||||
return null;
|
||||
|
||||
// Add the file group
|
||||
cabinet.FileGroups[fileGroupId++] = fileGroup;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Offsets
|
||||
|
||||
// Create and fill the component offsets
|
||||
cabinet.ComponentOffsets = new Dictionary<long, OffsetList?>();
|
||||
for (int i = 0; i < (descriptor.ComponentOffsets?.Length ?? 0); i++)
|
||||
{
|
||||
// Get the component offset
|
||||
uint offset = descriptor.ComponentOffsets![i];
|
||||
if (offset == 0)
|
||||
continue;
|
||||
|
||||
// Adjust the component offset
|
||||
offset += commonHeader.DescriptorOffset;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the component offset
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
OffsetList offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.ComponentOffsets[descriptor.ComponentOffsets[i]] = offsetList;
|
||||
|
||||
// If we have a nonzero next offset
|
||||
uint nextOffset = offsetList.NextOffset;
|
||||
while (nextOffset != 0)
|
||||
{
|
||||
// Get the next offset to read
|
||||
uint internalOffset = nextOffset + commonHeader.DescriptorOffset;
|
||||
|
||||
// Seek to the file group offset
|
||||
data.Seek(internalOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create and add the offset
|
||||
offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
cabinet.ComponentOffsets[nextOffset] = offsetList;
|
||||
|
||||
// Set the next offset
|
||||
nextOffset = offsetList.NextOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Components
|
||||
|
||||
// Create the components array
|
||||
cabinet.Components = new Component[cabinet.ComponentOffsets.Count];
|
||||
|
||||
// Create and fill the components
|
||||
int componentId = 0;
|
||||
foreach (KeyValuePair<long, OffsetList?> kvp in cabinet.ComponentOffsets)
|
||||
{
|
||||
// Get the offset
|
||||
OffsetList? list = kvp.Value;
|
||||
if (list == null)
|
||||
{
|
||||
componentId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have an invalid offset
|
||||
if (list.DescriptorOffset <= 0)
|
||||
{
|
||||
componentId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seek to the component
|
||||
data.Seek(list.DescriptorOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the component
|
||||
var component = ParseComponent(data, GetMajorVersion(commonHeader), descriptorOffset);
|
||||
if (component == null)
|
||||
return null;
|
||||
|
||||
// Add the component
|
||||
cabinet.Components[componentId++] = component;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// TODO: Parse setup types
|
||||
|
||||
return cabinet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a common header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled common header on success, null on error</returns>
|
||||
private static CommonHeader? ParseCommonHeader(Stream data)
|
||||
{
|
||||
CommonHeader commonHeader = new CommonHeader();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
commonHeader.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (commonHeader.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
commonHeader.Version = data.ReadUInt32();
|
||||
commonHeader.VolumeInfo = data.ReadUInt32();
|
||||
commonHeader.DescriptorOffset = data.ReadUInt32();
|
||||
commonHeader.DescriptorSize = data.ReadUInt32();
|
||||
|
||||
return commonHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a volume header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <returns>Filled volume header on success, null on error</returns>
|
||||
private static VolumeHeader ParseVolumeHeader(Stream data, int majorVersion)
|
||||
{
|
||||
VolumeHeader volumeHeader = new VolumeHeader();
|
||||
|
||||
// Read the descriptor based on version
|
||||
if (majorVersion <= 5)
|
||||
{
|
||||
volumeHeader.DataOffset = data.ReadUInt32();
|
||||
_ = data.ReadBytes(0x04); // Skip 0x04 bytes, unknown data?
|
||||
volumeHeader.FirstFileIndex = data.ReadUInt32();
|
||||
volumeHeader.LastFileIndex = data.ReadUInt32();
|
||||
volumeHeader.FirstFileOffset = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeCompressed = data.ReadUInt32();
|
||||
volumeHeader.LastFileOffset = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeCompressed = data.ReadUInt32();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Should standard and high values be combined?
|
||||
volumeHeader.DataOffset = data.ReadUInt32();
|
||||
volumeHeader.DataOffsetHigh = data.ReadUInt32();
|
||||
volumeHeader.FirstFileIndex = data.ReadUInt32();
|
||||
volumeHeader.LastFileIndex = data.ReadUInt32();
|
||||
volumeHeader.FirstFileOffset = data.ReadUInt32();
|
||||
volumeHeader.FirstFileOffsetHigh = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeExpandedHigh = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeCompressed = data.ReadUInt32();
|
||||
volumeHeader.FirstFileSizeCompressedHigh = data.ReadUInt32();
|
||||
volumeHeader.LastFileOffset = data.ReadUInt32();
|
||||
volumeHeader.LastFileOffsetHigh = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeExpanded = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeExpandedHigh = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeCompressed = data.ReadUInt32();
|
||||
volumeHeader.LastFileSizeCompressedHigh = data.ReadUInt32();
|
||||
}
|
||||
|
||||
return volumeHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a descriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled descriptor on success, null on error</returns>
|
||||
private static Descriptor ParseDescriptor(Stream data)
|
||||
{
|
||||
Descriptor descriptor = new Descriptor();
|
||||
|
||||
descriptor.StringsOffset = data.ReadUInt32();
|
||||
descriptor.Reserved0 = data.ReadBytes(4);
|
||||
descriptor.ComponentListOffset = data.ReadUInt32();
|
||||
descriptor.FileTableOffset = data.ReadUInt32();
|
||||
descriptor.Reserved1 = data.ReadBytes(4);
|
||||
descriptor.FileTableSize = data.ReadUInt32();
|
||||
descriptor.FileTableSize2 = data.ReadUInt32();
|
||||
descriptor.DirectoryCount = data.ReadUInt16();
|
||||
descriptor.Reserved2 = data.ReadBytes(4);
|
||||
descriptor.Reserved3 = data.ReadBytes(2);
|
||||
descriptor.Reserved4 = data.ReadBytes(4);
|
||||
descriptor.FileCount = data.ReadUInt32();
|
||||
descriptor.FileTableOffset2 = data.ReadUInt32();
|
||||
descriptor.ComponentTableInfoCount = data.ReadUInt16();
|
||||
descriptor.ComponentTableOffset = data.ReadUInt32();
|
||||
descriptor.Reserved5 = data.ReadBytes(4);
|
||||
descriptor.Reserved6 = data.ReadBytes(4);
|
||||
|
||||
descriptor.FileGroupOffsets = new uint[MAX_FILE_GROUP_COUNT];
|
||||
for (int i = 0; i < descriptor.FileGroupOffsets.Length; i++)
|
||||
{
|
||||
descriptor.FileGroupOffsets[i] = data.ReadUInt32();
|
||||
}
|
||||
|
||||
descriptor.ComponentOffsets = new uint[MAX_COMPONENT_COUNT];
|
||||
for (int i = 0; i < descriptor.ComponentOffsets.Length; i++)
|
||||
{
|
||||
descriptor.ComponentOffsets[i] = data.ReadUInt32();
|
||||
}
|
||||
|
||||
descriptor.SetupTypesOffset = data.ReadUInt32();
|
||||
descriptor.SetupTableOffset = data.ReadUInt32();
|
||||
descriptor.Reserved7 = data.ReadBytes(4);
|
||||
descriptor.Reserved8 = data.ReadBytes(4);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an offset list
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled offset list on success, null on error</returns>
|
||||
private static OffsetList ParseOffsetList(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
OffsetList offsetList = new OffsetList();
|
||||
|
||||
offsetList.NameOffset = data.ReadUInt32();
|
||||
offsetList.DescriptorOffset = data.ReadUInt32();
|
||||
offsetList.NextOffset = data.ReadUInt32();
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
// Seek to the name offset
|
||||
data.Seek(offsetList.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
offsetList.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
offsetList.Name = data.ReadString(Encoding.ASCII);
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentOffset, SeekOrigin.Begin);
|
||||
|
||||
return offsetList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file group
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled file group on success, null on error</returns>
|
||||
private static FileGroup ParseFileGroup(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
FileGroup fileGroup = new FileGroup();
|
||||
|
||||
fileGroup.NameOffset = data.ReadUInt32();
|
||||
|
||||
fileGroup.ExpandedSize = data.ReadUInt32();
|
||||
fileGroup.Reserved0 = data.ReadBytes(4);
|
||||
fileGroup.CompressedSize = data.ReadUInt32();
|
||||
fileGroup.Reserved1 = data.ReadBytes(4);
|
||||
fileGroup.Reserved2 = data.ReadBytes(2);
|
||||
fileGroup.Attribute1 = data.ReadUInt16();
|
||||
fileGroup.Attribute2 = data.ReadUInt16();
|
||||
|
||||
// TODO: Figure out what data lives in this area for V5 and below
|
||||
if (majorVersion <= 5)
|
||||
data.Seek(0x36, SeekOrigin.Current);
|
||||
|
||||
fileGroup.FirstFile = data.ReadUInt32();
|
||||
fileGroup.LastFile = data.ReadUInt32();
|
||||
fileGroup.UnknownOffset = data.ReadUInt32();
|
||||
fileGroup.Var4Offset = data.ReadUInt32();
|
||||
fileGroup.Var1Offset = data.ReadUInt32();
|
||||
fileGroup.HTTPLocationOffset = data.ReadUInt32();
|
||||
fileGroup.FTPLocationOffset = data.ReadUInt32();
|
||||
fileGroup.MiscOffset = data.ReadUInt32();
|
||||
fileGroup.Var2Offset = data.ReadUInt32();
|
||||
fileGroup.TargetDirectoryOffset = data.ReadUInt32();
|
||||
fileGroup.Reserved3 = data.ReadBytes(2);
|
||||
fileGroup.Reserved4 = data.ReadBytes(2);
|
||||
fileGroup.Reserved5 = data.ReadBytes(2);
|
||||
fileGroup.Reserved6 = data.ReadBytes(2);
|
||||
fileGroup.Reserved7 = data.ReadBytes(2);
|
||||
|
||||
// Cache the current position
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Read the name, if possible
|
||||
if (fileGroup.NameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(fileGroup.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
fileGroup.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
fileGroup.Name = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return fileGroup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a component
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled component on success, null on error</returns>
|
||||
private static Component ParseComponent(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
Component component = new Component();
|
||||
|
||||
component.IdentifierOffset = data.ReadUInt32();
|
||||
component.DescriptorOffset = data.ReadUInt32();
|
||||
component.DisplayNameOffset = data.ReadUInt32();
|
||||
component.Reserved0 = data.ReadBytes(2);
|
||||
component.ReservedOffset0 = data.ReadUInt32();
|
||||
component.ReservedOffset1 = data.ReadUInt32();
|
||||
component.ComponentIndex = data.ReadUInt16();
|
||||
component.NameOffset = data.ReadUInt32();
|
||||
component.ReservedOffset2 = data.ReadUInt32();
|
||||
component.ReservedOffset3 = data.ReadUInt32();
|
||||
component.ReservedOffset4 = data.ReadUInt32();
|
||||
component.Reserved1 = data.ReadBytes(32);
|
||||
component.CLSIDOffset = data.ReadUInt32();
|
||||
component.Reserved2 = data.ReadBytes(28);
|
||||
component.Reserved3 = data.ReadBytes(majorVersion <= 5 ? 2 : 1);
|
||||
component.DependsCount = data.ReadUInt16();
|
||||
component.DependsOffset = data.ReadUInt32();
|
||||
component.FileGroupCount = data.ReadUInt16();
|
||||
component.FileGroupNamesOffset = data.ReadUInt32();
|
||||
component.X3Count = data.ReadUInt16();
|
||||
component.X3Offset = data.ReadUInt32();
|
||||
component.SubComponentsCount = data.ReadUInt16();
|
||||
component.SubComponentsOffset = data.ReadUInt32();
|
||||
component.NextComponentOffset = data.ReadUInt32();
|
||||
component.ReservedOffset5 = data.ReadUInt32();
|
||||
component.ReservedOffset6 = data.ReadUInt32();
|
||||
component.ReservedOffset7 = data.ReadUInt32();
|
||||
component.ReservedOffset8 = data.ReadUInt32();
|
||||
|
||||
// Cache the current position
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Read the identifier, if possible
|
||||
if (component.IdentifierOffset != 0)
|
||||
{
|
||||
// Seek to the identifier
|
||||
data.Seek(component.IdentifierOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
component.Identifier = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
component.Identifier = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Read the display name, if possible
|
||||
if (component.DisplayNameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(component.DisplayNameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
component.DisplayName = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
component.DisplayName = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Read the name, if possible
|
||||
if (component.NameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(component.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
component.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
component.Name = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Read the CLSID, if possible
|
||||
if (component.CLSIDOffset != 0)
|
||||
{
|
||||
// Seek to the CLSID
|
||||
data.Seek(component.CLSIDOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the GUID
|
||||
component.CLSID = data.ReadGuid();
|
||||
}
|
||||
|
||||
// Read the file group names, if possible
|
||||
if (component.FileGroupCount != 0 && component.FileGroupNamesOffset != 0)
|
||||
{
|
||||
// Seek to the file group table offset
|
||||
data.Seek(component.FileGroupNamesOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the file group names table
|
||||
component.FileGroupNames = new string[component.FileGroupCount];
|
||||
for (int j = 0; j < component.FileGroupCount; j++)
|
||||
{
|
||||
// Get the name offset
|
||||
uint nameOffset = data.ReadUInt32();
|
||||
|
||||
// Cache the current offset
|
||||
long preNameOffset = data.Position;
|
||||
|
||||
// Seek to the name offset
|
||||
data.Seek(nameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
if (majorVersion >= 17)
|
||||
component.FileGroupNames[j] = data.ReadString(Encoding.Unicode) ?? string.Empty;
|
||||
else
|
||||
component.FileGroupNames[j] = data.ReadString(Encoding.ASCII) ?? string.Empty;
|
||||
|
||||
// Seek back to the original position
|
||||
data.Seek(preNameOffset, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a directory name
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <returns>Filled directory name on success, null on error</returns>
|
||||
private static string? ParseDirectoryName(Stream data, int majorVersion)
|
||||
{
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
return data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
return data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file descriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">Major version of the cabinet</param>
|
||||
/// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
|
||||
/// <returns>Filled file descriptor on success, null on error</returns>
|
||||
private static FileDescriptor ParseFileDescriptor(Stream data, int majorVersion, uint descriptorOffset)
|
||||
{
|
||||
FileDescriptor fileDescriptor = new FileDescriptor();
|
||||
|
||||
// Read the descriptor based on version
|
||||
if (majorVersion <= 5)
|
||||
{
|
||||
fileDescriptor.Volume = 0xFFFF; // Set by the header index
|
||||
fileDescriptor.NameOffset = data.ReadUInt32();
|
||||
fileDescriptor.DirectoryIndex = data.ReadUInt32();
|
||||
fileDescriptor.Flags = (FileFlags)data.ReadUInt16();
|
||||
fileDescriptor.ExpandedSize = data.ReadUInt32();
|
||||
fileDescriptor.CompressedSize = data.ReadUInt32();
|
||||
_ = data.ReadBytes(0x14); // Skip 0x14 bytes, unknown data?
|
||||
fileDescriptor.DataOffset = data.ReadUInt32();
|
||||
|
||||
if (majorVersion == 5)
|
||||
fileDescriptor.MD5 = data.ReadBytes(0x10);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileDescriptor.Flags = (FileFlags)data.ReadUInt16();
|
||||
fileDescriptor.ExpandedSize = data.ReadUInt64();
|
||||
fileDescriptor.CompressedSize = data.ReadUInt64();
|
||||
fileDescriptor.DataOffset = data.ReadUInt64();
|
||||
fileDescriptor.MD5 = data.ReadBytes(0x10);
|
||||
_ = data.ReadBytes(0x10); // Skip 0x10 bytes, unknown data?
|
||||
fileDescriptor.NameOffset = data.ReadUInt32();
|
||||
fileDescriptor.DirectoryIndex = data.ReadUInt16();
|
||||
_ = data.ReadBytes(0x0C); // Skip 0x0C bytes, unknown data?
|
||||
fileDescriptor.LinkPrevious = data.ReadUInt32();
|
||||
fileDescriptor.LinkNext = data.ReadUInt32();
|
||||
fileDescriptor.LinkFlags = (LinkFlags)data.ReadByteValue();
|
||||
fileDescriptor.Volume = data.ReadUInt16();
|
||||
}
|
||||
|
||||
// Cache the current position
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Read the name, if possible
|
||||
if (fileDescriptor.NameOffset != 0)
|
||||
{
|
||||
// Seek to the name
|
||||
data.Seek(fileDescriptor.NameOffset + descriptorOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the string
|
||||
if (majorVersion >= 17)
|
||||
fileDescriptor.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
fileDescriptor.Name = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Seek back to the correct offset
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Get the major version of the cabinet
|
||||
/// </summary>
|
||||
/// <remarks>This should live in the wrapper but is needed during parsing</remarks>
|
||||
private static int GetMajorVersion(CommonHeader commonHeader)
|
||||
{
|
||||
uint majorVersion = commonHeader.Version;
|
||||
if (majorVersion >> 24 == 1)
|
||||
{
|
||||
majorVersion = (majorVersion >> 12) & 0x0F;
|
||||
}
|
||||
else if (majorVersion >> 24 == 2 || majorVersion >> 24 == 4)
|
||||
{
|
||||
majorVersion = majorVersion & 0xFFFF;
|
||||
if (majorVersion != 0)
|
||||
majorVersion /= 100;
|
||||
}
|
||||
|
||||
return (int)majorVersion;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for other JSON serializers
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public partial class JsonFile<T> : IStreamSerializer<T>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public virtual T? Deserialize(Stream? data)
|
||||
=> Deserialize(data, new UTF8Encoding(false));
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a Stream into <typeparamref name="T"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of object to deserialize to</typeparam>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="encoding">Text encoding to use</param>
|
||||
/// <returns>Filled object on success, null on error</returns>
|
||||
public T? Deserialize(Stream? data, Encoding encoding)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the serializer and the reader
|
||||
var serializer = JsonSerializer.Create();
|
||||
var streamReader = new StreamReader(data, encoding);
|
||||
var jsonReader = new JsonTextReader(streamReader);
|
||||
|
||||
// Perform the deserialization and return
|
||||
return serializer.Deserialize<T>(jsonReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,249 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.Models.Listrom;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Listrom : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Listrom();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new StreamReader(data, Encoding.UTF8);
|
||||
var dat = new MetadataFile();
|
||||
|
||||
Set? set = null;
|
||||
var sets = new List<Set>();
|
||||
var rows = new List<Row>();
|
||||
|
||||
var additional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// Read the line and don't split yet
|
||||
string? line = reader.ReadLine();
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
// If we have a set to process
|
||||
if (set != null)
|
||||
{
|
||||
set.Row = rows.ToArray();
|
||||
sets.Add(set);
|
||||
set = null;
|
||||
rows.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set lines are unique
|
||||
if (line.StartsWith("ROMs required for driver"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string driver = line.Substring("ROMs required for driver".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string driver = line["ROMs required for driver".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Driver = driver };
|
||||
continue;
|
||||
}
|
||||
else if (line.StartsWith("No ROMs required for driver"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string driver = line.Substring("No ROMs required for driver".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string driver = line["No ROMs required for driver".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Driver = driver };
|
||||
continue;
|
||||
}
|
||||
else if (line.StartsWith("ROMs required for device"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string device = line.Substring("ROMs required for device".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string device = line["ROMs required for device".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Device = device };
|
||||
continue;
|
||||
}
|
||||
else if (line.StartsWith("No ROMs required for device"))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
string device = line.Substring("No ROMs required for device".Length).Trim('"', ' ', '.');
|
||||
#else
|
||||
string device = line["No ROMs required for device".Length..].Trim('"', ' ', '.');
|
||||
#endif
|
||||
set = new Set { Device = device };
|
||||
continue;
|
||||
}
|
||||
else if (line.Equals("Name Size Checksum", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// No-op
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the line for the name iteratively
|
||||
#if NETFRAMEWORK || NETCOREAPP3_1
|
||||
string[] lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
#else
|
||||
string[] lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lineParts.Length == 1)
|
||||
lineParts = line.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
#endif
|
||||
|
||||
// Read the name and set the rest of the line for processing
|
||||
string name = lineParts[0];
|
||||
#if NETFRAMEWORK
|
||||
string trimmedLine = line.Substring(name.Length);
|
||||
#else
|
||||
string trimmedLine = line[name.Length..];
|
||||
#endif
|
||||
if (trimmedLine == null)
|
||||
continue;
|
||||
|
||||
#if NETFRAMEWORK || NETCOREAPP3_1
|
||||
lineParts = trimmedLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
#else
|
||||
lineParts = trimmedLine.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
#endif
|
||||
|
||||
// The number of items in the row explains what type of row it is
|
||||
var row = new Row();
|
||||
switch (lineParts.Length)
|
||||
{
|
||||
// Normal CHD (Name, MD5/SHA1)
|
||||
case 1:
|
||||
row.Name = name;
|
||||
#if NETFRAMEWORK
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[0].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[0].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[0]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[0]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Normal ROM (Name, Size, CRC, MD5/SHA1)
|
||||
case 3 when line.Contains("CRC"):
|
||||
row.Name = name;
|
||||
row.Size = lineParts[0];
|
||||
#if NETFRAMEWORK
|
||||
row.CRC = lineParts[1].Substring("CRC".Length).Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[2].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[2].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
row.CRC = lineParts[1]["CRC".Length..].Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[2]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[2]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Bad CHD (Name, BAD, SHA1, BAD_DUMP)
|
||||
case 3 when line.Contains("BAD_DUMP"):
|
||||
row.Name = name;
|
||||
row.Bad = true;
|
||||
#if NETFRAMEWORK
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[1].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[1].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[1]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[1]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Nodump CHD (Name, NO GOOD DUMP KNOWN)
|
||||
case 4 when line.Contains("NO GOOD DUMP KNOWN"):
|
||||
row.Name = name;
|
||||
row.NoGoodDumpKnown = true;
|
||||
break;
|
||||
|
||||
// Bad ROM (Name, Size, BAD, CRC, MD5/SHA1, BAD_DUMP)
|
||||
case 5 when line.Contains("BAD_DUMP"):
|
||||
row.Name = name;
|
||||
row.Size = lineParts[0];
|
||||
row.Bad = true;
|
||||
#if NETFRAMEWORK
|
||||
row.CRC = lineParts[2].Substring("CRC".Length).Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[3].Substring("MD5".Length).Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[3].Substring("SHA1".Length).Trim('(', ')');
|
||||
#else
|
||||
row.CRC = lineParts[2]["CRC".Length..].Trim('(', ')');
|
||||
if (line.Contains("MD5("))
|
||||
row.MD5 = lineParts[3]["MD5".Length..].Trim('(', ')');
|
||||
else
|
||||
row.SHA1 = lineParts[3]["SHA1".Length..].Trim('(', ')');
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Nodump ROM (Name, Size, NO GOOD DUMP KNOWN)
|
||||
case 5 when line.Contains("NO GOOD DUMP KNOWN"):
|
||||
row.Name = name;
|
||||
row.Size = lineParts[0];
|
||||
row.NoGoodDumpKnown = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
row = null;
|
||||
additional.Add(line);
|
||||
break;
|
||||
}
|
||||
|
||||
if (row != null)
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
// If we have a set to process
|
||||
if (set != null)
|
||||
{
|
||||
set.Row = rows.ToArray();
|
||||
sets.Add(set);
|
||||
set = null;
|
||||
rows.Clear();
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.Set = sets.ToArray();
|
||||
dat.ADDITIONAL_ELEMENTS = additional.ToArray();
|
||||
return dat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Listxml : XmlFile<Models.Listxml.Mame>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.Listxml.Mame? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new Listxml();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Logiqx : XmlFile<Models.Logiqx.Datafile>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.Logiqx.Datafile? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new Logiqx();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class M1 : XmlFile<Models.Listxml.M1>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.Listxml.M1? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new M1();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.MSDOS;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.MSDOS.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class MSDOS : IStreamSerializer<Executable>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Executable? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new MSDOS();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Executable? 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 executable to fill
|
||||
var executable = new Executable();
|
||||
|
||||
#region Executable Header
|
||||
|
||||
// Try to parse the executable header
|
||||
var executableHeader = ParseExecutableHeader(data);
|
||||
if (executableHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the executable header
|
||||
executable.Header = executableHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Relocation Table
|
||||
|
||||
// If the offset for the relocation table doesn't exist
|
||||
int tableAddress = initialOffset + executableHeader.RelocationTableAddr;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the relocation table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var relocationTable = ParseRelocationTable(data, executableHeader.RelocationItems);
|
||||
if (relocationTable == null)
|
||||
return null;
|
||||
|
||||
// Set the relocation table
|
||||
executable.RelocationTable = relocationTable;
|
||||
|
||||
#endregion
|
||||
|
||||
// Return the executable
|
||||
return executable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an MS-DOS executable header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled executable header on success, null on error</returns>
|
||||
private static ExecutableHeader? ParseExecutableHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new ExecutableHeader();
|
||||
|
||||
#region Standard Fields
|
||||
|
||||
byte[]? magic = data.ReadBytes(2);
|
||||
if (magic == null)
|
||||
return null;
|
||||
|
||||
header.Magic = Encoding.ASCII.GetString(magic);
|
||||
if (header.Magic != SignatureString)
|
||||
return null;
|
||||
|
||||
header.LastPageBytes = data.ReadUInt16();
|
||||
header.Pages = data.ReadUInt16();
|
||||
header.RelocationItems = data.ReadUInt16();
|
||||
header.HeaderParagraphSize = data.ReadUInt16();
|
||||
header.MinimumExtraParagraphs = data.ReadUInt16();
|
||||
header.MaximumExtraParagraphs = data.ReadUInt16();
|
||||
header.InitialSSValue = data.ReadUInt16();
|
||||
header.InitialSPValue = data.ReadUInt16();
|
||||
header.Checksum = data.ReadUInt16();
|
||||
header.InitialIPValue = data.ReadUInt16();
|
||||
header.InitialCSValue = data.ReadUInt16();
|
||||
header.RelocationTableAddr = data.ReadUInt16();
|
||||
header.OverlayNumber = data.ReadUInt16();
|
||||
|
||||
#endregion
|
||||
|
||||
// If we don't have enough data for PE extensions
|
||||
if (data.Position >= data.Length || data.Length - data.Position < 36)
|
||||
return header;
|
||||
|
||||
#region PE Extensions
|
||||
|
||||
header.Reserved1 = new ushort[4];
|
||||
for (int i = 0; i < header.Reserved1.Length; i++)
|
||||
{
|
||||
header.Reserved1[i] = data.ReadUInt16();
|
||||
}
|
||||
header.OEMIdentifier = data.ReadUInt16();
|
||||
header.OEMInformation = data.ReadUInt16();
|
||||
header.Reserved2 = new ushort[10];
|
||||
for (int i = 0; i < header.Reserved2.Length; i++)
|
||||
{
|
||||
header.Reserved2[i] = data.ReadUInt16();
|
||||
}
|
||||
header.NewExeHeaderAddr = data.ReadUInt32();
|
||||
|
||||
#endregion
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a relocation table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of relocation table entries to read</param>
|
||||
/// <returns>Filled relocation table on success, null on error</returns>
|
||||
private static RelocationEntry[] ParseRelocationTable(Stream data, int count)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var relocationTable = new RelocationEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = new RelocationEntry();
|
||||
entry.Offset = data.ReadUInt16();
|
||||
entry.Segment = data.ReadUInt16();
|
||||
relocationTable[i] = entry;
|
||||
}
|
||||
|
||||
return relocationTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.MicrosoftCabinet;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.MicrosoftCabinet.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
// TODO: Add multi-cabinet reading
|
||||
public partial class MicrosoftCabinet : IStreamSerializer<Cabinet>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Cabinet? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new MicrosoftCabinet();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cabinet? 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 cabinet to fill
|
||||
var cabinet = new Cabinet();
|
||||
|
||||
#region Cabinet Header
|
||||
|
||||
// Try to parse the cabinet header
|
||||
var cabinetHeader = ParseCabinetHeader(data);
|
||||
if (cabinetHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the cabinet header
|
||||
cabinet.Header = cabinetHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Folders
|
||||
|
||||
// Set the folder array
|
||||
cabinet.Folders = new CFFOLDER[cabinetHeader.FolderCount];
|
||||
|
||||
// Try to parse each folder, if we have any
|
||||
for (int i = 0; i < cabinetHeader.FolderCount; i++)
|
||||
{
|
||||
var folder = ParseFolder(data, cabinetHeader);
|
||||
if (folder == null)
|
||||
return null;
|
||||
|
||||
// Set the folder
|
||||
cabinet.Folders[i] = folder;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Files
|
||||
|
||||
// Get the files offset
|
||||
int filesOffset = (int)cabinetHeader.FilesOffset + initialOffset;
|
||||
if (filesOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the offset
|
||||
data.Seek(filesOffset, SeekOrigin.Begin);
|
||||
|
||||
// Set the file array
|
||||
cabinet.Files = new CFFILE[cabinetHeader.FileCount];
|
||||
|
||||
// Try to parse each file, if we have any
|
||||
for (int i = 0; i < cabinetHeader.FileCount; i++)
|
||||
{
|
||||
var file = ParseFile(data);
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
// Set the file
|
||||
cabinet.Files[i] = file;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return cabinet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a cabinet header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled cabinet header on success, null on error</returns>
|
||||
private static CFHEADER? ParseCabinetHeader(Stream data)
|
||||
{
|
||||
var header = new CFHEADER();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.Reserved1 = data.ReadUInt32();
|
||||
header.CabinetSize = data.ReadUInt32();
|
||||
header.Reserved2 = data.ReadUInt32();
|
||||
header.FilesOffset = data.ReadUInt32();
|
||||
header.Reserved3 = data.ReadUInt32();
|
||||
header.VersionMinor = data.ReadByteValue();
|
||||
header.VersionMajor = data.ReadByteValue();
|
||||
header.FolderCount = data.ReadUInt16();
|
||||
header.FileCount = data.ReadUInt16();
|
||||
header.Flags = (HeaderFlags)data.ReadUInt16();
|
||||
header.SetID = data.ReadUInt16();
|
||||
header.CabinetIndex = data.ReadUInt16();
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((header.Flags & HeaderFlags.RESERVE_PRESENT) != 0)
|
||||
#else
|
||||
if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT))
|
||||
#endif
|
||||
{
|
||||
header.HeaderReservedSize = data.ReadUInt16();
|
||||
if (header.HeaderReservedSize > 60_000)
|
||||
return null;
|
||||
|
||||
header.FolderReservedSize = data.ReadByteValue();
|
||||
header.DataReservedSize = data.ReadByteValue();
|
||||
|
||||
if (header.HeaderReservedSize > 0)
|
||||
header.ReservedData = data.ReadBytes(header.HeaderReservedSize);
|
||||
}
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((header.Flags & HeaderFlags.PREV_CABINET) != 0)
|
||||
#else
|
||||
if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET))
|
||||
#endif
|
||||
{
|
||||
header.CabinetPrev = data.ReadString(Encoding.ASCII);
|
||||
header.DiskPrev = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((header.Flags & HeaderFlags.NEXT_CABINET) != 0)
|
||||
#else
|
||||
if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET))
|
||||
#endif
|
||||
{
|
||||
header.CabinetNext = data.ReadString(Encoding.ASCII);
|
||||
header.DiskNext = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a folder
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="header">Cabinet header to get flags and sizes from</param>
|
||||
/// <returns>Filled folder on success, null on error</returns>
|
||||
private static CFFOLDER ParseFolder(Stream data, CFHEADER header)
|
||||
{
|
||||
var folder = new CFFOLDER();
|
||||
|
||||
folder.CabStartOffset = data.ReadUInt32();
|
||||
folder.DataCount = data.ReadUInt16();
|
||||
folder.CompressionType = (CompressionType)data.ReadUInt16();
|
||||
|
||||
if (header.FolderReservedSize > 0)
|
||||
folder.ReservedData = data.ReadBytes(header.FolderReservedSize);
|
||||
|
||||
if (folder.CabStartOffset > 0)
|
||||
{
|
||||
long currentPosition = data.Position;
|
||||
data.Seek(folder.CabStartOffset, SeekOrigin.Begin);
|
||||
|
||||
folder.DataBlocks = new CFDATA[folder.DataCount];
|
||||
for (int i = 0; i < folder.DataCount; i++)
|
||||
{
|
||||
CFDATA dataBlock = ParseDataBlock(data, header.DataReservedSize);
|
||||
folder.DataBlocks[i] = dataBlock;
|
||||
}
|
||||
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a data block
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="dataReservedSize">Reserved byte size for data blocks</param>
|
||||
/// <returns>Filled folder on success, null on error</returns>
|
||||
private static CFDATA ParseDataBlock(Stream data, byte dataReservedSize)
|
||||
{
|
||||
var dataBlock = new CFDATA();
|
||||
|
||||
dataBlock.Checksum = data.ReadUInt32();
|
||||
dataBlock.CompressedSize = data.ReadUInt16();
|
||||
dataBlock.UncompressedSize = data.ReadUInt16();
|
||||
|
||||
if (dataReservedSize > 0)
|
||||
dataBlock.ReservedData = data.ReadBytes(dataReservedSize);
|
||||
|
||||
if (dataBlock.CompressedSize > 0)
|
||||
dataBlock.CompressedData = data.ReadBytes(dataBlock.CompressedSize);
|
||||
|
||||
return dataBlock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled file on success, null on error</returns>
|
||||
private static CFFILE ParseFile(Stream data)
|
||||
{
|
||||
var file = new CFFILE();
|
||||
|
||||
file.FileSize = data.ReadUInt32();
|
||||
file.FolderStartOffset = data.ReadUInt32();
|
||||
file.FolderIndex = (FolderIndex)data.ReadUInt16();
|
||||
file.Date = data.ReadUInt16();
|
||||
file.Time = data.ReadUInt16();
|
||||
file.Attributes = (Models.MicrosoftCabinet.FileAttributes)data.ReadUInt16();
|
||||
|
||||
#if NET20 || NET35
|
||||
if ((file.Attributes & Models.MicrosoftCabinet.FileAttributes.NAME_IS_UTF) != 0)
|
||||
#else
|
||||
if (file.Attributes.HasFlag(Models.MicrosoftCabinet.FileAttributes.NAME_IS_UTF))
|
||||
#endif
|
||||
file.Name = data.ReadString(Encoding.Unicode);
|
||||
else
|
||||
file.Name = data.ReadString(Encoding.ASCII);
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,639 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.MoPaQ;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.MoPaQ.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class MoPaQ : IStreamSerializer<Archive>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new MoPaQ();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 User Data
|
||||
|
||||
// Check for User Data
|
||||
uint possibleSignature = data.ReadUInt32();
|
||||
data.Seek(-4, SeekOrigin.Current);
|
||||
if (possibleSignature == 0x1B51504D)
|
||||
{
|
||||
// Save the current position for offset correction
|
||||
long basePtr = data.Position;
|
||||
|
||||
// Deserialize the user data, returning null if invalid
|
||||
var userData = ParseUserData(data);
|
||||
if (userData == null)
|
||||
return null;
|
||||
|
||||
// Set the user data
|
||||
archive.UserData = userData;
|
||||
|
||||
// Set the starting position according to the header offset
|
||||
data.Seek(basePtr + (int)archive.UserData.HeaderOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive Header
|
||||
|
||||
// Check for the Header
|
||||
possibleSignature = data.ReadUInt32();
|
||||
data.Seek(-4, SeekOrigin.Current);
|
||||
if (possibleSignature == 0x1A51504D)
|
||||
{
|
||||
// Try to parse the archive header
|
||||
var archiveHeader = ParseArchiveHeader(data);
|
||||
if (archiveHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the archive header
|
||||
archive.ArchiveHeader = archiveHeader;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hash Table
|
||||
|
||||
// TODO: The hash table has to be be decrypted before reading
|
||||
|
||||
// Version 1
|
||||
if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1)
|
||||
{
|
||||
// If we have a hash table
|
||||
long hashTableOffset = archive.ArchiveHeader.HashTablePosition;
|
||||
if (hashTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hashTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long hashTableEnd = hashTableOffset + archive.ArchiveHeader.HashTableSize;
|
||||
|
||||
// Read in the hash table
|
||||
var hashTable = new List<HashEntry>();
|
||||
|
||||
while (data.Position < hashTableEnd)
|
||||
{
|
||||
var hashEntry = ParseHashEntry(data);
|
||||
if (hashEntry == null)
|
||||
return null;
|
||||
|
||||
hashTable.Add(hashEntry);
|
||||
}
|
||||
|
||||
archive.HashTable = hashTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 2 and 3
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3)
|
||||
{
|
||||
// If we have a hash table
|
||||
long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition;
|
||||
if (hashTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hashTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long hashTableEnd = hashTableOffset + archive.ArchiveHeader.HashTableSize;
|
||||
|
||||
// Read in the hash table
|
||||
var hashTable = new List<HashEntry>();
|
||||
|
||||
while (data.Position < hashTableEnd)
|
||||
{
|
||||
var hashEntry = ParseHashEntry(data);
|
||||
if (hashEntry == null)
|
||||
return null;
|
||||
|
||||
hashTable.Add(hashEntry);
|
||||
}
|
||||
|
||||
archive.HashTable = hashTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 4
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4)
|
||||
{
|
||||
// If we have a hash table
|
||||
long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition;
|
||||
if (hashTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hashTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long hashTableEnd = hashTableOffset + (long)archive.ArchiveHeader.HashTableSizeLong;
|
||||
|
||||
// Read in the hash table
|
||||
var hashTable = new List<HashEntry>();
|
||||
|
||||
while (data.Position < hashTableEnd)
|
||||
{
|
||||
var hashEntry = ParseHashEntry(data);
|
||||
if (hashEntry == null)
|
||||
return null;
|
||||
|
||||
hashTable.Add(hashEntry);
|
||||
}
|
||||
|
||||
archive.HashTable = hashTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Block Table
|
||||
|
||||
// Version 1
|
||||
if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1)
|
||||
{
|
||||
// If we have a block table
|
||||
long blockTableOffset = archive.ArchiveHeader.BlockTablePosition;
|
||||
if (blockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(blockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long blockTableEnd = blockTableOffset + archive.ArchiveHeader.BlockTableSize;
|
||||
|
||||
// Read in the block table
|
||||
var blockTable = new List<BlockEntry>();
|
||||
|
||||
while (data.Position < blockTableEnd)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
if (blockEntry == null)
|
||||
return null;
|
||||
|
||||
blockTable.Add(blockEntry);
|
||||
}
|
||||
|
||||
archive.BlockTable = blockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 2 and 3
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3)
|
||||
{
|
||||
// If we have a block table
|
||||
long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition;
|
||||
if (blockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(blockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long blockTableEnd = blockTableOffset + archive.ArchiveHeader.BlockTableSize;
|
||||
|
||||
// Read in the block table
|
||||
var blockTable = new List<BlockEntry>();
|
||||
|
||||
while (data.Position < blockTableEnd)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
if (blockEntry == null)
|
||||
return null;
|
||||
|
||||
blockTable.Add(blockEntry);
|
||||
}
|
||||
|
||||
archive.BlockTable = blockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Version 4
|
||||
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4)
|
||||
{
|
||||
// If we have a block table
|
||||
long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition;
|
||||
if (blockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(blockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Find the ending offset based on size
|
||||
long blockTableEnd = blockTableOffset + (long)archive.ArchiveHeader.BlockTableSizeLong;
|
||||
|
||||
// Read in the block table
|
||||
var blockTable = new List<BlockEntry>();
|
||||
|
||||
while (data.Position < blockTableEnd)
|
||||
{
|
||||
var blockEntry = ParseBlockEntry(data);
|
||||
if (blockEntry == null)
|
||||
return null;
|
||||
|
||||
blockTable.Add(blockEntry);
|
||||
}
|
||||
|
||||
archive.BlockTable = blockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hi-Block Table
|
||||
|
||||
// Version 2, 3, and 4
|
||||
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format2)
|
||||
{
|
||||
// If we have a hi-block table
|
||||
long hiBlockTableOffset = (long)archive.ArchiveHeader.HiBlockTablePosition;
|
||||
if (hiBlockTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hiBlockTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read in the hi-block table
|
||||
var hiBlockTable = new List<short>();
|
||||
|
||||
for (int i = 0; i < (archive.BlockTable?.Length ?? 0); i++)
|
||||
{
|
||||
short hiBlockEntry = data.ReadInt16();
|
||||
hiBlockTable.Add(hiBlockEntry);
|
||||
}
|
||||
|
||||
archive.HiBlockTable = hiBlockTable.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BET Table
|
||||
|
||||
// Version 3 and 4
|
||||
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3)
|
||||
{
|
||||
// If we have a BET table
|
||||
long betTableOffset = (long)archive.ArchiveHeader.BetTablePosition;
|
||||
if (betTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(betTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read in the BET table
|
||||
var betTable = ParseBetTable(data);
|
||||
if (betTable != null)
|
||||
return null;
|
||||
|
||||
archive.BetTable = betTable;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HET Table
|
||||
|
||||
// Version 3 and 4
|
||||
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3)
|
||||
{
|
||||
// If we have a HET table
|
||||
long hetTableOffset = (long)archive.ArchiveHeader.HetTablePosition;
|
||||
if (hetTableOffset != 0)
|
||||
{
|
||||
// Seek to the offset
|
||||
data.Seek(hetTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read in the HET table
|
||||
var hetTable = ParseHetTable(data);
|
||||
if (hetTable != null)
|
||||
return null;
|
||||
|
||||
archive.HetTable = hetTable;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a archive header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled archive header on success, null on error</returns>
|
||||
private static ArchiveHeader? ParseArchiveHeader(Stream data)
|
||||
{
|
||||
ArchiveHeader archiveHeader = new ArchiveHeader();
|
||||
|
||||
// V1 - Common
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
archiveHeader.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (archiveHeader.Signature != ArchiveHeaderSignatureString)
|
||||
return null;
|
||||
|
||||
archiveHeader.HeaderSize = data.ReadUInt32();
|
||||
archiveHeader.ArchiveSize = data.ReadUInt32();
|
||||
archiveHeader.FormatVersion = (FormatVersion)data.ReadUInt16();
|
||||
archiveHeader.BlockSize = data.ReadUInt16();
|
||||
archiveHeader.HashTablePosition = data.ReadUInt32();
|
||||
archiveHeader.BlockTablePosition = data.ReadUInt32();
|
||||
archiveHeader.HashTableSize = data.ReadUInt32();
|
||||
archiveHeader.BlockTableSize = data.ReadUInt32();
|
||||
|
||||
// V2
|
||||
if (archiveHeader.FormatVersion >= FormatVersion.Format2)
|
||||
{
|
||||
archiveHeader.HiBlockTablePosition = data.ReadUInt64();
|
||||
archiveHeader.HashTablePositionHi = data.ReadUInt16();
|
||||
archiveHeader.BlockTablePositionHi = data.ReadUInt16();
|
||||
}
|
||||
|
||||
// V3
|
||||
if (archiveHeader.FormatVersion >= FormatVersion.Format3)
|
||||
{
|
||||
archiveHeader.ArchiveSizeLong = data.ReadUInt64();
|
||||
archiveHeader.BetTablePosition = data.ReadUInt64();
|
||||
archiveHeader.HetTablePosition = data.ReadUInt64();
|
||||
}
|
||||
|
||||
// V4
|
||||
if (archiveHeader.FormatVersion >= FormatVersion.Format4)
|
||||
{
|
||||
archiveHeader.HashTableSizeLong = data.ReadUInt64();
|
||||
archiveHeader.BlockTableSizeLong = data.ReadUInt64();
|
||||
archiveHeader.HiBlockTableSize = data.ReadUInt64();
|
||||
archiveHeader.HetTableSize = data.ReadUInt64();
|
||||
archiveHeader.BetTablesize = data.ReadUInt64();
|
||||
archiveHeader.RawChunkSize = data.ReadUInt32();
|
||||
|
||||
archiveHeader.BlockTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HashTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HiBlockTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.BetTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HetTableMD5 = data.ReadBytes(0x10);
|
||||
archiveHeader.HetTableMD5 = data.ReadBytes(0x10);
|
||||
}
|
||||
|
||||
return archiveHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a user data object
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled user data on success, null on error</returns>
|
||||
private static UserData? ParseUserData(Stream data)
|
||||
{
|
||||
UserData userData = new UserData();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
userData.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (userData.Signature != UserDataSignatureString)
|
||||
return null;
|
||||
|
||||
userData.UserDataSize = data.ReadUInt32();
|
||||
userData.HeaderOffset = data.ReadUInt32();
|
||||
userData.UserDataHeaderSize = data.ReadUInt32();
|
||||
|
||||
return userData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a HET table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled HET table on success, null on error</returns>
|
||||
private static HetTable? ParseHetTable(Stream data)
|
||||
{
|
||||
HetTable hetTable = new HetTable();
|
||||
|
||||
// Common Headers
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
hetTable.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (hetTable.Signature != HetTableSignatureString)
|
||||
return null;
|
||||
|
||||
hetTable.Version = data.ReadUInt32();
|
||||
hetTable.DataSize = data.ReadUInt32();
|
||||
|
||||
// HET-Specific
|
||||
hetTable.TableSize = data.ReadUInt32();
|
||||
hetTable.MaxFileCount = data.ReadUInt32();
|
||||
hetTable.HashTableSize = data.ReadUInt32();
|
||||
hetTable.TotalIndexSize = data.ReadUInt32();
|
||||
hetTable.IndexSizeExtra = data.ReadUInt32();
|
||||
hetTable.IndexSize = data.ReadUInt32();
|
||||
hetTable.BlockTableSize = data.ReadUInt32();
|
||||
hetTable.HashTable = data.ReadBytes((int)hetTable.HashTableSize);
|
||||
|
||||
// TODO: Populate the file indexes array
|
||||
hetTable.FileIndexes = new byte[(int)hetTable.HashTableSize][];
|
||||
|
||||
return hetTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a BET table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled BET table on success, null on error</returns>
|
||||
private static BetTable? ParseBetTable(Stream data)
|
||||
{
|
||||
BetTable betTable = new BetTable();
|
||||
|
||||
// Common Headers
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
betTable.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (betTable.Signature != BetTableSignatureString)
|
||||
return null;
|
||||
|
||||
betTable.Version = data.ReadUInt32();
|
||||
betTable.DataSize = data.ReadUInt32();
|
||||
|
||||
// BET-Specific
|
||||
betTable.TableSize = data.ReadUInt32();
|
||||
betTable.FileCount = data.ReadUInt32();
|
||||
betTable.Unknown = data.ReadUInt32();
|
||||
betTable.TableEntrySize = data.ReadUInt32();
|
||||
|
||||
betTable.FilePositionBitIndex = data.ReadUInt32();
|
||||
betTable.FileSizeBitIndex = data.ReadUInt32();
|
||||
betTable.CompressedSizeBitIndex = data.ReadUInt32();
|
||||
betTable.FlagIndexBitIndex = data.ReadUInt32();
|
||||
betTable.UnknownBitIndex = data.ReadUInt32();
|
||||
|
||||
betTable.FilePositionBitCount = data.ReadUInt32();
|
||||
betTable.FileSizeBitCount = data.ReadUInt32();
|
||||
betTable.CompressedSizeBitCount = data.ReadUInt32();
|
||||
betTable.FlagIndexBitCount = data.ReadUInt32();
|
||||
betTable.UnknownBitCount = data.ReadUInt32();
|
||||
|
||||
betTable.TotalBetHashSize = data.ReadUInt32();
|
||||
betTable.BetHashSizeExtra = data.ReadUInt32();
|
||||
betTable.BetHashSize = data.ReadUInt32();
|
||||
betTable.BetHashArraySize = data.ReadUInt32();
|
||||
betTable.FlagCount = data.ReadUInt32();
|
||||
|
||||
betTable.FlagsArray = new uint[betTable.FlagCount];
|
||||
byte[]? flagsArray = data.ReadBytes((int)betTable.FlagCount * 4);
|
||||
if (flagsArray != null)
|
||||
Buffer.BlockCopy(flagsArray, 0, betTable.FlagsArray, 0, (int)betTable.FlagCount * 4);
|
||||
|
||||
// TODO: Populate the file table
|
||||
// TODO: Populate the hash table
|
||||
|
||||
return betTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a hash entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled hash entry on success, null on error</returns>
|
||||
private static HashEntry ParseHashEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
HashEntry hashEntry = new HashEntry();
|
||||
|
||||
hashEntry.NameHashPartA = data.ReadUInt32();
|
||||
hashEntry.NameHashPartB = data.ReadUInt32();
|
||||
hashEntry.Locale = (Locale)data.ReadUInt16();
|
||||
hashEntry.Platform = data.ReadUInt16();
|
||||
hashEntry.BlockIndex = data.ReadUInt32();
|
||||
|
||||
return hashEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a block entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled block entry on success, null on error</returns>
|
||||
private static BlockEntry ParseBlockEntry(Stream data)
|
||||
{
|
||||
BlockEntry blockEntry = new BlockEntry();
|
||||
|
||||
blockEntry.FilePosition = data.ReadUInt32();
|
||||
blockEntry.CompressedSize = data.ReadUInt32();
|
||||
blockEntry.UncompressedSize = data.ReadUInt32();
|
||||
blockEntry.Flags = (FileFlags)data.ReadUInt32();
|
||||
|
||||
return blockEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a patch info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled patch info on success, null on error</returns>
|
||||
private static PatchInfo ParsePatchInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
PatchInfo patchInfo = new PatchInfo();
|
||||
|
||||
patchInfo.Length = data.ReadUInt32();
|
||||
patchInfo.Flags = data.ReadUInt32();
|
||||
patchInfo.DataSize = data.ReadUInt32();
|
||||
patchInfo.MD5 = data.ReadBytes(0x10);
|
||||
|
||||
// TODO: Fill the sector offset table
|
||||
|
||||
return patchInfo;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Buffer for encryption and decryption
|
||||
/// </summary>
|
||||
private uint[] _stormBuffer = new uint[STORM_BUFFER_SIZE];
|
||||
|
||||
/// <summary>
|
||||
/// Prepare the encryption table
|
||||
/// </summary>
|
||||
private void PrepareCryptTable()
|
||||
{
|
||||
uint seed = 0x00100001;
|
||||
for (uint index1 = 0; index1 < 0x100; index1++)
|
||||
{
|
||||
for (uint index2 = index1, i = 0; i < 5; i++, index2 += 0x100)
|
||||
{
|
||||
seed = (seed * 125 + 3) % 0x2AAAAB;
|
||||
uint temp1 = (seed & 0xFFFF) << 0x10;
|
||||
|
||||
seed = (seed * 125 + 3) % 0x2AAAAB;
|
||||
uint temp2 = (seed & 0xFFFF);
|
||||
|
||||
_stormBuffer[index2] = (temp1 | temp2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt a single block of data
|
||||
/// </summary>
|
||||
private unsafe byte[] DecryptBlock(byte[] block, uint length, uint key)
|
||||
{
|
||||
uint seed = 0xEEEEEEEE;
|
||||
|
||||
uint[] castBlock = new uint[length / 4];
|
||||
Buffer.BlockCopy(block, 0, castBlock, 0, (int)length);
|
||||
int castBlockPtr = 0;
|
||||
|
||||
// Round to uints
|
||||
length >>= 2;
|
||||
|
||||
while (length-- > 0)
|
||||
{
|
||||
seed += _stormBuffer[MPQ_HASH_KEY2_MIX + (key & 0xFF)];
|
||||
uint ch = castBlock[castBlockPtr] ^ (key + seed);
|
||||
|
||||
key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B);
|
||||
seed = ch + seed + (seed << 5) + 3;
|
||||
castBlock[castBlockPtr++] = ch;
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(castBlock, 0, block, 0, (int)length);
|
||||
return block;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,719 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.N3DS;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.N3DS.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class N3DS : IStreamSerializer<Cart>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Cart? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new N3DS();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cart? 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 cart image to fill
|
||||
var cart = new Cart();
|
||||
|
||||
#region NCSD Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseNCSDHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the cart image header
|
||||
cart.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Card Info Header
|
||||
|
||||
// Try to parse the card info header
|
||||
var cardInfoHeader = ParseCardInfoHeader(data);
|
||||
if (cardInfoHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the card info header
|
||||
cart.CardInfoHeader = cardInfoHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Development Card Info Header
|
||||
|
||||
// Try to parse the development card info header
|
||||
var developmentCardInfoHeader = ParseDevelopmentCardInfoHeader(data);
|
||||
if (developmentCardInfoHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the development card info header
|
||||
cart.DevelopmentCardInfoHeader = developmentCardInfoHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Partitions
|
||||
|
||||
// Create the partition table
|
||||
cart.Partitions = new NCCHHeader[8];
|
||||
|
||||
// Iterate and build the partitions
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
cart.Partitions[i] = ParseNCCHHeader(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the media unit size for further use
|
||||
long mediaUnitSize = 0;
|
||||
if (header.PartitionFlags != null)
|
||||
mediaUnitSize = (uint)(0x200 * Math.Pow(2, header.PartitionFlags[(int)NCSDFlags.MediaUnitSize]));
|
||||
|
||||
#region Extended Headers
|
||||
|
||||
// Create the extended header table
|
||||
cart.ExtendedHeaders = new NCCHExtendedHeader[8];
|
||||
|
||||
// Iterate and build the extended headers
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
// If we have an encrypted or invalid partition
|
||||
if (cart.Partitions[i]!.MagicID != NCCHMagicNumber)
|
||||
continue;
|
||||
|
||||
// If we have no partitions table
|
||||
if (cart.Header!.PartitionsTable == null)
|
||||
continue;
|
||||
|
||||
// Get the extended header offset
|
||||
long offset = (cart.Header.PartitionsTable[i]!.Offset * mediaUnitSize) + 0x200;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the extended header
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the extended header
|
||||
var extendedHeader = ParseNCCHExtendedHeader(data);
|
||||
if (extendedHeader != null)
|
||||
cart.ExtendedHeaders[i] = extendedHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ExeFS Headers
|
||||
|
||||
// Create the ExeFS header table
|
||||
cart.ExeFSHeaders = new ExeFSHeader[8];
|
||||
|
||||
// Iterate and build the ExeFS headers
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
// If we have an encrypted or invalid partition
|
||||
if (cart.Partitions[i]!.MagicID != NCCHMagicNumber)
|
||||
continue;
|
||||
|
||||
// If we have no partitions table
|
||||
if (cart.Header!.PartitionsTable == null)
|
||||
continue;
|
||||
|
||||
// Get the ExeFS header offset
|
||||
long offset = (cart.Header.PartitionsTable[i]!.Offset + cart.Partitions[i]!.ExeFSOffsetInMediaUnits) * mediaUnitSize;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the ExeFS header
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the ExeFS header
|
||||
cart.ExeFSHeaders[i] = ParseExeFSHeader(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RomFS Headers
|
||||
|
||||
// Create the RomFS header table
|
||||
cart.RomFSHeaders = new RomFSHeader[8];
|
||||
|
||||
// Iterate and build the RomFS headers
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
// If we have an encrypted or invalid partition
|
||||
if (cart.Partitions[i]!.MagicID != NCCHMagicNumber)
|
||||
continue;
|
||||
|
||||
// If we have no partitions table
|
||||
if (cart.Header!.PartitionsTable == null)
|
||||
continue;
|
||||
|
||||
// Get the RomFS header offset
|
||||
long offset = (cart.Header.PartitionsTable[i]!.Offset + cart.Partitions[i]!.RomFSOffsetInMediaUnits) * mediaUnitSize;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
continue;
|
||||
|
||||
// Seek to the RomFS header
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the RomFS header
|
||||
var romFsHeader = ParseRomFSHeader(data);
|
||||
if (romFsHeader != null)
|
||||
cart.RomFSHeaders[i] = romFsHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCSD header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled NCSD header on success, null on error</returns>
|
||||
private static NCSDHeader? ParseNCSDHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new NCSDHeader();
|
||||
|
||||
header.RSA2048Signature = data.ReadBytes(0x100);
|
||||
byte[]? magicNumber = data.ReadBytes(4);
|
||||
if (magicNumber == null)
|
||||
return null;
|
||||
|
||||
header.MagicNumber = Encoding.ASCII.GetString(magicNumber).TrimEnd('\0'); ;
|
||||
if (header.MagicNumber != NCSDMagicNumber)
|
||||
return null;
|
||||
|
||||
header.ImageSizeInMediaUnits = data.ReadUInt32();
|
||||
header.MediaId = data.ReadBytes(8);
|
||||
header.PartitionsFSType = (FilesystemType)data.ReadUInt64();
|
||||
header.PartitionsCryptType = data.ReadBytes(8);
|
||||
|
||||
header.PartitionsTable = new PartitionTableEntry[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
header.PartitionsTable[i] = ParsePartitionTableEntry(data);
|
||||
}
|
||||
|
||||
if (header.PartitionsFSType == FilesystemType.Normal || header.PartitionsFSType == FilesystemType.None)
|
||||
{
|
||||
header.ExheaderHash = data.ReadBytes(0x20);
|
||||
header.AdditionalHeaderSize = data.ReadUInt32();
|
||||
header.SectorZeroOffset = data.ReadUInt32();
|
||||
header.PartitionFlags = data.ReadBytes(8);
|
||||
|
||||
header.PartitionIdTable = new ulong[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
header.PartitionIdTable[i] = data.ReadUInt64();
|
||||
}
|
||||
|
||||
header.Reserved1 = data.ReadBytes(0x20);
|
||||
header.Reserved2 = data.ReadBytes(0x0E);
|
||||
header.FirmUpdateByte1 = data.ReadByteValue();
|
||||
header.FirmUpdateByte2 = data.ReadByteValue();
|
||||
}
|
||||
else if (header.PartitionsFSType == FilesystemType.FIRM)
|
||||
{
|
||||
header.Unknown = data.ReadBytes(0x5E);
|
||||
header.EncryptedMBR = data.ReadBytes(0x42);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a partition table entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled partition table entry on success, null on error</returns>
|
||||
private static PartitionTableEntry ParsePartitionTableEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var partitionTableEntry = new PartitionTableEntry();
|
||||
|
||||
partitionTableEntry.Offset = data.ReadUInt32();
|
||||
partitionTableEntry.Length = data.ReadUInt32();
|
||||
|
||||
return partitionTableEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a card info header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled card info header on success, null on error</returns>
|
||||
private static CardInfoHeader ParseCardInfoHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var cardInfoHeader = new CardInfoHeader();
|
||||
|
||||
cardInfoHeader.WritableAddressMediaUnits = data.ReadUInt32();
|
||||
cardInfoHeader.CardInfoBitmask = data.ReadUInt32();
|
||||
cardInfoHeader.Reserved1 = data.ReadBytes(0xF8);
|
||||
cardInfoHeader.FilledSize = data.ReadUInt32();
|
||||
cardInfoHeader.Reserved2 = data.ReadBytes(0x0C);
|
||||
cardInfoHeader.TitleVersion = data.ReadUInt16();
|
||||
cardInfoHeader.CardRevision = data.ReadUInt16();
|
||||
cardInfoHeader.Reserved3 = data.ReadBytes(0x0C);
|
||||
cardInfoHeader.CVerTitleID = data.ReadBytes(8);
|
||||
cardInfoHeader.CVerVersionNumber = data.ReadUInt16();
|
||||
cardInfoHeader.Reserved4 = data.ReadBytes(0xCD6);
|
||||
|
||||
return cardInfoHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a development card info header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled development card info header on success, null on error</returns>
|
||||
private static DevelopmentCardInfoHeader? ParseDevelopmentCardInfoHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var developmentCardInfoHeader = new DevelopmentCardInfoHeader();
|
||||
|
||||
developmentCardInfoHeader.InitialData = ParseInitialData(data);
|
||||
if (developmentCardInfoHeader.InitialData == null)
|
||||
return null;
|
||||
|
||||
developmentCardInfoHeader.CardDeviceReserved1 = data.ReadBytes(0x200);
|
||||
developmentCardInfoHeader.TitleKey = data.ReadBytes(0x10);
|
||||
developmentCardInfoHeader.CardDeviceReserved2 = data.ReadBytes(0x1BF0);
|
||||
|
||||
developmentCardInfoHeader.TestData = ParseTestData(data);
|
||||
if (developmentCardInfoHeader.TestData == null)
|
||||
return null;
|
||||
|
||||
return developmentCardInfoHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an initial data
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled initial data on success, null on error</returns>
|
||||
private static InitialData? ParseInitialData(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var initialData = new InitialData();
|
||||
|
||||
initialData.CardSeedKeyY = data.ReadBytes(0x10);
|
||||
initialData.EncryptedCardSeed = data.ReadBytes(0x10);
|
||||
initialData.CardSeedAESMAC = data.ReadBytes(0x10);
|
||||
initialData.CardSeedNonce = data.ReadBytes(0xC);
|
||||
initialData.Reserved = data.ReadBytes(0xC4);
|
||||
initialData.BackupHeader = ParseNCCHHeader(data, true);
|
||||
if (initialData.BackupHeader == null)
|
||||
return null;
|
||||
|
||||
return initialData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCCH header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="skipSignature">Indicates if the signature should be skipped</param>
|
||||
/// <returns>Filled NCCH header on success, null on error</returns>
|
||||
internal static NCCHHeader ParseNCCHHeader(Stream data, bool skipSignature = false)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new NCCHHeader();
|
||||
|
||||
if (!skipSignature)
|
||||
header.RSA2048Signature = data.ReadBytes(0x100);
|
||||
|
||||
byte[]? magicId = data.ReadBytes(4);
|
||||
if (magicId != null)
|
||||
header.MagicID = Encoding.ASCII.GetString(magicId).TrimEnd('\0');
|
||||
header.ContentSizeInMediaUnits = data.ReadUInt32();
|
||||
header.PartitionId = data.ReadUInt64();
|
||||
header.MakerCode = data.ReadUInt16();
|
||||
header.Version = data.ReadUInt16();
|
||||
header.VerificationHash = data.ReadUInt32();
|
||||
header.ProgramId = data.ReadBytes(8);
|
||||
header.Reserved1 = data.ReadBytes(0x10);
|
||||
header.LogoRegionHash = data.ReadBytes(0x20);
|
||||
byte[]? productCode = data.ReadBytes(0x10);
|
||||
if (productCode != null)
|
||||
header.ProductCode = Encoding.ASCII.GetString(productCode).TrimEnd('\0');
|
||||
header.ExtendedHeaderHash = data.ReadBytes(0x20);
|
||||
header.ExtendedHeaderSizeInBytes = data.ReadUInt32();
|
||||
header.Reserved2 = data.ReadBytes(4);
|
||||
header.Flags = ParseNCCHHeaderFlags(data);
|
||||
header.PlainRegionOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.PlainRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.LogoRegionOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.LogoRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.ExeFSOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.ExeFSSizeInMediaUnits = data.ReadUInt32();
|
||||
header.ExeFSHashRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.Reserved3 = data.ReadBytes(4);
|
||||
header.RomFSOffsetInMediaUnits = data.ReadUInt32();
|
||||
header.RomFSSizeInMediaUnits = data.ReadUInt32();
|
||||
header.RomFSHashRegionSizeInMediaUnits = data.ReadUInt32();
|
||||
header.Reserved4 = data.ReadBytes(4);
|
||||
header.ExeFSSuperblockHash = data.ReadBytes(0x20);
|
||||
header.RomFSSuperblockHash = data.ReadBytes(0x20);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCCH header flags
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled NCCH header flags on success, null on error</returns>
|
||||
private static NCCHHeaderFlags ParseNCCHHeaderFlags(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var headerFlags = new NCCHHeaderFlags();
|
||||
|
||||
headerFlags.Reserved0 = data.ReadByteValue();
|
||||
headerFlags.Reserved1 = data.ReadByteValue();
|
||||
headerFlags.Reserved2 = data.ReadByteValue();
|
||||
headerFlags.CryptoMethod = (CryptoMethod)data.ReadByteValue();
|
||||
headerFlags.ContentPlatform = (ContentPlatform)data.ReadByteValue();
|
||||
headerFlags.MediaPlatformIndex = (ContentType)data.ReadByteValue();
|
||||
headerFlags.ContentUnitSize = data.ReadByteValue();
|
||||
headerFlags.BitMasks = (BitMasks)data.ReadByteValue();
|
||||
|
||||
return headerFlags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an initial data
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled initial data on success, null on error</returns>
|
||||
private static TestData ParseTestData(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var testData = new TestData();
|
||||
|
||||
// TODO: Validate some of the values
|
||||
testData.Signature = data.ReadBytes(8);
|
||||
testData.AscendingByteSequence = data.ReadBytes(0x1F8);
|
||||
testData.DescendingByteSequence = data.ReadBytes(0x200);
|
||||
testData.Filled00 = data.ReadBytes(0x200);
|
||||
testData.FilledFF = data.ReadBytes(0x200);
|
||||
testData.Filled0F = data.ReadBytes(0x200);
|
||||
testData.FilledF0 = data.ReadBytes(0x200);
|
||||
testData.Filled55 = data.ReadBytes(0x200);
|
||||
testData.FilledAA = data.ReadBytes(0x1FF);
|
||||
testData.FinalByte = data.ReadByteValue();
|
||||
|
||||
return testData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an NCCH extended header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled NCCH extended header on success, null on error</returns>
|
||||
private static NCCHExtendedHeader? ParseNCCHExtendedHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var extendedHeader = new NCCHExtendedHeader();
|
||||
|
||||
extendedHeader.SCI = ParseSystemControlInfo(data);
|
||||
if (extendedHeader.SCI == null)
|
||||
return null;
|
||||
|
||||
extendedHeader.ACI = ParseAccessControlInfo(data);
|
||||
if (extendedHeader.ACI == null)
|
||||
return null;
|
||||
|
||||
extendedHeader.AccessDescSignature = data.ReadBytes(0x100);
|
||||
extendedHeader.NCCHHDRPublicKey = data.ReadBytes(0x100);
|
||||
|
||||
extendedHeader.ACIForLimitations = ParseAccessControlInfo(data);
|
||||
if (extendedHeader.ACI == null)
|
||||
return null;
|
||||
|
||||
return extendedHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a system control info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled system control info on success, null on error</returns>
|
||||
private static SystemControlInfo ParseSystemControlInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var systemControlInfo = new SystemControlInfo();
|
||||
|
||||
byte[]? applicationTitle = data.ReadBytes(8);
|
||||
if (applicationTitle != null)
|
||||
systemControlInfo.ApplicationTitle = Encoding.ASCII.GetString(applicationTitle).TrimEnd('\0');
|
||||
systemControlInfo.Reserved1 = data.ReadBytes(5);
|
||||
systemControlInfo.Flag = data.ReadByteValue();
|
||||
systemControlInfo.RemasterVersion = data.ReadUInt16();
|
||||
systemControlInfo.TextCodeSetInfo = ParseCodeSetInfo(data);
|
||||
systemControlInfo.StackSize = data.ReadUInt32();
|
||||
systemControlInfo.ReadOnlyCodeSetInfo = ParseCodeSetInfo(data);
|
||||
systemControlInfo.Reserved2 = data.ReadBytes(4);
|
||||
systemControlInfo.DataCodeSetInfo = ParseCodeSetInfo(data);
|
||||
systemControlInfo.BSSSize = data.ReadUInt32();
|
||||
systemControlInfo.DependencyModuleList = new ulong[48];
|
||||
for (int i = 0; i < 48; i++)
|
||||
{
|
||||
systemControlInfo.DependencyModuleList[i] = data.ReadUInt64();
|
||||
}
|
||||
systemControlInfo.SystemInfo = ParseSystemInfo(data);
|
||||
|
||||
return systemControlInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a code set info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled code set info on success, null on error</returns>
|
||||
private static CodeSetInfo ParseCodeSetInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var codeSetInfo = new CodeSetInfo();
|
||||
|
||||
codeSetInfo.Address = data.ReadUInt32();
|
||||
codeSetInfo.PhysicalRegionSizeInPages = data.ReadUInt32();
|
||||
codeSetInfo.SizeInBytes = data.ReadUInt32();
|
||||
|
||||
return codeSetInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a system info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled system info on success, null on error</returns>
|
||||
private static SystemInfo ParseSystemInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var systemInfo = new SystemInfo();
|
||||
|
||||
systemInfo.SaveDataSize = data.ReadUInt64();
|
||||
systemInfo.JumpID = data.ReadUInt64();
|
||||
systemInfo.Reserved = data.ReadBytes(0x30);
|
||||
|
||||
return systemInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an access control info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled access control info on success, null on error</returns>
|
||||
private static AccessControlInfo ParseAccessControlInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var accessControlInfo = new AccessControlInfo();
|
||||
|
||||
accessControlInfo.ARM11LocalSystemCapabilities = ParseARM11LocalSystemCapabilities(data);
|
||||
accessControlInfo.ARM11KernelCapabilities = ParseARM11KernelCapabilities(data);
|
||||
accessControlInfo.ARM9AccessControl = ParseARM9AccessControl(data);
|
||||
|
||||
return accessControlInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ARM11 local system capabilities
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ARM11 local system capabilities on success, null on error</returns>
|
||||
private static ARM11LocalSystemCapabilities ParseARM11LocalSystemCapabilities(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var arm11LocalSystemCapabilities = new ARM11LocalSystemCapabilities();
|
||||
|
||||
arm11LocalSystemCapabilities.ProgramID = data.ReadUInt64();
|
||||
arm11LocalSystemCapabilities.CoreVersion = data.ReadUInt32();
|
||||
arm11LocalSystemCapabilities.Flag1 = (ARM11LSCFlag1)data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.Flag2 = (ARM11LSCFlag2)data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.Flag0 = (ARM11LSCFlag0)data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.Priority = data.ReadByteValue();
|
||||
arm11LocalSystemCapabilities.ResourceLimitDescriptors = new ushort[16];
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
arm11LocalSystemCapabilities.ResourceLimitDescriptors[i] = data.ReadUInt16();
|
||||
}
|
||||
arm11LocalSystemCapabilities.StorageInfo = ParseStorageInfo(data);
|
||||
arm11LocalSystemCapabilities.ServiceAccessControl = new ulong[32];
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
arm11LocalSystemCapabilities.ServiceAccessControl[i] = data.ReadUInt64();
|
||||
}
|
||||
arm11LocalSystemCapabilities.ExtendedServiceAccessControl = new ulong[2];
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
arm11LocalSystemCapabilities.ExtendedServiceAccessControl[i] = data.ReadUInt64();
|
||||
}
|
||||
arm11LocalSystemCapabilities.Reserved = data.ReadBytes(0x0F);
|
||||
arm11LocalSystemCapabilities.ResourceLimitCategory = (ResourceLimitCategory)data.ReadByteValue();
|
||||
|
||||
return arm11LocalSystemCapabilities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a storage info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled storage info on success, null on error</returns>
|
||||
private static StorageInfo ParseStorageInfo(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var storageInfo = new StorageInfo();
|
||||
|
||||
storageInfo.ExtdataID = data.ReadUInt64();
|
||||
storageInfo.SystemSavedataIDs = data.ReadBytes(8);
|
||||
storageInfo.StorageAccessibleUniqueIDs = data.ReadBytes(8);
|
||||
storageInfo.FileSystemAccessInfo = data.ReadBytes(7);
|
||||
storageInfo.OtherAttributes = (StorageInfoOtherAttributes)data.ReadByteValue();
|
||||
|
||||
return storageInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ARM11 kernel capabilities
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ARM11 kernel capabilities on success, null on error</returns>
|
||||
private static ARM11KernelCapabilities ParseARM11KernelCapabilities(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var arm11KernelCapabilities = new ARM11KernelCapabilities();
|
||||
|
||||
arm11KernelCapabilities.Descriptors = new uint[28];
|
||||
for (int i = 0; i < 28; i++)
|
||||
{
|
||||
arm11KernelCapabilities.Descriptors[i] = data.ReadUInt32();
|
||||
}
|
||||
arm11KernelCapabilities.Reserved = data.ReadBytes(0x10);
|
||||
|
||||
return arm11KernelCapabilities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ARM11 access control
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ARM11 access control on success, null on error</returns>
|
||||
private static ARM9AccessControl ParseARM9AccessControl(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var arm9AccessControl = new ARM9AccessControl();
|
||||
|
||||
arm9AccessControl.Descriptors = data.ReadBytes(15);
|
||||
arm9AccessControl.DescriptorVersion = data.ReadByteValue();
|
||||
|
||||
return arm9AccessControl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ExeFS header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ExeFS header on success, null on error</returns>
|
||||
private static ExeFSHeader ParseExeFSHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var exeFSHeader = new ExeFSHeader();
|
||||
|
||||
exeFSHeader.FileHeaders = new ExeFSFileHeader[10];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
exeFSHeader.FileHeaders[i] = ParseExeFSFileHeader(data);
|
||||
}
|
||||
exeFSHeader.Reserved = data.ReadBytes(0x20);
|
||||
exeFSHeader.FileHashes = new byte[10][];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
exeFSHeader.FileHashes[i] = data.ReadBytes(0x20) ?? [];
|
||||
}
|
||||
|
||||
return exeFSHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an ExeFS file header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled ExeFS file header on success, null on error</returns>
|
||||
private static ExeFSFileHeader ParseExeFSFileHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var exeFSFileHeader = new ExeFSFileHeader();
|
||||
|
||||
byte[]? fileName = data.ReadBytes(8);
|
||||
if (fileName != null)
|
||||
exeFSFileHeader.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
|
||||
exeFSFileHeader.FileOffset = data.ReadUInt32();
|
||||
exeFSFileHeader.FileSize = data.ReadUInt32();
|
||||
|
||||
return exeFSFileHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an RomFS header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled RomFS header on success, null on error</returns>
|
||||
private static RomFSHeader? ParseRomFSHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var romFSHeader = new RomFSHeader();
|
||||
|
||||
byte[]? magicString = data.ReadBytes(4);
|
||||
if (magicString == null)
|
||||
return null;
|
||||
|
||||
romFSHeader.MagicString = Encoding.ASCII.GetString(magicString).TrimEnd('\0');
|
||||
if (romFSHeader.MagicString != RomFSMagicNumber)
|
||||
return null;
|
||||
|
||||
romFSHeader.MagicNumber = data.ReadUInt32();
|
||||
if (romFSHeader.MagicNumber != RomFSSecondMagicNumber)
|
||||
return null;
|
||||
|
||||
romFSHeader.MasterHashSize = data.ReadUInt32();
|
||||
romFSHeader.Level1LogicalOffset = data.ReadUInt64();
|
||||
romFSHeader.Level1HashdataSize = data.ReadUInt64();
|
||||
romFSHeader.Level1BlockSizeLog2 = data.ReadUInt32();
|
||||
romFSHeader.Reserved1 = data.ReadBytes(4);
|
||||
romFSHeader.Level2LogicalOffset = data.ReadUInt64();
|
||||
romFSHeader.Level2HashdataSize = data.ReadUInt64();
|
||||
romFSHeader.Level2BlockSizeLog2 = data.ReadUInt32();
|
||||
romFSHeader.Reserved2 = data.ReadBytes(4);
|
||||
romFSHeader.Level3LogicalOffset = data.ReadUInt64();
|
||||
romFSHeader.Level3HashdataSize = data.ReadUInt64();
|
||||
romFSHeader.Level3BlockSizeLog2 = data.ReadUInt32();
|
||||
romFSHeader.Reserved3 = data.ReadBytes(4);
|
||||
romFSHeader.Reserved4 = data.ReadBytes(4);
|
||||
romFSHeader.OptionalInfoSize = data.ReadUInt32();
|
||||
|
||||
return romFSHeader;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.NCF;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class NCF : IStreamSerializer<Models.NCF.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.NCF.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new NCF();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.NCF.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life No Cache to fill
|
||||
var file = new Models.NCF.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the no cache header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Directory Header
|
||||
|
||||
// Try to parse the directory header
|
||||
var directoryHeader = ParseDirectoryHeader(data);
|
||||
if (directoryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache directory header
|
||||
file.DirectoryHeader = directoryHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Entries
|
||||
|
||||
// Create the directory entry array
|
||||
file.DirectoryEntries = new DirectoryEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.DirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Names
|
||||
|
||||
if (directoryHeader.NameSize > 0)
|
||||
{
|
||||
// Get the current offset for adjustment
|
||||
long directoryNamesStart = data.Position;
|
||||
|
||||
// Get the ending offset
|
||||
long directoryNamesEnd = data.Position + directoryHeader.NameSize;
|
||||
|
||||
// Create the string dictionary
|
||||
file.DirectoryNames = new Dictionary<long, string?>();
|
||||
|
||||
// Loop and read the null-terminated strings
|
||||
while (data.Position < directoryNamesEnd)
|
||||
{
|
||||
long nameOffset = data.Position - directoryNamesStart;
|
||||
string? directoryName = data.ReadString(Encoding.ASCII);
|
||||
if (data.Position > directoryNamesEnd)
|
||||
{
|
||||
data.Seek(-directoryName?.Length ?? 0, SeekOrigin.Current);
|
||||
byte[]? endingData = data.ReadBytes((int)(directoryNamesEnd - data.Position));
|
||||
if (endingData != null)
|
||||
directoryName = Encoding.ASCII.GetString(endingData);
|
||||
else
|
||||
directoryName = null;
|
||||
}
|
||||
|
||||
file.DirectoryNames[nameOffset] = directoryName;
|
||||
}
|
||||
|
||||
// Loop and assign to entries
|
||||
foreach (var directoryEntry in file.DirectoryEntries)
|
||||
{
|
||||
if (directoryEntry != null)
|
||||
directoryEntry.Name = file.DirectoryNames[directoryEntry.NameOffset];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 1 Entries
|
||||
|
||||
// Create the directory info 1 entry array
|
||||
file.DirectoryInfo1Entries = new DirectoryInfo1Entry[directoryHeader.Info1Count];
|
||||
|
||||
// Try to parse the directory info 1 entries
|
||||
for (int i = 0; i < directoryHeader.Info1Count; i++)
|
||||
{
|
||||
var directoryInfo1Entry = ParseDirectoryInfo1Entry(data);
|
||||
file.DirectoryInfo1Entries[i] = directoryInfo1Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Info 2 Entries
|
||||
|
||||
// Create the directory info 2 entry array
|
||||
file.DirectoryInfo2Entries = new DirectoryInfo2Entry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the directory info 2 entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var directoryInfo2Entry = ParseDirectoryInfo2Entry(data);
|
||||
file.DirectoryInfo2Entries[i] = directoryInfo2Entry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Copy Entries
|
||||
|
||||
// Create the directory copy entry array
|
||||
file.DirectoryCopyEntries = new DirectoryCopyEntry[directoryHeader.CopyCount];
|
||||
|
||||
// Try to parse the directory copy entries
|
||||
for (int i = 0; i < directoryHeader.CopyCount; i++)
|
||||
{
|
||||
var directoryCopyEntry = ParseDirectoryCopyEntry(data);
|
||||
file.DirectoryCopyEntries[i] = directoryCopyEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Local Entries
|
||||
|
||||
// Create the directory local entry array
|
||||
file.DirectoryLocalEntries = new DirectoryLocalEntry[directoryHeader.LocalCount];
|
||||
|
||||
// Try to parse the directory local entries
|
||||
for (int i = 0; i < directoryHeader.LocalCount; i++)
|
||||
{
|
||||
var directoryLocalEntry = ParseDirectoryLocalEntry(data);
|
||||
file.DirectoryLocalEntries[i] = directoryLocalEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of directory section, just in case
|
||||
data.Seek(initialOffset + directoryHeader.DirectorySize, SeekOrigin.Begin);
|
||||
|
||||
#region Unknown Header
|
||||
|
||||
// Try to parse the unknown header
|
||||
var unknownHeader = ParseUnknownHeader(data);
|
||||
if (unknownHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache unknown header
|
||||
file.UnknownHeader = unknownHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Entries
|
||||
|
||||
// Create the unknown entry array
|
||||
file.UnknownEntries = new UnknownEntry[directoryHeader.ItemCount];
|
||||
|
||||
// Try to parse the unknown entries
|
||||
for (int i = 0; i < directoryHeader.ItemCount; i++)
|
||||
{
|
||||
var unknownEntry = ParseUnknownEntry(data);
|
||||
file.UnknownEntries[i] = unknownEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Header
|
||||
|
||||
// Try to parse the checksum header
|
||||
var checksumHeader = ParseChecksumHeader(data);
|
||||
if (checksumHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum header
|
||||
file.ChecksumHeader = checksumHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
#region Checksum Map Header
|
||||
|
||||
// Try to parse the checksum map header
|
||||
var checksumMapHeader = ParseChecksumMapHeader(data);
|
||||
if (checksumMapHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the game cache checksum map header
|
||||
file.ChecksumMapHeader = checksumMapHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Map Entries
|
||||
|
||||
// Create the checksum map entry array
|
||||
file.ChecksumMapEntries = new ChecksumMapEntry[checksumMapHeader.ItemCount];
|
||||
|
||||
// Try to parse the checksum map entries
|
||||
for (int i = 0; i < checksumMapHeader.ItemCount; i++)
|
||||
{
|
||||
var checksumMapEntry = ParseChecksumMapEntry(data);
|
||||
file.ChecksumMapEntries[i] = checksumMapEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checksum Entries
|
||||
|
||||
// Create the checksum entry array
|
||||
file.ChecksumEntries = new ChecksumEntry[checksumMapHeader.ChecksumCount];
|
||||
|
||||
// Try to parse the checksum entries
|
||||
for (int i = 0; i < checksumMapHeader.ChecksumCount; i++)
|
||||
{
|
||||
var checksumEntry = ParseChecksumEntry(data);
|
||||
file.ChecksumEntries[i] = checksumEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Seek to end of checksum section, just in case
|
||||
data.Seek(initialOffset + checksumHeader.ChecksumSize, SeekOrigin.Begin);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.Dummy0 = data.ReadUInt32();
|
||||
if (header.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
header.MajorVersion = data.ReadUInt32();
|
||||
if (header.MajorVersion != 0x00000002)
|
||||
return null;
|
||||
|
||||
header.MinorVersion = data.ReadUInt32();
|
||||
if (header.MinorVersion != 1)
|
||||
return null;
|
||||
|
||||
header.CacheID = data.ReadUInt32();
|
||||
header.LastVersionPlayed = data.ReadUInt32();
|
||||
header.Dummy1 = data.ReadUInt32();
|
||||
header.Dummy2 = data.ReadUInt32();
|
||||
header.FileSize = data.ReadUInt32();
|
||||
header.BlockSize = data.ReadUInt32();
|
||||
header.BlockCount = data.ReadUInt32();
|
||||
header.Dummy3 = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory header on success, null on error</returns>
|
||||
private static DirectoryHeader? ParseDirectoryHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryHeader directoryHeader = new DirectoryHeader();
|
||||
|
||||
directoryHeader.Dummy0 = data.ReadUInt32();
|
||||
if (directoryHeader.Dummy0 != 0x00000004)
|
||||
return null;
|
||||
|
||||
directoryHeader.CacheID = data.ReadUInt32();
|
||||
directoryHeader.LastVersionPlayed = data.ReadUInt32();
|
||||
directoryHeader.ItemCount = data.ReadUInt32();
|
||||
directoryHeader.FileCount = data.ReadUInt32();
|
||||
directoryHeader.ChecksumDataLength = data.ReadUInt32();
|
||||
directoryHeader.DirectorySize = data.ReadUInt32();
|
||||
directoryHeader.NameSize = data.ReadUInt32();
|
||||
directoryHeader.Info1Count = data.ReadUInt32();
|
||||
directoryHeader.CopyCount = data.ReadUInt32();
|
||||
directoryHeader.LocalCount = data.ReadUInt32();
|
||||
directoryHeader.Dummy1 = data.ReadUInt32();
|
||||
directoryHeader.Dummy2 = data.ReadUInt32();
|
||||
directoryHeader.Checksum = data.ReadUInt32();
|
||||
|
||||
return directoryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.NameOffset = data.ReadUInt32();
|
||||
directoryEntry.ItemSize = data.ReadUInt32();
|
||||
directoryEntry.ChecksumIndex = data.ReadUInt32();
|
||||
directoryEntry.DirectoryFlags = (HL_NCF_FLAG)data.ReadUInt32();
|
||||
directoryEntry.ParentIndex = data.ReadUInt32();
|
||||
directoryEntry.NextIndex = data.ReadUInt32();
|
||||
directoryEntry.FirstIndex = data.ReadUInt32();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory info 1 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory info 1 entry on success, null on error</returns>
|
||||
private static DirectoryInfo1Entry ParseDirectoryInfo1Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo1Entry directoryInfo1Entry = new DirectoryInfo1Entry();
|
||||
|
||||
directoryInfo1Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo1Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory info 2 entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory info 2 entry on success, null on error</returns>
|
||||
private static DirectoryInfo2Entry ParseDirectoryInfo2Entry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryInfo2Entry directoryInfo2Entry = new DirectoryInfo2Entry();
|
||||
|
||||
directoryInfo2Entry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return directoryInfo2Entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory copy entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory copy entry on success, null on error</returns>
|
||||
private static DirectoryCopyEntry ParseDirectoryCopyEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryCopyEntry directoryCopyEntry = new DirectoryCopyEntry();
|
||||
|
||||
directoryCopyEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryCopyEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache directory local entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache directory local entry on success, null on error</returns>
|
||||
private static DirectoryLocalEntry ParseDirectoryLocalEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryLocalEntry directoryLocalEntry = new DirectoryLocalEntry();
|
||||
|
||||
directoryLocalEntry.DirectoryIndex = data.ReadUInt32();
|
||||
|
||||
return directoryLocalEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache unknown header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache unknown header on success, null on error</returns>
|
||||
private static UnknownHeader? ParseUnknownHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownHeader unknownHeader = new UnknownHeader();
|
||||
|
||||
unknownHeader.Dummy0 = data.ReadUInt32();
|
||||
if (unknownHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
unknownHeader.Dummy1 = data.ReadUInt32();
|
||||
if (unknownHeader.Dummy1 != 0x00000000)
|
||||
return null;
|
||||
|
||||
return unknownHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache unknown entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cacheunknown entry on success, null on error</returns>
|
||||
private static UnknownEntry ParseUnknownEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownEntry unknownEntry = new UnknownEntry();
|
||||
|
||||
unknownEntry.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return unknownEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum header on success, null on error</returns>
|
||||
private static ChecksumHeader? ParseChecksumHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumHeader checksumHeader = new ChecksumHeader();
|
||||
|
||||
checksumHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumHeader.Dummy0 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumHeader.ChecksumSize = data.ReadUInt32();
|
||||
|
||||
return checksumHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum map header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum map header on success, null on error</returns>
|
||||
private static ChecksumMapHeader? ParseChecksumMapHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapHeader checksumMapHeader = new ChecksumMapHeader();
|
||||
|
||||
checksumMapHeader.Dummy0 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy0 != 0x14893721)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.Dummy1 = data.ReadUInt32();
|
||||
if (checksumMapHeader.Dummy1 != 0x00000001)
|
||||
return null;
|
||||
|
||||
checksumMapHeader.ItemCount = data.ReadUInt32();
|
||||
checksumMapHeader.ChecksumCount = data.ReadUInt32();
|
||||
|
||||
return checksumMapHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum map entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum map entry on success, null on error</returns>
|
||||
private static ChecksumMapEntry ParseChecksumMapEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumMapEntry checksumMapEntry = new ChecksumMapEntry();
|
||||
|
||||
checksumMapEntry.ChecksumCount = data.ReadUInt32();
|
||||
checksumMapEntry.FirstChecksumIndex = data.ReadUInt32();
|
||||
|
||||
return checksumMapEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life No Cache checksum entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life No Cache checksum entry on success, null on error</returns>
|
||||
private static ChecksumEntry ParseChecksumEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ChecksumEntry checksumEntry = new ChecksumEntry();
|
||||
|
||||
checksumEntry.Checksum = data.ReadUInt32();
|
||||
|
||||
return checksumEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.NewExecutable;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.NewExecutable.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class NewExecutable : IStreamSerializer<Executable>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Executable? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new NewExecutable();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Executable? 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 executable to fill
|
||||
var executable = new Executable();
|
||||
|
||||
#region MS-DOS Stub
|
||||
|
||||
// Parse the MS-DOS stub
|
||||
var stub = new MSDOS().Deserialize(data);
|
||||
if (stub?.Header == null || stub.Header.NewExeHeaderAddr == 0)
|
||||
return null;
|
||||
|
||||
// Set the MS-DOS stub
|
||||
executable.Stub = stub;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Executable Header
|
||||
|
||||
// Try to parse the executable header
|
||||
data.Seek(initialOffset + stub.Header.NewExeHeaderAddr, SeekOrigin.Begin);
|
||||
var executableHeader = ParseExecutableHeader(data);
|
||||
if (executableHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the executable header
|
||||
executable.Header = executableHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Segment Table
|
||||
|
||||
// If the offset for the segment table doesn't exist
|
||||
int tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.SegmentTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the segment table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var segmentTable = ParseSegmentTable(data, executableHeader.FileSegmentCount);
|
||||
if (segmentTable == null)
|
||||
return null;
|
||||
|
||||
// Set the segment table
|
||||
executable.SegmentTable = segmentTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resource Table
|
||||
|
||||
// If the offset for the segment table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.SegmentTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the resource table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var resourceTable = ParseResourceTable(data, executableHeader.ResourceEntriesCount);
|
||||
if (resourceTable == null)
|
||||
return null;
|
||||
|
||||
// Set the resource table
|
||||
executable.ResourceTable = resourceTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Resident-Name Table
|
||||
|
||||
// If the offset for the resident-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ResidentNameTableOffset;
|
||||
int endOffset = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ModuleReferenceTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the resident-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var residentNameTable = ParseResidentNameTable(data, endOffset);
|
||||
if (residentNameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the resident-name table
|
||||
executable.ResidentNameTable = residentNameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Module-Reference Table
|
||||
|
||||
// If the offset for the module-reference table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ModuleReferenceTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the module-reference table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var moduleReferenceTable = ParseModuleReferenceTable(data, executableHeader.ModuleReferenceTableSize);
|
||||
if (moduleReferenceTable == null)
|
||||
return null;
|
||||
|
||||
// Set the module-reference table
|
||||
executable.ModuleReferenceTable = moduleReferenceTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Imported-Name Table
|
||||
|
||||
// If the offset for the imported-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.ImportedNamesTableOffset;
|
||||
endOffset = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.EntryTableOffset;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the imported-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var importedNameTable = ParseImportedNameTable(data, endOffset);
|
||||
if (importedNameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the imported-name table
|
||||
executable.ImportedNameTable = importedNameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entry Table
|
||||
|
||||
// If the offset for the imported-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.EntryTableOffset;
|
||||
endOffset = initialOffset
|
||||
+ (int)stub.Header.NewExeHeaderAddr
|
||||
+ executableHeader.EntryTableOffset
|
||||
+ executableHeader.EntryTableSize;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the imported-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var entryTable = ParseEntryTable(data, endOffset);
|
||||
if (entryTable == null)
|
||||
return null;
|
||||
|
||||
// Set the entry table
|
||||
executable.EntryTable = entryTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nonresident-Name Table
|
||||
|
||||
// If the offset for the nonresident-name table doesn't exist
|
||||
tableAddress = initialOffset
|
||||
+ (int)executableHeader.NonResidentNamesTableOffset;
|
||||
endOffset = initialOffset
|
||||
+ (int)executableHeader.NonResidentNamesTableOffset
|
||||
+ executableHeader.NonResidentNameTableSize;
|
||||
if (tableAddress >= data.Length)
|
||||
return executable;
|
||||
|
||||
// Try to parse the nonresident-name table
|
||||
data.Seek(tableAddress, SeekOrigin.Begin);
|
||||
var nonResidentNameTable = ParseNonResidentNameTable(data, endOffset);
|
||||
if (nonResidentNameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the nonresident-name table
|
||||
executable.NonResidentNameTable = nonResidentNameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
return executable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a New Executable header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled executable header on success, null on error</returns>
|
||||
public static ExecutableHeader? ParseExecutableHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new ExecutableHeader();
|
||||
|
||||
byte[]? magic = data.ReadBytes(2);
|
||||
if (magic == null)
|
||||
return null;
|
||||
|
||||
header.Magic = Encoding.ASCII.GetString(magic);
|
||||
if (header.Magic != SignatureString)
|
||||
return null;
|
||||
|
||||
header.LinkerVersion = data.ReadByteValue();
|
||||
header.LinkerRevision = data.ReadByteValue();
|
||||
header.EntryTableOffset = data.ReadUInt16();
|
||||
header.EntryTableSize = data.ReadUInt16();
|
||||
header.CrcChecksum = data.ReadUInt32();
|
||||
header.FlagWord = (HeaderFlag)data.ReadUInt16();
|
||||
header.AutomaticDataSegmentNumber = data.ReadUInt16();
|
||||
header.InitialHeapAlloc = data.ReadUInt16();
|
||||
header.InitialStackAlloc = data.ReadUInt16();
|
||||
header.InitialCSIPSetting = data.ReadUInt32();
|
||||
header.InitialSSSPSetting = data.ReadUInt32();
|
||||
header.FileSegmentCount = data.ReadUInt16();
|
||||
header.ModuleReferenceTableSize = data.ReadUInt16();
|
||||
header.NonResidentNameTableSize = data.ReadUInt16();
|
||||
header.SegmentTableOffset = data.ReadUInt16();
|
||||
header.ResourceTableOffset = data.ReadUInt16();
|
||||
header.ResidentNameTableOffset = data.ReadUInt16();
|
||||
header.ModuleReferenceTableOffset = data.ReadUInt16();
|
||||
header.ImportedNamesTableOffset = data.ReadUInt16();
|
||||
header.NonResidentNamesTableOffset = data.ReadUInt32();
|
||||
header.MovableEntriesCount = data.ReadUInt16();
|
||||
header.SegmentAlignmentShiftCount = data.ReadUInt16();
|
||||
header.ResourceEntriesCount = data.ReadUInt16();
|
||||
header.TargetOperatingSystem = (OperatingSystem)data.ReadByteValue();
|
||||
header.AdditionalFlags = (OS2Flag)data.ReadByteValue();
|
||||
header.ReturnThunkOffset = data.ReadUInt16();
|
||||
header.SegmentReferenceThunkOffset = data.ReadUInt16();
|
||||
header.MinCodeSwapAreaSize = data.ReadUInt16();
|
||||
header.WindowsSDKRevision = data.ReadByteValue();
|
||||
header.WindowsSDKVersion = data.ReadByteValue();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a segment table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of segment table entries to read</param>
|
||||
/// <returns>Filled segment table on success, null on error</returns>
|
||||
public static SegmentTableEntry[] ParseSegmentTable(Stream data, int count)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var segmentTable = new SegmentTableEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = new SegmentTableEntry();
|
||||
entry.Offset = data.ReadUInt16();
|
||||
entry.Length = data.ReadUInt16();
|
||||
entry.FlagWord = (SegmentTableEntryFlag)data.ReadUInt16();
|
||||
entry.MinimumAllocationSize = data.ReadUInt16();
|
||||
segmentTable[i] = entry;
|
||||
}
|
||||
|
||||
return segmentTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a resource table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of resource table entries to read</param>
|
||||
/// <returns>Filled resource table on success, null on error</returns>
|
||||
public static ResourceTable ParseResourceTable(Stream data, int count)
|
||||
{
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// TODO: Use marshalling here instead of building
|
||||
var resourceTable = new ResourceTable();
|
||||
|
||||
resourceTable.AlignmentShiftCount = data.ReadUInt16();
|
||||
resourceTable.ResourceTypes = new ResourceTypeInformationEntry[count];
|
||||
for (int i = 0; i < resourceTable.ResourceTypes.Length; i++)
|
||||
{
|
||||
var entry = new ResourceTypeInformationEntry();
|
||||
entry.TypeID = data.ReadUInt16();
|
||||
entry.ResourceCount = data.ReadUInt16();
|
||||
entry.Reserved = data.ReadUInt32();
|
||||
entry.Resources = new ResourceTypeResourceEntry[entry.ResourceCount];
|
||||
for (int j = 0; j < entry.ResourceCount; j++)
|
||||
{
|
||||
// TODO: Should we read and store the resource data?
|
||||
var resource = new ResourceTypeResourceEntry();
|
||||
resource.Offset = data.ReadUInt16();
|
||||
resource.Length = data.ReadUInt16();
|
||||
resource.FlagWord = (ResourceTypeResourceFlag)data.ReadUInt16();
|
||||
resource.ResourceID = data.ReadUInt16();
|
||||
resource.Reserved = data.ReadUInt32();
|
||||
entry.Resources[j] = resource;
|
||||
}
|
||||
resourceTable.ResourceTypes[i] = entry;
|
||||
}
|
||||
|
||||
// Get the full list of unique string offsets
|
||||
var stringOffsets = resourceTable.ResourceTypes
|
||||
.Where(rt => rt != null)
|
||||
.Where(rt => rt!.IsIntegerType() == false)
|
||||
.Select(rt => rt!.TypeID)
|
||||
.Union(resourceTable.ResourceTypes
|
||||
.Where(rt => rt != null)
|
||||
.SelectMany(rt => rt!.Resources ?? [])
|
||||
.Where(r => r!.IsIntegerType() == false)
|
||||
.Select(r => r!.ResourceID))
|
||||
.Distinct()
|
||||
.OrderBy(o => o)
|
||||
.ToList();
|
||||
|
||||
// Populate the type and name string dictionary
|
||||
resourceTable.TypeAndNameStrings = [];
|
||||
for (int i = 0; i < stringOffsets.Count; i++)
|
||||
{
|
||||
int stringOffset = (int)(stringOffsets[i] + initialOffset);
|
||||
data.Seek(stringOffset, SeekOrigin.Begin);
|
||||
var str = new ResourceTypeAndNameString();
|
||||
str.Length = data.ReadByteValue();
|
||||
str.Text = data.ReadBytes(str.Length);
|
||||
resourceTable.TypeAndNameStrings[stringOffsets[i]] = str;
|
||||
}
|
||||
|
||||
return resourceTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a resident-name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the resident-name table</param>
|
||||
/// <returns>Filled resident-name table on success, null on error</returns>
|
||||
public static ResidentNameTableEntry[] ParseResidentNameTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var residentNameTable = new List<ResidentNameTableEntry>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
var entry = new ResidentNameTableEntry();
|
||||
entry.Length = data.ReadByteValue();
|
||||
entry.NameString = data.ReadBytes(entry.Length);
|
||||
entry.OrdinalNumber = data.ReadUInt16();
|
||||
residentNameTable.Add(entry);
|
||||
}
|
||||
|
||||
return [.. residentNameTable];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a module-reference table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of module-reference table entries to read</param>
|
||||
/// <returns>Filled module-reference table on success, null on error</returns>
|
||||
public static ModuleReferenceTableEntry[] ParseModuleReferenceTable(Stream data, int count)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var moduleReferenceTable = new ModuleReferenceTableEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entry = new ModuleReferenceTableEntry();
|
||||
entry.Offset = data.ReadUInt16();
|
||||
moduleReferenceTable[i] = entry;
|
||||
}
|
||||
|
||||
return moduleReferenceTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an imported-name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the imported-name table</param>
|
||||
/// <returns>Filled imported-name table on success, null on error</returns>
|
||||
public static Dictionary<ushort, ImportedNameTableEntry?> ParseImportedNameTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var importedNameTable = new Dictionary<ushort, ImportedNameTableEntry?>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
ushort currentOffset = (ushort)data.Position;
|
||||
var entry = new ImportedNameTableEntry();
|
||||
entry.Length = data.ReadByteValue();
|
||||
entry.NameString = data.ReadBytes(entry.Length);
|
||||
importedNameTable[currentOffset] = entry;
|
||||
}
|
||||
|
||||
return importedNameTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an entry table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the entry table</param>
|
||||
/// <returns>Filled entry table on success, null on error</returns>
|
||||
public static EntryTableBundle[] ParseEntryTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var entryTable = new List<EntryTableBundle>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
var entry = new EntryTableBundle();
|
||||
entry.EntryCount = data.ReadByteValue();
|
||||
entry.SegmentIndicator = data.ReadByteValue();
|
||||
switch (entry.GetEntryType())
|
||||
{
|
||||
case SegmentEntryType.Unused:
|
||||
break;
|
||||
|
||||
case SegmentEntryType.FixedSegment:
|
||||
entry.FixedFlagWord = (FixedSegmentEntryFlag)data.ReadByteValue();
|
||||
entry.FixedOffset = data.ReadUInt16();
|
||||
break;
|
||||
|
||||
case SegmentEntryType.MoveableSegment:
|
||||
entry.MoveableFlagWord = (MoveableSegmentEntryFlag)data.ReadByteValue();
|
||||
entry.MoveableReserved = data.ReadUInt16();
|
||||
entry.MoveableSegmentNumber = data.ReadByteValue();
|
||||
entry.MoveableOffset = data.ReadUInt16();
|
||||
break;
|
||||
}
|
||||
entryTable.Add(entry);
|
||||
}
|
||||
|
||||
return [.. entryTable];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a nonresident-name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="endOffset">First address not part of the nonresident-name table</param>
|
||||
/// <returns>Filled nonresident-name table on success, null on error</returns>
|
||||
public static NonResidentNameTableEntry[] ParseNonResidentNameTable(Stream data, int endOffset)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var residentNameTable = new List<NonResidentNameTableEntry>();
|
||||
|
||||
while (data.Position < endOffset)
|
||||
{
|
||||
var entry = new NonResidentNameTableEntry();
|
||||
entry.Length = data.ReadByteValue();
|
||||
entry.NameString = data.ReadBytes(entry.Length);
|
||||
entry.OrdinalNumber = data.ReadUInt16();
|
||||
residentNameTable.Add(entry);
|
||||
}
|
||||
|
||||
return [.. residentNameTable];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.Nitro;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Nitro : IStreamSerializer<Cart>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Cart? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Nitro();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Cart? 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 cart image to fill
|
||||
var cart = new Cart();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseCommonHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the cart image header
|
||||
cart.CommonHeader = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended DSi Header
|
||||
|
||||
// If we have a DSi-compatible cartridge
|
||||
if (header.UnitCode == Unitcode.NDSPlusDSi || header.UnitCode == Unitcode.DSi)
|
||||
{
|
||||
var extendedDSiHeader = ParseExtendedDSiHeader(data);
|
||||
if (extendedDSiHeader == null)
|
||||
return null;
|
||||
|
||||
cart.ExtendedDSiHeader = extendedDSiHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Secure Area
|
||||
|
||||
// Try to get the secure area offset
|
||||
long secureAreaOffset = 0x4000;
|
||||
if (secureAreaOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the secure area
|
||||
data.Seek(secureAreaOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the secure area without processing
|
||||
cart.SecureArea = data.ReadBytes(0x800);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Name Table
|
||||
|
||||
// Try to get the name table offset
|
||||
long nameTableOffset = header.FileNameTableOffset;
|
||||
if (nameTableOffset < 0 || nameTableOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the name table
|
||||
data.Seek(nameTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the name table
|
||||
var nameTable = ParseNameTable(data);
|
||||
if (nameTable == null)
|
||||
return null;
|
||||
|
||||
// Set the name table
|
||||
cart.NameTable = nameTable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Allocation Table
|
||||
|
||||
// Try to get the file allocation table offset
|
||||
long fileAllocationTableOffset = header.FileAllocationTableOffset;
|
||||
if (fileAllocationTableOffset < 0 || fileAllocationTableOffset > data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the file allocation table
|
||||
data.Seek(fileAllocationTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the file allocation table
|
||||
var fileAllocationTable = new List<FileAllocationTableEntry>();
|
||||
|
||||
// Try to parse the file allocation table
|
||||
while (data.Position - fileAllocationTableOffset < header.FileAllocationTableLength)
|
||||
{
|
||||
var entry = ParseFileAllocationTableEntry(data);
|
||||
fileAllocationTable.Add(entry);
|
||||
}
|
||||
|
||||
// Set the file allocation table
|
||||
cart.FileAllocationTable = fileAllocationTable.ToArray();
|
||||
|
||||
#endregion
|
||||
|
||||
// TODO: Read and optionally parse out the other areas
|
||||
// Look for offsets and lengths in the header pieces
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a common header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled common header on success, null on error</returns>
|
||||
private static CommonHeader ParseCommonHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
CommonHeader commonHeader = new CommonHeader();
|
||||
|
||||
byte[]? gameTitle = data.ReadBytes(12);
|
||||
if (gameTitle != null)
|
||||
commonHeader.GameTitle = Encoding.ASCII.GetString(gameTitle).TrimEnd('\0');
|
||||
commonHeader.GameCode = data.ReadUInt32();
|
||||
byte[]? makerCode = data.ReadBytes(2);
|
||||
if (makerCode != null)
|
||||
commonHeader.MakerCode = Encoding.ASCII.GetString(bytes: makerCode).TrimEnd('\0');
|
||||
commonHeader.UnitCode = (Unitcode)data.ReadByteValue();
|
||||
commonHeader.EncryptionSeedSelect = data.ReadByteValue();
|
||||
commonHeader.DeviceCapacity = data.ReadByteValue();
|
||||
commonHeader.Reserved1 = data.ReadBytes(7);
|
||||
commonHeader.GameRevision = data.ReadUInt16();
|
||||
commonHeader.RomVersion = data.ReadByteValue();
|
||||
commonHeader.InternalFlags = data.ReadByteValue();
|
||||
commonHeader.ARM9RomOffset = data.ReadUInt32();
|
||||
commonHeader.ARM9EntryAddress = data.ReadUInt32();
|
||||
commonHeader.ARM9LoadAddress = data.ReadUInt32();
|
||||
commonHeader.ARM9Size = data.ReadUInt32();
|
||||
commonHeader.ARM7RomOffset = data.ReadUInt32();
|
||||
commonHeader.ARM7EntryAddress = data.ReadUInt32();
|
||||
commonHeader.ARM7LoadAddress = data.ReadUInt32();
|
||||
commonHeader.ARM7Size = data.ReadUInt32();
|
||||
commonHeader.FileNameTableOffset = data.ReadUInt32();
|
||||
commonHeader.FileNameTableLength = data.ReadUInt32();
|
||||
commonHeader.FileAllocationTableOffset = data.ReadUInt32();
|
||||
commonHeader.FileAllocationTableLength = data.ReadUInt32();
|
||||
commonHeader.ARM9OverlayOffset = data.ReadUInt32();
|
||||
commonHeader.ARM9OverlayLength = data.ReadUInt32();
|
||||
commonHeader.ARM7OverlayOffset = data.ReadUInt32();
|
||||
commonHeader.ARM7OverlayLength = data.ReadUInt32();
|
||||
commonHeader.NormalCardControlRegisterSettings = data.ReadUInt32();
|
||||
commonHeader.SecureCardControlRegisterSettings = data.ReadUInt32();
|
||||
commonHeader.IconBannerOffset = data.ReadUInt32();
|
||||
commonHeader.SecureAreaCRC = data.ReadUInt16();
|
||||
commonHeader.SecureTransferTimeout = data.ReadUInt16();
|
||||
commonHeader.ARM9Autoload = data.ReadUInt32();
|
||||
commonHeader.ARM7Autoload = data.ReadUInt32();
|
||||
commonHeader.SecureDisable = data.ReadBytes(8);
|
||||
commonHeader.NTRRegionRomSize = data.ReadUInt32();
|
||||
commonHeader.HeaderSize = data.ReadUInt32();
|
||||
commonHeader.Reserved2 = data.ReadBytes(56);
|
||||
commonHeader.NintendoLogo = data.ReadBytes(156);
|
||||
commonHeader.NintendoLogoCRC = data.ReadUInt16();
|
||||
commonHeader.HeaderCRC = data.ReadUInt16();
|
||||
commonHeader.DebuggerReserved = data.ReadBytes(0x20);
|
||||
|
||||
return commonHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an extended DSi header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled extended DSi header on success, null on error</returns>
|
||||
private static ExtendedDSiHeader ParseExtendedDSiHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ExtendedDSiHeader extendedDSiHeader = new ExtendedDSiHeader();
|
||||
|
||||
extendedDSiHeader.GlobalMBK15Settings = new uint[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
extendedDSiHeader.GlobalMBK15Settings[i] = data.ReadUInt32();
|
||||
}
|
||||
extendedDSiHeader.LocalMBK68SettingsARM9 = new uint[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
extendedDSiHeader.LocalMBK68SettingsARM9[i] = data.ReadUInt32();
|
||||
}
|
||||
extendedDSiHeader.LocalMBK68SettingsARM7 = new uint[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
extendedDSiHeader.LocalMBK68SettingsARM7[i] = data.ReadUInt32();
|
||||
}
|
||||
extendedDSiHeader.GlobalMBK9Setting = data.ReadUInt32();
|
||||
extendedDSiHeader.RegionFlags = data.ReadUInt32();
|
||||
extendedDSiHeader.AccessControl = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7SCFGEXTMask = data.ReadUInt32();
|
||||
extendedDSiHeader.ReservedFlags = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM9iRomOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.Reserved3 = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM9iLoadAddress = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM9iSize = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7iRomOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.Reserved4 = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7iLoadAddress = data.ReadUInt32();
|
||||
extendedDSiHeader.ARM7iSize = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestNTRRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestNTRRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestTWLRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestTWLRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestSectorHashtableRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestSectorHashtableRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestBlockHashtableRegionOffset = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestBlockHashtableRegionLength = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestSectorSize = data.ReadUInt32();
|
||||
extendedDSiHeader.DigestBlockSectorCount = data.ReadUInt32();
|
||||
extendedDSiHeader.IconBannerSize = data.ReadUInt32();
|
||||
extendedDSiHeader.Unknown1 = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea1Offset = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea1Size = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea2Offset = data.ReadUInt32();
|
||||
extendedDSiHeader.ModcryptArea2Size = data.ReadUInt32();
|
||||
extendedDSiHeader.TitleID = data.ReadBytes(8);
|
||||
extendedDSiHeader.DSiWarePublicSavSize = data.ReadUInt32();
|
||||
extendedDSiHeader.DSiWarePrivateSavSize = data.ReadUInt32();
|
||||
extendedDSiHeader.ReservedZero = data.ReadBytes(176);
|
||||
extendedDSiHeader.Unknown2 = data.ReadBytes(0x10);
|
||||
extendedDSiHeader.ARM9WithSecureAreaSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.ARM7SHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.DigestMasterSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.BannerSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.ARM9iDecryptedSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.ARM7iDecryptedSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.Reserved5 = data.ReadBytes(40);
|
||||
extendedDSiHeader.ARM9NoSecureAreaSHA1HMACHash = data.ReadBytes(20);
|
||||
extendedDSiHeader.Reserved6 = data.ReadBytes(2636);
|
||||
extendedDSiHeader.ReservedAndUnchecked = data.ReadBytes(0x180);
|
||||
extendedDSiHeader.RSASignature = data.ReadBytes(0x80);
|
||||
|
||||
return extendedDSiHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a name table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled name table on success, null on error</returns>
|
||||
private static NameTable ParseNameTable(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
NameTable nameTable = new NameTable();
|
||||
|
||||
// Create a variable-length table
|
||||
var folderAllocationTable = new List<FolderAllocationTableEntry>();
|
||||
int entryCount = int.MaxValue;
|
||||
while (entryCount > 0)
|
||||
{
|
||||
var entry = ParseFolderAllocationTableEntry(data);
|
||||
folderAllocationTable.Add(entry);
|
||||
|
||||
// If we have the root entry
|
||||
if (entryCount == int.MaxValue)
|
||||
entryCount = (entry.Unknown << 8) | entry.ParentFolderIndex;
|
||||
|
||||
// Decrement the entry count
|
||||
entryCount--;
|
||||
}
|
||||
|
||||
// Assign the folder allocation table
|
||||
nameTable.FolderAllocationTable = folderAllocationTable.ToArray();
|
||||
|
||||
// Create a variable-length table
|
||||
var nameList = new List<NameListEntry>();
|
||||
while (true)
|
||||
{
|
||||
var entry = ParseNameListEntry(data);
|
||||
if (entry == null)
|
||||
break;
|
||||
|
||||
nameList.Add(entry);
|
||||
}
|
||||
|
||||
// Assign the name list
|
||||
nameTable.NameList = nameList.ToArray();
|
||||
|
||||
return nameTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a folder allocation table entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled folder allocation table entry on success, null on error</returns>
|
||||
private static FolderAllocationTableEntry ParseFolderAllocationTableEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FolderAllocationTableEntry entry = new FolderAllocationTableEntry();
|
||||
|
||||
entry.StartOffset = data.ReadUInt32();
|
||||
entry.FirstFileIndex = data.ReadUInt16();
|
||||
entry.ParentFolderIndex = data.ReadByteValue();
|
||||
entry.Unknown = data.ReadByteValue();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a name list entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled name list entry on success, null on error</returns>
|
||||
private static NameListEntry? ParseNameListEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
NameListEntry entry = new NameListEntry();
|
||||
|
||||
byte flagAndSize = data.ReadByteValue();
|
||||
if (flagAndSize == 0xFF)
|
||||
return null;
|
||||
|
||||
entry.Folder = (flagAndSize & 0x80) != 0;
|
||||
|
||||
byte size = (byte)(flagAndSize & ~0x80);
|
||||
if (size > 0)
|
||||
{
|
||||
byte[]? name = data.ReadBytes(size);
|
||||
if (name != null)
|
||||
entry.Name = Encoding.UTF8.GetString(name);
|
||||
}
|
||||
|
||||
if (entry.Folder)
|
||||
entry.Index = data.ReadUInt16();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a name list entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled name list entry on success, null on error</returns>
|
||||
private static FileAllocationTableEntry ParseFileAllocationTableEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FileAllocationTableEntry entry = new FileAllocationTableEntry();
|
||||
|
||||
entry.StartOffset = data.ReadUInt32();
|
||||
entry.EndOffset = data.ReadUInt32();
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class OfflineList : XmlFile<Models.OfflineList.Dat>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.OfflineList.Dat? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new OfflineList();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class OpenMSX : XmlFile<Models.OpenMSX.SoftwareDb>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.OpenMSX.SoftwareDb? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new OpenMSX();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PAK;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PAK.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class PAK : IStreamSerializer<Models.PAK.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.PAK.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PAK();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.PAK.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life Package to fill
|
||||
var file = new Models.PAK.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Items
|
||||
|
||||
// Get the directory items offset
|
||||
uint directoryItemsOffset = header.DirectoryOffset;
|
||||
if (directoryItemsOffset < 0 || directoryItemsOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the directory items
|
||||
data.Seek(directoryItemsOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the directory item array
|
||||
file.DirectoryItems = new DirectoryItem[header.DirectoryLength / 64];
|
||||
|
||||
// Try to parse the directory items
|
||||
for (int i = 0; i < file.DirectoryItems.Length; i++)
|
||||
{
|
||||
var directoryItem = ParseDirectoryItem(data);
|
||||
file.DirectoryItems[i] = directoryItem;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Package header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Package header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.DirectoryOffset = data.ReadUInt32();
|
||||
header.DirectoryLength = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Package directory item
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Package directory item on success, null on error</returns>
|
||||
private static DirectoryItem ParseDirectoryItem(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryItem directoryItem = new DirectoryItem();
|
||||
|
||||
byte[]? itemName = data.ReadBytes(56);
|
||||
if (itemName != null)
|
||||
directoryItem.ItemName = Encoding.ASCII.GetString(itemName).TrimEnd('\0');
|
||||
directoryItem.ItemOffset = data.ReadUInt32();
|
||||
directoryItem.ItemLength = data.ReadUInt32();
|
||||
|
||||
return directoryItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PFF;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PFF.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class PFF : IStreamSerializer<Archive>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PFF();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 Segments
|
||||
|
||||
// Get the segments
|
||||
long offset = header.FileListOffset;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the segments
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Create the segments array
|
||||
archive.Segments = new Segment[header.NumberOfFiles];
|
||||
|
||||
// Read all segments in turn
|
||||
for (int i = 0; i < header.NumberOfFiles; i++)
|
||||
{
|
||||
var file = ParseSegment(data, header.FileSegmentSize);
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
archive.Segments[i] = file;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Footer
|
||||
|
||||
// Get the footer offset
|
||||
offset = header.FileListOffset + (header.FileSegmentSize * header.NumberOfFiles);
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the footer
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the footer
|
||||
var footer = ParseFooter(data);
|
||||
if (footer == null)
|
||||
return null;
|
||||
|
||||
// Set the archive footer
|
||||
archive.Footer = footer;
|
||||
|
||||
#endregion
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.HeaderSize = data.ReadUInt32();
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
header.NumberOfFiles = data.ReadUInt32();
|
||||
header.FileSegmentSize = data.ReadUInt32();
|
||||
switch (header.Signature)
|
||||
{
|
||||
case Version0SignatureString:
|
||||
if (header.FileSegmentSize != Version0HSegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
case Version2SignatureString:
|
||||
if (header.FileSegmentSize != Version2SegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
// Version 3 can sometimes have Version 2 segment sizes
|
||||
case Version3SignatureString:
|
||||
if (header.FileSegmentSize != Version2SegmentSize && header.FileSegmentSize != Version3SegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
case Version4SignatureString:
|
||||
if (header.FileSegmentSize != Version4SegmentSize)
|
||||
return null;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
header.FileListOffset = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a footer
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled footer on success, null on error</returns>
|
||||
private static Footer ParseFooter(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Footer footer = new Footer();
|
||||
|
||||
footer.SystemIP = data.ReadUInt32();
|
||||
footer.Reserved = data.ReadUInt32();
|
||||
byte[]? kingTag = data.ReadBytes(4);
|
||||
if (kingTag != null)
|
||||
footer.KingTag = Encoding.ASCII.GetString(kingTag);
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="segmentSize">PFF segment size</param>
|
||||
/// <returns>Filled file entry on success, null on error</returns>
|
||||
private static Segment ParseSegment(Stream data, uint segmentSize)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Segment segment = new Segment();
|
||||
|
||||
segment.Deleted = data.ReadUInt32();
|
||||
segment.FileLocation = data.ReadUInt32();
|
||||
segment.FileSize = data.ReadUInt32();
|
||||
segment.PackedDate = data.ReadUInt32();
|
||||
byte[]? fileName = data.ReadBytes(0x10);
|
||||
if (fileName != null)
|
||||
segment.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
|
||||
if (segmentSize > Version2SegmentSize)
|
||||
segment.ModifiedDate = data.ReadUInt32();
|
||||
if (segmentSize > Version3SegmentSize)
|
||||
segment.CompressionLevel = data.ReadUInt32();
|
||||
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PIC;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PIC.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class PIC : IStreamSerializer<DiscInformation>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static DiscInformation? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PIC();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DiscInformation? 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;
|
||||
|
||||
var di = new DiscInformation();
|
||||
|
||||
// Read the initial disc information
|
||||
di.DataStructureLength = data.ReadUInt16BigEndian();
|
||||
di.Reserved0 = data.ReadByteValue();
|
||||
di.Reserved1 = data.ReadByteValue();
|
||||
|
||||
// Create a list for the units
|
||||
var diUnits = new List<DiscInformationUnit>();
|
||||
|
||||
// Loop and read all available units
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
var unit = ParseDiscInformationUnit(data);
|
||||
if (unit == null)
|
||||
continue;
|
||||
|
||||
diUnits.Add(unit);
|
||||
}
|
||||
|
||||
// Assign the units and return
|
||||
di.Units = diUnits.ToArray();
|
||||
return di;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit on success, null on error</returns>
|
||||
private static DiscInformationUnit? ParseDiscInformationUnit(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var unit = new DiscInformationUnit();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseDiscInformationUnitHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the information unit header
|
||||
unit.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Body
|
||||
|
||||
// Try to parse the body
|
||||
var body = ParseDiscInformationUnitBody(data);
|
||||
if (body == null)
|
||||
return null;
|
||||
|
||||
// Set the information unit body
|
||||
unit.Body = body;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trailer
|
||||
|
||||
if (unit.Body.DiscTypeIdentifier == DiscTypeIdentifierReWritable || unit.Body.DiscTypeIdentifier == DiscTypeIdentifierRecordable)
|
||||
{
|
||||
// Try to parse the trailer
|
||||
var trailer = ParseDiscInformationUnitTrailer(data);
|
||||
if (trailer == null)
|
||||
return null;
|
||||
|
||||
// Set the information unit trailer
|
||||
unit.Trailer = trailer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return unit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit header on success, null on error</returns>
|
||||
private static DiscInformationUnitHeader? ParseDiscInformationUnitHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var header = new DiscInformationUnitHeader();
|
||||
|
||||
// We only accept Disc Information units, not Emergency Brake or other
|
||||
byte[]? dic = data.ReadBytes(2);
|
||||
if (dic == null)
|
||||
return null;
|
||||
|
||||
header.DiscInformationIdentifier = Encoding.ASCII.GetString(dic);
|
||||
if (header.DiscInformationIdentifier != "DI")
|
||||
return null;
|
||||
|
||||
header.DiscInformationFormat = data.ReadByteValue();
|
||||
header.NumberOfUnitsInBlock = data.ReadByteValue();
|
||||
header.Reserved0 = data.ReadByteValue();
|
||||
header.SequenceNumber = data.ReadByteValue();
|
||||
header.BytesInUse = data.ReadByteValue();
|
||||
header.Reserved1 = data.ReadByteValue();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit body
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit body on success, null on error</returns>
|
||||
private static DiscInformationUnitBody? ParseDiscInformationUnitBody(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var body = new DiscInformationUnitBody();
|
||||
|
||||
byte[]? dti = data.ReadBytes(3);
|
||||
if (dti == null)
|
||||
return null;
|
||||
|
||||
body.DiscTypeIdentifier = Encoding.ASCII.GetString(dti);
|
||||
body.DiscSizeClassVersion = data.ReadByteValue();
|
||||
switch (body.DiscTypeIdentifier)
|
||||
{
|
||||
case DiscTypeIdentifierROM:
|
||||
case DiscTypeIdentifierROMUltra:
|
||||
case DiscTypeIdentifierXGD4:
|
||||
body.FormatDependentContents = data.ReadBytes(52);
|
||||
break;
|
||||
case DiscTypeIdentifierReWritable:
|
||||
case DiscTypeIdentifierRecordable:
|
||||
body.FormatDependentContents = data.ReadBytes(100);
|
||||
break;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a disc information unit trailer
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled disc information unit trailer on success, null on error</returns>
|
||||
private static DiscInformationUnitTrailer ParseDiscInformationUnitTrailer(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
var trailer = new DiscInformationUnitTrailer();
|
||||
|
||||
trailer.DiscManufacturerID = data.ReadBytes(6);
|
||||
trailer.MediaTypeID = data.ReadBytes(3);
|
||||
trailer.TimeStamp = data.ReadUInt16();
|
||||
trailer.ProductRevisionNumber = data.ReadByteValue();
|
||||
|
||||
return trailer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PlayJ;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.PlayJ.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class PlayJAudio : IStreamSerializer<AudioFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static AudioFile? DeserializeStream(Stream? data, long adjust = 0)
|
||||
{
|
||||
var deserializer = new PlayJAudio();
|
||||
return deserializer.Deserialize(data, adjust);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioFile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, 0);
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
/// <param name="adjust">Offset to adjust all seeking by</param>
|
||||
public AudioFile? Deserialize(Stream? data, long adjust)
|
||||
{
|
||||
// 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 audio file to fill
|
||||
var audioFile = new AudioFile();
|
||||
|
||||
#region Audio Header
|
||||
|
||||
// Try to parse the audio header
|
||||
var audioHeader = ParseAudioHeader(data);
|
||||
if (audioHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the audio header
|
||||
audioFile.Header = audioHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Block 1
|
||||
|
||||
uint unknownOffset1 = (audioHeader.Version == 0x00000000)
|
||||
? (audioHeader as AudioHeaderV1)?.UnknownOffset1 ?? 0
|
||||
: ((audioHeader as AudioHeaderV2)?.UnknownOffset1 ?? 0) + 0x54;
|
||||
|
||||
// If we have an unknown block 1 offset
|
||||
if (unknownOffset1 > 0)
|
||||
{
|
||||
// Get the unknown block 1 offset
|
||||
long offset = unknownOffset1 + adjust;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the unknown block 1
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Try to parse the unknown block 1
|
||||
var unknownBlock1 = ParseUnknownBlock1(data);
|
||||
if (unknownBlock1 == null)
|
||||
return null;
|
||||
|
||||
// Set the unknown block 1
|
||||
audioFile.UnknownBlock1 = unknownBlock1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region V1 Only
|
||||
|
||||
// If we have a V1 file
|
||||
if (audioHeader.Version == 0x00000000)
|
||||
{
|
||||
#region Unknown Value 2
|
||||
|
||||
// Get the V1 unknown offset 2
|
||||
uint? unknownOffset2 = (audioHeader as AudioHeaderV1)?.UnknownOffset2;
|
||||
|
||||
// If we have an unknown value 2 offset
|
||||
if (unknownOffset2 != null && unknownOffset2 > 0)
|
||||
{
|
||||
// Get the unknown value 2 offset
|
||||
long offset = unknownOffset2.Value + adjust;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the unknown value 2
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Set the unknown value 2
|
||||
audioFile.UnknownValue2 = data.ReadUInt32();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Block 3
|
||||
|
||||
// Get the V1 unknown offset 3
|
||||
uint? unknownOffset3 = (audioHeader as AudioHeaderV1)?.UnknownOffset3;
|
||||
|
||||
// If we have an unknown block 3 offset
|
||||
if (unknownOffset3 != null && unknownOffset3 > 0)
|
||||
{
|
||||
// Get the unknown block 3 offset
|
||||
long offset = unknownOffset3.Value + adjust;
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the unknown block 3
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Try to parse the unknown block 3
|
||||
var unknownBlock3 = ParseUnknownBlock3(data);
|
||||
if (unknownBlock3 == null)
|
||||
return null;
|
||||
|
||||
// Set the unknown block 3
|
||||
audioFile.UnknownBlock3 = unknownBlock3;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region V2 Only
|
||||
|
||||
// If we have a V2 file
|
||||
if (audioHeader.Version == 0x0000000A)
|
||||
{
|
||||
#region Data Files Count
|
||||
|
||||
// Set the data files count
|
||||
audioFile.DataFilesCount = data.ReadUInt32();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Files
|
||||
|
||||
// Create the data files array
|
||||
audioFile.DataFiles = new DataFile[audioFile.DataFilesCount];
|
||||
|
||||
// Try to parse the data files
|
||||
for (int i = 0; i < audioFile.DataFiles.Length; i++)
|
||||
{
|
||||
var dataFile = ParseDataFile(data);
|
||||
if (dataFile == null)
|
||||
return null;
|
||||
|
||||
audioFile.DataFiles[i] = dataFile;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return audioFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an audio header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled audio header on success, null on error</returns>
|
||||
private static AudioHeader? ParseAudioHeader(Stream data)
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// TODO: Use marshalling here instead of building
|
||||
AudioHeader audioHeader;
|
||||
|
||||
// Get the common header pieces
|
||||
uint signature = data.ReadUInt32();
|
||||
if (signature != SignatureUInt32)
|
||||
return null;
|
||||
|
||||
uint version = data.ReadUInt32();
|
||||
|
||||
// Build the header according to version
|
||||
uint unknownOffset1;
|
||||
switch (version)
|
||||
{
|
||||
// Version 1
|
||||
case 0x00000000:
|
||||
AudioHeaderV1 v1 = new AudioHeaderV1();
|
||||
|
||||
v1.Signature = signature;
|
||||
v1.Version = version;
|
||||
v1.TrackID = data.ReadUInt32();
|
||||
v1.UnknownOffset1 = data.ReadUInt32();
|
||||
v1.UnknownOffset2 = data.ReadUInt32();
|
||||
v1.UnknownOffset3 = data.ReadUInt32();
|
||||
v1.Unknown1 = data.ReadUInt32();
|
||||
v1.Unknown2 = data.ReadUInt32();
|
||||
v1.Year = data.ReadUInt32();
|
||||
v1.TrackNumber = data.ReadByteValue();
|
||||
v1.Subgenre = (Subgenre)data.ReadByteValue();
|
||||
v1.Duration = data.ReadUInt32();
|
||||
|
||||
audioHeader = v1;
|
||||
unknownOffset1 = v1.UnknownOffset1;
|
||||
break;
|
||||
|
||||
// Version 2
|
||||
case 0x0000000A:
|
||||
AudioHeaderV2 v2 = new AudioHeaderV2();
|
||||
|
||||
v2.Signature = signature;
|
||||
v2.Version = version;
|
||||
v2.Unknown1 = data.ReadUInt32();
|
||||
v2.Unknown2 = data.ReadUInt32();
|
||||
v2.Unknown3 = data.ReadUInt32();
|
||||
v2.Unknown4 = data.ReadUInt32();
|
||||
v2.Unknown5 = data.ReadUInt32();
|
||||
v2.Unknown6 = data.ReadUInt32();
|
||||
v2.UnknownOffset1 = data.ReadUInt32();
|
||||
v2.Unknown7 = data.ReadUInt32();
|
||||
v2.Unknown8 = data.ReadUInt32();
|
||||
v2.Unknown9 = data.ReadUInt32();
|
||||
v2.UnknownOffset2 = data.ReadUInt32();
|
||||
v2.Unknown10 = data.ReadUInt32();
|
||||
v2.Unknown11 = data.ReadUInt32();
|
||||
v2.Unknown12 = data.ReadUInt32();
|
||||
v2.Unknown13 = data.ReadUInt32();
|
||||
v2.Unknown14 = data.ReadUInt32();
|
||||
v2.Unknown15 = data.ReadUInt32();
|
||||
v2.Unknown16 = data.ReadUInt32();
|
||||
v2.Unknown17 = data.ReadUInt32();
|
||||
v2.TrackID = data.ReadUInt32();
|
||||
v2.Year = data.ReadUInt32();
|
||||
v2.TrackNumber = data.ReadUInt32();
|
||||
v2.Unknown18 = data.ReadUInt32();
|
||||
|
||||
audioHeader = v2;
|
||||
unknownOffset1 = v2.UnknownOffset1 + 0x54;
|
||||
break;
|
||||
|
||||
// No other version are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
audioHeader.TrackLength = data.ReadUInt16();
|
||||
byte[]? track = data.ReadBytes(audioHeader.TrackLength);
|
||||
if (track != null)
|
||||
audioHeader.Track = Encoding.ASCII.GetString(track);
|
||||
|
||||
audioHeader.ArtistLength = data.ReadUInt16();
|
||||
byte[]? artist = data.ReadBytes(audioHeader.ArtistLength);
|
||||
if (artist != null)
|
||||
audioHeader.Artist = Encoding.ASCII.GetString(artist);
|
||||
|
||||
audioHeader.AlbumLength = data.ReadUInt16();
|
||||
byte[]? album = data.ReadBytes(audioHeader.AlbumLength);
|
||||
if (album != null)
|
||||
audioHeader.Album = Encoding.ASCII.GetString(album);
|
||||
|
||||
audioHeader.WriterLength = data.ReadUInt16();
|
||||
byte[]? writer = data.ReadBytes(audioHeader.WriterLength);
|
||||
if (writer != null)
|
||||
audioHeader.Writer = Encoding.ASCII.GetString(writer);
|
||||
|
||||
audioHeader.PublisherLength = data.ReadUInt16();
|
||||
byte[]? publisher = data.ReadBytes(audioHeader.PublisherLength);
|
||||
if (publisher != null)
|
||||
audioHeader.Publisher = Encoding.ASCII.GetString(publisher);
|
||||
|
||||
audioHeader.LabelLength = data.ReadUInt16();
|
||||
byte[]? label = data.ReadBytes(audioHeader.LabelLength);
|
||||
if (label != null)
|
||||
audioHeader.Label = Encoding.ASCII.GetString(label);
|
||||
|
||||
if (data.Position - initialOffset < unknownOffset1)
|
||||
{
|
||||
audioHeader.CommentsLength = data.ReadUInt16();
|
||||
byte[]? comments = data.ReadBytes(audioHeader.CommentsLength);
|
||||
if (comments != null)
|
||||
audioHeader.Comments = Encoding.ASCII.GetString(comments);
|
||||
}
|
||||
|
||||
return audioHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an unknown block 1
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled unknown block 1 on success, null on error</returns>
|
||||
private static UnknownBlock1 ParseUnknownBlock1(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownBlock1 unknownBlock1 = new UnknownBlock1();
|
||||
|
||||
unknownBlock1.Length = data.ReadUInt32();
|
||||
unknownBlock1.Data = data.ReadBytes((int)unknownBlock1.Length);
|
||||
|
||||
return unknownBlock1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an unknown block 3
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled unknown block 3 on success, null on error</returns>
|
||||
private static UnknownBlock3 ParseUnknownBlock3(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
UnknownBlock3 unknownBlock3 = new UnknownBlock3();
|
||||
|
||||
// No-op because we don't even know the length
|
||||
|
||||
return unknownBlock3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a data file
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled data file on success, null on error</returns>
|
||||
private static DataFile ParseDataFile(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DataFile dataFile = new DataFile();
|
||||
|
||||
dataFile.FileNameLength = data.ReadUInt16();
|
||||
byte[]? fileName = data.ReadBytes(dataFile.FileNameLength);
|
||||
if (fileName != null)
|
||||
dataFile.FileName = Encoding.ASCII.GetString(fileName);
|
||||
|
||||
dataFile.DataLength = data.ReadUInt32();
|
||||
dataFile.Data = data.ReadBytes((int)dataFile.DataLength);
|
||||
|
||||
return dataFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
using System.IO;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.PlayJ;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class PlayJPlaylist : IStreamSerializer<Playlist>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Playlist? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new PlayJPlaylist();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Playlist? 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 playlist to fill
|
||||
var playlist = new Playlist();
|
||||
|
||||
#region Playlist Header
|
||||
|
||||
// Try to parse the playlist header
|
||||
var playlistHeader = ParsePlaylistHeader(data);
|
||||
if (playlistHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the playlist header
|
||||
playlist.Header = playlistHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Audio Files
|
||||
|
||||
// Create the audio files array
|
||||
playlist.AudioFiles = new AudioFile[playlistHeader.TrackCount];
|
||||
|
||||
// Try to parse the audio files
|
||||
for (int i = 0; i < playlist.AudioFiles.Length; i++)
|
||||
{
|
||||
long currentOffset = data.Position;
|
||||
var entryHeader = PlayJAudio.DeserializeStream(data, currentOffset);
|
||||
if (entryHeader == null)
|
||||
return null;
|
||||
|
||||
playlist.AudioFiles[i] = entryHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a playlist header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled playlist header on success, null on error</returns>
|
||||
private static PlaylistHeader ParsePlaylistHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
PlaylistHeader playlistHeader = new PlaylistHeader();
|
||||
|
||||
playlistHeader.TrackCount = data.ReadUInt32();
|
||||
playlistHeader.Data = data.ReadBytes(52);
|
||||
|
||||
return playlistHeader;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,164 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.Quantum;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.Quantum.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class Quantum : IStreamSerializer<Archive>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Archive? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new Quantum();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 File List
|
||||
|
||||
// If we have any files
|
||||
if (header.FileCount > 0)
|
||||
{
|
||||
var fileDescriptors = new FileDescriptor[header.FileCount];
|
||||
|
||||
// Read all entries in turn
|
||||
for (int i = 0; i < header.FileCount; i++)
|
||||
{
|
||||
var file = ParseFileDescriptor(data, header.MinorVersion);
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
fileDescriptors[i] = file;
|
||||
}
|
||||
|
||||
// Set the file list
|
||||
archive.FileList = fileDescriptors;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the compressed data offset
|
||||
archive.CompressedDataOffset = data.Position;
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(2);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.MajorVersion = data.ReadByteValue();
|
||||
header.MinorVersion = data.ReadByteValue();
|
||||
header.FileCount = data.ReadUInt16();
|
||||
header.TableSize = data.ReadByteValue();
|
||||
header.CompressionFlags = data.ReadByteValue();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a file descriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="minorVersion">Minor version of the archive</param>
|
||||
/// <returns>Filled file descriptor on success, null on error</returns>
|
||||
private static FileDescriptor ParseFileDescriptor(Stream data, byte minorVersion)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
FileDescriptor fileDescriptor = new FileDescriptor();
|
||||
|
||||
fileDescriptor.FileNameSize = ReadVariableLength(data);
|
||||
if (fileDescriptor.FileNameSize > 0)
|
||||
{
|
||||
byte[]? fileName = data.ReadBytes(fileDescriptor.FileNameSize);
|
||||
if (fileName != null)
|
||||
fileDescriptor.FileName = Encoding.ASCII.GetString(fileName);
|
||||
}
|
||||
|
||||
fileDescriptor.CommentFieldSize = ReadVariableLength(data);
|
||||
if (fileDescriptor.CommentFieldSize > 0)
|
||||
{
|
||||
byte[]? commentField = data.ReadBytes(fileDescriptor.CommentFieldSize);
|
||||
if (commentField != null)
|
||||
fileDescriptor.CommentField = Encoding.ASCII.GetString(commentField);
|
||||
}
|
||||
|
||||
fileDescriptor.ExpandedFileSize = data.ReadUInt32();
|
||||
fileDescriptor.FileTime = data.ReadUInt16();
|
||||
fileDescriptor.FileDate = data.ReadUInt16();
|
||||
|
||||
// Hack for unknown format data
|
||||
if (minorVersion == 22)
|
||||
fileDescriptor.Unknown = data.ReadUInt16();
|
||||
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a variable-length size prefix
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Variable-length size prefix</returns>
|
||||
/// <remarks>
|
||||
/// Strings are prefixed with their length. If the length is less than 128
|
||||
/// then it is stored directly in one byte. If it is greater than 127 then
|
||||
/// the high bit of the first byte is set to 1 and the remaining fifteen bits
|
||||
/// contain the actual length in big-endian format.
|
||||
/// </remarks>
|
||||
private static int ReadVariableLength(Stream data)
|
||||
{
|
||||
byte b0 = data.ReadByteValue();
|
||||
if (b0 < 0x7F)
|
||||
return b0;
|
||||
|
||||
b0 &= 0x7F;
|
||||
byte b1 = data.ReadByteValue();
|
||||
return (b0 << 8) | b1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.RomCenter;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class RomCenter : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new RomCenter();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new IniReader(data, Encoding.UTF8)
|
||||
{
|
||||
ValidateRows = false,
|
||||
};
|
||||
var dat = new MetadataFile();
|
||||
|
||||
// Loop through and parse out the values
|
||||
var roms = new List<Rom>();
|
||||
var additional = new List<string>();
|
||||
var creditsAdditional = new List<string>();
|
||||
var datAdditional = new List<string>();
|
||||
var emulatorAdditional = new List<string>();
|
||||
var gamesAdditional = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Ignore certain row types
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case IniRowType.None:
|
||||
case IniRowType.Comment:
|
||||
continue;
|
||||
case IniRowType.SectionHeader:
|
||||
switch (reader.Section?.ToLowerInvariant())
|
||||
{
|
||||
case "credits":
|
||||
dat.Credits ??= new Credits();
|
||||
break;
|
||||
case "dat":
|
||||
dat.Dat ??= new Dat();
|
||||
break;
|
||||
case "emulator":
|
||||
dat.Emulator ??= new Emulator();
|
||||
break;
|
||||
case "games":
|
||||
dat.Games ??= new Games();
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in credits
|
||||
if (reader.Section?.ToLowerInvariant() == "credits")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Credits ??= new Credits();
|
||||
|
||||
switch (reader.KeyValuePair?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "author":
|
||||
dat.Credits.Author = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "version":
|
||||
dat.Credits.Version = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "email":
|
||||
dat.Credits.Email = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "homepage":
|
||||
dat.Credits.Homepage = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "url":
|
||||
dat.Credits.Url = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "date":
|
||||
dat.Credits.Date = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "comment":
|
||||
dat.Credits.Comment = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
creditsAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in dat
|
||||
else if (reader.Section?.ToLowerInvariant() == "dat")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Dat ??= new Dat();
|
||||
|
||||
switch (reader.KeyValuePair?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "version":
|
||||
dat.Dat.Version = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "plugin":
|
||||
dat.Dat.Plugin = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "split":
|
||||
dat.Dat.Split = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "merge":
|
||||
dat.Dat.Merge = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
datAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in emulator
|
||||
else if (reader.Section?.ToLowerInvariant() == "emulator")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Emulator ??= new Emulator();
|
||||
|
||||
switch (reader.KeyValuePair?.Key?.ToLowerInvariant())
|
||||
{
|
||||
case "refname":
|
||||
dat.Emulator.RefName = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
case "version":
|
||||
dat.Emulator.Version = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
default:
|
||||
if (reader.CurrentLine != null)
|
||||
emulatorAdditional.Add(reader.CurrentLine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in games
|
||||
else if (reader.Section?.ToLowerInvariant() == "games")
|
||||
{
|
||||
// Create the section if we haven't already
|
||||
dat.Games ??= new Games();
|
||||
|
||||
// If the line doesn't contain the delimiter
|
||||
if (!(reader.CurrentLine?.Contains('¬') ?? false))
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
gamesAdditional.Add(reader.CurrentLine);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, separate out the line
|
||||
string[] splitLine = reader.CurrentLine.Split('¬');
|
||||
var rom = new Rom
|
||||
{
|
||||
// EMPTY = splitLine[0]
|
||||
ParentName = splitLine[1],
|
||||
ParentDescription = splitLine[2],
|
||||
GameName = splitLine[3],
|
||||
GameDescription = splitLine[4],
|
||||
RomName = splitLine[5],
|
||||
RomCRC = splitLine[6],
|
||||
RomSize = splitLine[7],
|
||||
RomOf = splitLine[8],
|
||||
MergeName = splitLine[9],
|
||||
// EMPTY = splitLine[10]
|
||||
};
|
||||
|
||||
if (splitLine.Length > 11)
|
||||
rom.ADDITIONAL_ELEMENTS = splitLine.Skip(11).ToArray();
|
||||
|
||||
roms.Add(rom);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (reader.CurrentLine != null)
|
||||
additional.Add(reader.CurrentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra pieces and return
|
||||
dat.ADDITIONAL_ELEMENTS = additional.Where(s => s != null).ToArray();
|
||||
if (dat.Credits != null)
|
||||
dat.Credits.ADDITIONAL_ELEMENTS = creditsAdditional.Where(s => s != null).ToArray();
|
||||
if (dat.Dat != null)
|
||||
dat.Dat.ADDITIONAL_ELEMENTS = datAdditional.Where(s => s != null).ToArray();
|
||||
if (dat.Emulator != null)
|
||||
dat.Emulator.ADDITIONAL_ELEMENTS = emulatorAdditional.Where(s => s != null).ToArray();
|
||||
if (dat.Games != null)
|
||||
{
|
||||
dat.Games.Rom = roms.ToArray();
|
||||
dat.Games.ADDITIONAL_ELEMENTS = gamesAdditional.Where(s => s != null).Select(s => s).ToArray();
|
||||
}
|
||||
return dat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,736 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.SGA;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.SGA.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class SGA : IStreamSerializer<Models.SGA.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.SGA.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new SGA();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.SGA.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new SGA to fill
|
||||
var file = new Models.SGA.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the SGA header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory
|
||||
|
||||
// Try to parse the directory
|
||||
var directory = ParseDirectory(data, header.MajorVersion);
|
||||
if (directory == null)
|
||||
return null;
|
||||
|
||||
// Set the SGA directory
|
||||
file.Directory = directory;
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
byte[]? signatureBytes = data.ReadBytes(8);
|
||||
if (signatureBytes == null)
|
||||
return null;
|
||||
|
||||
string signature = Encoding.ASCII.GetString(signatureBytes);
|
||||
if (signature != SignatureString)
|
||||
return null;
|
||||
|
||||
ushort majorVersion = data.ReadUInt16();
|
||||
ushort minorVersion = data.ReadUInt16();
|
||||
if (minorVersion != 0)
|
||||
return null;
|
||||
|
||||
switch (majorVersion)
|
||||
{
|
||||
// Versions 4 and 5 share the same header
|
||||
case 4:
|
||||
case 5:
|
||||
Header4 header4 = new Header4();
|
||||
|
||||
header4.Signature = signature;
|
||||
header4.MajorVersion = majorVersion;
|
||||
header4.MinorVersion = minorVersion;
|
||||
header4.FileMD5 = data.ReadBytes(0x10);
|
||||
byte[]? header4Name = data.ReadBytes(count: 128);
|
||||
if (header4Name != null)
|
||||
header4.Name = Encoding.Unicode.GetString(header4Name).TrimEnd('\0');
|
||||
header4.HeaderMD5 = data.ReadBytes(0x10);
|
||||
header4.HeaderLength = data.ReadUInt32();
|
||||
header4.FileDataOffset = data.ReadUInt32();
|
||||
header4.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return header4;
|
||||
|
||||
// Versions 6 and 7 share the same header
|
||||
case 6:
|
||||
case 7:
|
||||
Header6 header6 = new Header6();
|
||||
|
||||
header6.Signature = signature;
|
||||
header6.MajorVersion = majorVersion;
|
||||
header6.MinorVersion = minorVersion;
|
||||
byte[]? header6Name = data.ReadBytes(count: 128);
|
||||
if (header6Name != null)
|
||||
header6.Name = Encoding.Unicode.GetString(header6Name).TrimEnd('\0');
|
||||
header6.HeaderLength = data.ReadUInt32();
|
||||
header6.FileDataOffset = data.ReadUInt32();
|
||||
header6.Dummy0 = data.ReadUInt32();
|
||||
|
||||
return header6;
|
||||
|
||||
// No other major versions are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA directory on success, null on error</returns>
|
||||
private static Models.SGA.Directory? ParseDirectory(Stream data, ushort majorVersion)
|
||||
{
|
||||
#region Directory
|
||||
|
||||
// Create the appropriate type of directory
|
||||
Models.SGA.Directory directory;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: directory = new Directory4(); break;
|
||||
case 5: directory = new Directory5(); break;
|
||||
case 6: directory = new Directory6(); break;
|
||||
case 7: directory = new Directory7(); break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
#region Directory Header
|
||||
|
||||
// Try to parse the directory header
|
||||
var directoryHeader = ParseDirectoryHeader(data, majorVersion);
|
||||
if (directoryHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the directory header
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.DirectoryHeader = directoryHeader as DirectoryHeader4; break;
|
||||
case 5: (directory as Directory5)!.DirectoryHeader = directoryHeader as DirectoryHeader5; break;
|
||||
case 6: (directory as Directory6)!.DirectoryHeader = directoryHeader as DirectoryHeader5; break;
|
||||
case 7: (directory as Directory7)!.DirectoryHeader = directoryHeader as DirectoryHeader7; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sections
|
||||
|
||||
// Get the sections offset
|
||||
long sectionOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sectionOffset = (directoryHeader as DirectoryHeader4)!.SectionOffset; break;
|
||||
case 5:
|
||||
case 6: sectionOffset = (directoryHeader as DirectoryHeader5)!.SectionOffset; break;
|
||||
case 7: sectionOffset = (directoryHeader as DirectoryHeader7)!.SectionOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the sections offset based on the directory
|
||||
sectionOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (sectionOffset < 0 || sectionOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the sections
|
||||
data.Seek(sectionOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the section count
|
||||
uint sectionCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sectionCount = (directoryHeader as DirectoryHeader4)!.SectionCount; break;
|
||||
case 5:
|
||||
case 6: sectionCount = (directoryHeader as DirectoryHeader5)!.SectionCount; break;
|
||||
case 7: sectionCount = (directoryHeader as DirectoryHeader7)!.SectionCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Create the sections array
|
||||
object[] sections;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sections = new Section4[sectionCount]; break;
|
||||
case 5:
|
||||
case 6:
|
||||
case 7: sections = new Section5[sectionCount]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Try to parse the sections
|
||||
for (int i = 0; i < sections.Length; i++)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: sections[i] = ParseSection4(data); break;
|
||||
case 5:
|
||||
case 6:
|
||||
case 7: sections[i] = ParseSection5(data); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the sections
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Sections = sections as Section4[]; break;
|
||||
case 5: (directory as Directory5)!.Sections = sections as Section5[]; break;
|
||||
case 6: (directory as Directory6)!.Sections = sections as Section5[]; break;
|
||||
case 7: (directory as Directory7)!.Sections = sections as Section5[]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Folders
|
||||
|
||||
// Get the folders offset
|
||||
long folderOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folderOffset = (directoryHeader as DirectoryHeader4)!.FolderOffset; break;
|
||||
case 5: folderOffset = (directoryHeader as DirectoryHeader5)!.FolderOffset; break;
|
||||
case 6: folderOffset = (directoryHeader as DirectoryHeader5)!.FolderOffset; break;
|
||||
case 7: folderOffset = (directoryHeader as DirectoryHeader7)!.FolderOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the folders offset based on the directory
|
||||
folderOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (folderOffset < 0 || folderOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the folders
|
||||
data.Seek(folderOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the folder count
|
||||
uint folderCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folderCount = (directoryHeader as DirectoryHeader4)!.FolderCount; break;
|
||||
case 5: folderCount = (directoryHeader as DirectoryHeader5)!.FolderCount; break;
|
||||
case 6: folderCount = (directoryHeader as DirectoryHeader5)!.FolderCount; break;
|
||||
case 7: folderCount = (directoryHeader as DirectoryHeader7)!.FolderCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Create the folders array
|
||||
object[] folders;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folders = new Folder4[folderCount]; break;
|
||||
case 5: folders = new Folder5[folderCount]; break;
|
||||
case 6: folders = new Folder5[folderCount]; break;
|
||||
case 7: folders = new Folder5[folderCount]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Try to parse the folders
|
||||
for (int i = 0; i < folders.Length; i++)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: folders[i] = ParseFolder4(data); break;
|
||||
case 5: folders[i] = ParseFolder5(data); break;
|
||||
case 6: folders[i] = ParseFolder5(data); break;
|
||||
case 7: folders[i] = ParseFolder5(data); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the folders
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Folders = folders as Folder4[]; break;
|
||||
case 5: (directory as Directory5)!.Folders = folders as Folder5[]; break;
|
||||
case 6: (directory as Directory6)!.Folders = folders as Folder5[]; break;
|
||||
case 7: (directory as Directory7)!.Folders = folders as Folder5[]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Files
|
||||
|
||||
// Get the files offset
|
||||
long fileOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: fileOffset = (directoryHeader as DirectoryHeader4)!.FileOffset; break;
|
||||
case 5: fileOffset = (directoryHeader as DirectoryHeader5)!.FileOffset; break;
|
||||
case 6: fileOffset = (directoryHeader as DirectoryHeader5)!.FileOffset; break;
|
||||
case 7: fileOffset = (directoryHeader as DirectoryHeader7)!.FileOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the files offset based on the directory
|
||||
fileOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (fileOffset < 0 || fileOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the files
|
||||
data.Seek(fileOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the file count
|
||||
uint fileCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: fileCount = (directoryHeader as DirectoryHeader4)!.FileCount; break;
|
||||
case 5: fileCount = (directoryHeader as DirectoryHeader5)!.FileCount; break;
|
||||
case 6: fileCount = (directoryHeader as DirectoryHeader5)!.FileCount; break;
|
||||
case 7: fileCount = (directoryHeader as DirectoryHeader7)!.FileCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Create the files array
|
||||
object[] files;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: files = new File4[fileCount]; break;
|
||||
case 5: files = new File4[fileCount]; break;
|
||||
case 6: files = new File6[fileCount]; break;
|
||||
case 7: files = new File7[fileCount]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Try to parse the files
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: files[i] = ParseFile4(data); break;
|
||||
case 5: files[i] = ParseFile4(data); break;
|
||||
case 6: files[i] = ParseFile6(data); break;
|
||||
case 7: files[i] = ParseFile7(data); break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the files
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Files = files as File4[]; break;
|
||||
case 5: (directory as Directory5)!.Files = files as File4[]; break;
|
||||
case 6: (directory as Directory6)!.Files = files as File6[]; break;
|
||||
case 7: (directory as Directory7)!.Files = files as File7[]; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Table
|
||||
|
||||
// Get the string table offset
|
||||
long stringTableOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: stringTableOffset = (directoryHeader as DirectoryHeader4)!.StringTableOffset; break;
|
||||
case 5: stringTableOffset = (directoryHeader as DirectoryHeader5)!.StringTableOffset; break;
|
||||
case 6: stringTableOffset = (directoryHeader as DirectoryHeader5)!.StringTableOffset; break;
|
||||
case 7: stringTableOffset = (directoryHeader as DirectoryHeader7)!.StringTableOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Adjust the string table offset based on the directory
|
||||
stringTableOffset += currentOffset;
|
||||
|
||||
// Validate the offset
|
||||
if (stringTableOffset < 0 || stringTableOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the string table
|
||||
data.Seek(stringTableOffset, SeekOrigin.Begin);
|
||||
|
||||
// Get the string table count
|
||||
uint stringCount;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: stringCount = (directoryHeader as DirectoryHeader4)!.StringTableCount; break;
|
||||
case 5: stringCount = (directoryHeader as DirectoryHeader5)!.StringTableCount; break;
|
||||
case 6: stringCount = (directoryHeader as DirectoryHeader5)!.StringTableCount; break;
|
||||
case 7: stringCount = (directoryHeader as DirectoryHeader7)!.StringTableCount; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// TODO: Are these strings actually indexed by number and not position?
|
||||
// TODO: If indexed by position, I think it needs to be adjusted by start of table
|
||||
|
||||
// Create the strings dictionary
|
||||
Dictionary<long, string?> strings = new Dictionary<long, string?>((int)stringCount);
|
||||
|
||||
// Get the current position to adjust the offsets
|
||||
long stringTableStart = data.Position;
|
||||
|
||||
// Try to parse the strings
|
||||
for (int i = 0; i < stringCount; i++)
|
||||
{
|
||||
long currentPosition = data.Position - stringTableStart;
|
||||
strings[currentPosition] = data.ReadString(Encoding.ASCII);
|
||||
}
|
||||
|
||||
// Assign the files
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.StringTable = strings; break;
|
||||
case 5: (directory as Directory5)!.StringTable = strings; break;
|
||||
case 6: (directory as Directory6)!.StringTable = strings; break;
|
||||
case 7: (directory as Directory7)!.StringTable = strings; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
// Loop through all folders to assign names
|
||||
for (int i = 0; i < folderCount; i++)
|
||||
{
|
||||
uint nameOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: nameOffset = (directory as Directory4)!.Folders![i]!.NameOffset; break;
|
||||
case 5: nameOffset = (directory as Directory5)!.Folders![i]!.NameOffset; break;
|
||||
case 6: nameOffset = (directory as Directory6)!.Folders![i]!.NameOffset; break;
|
||||
case 7: nameOffset = (directory as Directory7)!.Folders![i]!.NameOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
case 5: (directory as Directory5)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
case 6: (directory as Directory6)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
case 7: (directory as Directory7)!.Folders![i]!.Name = strings[nameOffset]; break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through all files to assign names
|
||||
for (int i = 0; i < fileCount; i++)
|
||||
{
|
||||
uint nameOffset;
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: nameOffset = (directory as Directory4)!.Files![i]!.NameOffset; break;
|
||||
case 5: nameOffset = (directory as Directory5)!.Files![i]!.NameOffset; break;
|
||||
case 6: nameOffset = (directory as Directory6)!.Files![i]!.NameOffset; break;
|
||||
case 7: nameOffset = (directory as Directory7)!.Files![i]!.NameOffset; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: (directory as Directory4)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
case 5: (directory as Directory5)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
case 6: (directory as Directory6)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
case 7: (directory as Directory7)!.Files![i]!.Name = strings[nameOffset]; break;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA directory header on success, null on error</returns>
|
||||
private static object? ParseDirectoryHeader(Stream data, ushort majorVersion)
|
||||
{
|
||||
switch (majorVersion)
|
||||
{
|
||||
case 4: return ParseDirectory4Header(data);
|
||||
case 5: return ParseDirectory5Header(data);
|
||||
case 6: return ParseDirectory5Header(data);
|
||||
case 7: return ParseDirectory7Header(data);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA directory header version 4 on success, null on error</returns>
|
||||
private static DirectoryHeader4 ParseDirectory4Header(Stream data)
|
||||
{
|
||||
DirectoryHeader4 directoryHeader4 = new DirectoryHeader4();
|
||||
|
||||
directoryHeader4.SectionOffset = data.ReadUInt32();
|
||||
directoryHeader4.SectionCount = data.ReadUInt16();
|
||||
directoryHeader4.FolderOffset = data.ReadUInt32();
|
||||
directoryHeader4.FolderCount = data.ReadUInt16();
|
||||
directoryHeader4.FileOffset = data.ReadUInt32();
|
||||
directoryHeader4.FileCount = data.ReadUInt16();
|
||||
directoryHeader4.StringTableOffset = data.ReadUInt32();
|
||||
directoryHeader4.StringTableCount = data.ReadUInt16();
|
||||
|
||||
return directoryHeader4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header version 5
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA directory header version 5 on success, null on error</returns>
|
||||
private static DirectoryHeader5 ParseDirectory5Header(Stream data)
|
||||
{
|
||||
DirectoryHeader5 directoryHeader5 = new DirectoryHeader5();
|
||||
|
||||
directoryHeader5.SectionOffset = data.ReadUInt32();
|
||||
directoryHeader5.SectionCount = data.ReadUInt32();
|
||||
directoryHeader5.FolderOffset = data.ReadUInt32();
|
||||
directoryHeader5.FolderCount = data.ReadUInt32();
|
||||
directoryHeader5.FileOffset = data.ReadUInt32();
|
||||
directoryHeader5.FileCount = data.ReadUInt32();
|
||||
directoryHeader5.StringTableOffset = data.ReadUInt32();
|
||||
directoryHeader5.StringTableCount = data.ReadUInt32();
|
||||
|
||||
return directoryHeader5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA directory header version 7
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled SGA directory header version 7 on success, null on error</returns>
|
||||
private static DirectoryHeader7 ParseDirectory7Header(Stream data)
|
||||
{
|
||||
DirectoryHeader7 directoryHeader7 = new DirectoryHeader7();
|
||||
|
||||
directoryHeader7.SectionOffset = data.ReadUInt32();
|
||||
directoryHeader7.SectionCount = data.ReadUInt32();
|
||||
directoryHeader7.FolderOffset = data.ReadUInt32();
|
||||
directoryHeader7.FolderCount = data.ReadUInt32();
|
||||
directoryHeader7.FileOffset = data.ReadUInt32();
|
||||
directoryHeader7.FileCount = data.ReadUInt32();
|
||||
directoryHeader7.StringTableOffset = data.ReadUInt32();
|
||||
directoryHeader7.StringTableCount = data.ReadUInt32();
|
||||
directoryHeader7.HashTableOffset = data.ReadUInt32();
|
||||
directoryHeader7.BlockSize = data.ReadUInt32();
|
||||
|
||||
return directoryHeader7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA section version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA section version 4 on success, null on error</returns>
|
||||
private static Section4 ParseSection4(Stream data)
|
||||
{
|
||||
Section4 section4 = new Section4();
|
||||
|
||||
byte[]? section4Alias = data.ReadBytes(64);
|
||||
if (section4Alias != null)
|
||||
section4.Alias = Encoding.ASCII.GetString(section4Alias).TrimEnd('\0');
|
||||
byte[]? section4Name = data.ReadBytes(64);
|
||||
if (section4Name != null)
|
||||
section4.Name = Encoding.ASCII.GetString(section4Name).TrimEnd('\0');
|
||||
section4.FolderStartIndex = data.ReadUInt16();
|
||||
section4.FolderEndIndex = data.ReadUInt16();
|
||||
section4.FileStartIndex = data.ReadUInt16();
|
||||
section4.FileEndIndex = data.ReadUInt16();
|
||||
section4.FolderRootIndex = data.ReadUInt16();
|
||||
|
||||
return section4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA section version 5
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA section version 5 on success, null on error</returns>
|
||||
private static Section5 ParseSection5(Stream data)
|
||||
{
|
||||
Section5 section5 = new Section5();
|
||||
|
||||
byte[]? section5Alias = data.ReadBytes(64);
|
||||
if (section5Alias != null)
|
||||
section5.Alias = Encoding.ASCII.GetString(section5Alias).TrimEnd('\0');
|
||||
byte[]? section5Name = data.ReadBytes(64);
|
||||
if (section5Name != null)
|
||||
section5.Name = Encoding.ASCII.GetString(section5Name).TrimEnd('\0');
|
||||
section5.FolderStartIndex = data.ReadUInt32();
|
||||
section5.FolderEndIndex = data.ReadUInt32();
|
||||
section5.FileStartIndex = data.ReadUInt32();
|
||||
section5.FileEndIndex = data.ReadUInt32();
|
||||
section5.FolderRootIndex = data.ReadUInt32();
|
||||
|
||||
return section5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA folder version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA folder version 4 on success, null on error</returns>
|
||||
private static Folder4 ParseFolder4(Stream data)
|
||||
{
|
||||
Folder4 folder4 = new Folder4();
|
||||
|
||||
folder4.NameOffset = data.ReadUInt32();
|
||||
folder4.Name = null; // Read from string table
|
||||
folder4.FolderStartIndex = data.ReadUInt16();
|
||||
folder4.FolderEndIndex = data.ReadUInt16();
|
||||
folder4.FileStartIndex = data.ReadUInt16();
|
||||
folder4.FileEndIndex = data.ReadUInt16();
|
||||
|
||||
return folder4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA folder version 5
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA folder version 5 on success, null on error</returns>
|
||||
private static Folder5 ParseFolder5(Stream data)
|
||||
{
|
||||
Folder5 folder5 = new Folder5();
|
||||
|
||||
folder5.NameOffset = data.ReadUInt32();
|
||||
folder5.Name = null; // Read from string table
|
||||
folder5.FolderStartIndex = data.ReadUInt32();
|
||||
folder5.FolderEndIndex = data.ReadUInt32();
|
||||
folder5.FileStartIndex = data.ReadUInt32();
|
||||
folder5.FileEndIndex = data.ReadUInt32();
|
||||
|
||||
return folder5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA file version 4
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA file version 4 on success, null on error</returns>
|
||||
private static File4 ParseFile4(Stream data)
|
||||
{
|
||||
File4 file4 = new File4();
|
||||
|
||||
file4.NameOffset = data.ReadUInt32();
|
||||
file4.Name = null; // Read from string table
|
||||
file4.Offset = data.ReadUInt32();
|
||||
file4.SizeOnDisk = data.ReadUInt32();
|
||||
file4.Size = data.ReadUInt32();
|
||||
file4.TimeModified = data.ReadUInt32();
|
||||
file4.Dummy0 = data.ReadByteValue();
|
||||
file4.Type = data.ReadByteValue();
|
||||
|
||||
return file4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA file version 6
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA file version 6 on success, null on error</returns>
|
||||
private static File6 ParseFile6(Stream data)
|
||||
{
|
||||
File6 file6 = new File6();
|
||||
|
||||
file6.NameOffset = data.ReadUInt32();
|
||||
file6.Name = null; // Read from string table
|
||||
file6.Offset = data.ReadUInt32();
|
||||
file6.SizeOnDisk = data.ReadUInt32();
|
||||
file6.Size = data.ReadUInt32();
|
||||
file6.TimeModified = data.ReadUInt32();
|
||||
file6.Dummy0 = data.ReadByteValue();
|
||||
file6.Type = data.ReadByteValue();
|
||||
file6.CRC32 = data.ReadUInt32();
|
||||
|
||||
return file6;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an SGA file version 7
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="majorVersion">SGA major version</param>
|
||||
/// <returns>Filled SGA file version 7 on success, null on error</returns>
|
||||
private static File7 ParseFile7(Stream data)
|
||||
{
|
||||
File7 file7 = new File7();
|
||||
|
||||
file7.NameOffset = data.ReadUInt32();
|
||||
file7.Name = null; // Read from string table
|
||||
file7.Offset = data.ReadUInt32();
|
||||
file7.SizeOnDisk = data.ReadUInt32();
|
||||
file7.Size = data.ReadUInt32();
|
||||
file7.TimeModified = data.ReadUInt32();
|
||||
file7.Dummy0 = data.ReadByteValue();
|
||||
file7.Type = data.ReadByteValue();
|
||||
file7.CRC32 = data.ReadUInt32();
|
||||
file7.HashOffset = data.ReadUInt32();
|
||||
|
||||
return file7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.Models.SeparatedValue;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class SeparatedValue : IStreamSerializer<MetadataFile>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static MetadataFile? DeserializeStream(Stream? data, char delim = ',')
|
||||
{
|
||||
var deserializer = new SeparatedValue();
|
||||
return deserializer.Deserialize(data, delim);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MetadataFile? Deserialize(Stream? data)
|
||||
=> Deserialize(data, ',');
|
||||
|
||||
/// <inheritdoc cref="Deserialize(Stream)"/>
|
||||
public MetadataFile? Deserialize(Stream? data, char delim)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the reader and output
|
||||
var reader = new SeparatedValueReader(data, Encoding.UTF8)
|
||||
{
|
||||
Header = true,
|
||||
Separator = delim,
|
||||
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<Row>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we have no next line
|
||||
if (!reader.ReadNextLine() || reader.Line == null)
|
||||
break;
|
||||
|
||||
// Parse the line into a row
|
||||
Row? row = null;
|
||||
if (reader.Line.Count < Serialization.SeparatedValue.HeaderWithExtendedHashesCount)
|
||||
{
|
||||
row = new Row
|
||||
{
|
||||
FileName = reader.Line[0],
|
||||
InternalName = reader.Line[1],
|
||||
Description = reader.Line[2],
|
||||
GameName = reader.Line[3],
|
||||
GameDescription = reader.Line[4],
|
||||
Type = reader.Line[5],
|
||||
RomName = reader.Line[6],
|
||||
DiskName = reader.Line[7],
|
||||
Size = reader.Line[8],
|
||||
CRC = reader.Line[9],
|
||||
MD5 = reader.Line[10],
|
||||
SHA1 = reader.Line[11],
|
||||
SHA256 = reader.Line[12],
|
||||
Status = reader.Line[13],
|
||||
};
|
||||
|
||||
// If we have additional fields
|
||||
if (reader.Line.Count > Serialization.SeparatedValue.HeaderWithoutExtendedHashesCount)
|
||||
row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.SeparatedValue.HeaderWithoutExtendedHashesCount).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
row = new Row
|
||||
{
|
||||
FileName = reader.Line[0],
|
||||
InternalName = reader.Line[1],
|
||||
Description = reader.Line[2],
|
||||
GameName = reader.Line[3],
|
||||
GameDescription = reader.Line[4],
|
||||
Type = reader.Line[5],
|
||||
RomName = reader.Line[6],
|
||||
DiskName = reader.Line[7],
|
||||
Size = reader.Line[8],
|
||||
CRC = reader.Line[9],
|
||||
MD5 = reader.Line[10],
|
||||
SHA1 = reader.Line[11],
|
||||
SHA256 = reader.Line[12],
|
||||
SHA384 = reader.Line[13],
|
||||
SHA512 = reader.Line[14],
|
||||
SpamSum = reader.Line[15],
|
||||
Status = reader.Line[16],
|
||||
};
|
||||
|
||||
// If we have additional fields
|
||||
if (reader.Line.Count > Serialization.SeparatedValue.HeaderWithExtendedHashesCount)
|
||||
row.ADDITIONAL_ELEMENTS = reader.Line.Skip(Serialization.SeparatedValue.HeaderWithExtendedHashesCount).ToArray();
|
||||
}
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
// Assign the rows to the Dat and return
|
||||
dat.Row = rows.ToArray();
|
||||
return dat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class SoftwareList : XmlFile<Models.SoftwareList.SoftwareList>
|
||||
{
|
||||
/// <inheritdoc cref="Interfaces.IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.SoftwareList.SoftwareList? DeserializeStream(System.IO.Stream? data)
|
||||
{
|
||||
var deserializer = new SoftwareList();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.VBSP;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.VBSP.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class VBSP : IStreamSerializer<Models.VBSP.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.VBSP.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new VBSP();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.VBSP.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life 2 Level to fill
|
||||
var file = new Models.VBSP.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life 2 Level header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life 2 Level header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.Version = data.ReadInt32();
|
||||
if ((header.Version < 19 || header.Version > 22) && header.Version != 0x00040014)
|
||||
return null;
|
||||
|
||||
header.Lumps = new Lump[HL_VBSP_LUMP_COUNT];
|
||||
for (int i = 0; i < HL_VBSP_LUMP_COUNT; i++)
|
||||
{
|
||||
header.Lumps[i] = ParseLump(data, header.Version);
|
||||
}
|
||||
|
||||
header.MapRevision = data.ReadInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life 2 Level lump
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="version">VBSP version</param>
|
||||
/// <returns>Filled Half-Life 2 Level lump on success, null on error</returns>
|
||||
private static Lump ParseLump(Stream data, int version)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Lump lump = new Lump();
|
||||
|
||||
lump.Offset = data.ReadUInt32();
|
||||
lump.Length = data.ReadUInt32();
|
||||
lump.Version = data.ReadUInt32();
|
||||
lump.FourCC = new char[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
lump.FourCC[i] = (char)data.ReadByte();
|
||||
}
|
||||
|
||||
// This block was commented out because test VBSPs with header
|
||||
// version 21 had the values in the "right" order already and
|
||||
// were causing decompression issues
|
||||
|
||||
//if (version >= 21 && version != 0x00040014)
|
||||
//{
|
||||
// uint temp = lump.Version;
|
||||
// lump.Version = lump.Offset;
|
||||
// lump.Offset = lump.Length;
|
||||
// lump.Length = temp;
|
||||
//}
|
||||
|
||||
return lump;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.VPK;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.VPK.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class VPK : IStreamSerializer<Models.VPK.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.VPK.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new VPK();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.VPK.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Valve Package to fill
|
||||
var file = new Models.VPK.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
// The original version had no signature.
|
||||
var header = ParseHeader(data);
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended Header
|
||||
|
||||
if (header?.Version == 2)
|
||||
{
|
||||
// Try to parse the extended header
|
||||
var extendedHeader = ParseExtendedHeader(data);
|
||||
if (extendedHeader == null)
|
||||
return null;
|
||||
|
||||
// Set the package extended header
|
||||
file.ExtendedHeader = extendedHeader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Items
|
||||
|
||||
// Create the directory items tree
|
||||
var directoryItems = ParseDirectoryItemTree(data);
|
||||
|
||||
// Set the directory items
|
||||
file.DirectoryItems = directoryItems;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Archive Hashes
|
||||
|
||||
if (header?.Version == 2 && file.ExtendedHeader != null && file.ExtendedHeader.ArchiveHashLength > 0)
|
||||
{
|
||||
// Create the archive hashes list
|
||||
var archiveHashes = new List<ArchiveHash>();
|
||||
|
||||
// Cache the current offset
|
||||
initialOffset = data.Position;
|
||||
|
||||
// Try to parse the directory items
|
||||
while (data.Position < initialOffset + file.ExtendedHeader.ArchiveHashLength)
|
||||
{
|
||||
var archiveHash = ParseArchiveHash(data);
|
||||
archiveHashes.Add(archiveHash);
|
||||
}
|
||||
|
||||
file.ArchiveHashes = archiveHashes.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
header.Signature = data.ReadUInt32();
|
||||
if (header.Signature != SignatureUInt32)
|
||||
return null;
|
||||
|
||||
header.Version = data.ReadUInt32();
|
||||
if (header.Version > 2)
|
||||
return null;
|
||||
|
||||
header.DirectoryLength = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package extended header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package extended header on success, null on error</returns>
|
||||
private static ExtendedHeader ParseExtendedHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ExtendedHeader extendedHeader = new ExtendedHeader();
|
||||
|
||||
extendedHeader.Dummy0 = data.ReadUInt32();
|
||||
extendedHeader.ArchiveHashLength = data.ReadUInt32();
|
||||
extendedHeader.ExtraLength = data.ReadUInt32();
|
||||
extendedHeader.Dummy1 = data.ReadUInt32();
|
||||
|
||||
return extendedHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package archive hash
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package archive hash on success, null on error</returns>
|
||||
private static ArchiveHash ParseArchiveHash(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
ArchiveHash archiveHash = new ArchiveHash();
|
||||
|
||||
archiveHash.ArchiveIndex = data.ReadUInt32();
|
||||
archiveHash.ArchiveOffset = data.ReadUInt32();
|
||||
archiveHash.Length = data.ReadUInt32();
|
||||
archiveHash.Hash = data.ReadBytes(0x10);
|
||||
|
||||
return archiveHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package directory item tree
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package directory item tree on success, null on error</returns>
|
||||
private static DirectoryItem[] ParseDirectoryItemTree(Stream data)
|
||||
{
|
||||
// Create the directory items list
|
||||
var directoryItems = new List<DirectoryItem>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the extension
|
||||
string? extensionString = data.ReadString(Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(extensionString))
|
||||
break;
|
||||
|
||||
// Sanitize the extension
|
||||
for (int i = 0; i < 0x20; i++)
|
||||
{
|
||||
extensionString = extensionString!.Replace($"{(char)i}", string.Empty);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the path
|
||||
string? pathString = data.ReadString(Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(pathString))
|
||||
break;
|
||||
|
||||
// Sanitize the path
|
||||
for (int i = 0; i < 0x20; i++)
|
||||
{
|
||||
pathString = pathString!.Replace($"{(char)i}", string.Empty);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the name
|
||||
string? nameString = data.ReadString(Encoding.ASCII);
|
||||
if (string.IsNullOrEmpty(nameString))
|
||||
break;
|
||||
|
||||
// Sanitize the name
|
||||
for (int i = 0; i < 0x20; i++)
|
||||
{
|
||||
nameString = nameString!.Replace($"{(char)i}", string.Empty);
|
||||
}
|
||||
|
||||
// Get the directory item
|
||||
var directoryItem = ParseDirectoryItem(data, extensionString!, pathString!, nameString!);
|
||||
|
||||
// Add the directory item
|
||||
directoryItems.Add(directoryItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directoryItems.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package directory item
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package directory item on success, null on error</returns>
|
||||
private static DirectoryItem ParseDirectoryItem(Stream data, string extension, string path, string name)
|
||||
{
|
||||
DirectoryItem directoryItem = new DirectoryItem();
|
||||
|
||||
directoryItem.Extension = extension;
|
||||
directoryItem.Path = path;
|
||||
directoryItem.Name = name;
|
||||
|
||||
// Get the directory entry
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
|
||||
// Set the directory entry
|
||||
directoryItem.DirectoryEntry = directoryEntry;
|
||||
|
||||
// Get the preload data pointer
|
||||
long preloadDataPointer = -1; int preloadDataLength = -1;
|
||||
if (directoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE && directoryEntry.EntryLength > 0)
|
||||
{
|
||||
preloadDataPointer = directoryEntry.EntryOffset;
|
||||
preloadDataLength = (int)directoryEntry.EntryLength;
|
||||
}
|
||||
else if (directoryEntry.PreloadBytes > 0)
|
||||
{
|
||||
preloadDataPointer = data.Position;
|
||||
preloadDataLength = directoryEntry.PreloadBytes;
|
||||
}
|
||||
|
||||
// If we had a valid preload data pointer
|
||||
byte[]? preloadData = null;
|
||||
if (preloadDataPointer >= 0 && preloadDataLength > 0)
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Seek to the preload data offset
|
||||
data.Seek(preloadDataPointer, SeekOrigin.Begin);
|
||||
|
||||
// Read the preload data
|
||||
preloadData = data.ReadBytes(preloadDataLength);
|
||||
|
||||
// Seek back to the original offset
|
||||
data.Seek(initialOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Set the preload data
|
||||
directoryItem.PreloadData = preloadData;
|
||||
|
||||
return directoryItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Valve Package directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Valve Package directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.CRC = data.ReadUInt32();
|
||||
directoryEntry.PreloadBytes = data.ReadUInt16();
|
||||
directoryEntry.ArchiveIndex = data.ReadUInt16();
|
||||
directoryEntry.EntryOffset = data.ReadUInt32();
|
||||
directoryEntry.EntryLength = data.ReadUInt32();
|
||||
directoryEntry.Dummy0 = data.ReadUInt16();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.WAD;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.WAD.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class WAD : IStreamSerializer<Models.WAD.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.WAD.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new WAD();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.WAD.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Half-Life Texture Package to fill
|
||||
var file = new Models.WAD.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lumps
|
||||
|
||||
// Get the lump offset
|
||||
uint lumpOffset = header.LumpOffset;
|
||||
if (lumpOffset < 0 || lumpOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the lump offset
|
||||
data.Seek(lumpOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the lump array
|
||||
file.Lumps = new Lump[header.LumpCount];
|
||||
for (int i = 0; i < header.LumpCount; i++)
|
||||
{
|
||||
var lump = ParseLump(data);
|
||||
file.Lumps[i] = lump;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lump Infos
|
||||
|
||||
// Create the lump info array
|
||||
file.LumpInfos = new LumpInfo?[header.LumpCount];
|
||||
for (int i = 0; i < header.LumpCount; i++)
|
||||
{
|
||||
var lump = file.Lumps[i];
|
||||
if (lump == null)
|
||||
{
|
||||
file.LumpInfos[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lump.Compression != 0)
|
||||
{
|
||||
file.LumpInfos[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the lump info offset
|
||||
uint lumpInfoOffset = lump.Offset;
|
||||
if (lumpInfoOffset < 0 || lumpInfoOffset >= data.Length)
|
||||
{
|
||||
file.LumpInfos[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seek to the lump info offset
|
||||
data.Seek(lumpInfoOffset, SeekOrigin.Begin);
|
||||
|
||||
// Try to parse the lump info -- TODO: Do we ever set the mipmap level?
|
||||
var lumpInfo = ParseLumpInfo(data, lump.Type);
|
||||
file.LumpInfos[i] = lumpInfo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Texture Package header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Texture Package header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != SignatureString)
|
||||
return null;
|
||||
|
||||
header.LumpCount = data.ReadUInt32();
|
||||
header.LumpOffset = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Texture Package lump
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Half-Life Texture Package lump on success, null on error</returns>
|
||||
private static Lump ParseLump(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Lump lump = new Lump();
|
||||
|
||||
lump.Offset = data.ReadUInt32();
|
||||
lump.DiskLength = data.ReadUInt32();
|
||||
lump.Length = data.ReadUInt32();
|
||||
lump.Type = data.ReadByteValue();
|
||||
lump.Compression = data.ReadByteValue();
|
||||
lump.Padding0 = data.ReadByteValue();
|
||||
lump.Padding1 = data.ReadByteValue();
|
||||
byte[]? name = data.ReadBytes(16);
|
||||
if (name != null)
|
||||
lump.Name = Encoding.ASCII.GetString(name).TrimEnd('\0');
|
||||
|
||||
return lump;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Half-Life Texture Package lump info
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="type">Lump type</param>
|
||||
/// <param name="mipmap">Mipmap level</param>
|
||||
/// <returns>Filled Half-Life Texture Package lump info on success, null on error</returns>
|
||||
private static LumpInfo? ParseLumpInfo(Stream data, byte type, uint mipmap = 0)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
LumpInfo lumpInfo = new LumpInfo();
|
||||
|
||||
// Cache the initial offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Type 0x42 has no name, type 0x43 does. Are these flags?
|
||||
if (type == 0x42)
|
||||
{
|
||||
if (mipmap > 0)
|
||||
return null;
|
||||
|
||||
lumpInfo.Width = data.ReadUInt32();
|
||||
lumpInfo.Height = data.ReadUInt32();
|
||||
lumpInfo.PixelData = data.ReadBytes((int)(lumpInfo.Width * lumpInfo.Height));
|
||||
lumpInfo.PaletteSize = data.ReadUInt16();
|
||||
}
|
||||
else if (type == 0x43)
|
||||
{
|
||||
if (mipmap > 3)
|
||||
return null;
|
||||
|
||||
byte[]? name = data.ReadBytes(16);
|
||||
if (name != null)
|
||||
lumpInfo.Name = Encoding.ASCII.GetString(name);
|
||||
lumpInfo.Width = data.ReadUInt32();
|
||||
lumpInfo.Height = data.ReadUInt32();
|
||||
lumpInfo.PixelOffset = data.ReadUInt32();
|
||||
_ = data.ReadBytes(12); // Unknown data
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
// Seek to the pixel data
|
||||
data.Seek(initialOffset + lumpInfo.PixelOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the pixel data
|
||||
lumpInfo.PixelData = data.ReadBytes((int)(lumpInfo.Width * lumpInfo.Height));
|
||||
|
||||
// Seek back to the offset
|
||||
data.Seek(currentOffset, SeekOrigin.Begin);
|
||||
|
||||
uint pixelSize = lumpInfo.Width * lumpInfo.Height;
|
||||
|
||||
// Mipmap data -- TODO: How do we determine this during initial parsing?
|
||||
switch (mipmap)
|
||||
{
|
||||
case 1: _ = data.ReadBytes((int)pixelSize); break;
|
||||
case 2: _ = data.ReadBytes((int)(pixelSize + (pixelSize / 4))); break;
|
||||
case 3: _ = data.ReadBytes((int)(pixelSize + (pixelSize / 4) + (pixelSize / 16))); break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
_ = data.ReadBytes((int)(pixelSize + (pixelSize / 4) + (pixelSize / 16) + (pixelSize / 64))); // Pixel data
|
||||
lumpInfo.PaletteSize = data.ReadUInt16();
|
||||
lumpInfo.PaletteData = data.ReadBytes((int)lumpInfo.PaletteSize * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Adjust based on mipmap level
|
||||
switch (mipmap)
|
||||
{
|
||||
case 1:
|
||||
lumpInfo.Width /= 2;
|
||||
lumpInfo.Height /= 2;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
lumpInfo.Width /= 4;
|
||||
lumpInfo.Height /= 4;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
lumpInfo.Width /= 8;
|
||||
lumpInfo.Height /= 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return lumpInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.Models.XZP;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
using static SabreTools.Models.XZP.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
public partial class XZP : IStreamSerializer<Models.XZP.File>
|
||||
{
|
||||
/// <inheritdoc cref="IStreamSerializer.Deserialize(Stream?)"/>
|
||||
public static Models.XZP.File? DeserializeStream(Stream? data)
|
||||
{
|
||||
var deserializer = new XZP();
|
||||
return deserializer.Deserialize(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Models.XZP.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
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new XBox Package File to fill
|
||||
var file = new Models.XZP.File();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Set the package header
|
||||
file.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Entries
|
||||
|
||||
// Create the directory entry array
|
||||
file.DirectoryEntries = new DirectoryEntry[header.DirectoryEntryCount];
|
||||
|
||||
// Try to parse the directory entries
|
||||
for (int i = 0; i < header.DirectoryEntryCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.DirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preload Directory Entries
|
||||
|
||||
if (header.PreloadBytes > 0)
|
||||
{
|
||||
// Create the preload directory entry array
|
||||
file.PreloadDirectoryEntries = new DirectoryEntry[header.PreloadDirectoryEntryCount];
|
||||
|
||||
// Try to parse the preload directory entries
|
||||
for (int i = 0; i < header.PreloadDirectoryEntryCount; i++)
|
||||
{
|
||||
var directoryEntry = ParseDirectoryEntry(data);
|
||||
file.PreloadDirectoryEntries[i] = directoryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Preload Directory Mappings
|
||||
|
||||
if (header.PreloadBytes > 0)
|
||||
{
|
||||
// Create the preload directory mapping array
|
||||
file.PreloadDirectoryMappings = new DirectoryMapping[header.PreloadDirectoryEntryCount];
|
||||
|
||||
// Try to parse the preload directory mappings
|
||||
for (int i = 0; i < header.PreloadDirectoryEntryCount; i++)
|
||||
{
|
||||
var directoryMapping = ParseDirectoryMapping(data);
|
||||
file.PreloadDirectoryMappings[i] = directoryMapping;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directory Items
|
||||
|
||||
if (header.DirectoryItemCount > 0)
|
||||
{
|
||||
// Get the directory item offset
|
||||
uint directoryItemOffset = header.DirectoryItemOffset;
|
||||
if (directoryItemOffset < 0 || directoryItemOffset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Seek to the directory items
|
||||
data.Seek(directoryItemOffset, SeekOrigin.Begin);
|
||||
|
||||
// Create the directory item array
|
||||
file.DirectoryItems = new DirectoryItem[header.DirectoryItemCount];
|
||||
|
||||
// Try to parse the directory items
|
||||
for (int i = 0; i < header.DirectoryItemCount; i++)
|
||||
{
|
||||
var directoryItem = ParseDirectoryItem(data);
|
||||
file.DirectoryItems[i] = directoryItem;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Footer
|
||||
|
||||
// Seek to the footer
|
||||
data.Seek(-8, SeekOrigin.End);
|
||||
|
||||
// Try to parse the footer
|
||||
var footer = ParseFooter(data);
|
||||
if (footer == null)
|
||||
return null;
|
||||
|
||||
// Set the package footer
|
||||
file.Footer = footer;
|
||||
|
||||
#endregion
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File header on success, null on error</returns>
|
||||
private static Header? ParseHeader(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Header header = new Header();
|
||||
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
header.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (header.Signature != HeaderSignatureString)
|
||||
return null;
|
||||
|
||||
header.Version = data.ReadUInt32();
|
||||
if (header.Version != 6)
|
||||
return null;
|
||||
|
||||
header.PreloadDirectoryEntryCount = data.ReadUInt32();
|
||||
header.DirectoryEntryCount = data.ReadUInt32();
|
||||
header.PreloadBytes = data.ReadUInt32();
|
||||
header.HeaderLength = data.ReadUInt32();
|
||||
header.DirectoryItemCount = data.ReadUInt32();
|
||||
header.DirectoryItemOffset = data.ReadUInt32();
|
||||
header.DirectoryItemLength = data.ReadUInt32();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File directory entry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File directory entry on success, null on error</returns>
|
||||
private static DirectoryEntry ParseDirectoryEntry(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryEntry directoryEntry = new DirectoryEntry();
|
||||
|
||||
directoryEntry.FileNameCRC = data.ReadUInt32();
|
||||
directoryEntry.EntryLength = data.ReadUInt32();
|
||||
directoryEntry.EntryOffset = data.ReadUInt32();
|
||||
|
||||
return directoryEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File directory mapping
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File directory mapping on success, null on error</returns>
|
||||
private static DirectoryMapping ParseDirectoryMapping(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryMapping directoryMapping = new DirectoryMapping();
|
||||
|
||||
directoryMapping.PreloadDirectoryEntryIndex = data.ReadUInt16();
|
||||
|
||||
return directoryMapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File directory item
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File directory item on success, null on error</returns>
|
||||
private static DirectoryItem ParseDirectoryItem(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
DirectoryItem directoryItem = new DirectoryItem();
|
||||
|
||||
directoryItem.FileNameCRC = data.ReadUInt32();
|
||||
directoryItem.NameOffset = data.ReadUInt32();
|
||||
directoryItem.TimeCreated = data.ReadUInt32();
|
||||
|
||||
// Cache the current offset
|
||||
long currentPosition = data.Position;
|
||||
|
||||
// Seek to the name offset
|
||||
data.Seek(directoryItem.NameOffset, SeekOrigin.Begin);
|
||||
|
||||
// Read the name
|
||||
directoryItem.Name = data.ReadString(Encoding.ASCII);
|
||||
|
||||
// Seek back to the right position
|
||||
data.Seek(currentPosition, SeekOrigin.Begin);
|
||||
|
||||
return directoryItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a XBox Package File footer
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled XBox Package File footer on success, null on error</returns>
|
||||
private static Footer? ParseFooter(Stream data)
|
||||
{
|
||||
// TODO: Use marshalling here instead of building
|
||||
Footer footer = new Footer();
|
||||
|
||||
footer.FileLength = data.ReadUInt32();
|
||||
byte[]? signature = data.ReadBytes(4);
|
||||
if (signature == null)
|
||||
return null;
|
||||
|
||||
footer.Signature = Encoding.ASCII.GetString(signature);
|
||||
if (footer.Signature != FooterSignatureString)
|
||||
return null;
|
||||
|
||||
return footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
|
||||
namespace SabreTools.Serialization.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for other XML serializers
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public partial class XmlFile<T> : IStreamSerializer<T>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public T? Deserialize(Stream? data)
|
||||
{
|
||||
// If the stream is null
|
||||
if (data == null)
|
||||
return default;
|
||||
|
||||
// Setup the serializer and the reader
|
||||
var serializer = new XmlSerializer(typeof(T));
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
#endif
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
};
|
||||
var streamReader = new StreamReader(data);
|
||||
var xmlReader = XmlReader.Create(streamReader, settings);
|
||||
|
||||
// Perform the deserialization and return
|
||||
return (T?)serializer.Deserialize(xmlReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ namespace SabreTools.Serialization.Wrappers
|
||||
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
|
||||
return null;
|
||||
|
||||
var mediaKeyBlock = Streams.AACS.DeserializeStream(data);
|
||||
var mediaKeyBlock = Deserializers.AACS.DeserializeStream(data);
|
||||
if (mediaKeyBlock == null)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace SabreTools.Serialization.Wrappers
|
||||
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
|
||||
return null;
|
||||
|
||||
var svm = Streams.BDPlus.DeserializeStream(data);
|
||||
var svm = Deserializers.BDPlus.DeserializeStream(data);
|
||||
if (svm == null)
|
||||
return null;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user