using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
///
/// 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: (e.g. exception-list prefix) +
/// all segment headers and data bytes.
///
/// This is the exact inverse of .
///
internal static class PurgeCompressor
{
///
/// Zero-byte runs of this length or fewer are bridged
///
private const int MaxGap = 8;
///
/// Compress [ ..
/// +) into PURGE format.
///
/// Source buffer.
/// Start of data within .
/// Number of bytes to compress.
///
/// 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.
///
/// PURGE-compressed byte array (segments + 20-byte SHA-1).
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;
}
///
/// Get the SHA-1 hash of preceeding bytes and segment data
///
/// Optional preceeding bytes
/// Segment data
/// SHA-1 hash of the data, all 0x00 on error
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];
}
}
}