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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
221 lines · 7.1 KB Raw History
/**
 * Wire-format primitives behind the smart HTTP layer, checked against real
 * git: the SHA-1 against node:crypto, and pack merging against git's own
 * index-pack, which is the consumer the merged bytes must satisfy.
 */

import { execFile, execFileSync } from 'node:child_process';
import { createHash, randomBytes } from 'node:crypto';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { parseBundle } from '../src/bundle.js';
import {
  emptyPack,
  mergePackStream,
  mergePacks,
  splitPack,
} from '../src/pack.js';
import { concatBytes, FLUSH, parsePkts, pktLine, pktText } from '../src/pkt.js';
import { Sha1 } from '../src/sha1.js';

const execFileAsync = promisify(execFile);

describe('sha1', () => {
  it('matches node:crypto across block boundaries', () => {
    for (const size of [0, 1, 55, 56, 57, 63, 64, 65, 127, 1000, 100_000]) {
      const data = randomBytes(size);
      const expected = createHash('sha1').update(data).digest('hex');
      const actual = Buffer.from(new Sha1().update(data).digest()).toString(
        'hex',
      );
      expect(actual, `size ${size}`).toBe(expected);
    }
  });

  it('is incremental', () => {
    const data = randomBytes(10_000);
    const hash = new Sha1();
    for (let i = 0; i < data.length; i += 7) {
      hash.update(data.subarray(i, i + 7));
    }
    expect(Buffer.from(hash.digest()).toString('hex')).toBe(
      createHash('sha1').update(data).digest('hex'),
    );
  });
});

describe('pkt-line', () => {
  it('round-trips lines and flushes', () => {
    const stream = concatBytes([
      pktLine('want abc\n'),
      FLUSH,
      pktLine('done\n'),
      pktLine(new Uint8Array([1, 2, 3])),
    ]);
    const pkts = parsePkts(stream);
    expect(pkts).toHaveLength(4);
    expect(pkts[0].type).toBe('line');
    expect(pktText(/** @type {{data: Uint8Array}} */ (pkts[0]).data)).toBe(
      'want abc',
    );
    expect(pkts[1].type).toBe('flush');
    expect(pktText(/** @type {{data: Uint8Array}} */ (pkts[2]).data)).toBe(
      'done',
    );
    expect(/** @type {{data: Uint8Array}} */ (pkts[3]).data).toEqual(
      new Uint8Array([1, 2, 3]),
    );
  });

  it('rejects truncated frames', () => {
    expect(() => parsePkts(pktLine('hello').subarray(0, 6))).toThrow();
  });
});

