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

johnandersen777.bsky.social/atlibs

0
0
main
112 lines · 4.2 KB Raw History
// The byte encodings the hosts share: base58btc (did:key), base32 (CIDv1) and DAG-CBOR (attestations,
// event-stream frames). No imports; browsers, Node and Workers.

const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const B32 = "abcdefghijklmnopqrstuvwxyz234567";
const encoder = new TextEncoder();

export function base58(bytes) {
	let n = BigInt(`0x${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") || "0"}`);
	let out = "";
	for (; n > 0n; n /= 58n) out = B58[Number(n % 58n)] + out;
	for (const byte of bytes) {
		if (byte) break;
		out = `1${out}`;
	}
	return out;
}

export function unbase58(text) {
	let n = 0n;
	for (const c of text) {
		const digit = B58.indexOf(c);
		if (digit < 0) throw new Error("bad base58");
		n = n * 58n + BigInt(digit);
	}
	const hex = n ? n.toString(16) : "";
	const bytes = (hex.length % 2 ? `0${hex}` : hex).match(/../g)?.map((h) => parseInt(h, 16)) ?? [];
	return Uint8Array.from([...Array(text.length - text.replace(/^1+/, "").length).fill(0), ...bytes]);
}

/** Lowercase RFC 4648 base32, no padding (a CIDv1's "b" multibase, without the "b"). */
export function toBase32(bytes) {
	let out = "";
	let value = 0;
	let bits = 0;
	for (const byte of bytes) {
		value = (value << 8) | byte;
		bits += 8;
		while (bits >= 5) out += B32[(value >>> (bits -= 5)) & 31];
		value &= (1 << bits) - 1;
	}
	return bits ? out + B32[(value << (5 - bits)) & 31] : out;
}

export function fromBase32(text) {
	const bytes = [];
	let value = 0;
	let bits = 0;
	for (const char of text) {
		const digit = B32.indexOf(char);
		if (digit < 0) throw new Error("bad base32");
		value = (value << 5) | digit;
		bits += 5;
		if (bits >= 8) bytes.push((value >>> (bits -= 8)) & 0xff), (value &= (1 << bits) - 1);
	}
	return new Uint8Array(bytes);
}

const fromBase64 = (text) => Uint8Array.from(atob(text.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));

// DAG-CBOR map order: keys by encoded (UTF-8) length, then bytewise.
function compareKeys(a, b) {
	if (a.length !== b.length) return a.length - b.length;
	const i = a.findIndex((byte, index) => byte !== b[index]);
	return i < 0 ? 0 : a[i] - b[i];
}

/**
 * A JSON value (atproto's data model) as DAG-CBOR: integers only, undefined map entries left out,
 * Uint8Array as bytes. links: { $bytes } and { $link } become bytes and CIDs (tag 42), as decodeCbor
 * returns them; without it they stay plain maps (attestations hash them that way).
 */
export function dagCbor(value, { links = false } = {}) {
	const out = [];
	const head = (major, n) => {
		if (n < 24) out.push((major << 5) | n);
		else if (n < 0x100) out.push((major << 5) | 24, n);
		else if (n < 0x10000) out.push((major << 5) | 25, n >> 8, n & 0xff);
		else if (n < 0x100000000) out.push((major << 5) | 26, n >>> 24, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff);
		else {
			out.push((major << 5) | 27);
			for (let shift = 56n; shift >= 0n; shift -= 8n) out.push(Number((BigInt(n) >> shift) & 0xffn));
		}
	};
	// a loop, not out.push(...b): spreading a large byte string overflows the stack
	const append = (b) => {
		for (const x of b) out.push(x);
	};
	const bytes = (b) => (head(2, b.length), append(b));
	const item = (v) => {
		if (v === null || v === undefined) return out.push(0xf6);
		if (v === false || v === true) return out.push(v ? 0xf5 : 0xf4);
		if (typeof v === "number") {
			if (!Number.isSafeInteger(v)) throw new Error("DAG-CBOR in atproto has integers only");
			return v >= 0 ? head(0, v) : head(1, -1 - v);
		}
		if (typeof v === "string") {
			const b = encoder.encode(v);
			return head(3, b.length), append(b);
		}
		if (v instanceof Uint8Array) return bytes(v);
		if (Array.isArray(v)) return head(4, v.length), v.forEach(item);
		const keys = Object.keys(v).filter((k) => v[k] !== undefined);
		if (links && keys.length === 1 && keys[0] === "$bytes") return bytes(fromBase64(v.$bytes));
		if (links && keys.length === 1 && keys[0] === "$link") return head(6, 42), bytes([0, ...fromBase32(v.$link.slice(1))]);
		const entries = keys.map((k) => [encoder.encode(k), v[k]]).sort(([a], [b]) => compareKeys(a, b));
		head(5, entries.length);
		for (const [k, x] of entries) head(3, k.length), append(k), item(x);
	};
	item(value);
	return new Uint8Array(out);
}