From b56fc0d0d814d04648d4ba4813fbd21b90c61897 Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Tue, 22 Sep 2026 13:33:45 +0400 Subject: [PATCH] feat: crc32 --- README.md | 15 +++ benchmarks/crc.bench.js | 60 +++++++++ crc.d.ts | 20 +++ crc.js | 52 ++++++++ crc.node.js | 38 ++++++ fallback/crc.js | 18 +++ package.json | 11 ++ pnpm-lock.yaml | 26 ++++ tests/crc.test.js | 263 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 503 insertions(+) create mode 100644 benchmarks/crc.bench.js create mode 100644 crc.d.ts create mode 100644 crc.js create mode 100644 crc.node.js create mode 100644 fallback/crc.js create mode 100644 tests/crc.test.js diff --git a/README.md b/README.md index 53ff93da..bd4805bd 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,21 @@ Uses double SHA-256 for checksum calculation Create a base58check encoder/decoder with custom hash functions +### @exodus/bytes/crc.js ![](https://img.shields.io/bundlejs/size/@exodus/bytes/crc.js?style=flat-square) + +Implements crc32 from [IEEE 802.3](https://standards.ieee.org/ieee/802.3/10422/), +[ISO 3309](https://www.iso.org/standard/8561.html), +[ISO/IEC 13239:2002](https://www.iso.org/standard/37010.html), +and others. + +```js +import { crc32 } from '@exodus/bytes/crc.js' +``` + +#### `crc32(arr)` + +Calculate the CRC-32 of a `Uint8Array`, as an unsigned number. + ### @exodus/bytes/wif.js ![](https://img.shields.io/bundlejs/size/@exodus/bytes/wif.js?style=flat-square) Wallet Import Format (WIF) encoding and decoding. diff --git a/benchmarks/crc.bench.js b/benchmarks/crc.bench.js new file mode 100644 index 00000000..518dd193 --- /dev/null +++ b/benchmarks/crc.bench.js @@ -0,0 +1,60 @@ +import * as exodus from '@exodus/bytes/crc.js' +import { benchmark } from '@exodus/test/benchmark' // eslint-disable-line @exodus/import/no-unresolved +import crc from 'crc' +import crc32 from 'crc-32' +import { describe, test } from 'node:test' +import * as zlib from 'node:zlib' + +import { Table } from './utils/table.js' + +const columns = ['@exodus/bytes/crc32', 'crc', 'crc-32', 'zlib'] + +const seed = crypto.getRandomValues(new Uint8Array(5 * 1024)) + +const bufs32 = [] +const bufs5mb = [] +const N = 3000 + +for (let i = 0; i < N; i++) { + bufs5mb.push(seed.map((x, j) => x + i * j)) + const at = Math.floor(Math.random() * 100) + bufs32.push(seed.subarray(at, at + 32).map((x, j) => x + i * j)) +} + +describe('benchmarks: crc32', async () => { + // [name, impl, skip] + const libs = [ + ['@exodus/bytes/crc32', (x) => exodus.crc32(x)], + ['crc', (x) => crc.crc32(x)], + ['crc-32', (x) => crc32.buf(x)], + ['zlib', (x) => zlib.crc32(x), !zlib.crc32], + ] + + test('crc32 coherence', (t) => { + for (let i = 0; i < 10; i++) { + for (const [name, f, skip] of libs) { + if (skip) continue + t.assert.deepEqual(f(bufs32[i]) >>> 0, exodus.crc32(bufs32[i]), name) + t.assert.deepEqual(f(bufs5mb[i]) >>> 0, exodus.crc32(bufs5mb[i]), name) + } + } + }) + + test('crc32, 32 bytes', { timeout: 10_000 }, async () => { + const res = new Table() + for (const [name, f, skip] of libs) { + res.add(name, await benchmark(`crc32: ${name}`, { skip, args: bufs32 }, f)) + } + + res.print(columns) + }) + + test('crc32, 5 KiB', { timeout: 10_000 }, async () => { + const res = new Table() + for (const [name, f, skip] of libs) { + res.add(name, await benchmark(`crc32: ${name}`, { skip, args: bufs5mb }, f)) + } + + res.print(columns) + }) +}) diff --git a/crc.d.ts b/crc.d.ts new file mode 100644 index 00000000..a2c45cea --- /dev/null +++ b/crc.d.ts @@ -0,0 +1,20 @@ +/** + * Implements crc32 from [IEEE 802.3](https://standards.ieee.org/ieee/802.3/10422/), + * [ISO 3309](https://www.iso.org/standard/8561.html), + * [ISO/IEC 13239:2002](https://www.iso.org/standard/37010.html), + * and others. + * + * ```js + * import { crc32 } from '@exodus/bytes/crc.js' + * ``` + * + * @module @exodus/bytes/crc.js + */ + +/** + * Calculate the CRC-32 of a `Uint8Array`, as an unsigned number. + * + * @param arr - The input bytes + * @returns CRC-32 as an unsigned number from 0 to 2**32-1 + */ +export function crc32(arr: Uint8Array): number; diff --git a/crc.js b/crc.js new file mode 100644 index 00000000..d74b49e4 --- /dev/null +++ b/crc.js @@ -0,0 +1,52 @@ +import { assertU8 } from './fallback/_utils.js' +import { crc32Table } from './fallback/crc.js' +import { isHermes, isLE } from './fallback/platform.js' + +const T = crc32Table() + +const [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, Ta, Tb, Tc, Td, Te, Tf] = Array.from( + { length: 16 }, + (_, k) => T.subarray(k * 256, k * 256 + 256) +) + +export function crc32(x) { + assertU8(x) + let c = -1 + let i = 0 + const n = x.length + + if (isLE && n > 1024) { + const pre = (4 - (x.byteOffset & 3)) & 3 + for (; i < pre; i++) c = T0[(c ^ x[i]) & 0xff] ^ (c >>> 8) + const words = (n - i) >>> 2 + const W = new (isHermes ? Uint32Array : Int32Array)(x.buffer, x.byteOffset + i, words) + let j = 0 + for (const end = words - 3; j < end; j += 4) { + const a = W[j] ^ c + const b = W[j + 1] + const d = W[j + 2] + const e = W[j + 3] + // prettier-ignore + c = + T[0xf_00 + (a & 0xff)] ^ T[0xe_00 + ((a >>> 8) & 0xff)] ^ T[0xd_00 + ((a >>> 16) & 0xff)] ^ T[0xc_00 + (a >>> 24)] ^ + T[0xb_00 + (b & 0xff)] ^ T[0xa_00 + ((b >>> 8) & 0xff)] ^ T[0x9_00 + ((b >>> 16) & 0xff)] ^ T[0x8_00 + (b >>> 24)] ^ + T[0x7_00 + (d & 0xff)] ^ T[0x6_00 + ((d >>> 8) & 0xff)] ^ T[0x5_00 + ((d >>> 16) & 0xff)] ^ T[0x4_00 + (d >>> 24)] ^ + T[0x3_00 + (e & 0xff)] ^ T[0x2_00 + ((e >>> 8) & 0xff)] ^ T[0x1_00 + ((e >>> 16) & 0xff)] ^ T[e >>> 24] + } + + i += j * 4 // not << 2, so that inputs >= 2 GiB stay correct + } else { + for (const end = n - 15; i < end; i += 16) { + // prettier-ignore + c = + Tf[(x[i] ^ c) & 0xff] ^ Te[(x[i + 1] ^ (c >>> 8)) & 0xff] ^ + Td[(x[i + 2] ^ (c >>> 16)) & 0xff] ^ Tc[x[i + 3] ^ (c >>> 24)] ^ + Tb[x[i + 4]] ^ Ta[x[i + 5]] ^ T9[x[i + 6]] ^ T8[x[i + 7]] ^ + T7[x[i + 8]] ^ T6[x[i + 9]] ^ T5[x[i + 10]] ^ T4[x[i + 11]] ^ + T3[x[i + 12]] ^ T2[x[i + 13]] ^ T1[x[i + 14]] ^ T0[x[i + 15]] + } + } + + for (let k = i; k < n; k++) c = T0[(c ^ x[k]) & 0xff] ^ (c >>> 8) + return ~c >>> 0 +} diff --git a/crc.node.js b/crc.node.js new file mode 100644 index 00000000..197d531d --- /dev/null +++ b/crc.node.js @@ -0,0 +1,38 @@ +import { crc32 as native } from 'node:zlib' +import { assertU8 } from './fallback/_utils.js' +import { crc32Table } from './fallback/crc.js' + +const T = crc32Table() +const [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, Ta, Tb, Tc, Td, Te, Tf] = Array.from( + { length: 16 }, + (_, k) => T.subarray(k * 256, k * 256 + 256) +) + +// zlib.crc32 is a V8 fast API call since Node.js 24.9.0 (nodejs/node#59813) +const [major, minor] = (process.versions?.node || '0').split('.').map(Number) +const fastNative = major > 24 || (major === 24 && minor >= 9) + +// Node.js x64 builds have SIMD crc32 disabled in zlib (nodejs/node#45268) +// arm64 builds use the hardware crc32x instruction +const NATIVE_MIN = + !!globalThis.Deno || !!globalThis.Bun ? 0 : process.arch === 'arm64' ? (fastNative ? 0 : 64) : 256 + +export function crc32(x) { + assertU8(x) + const n = x.length + if (n >= NATIVE_MIN) return native(x) + let c = -1 + let i = 0 + for (const end = n - 15; i < end; i += 16) { + // prettier-ignore + c = + Tf[(x[i] ^ c) & 0xff] ^ Te[(x[i + 1] ^ (c >>> 8)) & 0xff] ^ + Td[(x[i + 2] ^ (c >>> 16)) & 0xff] ^ Tc[x[i + 3] ^ (c >>> 24)] ^ + Tb[x[i + 4]] ^ Ta[x[i + 5]] ^ T9[x[i + 6]] ^ T8[x[i + 7]] ^ + T7[x[i + 8]] ^ T6[x[i + 9]] ^ T5[x[i + 10]] ^ T4[x[i + 11]] ^ + T3[x[i + 12]] ^ T2[x[i + 13]] ^ T1[x[i + 14]] ^ T0[x[i + 15]] + } + + for (let j = i; j < n; j++) c = T0[(c ^ x[j]) & 0xff] ^ (c >>> 8) + return ~c >>> 0 +} diff --git a/fallback/crc.js b/fallback/crc.js new file mode 100644 index 00000000..1a04b60c --- /dev/null +++ b/fallback/crc.js @@ -0,0 +1,18 @@ +import { isHermes } from './platform.js' + +export function crc32Table() { + const T = new (isHermes ? Uint32Array : Int32Array)(16 * 256) // Signed to fit int32 on read + + for (let n = 0; n < 256; n++) { + let c = n + for (let i = 0; i < 8; i++) c = c & 1 ? 0xed_b8_83_20 ^ (c >>> 1) : c >>> 1 + T[n] = c + } + + for (let n = 0; n < 256; n++) { + let v = T[n] + for (let c = 256 + n; c < T.length; c += 256) v = T[c] = (v >>> 8) ^ T[v & 0xff] + } + + return T +} diff --git a/package.json b/package.json index 380b34a6..007a7fb0 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "/fallback/base32.js", "/fallback/base58check.js", "/fallback/base64.js", + "/fallback/crc.js", "/fallback/encoding.js", "/fallback/encoding.api.js", "/fallback/encoding.labels.js", @@ -106,6 +107,9 @@ "/bech32.d.ts", "/bigint.js", "/bigint.d.ts", + "/crc.js", + "/crc.d.ts", + "/crc.node.js", "/encoding-browser.js", "/encoding-browser.browser.js", "/encoding-browser.native.js", @@ -175,6 +179,11 @@ "types": "./bigint.d.ts", "default": "./bigint.js" }, + "./crc.js": { + "types": "./crc.d.ts", + "node": "./crc.node.js", + "default": "./crc.js" + }, "./hex.js": { "types": "./hex.d.ts", "node": "./hex.node.js", @@ -272,6 +281,8 @@ "bstring": "^0.3.9", "buffer": "^6.0.3", "c8": "^10.1.3", + "crc": "^4.3.2", + "crc-32": "^1.2.2", "decode-utf8": "^1.0.1", "electron": "39.4.0", "encode-utf8": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e3be4c4..50b91e2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,12 @@ importers: c8: specifier: ^10.1.3 version: 10.1.3 + crc: + specifier: ^4.3.2 + version: 4.3.2(buffer@6.0.3) + crc-32: + specifier: ^1.2.2 + version: 1.2.2 decode-utf8: specifier: ^1.0.1 version: 1.0.1 @@ -1183,6 +1189,20 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, tarball: https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==, tarball: https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz} + engines: {node: '>=0.8'} + hasBin: true + + crc@4.3.2: + resolution: {integrity: sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==, tarball: https://registry.npmjs.org/crc/-/crc-4.3.2.tgz} + engines: {node: '>=12'} + peerDependencies: + buffer: '>=6.0.3' + peerDependenciesMeta: + buffer: + optional: true + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz} @@ -4426,6 +4446,12 @@ snapshots: core-util-is@1.0.3: {} + crc-32@1.2.2: {} + + crc@4.3.2(buffer@6.0.3): + optionalDependencies: + buffer: 6.0.3 + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 diff --git a/tests/crc.test.js b/tests/crc.test.js new file mode 100644 index 00000000..0e3599c0 --- /dev/null +++ b/tests/crc.test.js @@ -0,0 +1,263 @@ +import { crc32 } from '@exodus/bytes/crc.js' +import * as lib from '../crc.js' +import { randomValues } from '@exodus/crypto/randomBytes' +import { describe, test } from 'node:test' + +const SharedArrayBuffer = globalThis.SharedArrayBuffer ?? ArrayBuffer +const toShared = (u8, offset = 0) => { + const res = new Uint8Array(new SharedArrayBuffer(u8.length + offset)).subarray(offset) + res.set(u8) + return res +} + +// Bitwise reference implementation (no tables, no fast paths), for coherence checks +function crc32reference(arr) { + let c = 0xff_ff_ff_ff + for (let i = 0; i < arr.length; i++) { + c ^= arr[i] + for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xed_b8_83_20 : c >>> 1 + } + + return (c ^ 0xff_ff_ff_ff) >>> 0 +} + +const ascii = (str) => Uint8Array.from(str, (c) => c.charCodeAt(0)) + +// Well-known CRC-32 test vectors, e.g. Go hash/crc32 test suite +// prettier-ignore +const STRINGS = [ + ['', 0x00_00_00_00], + ['a', 0xe8_b7_be_43], + ['ab', 0x9e_83_48_6d], + ['abc', 0x35_24_41_c2], + ['abcd', 0xed_82_cd_11], + ['abcde', 0x85_87_d8_65], + ['abcdef', 0x4b_8e_39_ef], + ['abcdefg', 0x31_2a_6a_a6], + ['abcdefgh', 0xae_ef_2a_50], + ['abcdefghi', 0x8d_a9_88_af], + ['abcdefghij', 0x39_81_70_3a], + ['123456789', 0xcb_f4_39_26], // "check" value from the CRC catalogue + ['The quick brown fox jumps over the lazy dog', 0x41_4f_a3_39], + ['Discard medicine more than two years old.', 0x6b_9c_df_e7], + ['He who has a shady past knows that nice guys finish last.', 0xc9_0e_f7_3f], + ["I wouldn't marry him with a ten foot pole.", 0xb9_02_34_1f], + ['Free! Free!/A trip/to Mars/for 900/empty jars/Burma Shave', 0x04_20_80_e8], + ['The days of the digital watch are numbered. -Tom Stoppard', 0x15_4c_6d_11], + ["Nepal premier won't resign.", 0x4c_41_83_25], + ['For every action there is an equal and opposite government program.', 0x33_95_51_50], + ["His money is twice tainted: 'taint yours and 'taint mine.", 0x26_21_6a_4b], + ['There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977', 0x1a_bb_e4_5e], + ["It's a tiny change to the code and not completely disgusting. - Bob Manchek", 0xc8_9a_94_f7], + ['size: a.out: bad magic', 0xab_3a_be_14], + ['The major problem is with sendmail. -Mark Horton', 0xba_b1_02_b6], + ['Give me a rock, paper and scissors and I will move the world. CCFestoon', 0x99_91_49_d7], + ['If the enemy is within range, then so are you.', 0x6d_52_a3_3c], + ["It's well we cannot hear the screams/That we create in others' dreams.", 0x90_63_1e_8d], + ["You remind me of a TV show, but that's all right: I watch it anyway.", 0x78_30_91_30], + ['C is as portable as Stonehedge!!', 0x7d_0a_37_7f], + ['Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley', 0x8c_79_fd_79], + ['The fugacity of a constituent in a mixture of gases at a given temperature is proportional to its mole fraction. Lewis-Randall Rule', 0xa2_0b_71_67], + ['How can you write a big system without C++? -Paul Glick', 0x8e_0b_b4_43], +] + +const zeros = (n) => new Uint8Array(n) +const filled = (n, v) => new Uint8Array(n).fill(v) +const incrementing = (n) => Uint8Array.from({ length: n }, (_, i) => i & 0xff) +const decrementing = (n) => Uint8Array.from({ length: n }, (_, i) => (n - 1 - i) & 0xff) + +// prettier-ignore +const BYTES = [ + ['[0] x32', zeros(32), 0x19_0a_55_ad], + ['[255] x32', filled(32, 0xff), 0xff_6c_ab_0b], + ['0..31', incrementing(32), 0x91_26_7e_8a], + ['31..0', decrementing(32), 0x9a_b0_ef_72], + ['0..63', incrementing(64), 0x10_0e_ce_8c], + ['[0] x65', zeros(65), 0x1d_cd_f7_77], + ['0..64', incrementing(65), 0x40_c0_6f_d8], + ['0..255', incrementing(256), 0x29_05_8c_73], + ['[0] x512', zeros(512), 0xb2_aa_75_78], + ['[0] x1024', zeros(1024), 0xef_b5_af_2e], + ['[255] x1024', filled(1024, 0xff), 0xb8_3a_ff_f4], + ['(0..255) x4', incrementing(1024), 0xb7_0b_4c_26], + ['(0..255) x16', incrementing(4096), 0xa2_91_20_82], +] + +const INVALID = [ + null, + undefined, + [], + [1, 2], + 'string', + '', + 0, + 12, + {}, + new Uint16Array(1), + new Uint8ClampedArray(1), + new Int8Array(1), + new ArrayBuffer(4), + new DataView(new ArrayBuffer(4)), +] + +const seed = randomValues(2048) // enough for the largest size + offset below + +const skipLarge = + process.env.EXODUS_TEST_PLATFORM === 'quickjs' || + process.env.EXODUS_TEST_PLATFORM === 'xs' || + process.env.EXODUS_TEST_PLATFORM === 'boa' || + process.env.EXODUS_TEST_PLATFORM === 'graaljs' || + process.env.EXODUS_TEST_PLATFORM === 'engine262' + +describe('crc32', () => { + test('invalid input', (t) => { + for (const input of INVALID) { + t.assert.throws(() => crc32(input), TypeError) + t.assert.throws(() => lib.crc32(input), TypeError) + } + }) + + test('fixtures, strings', (t) => { + for (const [str, expected] of STRINGS) { + const uint8 = ascii(str) + t.assert.strictEqual(crc32reference(uint8), expected, `reference: ${str}`) + for (const arg of [uint8, toShared(uint8), Buffer.from(uint8)]) { + t.assert.strictEqual(crc32(arg), expected, str) + t.assert.strictEqual(lib.crc32(arg), expected, str) + } + } + }) + + test('fixtures, bytes', (t) => { + for (const [name, uint8, expected] of BYTES) { + t.assert.strictEqual(crc32reference(uint8), expected, `reference: ${name}`) + for (const arg of [uint8, toShared(uint8), Buffer.from(uint8)]) { + t.assert.strictEqual(crc32(arg), expected, name) + t.assert.strictEqual(lib.crc32(arg), expected, name) + } + } + }) + + test('returns an unsigned 32-bit integer', (t) => { + for (const uint8 of [ascii('a'), ascii('abc'), seed, ...BYTES.map(([, x]) => x)]) { + for (const res of [crc32(uint8), lib.crc32(uint8)]) { + t.assert.strictEqual(typeof res, 'number') + t.assert.ok(Number.isInteger(res)) + t.assert.ok(res >= 0 && res <= 0xff_ff_ff_ff) + t.assert.strictEqual(res >>> 0, res) + } + } + + // Values with the top bit set stay positive + t.assert.strictEqual(crc32(ascii('a')), 0xe8_b7_be_43) + t.assert.ok(crc32(ascii('a')) > 0x7f_ff_ff_ff) + }) + + test('does not depend on the buffer around the view', (t) => { + for (const size of [ + 0, 1, 3, 4, 5, 15, 16, 17, 63, 64, 65, 100, 255, 256, 257, 511, 512, 513, 1024, 1025, + ]) { + for (const offset of [0, 1, 2, 3, 4, 5, 6, 7, 64, 65, 100, 101]) { + if (offset + size > seed.length) continue + const arr = seed.subarray(offset, offset + size) + const copy = Uint8Array.from(arr) + t.assert.strictEqual(copy.byteOffset, 0) + const expected = crc32reference(copy) + t.assert.strictEqual(crc32(copy), expected, `x${size} copy`) + t.assert.strictEqual(crc32(arr), expected, `x${size} +${offset}`) + t.assert.strictEqual(lib.crc32(arr), expected, `x${size} +${offset}`) + t.assert.strictEqual(crc32(toShared(arr, offset)), expected, `x${size} +${offset} shared`) + t.assert.strictEqual( + lib.crc32(toShared(arr, offset)), + expected, + `x${size} +${offset} shared` + ) + const buffer = Buffer.from(arr) // pooled on small sizes, so has arbitrary byteOffset + t.assert.strictEqual(crc32(buffer), expected, `x${size} +${offset} Buffer`) + t.assert.strictEqual(lib.crc32(buffer), expected, `x${size} +${offset} Buffer`) + } + } + }) + + test('sizes and offsets, random data', (t) => { + // All small sizes at all alignments: covers the byte path with all tail lengths, and crosses + // both native thresholds of the Node.js entry (64 on arm64 before 24.9, 256 elsewhere) + for (let offset = 0; offset < 8; offset++) { + for (let size = 0; size <= 300; size++) { + const arr = seed.subarray(offset, offset + size) + const expected = crc32reference(arr) + t.assert.strictEqual(crc32(arr), expected, `random x${size} +${offset}`) + t.assert.strictEqual(lib.crc32(arr), expected, `random x${size} +${offset}`) + t.assert.strictEqual( + crc32(toShared(arr, offset)), + expected, + `random x${size} +${offset} shared` + ) + t.assert.strictEqual( + lib.crc32(toShared(arr, offset)), + expected, + `random x${size} +${offset} shared` + ) + t.assert.strictEqual(crc32(Buffer.from(arr)), expected, `random x${size} +${offset} Buffer`) + } + } + }) + + test('sizes, random data, around the word-sliced threshold', (t) => { + // crc.js switches to 32-bit reads above 1024 bytes: cross it at all alignment prefixes and tail lengths + for (let size = 960; size <= 1100; size++) { + const offset = size & 7 + const arr = seed.subarray(offset, offset + size) + t.assert.strictEqual(arr.length, size) + const expected = lib.crc32(arr) + t.assert.strictEqual(crc32(arr), expected, `random x${size} +${offset}`) + t.assert.strictEqual( + crc32(toShared(arr, offset)), + expected, + `random x${size} +${offset} shared` + ) + t.assert.strictEqual( + lib.crc32(toShared(arr, offset)), + expected, + `random x${size} +${offset} shared` + ) + t.assert.strictEqual(crc32(Buffer.from(arr)), expected, `random x${size} +${offset} Buffer`) + if (size % 64 === 0) + t.assert.strictEqual(crc32reference(arr), expected, `random x${size} reference`) + } + }) + + test('large input', { skip: skipLarge }, (t) => { + const block = new Uint8Array(64 * seed.length) + for (let k = 0; k < 64; k++) + block.set( + seed.map((x, j) => x + k * j), + k * seed.length + ) + const large = new Uint8Array(64 * block.length + 7) + for (let i = 0; i < large.length; i += block.length) { + large.set(block.subarray(0, large.length - i), i) + } + + for (const offset of [0, 1, 2, 3, 4, 7]) { + for (const size of [block.length, large.length - 7]) { + const arr = large.subarray(offset, offset + size) + const expected = lib.crc32(arr) + t.assert.strictEqual(crc32(arr), expected, `large x${size} +${offset}`) + t.assert.strictEqual( + crc32(toShared(arr, offset)), + expected, + `large x${size} +${offset} shared` + ) + t.assert.strictEqual( + lib.crc32(toShared(arr, offset)), + expected, + `large x${size} +${offset} shared` + ) + } + } + + // The reference is affordable once on a 64 KiB block + t.assert.strictEqual(crc32reference(block), lib.crc32(block), 'reference x65536') + }) +})