import { Buffer } from 'node:buffer'; import { deflateSync } from 'node:zlib'; import { sha1 } from '@noble/hashes/legacy.js'; import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils.js'; import { fail } from './types'; export interface GitObject { type: 'commit' | 'tree' | 'blob' | 'tag'; data: Uint8Array; oid: string; } export function gitObject( type: GitObject['type'], data: Uint8Array, ): GitObject { if (data.byteLength > 16 * 1024 * 1024) fail(413, 'ObjectTooLarge', 'Git object exceeds operation limit'); return { type, data, oid: bytesToHex( sha1( concatBytes( new TextEncoder().encode(`${type} ${data.byteLength}\0`), data, ), ), ), }; } export function gitTree( entries: { name: string; mode: string; oid: string }[], ): GitObject { const names = new Set(); for (const entry of entries) { if ( !entry.name || /[\/\0]/.test(entry.name) || names.has(entry.name) || !['100644', '100755', '120000', '160000', '40000'].includes(entry.mode) || !/^[a-f0-9]{40}$/.test(entry.oid) ) fail(400, 'InvalidTree', 'invalid Git tree entry'); names.add(entry.name); } const sorted = [...entries].sort((a, b) => Buffer.compare( Buffer.from(a.name + (a.mode === '40000' ? '/' : '')), Buffer.from(b.name + (b.mode === '40000' ? '/' : '')), ), ); return gitObject( 'tree', concatBytes( ...sorted.map((e) => concatBytes( new TextEncoder().encode(`${e.mode} ${e.name}\0`), hexToBytes(e.oid), ), ), ), ); } /** Small full-object pack writer; no deltas or unbounded materialization. */ export function gitPack( objects: GitObject[], maxBytes = 16 * 1024 * 1024, ): Uint8Array { if (objects.length > 2000) fail(413, 'OperationTooLarge', 'too many objects in pack'); const parts: Uint8Array[] = []; const header = Buffer.alloc(12); header.write('PACK'); header.writeUInt32BE(2, 4); header.writeUInt32BE(objects.length, 8); parts.push(header); let total = 12; const codes = { commit: 1, tree: 2, blob: 3, tag: 4 }; for (const object of objects) { if (gitObject(object.type, object.data).oid !== object.oid) fail(400, 'InvalidObject', 'Git object hash mismatch'); let size = object.data.length; const bytes: number[] = [(codes[object.type] << 4) | (size & 15)]; size >>>= 4; while (size) { bytes[bytes.length - 1] |= 128; bytes.push(size & 127); size >>>= 7; } const data = deflateSync(object.data); total += bytes.length + data.length; if (total > maxBytes) fail(413, 'OperationTooLarge', 'encoded pack exceeds operation limit'); parts.push(new Uint8Array(bytes), data); } const body = concatBytes(...parts); return concatBytes(body, sha1(body)); }