返回 DeepSeek-Reasonix
lib.mjs
根目录 / desktop / packaging / lib.mjs
1 import { closeSync, fstatSync, lstatSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, realpathSync, statSync } from "node:fs";
2 import { basename, dirname, join, relative, resolve } from "node:path";
3 import { spawnSync } from "node:child_process";
4 import { inflateRawSync } from "node:zlib";
5
6 // These package scripts are Node entry points. Starting Node directly keeps
7 // paths and arguments out of cmd.exe quoting, including trailing backslashes,
8 // embedded quotes and shell metacharacters in checkout paths.
9 export function runBuildScript(directory, script, args = [], env = {}) {
10 const result = spawnSync(process.execPath, [join(directory, "scripts", script), ...args], {
11 cwd: directory,
12 env: { ...process.env, ...env },
13 stdio: "inherit",
14 shell: false,
15 });
16 if (result.error) throw result.error;
17 if (result.status !== 0) throw new Error(`${script} exited with ${result.status ?? result.signal}`);
18 }
19
20 export const PRODUCT = Object.freeze({
21 name: "Reasonix",
22 executable: "Reasonix",
23 // The Wails-era CFBundleIdentifier (com.wails.<wails.json name>). LaunchServices,
24 // saved-state and the macOS update swap key off it, so it survives the shell change.
25 bundleId: "com.wails.reasonix-desktop",
26 serviceExecutable: "reasonix-desktop",
27 cliExecutable: "reasonix",
28 windowsCliExecutable: "reasonix-cli",
29 category: "public.app-category.developer-tools",
30 });
31
32 const TARGET_TABLE = {
33 "darwin/arm64": { os: "darwin", arch: "arm64", packagerPlatform: "darwin", packagerArch: "arm64" },
34 "darwin/amd64": { os: "darwin", arch: "amd64", packagerPlatform: "darwin", packagerArch: "x64" },
35 "darwin/universal": { os: "darwin", arch: "universal", packagerPlatform: "darwin", packagerArch: "universal" },
36 "windows/amd64": { os: "windows", arch: "amd64", packagerPlatform: "win32", packagerArch: "x64" },
37 "windows/arm64": { os: "windows", arch: "arm64", packagerPlatform: "win32", packagerArch: "arm64" },
38 "linux/amd64": { os: "linux", arch: "amd64", packagerPlatform: "linux", packagerArch: "x64" },
39 "linux/arm64": { os: "linux", arch: "arm64", packagerPlatform: "linux", packagerArch: "arm64" },
40 };
41
42 export function parseTarget(spec) {
43 const target = TARGET_TABLE[spec];
44 if (!target) throw new Error(`unsupported target ${JSON.stringify(spec)}; expected one of ${Object.keys(TARGET_TABLE).join(", ")}`);
45 return { ...target, spec, key: `${target.os}-${target.arch}` };
46 }
47
48 const TAG = /^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$/;
49
50 export function versionTag(tag) {
51 if (!TAG.test(tag)) throw new Error(`version must look like v1.2.3 or v1.2.3-rc.1, got ${JSON.stringify(tag)}`);
52 return tag;
53 }
54
55 export function releaseVersions(tag) {
56 const canonical = versionTag(tag);
57 const display = canonical.slice(1);
58 return Object.freeze({ canonical, display, resource: display.split("-")[0] });
59 }
60
61 // Windows version resources, CFBundleVersion and the NSIS VIProductVersion only
62 // accept X.Y.Z; the full tag identifies the build through build.json and the
63 // Go -X main.version ldflag. Packager also writes appVersion to package.json.
64 export function numericVersion(tag) {
65 return releaseVersions(tag).resource;
66 }
67
68 export function displayVersion(tag) {
69 return releaseVersions(tag).display;
70 }
71
72 // The product identity lived in wails.json while the Wails shell was the build
73 // entry point; with the shell retired it is a constant here.
74 export function readProductIdentity() {
75 return {
76 projectName: PRODUCT.serviceExecutable,
77 companyName: "Reasonix",
78 productName: PRODUCT.name,
79 copyright: "Copyright © 2026 Reasonix Contributors",
80 };
81 }
82
83 export function shellIgnore(path) {
84 if (path === "" || path === "/package.json" || path === "/dist") return false;
85 if (path.startsWith("/dist/")) return path.endsWith(".map");
86 return true;
87 }
88
89 export function sanitizeShellPackageJson(pkg, { version, productName }) {
90 const keep = ["name", "description", "main", "type"];
91 const out = {};
92 for (const key of keep) if (key in pkg) out[key] = pkg[key];
93 out.productName = productName;
94 out.version = numericVersion(version);
95 return out;
96 }
97
98 export function buildInfo({ version, channel, commit, electronVersion, target, buildTime }) {
99 return {
100 schemaVersion: 1,
101 version: versionTag(version),
102 channel,
103 commit,
104 buildTime,
105 electron: electronVersion,
106 platform: `${target.os}/${target.arch}`,
107 };
108 }
109
110 export function packagerOptions({ target, version, identity, root, electronVersion, extraResources, icon }) {
111 const numeric = numericVersion(version);
112 const options = {
113 dir: join(root, "electron"),
114 out: join(root, "build", "electron", ".packager"),
115 name: PRODUCT.name,
116 executableName: PRODUCT.executable,
117 platform: target.packagerPlatform,
118 arch: target.packagerArch,
119 electronVersion,
120 appBundleId: PRODUCT.bundleId,
121 appVersion: numeric,
122 buildVersion: numeric,
123 appCopyright: identity.copyright,
124 appCategoryType: PRODUCT.category,
125 asar: true,
126 prune: true,
127 overwrite: true,
128 junk: true,
129 darwinDarkModeSupport: true,
130 extraResource: extraResources,
131 ignore: shellIgnore,
132 };
133 if (icon) options.icon = icon;
134 if (target.packagerPlatform === "win32") {
135 options.win32metadata = {
136 CompanyName: identity.companyName,
137 FileDescription: identity.productName,
138 ProductName: identity.productName,
139 InternalName: PRODUCT.executable,
140 OriginalFilename: `${PRODUCT.executable}.exe`,
141 };
142 }
143 return options;
144 }
145
146 export function nsisProjectDefines(identity, version) {
147 const versions = releaseVersions(version);
148 const lines = [
149 "; Generated by desktop/packaging/package.mjs - do not edit or commit.",
150 `!define INFO_PROJECTNAME "${identity.projectName}"`,
151 `!define INFO_COMPANYNAME "${identity.companyName}"`,
152 `!define INFO_PRODUCTNAME "${identity.productName}"`,
153 `!define INFO_PRODUCTVERSION "${versions.resource}"`,
154 `!define REASONIX_DISPLAY_VERSION "${versions.display}"`,
155 `!define INFO_COPYRIGHT "${identity.copyright}"`,
156 `!define REASONIX_VERSION_TAG "${versions.canonical}"`,
157 ];
158 // makensis only decodes an include as UTF-8 when it carries a BOM; the copyright sign needs it.
159 return "" + lines.join("\r\n") + "\r\n";
160 }
161
162 export function normalizeEntry(name) {
163 let out = name.replace(/\\/g, "/");
164 while (out.startsWith("./")) out = out.slice(2);
165 return out;
166 }
167
168 export function walkFiles(dir, base = dir) {
169 const out = [];
170 for (const entry of readdirSync(dir, { withFileTypes: true })) {
171 const path = join(dir, entry.name);
172 if (entry.isDirectory()) out.push(...walkFiles(path, base));
173 else out.push(normalizeEntry(relative(base, path)));
174 }
175 return out.sort();
176 }
177
178 const PE_SUFFIX = /\.(exe|dll)$/i;
179
180 export function signingFileList(entries) {
181 return [...new Set(entries.map(normalizeEntry).filter((name) => PE_SUFFIX.test(name)))].sort();
182 }
183
184 export function parseSigningFileList(text) {
185 return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
186 }
187
188 export const WINDOWS_FLAT_PAYLOAD = Object.freeze([
189 "reasonix-desktop.exe",
190 "reasonix-guard.exe",
191 "reasonix-launcher.exe",
192 "reasonix-update-helper.exe",
193 "reasonix-cli.exe",
194 "reasonix-uninstall.exe",
195 ]);
196
197 const APP_RESOURCES = ["resources/app.asar", "resources/app/index.html", "resources/build.json", "resources/icons/appicon.png", "resources/icons/trayTemplate.png", "resources/icons/trayTemplate@2x.png"];
198
199 function darwinBundleMembers() {
200 const helper = (kind) => `Contents/Frameworks/${PRODUCT.name} Helper (${kind}).app/Contents/MacOS/${PRODUCT.name} Helper (${kind})`;
201 return [
202 "Contents/Info.plist",
203 `Contents/MacOS/${PRODUCT.executable}`,
204 `Contents/MacOS/${PRODUCT.serviceExecutable}`,
205 `Contents/Resources/service/${PRODUCT.serviceExecutable}`,
206 // The CLI sidecar lives in Resources/service/, never Contents/MacOS/: on
207 // case-insensitive APFS "reasonix" there collides with the Electron main
208 // executable "Reasonix" and cp would clobber it.
209 `Contents/Resources/service/${PRODUCT.cliExecutable}`,
210 "Contents/Resources/app.asar",
211 "Contents/Resources/app/index.html",
212 "Contents/Resources/build.json",
213 "Contents/Resources/icons/appicon.png",
214 "Contents/Resources/icons/trayTemplate.png",
215 "Contents/Resources/icons/trayTemplate@2x.png",
216 "Contents/Frameworks/Electron Framework.framework/Electron Framework",
217 helper("Renderer"),
218 helper("GPU"),
219 ];
220 }
221
222 const VERSION_DIR = "versions/v[^/]+";
223 const PACKAGING_JUNK = /(^|\/)(?:[^/]+\.map|__tests__|testdata|\.cache|coverage|npm-debug\.log|pnpm-debug\.log|yarn-error\.log)(?:$|\/)/;
224
225 const MEMBERS = {
226 "darwin-app-dir": { required: darwinBundleMembers(), forbidden: ["Contents/MacOS/reasonix-guard"] },
227 "darwin-zip": {
228 required: darwinBundleMembers().map((name) => `${PRODUCT.name}.app/${name}`),
229 forbidden: [`${PRODUCT.name}.app/Contents/MacOS/reasonix-guard`],
230 },
231 "windows-app-dir": {
232 required: [`${PRODUCT.executable}.exe`, "ffmpeg.dll", "libEGL.dll", "libGLESv2.dll", "resources.pak", "icudtl.dat", "locales/en-US.pak", ...APP_RESOURCES],
233 forbidden: [],
234 },
235 "windows-portable-zip": {
236 required: [
237 `${PRODUCT.executable}.exe`,
238 `${PRODUCT.windowsCliExecutable}.exe`,
239 "current.json",
240 new RegExp(`^${VERSION_DIR}/reasonix-desktop\\.exe$`),
241 new RegExp(`^${VERSION_DIR}/reasonix-update-helper\\.exe$`),
242 new RegExp(`^${VERSION_DIR}/reasonix-cli\\.exe$`),
243 new RegExp(`^${VERSION_DIR}/app/${PRODUCT.executable}\\.exe$`),
244 new RegExp(`^${VERSION_DIR}/app/resources/bin/reasonix-cli-launcher\\.exe$`),
245 new RegExp(`^${VERSION_DIR}/app/resources/app\\.asar$`),
246 new RegExp(`^${VERSION_DIR}/app/resources/app/index\\.html$`),
247 new RegExp(`^${VERSION_DIR}/app/resources/build\\.json$`),
248 ],
249 forbidden: ["reasonix-guard.exe", "reasonix-desktop.exe"],
250 },
251 "linux-app-dir": {
252 required: [PRODUCT.executable, "chrome-sandbox", "chrome_crashpad_handler", "libffmpeg.so", "resources.pak", "locales/en-US.pak", ...APP_RESOURCES],
253 forbidden: [],
254 },
255 "linux-tar": {
256 required: ["reasonix-desktop", "reasonix-launcher", "reasonix-guard", "reasonix", `app/${PRODUCT.executable}`, "app/chrome-sandbox", ...APP_RESOURCES.map((name) => `app/${name}`)],
257 forbidden: [],
258 },
259 "linux-deb": {
260 required: [
261 "usr/bin/reasonix-desktop",
262 "usr/bin/reasonix-launcher",
263 "usr/bin/reasonix",
264 "usr/lib/reasonix/reasonix-update-helper",
265 `usr/lib/reasonix/app/${PRODUCT.executable}`,
266 "usr/lib/reasonix/app/chrome-sandbox",
267 ...APP_RESOURCES.map((name) => `usr/lib/reasonix/app/${name}`),
268 "usr/share/polkit-1/actions/io.reasonix.desktop.update.policy",
269 "usr/share/applications/reasonix.desktop",
270 ],
271 forbidden: ["usr/bin/reasonix-guard"],
272 },
273 };
274
275 export const ARTIFACT_KINDS = Object.freeze(Object.keys(MEMBERS));
276
277 export const WINDOWS_PORTABLE_LAYOUTS = Object.freeze(["canonical", "legacy-dual"]);
278
279 function memberSpec(kind, portableLayout) {
280 if (!WINDOWS_PORTABLE_LAYOUTS.includes(portableLayout)) throw new Error(`unknown Windows portable layout ${JSON.stringify(portableLayout)}`);
281 const spec = MEMBERS[kind];
282 if (!spec) throw new Error(`unknown artifact kind ${JSON.stringify(kind)}`);
283 if (kind !== "windows-portable-zip") return spec;
284 return portableLayout === "legacy-dual"
285 ? { required: [...spec.required, "reasonix-launcher.exe"], forbidden: spec.forbidden }
286 : { required: spec.required, forbidden: [...spec.forbidden, "reasonix-launcher.exe"] };
287 }
288
289 export function requiredMembers(kind, portableLayout = "canonical") {
290 return memberSpec(kind, portableLayout).required;
291 }
292
293 export function checkMembers(entries, kind, portableLayout = "canonical") {
294 const spec = memberSpec(kind, portableLayout);
295 const names = new Set(entries.map(normalizeEntry).filter((name) => name !== "" && !name.endsWith("/")));
296 const matches = (rule) => (rule instanceof RegExp ? [...names].some((name) => rule.test(name)) : names.has(rule));
297 const forbidden = [...spec.forbidden, PACKAGING_JUNK].filter((rule) => matches(rule)).map(String);
298 if (kind === "windows-portable-zip") {
299 const rootEntries = new Set(["Reasonix.exe", "reasonix-cli.exe", ...(portableLayout === "legacy-dual" ? ["reasonix-launcher.exe"] : [])]);
300 for (const name of names) {
301 if (!name.includes("/") && /\.(exe|dll)$/i.test(name) && !rootEntries.has(name) && !forbidden.includes(name)) forbidden.push(name);
302 }
303 }
304 return {
305 missing: spec.required.filter((rule) => !matches(rule)).map(String),
306 forbidden,
307 };
308 }
309
310 // GNU tar -tv and dpkg-deb -c share this column layout; bsdtar does not, so an
311 // unrecognised line fails instead of silently dropping the mode check.
312 const VERBOSE_LISTING = /^([-dl][rwxsStT-]{9})\s+(\S+)\s+\d+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+(.*)$/;
313
314 export function parseVerboseListing(lines) {
315 return lines.map((line) => {
316 const match = VERBOSE_LISTING.exec(line);
317 if (!match) throw new Error(`unrecognised listing line: ${JSON.stringify(line)}`);
318 const [, mode, owner, rest] = match;
319 const name = mode.startsWith("l") ? rest.split(" -> ")[0] : rest;
320 return { mode, owner, name };
321 });
322 }
323
324 const DIRECTORY_MODE = /^drwxr-xr-x$/;
325
326 export function checkEntryModes(rows, kind) {
327 const errors = [];
328 for (const { mode, owner, name } of rows) {
329 if (mode.startsWith("l")) continue;
330 if (mode.startsWith("d") && !DIRECTORY_MODE.test(mode)) errors.push(`${name} has mode ${mode}; directories must be drwxr-xr-x`);
331 if (mode.startsWith("-") && mode[7] !== "r") errors.push(`${name} has mode ${mode}; files must be world-readable`);
332 if (kind === "linux-deb" && owner !== "root/root") errors.push(`${name} is owned by ${owner}; package members must be root/root`);
333 }
334 return errors;
335 }
336
337 const PT_INTERP = 3;
338
339 // Returns the program interpreter an ELF executable names, or null for a static
340 // one. Only the Electron shell may carry a glibc floor: a Go binary that names
341 // ld.so inherits the build runner's glibc and fails to start on older systems.
342 export function elfInterpreter(bytes) {
343 if (bytes.length < 52 || bytes.readUInt32BE(0) !== 0x7f454c46) throw new Error("not an ELF file");
344 const wide = bytes[4] === 2;
345 const little = bytes[5] === 1;
346 if (bytes[4] !== 1 && !wide) throw new Error(`unknown ELF class ${bytes[4]}`);
347 if (bytes[5] !== 2 && !little) throw new Error(`unknown ELF data encoding ${bytes[5]}`);
348 const u16 = (at) => (little ? bytes.readUInt16LE(at) : bytes.readUInt16BE(at));
349 const u32 = (at) => (little ? bytes.readUInt32LE(at) : bytes.readUInt32BE(at));
350 const word = (at) => (wide ? Number(little ? bytes.readBigUInt64LE(at) : bytes.readBigUInt64BE(at)) : u32(at));
351 if (wide && bytes.length < 64) throw new Error("truncated ELF header");
352 const phoff = word(wide ? 32 : 28);
353 const phentsize = u16(wide ? 54 : 42);
354 const phnum = u16(wide ? 56 : 44);
355 for (let i = 0; i < phnum; i++) {
356 const header = phoff + i * phentsize;
357 if (header + phentsize > bytes.length) throw new Error("truncated ELF program header table");
358 if (u32(header) !== PT_INTERP) continue;
359 const offset = word(header + (wide ? 8 : 4));
360 const size = word(header + (wide ? 32 : 16));
361 if (offset + size > bytes.length) throw new Error("truncated ELF interpreter path");
362 return bytes.toString("latin1", offset, offset + size).replace(/\0+$/, "");
363 }
364 return null;
365 }
366
367 const LINUX_GO_MEMBERS = {
368 "linux-tar": ["reasonix-desktop", "reasonix-launcher", "reasonix-guard", "reasonix"],
369 "linux-deb": ["usr/bin/reasonix-desktop", "usr/bin/reasonix-launcher", "usr/bin/reasonix", "usr/lib/reasonix/reasonix-update-helper"],
370 };
371
372 export function checkStaticGoMembers(kind, readMember) {
373 const errors = [];
374 for (const name of LINUX_GO_MEMBERS[kind] ?? []) {
375 const interpreter = elfInterpreter(readMember(name));
376 if (interpreter !== null) errors.push(`${name} is dynamically linked (interpreter ${interpreter}); Linux Go binaries must be built with CGO_ENABLED=0`);
377 }
378 return errors;
379 }
380
381 export function validateMacServiceLink(appDir) {
382 const link = join(appDir, "Contents", "MacOS", PRODUCT.serviceExecutable);
383 const expectedTarget = `../Resources/service/${PRODUCT.serviceExecutable}`;
384 const errors = [];
385 let stat;
386 try {
387 stat = lstatSync(link);
388 } catch (error) {
389 return [`service compatibility link is unavailable: ${error.message}`];
390 }
391 if (!stat.isSymbolicLink()) return ["service compatibility path is not a symbolic link"];
392 const target = readlinkSync(link);
393 if (target !== expectedTarget) errors.push(`service compatibility link target is ${JSON.stringify(target)}, want ${JSON.stringify(expectedTarget)}`);
394 if (resolve(dirname(link), target) !== resolve(appDir, "Contents", "Resources", "service", PRODUCT.serviceExecutable)) {
395 errors.push("service compatibility link does not resolve to the package service entity");
396 }
397 try {
398 const realApp = realpathSync(appDir);
399 const realTarget = realpathSync(link);
400 const rel = relative(realApp, realTarget);
401 if (rel === "" || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
402 errors.push("service compatibility link resolves outside the application bundle");
403 }
404 if (!statSync(realTarget).isFile()) errors.push("service compatibility link target is not a regular file");
405 } catch (error) {
406 errors.push(`service compatibility link is dangling or cyclic: ${error.message}`);
407 }
408 return errors;
409 }
410
411 export function inferArtifactKind(pathname, isDirectory, entries = []) {
412 const name = basename(pathname);
413 if (isDirectory) {
414 if (name.endsWith(".app")) return "darwin-app-dir";
415 if (entries.includes(`${PRODUCT.executable}.exe`)) return "windows-app-dir";
416 if (entries.includes("chrome-sandbox")) return "linux-app-dir";
417 throw new Error(`cannot infer the artifact kind of directory ${pathname}`);
418 }
419 if (/^Reasonix-darwin-.*\.zip$/.test(name)) return "darwin-zip";
420 if (/^Reasonix-windows-.*\.zip$/.test(name)) return "windows-portable-zip";
421 if (name.endsWith(".tar.gz")) return "linux-tar";
422 if (name.endsWith(".deb")) return "linux-deb";
423 throw new Error(`cannot infer the artifact kind of ${pathname}`);
424 }
425
426 const EOCD = 0x06054b50;
427 const EOCD64_LOCATOR = 0x07064b50;
428 const EOCD64 = 0x06064b50;
429 const CENTRAL_HEADER = 0x02014b50;
430
431 // Only the central directory is read, so a 300 MB bundle costs a few reads;
432 // zip64 records are honoured because ditto emits them for large archives.
433 export function listZipEntries(file) {
434 return scanZip(file);
435 }
436
437 // Read a bounded member without extracting the archive or requiring a native
438 // unzip tool. Used only for small pointers and the stable launcher entries.
439 export function readZipMember(file, member) {
440 return scanZip(file, member);
441 }
442
443 function scanZip(file, member) {
444 const fd = openSync(file, "r");
445 try {
446 const size = fstatSync(fd).size;
447 const tailLength = Math.min(size, 22 + 65535);
448 const tail = Buffer.alloc(tailLength);
449 readSync(fd, tail, 0, tailLength, size - tailLength);
450 let eocd = -1;
451 for (let i = tailLength - 22; i >= 0; i--) {
452 if (tail.readUInt32LE(i) === EOCD) {
453 eocd = i;
454 break;
455 }
456 }
457 if (eocd < 0) throw new Error(`${file}: not a zip archive (no end-of-central-directory record)`);
458 let count = tail.readUInt16LE(eocd + 10);
459 let directorySize = tail.readUInt32LE(eocd + 12);
460 let directoryOffset = tail.readUInt32LE(eocd + 16);
461 const locator = eocd - 20;
462 if ((count === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) && locator >= 0 && tail.readUInt32LE(locator) === EOCD64_LOCATOR) {
463 const record = Buffer.alloc(56);
464 readSync(fd, record, 0, 56, Number(tail.readBigUInt64LE(locator + 8)));
465 if (record.readUInt32LE(0) !== EOCD64) throw new Error(`${file}: corrupt zip64 end-of-central-directory record`);
466 count = Number(record.readBigUInt64LE(32));
467 directorySize = Number(record.readBigUInt64LE(40));
468 directoryOffset = Number(record.readBigUInt64LE(48));
469 }
470 const directory = Buffer.alloc(directorySize);
471 readSync(fd, directory, 0, directorySize, directoryOffset);
472 const names = [];
473 let contents;
474 let offset = 0;
475 for (let i = 0; i < count; i++) {
476 if (directory.readUInt32LE(offset) !== CENTRAL_HEADER) throw new Error(`${file}: corrupt central directory at entry ${i}`);
477 const nameLength = directory.readUInt16LE(offset + 28);
478 const extraLength = directory.readUInt16LE(offset + 30);
479 const commentLength = directory.readUInt16LE(offset + 32);
480 const name = directory.toString("utf8", offset + 46, offset + 46 + nameLength);
481 names.push(name);
482 if (member !== undefined && normalizeEntry(name) === member) {
483 if (contents !== undefined) throw new Error(`duplicate zip member ${member}`);
484 let compressed = directory.readUInt32LE(offset + 20);
485 let uncompressed = directory.readUInt32LE(offset + 24);
486 let localOffset = directory.readUInt32LE(offset + 42);
487 const extraStart = offset + 46 + nameLength;
488 for (let pos = extraStart; pos + 4 <= extraStart + extraLength;) {
489 const tag = directory.readUInt16LE(pos), length = directory.readUInt16LE(pos + 2);
490 if (pos + 4 + length > extraStart + extraLength) throw new Error("invalid zip extra field");
491 if (tag === 1) {
492 let field = pos + 4;
493 const next = () => {
494 if (field + 8 > pos + 4 + length) throw new Error("invalid zip64 entry");
495 const value = Number(directory.readBigUInt64LE(field)); field += 8;
496 if (!Number.isSafeInteger(value)) throw new Error("zip64 value is too large");
497 return value;
498 };
499 if (uncompressed === 0xffffffff) uncompressed = next();
500 if (compressed === 0xffffffff) compressed = next();
501 if (localOffset === 0xffffffff) localOffset = next();
502 }
503 pos += 4 + length;
504 }
505 const limit = 32 * 1024 * 1024;
506 if (compressed > limit || uncompressed > limit || localOffset + 30 > size) throw new Error(`zip member ${member} exceeds bounds`);
507 if (directory.readUInt16LE(offset + 8) & 1) throw new Error("encrypted zip members are unsupported");
508 const local = Buffer.alloc(30);
509 readSync(fd, local, 0, local.length, localOffset);
510 if (local.readUInt32LE(0) !== 0x04034b50) throw new Error("invalid zip local header");
511 const method = directory.readUInt16LE(offset + 10);
512 if (local.readUInt16LE(8) !== method || (local.readUInt16LE(6) & 1)) throw new Error("zip local/central header mismatch");
513 const localName = Buffer.alloc(local.readUInt16LE(26));
514 readSync(fd, localName, 0, localName.length, localOffset + 30);
515 if (localName.toString("utf8") !== name) throw new Error("zip local/central name mismatch");
516 const start = localOffset + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
517 if (start + compressed > directoryOffset) throw new Error("zip member overlaps central directory");
518 const packed = Buffer.alloc(compressed);
519 readSync(fd, packed, 0, packed.length, start);
520 if (method !== 0 && method !== 8) throw new Error(`unsupported zip compression ${method}`);
521 contents = method === 0 ? packed : inflateRawSync(packed, { maxOutputLength: limit });
522 if (contents.length !== uncompressed) throw new Error(`zip member ${member} size mismatch`);
523 }
524 offset += 46 + nameLength + extraLength + commentLength;
525 }
526 if (member !== undefined && contents === undefined) throw new Error(`zip member ${member} is missing`);
527 return member === undefined ? names : contents;
528 } finally {
529 closeSync(fd);
530 }
531 }
532
533 export function isDirectory(path) {
534 try {
535 return statSync(path).isDirectory();
536 } catch {
537 return false;
538 }
539 }
540
540 lines Plain Text