Skip to content
File

Blob: src/worker/git/pack/indexer/writeIdx.ts

typescript153 lines
1/**
2 * Binary writer for Git pack-index v2.
3 *
4 * Produces a standard `.idx` file that can be verified byte-for-byte against
5 * `git index-pack` output. The format is fully deterministic given the same
6 * pack contents.
7 *
8 * Layout (all multi-byte integers are big-endian):
9 * [4] magic 0xff744f63
10 * [4] version 0x00000002
11 * [1024] fanout 256 x uint32 cumulative OID counts
12 * [N*20] oid table sorted raw 20-byte SHA-1 hashes
13 * [N*4] crc32 table per-object CRC-32 in OID-sorted order
14 * [N*4] offset table uint32, MSB set -> index into 64-bit table
15 * [var] 64-bit table 8-byte entries for offsets >= 0x80000000
16 * [20] pack SHA-1 trailing checksum copied from the pack
17 * [20] idx SHA-1 SHA-1 of everything above
18 */
19 
20import type { PackEntryTable } from "./types";
21import { asBufferSource } from "@/worker/common/webtypes";
22 
23/** Compare two raw 20-byte OIDs located at positions a and b in a flat buffer. */
24function compareOids(oids: Uint8Array, a: number, b: number): number {
25 for (let i = 0; i < 20; i++) {
26 const diff = oids[a + i] - oids[b + i];
27 if (diff !== 0) return diff;
28 }
29 return 0;
30}
31 
32export function buildOidSortedEntryIndices(
33 table: PackEntryTable,
34 objectCount: number
35): Uint32Array {
36 const sortedIndices = new Uint32Array(objectCount);
37 for (let i = 0; i < objectCount; i++) sortedIndices[i] = i;
38 // Duplicate OIDs are invalid in normal packs but can appear in defensive
39 // tests and malformed inputs. Tie-break by pack entry index so every derived
40 // artifact writes duplicate rows in the same deterministic order.
41 sortedIndices.sort((a, b) => compareOids(table.oids, a * 20, b * 20) || a - b);
42 return sortedIndices;
43}
44 
45export async function writeIdxV2(
46 table: PackEntryTable,
47 objectCount: number,
48 packChecksum: Uint8Array
49): Promise<Uint8Array> {
50 const N = objectCount;
51 
52 // 1. Build sorted index by OID (raw 20-byte comparison).
53 const sortedIndices = buildOidSortedEntryIndices(table, N);
54 
55 // 2. Count large offsets (>= 0x80000000).
56 let largeCount = 0;
57 for (let i = 0; i < N; i++) {
58 if (table.offsets[i] >= 0x80000000) largeCount++;
59 }
60 
61 // 3. Calculate total size.
62 const totalSize =
63 4 + // magic
64 4 + // version
65 256 * 4 + // fanout
66 N * 20 + // OID table
67 N * 4 + // CRC-32 table
68 N * 4 + // 32-bit offset table
69 largeCount * 8 + // 64-bit offset table
70 20 + // pack checksum
71 20; // idx checksum
72 
73 const buf = new Uint8Array(totalSize);
74 const dv = new DataView(buf.buffer);
75 let pos = 0;
76 
77 // 4. Magic + version.
78 buf[pos++] = 0xff;
79 buf[pos++] = 0x74;
80 buf[pos++] = 0x4f;
81 buf[pos++] = 0x63;
82 dv.setUint32(pos, 2, false);
83 pos += 4;
84 
85 // 5. Fanout table.
86 const fanoutStart = pos;
87 // Fill with zeros first; we'll accumulate below.
88 pos += 256 * 4;
89 
90 // Compute fanout: for each first-byte bucket, count how many sorted OIDs
91 // have a first byte <= that value.
92 const buckets = new Uint32Array(256);
93 for (let i = 0; i < N; i++) {
94 const idx = sortedIndices[i];
95 const firstByte = table.oids[idx * 20];
96 buckets[firstByte]++;
97 }
98 let cumulative = 0;
99 for (let b = 0; b < 256; b++) {
100 cumulative += buckets[b];
101 dv.setUint32(fanoutStart + b * 4, cumulative, false);
102 }
103 
104 // 6. OID table (sorted).
105 for (let i = 0; i < N; i++) {
106 const idx = sortedIndices[i];
107 buf.set(table.oids.subarray(idx * 20, idx * 20 + 20), pos);
108 pos += 20;
109 }
110 
111 // 7. CRC-32 table (in OID-sorted order).
112 for (let i = 0; i < N; i++) {
113 const idx = sortedIndices[i];
114 dv.setUint32(pos, table.crc32s[idx], false);
115 pos += 4;
116 }
117 
118 // 8. 32-bit offset table.
119 const offsets32Start = pos;
120 pos += N * 4;
121 
122 // 9. 64-bit offset table (only for offsets >= 0x80000000).
123 const offsets64Start = pos;
124 let largeIdx = 0;
125 for (let i = 0; i < N; i++) {
126 const idx = sortedIndices[i];
127 const off = table.offsets[idx];
128 if (off >= 0x80000000) {
129 // Mark MSB in the 32-bit table entry to point into the 64-bit table.
130 dv.setUint32(offsets32Start + i * 4, 0x80000000 | largeIdx, false);
131 // Write 64-bit offset as two uint32.
132 dv.setUint32(offsets64Start + largeIdx * 8, 0, false); // high 32 bits (always 0 for < 4GB)
133 dv.setUint32(offsets64Start + largeIdx * 8 + 4, off, false);
134 largeIdx++;
135 } else {
136 dv.setUint32(offsets32Start + i * 4, off, false);
137 }
138 }
139 pos = offsets64Start + largeCount * 8;
140 
141 // 10. Pack checksum.
142 buf.set(packChecksum, pos);
143 pos += 20;
144 
145 // 11. Idx checksum: SHA-1 of everything before this field.
146 const idxHash = new Uint8Array(
147 await crypto.subtle.digest("SHA-1", asBufferSource(buf.subarray(0, pos)))
148 );
149 buf.set(idxHash, pos);
150 
151 return buf;
152}