codex-copilot-dx 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -8
- package/bin/cli.mjs +23 -4
- package/package.json +1 -1
- package/scripts/benchmark.mjs +34 -0
- package/src/adapter.mjs +16 -0
- package/src/auth.mjs +5 -0
- package/src/cli-auth.mjs +43 -12
- package/src/cli-options.mjs +143 -47
- package/src/cli-status.mjs +35 -2
- package/src/doctor.mjs +56 -3
- package/src/observability.mjs +10 -4
- package/src/pm-studio-asar.mjs +392 -0
- package/src/pm-studio-relay.mjs +523 -0
- package/src/pm-studio-setup.mjs +734 -0
- package/src/pm-studio-status.mjs +130 -0
- package/src/profile-model-runtime.mjs +3 -2
- package/src/profile-routing.mjs +74 -0
- package/src/profile-runtime.mjs +13 -0
- package/src/running-adapter.mjs +16 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const PM_STUDIO_ORIGIN = "https://api.githubcopilot.com";
|
|
4
|
+
export const CCDX_PM_STUDIO_ORIGIN = "http://127.0.0.1:2026/pm-ccdx";
|
|
5
|
+
export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A";
|
|
6
|
+
|
|
7
|
+
export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
|
|
8
|
+
id: "pm-studio-2.9.7-build-2.9.7",
|
|
9
|
+
version: "2.9.7",
|
|
10
|
+
build: "2.9.7",
|
|
11
|
+
bundleIdentifier: "com.pm-studio.app",
|
|
12
|
+
sourceTeamIdentifier: "HL75GKK4W4",
|
|
13
|
+
executable: "PM Studio",
|
|
14
|
+
sourceExecutableSha256: "6364edd0561790610ee82399865f42f160523551d8d72220e26c4b18da324017",
|
|
15
|
+
electronFrameworkPath: "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
|
|
16
|
+
sourceElectronFrameworkSha256: "e684d310334840f7a64f2d5171052eb514822af155779d3185e4f068daee4387",
|
|
17
|
+
embeddedAsarIntegrity: "absent",
|
|
18
|
+
asarPath: "Contents/Resources/app.asar",
|
|
19
|
+
infoPlistPath: "Contents/Info.plist",
|
|
20
|
+
integrityKey: "ElectronAsarIntegrity.Resources/app.asar",
|
|
21
|
+
dataOffset: 3_832_764,
|
|
22
|
+
sourceAsarSha256: "36fb8bf2fc9326ddf49baab5cc0b54b38751a7db041bda3c42a3b01107b54118",
|
|
23
|
+
sourceHeaderSha256: "87746d92e00db2d40c03830ea06de22ec7cba112d07729801ccd02e226ffd961",
|
|
24
|
+
patchedAsarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
|
|
25
|
+
patchedHeaderSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
|
|
26
|
+
sourceSentinel: PM_STUDIO_ORIGIN,
|
|
27
|
+
patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
|
|
28
|
+
targets: Object.freeze([
|
|
29
|
+
Object.freeze({
|
|
30
|
+
path: "dist/main/main.js",
|
|
31
|
+
offset: 101_621_627,
|
|
32
|
+
size: 9_909_974,
|
|
33
|
+
sentinelOffset: 1_120_480,
|
|
34
|
+
absoluteSentinelOffset: 106_574_871,
|
|
35
|
+
sentinelCount: 1,
|
|
36
|
+
blockSize: 4_194_304,
|
|
37
|
+
sourceSha256: "58383be112886becdb8d8e7cce1b3efce07be6716190aa4940627f456eb8edb2",
|
|
38
|
+
patchedSha256: "24f1c47831c30f59e8464a66237c6bc9ed771d79f3a6889019dfdf53bc20fe42",
|
|
39
|
+
sourceBlocks: Object.freeze([
|
|
40
|
+
"3ce05a21e8992ccf12966cf9b78fcd28b2f66e6786489b5bb0a06f1d4b2a0c1e",
|
|
41
|
+
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
42
|
+
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
43
|
+
]),
|
|
44
|
+
patchedBlocks: Object.freeze([
|
|
45
|
+
"afc18cc7d56cce6dd6f3bade8661268a534f2de93de5ba638f6609d2638c7436",
|
|
46
|
+
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
47
|
+
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
48
|
+
]),
|
|
49
|
+
}),
|
|
50
|
+
Object.freeze({
|
|
51
|
+
path: "dist/renderer/js/main.09f18d95.js",
|
|
52
|
+
offset: 137_002_230,
|
|
53
|
+
size: 2_469_533,
|
|
54
|
+
sentinelOffset: 489_935,
|
|
55
|
+
absoluteSentinelOffset: 141_324_929,
|
|
56
|
+
sentinelCount: 1,
|
|
57
|
+
blockSize: 4_194_304,
|
|
58
|
+
sourceSha256: "eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
|
|
59
|
+
patchedSha256: "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
|
|
60
|
+
sourceBlocks: Object.freeze([
|
|
61
|
+
"eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
|
|
62
|
+
]),
|
|
63
|
+
patchedBlocks: Object.freeze([
|
|
64
|
+
"89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
|
|
65
|
+
]),
|
|
66
|
+
}),
|
|
67
|
+
]),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const PM_STUDIO_RECIPES = Object.freeze([PM_STUDIO_2_9_7_RECIPE]);
|
|
71
|
+
|
|
72
|
+
export function sha256Hex(value) {
|
|
73
|
+
return createHash("sha256").update(value).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkedBuffer(value) {
|
|
77
|
+
if (!Buffer.isBuffer(value)) throw new TypeError("ASAR input must be a Buffer");
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkedUInt32(buffer, offset, name) {
|
|
82
|
+
if (offset < 0 || offset + 4 > buffer.length) throw new Error(`Invalid ASAR: missing ${name}`);
|
|
83
|
+
return buffer.readUInt32LE(offset);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function integer(value, name) {
|
|
87
|
+
const result = Number(value);
|
|
88
|
+
if (!Number.isSafeInteger(result) || result < 0) throw new Error(`Invalid ASAR ${name}: ${value}`);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function entryOffset(value, name) {
|
|
93
|
+
if (typeof value !== "string" || !/^(?:0|[1-9]\d*)$/.test(value)) {
|
|
94
|
+
throw new Error(`Invalid ASAR ${name}: ${value}`);
|
|
95
|
+
}
|
|
96
|
+
return integer(value, name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function entrySize(value, name) {
|
|
100
|
+
if (typeof value !== "number") throw new Error(`Invalid ASAR ${name}: ${value}`);
|
|
101
|
+
return integer(value, name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function aligned4(value) {
|
|
105
|
+
return (value + 3) & ~3;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseAsarBuffer(value) {
|
|
109
|
+
const buffer = checkedBuffer(value);
|
|
110
|
+
if (buffer.length < 16) throw new Error("Invalid ASAR: header is truncated");
|
|
111
|
+
|
|
112
|
+
const sizePicklePayload = checkedUInt32(buffer, 0, "size pickle");
|
|
113
|
+
const headerPickleSize = checkedUInt32(buffer, 4, "header pickle size");
|
|
114
|
+
const headerPicklePayload = checkedUInt32(buffer, 8, "header pickle payload");
|
|
115
|
+
const headerLength = checkedUInt32(buffer, 12, "header string size");
|
|
116
|
+
const headerStart = 16;
|
|
117
|
+
const headerEnd = headerStart + headerLength;
|
|
118
|
+
const dataOffset = 8 + headerPickleSize;
|
|
119
|
+
|
|
120
|
+
if (sizePicklePayload !== 4
|
|
121
|
+
|| headerPickleSize < 8
|
|
122
|
+
|| headerPickleSize % 4 !== 0
|
|
123
|
+
|| headerPicklePayload !== headerPickleSize - 4
|
|
124
|
+
|| headerPicklePayload !== 4 + aligned4(headerLength)) {
|
|
125
|
+
throw new Error("Invalid ASAR: malformed pickle sizes");
|
|
126
|
+
}
|
|
127
|
+
if (headerEnd > dataOffset || dataOffset > buffer.length) {
|
|
128
|
+
throw new Error("Invalid ASAR: header extends outside the archive");
|
|
129
|
+
}
|
|
130
|
+
if (buffer.subarray(headerEnd, dataOffset).some((byte) => byte !== 0)) {
|
|
131
|
+
throw new Error("Invalid ASAR: header padding is not zeroed");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const headerBytes = buffer.subarray(headerStart, headerEnd);
|
|
135
|
+
let header;
|
|
136
|
+
try {
|
|
137
|
+
header = JSON.parse(headerBytes.toString("utf8"));
|
|
138
|
+
} catch {
|
|
139
|
+
throw new Error("Invalid ASAR: header JSON could not be parsed");
|
|
140
|
+
}
|
|
141
|
+
if (!header || typeof header !== "object" || !header.files || typeof header.files !== "object") {
|
|
142
|
+
throw new Error("Invalid ASAR: header has no files tree");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
buffer,
|
|
147
|
+
header,
|
|
148
|
+
headerBytes,
|
|
149
|
+
headerLength,
|
|
150
|
+
headerStart,
|
|
151
|
+
headerEnd,
|
|
152
|
+
headerPickleSize,
|
|
153
|
+
dataOffset,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function findAsarEntry(header, filePath) {
|
|
158
|
+
const parts = String(filePath || "").split("/").filter(Boolean);
|
|
159
|
+
if (parts.length === 0) return null;
|
|
160
|
+
let node = { files: header?.files };
|
|
161
|
+
for (const part of parts) {
|
|
162
|
+
node = node?.files?.[part];
|
|
163
|
+
if (!node) return null;
|
|
164
|
+
}
|
|
165
|
+
return node;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function occurrencePositions(haystack, needle) {
|
|
169
|
+
const positions = [];
|
|
170
|
+
if (needle.length === 0) return positions;
|
|
171
|
+
let offset = 0;
|
|
172
|
+
while (offset <= haystack.length - needle.length) {
|
|
173
|
+
const position = haystack.indexOf(needle, offset);
|
|
174
|
+
if (position < 0) break;
|
|
175
|
+
positions.push(position);
|
|
176
|
+
offset = position + needle.length;
|
|
177
|
+
}
|
|
178
|
+
return positions;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function blockSha256(buffer, blockSize) {
|
|
182
|
+
const size = integer(blockSize, "block size");
|
|
183
|
+
if (size === 0) throw new Error("Invalid ASAR block size: 0");
|
|
184
|
+
const hashes = [];
|
|
185
|
+
if (buffer.length === 0) return [sha256Hex(buffer)];
|
|
186
|
+
for (let offset = 0; offset < buffer.length; offset += size) {
|
|
187
|
+
hashes.push(sha256Hex(buffer.subarray(offset, Math.min(offset + size, buffer.length))));
|
|
188
|
+
}
|
|
189
|
+
return hashes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function inspectElectronAsarIntegritySlots(value) {
|
|
193
|
+
const buffer = checkedBuffer(value);
|
|
194
|
+
const sentinel = Buffer.from(ELECTRON_ASAR_INTEGRITY_SENTINEL);
|
|
195
|
+
const slots = [];
|
|
196
|
+
let searchOffset = 0;
|
|
197
|
+
let truncated = false;
|
|
198
|
+
while (searchOffset <= buffer.length - sentinel.length) {
|
|
199
|
+
const offset = buffer.indexOf(sentinel, searchOffset);
|
|
200
|
+
if (offset < 0) break;
|
|
201
|
+
if (offset + 66 > buffer.length) {
|
|
202
|
+
truncated = true;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const used = buffer[offset + 32];
|
|
206
|
+
const version = buffer[offset + 33];
|
|
207
|
+
slots.push({
|
|
208
|
+
offset,
|
|
209
|
+
used,
|
|
210
|
+
version,
|
|
211
|
+
digest: buffer.subarray(offset + 34, offset + 66).toString("hex"),
|
|
212
|
+
});
|
|
213
|
+
searchOffset = offset + sentinel.length;
|
|
214
|
+
}
|
|
215
|
+
const malformed = truncated
|
|
216
|
+
|| slots.length > 1
|
|
217
|
+
|| slots.some((slot) => ![0, 1].includes(slot.used) || (slot.used === 1 && slot.version !== 1));
|
|
218
|
+
const active = slots.some((slot) => slot.used === 1);
|
|
219
|
+
return {
|
|
220
|
+
state: malformed ? "drift" : active ? "active" : slots.length === 1 ? "inactive" : "absent",
|
|
221
|
+
active,
|
|
222
|
+
supported: !malformed && !active,
|
|
223
|
+
slots,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sameArray(actual, expected) {
|
|
228
|
+
return Array.isArray(actual)
|
|
229
|
+
&& Array.isArray(expected)
|
|
230
|
+
&& actual.length === expected.length
|
|
231
|
+
&& actual.every((value, index) => value === expected[index]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function addMismatch(issues, label, actual, expected) {
|
|
235
|
+
if (actual !== expected) issues.push(`${label}: expected ${expected}, got ${actual}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function inspectTarget(parsed, target, recipe) {
|
|
239
|
+
const entry = findAsarEntry(parsed.header, target.path);
|
|
240
|
+
if (!entry) return { path: target.path, missing: true };
|
|
241
|
+
|
|
242
|
+
let offset;
|
|
243
|
+
let size;
|
|
244
|
+
try {
|
|
245
|
+
offset = entryOffset(entry.offset, `${target.path} offset`);
|
|
246
|
+
size = entrySize(entry.size, `${target.path} size`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return { path: target.path, malformed: error.message };
|
|
249
|
+
}
|
|
250
|
+
const absoluteOffset = parsed.dataOffset + offset;
|
|
251
|
+
if (entry.unpacked || absoluteOffset + size > parsed.buffer.length) {
|
|
252
|
+
return { path: target.path, offset, size, absoluteOffset, malformed: "entry data is outside the packed archive" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const bytes = parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
|
|
256
|
+
const sourceSentinel = Buffer.from(recipe.sourceSentinel);
|
|
257
|
+
const patchedSentinel = Buffer.from(recipe.patchedSentinel);
|
|
258
|
+
return {
|
|
259
|
+
path: target.path,
|
|
260
|
+
offset,
|
|
261
|
+
size,
|
|
262
|
+
absoluteOffset,
|
|
263
|
+
sha256: sha256Hex(bytes),
|
|
264
|
+
blockSize: entry.integrity?.blockSize,
|
|
265
|
+
blocks: blockSha256(bytes, target.blockSize),
|
|
266
|
+
integrity: entry.integrity || null,
|
|
267
|
+
sourceSentinelPositions: occurrencePositions(bytes, sourceSentinel),
|
|
268
|
+
patchedSentinelPositions: occurrencePositions(bytes, patchedSentinel),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function targetStateIssues(detail, target, recipe, state) {
|
|
273
|
+
const issues = [];
|
|
274
|
+
if (detail.missing) return [`${target.path}: entry is missing`];
|
|
275
|
+
if (detail.malformed) return [`${target.path}: ${detail.malformed}`];
|
|
276
|
+
|
|
277
|
+
addMismatch(issues, `${target.path} offset`, detail.offset, target.offset);
|
|
278
|
+
addMismatch(issues, `${target.path} size`, detail.size, target.size);
|
|
279
|
+
addMismatch(issues, `${target.path} absolute sentinel offset`,
|
|
280
|
+
detail.absoluteOffset + target.sentinelOffset, target.absoluteSentinelOffset);
|
|
281
|
+
addMismatch(issues, `${target.path} integrity algorithm`,
|
|
282
|
+
detail.integrity?.algorithm, "SHA256");
|
|
283
|
+
addMismatch(issues, `${target.path} block size`, detail.integrity?.blockSize, target.blockSize);
|
|
284
|
+
|
|
285
|
+
const patched = state === "patched";
|
|
286
|
+
const expectedHash = patched ? target.patchedSha256 : target.sourceSha256;
|
|
287
|
+
const expectedBlocks = patched ? target.patchedBlocks : target.sourceBlocks;
|
|
288
|
+
const expectedSentinelPositions = [target.sentinelOffset];
|
|
289
|
+
const presentPositions = patched ? detail.patchedSentinelPositions : detail.sourceSentinelPositions;
|
|
290
|
+
const absentPositions = patched ? detail.sourceSentinelPositions : detail.patchedSentinelPositions;
|
|
291
|
+
|
|
292
|
+
addMismatch(issues, `${target.path} SHA-256`, detail.sha256, expectedHash);
|
|
293
|
+
addMismatch(issues, `${target.path} integrity hash`, detail.integrity?.hash, expectedHash);
|
|
294
|
+
if (!sameArray(detail.blocks, expectedBlocks)) issues.push(`${target.path} computed block hashes do not match ${state} recipe`);
|
|
295
|
+
if (!sameArray(detail.integrity?.blocks, expectedBlocks)) issues.push(`${target.path} header block hashes do not match ${state} recipe`);
|
|
296
|
+
if (!sameArray(presentPositions, expectedSentinelPositions)) {
|
|
297
|
+
issues.push(`${target.path} ${state} sentinel position/count does not match recipe`);
|
|
298
|
+
}
|
|
299
|
+
if (absentPositions.length !== 0) issues.push(`${target.path} contains the opposite-state sentinel`);
|
|
300
|
+
if (presentPositions.length !== target.sentinelCount) {
|
|
301
|
+
issues.push(`${target.path} ${state} sentinel count is ${presentPositions.length}, expected ${target.sentinelCount}`);
|
|
302
|
+
}
|
|
303
|
+
return issues;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
307
|
+
const parsed = parseAsarBuffer(value);
|
|
308
|
+
const asarSha256 = sha256Hex(parsed.buffer);
|
|
309
|
+
const headerSha256 = sha256Hex(parsed.headerBytes);
|
|
310
|
+
const targets = recipe.targets.map((target) => inspectTarget(parsed, target, recipe));
|
|
311
|
+
const commonIssues = [];
|
|
312
|
+
addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
|
|
313
|
+
|
|
314
|
+
const issuesFor = (state) => {
|
|
315
|
+
const patched = state === "patched";
|
|
316
|
+
const issues = [...commonIssues];
|
|
317
|
+
addMismatch(issues, "ASAR SHA-256", asarSha256,
|
|
318
|
+
patched ? recipe.patchedAsarSha256 : recipe.sourceAsarSha256);
|
|
319
|
+
addMismatch(issues, "ASAR header SHA-256", headerSha256,
|
|
320
|
+
patched ? recipe.patchedHeaderSha256 : recipe.sourceHeaderSha256);
|
|
321
|
+
for (let index = 0; index < recipe.targets.length; index += 1) {
|
|
322
|
+
issues.push(...targetStateIssues(targets[index], recipe.targets[index], recipe, state));
|
|
323
|
+
}
|
|
324
|
+
return issues;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const cleanIssues = issuesFor("clean");
|
|
328
|
+
const patchedIssues = issuesFor("patched");
|
|
329
|
+
const state = cleanIssues.length === 0 ? "clean" : patchedIssues.length === 0 ? "patched" : "drift";
|
|
330
|
+
return {
|
|
331
|
+
recipeId: recipe.id,
|
|
332
|
+
state,
|
|
333
|
+
asarSha256,
|
|
334
|
+
headerSha256,
|
|
335
|
+
dataOffset: parsed.dataOffset,
|
|
336
|
+
targets,
|
|
337
|
+
cleanIssues,
|
|
338
|
+
patchedIssues,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
343
|
+
if (Buffer.byteLength(recipe.sourceSentinel) !== Buffer.byteLength(recipe.patchedSentinel)) {
|
|
344
|
+
throw new Error("PM Studio patch requires equal-length origin sentinels");
|
|
345
|
+
}
|
|
346
|
+
const before = inspectAsarBuffer(value, recipe);
|
|
347
|
+
if (before.state === "patched") {
|
|
348
|
+
return { changed: false, buffer: Buffer.from(value), before, after: before };
|
|
349
|
+
}
|
|
350
|
+
if (before.state !== "clean") {
|
|
351
|
+
const error = new Error("PM Studio ASAR does not match the clean or patched recipe");
|
|
352
|
+
error.code = "PM_STUDIO_ASAR_DRIFT";
|
|
353
|
+
error.inspection = before;
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const output = Buffer.from(value);
|
|
358
|
+
const parsed = parseAsarBuffer(output);
|
|
359
|
+
const sourceSentinel = Buffer.from(recipe.sourceSentinel);
|
|
360
|
+
const patchedSentinel = Buffer.from(recipe.patchedSentinel);
|
|
361
|
+
|
|
362
|
+
for (const target of recipe.targets) {
|
|
363
|
+
const entry = findAsarEntry(parsed.header, target.path);
|
|
364
|
+
const offset = entryOffset(entry.offset, `${target.path} offset`);
|
|
365
|
+
const absoluteSentinelOffset = parsed.dataOffset + offset + target.sentinelOffset;
|
|
366
|
+
const existing = output.subarray(absoluteSentinelOffset, absoluteSentinelOffset + sourceSentinel.length);
|
|
367
|
+
if (!existing.equals(sourceSentinel)) throw new Error(`${target.path} source sentinel moved before patching`);
|
|
368
|
+
patchedSentinel.copy(output, absoluteSentinelOffset);
|
|
369
|
+
entry.integrity = {
|
|
370
|
+
...entry.integrity,
|
|
371
|
+
algorithm: "SHA256",
|
|
372
|
+
hash: target.patchedSha256,
|
|
373
|
+
blockSize: target.blockSize,
|
|
374
|
+
blocks: [...target.patchedBlocks],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const nextHeaderBytes = Buffer.from(JSON.stringify(parsed.header));
|
|
379
|
+
if (nextHeaderBytes.length !== parsed.headerLength) {
|
|
380
|
+
throw new Error(`Patched ASAR header changed size (${parsed.headerLength} -> ${nextHeaderBytes.length})`);
|
|
381
|
+
}
|
|
382
|
+
nextHeaderBytes.copy(output, parsed.headerStart);
|
|
383
|
+
|
|
384
|
+
const after = inspectAsarBuffer(output, recipe);
|
|
385
|
+
if (after.state !== "patched") {
|
|
386
|
+
const error = new Error("Patched PM Studio ASAR failed recipe verification");
|
|
387
|
+
error.code = "PM_STUDIO_ASAR_PATCH_VERIFY_FAILED";
|
|
388
|
+
error.inspection = after;
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
return { changed: true, buffer: output, before, after };
|
|
392
|
+
}
|