mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-05 22:01:33 +00:00
307 lines
10 KiB
C#
307 lines
10 KiB
C#
using System;
|
|
using System.IO;
|
|
using SabreTools.Data.Extensions;
|
|
using SabreTools.Data.Models.XZ;
|
|
using SabreTools.IO.Extensions;
|
|
using SabreTools.Matching;
|
|
using SabreTools.Numerics.Extensions;
|
|
using static SabreTools.Data.Models.XZ.Constants;
|
|
|
|
#pragma warning disable IDE0017 // Simplify object initialization
|
|
namespace SabreTools.Serialization.Readers
|
|
{
|
|
public class XZ : BaseBinaryReader<Archive>
|
|
{
|
|
/// <inheritdoc/>
|
|
public override Archive? Deserialize(Stream? data)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || !data.CanRead)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
// Cache the current offset
|
|
long initialOffset = 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.Signature.EqualsExactly(HeaderSignatureBytes))
|
|
return null;
|
|
|
|
// Set the stream header
|
|
archive.Header = header;
|
|
|
|
// Cache the current offset
|
|
long endOfHeader = data.Position;
|
|
|
|
#endregion
|
|
|
|
#region Footer
|
|
|
|
// Seek to the start of the footer
|
|
data.SeekIfPossible(-12, SeekOrigin.End);
|
|
|
|
// Cache the current offset
|
|
long startOfFooter = data.Position;
|
|
|
|
// Try to parse the footer
|
|
var footer = ParseFooter(data);
|
|
if (!footer.Signature.EqualsExactly(FooterSignatureBytes))
|
|
return null;
|
|
|
|
// Set the footer
|
|
archive.Footer = footer;
|
|
|
|
#endregion
|
|
|
|
#region Index
|
|
|
|
// Seek to the start of the index
|
|
long indexOffset = startOfFooter - ((footer.BackwardSize + 1) * 4);
|
|
data.SeekIfPossible(indexOffset, SeekOrigin.Begin);
|
|
|
|
// Try to parse the index
|
|
var index = ParseIndex(data);
|
|
if (index.IndexIndicator != 0x00)
|
|
return null;
|
|
if (index.Records is null)
|
|
return null;
|
|
|
|
// Set the index
|
|
archive.Index = index;
|
|
|
|
#endregion
|
|
|
|
#region Blocks
|
|
|
|
// Seek to the start of the blocks
|
|
data.SeekIfPossible(endOfHeader, SeekOrigin.Begin);
|
|
|
|
// Create the block array
|
|
int blockCount = index.Records.Length;
|
|
archive.Blocks = new Block[blockCount];
|
|
|
|
// Try to parse the blocks
|
|
for (int i = 0; i < archive.Blocks.Length; i++)
|
|
{
|
|
// Get the record for this block
|
|
var record = index.Records[i];
|
|
|
|
// Try to parse the block
|
|
archive.Blocks[i] = ParseBlock(data, header.Flags, record.UnpaddedSize);
|
|
}
|
|
|
|
#endregion
|
|
|
|
return archive;
|
|
}
|
|
catch
|
|
{
|
|
// Ignore the actual error
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a Header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled Header on success, null on error</returns>
|
|
public static Header ParseHeader(Stream data)
|
|
{
|
|
var obj = new Header();
|
|
|
|
obj.Signature = data.ReadBytes(6);
|
|
obj.Flags = (HeaderFlags)data.ReadUInt16LittleEndian();
|
|
obj.Crc32 = data.ReadUInt32LittleEndian();
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a Block
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <param name="headerFlags">HeaderFlags to for determining the check value</param>
|
|
/// <param name="unpaddedSize">Unpadded data size from the index</param>
|
|
/// <returns>Filled Block on success, null on error</returns>
|
|
public static Block ParseBlock(Stream data, HeaderFlags headerFlags, ulong unpaddedSize)
|
|
{
|
|
// Cache the current offset
|
|
long currentOffset = data.Position;
|
|
|
|
// Determine the size of the check field
|
|
int checkSize = 0;
|
|
if (headerFlags == HeaderFlags.Crc32)
|
|
checkSize = 4;
|
|
else if (headerFlags == HeaderFlags.Crc64)
|
|
checkSize = 8;
|
|
else if (headerFlags == HeaderFlags.Sha256)
|
|
checkSize = 32;
|
|
|
|
var obj = new Block();
|
|
|
|
obj.HeaderSize = data.ReadByteValue();
|
|
int realHeaderSize = (obj.HeaderSize + 1) * 4;
|
|
obj.Flags = (BlockFlags)data.ReadByteValue();
|
|
|
|
#if NET20 || NET35
|
|
if ((obj.Flags & BlockFlags.CompressedSize) != 0)
|
|
#else
|
|
if (obj.Flags.HasFlag(BlockFlags.CompressedSize))
|
|
#endif
|
|
obj.CompressedSize = ParseVariableLength(data);
|
|
|
|
#if NET20 || NET35
|
|
if ((obj.Flags & BlockFlags.UncompressedSize) != 0)
|
|
#else
|
|
if (obj.Flags.HasFlag(BlockFlags.UncompressedSize))
|
|
#endif
|
|
obj.UncompressedSize = ParseVariableLength(data);
|
|
|
|
// Determine the number of filters to read
|
|
int filterCount = ((byte)obj.Flags & 0x03) + 1;
|
|
|
|
// Try to parse the filters
|
|
obj.FilterFlags = new FilterFlag[filterCount];
|
|
for (int i = 0; i < obj.FilterFlags.Length; i++)
|
|
{
|
|
obj.FilterFlags[i] = ParseFilterFlag(data);
|
|
}
|
|
|
|
// Parse the padding as needed, adjusting for CRC size
|
|
int paddingLength = realHeaderSize - (int)(data.Position - currentOffset) - 4;
|
|
if (paddingLength >= 0)
|
|
obj.HeaderPadding = data.ReadBytes(paddingLength);
|
|
|
|
obj.Crc32 = data.ReadUInt32LittleEndian();
|
|
|
|
// Determine the compressed size
|
|
ulong compressedSize = obj.CompressedSize != 0
|
|
? obj.CompressedSize
|
|
: unpaddedSize - (ulong)(realHeaderSize + checkSize);
|
|
|
|
// TODO: How to handle large blocks?
|
|
if ((int)compressedSize > 0)
|
|
obj.CompressedData = data.ReadBytes((int)compressedSize);
|
|
|
|
// Parse the padding as needed
|
|
paddingLength = 4 - (int)(unpaddedSize % 4);
|
|
if (paddingLength >= 0)
|
|
obj.BlockPadding = data.ReadBytes(paddingLength);
|
|
|
|
// Read the Check as needed
|
|
obj.Check = data.ReadBytes(checkSize);
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a FilterFlag
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled FilterFlag on success, null on error</returns>
|
|
public static FilterFlag ParseFilterFlag(Stream data)
|
|
{
|
|
var obj = new FilterFlag();
|
|
|
|
obj.FilterID = ParseVariableLength(data);
|
|
obj.SizeOfProperties = ParseVariableLength(data);
|
|
obj.Properties = data.ReadBytes((int)obj.SizeOfProperties);
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a Index
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled Index on success, null on error</returns>
|
|
public static Data.Models.XZ.Index ParseIndex(Stream data)
|
|
{
|
|
// Cache the current offset
|
|
long currentOffset = data.Position;
|
|
|
|
var obj = new Data.Models.XZ.Index();
|
|
|
|
obj.IndexIndicator = data.ReadByteValue();
|
|
obj.NumberOfRecords = ParseVariableLength(data);
|
|
|
|
obj.Records = new Record[obj.NumberOfRecords];
|
|
for (int i = 0; i < obj.Records.Length; i++)
|
|
{
|
|
obj.Records[i] = ParseRecord(data);
|
|
}
|
|
|
|
// Parse the padding as needed
|
|
int paddingLength = 4 - ((int)(data.Position - currentOffset) % 4);
|
|
if (paddingLength >= 0)
|
|
obj.Padding = data.ReadBytes(paddingLength);
|
|
|
|
obj.Crc32 = data.ReadUInt32LittleEndian();
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a Record
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled Record on success, null on error</returns>
|
|
public static Record ParseRecord(Stream data)
|
|
{
|
|
var obj = new Record();
|
|
|
|
obj.UnpaddedSize = ParseVariableLength(data);
|
|
obj.UncompressedSize = ParseVariableLength(data);
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a Footer
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled Footer on success, null on error</returns>
|
|
public static Footer ParseFooter(Stream data)
|
|
{
|
|
var obj = new Footer();
|
|
|
|
obj.Crc32 = data.ReadUInt32LittleEndian();
|
|
obj.BackwardSize = data.ReadUInt32LittleEndian();
|
|
obj.Flags = (HeaderFlags)data.ReadUInt16LittleEndian();
|
|
obj.Signature = data.ReadBytes(2);
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a variable-length number from the stream
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Decoded variable-length value</returns>
|
|
private static ulong ParseVariableLength(Stream data)
|
|
{
|
|
// Cache the current offset
|
|
long currentOffset = data.Position;
|
|
|
|
// Read up to 9 bytes for decoding
|
|
int byteCount = (int)Math.Min(data.Length - data.Position, 9);
|
|
byte[] encoded = data.ReadBytes(byteCount);
|
|
|
|
// Attempt to decode the value
|
|
ulong output = encoded.DecodeVariableLength(byteCount, out int length);
|
|
|
|
// Seek the actual length processed and return
|
|
data.SeekIfPossible(currentOffset + length, SeekOrigin.Begin);
|
|
return output;
|
|
}
|
|
}
|
|
}
|