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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
561 lines · 16.4 KB Raw History
import {
  buildCarFile,
  cborDecode,
  cborEncode,
  cborEncodeDagCbor,
  cidToString,
  createBlobCid,
  createCid,
  createTid,
  findBlobRefs,
  recordTimeOf,
  sniffMimeType,
  varint,
} from '@pdsjs/core/repo';
import { describe, expect, test } from 'vitest';

describe('CBOR Encoding', () => {
  test('encodes simple map', () => {
    const encoded = cborEncode({ hello: 'world', num: 42 });
    // Expected: a2 65 68 65 6c 6c 6f 65 77 6f 72 6c 64 63 6e 75 6d 18 2a
    const expected = new Uint8Array([
      0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c,
      0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a,
    ]);
    expect(encoded).toEqual(expected);
  });

  test('encodes null', () => {
    const encoded = cborEncode(null);
    expect(encoded).toEqual(new Uint8Array([0xf6]));
  });

  test('encodes byte strings past the engine argument limit', () => {
    // A commit event carries a whole CAR file as one byte string. Spreading
    // it into push() throws RangeError past about 125k arguments, so a large
    // record used to crash createCommit.
    const big = new Uint8Array(300_000).fill(7);
    const encoded = cborEncode({ blocks: big });
    const decoded = /** @type {{blocks: Uint8Array}} */ (cborDecode(encoded));
    expect(decoded.blocks.length).toBe(300_000);
    expect(decoded.blocks[299_999]).toBe(7);
  });

  test('encodes booleans', () => {
    expect(cborEncode(true)).toEqual(new Uint8Array([0xf5]));
    expect(cborEncode(false)).toEqual(new Uint8Array([0xf4]));
  });

  test('encodes small integers', () => {
    expect(cborEncode(0)).toEqual(new Uint8Array([0x00]));
    expect(cborEncode(1)).toEqual(new Uint8Array([0x01]));
    expect(cborEncode(23)).toEqual(new Uint8Array([0x17]));
  });

  test('encodes integers >= 24', () => {
    expect(cborEncode(24)).toEqual(new Uint8Array([0x18, 0x18]));
    expect(cborEncode(255)).toEqual(new Uint8Array([0x18, 0xff]));
  });

  test('encodes negative integers', () => {
    expect(cborEncode(-1)).toEqual(new Uint8Array([0x20]));
    expect(cborEncode(-10)).toEqual(new Uint8Array([0x29]));
  });

  test('encodes strings', () => {
    const encoded = cborEncode('hello');
    // 0x65 = text string of length 5
    expect(encoded).toEqual(
      new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]),
    );
  });

  test('encodes byte strings', () => {
    const bytes = new Uint8Array([1, 2, 3]);
    const encoded = cborEncode(bytes);
    // 0x43 = byte string of length 3
    expect(encoded).toEqual(new Uint8Array([0x43, 1, 2, 3]));
  });

  test('encodes arrays', () => {
    const encoded = cborEncode([1, 2, 3]);
    // 0x83 = array of length 3
    expect(encoded).toEqual(new Uint8Array([0x83, 0x01, 0x02, 0x03]));
  });

  test('sorts map keys deterministically', () => {
    const encoded1 = cborEncode({ z: 1, a: 2 });
    const encoded2 = cborEncode({ a: 2, z: 1 });
    expect(encoded1).toEqual(encoded2);
    // First key should be 'a' (0x61)
    expect(encoded1[1]).toBe(0x61);
  });

  test('encodes large integers >= 2^31 without overflow', () => {
    // 2^31 would overflow with bitshift operators (treated as signed 32-bit)
    const twoTo31 = 2147483648;
    const encoded = cborEncode(twoTo31);
    const decoded = cborDecode(encoded);
    expect(decoded).toBe(twoTo31);

    // 2^32 - 1 (max unsigned 32-bit)
    const maxU32 = 4294967295;
    const encoded2 = cborEncode(maxU32);
    const decoded2 = cborDecode(encoded2);
    expect(decoded2).toBe(maxU32);
  });

  test('encodes 2^31 with correct byte format', () => {
    // 2147483648 = 0x80000000
    // CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows)
    const encoded = cborEncode(2147483648);
    expect(encoded[0]).toBe(0x1a); // type 0 | info 26
    expect(encoded[1]).toBe(0x80);
    expect(encoded[2]).toBe(0x00);
    expect(encoded[3]).toBe(0x00);
    expect(encoded[4]).toBe(0x00);
  });
});

