File
Blob: test/util/git-pack.ts
| 1 | import { asBufferSource, deflate, hexToBytes, zeroOid } from "@/worker/common"; |
| 2 | import type { GitObjectType } from "@/worker/git"; |
| 3 | import { concatChunks, encodeObjHeader, encodeOfsDeltaDistance, objTypeCode } from "@/worker/git"; |
| 4 | |
| 5 | type PackObjectEntry = { |
| 6 | type: GitObjectType; |
| 7 | payload: Uint8Array; |
| 8 | }; |
| 9 | |
| 10 | type OfsDeltaEntry = { |
| 11 | type: "ofs-delta"; |
| 12 | baseIndex: number; |
| 13 | delta: Uint8Array; |
| 14 | }; |
| 15 | |
| 16 | type RefDeltaEntry = { |
| 17 | type: "ref-delta"; |
| 18 | baseOid: string; |
| 19 | delta: Uint8Array; |
| 20 | }; |
| 21 | |
| 22 | type PackBuildEntry = PackObjectEntry | OfsDeltaEntry | RefDeltaEntry; |
| 23 | |
| 24 | function encodeDeltaVarint(value: number): Uint8Array { |
| 25 | if (!Number.isInteger(value) || value < 0) { |
| 26 | throw new Error(`invalid delta varint value: ${value}`); |
| 27 | } |
| 28 | const out: number[] = []; |
| 29 | do { |
| 30 | let byte = value & 0x7f; |
| 31 | value >>>= 7; |
| 32 | if (value > 0) byte |= 0x80; |
| 33 | out.push(byte); |
| 34 | } while (value > 0); |
| 35 | return Uint8Array.from(out); |
| 36 | } |
| 37 | |
| 38 | function encodeDeltaCopy(offset: number, size: number): Uint8Array { |
| 39 | if (!Number.isInteger(offset) || offset < 0) { |
| 40 | throw new Error(`invalid delta copy offset: ${offset}`); |
| 41 | } |
| 42 | if (!Number.isInteger(size) || size <= 0 || size > 0x10000) { |
| 43 | throw new Error(`invalid delta copy size: ${size}`); |
| 44 | } |
| 45 | |
| 46 | let opcode = 0x80; |
| 47 | const bytes: number[] = []; |
| 48 | |
| 49 | if (offset & 0xff) { |
| 50 | opcode |= 0x01; |
| 51 | bytes.push(offset & 0xff); |
| 52 | } |
| 53 | if (offset & 0xff00) { |
| 54 | opcode |= 0x02; |
| 55 | bytes.push((offset >>> 8) & 0xff); |
| 56 | } |
| 57 | if (offset & 0xff0000) { |
| 58 | opcode |= 0x04; |
| 59 | bytes.push((offset >>> 16) & 0xff); |
| 60 | } |
| 61 | if (offset & 0xff000000) { |
| 62 | opcode |= 0x08; |
| 63 | bytes.push((offset >>> 24) & 0xff); |
| 64 | } |
| 65 | |
| 66 | if (size !== 0x10000) { |
| 67 | if (size & 0xff) { |
| 68 | opcode |= 0x10; |
| 69 | bytes.push(size & 0xff); |
| 70 | } |
| 71 | if (size & 0xff00) { |
| 72 | opcode |= 0x20; |
| 73 | bytes.push((size >>> 8) & 0xff); |
| 74 | } |
| 75 | if (size & 0xff0000) { |
| 76 | opcode |= 0x40; |
| 77 | bytes.push((size >>> 16) & 0xff); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | return Uint8Array.from([opcode, ...bytes]); |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Build a simple append-only git delta. The result is the full base payload |
| 86 | * followed by the provided suffix bytes. |
| 87 | */ |
| 88 | export function buildAppendOnlyDelta(base: Uint8Array, suffix: Uint8Array): Uint8Array { |
| 89 | const parts: Uint8Array[] = [ |
| 90 | encodeDeltaVarint(base.length), |
| 91 | encodeDeltaVarint(base.length + suffix.length), |
| 92 | ]; |
| 93 | |
| 94 | if (base.length > 0) parts.push(encodeDeltaCopy(0, base.length)); |
| 95 | |
| 96 | for (let offset = 0; offset < suffix.length; offset += 0x7f) { |
| 97 | const chunk = suffix.subarray(offset, offset + 0x7f); |
| 98 | parts.push(Uint8Array.from([chunk.length])); |
| 99 | parts.push(chunk); |
| 100 | } |
| 101 | |
| 102 | return concatChunks(parts); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Build a delta whose result is the leading `prefixLength` bytes of `base`. |
| 107 | */ |
| 108 | export function buildCopyPrefixDelta(base: Uint8Array, prefixLength: number): Uint8Array { |
| 109 | if (!Number.isInteger(prefixLength) || prefixLength < 0 || prefixLength > base.length) { |
| 110 | throw new Error(`invalid delta prefix length: ${prefixLength}`); |
| 111 | } |
| 112 | |
| 113 | const parts: Uint8Array[] = [encodeDeltaVarint(base.length), encodeDeltaVarint(prefixLength)]; |
| 114 | if (prefixLength > 0) parts.push(encodeDeltaCopy(0, prefixLength)); |
| 115 | return concatChunks(parts); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Build a Git pack file from objects, including delta entries used by pack-only tests. |
| 120 | */ |
| 121 | export async function buildPack(objects: PackBuildEntry[]): Promise<Uint8Array> { |
| 122 | const header = new Uint8Array(12); |
| 123 | header.set(new TextEncoder().encode("PACK"), 0); |
| 124 | const view = new DataView(header.buffer); |
| 125 | view.setUint32(4, 2); |
| 126 | view.setUint32(8, objects.length); |
| 127 | |
| 128 | const parts: Uint8Array[] = [header]; |
| 129 | const entryOffsets: number[] = []; |
| 130 | let bodyLength = header.byteLength; |
| 131 | |
| 132 | for (const object of objects) { |
| 133 | const entryOffset = bodyLength; |
| 134 | let objectHeader: Uint8Array; |
| 135 | let objectPayload: Uint8Array; |
| 136 | |
| 137 | if (object.type === "ofs-delta") { |
| 138 | const baseOffset = entryOffsets[object.baseIndex]; |
| 139 | if (baseOffset === undefined) { |
| 140 | throw new Error(`missing OFS_DELTA base index: ${object.baseIndex}`); |
| 141 | } |
| 142 | objectHeader = concatChunks([ |
| 143 | encodeObjHeader(6, object.delta.byteLength), |
| 144 | encodeOfsDeltaDistance(entryOffset - baseOffset), |
| 145 | ]); |
| 146 | objectPayload = await deflate(object.delta); |
| 147 | } else if (object.type === "ref-delta") { |
| 148 | objectHeader = concatChunks([ |
| 149 | encodeObjHeader(7, object.delta.byteLength), |
| 150 | hexToBytes(object.baseOid), |
| 151 | ]); |
| 152 | objectPayload = await deflate(object.delta); |
| 153 | } else { |
| 154 | objectHeader = encodeObjHeader(objTypeCode(object.type), object.payload.byteLength); |
| 155 | objectPayload = await deflate(object.payload); |
| 156 | } |
| 157 | |
| 158 | parts.push(objectHeader); |
| 159 | parts.push(objectPayload); |
| 160 | entryOffsets.push(entryOffset); |
| 161 | bodyLength += objectHeader.byteLength + objectPayload.byteLength; |
| 162 | } |
| 163 | |
| 164 | const body = concatChunks(parts); |
| 165 | const sha = new Uint8Array(await crypto.subtle.digest("SHA-1", asBufferSource(body))); |
| 166 | const out = new Uint8Array(body.byteLength + 20); |
| 167 | out.set(body, 0); |
| 168 | out.set(sha, body.byteLength); |
| 169 | return out; |
| 170 | } |
| 171 | |
| 172 | export async function makeCommit(treeOid: string, msg: string) { |
| 173 | const author = `You <you@example.com> 0 +0000`; |
| 174 | const payload = new TextEncoder().encode( |
| 175 | `tree ${treeOid}\n` + `author ${author}\n` + `committer ${author}\n\n${msg}` |
| 176 | ); |
| 177 | const header = new TextEncoder().encode(`commit ${payload.byteLength}\0`); |
| 178 | const raw = new Uint8Array(header.length + payload.length); |
| 179 | raw.set(header, 0); |
| 180 | raw.set(payload, header.length); |
| 181 | const hash = await crypto.subtle.digest("SHA-1", raw); |
| 182 | const oid = Array.from(new Uint8Array(hash)) |
| 183 | .map((byte) => byte.toString(16).padStart(2, "0")) |
| 184 | .join(""); |
| 185 | return { oid, payload }; |
| 186 | } |
| 187 | |
| 188 | export async function makeTree(): Promise<{ oid: string; payload: Uint8Array }> { |
| 189 | const payload = new Uint8Array(0); |
| 190 | const header = new TextEncoder().encode(`tree ${payload.byteLength}\0`); |
| 191 | const raw = new Uint8Array(header.length + payload.length); |
| 192 | raw.set(header, 0); |
| 193 | raw.set(payload, header.length); |
| 194 | const oid = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-1", raw))) |
| 195 | .map((byte) => byte.toString(16).padStart(2, "0")) |
| 196 | .join(""); |
| 197 | return { oid, payload }; |
| 198 | } |
| 199 | |
| 200 | export function zero40(): string { |
| 201 | return zeroOid(); |
| 202 | } |
| 203 | |
| 204 | export { encodeObjHeader } from "@/worker/git"; |