Skip to content
File

Blob: src/worker/git/object-store/materialize.ts

typescript180 lines
1import type { Logger } from "@/worker/common/logger";
2import type { Limiter } from "@/worker/git/operations/limits";
3import type { PackedObjectResult } from "./types";
4import type { PackedObjectCandidate } from "./candidates";
5 
6import { inflate } from "@/worker/common";
7import { readPackHeaderExFromBuf, readPackRange } from "@/worker/git/pack/packMeta";
8import { applyGitDelta } from "./delta";
9import { findOffsetIndex, getNextOffsetByIndex, getOidHexAt } from "./idxView";
10import { toPackedObjectResult, typeCodeToObjectType } from "./support";
11 
12export type PackedMaterializerCyclePolicy = "throw" | "miss";
13 
14export type PackedRefBaseResolver = (
15 oid: string,
16 visited: Set<string>
17) => Promise<PackedObjectResult | undefined>;
18 
19export type PackedMaterializerAbortCheck = (stage: string) => void;
20 
21export type PackedMaterializerOptions = {
22 env: Env;
23 candidate: PackedObjectCandidate;
24 limiter: Limiter;
25 countSubrequest: (n?: number) => boolean | void;
26 log: Logger;
27 cyclePolicy: PackedMaterializerCyclePolicy;
28 resolveRefBase: PackedRefBaseResolver;
29 visited: Set<string>;
30 signal?: AbortSignal;
31 checkAborted?: PackedMaterializerAbortCheck;
32};
33 
34function makeVisitKey(candidate: PackedObjectCandidate): string {
35 return `${candidate.source.packKey}#${candidate.objectIndex}`;
36}
37 
38function makeCandidateFromOffset(
39 source: PackedObjectCandidate["source"],
40 packSlot: number,
41 objectIndex: number,
42 offset: number
43): PackedObjectCandidate | undefined {
44 const nextOffset = getNextOffsetByIndex(source.idx, objectIndex);
45 if (nextOffset === undefined) return undefined;
46 
47 return {
48 source,
49 packSlot,
50 objectIndex,
51 offset,
52 nextOffset,
53 oid: getOidHexAt(source.idx, objectIndex),
54 };
55}
56 
57async function materializeCandidate(
58 options: PackedMaterializerOptions,
59 candidate: PackedObjectCandidate
60): Promise<PackedObjectResult | undefined> {
61 options.checkAborted?.("packed-materialize:entry");
62 
63 const visitKey = makeVisitKey(candidate);
64 if (options.visited.has(visitKey)) {
65 options.log.debug("packed-materialize:cycle", {
66 packKey: candidate.source.packKey,
67 objectIndex: candidate.objectIndex,
68 oid: candidate.oid,
69 cyclePolicy: options.cyclePolicy,
70 });
71 if (options.cyclePolicy === "throw") throw new Error("pack object recursion cycle");
72 return undefined;
73 }
74 
75 options.visited.add(visitKey);
76 try {
77 const entryLength = candidate.nextOffset - candidate.offset;
78 if (entryLength <= 0) {
79 options.log.warn("packed-materialize:invalid-entry-span", {
80 packKey: candidate.source.packKey,
81 objectIndex: candidate.objectIndex,
82 oid: candidate.oid,
83 offset: candidate.offset,
84 nextOffset: candidate.nextOffset,
85 });
86 return undefined;
87 }
88 
89 options.log.debug("packed-materialize:read-entry", {
90 packKey: candidate.source.packKey,
91 objectIndex: candidate.objectIndex,
92 oid: candidate.oid,
93 offset: candidate.offset,
94 length: entryLength,
95 });
96 const entry = await readPackRange(
97 options.env,
98 candidate.source.packKey,
99 candidate.offset,
100 entryLength,
101 {
102 limiter: options.limiter,
103 countSubrequest: options.countSubrequest,
104 signal: options.signal,
105 }
106 );
107 if (!entry) {
108 options.log.debug("packed-materialize:entry-miss", {
109 packKey: candidate.source.packKey,
110 objectIndex: candidate.objectIndex,
111 oid: candidate.oid,
112 });
113 return undefined;
114 }
115 
116 options.checkAborted?.("packed-materialize:inflate");
117 const header = readPackHeaderExFromBuf(entry, 0);
118 if (!header) {
119 options.log.warn("packed-materialize:bad-header", {
120 packKey: candidate.source.packKey,
121 objectIndex: candidate.objectIndex,
122 oid: candidate.oid,
123 });
124 return undefined;
125 }
126 
127 const payload = await inflate(entry.subarray(header.headerLen));
128 const objectType = typeCodeToObjectType(header.type);
129 if (objectType) return toPackedObjectResult(candidate, objectType, payload);
130 
131 let base: PackedObjectResult | undefined;
132 if (header.type === 6) {
133 const baseOffset = candidate.offset - (header.baseRel || 0);
134 const baseIndex = findOffsetIndex(candidate.source.idx, baseOffset);
135 if (baseIndex === undefined) {
136 options.log.debug("packed-materialize:ofs-base-miss", {
137 packKey: candidate.source.packKey,
138 objectIndex: candidate.objectIndex,
139 oid: candidate.oid,
140 baseOffset,
141 });
142 return undefined;
143 }
144 
145 const baseCandidate = makeCandidateFromOffset(
146 candidate.source,
147 candidate.packSlot,
148 baseIndex,
149 baseOffset
150 );
151 if (!baseCandidate) return undefined;
152 base = await materializeCandidate(options, baseCandidate);
153 } else if (header.type === 7 && header.baseOid) {
154 base = await options.resolveRefBase(header.baseOid, options.visited);
155 }
156 
157 if (!base) {
158 options.log.debug("packed-materialize:base-miss", {
159 packKey: candidate.source.packKey,
160 objectIndex: candidate.objectIndex,
161 oid: candidate.oid,
162 typeCode: header.type,
163 baseOid: header.baseOid,
164 });
165 return undefined;
166 }
167 
168 options.checkAborted?.("packed-materialize:delta");
169 return toPackedObjectResult(candidate, base.type, applyGitDelta(base.payload, payload));
170 } finally {
171 options.visited.delete(visitKey);
172 }
173}
174 
175export async function materializePackedObjectCandidate(
176 options: PackedMaterializerOptions
177): Promise<PackedObjectResult | undefined> {
178 return await materializeCandidate(options, options.candidate);
179}