describe('CBOR Decoding', () => {
  test('decodes what encode produces (roundtrip)', () => {
    const original = { hello: 'world', num: 42 };
    const encoded = cborEncode(original);
    const decoded = cborDecode(encoded);
    expect(decoded).toEqual(original);
  });

  test('decodes null', () => {
    const encoded = cborEncode(null);
    const decoded = cborDecode(encoded);
    expect(decoded).toBe(null);
  });

  test('decodes booleans', () => {
    expect(cborDecode(cborEncode(true))).toBe(true);
    expect(cborDecode(cborEncode(false))).toBe(false);
  });

  test('decodes integers', () => {
    expect(cborDecode(cborEncode(0))).toBe(0);
    expect(cborDecode(cborEncode(42))).toBe(42);
    expect(cborDecode(cborEncode(255))).toBe(255);
    expect(cborDecode(cborEncode(-1))).toBe(-1);
    expect(cborDecode(cborEncode(-10))).toBe(-10);
  });

  test('decodes strings', () => {
    expect(cborDecode(cborEncode('hello'))).toBe('hello');
    expect(cborDecode(cborEncode(''))).toBe('');
  });

  test('decodes arrays', () => {
    expect(cborDecode(cborEncode([1, 2, 3]))).toEqual([1, 2, 3]);
    expect(cborDecode(cborEncode([]))).toEqual([]);
  });

  test('decodes nested structures', () => {
    const original = { arr: [1, { nested: true }], str: 'test' };
    const decoded = cborDecode(cborEncode(original));
    expect(decoded).toEqual(original);
  });
});

describe('CID Generation', () => {
  test('createCid uses dag-cbor codec', async () => {
    const data = cborEncode({ test: 'data' });
    const cid = await createCid(data);

    expect(cid.length).toBe(36); // 2 prefix + 2 multihash header + 32 hash
    expect(cid[0]).toBe(0x01); // CIDv1
    expect(cid[1]).toBe(0x71); // dag-cbor
    expect(cid[2]).toBe(0x12); // sha-256
    expect(cid[3]).toBe(0x20); // 32 bytes
  });

  test('createBlobCid uses raw codec', async () => {
    const data = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic bytes
    const cid = await createBlobCid(data);

    expect(cid.length).toBe(36);
    expect(cid[0]).toBe(0x01); // CIDv1
    expect(cid[1]).toBe(0x55); // raw codec
    expect(cid[2]).toBe(0x12); // sha-256
    expect(cid[3]).toBe(0x20); // 32 bytes
  });

  test('same bytes produce different CIDs with different codecs', async () => {
    const data = new Uint8Array([1, 2, 3, 4]);
    const dagCborCid = cidToString(await createCid(data));
    const rawCid = cidToString(await createBlobCid(data));

    expect(dagCborCid).not.toBe(rawCid);
  });

  test('cidToString returns base32lower with b prefix', async () => {
    const data = cborEncode({ test: 'data' });
    const cid = await createCid(data);
    const cidStr = cidToString(cid);

    expect(cidStr[0]).toBe('b');
    expect(cidStr).toMatch(/^b[a-z2-7]+$/);
  });

  test('same input produces same CID', async () => {
    const data1 = cborEncode({ test: 'data' });
    const data2 = cborEncode({ test: 'data' });
    const cid1 = cidToString(await createCid(data1));
    const cid2 = cidToString(await createCid(data2));

    expect(cid1).toBe(cid2);
  });

  test('different input produces different CID', async () => {
    const cid1 = cidToString(await createCid(cborEncode({ a: 1 })));
    const cid2 = cidToString(await createCid(cborEncode({ a: 2 })));

    expect(cid1).not.toBe(cid2);
  });
});

describe('TID Generation', () => {
  test('creates 13-character TIDs', () => {
    const tid = createTid();
    expect(tid.length).toBe(13);
  });

  test('uses valid base32-sort characters', () => {
    const tid = createTid();
    expect(tid).toMatch(/^[234567abcdefghijklmnopqrstuvwxyz]+$/);
  });

  test('generates monotonically increasing TIDs', () => {
    const tid1 = createTid();
    const tid2 = createTid();
    const tid3 = createTid();

    expect(tid1 < tid2).toBe(true);
    expect(tid2 < tid3).toBe(true);
  });

  test('generates unique TIDs', () => {
    const tids = new Set();
    for (let i = 0; i < 100; i++) {
      tids.add(createTid());
    }
    expect(tids.size).toBe(100);
  });
});

