~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
86 lines · 2.7 KB Raw History
// The incremental hasher against WebCrypto, across block boundaries and
// chunkings. 55, 56, 63, 64 and 65 bytes surround the two padding cases: a
// length that fits after the 0x80 marker and one that forces an extra block.
import { describe, expect, it } from 'vitest';
import { blobCidFromDigest, cidToString, createBlobCid } from '../src/repo.js';
import { createSha256 } from '../src/sha256.js';

/**
 * @param {number} size
 * @returns {Uint8Array}
 */
function bytes(size) {
  const out = new Uint8Array(size);
  for (let i = 0; i < size; i += 1) out[i] = (i * 31 + 7) % 256;
  return out;
}

/**
 * @param {Uint8Array} data
 * @returns {Promise<Uint8Array>}
 */
async function reference(data) {
  return new Uint8Array(
    await crypto.subtle.digest('SHA-256', /** @type {BufferSource} */ (data)),
  );
}

describe('createSha256', () => {
  const sizes = [0, 1, 3, 55, 56, 57, 63, 64, 65, 127, 128, 1000, 70000];

  for (const size of sizes) {
    it(`matches WebCrypto for ${size} bytes in one update`, async () => {
      const data = bytes(size);
      const hasher = createSha256();
      hasher.update(data);
      expect(await hasher.digest()).toEqual(await reference(data));
    });
  }

  it('matches WebCrypto when fed a byte at a time', async () => {
    const data = bytes(200);
    const hasher = createSha256();
    for (const b of data) hasher.update(new Uint8Array([b]));
    expect(await hasher.digest()).toEqual(await reference(data));
  });

  it('matches WebCrypto across uneven chunk splits', async () => {
    const data = bytes(10_000);
    for (const split of [1, 7, 63, 64, 65, 4096]) {
      const hasher = createSha256();
      for (let at = 0; at < data.length; at += split) {
        hasher.update(data.subarray(at, at + split));
      }
      expect(await hasher.digest()).toEqual(await reference(data));
    }
  });

  it('hashes nothing at all', async () => {
    const hasher = createSha256();
    expect(await hasher.digest()).toEqual(await reference(new Uint8Array(0)));
  });

  it('answers the same digest twice', async () => {
    const hasher = createSha256();
    hasher.update(bytes(100));
    const first = await hasher.digest();
    expect(await hasher.digest()).toBe(first);
  });

  it('refuses an update after the digest', async () => {
    const hasher = createSha256();
    await hasher.digest();
    expect(() => hasher.update(bytes(1))).toThrow(/final/);
  });
});

describe('blobCidFromDigest', () => {
  it('builds the same CID the buffered path builds', async () => {
    const data = bytes(500);
    const hasher = createSha256();
    hasher.update(data);
    const streamed = cidToString(blobCidFromDigest(await hasher.digest()));
    const buffered = cidToString(await createBlobCid(data));
    expect(streamed).toBe(buffered);
  });
});