mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-22 06:45:11 +00:00
Add XZ reader, fix some minor issues
This commit is contained in:
73
SabreTools.Serialization.Test/Readers/XZTests.cs
Normal file
73
SabreTools.Serialization.Test/Readers/XZTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
280
SabreTools.Serialization/Readers/XZ.cs
Normal file
280
SabreTools.Serialization/Readers/XZ.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user