chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Writing a packfile and a bundle, for a caller that has objects in hand and
* no git to shell out to.
*
* The node helper takes the pack git hands it over the wire and never builds
* one. A server merging a pull request has no such pack: it holds the commit
* it just made and the objects the branch it is merging brought with it, and
* a push is a bundle plus a record write.
*
* Every object is stored whole. A delta would be smaller, and finding a good
* base is the whole cost of packing; a merge writes one commit and the trees
* under it, so the saving is not worth the code that finds it. A repack on the
* server side is what compacts a chain.
*
* Workers-clean: pako and a pure-JS SHA-1, no node imports.
*/
import pako from 'pako';
import { Sha1 } from './sha1.js';
/** Pack object type codes, in the order the format numbers them. */
const TYPE_CODES = { commit: 1, tree: 2, blob: 3, tag: 4 };
const encoder = new TextEncoder();
/**
* One object's header: its type and its inflated size, seven bits at a time.
* The first byte carries four size bits after the type, and the top bit says
* another byte follows.
* @param {number} type
* @param {number} size
* @returns {number[]}
*/
function objectHeader(type, size) {
const bytes = [(type << 4) | (size & 0x0f)];
let rest = Math.floor(size / 16);
while (rest > 0) {
bytes[bytes.length - 1] |= 0x80;
bytes.push(rest & 0x7f);
rest >>>= 7;
}
return bytes;
}
/**
* A packfile holding every object given, none of them deltified.
* @param {Array<{type: string, data: Uint8Array}>} objects
* @returns {Uint8Array}
*/
export function writePack(objects) {
/** @type {Uint8Array[]} */
const parts = [];
const head = new Uint8Array(12);
head.set(encoder.encode('PACK'), 0);
new DataView(head.buffer).setUint32(4, 2);
new DataView(head.buffer).setUint32(8, objects.length);
parts.push(head);
for (const object of objects) {
const code =
TYPE_CODES[/** @type {keyof typeof TYPE_CODES} */ (object.type)];
if (!code) throw new Error(`cannot pack a ${object.type}`);
parts.push(Uint8Array.from(objectHeader(code, object.data.length)));
parts.push(pako.deflate(object.data));
}
const body = concat(parts);
const digest = new Sha1().update(body).digest();
const pack = new Uint8Array(body.length + 20);
pack.set(body, 0);
pack.set(digest, body.length);
return pack;
}
/**
* A v2 bundle: the refs it carries, the objects it assumes the reader already
* has, and the pack.
*
* A prerequisite is what makes a bundle incremental. The reader must hold that
* object already, which for a chain means an earlier bundle carried it.
* @param {Object} bundle
* @param {Array<{sha: string, name: string}>} bundle.refs
* @param {string[]} [bundle.prerequisites]
* @param {Uint8Array} bundle.pack
* @returns {Uint8Array}
*/
export function writeBundle({ refs, prerequisites = [], pack }) {
const lines = ['# v2 git bundle'];
for (const sha of prerequisites) lines.push(`-${sha}`);
for (const ref of refs) lines.push(`${ref.sha} ${ref.name}`);
lines.push('', '');
return concat([encoder.encode(lines.join('\n')), pack]);
}
/** @param {Uint8Array[]} parts */
function concat(parts) {
const size = parts.reduce((total, part) => total + part.length, 0);
const all = new Uint8Array(size);
let at = 0;
for (const part of parts) {
all.set(part, at);
at += part.length;
}
return all;
}