Editor/HumanoidRetargeter/Embedded/Lz4.cs
// Bounded LZ4 block decoding, including the chained 64K dictionary used by binary KV3 blobs.
#nullable enable
using System;
using System.IO;
namespace HumanoidRetargeterCompression;
internal static class Lz4
{
internal static int Decode(ReadOnlySpan<byte> input, Span<byte> output, ReadOnlySpan<byte> history = default)
{
int read = 0, written = 0;
while (read < input.Length)
{
var token = input[read++];
var literals = Length(input, ref read, token >> 4);
if (literals > input.Length - read || literals > output.Length - written) throw new InvalidDataException("Invalid LZ4 literal length.");
input.Slice(read, literals).CopyTo(output[written..]); read += literals; written += literals;
if (read == input.Length) break;
if (input.Length - read < 2) throw new InvalidDataException("Truncated LZ4 match.");
var distance = input[read] | input[read + 1] << 8; read += 2;
var length = checked(Length(input, ref read, token & 15) + 4);
if (distance == 0 || distance > written + history.Length || length > output.Length - written) throw new InvalidDataException("Invalid LZ4 match.");
for (var i = 0; i < length; i++)
{
var from = written - distance;
output[written++] = from < 0 ? history[history.Length + from] : output[from];
}
}
return written;
}
static int Length(ReadOnlySpan<byte> input, ref int read, int value)
{
if (value != 15) return value;
byte next;
do
{
if (read == input.Length) throw new InvalidDataException("Truncated LZ4 length.");
next = input[read++]; value = checked(value + next);
} while (next == 255);
return value;
}
}
internal sealed class Lz4Chain : IDisposable
{
byte[] history = Array.Empty<byte>();
public Lz4Chain(int frameSize, int unused) { }
public bool DecodeAndDrain(ReadOnlySpan<byte> input, Span<byte> output, out int decoded)
{
decoded = Lz4.Decode(input, output, history);
var length = Math.Min(65536, history.Length + decoded);
var next = new byte[length];
var copied = Math.Min(decoded, length);
history.AsSpan(history.Length - (length - copied)).CopyTo(next);
output.Slice(decoded - copied, copied).CopyTo(next.AsSpan(length - copied));
history = next;
return true;
}
public void Dispose() { }
}