describe('pack merging against real git', () => {
  /** @type {string} */
  let dir = '';
  const env = {
    .../** @type {Record<string, string>} */ (process.env),
    GIT_AUTHOR_NAME: 'Test',
    GIT_AUTHOR_EMAIL: 'test@example.com',
    GIT_COMMITTER_NAME: 'Test',
    GIT_COMMITTER_EMAIL: 'test@example.com',
  };

  /**
   * @param {string} cwd
   * @param {string[]} args
   */
  const runGit = async (cwd, args) => {
    const { stdout } = await execFileAsync('git', args, { cwd, env });
    return stdout.trim();
  };

  beforeAll(() => {
    dir = mkdtempSync(join(tmpdir(), 'pdsjs-git-wire-'));
  });

  afterAll(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  it('merges chained bundle packs into one git accepts', async () => {
    const repo = join(dir, 'repo');
    await runGit(dir, ['init', '-b', 'main', repo]);
    writeFileSync(join(repo, 'a.txt'), 'first\n');
    writeFileSync(join(repo, 'blob.bin'), randomBytes(2048));
    await runGit(repo, ['add', '.']);
    await runGit(repo, ['commit', '-m', 'one']);
    const shaOne = await runGit(repo, ['rev-parse', 'HEAD']);
    await runGit(repo, ['bundle', 'create', join(dir, 'b1.bundle'), 'main']);

    writeFileSync(join(repo, 'b.txt'), 'second\n');
    await runGit(repo, ['add', '.']);
    await runGit(repo, ['commit', '-m', 'two']);
    const shaTwo = await runGit(repo, ['rev-parse', 'HEAD']);
    await runGit(repo, [
      'bundle',
      'create',
      join(dir, 'b2.bundle'),
      `^${shaOne}`,
      'main',
    ]);

    const bundleOne = parseBundle(readFileSync(join(dir, 'b1.bundle')));
    const bundleTwo = parseBundle(readFileSync(join(dir, 'b2.bundle')));
    expect(bundleOne.refs[0].name).toBe('refs/heads/main');
    expect(bundleTwo.prerequisites).toContain(shaOne);

    const merged = mergePacks([bundleOne.pack, bundleTwo.pack]);
    expect(splitPack(merged).count).toBe(
      splitPack(bundleOne.pack).count + splitPack(bundleTwo.pack).count,
    );

    // The proof: a fresh repository indexes the merged pack and finds both
    // commits fully connected.
    const target = join(dir, 'target');
    await runGit(dir, ['init', '-b', 'main', target]);
    execFileSync('git', ['index-pack', '--stdin'], {
      cwd: target,
      env,
      input: Buffer.from(merged),
      maxBuffer: 64 * 1024 * 1024,
    });
    await runGit(target, ['cat-file', '-e', `${shaOne}^{commit}`]);
    await runGit(target, ['cat-file', '-e', `${shaTwo}^{commit}`]);
    // Full connectivity from the tip, not just object presence
    await runGit(target, ['rev-list', '--objects', shaTwo]);
  });

  it('streams the same bytes it buffers, holding one read at a time', async () => {
    const packs = [
      parseBundle(readFileSync(join(dir, 'b1.bundle'))).pack,
      parseBundle(readFileSync(join(dir, 'b2.bundle'))).pack,
    ];
    const READ = 512;
    let largestRead = 0;
    const sources = packs.map((pack) => ({
      count: splitPack(pack).count,
      length: pack.length,
      /**
       * @param {number} start
       * @param {number} end
       */
      read: async function* (start, end) {
        for (let offset = start; offset < end; offset += READ) {
          const slice = pack.subarray(offset, Math.min(end, offset + READ));
          largestRead = Math.max(largestRead, slice.length);
          yield slice;
        }
      },
    }));

    /** @type {Uint8Array[]} */
    const chunks = [];
    for await (const chunk of mergePackStream(sources)) chunks.push(chunk);
    expect(concatBytes(chunks)).toEqual(mergePacks(packs));
    expect(largestRead).toBe(READ);
  });

  it('streams a lone pack through unchanged', async () => {
    const pack = parseBundle(readFileSync(join(dir, 'b1.bundle'))).pack;
    /** @type {Uint8Array[]} */
    const chunks = [];
    const stream = mergePackStream([
      {
        count: splitPack(pack).count,
        length: pack.length,
        /**
         * @param {number} start
         * @param {number} end
         */
        read: async function* (start, end) {
          yield pack.subarray(start, end);
        },
      },
    ]);
    for await (const chunk of stream) chunks.push(chunk);
    expect(concatBytes(chunks)).toEqual(new Uint8Array(pack));
  });

  it('streams an empty pack for an empty source list', async () => {
    /** @type {Uint8Array[]} */
    const chunks = [];
    for await (const chunk of mergePackStream([])) chunks.push(chunk);
    expect(concatBytes(chunks)).toEqual(emptyPack());
  });

  it('produces an empty pack git accepts', async () => {
    const target = join(dir, 'empty-target');
    await runGit(dir, ['init', '-b', 'main', target]);
    execFileSync('git', ['index-pack', '--stdin'], {
      cwd: target,
      env,
      input: Buffer.from(emptyPack()),
    });
  });
});