describe('CAR File Builder', () => {
  test('varint encodes small numbers', () => {
    expect(varint(0)).toEqual(new Uint8Array([0]));
    expect(varint(1)).toEqual(new Uint8Array([1]));
    expect(varint(127)).toEqual(new Uint8Array([127]));
  });

  test('varint encodes multi-byte numbers', () => {
    // 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01]
    expect(varint(128)).toEqual(new Uint8Array([0x80, 0x01]));
    // 300 = 0x12c -> [0xac, 0x02]
    expect(varint(300)).toEqual(new Uint8Array([0xac, 0x02]));
  });

  test('buildCarFile produces valid structure', async () => {
    const data = cborEncode({ test: 'data' });
    const cid = await createCid(data);
    const cidStr = cidToString(cid);

    const car = buildCarFile(cidStr, [{ cid: cidStr, data }]);

    expect(car instanceof Uint8Array).toBe(true);
    expect(car.length > 0).toBe(true);
    // First byte should be varint of header length
    expect(car[0] > 0).toBe(true);
  });
});

describe('MIME Type Sniffing', () => {
  test('detects JPEG', () => {
    const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]);
    expect(sniffMimeType(bytes)).toBe('image/jpeg');
  });

  test('detects PNG', () => {
    const bytes = new Uint8Array([
      0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
    ]);
    expect(sniffMimeType(bytes)).toBe('image/png');
  });

  test('detects GIF', () => {
    const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
    expect(sniffMimeType(bytes)).toBe('image/gif');
  });

  test('detects WebP', () => {
    const bytes = new Uint8Array([
      0x52,
      0x49,
      0x46,
      0x46, // RIFF
      0x00,
      0x00,
      0x00,
      0x00, // size (ignored)
      0x57,
      0x45,
      0x42,
      0x50, // WEBP
    ]);
    expect(sniffMimeType(bytes)).toBe('image/webp');
  });

  test('detects MP4', () => {
    const bytes = new Uint8Array([
      0x00,
      0x00,
      0x00,
      0x18, // size
      0x66,
      0x74,
      0x79,
      0x70, // ftyp
      0x69,
      0x73,
      0x6f,
      0x6d, // isom brand
    ]);
    expect(sniffMimeType(bytes)).toBe('video/mp4');
  });

  test('detects AVIF', () => {
    const bytes = new Uint8Array([
      0x00,
      0x00,
      0x00,
      0x1c, // size
      0x66,
      0x74,
      0x79,
      0x70, // ftyp
      0x61,
      0x76,
      0x69,
      0x66, // avif brand
    ]);
    expect(sniffMimeType(bytes)).toBe('image/avif');
  });

  test('detects HEIC', () => {
    const bytes = new Uint8Array([
      0x00,
      0x00,
      0x00,
      0x18, // size
      0x66,
      0x74,
      0x79,
      0x70, // ftyp
      0x68,
      0x65,
      0x69,
      0x63, // heic brand
    ]);
    expect(sniffMimeType(bytes)).toBe('image/heic');
  });

  test('returns null for unknown', () => {
    const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
    expect(sniffMimeType(bytes)).toBe(null);
  });
});

describe('Blob Ref Detection', () => {
  test('finds blob ref in simple object', () => {
    const record = {
      $type: 'app.bsky.feed.post',
      text: 'Hello',
      embed: {
        $type: 'app.bsky.embed.images',
        images: [
          {
            image: {
              $type: 'blob',
              ref: { $link: 'bafkreiabc123' },
              mimeType: 'image/jpeg',
              size: 1234,
            },
            alt: 'test image',
          },
        ],
      },
    };
    const refs = findBlobRefs(record);
    expect(refs).toEqual(['bafkreiabc123']);
  });

  test('finds a blob ref decoded from a CAR (raw CID bytes)', async () => {
    // An imported record carries its blob ref as the raw CID bytes cborDecode
    // hands back for a tag-42 link, not `{$link}`. Both must be detected, or an
    // imported account's blobs never transfer.
    const cidBytes = await createBlobCid(new Uint8Array([1, 2, 3]));
    const record = {
      $type: 'app.bsky.actor.profile',
      avatar: {
        $type: 'blob',
        ref: cidBytes,
        mimeType: 'image/jpeg',
        size: 3,
      },
    };
    expect(findBlobRefs(record)).toEqual([cidToString(cidBytes)]);
  });

  test('finds multiple blob refs', () => {
    const record = {
      images: [
        {
          image: {
            $type: 'blob',
            ref: { $link: 'cid1' },
            mimeType: 'image/png',
            size: 100,
          },
        },
        {
          image: {
            $type: 'blob',
            ref: { $link: 'cid2' },
            mimeType: 'image/png',
            size: 200,
          },
        },
      ],
    };
    const refs = findBlobRefs(record);
    expect(refs).toEqual(['cid1', 'cid2']);
  });

  test('returns empty array when no blobs', () => {
    const record = { text: 'Hello world', count: 42 };
    const refs = findBlobRefs(record);
    expect(refs).toEqual([]);
  });

  test('handles null and primitives', () => {
    expect(findBlobRefs(null)).toEqual([]);
    expect(findBlobRefs('string')).toEqual([]);
    expect(findBlobRefs(42)).toEqual([]);
  });

  test('finds legacy blob refs (pre-$type format)', () => {
    const record = {
      embed: {
        image: { cid: 'bafkreilegacyblob', mimeType: 'image/jpeg' },
      },
    };
    expect(findBlobRefs(record)).toEqual(['bafkreilegacyblob']);
  });

  test('does not treat objects with extra keys as legacy blob refs', () => {
    const record = {
      subject: {
        cid: 'bafyreirecordcid',
        mimeType: 'image/jpeg',
        uri: 'at://x/y/z',
      },
      strongRef: { cid: 'bafyreirecordcid', uri: 'at://x/y/z' },
    };
    expect(findBlobRefs(record)).toEqual([]);
  });
});

