codex-copilot-dx 0.6.21 → 0.7.1
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 +142 -323
- package/bin/cli.mjs +26 -130
- package/package.json +2 -8
- package/scripts/benchmark.mjs +0 -114
- package/src/adapter.mjs +22 -107
- package/src/auth-profile.mjs +18 -130
- package/src/cli-animation.mjs +108 -0
- package/src/cli-auth.mjs +44 -398
- package/src/cli-models.mjs +21 -49
- package/src/cli-options.mjs +53 -75
- package/src/cli-status.mjs +6 -23
- package/src/config.mjs +2 -8
- package/src/copilot.mjs +2 -2
- package/src/doctor.mjs +44 -291
- package/src/model-cache.mjs +4 -11
- package/src/models.mjs +0 -184
- package/src/observability.mjs +1 -38
- package/src/profile-model-runtime.mjs +61 -189
- package/src/profile-routing.mjs +2 -102
- package/src/profile-runtime.mjs +15 -68
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +1 -1
- package/src/running-adapter.mjs +2 -4
- package/src/stream-errors.mjs +3 -6
- package/src/stream-performance.mjs +1 -1
- package/src/terminal-activity.mjs +40 -34
- package/src/terminal-animation.mjs +249 -0
- package/src/usage.mjs +0 -31
- package/src/user-settings.mjs +50 -0
- package/src/anthropic-handler.mjs +0 -195
- package/src/anthropic.mjs +0 -387
- package/src/claude-config.mjs +0 -57
- package/src/claude-desktop-config.mjs +0 -317
- package/src/cli-error.mjs +0 -50
- package/src/pm-studio-asar.mjs +0 -1244
- package/src/pm-studio-bundle-content.mjs +0 -165
- package/src/pm-studio-relay.mjs +0 -563
- package/src/pm-studio-setup-lock.mjs +0 -336
- package/src/pm-studio-setup.mjs +0 -1581
- package/src/pm-studio-status.mjs +0 -369
- package/src/token-counter-service.mjs +0 -211
- package/src/token-counter-worker.mjs +0 -16
package/src/pm-studio-asar.mjs
DELETED
|
@@ -1,1244 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
|
|
4
|
-
export const PM_STUDIO_ORIGIN = "https://api.githubcopilot.com";
|
|
5
|
-
export const CCDX_PM_STUDIO_ORIGIN = "http://127.0.0.1:2026/pm-ccdx";
|
|
6
|
-
export const PM_STUDIO_SPLIT_ORIGIN_MARKER = "split-origin-v1";
|
|
7
|
-
export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A";
|
|
8
|
-
|
|
9
|
-
const PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH = 2_121;
|
|
10
|
-
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256 = "3da6245d676015ab6cd26e4d6a0d2f54894a687c370aa419c5ad170533d086aa";
|
|
11
|
-
const PM_STUDIO_COPILOT_CONFIG_MODULE_ANCHOR = "47024(I,e,t){";
|
|
12
|
-
const PM_STUDIO_COPILOT_CONFIG_MODULE_SENTINEL = `API_ENDPOINT:"${PM_STUDIO_ORIGIN}"`;
|
|
13
|
-
const PM_STUDIO_COPILOT_CONFIG_MODULE_PATH = "dist/main/main.js";
|
|
14
|
-
const PM_STUDIO_COMPATIBILITY_ID = "exact-copilot-config-module-v1";
|
|
15
|
-
const PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES = 128 * 1024 * 1024;
|
|
16
|
-
const PM_STUDIO_COMPATIBLE_BLOCK_MAX_COUNT = 16_384;
|
|
17
|
-
const PM_STUDIO_COMPATIBLE_MODULE_PREFIX_MAX_BYTES = 512;
|
|
18
|
-
const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE = '47024(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"https://api.githubcopilot.com",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>`req_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}};class c{constructor(){this.config={...l}}static getInstance(){return c.instance||(c.instance=new c),c.instance}getConfig(){return{...this.config}}updateConfig(I){this.config={...this.config,...I}}resetConfig(){this.config={...l}}validateConfig(){return function(){const I=[];return l.CLIENT_ID||I.push("GitHub Copilot Client ID is required"),l.API_ENDPOINT||I.push("GitHub Copilot API endpoint is required"),{valid:0===I.length,errors:I}}()}getStandardHeaders(I){return function(I){const e={Accept:l.STANDARD_HEADERS.Accept,"Content-Type":l.STANDARD_HEADERS["Content-Type"],"User-Agent":l.STANDARD_HEADERS["User-Agent"],"Editor-Version":l.STANDARD_HEADERS["Editor-Version"],"Editor-Plugin-Version":l.STANDARD_HEADERS["Editor-Plugin-Version"],"Copilot-Integration-Id":l.STANDARD_HEADERS["Copilot-Integration-Id"],"X-Request-Id":"function"==typeof l.STANDARD_HEADERS["X-Request-Id"]?l.STANDARD_HEADERS["X-Request-Id"]():l.STANDARD_HEADERS["X-Request-Id"]};return I&&(e.Authorization=`Bearer ${I}`),e}(I)}getCopilotTokenHeaders(I){return function(I){return{Authorization:`Bearer ${I}`,Accept:"application/json","User-Agent":l.USER_AGENT,"Editor-Version":l.EDITOR_VERSION,"Editor-Plugin-Version":l.EDITOR_PLUGIN_VERSION}}(I)}getDeviceCodeHeaders(){return{Accept:"application/json","User-Agent":l.USER_AGENT}}}c.getInstance()}';
|
|
19
|
-
|
|
20
|
-
function copilotConfigAlphaTemplate() {
|
|
21
|
-
// This is a reviewed byte template, not a generic JavaScript normalizer:
|
|
22
|
-
// only the five minifier bindings vary; every other byte remains exact.
|
|
23
|
-
const parts = [];
|
|
24
|
-
const moduleIdEnd = PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.indexOf("(");
|
|
25
|
-
const pattern = /(?<![$\w])(?:I|e|t|l|c)(?![$\w])/g;
|
|
26
|
-
pattern.lastIndex = moduleIdEnd;
|
|
27
|
-
let offset = moduleIdEnd;
|
|
28
|
-
for (let match = pattern.exec(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE); match; match = pattern.exec(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE)) {
|
|
29
|
-
parts.push(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.slice(offset, match.index));
|
|
30
|
-
parts.push(Object.freeze({ identifier: match[0] }));
|
|
31
|
-
offset = match.index + match[0].length;
|
|
32
|
-
}
|
|
33
|
-
parts.push(PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE.slice(offset));
|
|
34
|
-
return Object.freeze(parts);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const PM_STUDIO_COPILOT_CONFIG_ALPHA_TEMPLATE = copilotConfigAlphaTemplate();
|
|
38
|
-
|
|
39
|
-
function splitOriginConfigModule(
|
|
40
|
-
moduleId = "47024",
|
|
41
|
-
windowLength = PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
42
|
-
) {
|
|
43
|
-
const source = `${moduleId}(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"${PM_STUDIO_ORIGIN}",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>\`req_\${Date.now()}_\${Math.random().toString(36).substr(2,9)}\`}};const c=fetch.bind(globalThis),n="${CCDX_PM_STUDIO_ORIGIN}",a="${PM_STUDIO_SPLIT_ORIGIN_MARKER}",h="X-CCDX-PM-Relay",d=new Set,m=I=>I.headers.get(h)===a,q=I=>AbortSignal.any([I,AbortSignal.timeout(750)].filter(Boolean)),f=(I,e=400)=>Response.json({error:{code:I}},{status:e,headers:{[h]:a}});globalThis.fetch=async(I,e)=>{const t=I.url||I+"";if(t===\`\${l.API_ENDPOINT}/models\`){try{const t=await c(\`\${n}/models\`,e);if(t.ok&&m(t))try{let I=await t.clone().json(),e=I.data||I;if(Array.isArray(e)){d.clear();for(const I of e)/anthropic/i.test(I.vendor||I.owned_by)&&d.add(I.id);return t}}catch{}t.body?.cancel?.()?.catch?.(()=>{})}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I}return c(I,e)}const s=t.startsWith(l.API_ENDPOINT)?t.slice(l.API_ENDPOINT.length):"";if("POST"===e?.method&&["/chat/completions","/responses","/embeddings"].includes(s)){let t="";try{t=(JSON.parse(e.body).model||"").trim()}catch{}if(d.has(t)||/^claude-/i.test(t)){if("/chat/completions"!==s)return f("model_not_supported");try{const I=await c(\`\${n}/models\`,{signal:q(e?.signal)}),t=401===I.status&&m(I);await I.text();if(!t)throw 0;const l=await c(\`\${n}\${s}\`,e);if(!m(l)){await l.body?.cancel?.();throw 0}return l}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I;return f("relay_incompatible",503)}}}return c(I,e)}}`;
|
|
44
|
-
if (source.length > windowLength) {
|
|
45
|
-
throw new Error("PM Studio split-origin module exceeds its exact source window");
|
|
46
|
-
}
|
|
47
|
-
return `${source.slice(0, -1)}${" ".repeat(windowLength - source.length)}}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE = splitOriginConfigModule();
|
|
51
|
-
|
|
52
|
-
export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
|
|
53
|
-
id: "pm-studio-2.9.7-build-2.9.7",
|
|
54
|
-
version: "2.9.7",
|
|
55
|
-
build: "2.9.7",
|
|
56
|
-
bundleIdentifier: "com.pm-studio.app",
|
|
57
|
-
sourceTeamIdentifier: "HL75GKK4W4",
|
|
58
|
-
executable: "PM Studio",
|
|
59
|
-
sourceExecutableSha256: "6364edd0561790610ee82399865f42f160523551d8d72220e26c4b18da324017",
|
|
60
|
-
electronFrameworkPath: "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
|
|
61
|
-
sourceElectronFrameworkSha256: "e684d310334840f7a64f2d5171052eb514822af155779d3185e4f068daee4387",
|
|
62
|
-
embeddedAsarIntegrity: "absent",
|
|
63
|
-
asarPath: "Contents/Resources/app.asar",
|
|
64
|
-
infoPlistPath: "Contents/Info.plist",
|
|
65
|
-
integrityKey: "ElectronAsarIntegrity.Resources/app.asar",
|
|
66
|
-
dataOffset: 3_832_764,
|
|
67
|
-
sourceAsarSha256: "36fb8bf2fc9326ddf49baab5cc0b54b38751a7db041bda3c42a3b01107b54118",
|
|
68
|
-
sourceHeaderSha256: "87746d92e00db2d40c03830ea06de22ec7cba112d07729801ccd02e226ffd961",
|
|
69
|
-
patchedAsarSha256: "799dc18103a4553daefcaaddbe3410a1bfaf68d84407f4c10ef3d996afd71ad0",
|
|
70
|
-
patchedHeaderSha256: "08253795176d5471c6f406a29ccb134875d503bb55bca909b2cd131cd7708510",
|
|
71
|
-
predecessor: Object.freeze({
|
|
72
|
-
id: "split-origin-v1-model-discovery-750ms",
|
|
73
|
-
kind: "split-origin-predecessor",
|
|
74
|
-
asarSha256: "d520d115604225c1a3feb749dbe29ad0d2cd175c5233c010de0e1fade527fa0b",
|
|
75
|
-
headerSha256: "83e5fba3b6751b3098d8f6901db361a9b620ab3b924e0e551d3df612ff412642",
|
|
76
|
-
signingMetadata: Object.freeze({
|
|
77
|
-
identifier: "com.pm-studio.app",
|
|
78
|
-
flags: "0x10002(adhoc,runtime)",
|
|
79
|
-
runtime_version: "26.0.0",
|
|
80
|
-
entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
|
|
81
|
-
}),
|
|
82
|
-
}),
|
|
83
|
-
targets: Object.freeze([
|
|
84
|
-
Object.freeze({
|
|
85
|
-
path: "dist/main/main.js",
|
|
86
|
-
offset: 101_621_627,
|
|
87
|
-
size: 9_909_974,
|
|
88
|
-
blockSize: 4_194_304,
|
|
89
|
-
sourceSha256: "58383be112886becdb8d8e7cce1b3efce07be6716190aa4940627f456eb8edb2",
|
|
90
|
-
patchedSha256: "b219d08290b208fe41fa29b1e8ac4a1dee789e15f68fd4ff01eb5cd21249099b",
|
|
91
|
-
predecessorSha256: "69156040fb233a835b7be2f86fbe2f653380f03379adfedf46c63ab45de9d39d",
|
|
92
|
-
sourceBlocks: Object.freeze([
|
|
93
|
-
"3ce05a21e8992ccf12966cf9b78fcd28b2f66e6786489b5bb0a06f1d4b2a0c1e",
|
|
94
|
-
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
95
|
-
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
96
|
-
]),
|
|
97
|
-
patchedBlocks: Object.freeze([
|
|
98
|
-
"dcbffb0f3aaa79a5ca8f1ee9170826b874d9db237c230abcdc0490335b2dc6a8",
|
|
99
|
-
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
100
|
-
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
101
|
-
]),
|
|
102
|
-
predecessorBlocks: Object.freeze([
|
|
103
|
-
"f0b5585d2ff0d403e5c4d35e9bd0b8cbfec5c41a9b9057b8cc808107e889cf46",
|
|
104
|
-
"0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
|
|
105
|
-
"bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
|
|
106
|
-
]),
|
|
107
|
-
edit: Object.freeze({
|
|
108
|
-
offset: 1_120_359,
|
|
109
|
-
absoluteOffset: 106_574_750,
|
|
110
|
-
length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
111
|
-
anchor: "47024(I,e,t){",
|
|
112
|
-
anchorCount: 1,
|
|
113
|
-
sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
|
|
114
|
-
patchedSha256: "873d2dd276bd12a7dc2876f5eaf8c67d4ae34749ead8911d0588badf4e9fc0cf",
|
|
115
|
-
predecessorSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
|
|
116
|
-
replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
|
|
117
|
-
}),
|
|
118
|
-
}),
|
|
119
|
-
]),
|
|
120
|
-
legacy: Object.freeze({
|
|
121
|
-
kind: "global-origin-replacement",
|
|
122
|
-
asarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
|
|
123
|
-
headerSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
|
|
124
|
-
}),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
|
|
128
|
-
id: "pm-studio-2.9.10-build-2.9.10",
|
|
129
|
-
version: "2.9.10",
|
|
130
|
-
build: "2.9.10",
|
|
131
|
-
bundleIdentifier: "com.pm-studio.app",
|
|
132
|
-
sourceTeamIdentifier: "HL75GKK4W4",
|
|
133
|
-
sourceArtifact: Object.freeze({
|
|
134
|
-
releaseUrl: "https://github.com/gim-home/max-studio/releases/tag/v2.9.10",
|
|
135
|
-
asset: "PM-Studio-2.9.10-mac-arm64.zip",
|
|
136
|
-
sha256: "85654e6ed173ce2565b5ef3694137de2c5f92eba1b749316c9e5b63181ccc3b0",
|
|
137
|
-
}),
|
|
138
|
-
sourceBundleContent: Object.freeze({
|
|
139
|
-
scheme: "ccdx-bundle-content-v2",
|
|
140
|
-
sha256: "478ca7f1f0826b07b7706fd1f410dee01b6dbaae21c40c63ecbdc0942eab63d9",
|
|
141
|
-
entryCount: 1_521,
|
|
142
|
-
regularFileCount: 1_013,
|
|
143
|
-
regularBytes: 494_995_602,
|
|
144
|
-
symlinkCount: 14,
|
|
145
|
-
xattrCount: 735,
|
|
146
|
-
ignoredXattrs: Object.freeze([
|
|
147
|
-
"com.apple.macl",
|
|
148
|
-
"com.apple.provenance",
|
|
149
|
-
"com.apple.quarantine",
|
|
150
|
-
]),
|
|
151
|
-
}),
|
|
152
|
-
sourceCodeSignature: Object.freeze({
|
|
153
|
-
identifier: "com.pm-studio.app",
|
|
154
|
-
teamIdentifier: "HL75GKK4W4",
|
|
155
|
-
flags: "0x10000(runtime)",
|
|
156
|
-
runtimeVersion: "26.0.0",
|
|
157
|
-
cdHashFull: "86a4d64946123d21d20918041e88f58e8c497ce1a1f75a9f9d8cd592ce86e158",
|
|
158
|
-
notarizationTicket: "stapled",
|
|
159
|
-
}),
|
|
160
|
-
patchedSigningMetadata: Object.freeze({
|
|
161
|
-
identifier: "com.pm-studio.app",
|
|
162
|
-
flags: "0x10002(adhoc,runtime)",
|
|
163
|
-
runtime_version: "26.0.0",
|
|
164
|
-
entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
|
|
165
|
-
}),
|
|
166
|
-
executable: "PM Studio",
|
|
167
|
-
sourceExecutableSha256: "a38f2054abb9770ba35d054d447be0de8e7c9d4fed6b37486bf15cf28de1e122",
|
|
168
|
-
electronFrameworkPath: "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
|
|
169
|
-
sourceElectronFrameworkSha256: "a562a31f8f8d14440974466e5e611b19d93705dcf3ca2f57647578ffc002f5b4",
|
|
170
|
-
embeddedAsarIntegrity: "absent",
|
|
171
|
-
asarPath: "Contents/Resources/app.asar",
|
|
172
|
-
infoPlistPath: "Contents/Info.plist",
|
|
173
|
-
integrityKey: "ElectronAsarIntegrity.Resources/app.asar",
|
|
174
|
-
dataOffset: 3_833_292,
|
|
175
|
-
sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
|
|
176
|
-
sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
|
|
177
|
-
patchedAsarSha256: "f309fc7e86bb4edaf1fecd9061fb4ddc634357c0ba9299f1d13f8b2ff33efd90",
|
|
178
|
-
patchedHeaderSha256: "98c4af8656e61cc1a1a8b2188592a499a987136f8e6b763b0e83abda788e798d",
|
|
179
|
-
predecessor: Object.freeze({
|
|
180
|
-
id: "split-origin-v1-model-discovery-750ms",
|
|
181
|
-
kind: "split-origin-predecessor",
|
|
182
|
-
asarSha256: "ea28d998056b32ca4115d208a4d81ce629c83f3f5f89c6a66d50749849beb6fc",
|
|
183
|
-
headerSha256: "348d80f5f2d1ebb92e6089195b1703b87467678f80d8946b8ab83b3ead03e5be",
|
|
184
|
-
signingMetadata: Object.freeze({
|
|
185
|
-
identifier: "com.pm-studio.app",
|
|
186
|
-
flags: "0x10002(adhoc,runtime)",
|
|
187
|
-
runtime_version: "26.0.0",
|
|
188
|
-
entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
|
|
189
|
-
}),
|
|
190
|
-
}),
|
|
191
|
-
targets: Object.freeze([
|
|
192
|
-
Object.freeze({
|
|
193
|
-
path: "dist/main/main.js",
|
|
194
|
-
offset: 101_622_115,
|
|
195
|
-
size: 10_150_157,
|
|
196
|
-
blockSize: 4_194_304,
|
|
197
|
-
sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
|
|
198
|
-
patchedSha256: "71e936c79c3560ab5bd9813479f8a66f4a7b06629edca74953feaaf7904a6564",
|
|
199
|
-
predecessorSha256: "b5b5e37af3d06ffc603bdef620fc1e0254580f0ee325f1a77096af6c78379b79",
|
|
200
|
-
sourceBlocks: Object.freeze([
|
|
201
|
-
"3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
|
|
202
|
-
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
203
|
-
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
204
|
-
]),
|
|
205
|
-
patchedBlocks: Object.freeze([
|
|
206
|
-
"203ad1efbdcd869314510086632fdb0089196eb25179c46fb4108133cc65412f",
|
|
207
|
-
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
208
|
-
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
209
|
-
]),
|
|
210
|
-
predecessorBlocks: Object.freeze([
|
|
211
|
-
"63aa15d8c44614a7b2d9656ddba23109d40bab66b66b36339154953bd164da75",
|
|
212
|
-
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
213
|
-
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
214
|
-
]),
|
|
215
|
-
edit: Object.freeze({
|
|
216
|
-
offset: 1_241_223,
|
|
217
|
-
absoluteOffset: 106_696_630,
|
|
218
|
-
length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
|
|
219
|
-
anchor: "47024(I,e,t){",
|
|
220
|
-
anchorCount: 1,
|
|
221
|
-
sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
|
|
222
|
-
patchedSha256: "873d2dd276bd12a7dc2876f5eaf8c67d4ae34749ead8911d0588badf4e9fc0cf",
|
|
223
|
-
predecessorSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
|
|
224
|
-
replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
|
|
225
|
-
}),
|
|
226
|
-
}),
|
|
227
|
-
]),
|
|
228
|
-
legacy: Object.freeze({
|
|
229
|
-
kind: "global-origin-replacement",
|
|
230
|
-
asarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
|
|
231
|
-
headerSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
|
|
232
|
-
}),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
export const PM_STUDIO_RECIPES = Object.freeze([
|
|
236
|
-
PM_STUDIO_2_9_7_RECIPE,
|
|
237
|
-
PM_STUDIO_2_9_10_RECIPE,
|
|
238
|
-
]);
|
|
239
|
-
|
|
240
|
-
export function sha256Hex(value) {
|
|
241
|
-
return createHash("sha256").update(value).digest("hex");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function checkedBuffer(value) {
|
|
245
|
-
if (!Buffer.isBuffer(value)) throw new TypeError("ASAR input must be a Buffer");
|
|
246
|
-
return value;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function checkedUInt32(buffer, offset, name) {
|
|
250
|
-
if (offset < 0 || offset + 4 > buffer.length) throw new Error(`Invalid ASAR: missing ${name}`);
|
|
251
|
-
return buffer.readUInt32LE(offset);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function integer(value, name) {
|
|
255
|
-
const result = Number(value);
|
|
256
|
-
if (!Number.isSafeInteger(result) || result < 0) throw new Error(`Invalid ASAR ${name}: ${value}`);
|
|
257
|
-
return result;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function entryOffset(value, name) {
|
|
261
|
-
if (typeof value !== "string" || !/^(?:0|[1-9]\d*)$/.test(value)) {
|
|
262
|
-
throw new Error(`Invalid ASAR ${name}: ${value}`);
|
|
263
|
-
}
|
|
264
|
-
return integer(value, name);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function entrySize(value, name) {
|
|
268
|
-
if (typeof value !== "number") throw new Error(`Invalid ASAR ${name}: ${value}`);
|
|
269
|
-
return integer(value, name);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function aligned4(value) {
|
|
273
|
-
return (value + 3) & ~3;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const FILE_READ_CHUNK_BYTES = 1024 * 1024;
|
|
277
|
-
const PM_STUDIO_ASAR_HEADER_MAX_BYTES = 64 * 1024 * 1024;
|
|
278
|
-
|
|
279
|
-
function fileIdentity(filePath, stat) {
|
|
280
|
-
return [
|
|
281
|
-
filePath,
|
|
282
|
-
stat.dev,
|
|
283
|
-
stat.ino,
|
|
284
|
-
stat.size,
|
|
285
|
-
stat.mtimeNs,
|
|
286
|
-
stat.ctimeNs,
|
|
287
|
-
].join("\0");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function readExact(file, buffer, position, io) {
|
|
291
|
-
let offset = 0;
|
|
292
|
-
while (offset < buffer.length) {
|
|
293
|
-
const length = io.readSync(file, buffer, offset, buffer.length - offset, position + offset);
|
|
294
|
-
if (length === 0) throw new Error("Invalid ASAR: header is truncated");
|
|
295
|
-
offset += length;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function sameFileSnapshot(left, right) {
|
|
300
|
-
return left.dev === right.dev
|
|
301
|
-
&& left.ino === right.ino
|
|
302
|
-
&& left.size === right.size
|
|
303
|
-
&& left.mtimeNs === right.mtimeNs
|
|
304
|
-
&& left.ctimeNs === right.ctimeNs;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function withVerifiedFile(filePath, {
|
|
308
|
-
expectedIdentity = "",
|
|
309
|
-
io = fs,
|
|
310
|
-
label,
|
|
311
|
-
} = {}, read) {
|
|
312
|
-
const realPath = io.realpathSync(filePath);
|
|
313
|
-
const pathBefore = io.statSync(realPath, { bigint: true });
|
|
314
|
-
const file = io.openSync(realPath, io.constants.O_RDONLY | (io.constants.O_NOFOLLOW || 0));
|
|
315
|
-
try {
|
|
316
|
-
const before = io.fstatSync(file, { bigint: true });
|
|
317
|
-
const identity = fileIdentity(realPath, before);
|
|
318
|
-
if (!sameFileSnapshot(pathBefore, before) || (expectedIdentity && expectedIdentity !== identity)) {
|
|
319
|
-
throw new Error(`PM Studio ${label} changed before it could be read: ${filePath}`);
|
|
320
|
-
}
|
|
321
|
-
const result = read({ file, before, realPath });
|
|
322
|
-
const after = io.fstatSync(file, { bigint: true });
|
|
323
|
-
const pathAfter = io.statSync(realPath, { bigint: true });
|
|
324
|
-
if (!sameFileSnapshot(before, after) || !sameFileSnapshot(after, pathAfter)) {
|
|
325
|
-
throw new Error(`PM Studio ${label} changed while reading: ${filePath}`);
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
result,
|
|
329
|
-
realPath,
|
|
330
|
-
identity: fileIdentity(realPath, after),
|
|
331
|
-
};
|
|
332
|
-
} finally {
|
|
333
|
-
io.closeSync(file);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function populateAsarTargetBytes(evidence, targetPaths, io) {
|
|
338
|
-
const missing = targetPaths.filter((targetPath) => !evidence.parsed.targetBytes.has(targetPath));
|
|
339
|
-
withVerifiedFile(evidence.realPath, {
|
|
340
|
-
expectedIdentity: evidence.identity,
|
|
341
|
-
io,
|
|
342
|
-
label: "ASAR",
|
|
343
|
-
}, ({ file, before }) => {
|
|
344
|
-
for (const targetPath of missing) {
|
|
345
|
-
const entry = findAsarEntry(evidence.parsed.header, targetPath);
|
|
346
|
-
if (!entry || entry.unpacked) continue;
|
|
347
|
-
let offset;
|
|
348
|
-
let size;
|
|
349
|
-
try {
|
|
350
|
-
offset = entryOffset(entry.offset, `${targetPath} offset`);
|
|
351
|
-
size = entrySize(entry.size, `${targetPath} size`);
|
|
352
|
-
} catch {
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
if (size > PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES) {
|
|
356
|
-
throw new Error(`Invalid ASAR: ${targetPath} exceeds the safe inspection limit`);
|
|
357
|
-
}
|
|
358
|
-
const absoluteOffset = evidence.parsed.dataOffset + offset;
|
|
359
|
-
if (absoluteOffset + size > Number(before.size)) continue;
|
|
360
|
-
const bytes = Buffer.allocUnsafe(size);
|
|
361
|
-
readExact(file, bytes, absoluteOffset, io);
|
|
362
|
-
evidence.parsed.targetBytes.set(targetPath, bytes);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function readAsarFileEvidence(filePath, targetPaths, { cache, io = fs } = {}) {
|
|
368
|
-
const realPath = io.realpathSync(filePath);
|
|
369
|
-
const pathStat = io.statSync(realPath, { bigint: true });
|
|
370
|
-
const paths = [...new Set(targetPaths)].sort();
|
|
371
|
-
const cacheKey = `${fileIdentity(realPath, pathStat)}\0${paths.join("\0")}`;
|
|
372
|
-
if (cache?.has(cacheKey)) {
|
|
373
|
-
const evidence = cache.get(cacheKey);
|
|
374
|
-
populateAsarTargetBytes(evidence, paths, io);
|
|
375
|
-
return evidence;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const opened = withVerifiedFile(realPath, {
|
|
379
|
-
expectedIdentity: fileIdentity(realPath, pathStat),
|
|
380
|
-
io,
|
|
381
|
-
label: "ASAR",
|
|
382
|
-
}, ({ file, before }) => {
|
|
383
|
-
const prefix = Buffer.allocUnsafe(16);
|
|
384
|
-
readExact(file, prefix, 0, io);
|
|
385
|
-
const headerPickleSize = checkedUInt32(prefix, 4, "header pickle size");
|
|
386
|
-
const dataOffset = 8 + headerPickleSize;
|
|
387
|
-
if (dataOffset > PM_STUDIO_ASAR_HEADER_MAX_BYTES) {
|
|
388
|
-
throw new Error("Invalid ASAR: header exceeds the safe inspection limit");
|
|
389
|
-
}
|
|
390
|
-
if (dataOffset > Number(before.size)) {
|
|
391
|
-
throw new Error("Invalid ASAR: header extends outside the archive");
|
|
392
|
-
}
|
|
393
|
-
const headerRegion = Buffer.allocUnsafe(dataOffset);
|
|
394
|
-
readExact(file, headerRegion, 0, io);
|
|
395
|
-
const parsedHeader = parseAsarBuffer(headerRegion);
|
|
396
|
-
const targetRanges = new Map();
|
|
397
|
-
for (const targetPath of paths) {
|
|
398
|
-
const entry = findAsarEntry(parsedHeader.header, targetPath);
|
|
399
|
-
if (!entry || entry.unpacked) continue;
|
|
400
|
-
let offset;
|
|
401
|
-
let size;
|
|
402
|
-
try {
|
|
403
|
-
offset = entryOffset(entry.offset, `${targetPath} offset`);
|
|
404
|
-
size = entrySize(entry.size, `${targetPath} size`);
|
|
405
|
-
} catch {
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (size > PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES) {
|
|
409
|
-
throw new Error(`Invalid ASAR: ${targetPath} exceeds the safe inspection limit`);
|
|
410
|
-
}
|
|
411
|
-
const absoluteOffset = parsedHeader.dataOffset + offset;
|
|
412
|
-
if (absoluteOffset + size <= Number(before.size)) {
|
|
413
|
-
targetRanges.set(targetPath, { start: absoluteOffset, end: absoluteOffset + size, bytes: Buffer.allocUnsafe(size) });
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const hash = createHash("sha256");
|
|
418
|
-
const chunk = Buffer.allocUnsafe(FILE_READ_CHUNK_BYTES);
|
|
419
|
-
let position = 0;
|
|
420
|
-
for (;;) {
|
|
421
|
-
const length = io.readSync(file, chunk, 0, chunk.length, position);
|
|
422
|
-
if (length === 0) break;
|
|
423
|
-
const bytes = chunk.subarray(0, length);
|
|
424
|
-
hash.update(bytes);
|
|
425
|
-
const chunkEnd = position + length;
|
|
426
|
-
for (const range of targetRanges.values()) {
|
|
427
|
-
const start = Math.max(position, range.start);
|
|
428
|
-
const end = Math.min(chunkEnd, range.end);
|
|
429
|
-
if (start < end) bytes.copy(range.bytes, start - range.start, start - position, end - position);
|
|
430
|
-
}
|
|
431
|
-
position = chunkEnd;
|
|
432
|
-
}
|
|
433
|
-
if (position !== Number(before.size)) {
|
|
434
|
-
throw new Error(`PM Studio ASAR changed while reading: ${filePath}`);
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
parsed: {
|
|
438
|
-
...parsedHeader,
|
|
439
|
-
buffer: null,
|
|
440
|
-
headerBytes: null,
|
|
441
|
-
headerSha256: sha256Hex(parsedHeader.headerBytes),
|
|
442
|
-
archiveLength: Number(before.size),
|
|
443
|
-
targetBytes: new Map([...targetRanges].map(([name, range]) => [name, range.bytes])),
|
|
444
|
-
},
|
|
445
|
-
asarSha256: hash.digest("hex"),
|
|
446
|
-
};
|
|
447
|
-
});
|
|
448
|
-
const evidence = {
|
|
449
|
-
...opened.result,
|
|
450
|
-
realPath: opened.realPath,
|
|
451
|
-
identity: opened.identity,
|
|
452
|
-
};
|
|
453
|
-
cache?.set(cacheKey, evidence);
|
|
454
|
-
return evidence;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
export function parseAsarBuffer(value) {
|
|
458
|
-
const buffer = checkedBuffer(value);
|
|
459
|
-
if (buffer.length < 16) throw new Error("Invalid ASAR: header is truncated");
|
|
460
|
-
|
|
461
|
-
const sizePicklePayload = checkedUInt32(buffer, 0, "size pickle");
|
|
462
|
-
const headerPickleSize = checkedUInt32(buffer, 4, "header pickle size");
|
|
463
|
-
const headerPicklePayload = checkedUInt32(buffer, 8, "header pickle payload");
|
|
464
|
-
const headerLength = checkedUInt32(buffer, 12, "header string size");
|
|
465
|
-
const headerStart = 16;
|
|
466
|
-
const headerEnd = headerStart + headerLength;
|
|
467
|
-
const dataOffset = 8 + headerPickleSize;
|
|
468
|
-
|
|
469
|
-
if (sizePicklePayload !== 4
|
|
470
|
-
|| headerPickleSize < 8
|
|
471
|
-
|| headerPickleSize % 4 !== 0
|
|
472
|
-
|| headerPicklePayload !== headerPickleSize - 4
|
|
473
|
-
|| headerPicklePayload !== 4 + aligned4(headerLength)) {
|
|
474
|
-
throw new Error("Invalid ASAR: malformed pickle sizes");
|
|
475
|
-
}
|
|
476
|
-
if (headerEnd > dataOffset || dataOffset > buffer.length) {
|
|
477
|
-
throw new Error("Invalid ASAR: header extends outside the archive");
|
|
478
|
-
}
|
|
479
|
-
if (buffer.subarray(headerEnd, dataOffset).some((byte) => byte !== 0)) {
|
|
480
|
-
throw new Error("Invalid ASAR: header padding is not zeroed");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const headerBytes = buffer.subarray(headerStart, headerEnd);
|
|
484
|
-
let header;
|
|
485
|
-
try {
|
|
486
|
-
header = JSON.parse(headerBytes.toString("utf8"));
|
|
487
|
-
} catch {
|
|
488
|
-
throw new Error("Invalid ASAR: header JSON could not be parsed");
|
|
489
|
-
}
|
|
490
|
-
if (!header || typeof header !== "object" || !header.files || typeof header.files !== "object") {
|
|
491
|
-
throw new Error("Invalid ASAR: header has no files tree");
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return {
|
|
495
|
-
buffer,
|
|
496
|
-
header,
|
|
497
|
-
headerBytes,
|
|
498
|
-
headerLength,
|
|
499
|
-
headerStart,
|
|
500
|
-
headerEnd,
|
|
501
|
-
headerPickleSize,
|
|
502
|
-
dataOffset,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
export function findAsarEntry(header, filePath) {
|
|
507
|
-
const parts = String(filePath || "").split("/").filter(Boolean);
|
|
508
|
-
if (parts.length === 0) return null;
|
|
509
|
-
let node = { files: header?.files };
|
|
510
|
-
for (const part of parts) {
|
|
511
|
-
node = node?.files?.[part];
|
|
512
|
-
if (!node) return null;
|
|
513
|
-
}
|
|
514
|
-
return node;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function occurrencePositions(haystack, needle) {
|
|
518
|
-
const positions = [];
|
|
519
|
-
if (needle.length === 0) return positions;
|
|
520
|
-
let offset = 0;
|
|
521
|
-
while (offset <= haystack.length - needle.length) {
|
|
522
|
-
const position = haystack.indexOf(needle, offset);
|
|
523
|
-
if (position < 0) break;
|
|
524
|
-
positions.push(position);
|
|
525
|
-
offset = position + needle.length;
|
|
526
|
-
}
|
|
527
|
-
return positions;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function asciiDigit(byte) {
|
|
531
|
-
return byte >= 48 && byte <= 57;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function asciiIdentifierStart(byte) {
|
|
535
|
-
return byte === 36
|
|
536
|
-
|| byte === 95
|
|
537
|
-
|| (byte >= 65 && byte <= 90)
|
|
538
|
-
|| (byte >= 97 && byte <= 122);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function matchCopilotConfigAlphaTemplate(source, offset) {
|
|
542
|
-
let moduleIdEnd = offset;
|
|
543
|
-
while (moduleIdEnd < source.length && asciiDigit(source[moduleIdEnd])) moduleIdEnd += 1;
|
|
544
|
-
const moduleIdLength = moduleIdEnd - offset;
|
|
545
|
-
if (moduleIdLength === 0
|
|
546
|
-
|| moduleIdLength > 10
|
|
547
|
-
|| (moduleIdLength > 1 && source[offset] === 48)) return null;
|
|
548
|
-
const moduleId = source.subarray(offset, moduleIdEnd).toString("ascii");
|
|
549
|
-
if (!Number.isSafeInteger(Number(moduleId))) return null;
|
|
550
|
-
|
|
551
|
-
let cursor = moduleIdEnd;
|
|
552
|
-
const identifiers = new Map();
|
|
553
|
-
const reverseIdentifiers = new Map();
|
|
554
|
-
for (const part of PM_STUDIO_COPILOT_CONFIG_ALPHA_TEMPLATE) {
|
|
555
|
-
if (typeof part === "string") {
|
|
556
|
-
const literal = Buffer.from(part);
|
|
557
|
-
if (cursor + literal.length > source.length
|
|
558
|
-
|| !source.subarray(cursor, cursor + literal.length).equals(literal)) return null;
|
|
559
|
-
cursor += literal.length;
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (!asciiIdentifierStart(source[cursor])
|
|
564
|
-
|| asciiIdentifierStart(source[cursor + 1])
|
|
565
|
-
|| asciiDigit(source[cursor + 1])) return null;
|
|
566
|
-
const candidate = String.fromCharCode(source[cursor]);
|
|
567
|
-
// The audited module uses single-byte minifier names. Keeping that bound
|
|
568
|
-
// rejects capture-prone "semantic" guesses while allowing permutations.
|
|
569
|
-
const mapped = identifiers.get(part.identifier);
|
|
570
|
-
const reverse = reverseIdentifiers.get(candidate);
|
|
571
|
-
if ((mapped && mapped !== candidate) || (reverse && reverse !== part.identifier)) return null;
|
|
572
|
-
identifiers.set(part.identifier, candidate);
|
|
573
|
-
reverseIdentifiers.set(candidate, part.identifier);
|
|
574
|
-
cursor += 1;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (cursor < source.length && source[cursor] !== 44 && source[cursor] !== 125) return null;
|
|
578
|
-
return {
|
|
579
|
-
offset,
|
|
580
|
-
end: cursor,
|
|
581
|
-
length: cursor - offset,
|
|
582
|
-
moduleId,
|
|
583
|
-
anchor: `${moduleId}(`,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function locateCompatibleCopilotConfigModule(source) {
|
|
588
|
-
const sentinel = Buffer.from(PM_STUDIO_COPILOT_CONFIG_MODULE_SENTINEL);
|
|
589
|
-
const matches = new Map();
|
|
590
|
-
let sentinelCount = 0;
|
|
591
|
-
let searchOffset = 0;
|
|
592
|
-
while (searchOffset <= source.length - sentinel.length) {
|
|
593
|
-
const sentinelOffset = source.indexOf(sentinel, searchOffset);
|
|
594
|
-
if (sentinelOffset < 0) break;
|
|
595
|
-
sentinelCount += 1;
|
|
596
|
-
searchOffset = sentinelOffset + sentinel.length;
|
|
597
|
-
|
|
598
|
-
const lowerBound = Math.max(0, sentinelOffset - PM_STUDIO_COMPATIBLE_MODULE_PREFIX_MAX_BYTES);
|
|
599
|
-
for (let offset = lowerBound; offset <= sentinelOffset; offset += 1) {
|
|
600
|
-
if (!asciiDigit(source[offset]) || (offset > 0 && asciiDigit(source[offset - 1]))) continue;
|
|
601
|
-
if (offset > 0 && source[offset - 1] !== 44 && source[offset - 1] !== 123) continue;
|
|
602
|
-
const match = matchCopilotConfigAlphaTemplate(source, offset);
|
|
603
|
-
if (match && sentinelOffset >= match.offset && sentinelOffset < match.end) {
|
|
604
|
-
matches.set(match.offset, match);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (sentinelCount === 0) {
|
|
609
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has no compatible Copilot configuration sentinel`);
|
|
610
|
-
}
|
|
611
|
-
if (matches.size !== 1) {
|
|
612
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has ${matches.size} alpha-compatible Copilot configuration modules, expected exactly one`);
|
|
613
|
-
}
|
|
614
|
-
return matches.values().next().value;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
export function blockSha256(buffer, blockSize) {
|
|
618
|
-
const size = integer(blockSize, "block size");
|
|
619
|
-
if (size === 0) throw new Error("Invalid ASAR block size: 0");
|
|
620
|
-
const hashes = [];
|
|
621
|
-
if (buffer.length === 0) return [sha256Hex(buffer)];
|
|
622
|
-
for (let offset = 0; offset < buffer.length; offset += size) {
|
|
623
|
-
hashes.push(sha256Hex(buffer.subarray(offset, Math.min(offset + size, buffer.length))));
|
|
624
|
-
}
|
|
625
|
-
return hashes;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function incompatibleAsar(message) {
|
|
629
|
-
const error = new Error(message);
|
|
630
|
-
error.code = "PM_STUDIO_ASAR_INCOMPATIBLE";
|
|
631
|
-
return error;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function requiredRecipeString(value, name) {
|
|
635
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
636
|
-
throw incompatibleAsar(`PM Studio compatible recipe has no ${name}`);
|
|
637
|
-
}
|
|
638
|
-
return value;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function compatibleRecipeFromEvidence(parsed, sourceTarget, sourceAsarSha256, patchedAsarSha256For, options) {
|
|
642
|
-
const entry = findAsarEntry(parsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
643
|
-
if (!entry) throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is missing`);
|
|
644
|
-
|
|
645
|
-
let targetOffset;
|
|
646
|
-
let targetSize;
|
|
647
|
-
let blockSize;
|
|
648
|
-
try {
|
|
649
|
-
targetOffset = entryOffset(entry.offset, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} offset`);
|
|
650
|
-
targetSize = entrySize(entry.size, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} size`);
|
|
651
|
-
blockSize = integer(entry.integrity?.blockSize,
|
|
652
|
-
`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} block size`);
|
|
653
|
-
} catch (error) {
|
|
654
|
-
throw incompatibleAsar(error.message);
|
|
655
|
-
}
|
|
656
|
-
const absoluteOffset = parsed.dataOffset + targetOffset;
|
|
657
|
-
const archiveLength = parsed.archiveLength ?? parsed.buffer.length;
|
|
658
|
-
if (entry.unpacked || entry.link || absoluteOffset + targetSize > archiveLength) {
|
|
659
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is not one packed regular ASAR entry`);
|
|
660
|
-
}
|
|
661
|
-
if (blockSize === 0
|
|
662
|
-
|| entry.integrity?.algorithm !== "SHA256"
|
|
663
|
-
|| !sameArray(Object.keys(entry.integrity || {}).sort(), ["algorithm", "blockSize", "blocks", "hash"])) {
|
|
664
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has unsupported integrity metadata`);
|
|
665
|
-
}
|
|
666
|
-
const expectedBlockCount = Math.ceil(targetSize / blockSize);
|
|
667
|
-
if (targetSize > PM_STUDIO_COMPATIBLE_TARGET_MAX_BYTES
|
|
668
|
-
|| blockSize < 16
|
|
669
|
-
|| expectedBlockCount > PM_STUDIO_COMPATIBLE_BLOCK_MAX_COUNT
|
|
670
|
-
|| entry.integrity.blocks.length !== expectedBlockCount) {
|
|
671
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} has unsafe integrity block geometry`);
|
|
672
|
-
}
|
|
673
|
-
if (!sourceTarget || sourceTarget.length !== targetSize) {
|
|
674
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} is not one packed regular ASAR entry`);
|
|
675
|
-
}
|
|
676
|
-
const sourceTargetSha256 = sha256Hex(sourceTarget);
|
|
677
|
-
const sourceBlocks = blockSha256(sourceTarget, blockSize);
|
|
678
|
-
if (entry.integrity.hash !== sourceTargetSha256
|
|
679
|
-
|| !sameArray(entry.integrity.blocks, sourceBlocks)) {
|
|
680
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} integrity metadata does not match its bytes`);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const compatibleModule = locateCompatibleCopilotConfigModule(sourceTarget);
|
|
684
|
-
const editOffset = compatibleModule.offset;
|
|
685
|
-
const editEnd = compatibleModule.end;
|
|
686
|
-
const sourceEdit = sourceTarget.subarray(editOffset, editEnd);
|
|
687
|
-
const anchorPositions = occurrencePositions(sourceTarget, Buffer.from(compatibleModule.anchor));
|
|
688
|
-
if (anchorPositions.length !== 1 || anchorPositions[0] !== editOffset) {
|
|
689
|
-
throw incompatibleAsar(`${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} compatible module header is not unique`);
|
|
690
|
-
}
|
|
691
|
-
let replacementSource;
|
|
692
|
-
try {
|
|
693
|
-
replacementSource = splitOriginConfigModule(compatibleModule.moduleId, compatibleModule.length);
|
|
694
|
-
} catch (error) {
|
|
695
|
-
throw incompatibleAsar(error.message);
|
|
696
|
-
}
|
|
697
|
-
const replacement = Buffer.from(replacementSource);
|
|
698
|
-
if (replacement.length !== compatibleModule.length) {
|
|
699
|
-
throw incompatibleAsar("PM Studio split-origin replacement does not preserve the compatible module size");
|
|
700
|
-
}
|
|
701
|
-
const patchedTarget = Buffer.from(sourceTarget);
|
|
702
|
-
replacement.copy(patchedTarget, editOffset);
|
|
703
|
-
const patchedTargetSha256 = sha256Hex(patchedTarget);
|
|
704
|
-
const patchedBlocks = blockSha256(patchedTarget, blockSize);
|
|
705
|
-
const patchedHeader = JSON.parse(JSON.stringify(parsed.header));
|
|
706
|
-
const patchedEntry = findAsarEntry(patchedHeader, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
707
|
-
patchedEntry.integrity = {
|
|
708
|
-
...patchedEntry.integrity,
|
|
709
|
-
algorithm: "SHA256",
|
|
710
|
-
hash: patchedTargetSha256,
|
|
711
|
-
blockSize,
|
|
712
|
-
blocks: [...patchedBlocks],
|
|
713
|
-
};
|
|
714
|
-
const patchedHeaderBytes = Buffer.from(JSON.stringify(patchedHeader));
|
|
715
|
-
if (patchedHeaderBytes.length !== parsed.headerLength) {
|
|
716
|
-
throw incompatibleAsar(`Compatible ASAR header changed size (${parsed.headerLength} -> ${patchedHeaderBytes.length})`);
|
|
717
|
-
}
|
|
718
|
-
const patchedAsarSha256 = patchedAsarSha256For({
|
|
719
|
-
absoluteEditOffset: absoluteOffset + editOffset,
|
|
720
|
-
patchedHeaderBytes,
|
|
721
|
-
replacement,
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
const version = requiredRecipeString(String(options.version || ""), "version");
|
|
725
|
-
const build = requiredRecipeString(String(options.build || ""), "build");
|
|
726
|
-
const bundleIdentifier = requiredRecipeString(options.bundleIdentifier, "bundle identifier");
|
|
727
|
-
const identity = sha256Hex(`${PM_STUDIO_COMPATIBILITY_ID}\0${bundleIdentifier}\0${version}\0${build}\0${sourceAsarSha256}`).slice(0, 24);
|
|
728
|
-
const sourceBundleContent = options.sourceBundleContent
|
|
729
|
-
? Object.freeze({
|
|
730
|
-
...options.sourceBundleContent,
|
|
731
|
-
ignoredXattrs: Object.freeze([...(options.sourceBundleContent.ignoredXattrs || [])]),
|
|
732
|
-
})
|
|
733
|
-
: undefined;
|
|
734
|
-
const edit = Object.freeze({
|
|
735
|
-
offset: editOffset,
|
|
736
|
-
absoluteOffset: absoluteOffset + editOffset,
|
|
737
|
-
length: compatibleModule.length,
|
|
738
|
-
anchor: compatibleModule.anchor,
|
|
739
|
-
anchorCount: 1,
|
|
740
|
-
sourceSha256: sha256Hex(sourceEdit),
|
|
741
|
-
patchedSha256: sha256Hex(replacement),
|
|
742
|
-
replacement: replacementSource,
|
|
743
|
-
});
|
|
744
|
-
const target = Object.freeze({
|
|
745
|
-
path: PM_STUDIO_COPILOT_CONFIG_MODULE_PATH,
|
|
746
|
-
offset: targetOffset,
|
|
747
|
-
size: targetSize,
|
|
748
|
-
blockSize,
|
|
749
|
-
sourceSha256: sourceTargetSha256,
|
|
750
|
-
patchedSha256: patchedTargetSha256,
|
|
751
|
-
sourceBlocks: Object.freeze([...sourceBlocks]),
|
|
752
|
-
patchedBlocks: Object.freeze([...patchedBlocks]),
|
|
753
|
-
edit,
|
|
754
|
-
});
|
|
755
|
-
return Object.freeze({
|
|
756
|
-
id: `pm-studio-compatible-${identity}`,
|
|
757
|
-
compatibility: PM_STUDIO_COMPATIBILITY_ID,
|
|
758
|
-
version,
|
|
759
|
-
build,
|
|
760
|
-
bundleIdentifier,
|
|
761
|
-
sourceTeamIdentifier: requiredRecipeString(options.sourceTeamIdentifier, "source TeamIdentifier"),
|
|
762
|
-
executable: requiredRecipeString(options.executable, "executable"),
|
|
763
|
-
sourceExecutableSha256: requiredRecipeString(options.sourceExecutableSha256, "source executable SHA-256"),
|
|
764
|
-
electronFrameworkPath: requiredRecipeString(options.electronFrameworkPath, "Electron Framework path"),
|
|
765
|
-
sourceElectronFrameworkSha256: requiredRecipeString(
|
|
766
|
-
options.sourceElectronFrameworkSha256,
|
|
767
|
-
"source Electron Framework SHA-256",
|
|
768
|
-
),
|
|
769
|
-
embeddedAsarIntegrity: requiredRecipeString(options.embeddedAsarIntegrity, "embedded ASAR integrity state"),
|
|
770
|
-
asarPath: requiredRecipeString(options.asarPath, "ASAR path"),
|
|
771
|
-
infoPlistPath: requiredRecipeString(options.infoPlistPath, "Info.plist path"),
|
|
772
|
-
integrityKey: requiredRecipeString(options.integrityKey, "ASAR integrity key"),
|
|
773
|
-
dataOffset: parsed.dataOffset,
|
|
774
|
-
sourceAsarSha256,
|
|
775
|
-
sourceHeaderSha256: parsed.headerSha256 || sha256Hex(parsed.headerBytes),
|
|
776
|
-
patchedAsarSha256,
|
|
777
|
-
patchedHeaderSha256: sha256Hex(patchedHeaderBytes),
|
|
778
|
-
...(sourceBundleContent ? { sourceBundleContent } : {}),
|
|
779
|
-
targets: Object.freeze([target]),
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export function createCompatiblePmStudioRecipe(value, options = {}) {
|
|
784
|
-
const parsed = parseAsarBuffer(value);
|
|
785
|
-
const entry = findAsarEntry(parsed.header, PM_STUDIO_COPILOT_CONFIG_MODULE_PATH);
|
|
786
|
-
let sourceTarget = null;
|
|
787
|
-
try {
|
|
788
|
-
const absoluteOffset = parsed.dataOffset
|
|
789
|
-
+ entryOffset(entry?.offset, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} offset`);
|
|
790
|
-
const targetSize = entrySize(entry?.size, `${PM_STUDIO_COPILOT_CONFIG_MODULE_PATH} size`);
|
|
791
|
-
if (entry && !entry.unpacked && absoluteOffset + targetSize <= parsed.buffer.length) {
|
|
792
|
-
sourceTarget = parsed.buffer.subarray(absoluteOffset, absoluteOffset + targetSize);
|
|
793
|
-
}
|
|
794
|
-
} catch {
|
|
795
|
-
// The shared validator below preserves the existing classified error.
|
|
796
|
-
}
|
|
797
|
-
return compatibleRecipeFromEvidence(
|
|
798
|
-
parsed,
|
|
799
|
-
sourceTarget,
|
|
800
|
-
sha256Hex(parsed.buffer),
|
|
801
|
-
({ absoluteEditOffset, patchedHeaderBytes, replacement }) => {
|
|
802
|
-
const patchedAsar = Buffer.from(parsed.buffer);
|
|
803
|
-
replacement.copy(patchedAsar, absoluteEditOffset);
|
|
804
|
-
patchedHeaderBytes.copy(patchedAsar, parsed.headerStart);
|
|
805
|
-
return sha256Hex(patchedAsar);
|
|
806
|
-
},
|
|
807
|
-
options,
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function hashFileWithReplacements(filePath, expectedIdentity, replacements, io = fs) {
|
|
812
|
-
return withVerifiedFile(filePath, {
|
|
813
|
-
expectedIdentity,
|
|
814
|
-
io,
|
|
815
|
-
label: "ASAR",
|
|
816
|
-
}, ({ file, before }) => {
|
|
817
|
-
const ordered = [...replacements].sort((left, right) => left.start - right.start);
|
|
818
|
-
let cursor = 0;
|
|
819
|
-
for (const item of ordered) {
|
|
820
|
-
if (item.start < cursor || item.end < item.start || item.end > Number(before.size)
|
|
821
|
-
|| item.bytes.length !== item.end - item.start) {
|
|
822
|
-
throw new Error("Invalid patched ASAR hash replacement range");
|
|
823
|
-
}
|
|
824
|
-
cursor = item.end;
|
|
825
|
-
}
|
|
826
|
-
const hash = createHash("sha256");
|
|
827
|
-
const chunk = Buffer.allocUnsafe(FILE_READ_CHUNK_BYTES);
|
|
828
|
-
const hashRange = (start, end) => {
|
|
829
|
-
let position = start;
|
|
830
|
-
while (position < end) {
|
|
831
|
-
const requested = Math.min(chunk.length, end - position);
|
|
832
|
-
const length = io.readSync(file, chunk, 0, requested, position);
|
|
833
|
-
if (length === 0) throw new Error(`PM Studio ASAR changed while reading: ${filePath}`);
|
|
834
|
-
hash.update(chunk.subarray(0, length));
|
|
835
|
-
position += length;
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
cursor = 0;
|
|
839
|
-
for (const item of ordered) {
|
|
840
|
-
hashRange(cursor, item.start);
|
|
841
|
-
hash.update(item.bytes);
|
|
842
|
-
cursor = item.end;
|
|
843
|
-
}
|
|
844
|
-
hashRange(cursor, Number(before.size));
|
|
845
|
-
return hash.digest("hex");
|
|
846
|
-
}).result;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
export function createCompatiblePmStudioRecipeFromFile(filePath, options = {}, fileOptions = {}) {
|
|
850
|
-
const evidence = readAsarFileEvidence(filePath, [PM_STUDIO_COPILOT_CONFIG_MODULE_PATH], fileOptions);
|
|
851
|
-
try {
|
|
852
|
-
return compatibleRecipeFromEvidence(
|
|
853
|
-
evidence.parsed,
|
|
854
|
-
evidence.parsed.targetBytes.get(PM_STUDIO_COPILOT_CONFIG_MODULE_PATH),
|
|
855
|
-
evidence.asarSha256,
|
|
856
|
-
({ absoluteEditOffset, patchedHeaderBytes, replacement }) => hashFileWithReplacements(
|
|
857
|
-
evidence.realPath,
|
|
858
|
-
evidence.identity,
|
|
859
|
-
[
|
|
860
|
-
{
|
|
861
|
-
start: evidence.parsed.headerStart,
|
|
862
|
-
end: evidence.parsed.headerEnd,
|
|
863
|
-
bytes: patchedHeaderBytes,
|
|
864
|
-
},
|
|
865
|
-
{
|
|
866
|
-
start: absoluteEditOffset,
|
|
867
|
-
end: absoluteEditOffset + replacement.length,
|
|
868
|
-
bytes: replacement,
|
|
869
|
-
},
|
|
870
|
-
],
|
|
871
|
-
fileOptions.io || fs,
|
|
872
|
-
),
|
|
873
|
-
options,
|
|
874
|
-
);
|
|
875
|
-
} finally {
|
|
876
|
-
if (fileOptions.releaseTargets) evidence.parsed.targetBytes.clear();
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
export function inspectElectronAsarIntegritySlots(value) {
|
|
881
|
-
const buffer = checkedBuffer(value);
|
|
882
|
-
const sentinel = Buffer.from(ELECTRON_ASAR_INTEGRITY_SENTINEL);
|
|
883
|
-
const slots = [];
|
|
884
|
-
let searchOffset = 0;
|
|
885
|
-
let truncated = false;
|
|
886
|
-
while (searchOffset <= buffer.length - sentinel.length) {
|
|
887
|
-
const offset = buffer.indexOf(sentinel, searchOffset);
|
|
888
|
-
if (offset < 0) break;
|
|
889
|
-
if (offset + 66 > buffer.length) {
|
|
890
|
-
truncated = true;
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
const used = buffer[offset + 32];
|
|
894
|
-
const version = buffer[offset + 33];
|
|
895
|
-
slots.push({
|
|
896
|
-
offset,
|
|
897
|
-
used,
|
|
898
|
-
version,
|
|
899
|
-
digest: buffer.subarray(offset + 34, offset + 66).toString("hex"),
|
|
900
|
-
});
|
|
901
|
-
searchOffset = offset + sentinel.length;
|
|
902
|
-
}
|
|
903
|
-
return electronSlotResult(slots, truncated);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function electronSlotResult(slots, truncated) {
|
|
907
|
-
const malformed = truncated
|
|
908
|
-
|| slots.length > 1
|
|
909
|
-
|| slots.some((slot) => ![0, 1].includes(slot.used) || (slot.used === 1 && slot.version !== 1));
|
|
910
|
-
const active = slots.some((slot) => slot.used === 1);
|
|
911
|
-
return {
|
|
912
|
-
state: malformed ? "drift" : active ? "active" : slots.length === 1 ? "inactive" : "absent",
|
|
913
|
-
active,
|
|
914
|
-
supported: !malformed && !active,
|
|
915
|
-
slots,
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
export function inspectElectronAsarIntegrityFile(filePath, {
|
|
920
|
-
cache,
|
|
921
|
-
io = fs,
|
|
922
|
-
chunkSize = FILE_READ_CHUNK_BYTES,
|
|
923
|
-
} = {}) {
|
|
924
|
-
if (!Number.isSafeInteger(chunkSize) || chunkSize < 66) {
|
|
925
|
-
throw new TypeError("Electron integrity file chunk size must be at least 66 bytes");
|
|
926
|
-
}
|
|
927
|
-
const realPath = io.realpathSync(filePath);
|
|
928
|
-
const pathStat = io.statSync(realPath, { bigint: true });
|
|
929
|
-
const cacheKey = `electron\0${fileIdentity(realPath, pathStat)}`;
|
|
930
|
-
if (cache?.has(cacheKey)) {
|
|
931
|
-
withVerifiedFile(realPath, {
|
|
932
|
-
expectedIdentity: fileIdentity(realPath, pathStat),
|
|
933
|
-
io,
|
|
934
|
-
label: "executable",
|
|
935
|
-
}, () => {});
|
|
936
|
-
return cache.get(cacheKey);
|
|
937
|
-
}
|
|
938
|
-
const opened = withVerifiedFile(realPath, {
|
|
939
|
-
expectedIdentity: fileIdentity(realPath, pathStat),
|
|
940
|
-
io,
|
|
941
|
-
label: "executable",
|
|
942
|
-
}, ({ file, before }) => {
|
|
943
|
-
const sentinel = Buffer.from(ELECTRON_ASAR_INTEGRITY_SENTINEL);
|
|
944
|
-
const hash = createHash("sha256");
|
|
945
|
-
const chunk = Buffer.allocUnsafe(chunkSize);
|
|
946
|
-
const slots = [];
|
|
947
|
-
let tail = Buffer.alloc(0);
|
|
948
|
-
let position = 0;
|
|
949
|
-
let nextSearchOffset = 0;
|
|
950
|
-
for (;;) {
|
|
951
|
-
const length = io.readSync(file, chunk, 0, chunk.length, position);
|
|
952
|
-
if (length === 0) break;
|
|
953
|
-
const bytes = chunk.subarray(0, length);
|
|
954
|
-
hash.update(bytes);
|
|
955
|
-
const scan = tail.length ? Buffer.concat([tail, bytes]) : bytes;
|
|
956
|
-
const scanOffset = position - tail.length;
|
|
957
|
-
let localOffset = Math.max(0, nextSearchOffset - scanOffset);
|
|
958
|
-
while (localOffset <= scan.length - sentinel.length) {
|
|
959
|
-
const found = scan.indexOf(sentinel, localOffset);
|
|
960
|
-
if (found < 0) break;
|
|
961
|
-
const absoluteOffset = scanOffset + found;
|
|
962
|
-
if (found + 66 > scan.length) break;
|
|
963
|
-
slots.push({
|
|
964
|
-
offset: absoluteOffset,
|
|
965
|
-
used: scan[found + 32],
|
|
966
|
-
version: scan[found + 33],
|
|
967
|
-
digest: scan.subarray(found + 34, found + 66).toString("hex"),
|
|
968
|
-
});
|
|
969
|
-
nextSearchOffset = absoluteOffset + sentinel.length;
|
|
970
|
-
localOffset = nextSearchOffset - scanOffset;
|
|
971
|
-
}
|
|
972
|
-
tail = Buffer.from(scan.subarray(Math.max(0, scan.length - 65)));
|
|
973
|
-
position += length;
|
|
974
|
-
}
|
|
975
|
-
if (position !== Number(before.size)) {
|
|
976
|
-
throw new Error(`PM Studio executable changed while reading: ${filePath}`);
|
|
977
|
-
}
|
|
978
|
-
let truncated = false;
|
|
979
|
-
const tailOffset = position - tail.length;
|
|
980
|
-
let localOffset = Math.max(0, nextSearchOffset - tailOffset);
|
|
981
|
-
while (localOffset <= tail.length - sentinel.length) {
|
|
982
|
-
const found = tail.indexOf(sentinel, localOffset);
|
|
983
|
-
if (found < 0) break;
|
|
984
|
-
if (found + 66 > tail.length) {
|
|
985
|
-
truncated = true;
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
localOffset = found + sentinel.length;
|
|
989
|
-
}
|
|
990
|
-
return {
|
|
991
|
-
sha256: hash.digest("hex"),
|
|
992
|
-
integrity: electronSlotResult(slots, truncated),
|
|
993
|
-
};
|
|
994
|
-
});
|
|
995
|
-
cache?.set(cacheKey, opened.result);
|
|
996
|
-
return opened.result;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function sameArray(actual, expected) {
|
|
1000
|
-
return Array.isArray(actual)
|
|
1001
|
-
&& Array.isArray(expected)
|
|
1002
|
-
&& actual.length === expected.length
|
|
1003
|
-
&& actual.every((value, index) => value === expected[index]);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function addMismatch(issues, label, actual, expected) {
|
|
1007
|
-
if (actual !== expected) issues.push(`${label}: expected ${expected}, got ${actual}`);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function inspectTarget(parsed, target) {
|
|
1011
|
-
const entry = findAsarEntry(parsed.header, target.path);
|
|
1012
|
-
if (!entry) return { path: target.path, missing: true };
|
|
1013
|
-
|
|
1014
|
-
let offset;
|
|
1015
|
-
let size;
|
|
1016
|
-
try {
|
|
1017
|
-
offset = entryOffset(entry.offset, `${target.path} offset`);
|
|
1018
|
-
size = entrySize(entry.size, `${target.path} size`);
|
|
1019
|
-
} catch (error) {
|
|
1020
|
-
return { path: target.path, malformed: error.message };
|
|
1021
|
-
}
|
|
1022
|
-
const absoluteOffset = parsed.dataOffset + offset;
|
|
1023
|
-
const archiveLength = parsed.archiveLength ?? parsed.buffer.length;
|
|
1024
|
-
if (entry.unpacked || absoluteOffset + size > archiveLength) {
|
|
1025
|
-
return { path: target.path, offset, size, absoluteOffset, malformed: "entry data is outside the packed archive" };
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
const bytes = parsed.targetBytes?.get(target.path)
|
|
1029
|
-
|| parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
|
|
1030
|
-
let edit = null;
|
|
1031
|
-
if (target.edit) {
|
|
1032
|
-
let editOffset;
|
|
1033
|
-
let editLength;
|
|
1034
|
-
try {
|
|
1035
|
-
editOffset = integer(target.edit.offset, `${target.path} edit offset`);
|
|
1036
|
-
editLength = integer(target.edit.length, `${target.path} edit length`);
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
return { path: target.path, offset, size, absoluteOffset, malformed: error.message };
|
|
1039
|
-
}
|
|
1040
|
-
if (editLength === 0 || editOffset + editLength > bytes.length) {
|
|
1041
|
-
return { path: target.path, offset, size, absoluteOffset, malformed: "edit window is outside the target" };
|
|
1042
|
-
}
|
|
1043
|
-
const anchor = Buffer.from(target.edit.anchor || "");
|
|
1044
|
-
edit = {
|
|
1045
|
-
offset: editOffset,
|
|
1046
|
-
absoluteOffset: absoluteOffset + editOffset,
|
|
1047
|
-
length: editLength,
|
|
1048
|
-
sha256: sha256Hex(bytes.subarray(editOffset, editOffset + editLength)),
|
|
1049
|
-
anchorPositions: occurrencePositions(bytes, anchor),
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
return {
|
|
1053
|
-
path: target.path,
|
|
1054
|
-
offset,
|
|
1055
|
-
size,
|
|
1056
|
-
absoluteOffset,
|
|
1057
|
-
sha256: sha256Hex(bytes),
|
|
1058
|
-
blockSize: entry.integrity?.blockSize,
|
|
1059
|
-
blocks: blockSha256(bytes, target.blockSize),
|
|
1060
|
-
integrity: entry.integrity || null,
|
|
1061
|
-
edit,
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function targetStateIssues(detail, target, state) {
|
|
1066
|
-
const issues = [];
|
|
1067
|
-
if (detail.missing) return [`${target.path}: entry is missing`];
|
|
1068
|
-
if (detail.malformed) return [`${target.path}: ${detail.malformed}`];
|
|
1069
|
-
|
|
1070
|
-
addMismatch(issues, `${target.path} offset`, detail.offset, target.offset);
|
|
1071
|
-
addMismatch(issues, `${target.path} size`, detail.size, target.size);
|
|
1072
|
-
addMismatch(issues, `${target.path} integrity algorithm`,
|
|
1073
|
-
detail.integrity?.algorithm, "SHA256");
|
|
1074
|
-
addMismatch(issues, `${target.path} block size`, detail.integrity?.blockSize, target.blockSize);
|
|
1075
|
-
|
|
1076
|
-
const patched = state === "patched";
|
|
1077
|
-
const predecessor = state === "predecessor";
|
|
1078
|
-
const expectedHash = patched
|
|
1079
|
-
? target.patchedSha256
|
|
1080
|
-
: predecessor ? target.predecessorSha256 : target.sourceSha256;
|
|
1081
|
-
const expectedBlocks = patched
|
|
1082
|
-
? target.patchedBlocks
|
|
1083
|
-
: predecessor ? target.predecessorBlocks : target.sourceBlocks;
|
|
1084
|
-
|
|
1085
|
-
addMismatch(issues, `${target.path} SHA-256`, detail.sha256, expectedHash);
|
|
1086
|
-
addMismatch(issues, `${target.path} integrity hash`, detail.integrity?.hash, expectedHash);
|
|
1087
|
-
if (!sameArray(detail.blocks, expectedBlocks)) issues.push(`${target.path} computed block hashes do not match ${state} recipe`);
|
|
1088
|
-
if (!sameArray(detail.integrity?.blocks, expectedBlocks)) issues.push(`${target.path} header block hashes do not match ${state} recipe`);
|
|
1089
|
-
if (target.edit) {
|
|
1090
|
-
addMismatch(issues, `${target.path} edit offset`, detail.edit?.offset, target.edit.offset);
|
|
1091
|
-
addMismatch(issues, `${target.path} absolute edit offset`, detail.edit?.absoluteOffset, target.edit.absoluteOffset);
|
|
1092
|
-
addMismatch(issues, `${target.path} edit length`, detail.edit?.length, target.edit.length);
|
|
1093
|
-
addMismatch(issues, `${target.path} edit SHA-256`, detail.edit?.sha256,
|
|
1094
|
-
patched
|
|
1095
|
-
? target.edit.patchedSha256
|
|
1096
|
-
: predecessor ? target.edit.predecessorSha256 : target.edit.sourceSha256);
|
|
1097
|
-
if (!sameArray(detail.edit?.anchorPositions, [target.edit.offset])) {
|
|
1098
|
-
issues.push(`${target.path} module anchor position/count does not match recipe`);
|
|
1099
|
-
}
|
|
1100
|
-
if (detail.edit?.anchorPositions?.length !== target.edit.anchorCount) {
|
|
1101
|
-
issues.push(`${target.path} module anchor count is ${detail.edit?.anchorPositions?.length || 0}, expected ${target.edit.anchorCount}`);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
return issues;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
1108
|
-
const parsed = parseAsarBuffer(value);
|
|
1109
|
-
const asarSha256 = sha256Hex(parsed.buffer);
|
|
1110
|
-
return inspectParsedAsar(parsed, asarSha256, recipe);
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
function inspectParsedAsar(parsed, asarSha256, recipe) {
|
|
1114
|
-
const headerSha256 = parsed.headerSha256 || sha256Hex(parsed.headerBytes);
|
|
1115
|
-
const targets = recipe.targets.map((target) => inspectTarget(parsed, target));
|
|
1116
|
-
const commonIssues = [];
|
|
1117
|
-
addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
|
|
1118
|
-
|
|
1119
|
-
const issuesFor = (state) => {
|
|
1120
|
-
const patched = state === "patched";
|
|
1121
|
-
const predecessor = state === "predecessor";
|
|
1122
|
-
const expected = predecessor ? recipe.predecessor : null;
|
|
1123
|
-
const issues = [...commonIssues];
|
|
1124
|
-
addMismatch(issues, "ASAR SHA-256", asarSha256,
|
|
1125
|
-
patched ? recipe.patchedAsarSha256 : predecessor ? expected?.asarSha256 : recipe.sourceAsarSha256);
|
|
1126
|
-
addMismatch(issues, "ASAR header SHA-256", headerSha256,
|
|
1127
|
-
patched ? recipe.patchedHeaderSha256 : predecessor ? expected?.headerSha256 : recipe.sourceHeaderSha256);
|
|
1128
|
-
for (let index = 0; index < recipe.targets.length; index += 1) {
|
|
1129
|
-
issues.push(...targetStateIssues(targets[index], recipe.targets[index], state));
|
|
1130
|
-
}
|
|
1131
|
-
return issues;
|
|
1132
|
-
};
|
|
1133
|
-
|
|
1134
|
-
const cleanIssues = issuesFor("clean");
|
|
1135
|
-
const patchedIssues = issuesFor("patched");
|
|
1136
|
-
const predecessorIssues = recipe.predecessor
|
|
1137
|
-
? issuesFor("predecessor")
|
|
1138
|
-
: ["recipe has no recognized predecessor patch"];
|
|
1139
|
-
const legacyIssues = [...commonIssues];
|
|
1140
|
-
if (!recipe.legacy) {
|
|
1141
|
-
legacyIssues.push("recipe has no recognized legacy patch");
|
|
1142
|
-
} else {
|
|
1143
|
-
addMismatch(legacyIssues, "ASAR SHA-256", asarSha256, recipe.legacy.asarSha256);
|
|
1144
|
-
addMismatch(legacyIssues, "ASAR header SHA-256", headerSha256, recipe.legacy.headerSha256);
|
|
1145
|
-
}
|
|
1146
|
-
const state = cleanIssues.length === 0
|
|
1147
|
-
? "clean"
|
|
1148
|
-
: patchedIssues.length === 0
|
|
1149
|
-
? "patched"
|
|
1150
|
-
: predecessorIssues.length === 0
|
|
1151
|
-
? "predecessor"
|
|
1152
|
-
: legacyIssues.length === 0 ? "legacy" : "drift";
|
|
1153
|
-
return {
|
|
1154
|
-
recipeId: recipe.id,
|
|
1155
|
-
state,
|
|
1156
|
-
asarSha256,
|
|
1157
|
-
headerSha256,
|
|
1158
|
-
dataOffset: parsed.dataOffset,
|
|
1159
|
-
targets,
|
|
1160
|
-
cleanIssues,
|
|
1161
|
-
patchedIssues,
|
|
1162
|
-
predecessorIssues,
|
|
1163
|
-
legacyIssues,
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
export function inspectAsarFile(filePath, recipe = PM_STUDIO_2_9_7_RECIPE, options = {}) {
|
|
1168
|
-
const evidence = readAsarFileEvidence(filePath, recipe.targets.map((target) => target.path), options);
|
|
1169
|
-
try {
|
|
1170
|
-
return inspectParsedAsar(evidence.parsed, evidence.asarSha256, recipe);
|
|
1171
|
-
} finally {
|
|
1172
|
-
if (options.releaseTargets) evidence.parsed.targetBytes.clear();
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
|
|
1177
|
-
const before = inspectAsarBuffer(value, recipe);
|
|
1178
|
-
if (before.state === "patched") {
|
|
1179
|
-
return { changed: false, buffer: Buffer.from(value), before, after: before };
|
|
1180
|
-
}
|
|
1181
|
-
if (before.state === "predecessor") {
|
|
1182
|
-
const error = new Error("Predecessor PM Studio split-origin patch must be migrated from a verified clean backup");
|
|
1183
|
-
error.code = "PM_STUDIO_ASAR_PREDECESSOR";
|
|
1184
|
-
error.inspection = before;
|
|
1185
|
-
throw error;
|
|
1186
|
-
}
|
|
1187
|
-
if (before.state === "legacy") {
|
|
1188
|
-
const error = new Error("Legacy PM Studio global-origin patch must be migrated from a verified clean backup");
|
|
1189
|
-
error.code = "PM_STUDIO_ASAR_LEGACY";
|
|
1190
|
-
error.inspection = before;
|
|
1191
|
-
throw error;
|
|
1192
|
-
}
|
|
1193
|
-
if (before.state !== "clean") {
|
|
1194
|
-
const error = new Error("PM Studio ASAR does not match the clean, current split-origin, predecessor, or legacy recipe");
|
|
1195
|
-
error.code = "PM_STUDIO_ASAR_DRIFT";
|
|
1196
|
-
error.inspection = before;
|
|
1197
|
-
throw error;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const output = Buffer.from(value);
|
|
1201
|
-
const parsed = parseAsarBuffer(output);
|
|
1202
|
-
|
|
1203
|
-
for (const target of recipe.targets) {
|
|
1204
|
-
const entry = findAsarEntry(parsed.header, target.path);
|
|
1205
|
-
const offset = entryOffset(entry.offset, `${target.path} offset`);
|
|
1206
|
-
const edit = target.edit;
|
|
1207
|
-
if (!edit) throw new Error(`${target.path} has no split-origin edit recipe`);
|
|
1208
|
-
const replacement = Buffer.from(edit.replacement);
|
|
1209
|
-
if (replacement.length !== edit.length || sha256Hex(replacement) !== edit.patchedSha256) {
|
|
1210
|
-
throw new Error(`${target.path} split-origin replacement does not match its recipe`);
|
|
1211
|
-
}
|
|
1212
|
-
const absoluteEditOffset = parsed.dataOffset + offset + edit.offset;
|
|
1213
|
-
if (absoluteEditOffset !== edit.absoluteOffset) {
|
|
1214
|
-
throw new Error(`${target.path} split-origin edit moved before patching`);
|
|
1215
|
-
}
|
|
1216
|
-
const existing = output.subarray(absoluteEditOffset, absoluteEditOffset + edit.length);
|
|
1217
|
-
if (sha256Hex(existing) !== edit.sourceSha256) {
|
|
1218
|
-
throw new Error(`${target.path} source edit window does not match its recipe`);
|
|
1219
|
-
}
|
|
1220
|
-
replacement.copy(output, absoluteEditOffset);
|
|
1221
|
-
entry.integrity = {
|
|
1222
|
-
...entry.integrity,
|
|
1223
|
-
algorithm: "SHA256",
|
|
1224
|
-
hash: target.patchedSha256,
|
|
1225
|
-
blockSize: target.blockSize,
|
|
1226
|
-
blocks: [...target.patchedBlocks],
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
const nextHeaderBytes = Buffer.from(JSON.stringify(parsed.header));
|
|
1231
|
-
if (nextHeaderBytes.length !== parsed.headerLength) {
|
|
1232
|
-
throw new Error(`Patched ASAR header changed size (${parsed.headerLength} -> ${nextHeaderBytes.length})`);
|
|
1233
|
-
}
|
|
1234
|
-
nextHeaderBytes.copy(output, parsed.headerStart);
|
|
1235
|
-
|
|
1236
|
-
const after = inspectAsarBuffer(output, recipe);
|
|
1237
|
-
if (after.state !== "patched") {
|
|
1238
|
-
const error = new Error("Patched PM Studio ASAR failed recipe verification");
|
|
1239
|
-
error.code = "PM_STUDIO_ASAR_PATCH_VERIFY_FAILED";
|
|
1240
|
-
error.inspection = after;
|
|
1241
|
-
throw error;
|
|
1242
|
-
}
|
|
1243
|
-
return { changed: true, buffer: output, before, after };
|
|
1244
|
-
}
|