Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions handwritten/storage/src/crc32c.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ const CRC32C_EXTENSIONS = [

const CRC32C_EXTENSION_TABLE = new Int32Array(CRC32C_EXTENSIONS);

/**
* Slicing-by-8 lookup tables derived from `CRC32C_EXTENSION_TABLE`.
* See {@link https://github.com/google/crc32c/blob/21fc8ef30415a635e7351ffa0e5d5367943d4a94/src/crc32c_portable.cc#L16-L386 github.com/google/crc32c}
*/
const CRC32C_SLICING_TABLES = Array.from(
{length: 8},
() => new Int32Array(256)
);
CRC32C_SLICING_TABLES[0].set(CRC32C_EXTENSION_TABLE);
for (let i = 0; i < 256; i++) {
let crc = CRC32C_SLICING_TABLES[0][i];
for (let t = 1; t < 8; t++) {
crc = CRC32C_SLICING_TABLES[0][crc & 0xff] ^ (crc >>> 8);
CRC32C_SLICING_TABLES[t][i] = crc;
}
}
const [T0, T1, T2, T3, T4, T5, T6, T7] = CRC32C_SLICING_TABLES;

/** An interface for CRC32C hashing and validation */
interface CRC32CValidator {
/**
Expand Down Expand Up @@ -158,10 +176,36 @@ class CRC32C implements CRC32CValidator {
*/
update(data: Buffer) {
let current = this.#crc32c ^ 0xffffffff;
let i = 0;
const len = data.length;
const limit = len - 8;

while (i <= limit) {
const c1 =
current ^
(data[i] |
(data[i + 1] << 8) |
(data[i + 2] << 16) |
(data[i + 3] << 24));
const c2 =
data[i + 4] |
(data[i + 5] << 8) |
(data[i + 6] << 16) |
(data[i + 7] << 24);
current =
T7[c1 & 0xff] ^
T6[(c1 >>> 8) & 0xff] ^
T5[(c1 >>> 16) & 0xff] ^
T4[c1 >>> 24] ^
T3[c2 & 0xff] ^
T2[(c2 >>> 8) & 0xff] ^
T1[(c2 >>> 16) & 0xff] ^
T0[c2 >>> 24];
i += 8;
}

for (const d of data) {
const tablePoly = CRC32C.CRC32C_EXTENSION_TABLE[(d ^ current) & 0xff];
current = tablePoly ^ (current >>> 8);
while (i < len) {
current = T0[(current ^ data[i++]) & 0xff] ^ (current >>> 8);
}

this.#crc32c = current ^ 0xffffffff;
Expand Down
80 changes: 80 additions & 0 deletions handwritten/storage/test/crc32c.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,86 @@ describe('CRC32C', () => {

assert.equal(buffer.toString(), value);
});

it('should match RFC 3720 test vectors', () => {
const vectors: Array<[Buffer, number]> = [
[Buffer.from('123456789'), 0xe3069283],
[Buffer.alloc(32, 0x00), 0x8a9136aa],
[Buffer.alloc(32, 0xff), 0x62a8ab43],
[Buffer.from(Uint8Array.from({length: 32}, (_, i) => i)), 0x46dd794e],
[
Buffer.from(Uint8Array.from({length: 32}, (_, i) => 31 - i)),
0x113fdb5c,
],
];

for (const [input, expected] of vectors) {
const crc32c = new CRC32C();
crc32c.update(input);
assert.equal(crc32c.valueOf() >>> 0, expected);
}
});

it('should handle high-bit bytes (0x80-0xff)', () => {
const buf = Buffer.from(Uint8Array.from({length: 256}, (_, i) => i));

const actual = new CRC32C();
actual.update(buf);

const expected = new CRC32C();
for (let i = 0; i < buf.length; i++) {
expected.update(buf.subarray(i, i + 1));
}

assert.equal(actual.toString(), expected.toString());
});

it('should handle all remainder lengths modulo 8', () => {
const buf = Buffer.from(Uint8Array.from({length: 32}, (_, i) => i));

for (let len = 0; len <= 31; len++) {
const actual = new CRC32C();
actual.update(buf.subarray(0, len));

const expected = new CRC32C();
for (let i = 0; i < len; i++) {
expected.update(buf.subarray(i, i + 1));
}

assert.equal(actual.toString(), expected.toString());
}
});

it('should handle multi-byte chunk boundaries', () => {
const buf = Buffer.from(Uint8Array.from({length: 256}, (_, i) => i));
const expected = new CRC32C();
expected.update(buf);

for (const size of [3, 7, 8, 9, 15, 16, 17, 64]) {
const actual = new CRC32C();
for (let i = 0; i < buf.length; i += size) {
actual.update(buf.subarray(i, i + size));
}

assert.equal(actual.toString(), expected.toString());
}
});

it('should handle unaligned Buffer.subarray views', () => {
const base = Buffer.from(Uint8Array.from({length: 64}, (_, i) => i));

for (let offset = 1; offset <= 7; offset++) {
const view = base.subarray(offset, offset + 27);

const actual = new CRC32C();
actual.update(view);

const expected = new CRC32C();
expected.update(Buffer.from(view));

assert.equal(actual.toString(), expected.toString());
}
});
});

describe('#validate', () => {
Expand Down
Loading