describe('Record Time Detection', () => {
  test('reads the conventional createdAt', () => {
    expect(recordTimeOf({ createdAt: '2026-05-07T14:32:34.621Z' })).toBe(
      Date.parse('2026-05-07T14:32:34.621Z'),
    );
  });

  test('accepts timestamp fields under other names', () => {
    expect(recordTimeOf({ playedTime: '2025-03-01T10:00:00Z' })).toBe(
      Date.parse('2025-03-01T10:00:00Z'),
    );
    expect(recordTimeOf({ publishDate: '2024-06-05T00:00:00Z' })).toBe(
      Date.parse('2024-06-05T00:00:00Z'),
    );
  });

  test('the earliest moment wins, so updates never mask creation', () => {
    expect(
      recordTimeOf({
        createdAt: '2026-05-07T14:32:34Z',
        updatedAt: '2026-05-07T18:44:29Z',
      }),
    ).toBe(Date.parse('2026-05-07T14:32:34Z'));
  });

  test('ignores moments outside the plausible window', () => {
    // A birthday must not date an avatar; a scheduled future event must not
    // pin its poster to that day.
    expect(recordTimeOf({ birthDate: '1985-03-12T00:00:00Z' })).toBe(null);
    expect(recordTimeOf({ startsAt: '2999-01-01T00:00:00Z' })).toBe(null);
    expect(
      recordTimeOf({
        birthDate: '1985-03-12T00:00:00Z',
        createdAt: '2026-05-07T14:32:34Z',
      }),
    ).toBe(Date.parse('2026-05-07T14:32:34Z'));
  });

  test('ignores non-time fields and malformed values', () => {
    expect(recordTimeOf({ format: 'flat', text: 'hello' })).toBe(null);
    expect(recordTimeOf({ createdAt: 'yesterday' })).toBe(null);
    expect(recordTimeOf({ createdAt: 1746628354621 })).toBe(null);
    expect(recordTimeOf(null)).toBe(null);
    expect(recordTimeOf(['2026-05-07T14:32:34Z'])).toBe(null);
  });
});

describe('CBOR integers past 32 bits', () => {
  // knotless starts a ref's generation at the time in milliseconds.
  const ref = {
    $type: 'dev.knotless.ref',
    name: 'refs/heads/main',
    generation: 1793736300000,
  };

  test('encode with an 8-byte head and read back', () => {
    for (const encode of [cborEncode, cborEncodeDagCbor]) {
      expect(encode(1793736300000)).toEqual(
        new Uint8Array([0x1b, 0x00, 0x00, 0x01, 0xa1, 0xa3, 0x03, 0xd5, 0xe0]),
      );
      expect(encode(2 ** 32)).toEqual(
        new Uint8Array([0x1b, 0, 0, 0, 1, 0, 0, 0, 0]),
      );
      expect(cborDecode(encode(-(2 ** 40)))).toBe(-(2 ** 40));
      expect(cborDecode(encode(Number.MAX_SAFE_INTEGER))).toBe(
        Number.MAX_SAFE_INTEGER,
      );
    }
    expect(cborDecode(cborEncodeDagCbor(ref))).toEqual(ref);
  });

  test('a value an encoder cannot write is refused, not left out', () => {
    for (const encode of [cborEncode, cborEncodeDagCbor]) {
      for (const bad of [1.5, Number.NaN, 2 ** 53, 10n, [undefined]]) {
        expect(() => encode({ a: bad })).toThrow(/CBOR cannot encode/);
      }
    }
  });

  test('a record stored cut short by an earlier encoder still reads', () => {
    // Before 8-byte heads, the encoders wrote nothing for such an integer.
    // A read gives 0 for the missing value. Refusing the read made every
    // listing of a collection holding one fail.
    const whole = cborEncodeDagCbor({ ...ref, generation: 0 });
    const stored = whole.subarray(0, whole.length - 1);
    expect(cborDecode(stored)).toEqual({ ...ref, generation: 0 });
  });
});