chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* 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()),
});
});
});