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)); } }