mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
143 lines
5.2 KiB
C#
143 lines
5.2 KiB
C#
using System;
|
|
using System.IO;
|
|
using SabreTools.Hashing;
|
|
using SabreTools.Numerics.Extensions;
|
|
|
|
// TODO: Remove when IO is updated
|
|
namespace SabreTools.Wrappers
|
|
{
|
|
/// <summary>
|
|
/// Compresses data using the WIA PURGE format.
|
|
///
|
|
/// PURGE layout produced:
|
|
/// [ { u32 offset BE, u32 size BE, data[size] } ] ... (zero or more segments)
|
|
/// [ SHA-1 (20 bytes) ]
|
|
///
|
|
/// Only non-zero byte runs are emitted as segments; consecutive non-zero regions
|
|
/// separated by a gap of 8 or fewer zero bytes are merged into a single segment.
|
|
/// The SHA-1 covers: <paramref name="precedingBytes"/> (e.g. exception-list prefix) +
|
|
/// all segment headers and data bytes.
|
|
///
|
|
/// This is the exact inverse of <see cref="PurgeDecompressor"/>.
|
|
/// </summary>
|
|
internal static class PurgeCompressor
|
|
{
|
|
/// <summary>
|
|
/// Zero-byte runs of this length or fewer are bridged
|
|
/// </summary>
|
|
private const int MaxGap = 8;
|
|
|
|
/// <summary>
|
|
/// Compress <paramref name="data"/>[<paramref name="offset"/> ..
|
|
/// <paramref name="offset"/>+<paramref name="count"/>) into PURGE format.
|
|
/// </summary>
|
|
/// <param name="data">Source buffer.</param>
|
|
/// <param name="offset">Start of data within <paramref name="data"/>.</param>
|
|
/// <param name="count">Number of bytes to compress.</param>
|
|
/// <param name="precedingBytes">
|
|
/// Optional bytes that precede this payload in the WIA group
|
|
/// (e.g. the serialised exception list). Included in the SHA-1 but not emitted.
|
|
/// Pass null or empty if there are none.
|
|
/// </param>
|
|
/// <returns>PURGE-compressed byte array (segments + 20-byte SHA-1).</returns>
|
|
public static byte[] Compress(byte[] data, int offset, int count, byte[]? precedingBytes = null)
|
|
{
|
|
var output = new MemoryStream((count / 2) + 32);
|
|
|
|
int end = offset + count;
|
|
int pos = offset;
|
|
|
|
while (pos < end)
|
|
{
|
|
// Skip leading zeros
|
|
while (pos < end && data[pos] == 0)
|
|
{
|
|
pos++;
|
|
}
|
|
|
|
if (pos >= end)
|
|
break;
|
|
|
|
// pos is now the start of a non-zero run (segment start)
|
|
int segStart = pos;
|
|
int segEnd = pos;
|
|
|
|
// Extend the segment, bridging zero-gaps of <= MaxGap bytes
|
|
while (segEnd < end)
|
|
{
|
|
// advance through non-zero bytes
|
|
while (segEnd < end && data[segEnd] != 0)
|
|
{
|
|
segEnd++;
|
|
}
|
|
|
|
// peek ahead: count zero bytes
|
|
int zeroRun = 0;
|
|
while (segEnd + zeroRun < end && data[segEnd + zeroRun] == 0)
|
|
{
|
|
zeroRun++;
|
|
}
|
|
|
|
// If the gap is small enough (and there is more non-zero data after it),
|
|
// bridge the gap by including it in the segment.
|
|
if (zeroRun > 0 && zeroRun <= MaxGap && segEnd + zeroRun < end)
|
|
segEnd += zeroRun; // include zeros in segment, keep scanning
|
|
else
|
|
break; // end of segment
|
|
}
|
|
|
|
// Trim trailing zeros from segment end
|
|
while (segEnd > segStart && data[segEnd - 1] == 0)
|
|
{
|
|
segEnd--;
|
|
}
|
|
|
|
if (segEnd <= segStart)
|
|
{
|
|
pos = segEnd + 1;
|
|
continue;
|
|
}
|
|
|
|
uint segOffset = (uint)(segStart - offset);
|
|
uint segSize = (uint)(segEnd - segStart);
|
|
|
|
output.WriteBigEndian(segOffset);
|
|
output.WriteBigEndian(segSize);
|
|
output.Write(data, segStart, (int)segSize);
|
|
|
|
pos = segEnd;
|
|
}
|
|
|
|
byte[] segments = output.ToArray();
|
|
|
|
// SHA-1 over: precedingBytes + segments
|
|
byte[] hash = ComputeSha1(precedingBytes, segments);
|
|
|
|
// Final result: segments + hash
|
|
byte[] result = new byte[segments.Length + 20];
|
|
Array.Copy(segments, 0, result, 0, segments.Length);
|
|
Array.Copy(hash, 0, result, segments.Length, 20);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the SHA-1 hash of preceeding bytes and segment data
|
|
/// </summary>
|
|
/// <param name="precedingBytes">Optional preceeding bytes</param>
|
|
/// <param name="segments">Segment data</param>
|
|
/// <returns>SHA-1 hash of the data, all 0x00 on error</returns>
|
|
private static byte[] ComputeSha1(byte[]? precedingBytes, byte[] segments)
|
|
{
|
|
using var sha1 = new HashWrapper(HashType.SHA1);
|
|
|
|
if (precedingBytes is not null && precedingBytes.Length > 0)
|
|
sha1.Process(precedingBytes, 0, precedingBytes.Length);
|
|
|
|
sha1.Process(segments, 0, segments.Length);
|
|
sha1.Terminate();
|
|
|
|
return sha1.CurrentHashBytes ?? new byte[20];
|
|
}
|
|
}
|
|
}
|