namespace VFXReviewWorker;
///
/// xxHash64 (ยง8.2 `checksumAlgo: "xxh64"`). Implemented here rather than taken
/// from System.IO.Hashing so the worker stays a dependency-free single-file
/// publish for offline workstation installs. Verified against the reference
/// test vectors in XxHash64Tests.
///
public static class XxHash64
{
private const ulong Prime1 = 11400714785074694791UL;
private const ulong Prime2 = 14029467366897019727UL;
private const ulong Prime3 = 1609587929392839161UL;
private const ulong Prime4 = 9650029242287828579UL;
private const ulong Prime5 = 2870177450012600261UL;
public static ulong Hash(ReadOnlySpan data, ulong seed = 0)
{
var state = new State(seed);
state.Update(data);
return state.Digest();
}
public static string HashFile(string path, int bufferSize = 1 << 20)
{
var state = new State(0);
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize, FileOptions.SequentialScan);
var buffer = new byte[bufferSize];
int read;
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
{
state.Update(new ReadOnlySpan(buffer, 0, read));
}
return state.Digest().ToString("x16");
}
/// Streaming state so multi-GB sequences hash without being held in memory.
public struct State
{
private ulong _v1, _v2, _v3, _v4;
private readonly ulong _seed;
private ulong _length;
private readonly byte[] _buffer;
private int _bufferLen;
public State(ulong seed)
{
_seed = seed;
_v1 = seed + Prime1 + Prime2;
_v2 = seed + Prime2;
_v3 = seed;
_v4 = seed - Prime1;
_length = 0;
_buffer = new byte[32];
_bufferLen = 0;
}
public void Update(ReadOnlySpan data)
{
_length += (ulong)data.Length;
if (_bufferLen > 0)
{
var need = Math.Min(32 - _bufferLen, data.Length);
data[..need].CopyTo(_buffer.AsSpan(_bufferLen));
_bufferLen += need;
data = data[need..];
if (_bufferLen < 32) return;
ProcessStripe(_buffer);
_bufferLen = 0;
}
while (data.Length >= 32)
{
ProcessStripe(data[..32]);
data = data[32..];
}
if (data.Length > 0)
{
data.CopyTo(_buffer.AsSpan(0));
_bufferLen = data.Length;
}
}
private void ProcessStripe(ReadOnlySpan stripe)
{
_v1 = Round(_v1, BitConverter.ToUInt64(stripe[..8]));
_v2 = Round(_v2, BitConverter.ToUInt64(stripe.Slice(8, 8)));
_v3 = Round(_v3, BitConverter.ToUInt64(stripe.Slice(16, 8)));
_v4 = Round(_v4, BitConverter.ToUInt64(stripe.Slice(24, 8)));
}
public ulong Digest()
{
ulong h;
if (_length >= 32)
{
h = RotateLeft(_v1, 1) + RotateLeft(_v2, 7) + RotateLeft(_v3, 12) + RotateLeft(_v4, 18);
h = MergeRound(h, _v1);
h = MergeRound(h, _v2);
h = MergeRound(h, _v3);
h = MergeRound(h, _v4);
}
else
{
h = _seed + Prime5;
}
h += _length;
var tail = new ReadOnlySpan(_buffer, 0, _bufferLen);
int i = 0;
while (i + 8 <= tail.Length)
{
h ^= Round(0, BitConverter.ToUInt64(tail.Slice(i, 8)));
h = RotateLeft(h, 27) * Prime1 + Prime4;
i += 8;
}
if (i + 4 <= tail.Length)
{
h ^= BitConverter.ToUInt32(tail.Slice(i, 4)) * Prime1;
h = RotateLeft(h, 23) * Prime2 + Prime3;
i += 4;
}
while (i < tail.Length)
{
h ^= tail[i] * Prime5;
h = RotateLeft(h, 11) * Prime1;
i++;
}
h ^= h >> 33;
h *= Prime2;
h ^= h >> 29;
h *= Prime3;
h ^= h >> 32;
return h;
}
private static ulong Round(ulong acc, ulong input)
{
acc += input * Prime2;
acc = RotateLeft(acc, 31);
acc *= Prime1;
return acc;
}
private static ulong MergeRound(ulong acc, ulong val)
{
acc ^= Round(0, val);
acc = acc * Prime1 + Prime4;
return acc;
}
private static ulong RotateLeft(ulong value, int bits) => (value << bits) | (value >> (64 - bits));
}
}