Add XZ reader, fix some minor issues

This commit is contained in:
Matt Nadareski
2025-09-30 09:47:19 -04:00
parent 635170a051
commit 3c520d33eb
4 changed files with 358 additions and 2 deletions

View File

@@ -0,0 +1,73 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Readers;
using Xunit;
namespace SabreTools.Serialization.Test.Readers
{
public class XZTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new XZ();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new XZ();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var deserializer = new XZ();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new XZ();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new XZ();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var deserializer = new XZ();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -9,10 +9,12 @@ namespace SabreTools.Data.Extensions
/// </summary>
/// <param name="value">Value to decode</param>
/// <param name="maxSize">Maximum number of bytes to parse</param>
/// <param name="length">Number of bytes parsed</param>
/// <returns>UInt64 representing the decoded integer</returns>
/// <see href="https://tukaani.org/xz/xz-file-format.txt"/>
public static ulong DecodeVariableLength(this byte[] value, int maxSize)
public static ulong DecodeVariableLength(this byte[] value, int maxSize, out int length)
{
length = 0;
if (maxSize <= 0)
return 0;
@@ -30,6 +32,7 @@ namespace SabreTools.Data.Extensions
output |= (ulong)(value[i] & 0x7F) << (i * 7);
}
length = i;
return output;
}

View File

@@ -12,6 +12,6 @@ namespace SabreTools.Data.Models.XZ
/// Uncompressed size of the block
/// </summary>
/// <remarks>Stored as a variable-length integer</remarks>
public ulong NumberOfRecords { get; set; }
public ulong UncompressedSize { get; set; }
}
}

View File

@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.XZ;
using SabreTools.IO.Extensions;
using static SabreTools.Data.Models.XZ.Constants;
namespace SabreTools.Serialization.Readers
{
public class XZ : BaseBinaryReader<Archive>
{
/// <inheritdoc/>
public override Archive? Deserialize(Stream? data)
{
// If the data is invalid
if (data == 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;
#endregion
#region Blocks and Index
// Create the block array
var blocks = new List<Block>();
// Try to parse the blocks
while (data.Position < data.Length)
{
// Peek at the first byte in the block
byte peek = data.ReadByteValue();
data.Seek(-1, SeekOrigin.Current);
// Blocks have values from 0x01-0x0F, index is 0x00
if (peek == 0x00)
break;
// Try to parse the block
var block = ParseBlock(data, header.Flags);
blocks.Add(block);
}
// Set the blocks
archive.Blocks = [.. blocks];
// Try to parse the index
var index = ParseIndex(data);
if (index.IndexIndicator != 0x00)
return null;
// Set the index
archive.Index = index;
#endregion
#region Footer
// Try to parse the footer
var footer = ParseFooter(data);
if (!footer.Signature.EqualsExactly(FooterSignatureBytes))
return null;
// Set the footer
archive.Footer = footer;
#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>
/// <returns>Filled Block on success, null on error</returns>
public static Block ParseBlock(Stream data, HeaderFlags headerFlags)
{
// Cache the current offset
long currentOffset = data.Position;
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
int paddingLength = realHeaderSize - (int)(data.Position - currentOffset);
if (paddingLength >= 0)
obj.HeaderPadding = data.ReadBytes(paddingLength);
obj.Crc32 = data.ReadUInt32LittleEndian();
// TODO: How to handle large blocks?
if ((int)obj.UncompressedSize >= 0)
obj.CompressedData = data.ReadBytes((int)obj.UncompressedSize);
// Parse the padding as needed
paddingLength = (int)obj.UncompressedSize % 4;
if (paddingLength >= 0)
obj.BlockPadding = data.ReadBytes(paddingLength);
// Read the Check as needed
if (headerFlags == HeaderFlags.Crc32)
obj.Check = data.ReadBytes(4);
else if (headerFlags == HeaderFlags.Crc64)
obj.Check = data.ReadBytes(8);
else if (headerFlags == HeaderFlags.Sha256)
obj.Check = data.ReadBytes(32);
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 = (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.ReadByteValue();
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.Seek(currentOffset + length, SeekOrigin.Begin);
return output;
}
}
}