100 lines · 2.6 KB
| 1 | /** |
| 2 | * Git pack file generator. |
| 3 | * |
| 4 | * Generates a pack file (v2) from a set of git objects. |
| 5 | * Used by git-upload-pack to serve clones and fetches. |
| 6 | * |
| 7 | * Pack format: |
| 8 | * - Header: "PACK" + version 2 (4 bytes) + object count (4 bytes) |
| 9 | * - Objects: repeated { type+size header + zlib-compressed content } |
| 10 | * - Trailer: 20-byte SHA-1 of the entire pack |
| 11 | */ |
| 12 | |
| 13 | import { type GitObjectType, stringToPackType } from './object.js'; |
| 14 | import { deflate } from './zlib.js'; |
| 15 | import { sha1, hexDecode } from './sha1.js'; |
| 16 | |
| 17 | const encoder = new TextEncoder(); |
| 18 | |
| 19 | export interface PackInput { |
| 20 | type: GitObjectType; |
| 21 | content: Uint8Array; |
| 22 | } |
| 23 | |
| 24 | /** Write a 4-byte big-endian unsigned integer. */ |
| 25 | function writeUint32BE(value: number): Uint8Array { |
| 26 | const buf = new Uint8Array(4); |
| 27 | buf[0] = (value >>> 24) & 0xff; |
| 28 | buf[1] = (value >>> 16) & 0xff; |
| 29 | buf[2] = (value >>> 8) & 0xff; |
| 30 | buf[3] = value & 0xff; |
| 31 | return buf; |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * Encode a pack object header (type + size) in variable-length format. |
| 36 | * First byte: bits 6-4 = type, bits 3-0 = size (low 4 bits). |
| 37 | * Subsequent bytes: 7 bits of size each, high bit = continuation. |
| 38 | */ |
| 39 | function encodePackObjectHeader(type: number, size: number): Uint8Array { |
| 40 | const bytes: number[] = []; |
| 41 | let byte = (type << 4) | (size & 0x0f); |
| 42 | size >>= 4; |
| 43 | |
| 44 | if (size > 0) { |
| 45 | byte |= 0x80; |
| 46 | } |
| 47 | bytes.push(byte); |
| 48 | |
| 49 | while (size > 0) { |
| 50 | byte = size & 0x7f; |
| 51 | size >>= 7; |
| 52 | if (size > 0) byte |= 0x80; |
| 53 | bytes.push(byte); |
| 54 | } |
| 55 | |
| 56 | return new Uint8Array(bytes); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Generate a pack file from a list of git objects. |
| 61 | * Returns the complete pack file as a Uint8Array. |
| 62 | */ |
| 63 | export async function generatePack(objects: PackInput[]): Promise<Uint8Array> { |
| 64 | const parts: Uint8Array[] = []; |
| 65 | |
| 66 | // Header |
| 67 | parts.push(encoder.encode('PACK')); |
| 68 | parts.push(writeUint32BE(2)); // version 2 |
| 69 | parts.push(writeUint32BE(objects.length)); |
| 70 | |
| 71 | // Objects |
| 72 | for (const obj of objects) { |
| 73 | const typeNum = stringToPackType(obj.type); |
| 74 | const header = encodePackObjectHeader(typeNum, obj.content.length); |
| 75 | const compressed = await deflate(obj.content); |
| 76 | parts.push(header); |
| 77 | parts.push(compressed); |
| 78 | } |
| 79 | |
| 80 | // Compute total size for concatenation (without trailer) |
| 81 | const totalLen = parts.reduce((sum, p) => sum + p.length, 0); |
| 82 | const pack = new Uint8Array(totalLen); |
| 83 | let offset = 0; |
| 84 | for (const part of parts) { |
| 85 | pack.set(part, offset); |
| 86 | offset += part.length; |
| 87 | } |
| 88 | |
| 89 | // Trailer: SHA-1 of everything so far |
| 90 | const checksum = await sha1(pack); |
| 91 | const checksumBytes = hexDecode(checksum); |
| 92 | |
| 93 | // Final pack = data + 20-byte checksum |
| 94 | const result = new Uint8Array(pack.length + 20); |
| 95 | result.set(pack, 0); |
| 96 | result.set(checksumBytes, pack.length); |
| 97 | |
| 98 | return result; |
| 99 | } |
| 100 |