import 'dart:typed_data'; import 'package:pure_zstd/src/xxhash.dart'; import 'package:test/test.dart'; // Every digest below came from the `xxhash` Python package, version 3.0.0, // which wraps the reference C implementation. The two byte generators are // written identically on both sides, so the inputs are reproducible here // without carrying the bytes themselves. // // The lengths are chosen around the boundaries of the tail, which is // consumed in 32-byte stripes, then 8, then 4, then 1. Each of those is a // separate branch and each gets a length either side of it. /// A repeating pattern. 251 is prime, so it does not line up with the stripe. Uint8List _pattern(int length) => Uint8List.fromList(List.generate(length, (i) => i % 251)); /// Bytes with no structure, from a deterministic generator rather than /// `Random`, so the expected digests stay valid. Uint8List _pseudoRandom(int length) { final out = Uint8List(length); var x = 1; for (var i = 0; i < length; i++) { x = (x * 1103515245 + 12345) & 0x7FFFFFFF; out[i] = (x >> 16) & 0xFF; } return out; } const List<(int, int)> _patternDigests = [ (0, 0xEF46DB3751D8E999), (1, 0xE934A84ADB052768), (2, 0x4865A00BA85B7CD6), (3, 0xE5C7BB4533BC65DD), (4, 0xFFCED8604453CC1E), (5, 0xDD0274386E26030C), (7, 0x14CC643F630C72D2), (8, 0x884A173614B81B8D), (9, 0x67D85784A7C78C5B), (15, 0xA948F5F0F6ABAC2D), (16, 0x44B6EF2FB84169F7), (17, 0x5603E60C527599B6), (31, 0xC346D2B59B4D8EE1), (32, 0xCBF59C5116FF32B4), (33, 0x0C535D1ACAFB8EAD), (63, 0xE26AA9E2A95F8E4F), (64, 0xF7C67301DB6713F0), (65, 0xC31EB63B2AE4465B), (127, 0x464D085810CE0199), (128, 0x7A7FE14647B9AB92), (129, 0x0BA25DFD6E891FCF), (255, 0x566D96B832B967C1), (256, 0xF33944343EE85824), (1000, 0xF306F04AA88B54D3), (4096, 0x122A8C8D994AD3EC), (5000, 0xA6833D648FD6A332), ]; const List<(int, int)> _pseudoRandomDigests = [ (0, 0xEF46DB3751D8E999), (1, 0xD2DA77930F69647C), (2, 0x9F33D4BD50F99C34), (3, 0xF7C331CF2042B940), (4, 0x1B042F821FC4A793), (5, 0x6E1F98107D9DB571), (7, 0x41D39023940006C8), (8, 0xEF7823FCE4AD9AFB), (9, 0x81CF88E9D8340150), (15, 0x6F0095B1C00E82FB), (16, 0x29965039DF6047BB), (17, 0x5236FCB96D8FB228), (31, 0x68C3DBB752F3FA97), (32, 0xAD7DC5A569BB047C), (33, 0x475B642AC692E380), (63, 0xC18C4743DE1B99A8), (64, 0x06872C5D6370DE25), (65, 0x8B5F4E34A32C5A09), (127, 0x77DC651915D836B5), (128, 0x2E9C4CB0B29BC8EC), (129, 0xB5E3A45BFF735CFA), (255, 0x9AD1F660B88ABC38), (256, 0x9AE30993C3F4B05A), (1000, 0x7AD5B044FB6FA81D), (4096, 0x4AFD74BF8BD52DF5), (5000, 0xCAEF4049E5C55187), ]; /// `Mt Herman 2764 m\n` 241 times, through `zstd -19 --check`. The frame is /// embedded whole so the trailing `Content_Checksum` is read from a real /// frame rather than asserted from a constant. const String _checkedFrame = '28b52ffd64010fcd0000884d74204865726d616e2032373634206d0a01' '00db4ffe5c02fe13e790'; Uint8List _hex(String text) { final out = Uint8List(text.length ~/ 2); for (var i = 0; i < out.length; i++) { out[i] = int.parse(text.substring(i * 2, i * 2 + 2), radix: 16); } return out; } void main() { group('xxh64 in one shot', () { for (final (length, expected) in _patternDigests) { test('matches the reference over $length repeating bytes', () { expect(xxh64(_pattern(length)), expected); }); } for (final (length, expected) in _pseudoRandomDigests) { test('matches the reference over $length unstructured bytes', () { expect(xxh64(_pseudoRandom(length)), expected); }); } test('honours a seed, including one that is negative as a Dart int', () { expect(xxh64(_pattern(0), seed: 1), 0xD5AFBA1336A3BE4B); expect(xxh64(_pattern(5), seed: 1), 0x27CE45508B78A340); expect(xxh64(_pattern(32), seed: 1), 0xD74E6766CE9DBA94); expect(xxh64(_pattern(200), seed: 1), 0x20BD094800CB1DFA); const seed = 0x9E3779B185EBCA87; expect(seed.isNegative, isTrue); expect(xxh64(_pattern(0), seed: seed), 0x6EC6D05F61C7E7A7); expect(xxh64(_pattern(5), seed: seed), 0xF60DC54C180A9098); expect(xxh64(_pattern(32), seed: seed), 0xBFB3E4EF6096C49C); expect(xxh64(_pattern(200), seed: seed), 0x6EA98426E0B64F4A); }); }); group('the incremental form', () { // Lengths either side of every boundary, plus enough stripes that a // chunking can straddle several. const lengths = [ 0, 1, 3, 4, 7, 8, 15, 16, 31, 32, 33, 63, 64, 65, 127, 128, 129, 1000, 4096, 5001, ]; for (final chunk in [1, 3, 7, 8, 17, 31, 32, 33, 64, 250]) { test('in $chunk-byte chunks equals the one shot', () { for (final length in lengths) { final data = _pseudoRandom(length); final hash = Xxh64(); for (var at = 0; at < length; at += chunk) { final end = at + chunk < length ? at + chunk : length; hash.update(data, at, end); } expect(hash.digest, xxh64(data), reason: '$length bytes'); } }); } test('ignores empty chunks, leading, trailing and interleaved', () { final data = _pattern(300); final empty = Uint8List(0); final hash = Xxh64() ..update(empty) ..update(data, 0, 1) ..update(empty) ..update(data, 1, 40) ..update(data, 40, 40) ..update(data, 40, 300) ..update(empty); expect(hash.digest, xxh64(data)); }); test('accepts a whole list with no range', () { final data = _pattern(100); final hash = Xxh64() ..update(Uint8List.sublistView(data, 0, 37)) ..update(Uint8List.sublistView(data, 37)); expect(hash.digest, xxh64(data)); }); test('reads the same digest twice and keeps hashing afterwards', () { final data = _pattern(500); final hash = Xxh64()..update(data, 0, 200); expect(hash.digest, xxh64(_pattern(200))); expect(hash.digest, xxh64(_pattern(200))); hash.update(data, 200, 500); expect(hash.digest, xxh64(data)); }); test('reset returns it to the state of a fresh instance', () { final hash = Xxh64()..update(_pattern(999)); hash.reset(); expect(hash.digest, xxh64(_pattern(0))); hash.update(_pattern(65)); expect(hash.digest, xxh64(_pattern(65))); }); test('rejects a range outside the list', () { expect(() => Xxh64().update(_pattern(10), 0, 11), throwsRangeError); expect(() => Xxh64().update(_pattern(10), -1), throwsRangeError); expect(() => Xxh64().update(_pattern(10), 6, 5), throwsRangeError); }); }); group('a real zstd frame', () { final frame = _hex(_checkedFrame); final content = Uint8List.fromList( List.filled( 241, 'Mt Herman 2764 m\n'.codeUnits, ).expand((e) => e).toList(), ); test('declares a content checksum', () { expect((frame[4] >> 2) & 1, 1); }); test('ends with the low 32 bits of the content digest', () { final stored = ByteData.sublistView( frame, frame.length - 4, ).getUint32(0, Endian.little); expect((Xxh64()..update(content)).digest32, stored); expect(xxh64(content) & 0xFFFFFFFF, stored); }); }); }