@gajae-code/natives 0.18.11 → 0.18.12
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/native/diagnostic-artifact-embedded.json +5 -0
- package/native/diagnostic-artifact.json +7 -7
- package/native/diagnostic-loader.js +121 -8
- package/native/index.d.ts +15 -8
- package/native/index.js +1 -1
- package/native/loader-state.d.ts +3 -0
- package/native/loader-state.js +96 -12
- package/package.json +8 -7
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "gjc.diagnostic-artifact",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.12",
|
|
4
4
|
"artifacts": {
|
|
5
|
-
"pi_natives.darwin-arm64.node": "
|
|
6
|
-
"pi_natives.darwin-x64-baseline.node": "
|
|
7
|
-
"pi_natives.linux-arm64.node": "
|
|
8
|
-
"pi_natives.linux-x64-baseline.node": "
|
|
9
|
-
"pi_natives.linux-x64-modern.node": "
|
|
10
|
-
"pi_natives.win32-x64-baseline.node": "
|
|
5
|
+
"pi_natives.darwin-arm64.node": "afc623540979280ece88b16c57c5a5941407a98bc0344f6fd851537d886dcfee",
|
|
6
|
+
"pi_natives.darwin-x64-baseline.node": "a1aee260794c49ed6ce8590f23ac89e899e20d93dae95900bb0b1503ff4ea159",
|
|
7
|
+
"pi_natives.linux-arm64.node": "2d695e63e7d1f89eff9a0809bb2f417cb65c91ae75161e5fd58a405d29689322",
|
|
8
|
+
"pi_natives.linux-x64-baseline.node": "7cada23a1841fe89670a80853ca8c35905f80783d8bb19a284a3f45821d9e56e",
|
|
9
|
+
"pi_natives.linux-x64-modern.node": "75e7f945bf27526f1d833f6c80490d9600fabc6700de70e5eb83eea6ef8cd8ea",
|
|
10
|
+
"pi_natives.win32-x64-baseline.node": "a88258561c1027a3d6e8477de335f903182f300091700cd3d6cfd96abc53ce95"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* - proves the trusted namespace before reading: every component is owned by the
|
|
20
20
|
* current user or by root, is not a symlink, and grants no group/other write,
|
|
21
21
|
* so an account that owns none of the path cannot swap the artifact out;
|
|
22
|
-
* - proves the bytes on a retained descriptor: SHA-256 equal to the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* - proves the bytes on a retained descriptor: SHA-256 equal to the shipped
|
|
23
|
+
* `diagnostic-artifact.json` digest, a compiled-binary digest captured from the
|
|
24
|
+
* verified embed input, or (in a source checkout only) the gitignored local
|
|
25
|
+
* manifest when adjacent build metadata explicitly identifies a local build.
|
|
26
|
+
* Each manifest belongs to the package and is checked before use. An addon's own
|
|
27
|
+
* `nativeBuildInfo()` is self-reported by code that has already been executed, so
|
|
26
28
|
* it is only a consistency check afterwards, never the trust anchor;
|
|
27
29
|
* - activates the verified bytes through that same retained descriptor
|
|
28
30
|
* (`process.dlopen` on `/dev/fd/<fd>`), so a pathname replaced between
|
|
@@ -75,6 +77,8 @@ import * as fs from "node:fs";
|
|
|
75
77
|
import { createRequire } from "node:module";
|
|
76
78
|
import * as os from "node:os";
|
|
77
79
|
import * as nodePath from "node:path";
|
|
80
|
+
import { fileURLToPath } from "node:url";
|
|
81
|
+
import compiledArtifacts from "./diagnostic-artifact-embedded.json" with { type: "json" };
|
|
78
82
|
import trustedArtifacts from "./diagnostic-artifact.json" with { type: "json" };
|
|
79
83
|
import packageManifest from "../package.json" with { type: "json" };
|
|
80
84
|
|
|
@@ -127,17 +131,126 @@ function cachedArtifactPath() {
|
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/**
|
|
130
|
-
* The package-owned expected digest
|
|
131
|
-
*
|
|
134
|
+
* The package-owned expected digest, or null when this package has no trusted
|
|
135
|
+
* record. Compiled binaries prefer the digest recorded at their embed boundary;
|
|
136
|
+
* source checkouts may use the gitignored local manifest for an explicitly local
|
|
137
|
+
* build. Installed packages use the shipped record.
|
|
132
138
|
*
|
|
139
|
+
* @param {string} file
|
|
133
140
|
* @returns {string | null}
|
|
134
141
|
*/
|
|
135
|
-
function trustedDigest() {
|
|
142
|
+
function trustedDigest(file) {
|
|
143
|
+
const compiledDigest = compiledArtifactDigest(file);
|
|
144
|
+
if (compiledDigest !== undefined) return compiledDigest;
|
|
145
|
+
const localDigest = sourceLocalBuildDigest(file);
|
|
146
|
+
if (localDigest !== undefined) return localDigest;
|
|
136
147
|
if (trustedArtifacts?.version !== packageManifest.version) return null;
|
|
137
148
|
const digest = trustedArtifacts?.artifacts?.[ARTIFACT_BASENAME];
|
|
138
149
|
return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest) ? digest : null;
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A compiled binary carries the digest of the exact addon verified before embedding.
|
|
154
|
+
* An empty stub falls back to the shipped record; a populated but stale or malformed
|
|
155
|
+
* compiled record fails closed.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} file
|
|
158
|
+
* @returns {string | null | undefined} Undefined means no compiled digest is present.
|
|
159
|
+
*/
|
|
160
|
+
function compiledArtifactDigest(file) {
|
|
161
|
+
if (!runningFromCompiledBinary() || compiledArtifacts?.version === "") return undefined;
|
|
162
|
+
if (compiledArtifacts?.schema !== "gjc.diagnostic-artifact" || compiledArtifacts?.version !== packageManifest.version) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const digest = compiledArtifacts?.artifacts?.[nodePath.basename(file)];
|
|
166
|
+
if (digest === undefined) return undefined;
|
|
167
|
+
return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest) ? digest : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read one bounded JSON file only after proving its path and retained descriptor are
|
|
172
|
+
* in the same trusted namespace used for the native artifact.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} file
|
|
175
|
+
* @returns {Record<string, unknown> | null}
|
|
176
|
+
*/
|
|
177
|
+
function readTrustedLocalJson(file) {
|
|
178
|
+
let fd = -1;
|
|
179
|
+
try {
|
|
180
|
+
if (!trustedNamespace(file)) return null;
|
|
181
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
182
|
+
const stat = fs.fstatSync(fd);
|
|
183
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > 1 << 20) return null;
|
|
184
|
+
if ((stat.mode & 0o022) !== 0) return null;
|
|
185
|
+
if (stat.uid !== process.geteuid?.() && stat.uid !== 0) return null;
|
|
186
|
+
if (inspectAclOnDescriptor(fd) !== "absent") return null;
|
|
187
|
+
const bytes = Buffer.allocUnsafe(stat.size);
|
|
188
|
+
let offset = 0;
|
|
189
|
+
while (offset < bytes.length) {
|
|
190
|
+
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
191
|
+
if (read === 0) return null;
|
|
192
|
+
offset += read;
|
|
193
|
+
}
|
|
194
|
+
if (fs.fstatSync(fd).size !== stat.size) return null;
|
|
195
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
} finally {
|
|
200
|
+
if (fd >= 0) closeDescriptor(fd);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* An unversioned local build is accepted only when the package-owned record proves
|
|
206
|
+
* these exact bytes for the current package version. This is the migration path for
|
|
207
|
+
* builds produced before local sidecars carried their version.
|
|
208
|
+
*
|
|
209
|
+
* @param {unknown} record
|
|
210
|
+
* @param {string} version
|
|
211
|
+
* @param {string} artifact
|
|
212
|
+
* @param {string} digest
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function trustedRecordHasDigest(record, version, artifact, digest) {
|
|
216
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) return false;
|
|
217
|
+
const candidate = /** @type {Record<string, unknown>} */ (record);
|
|
218
|
+
if (candidate.schema !== "gjc.diagnostic-artifact" || candidate.version !== version) return false;
|
|
219
|
+
const artifacts = candidate.artifacts;
|
|
220
|
+
if (typeof artifacts !== "object" || artifacts === null || Array.isArray(artifacts)) return false;
|
|
221
|
+
return /** @type {Record<string, unknown>} */ (artifacts)[artifact] === digest;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A source-mode local build has an explicit profile marker and a gitignored digest
|
|
226
|
+
* record. Never consult these files for a compiled binary, an installed platform
|
|
227
|
+
* package, or a release/CI addon.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} file
|
|
230
|
+
* @returns {string | null | undefined} Undefined means this is not a local source build.
|
|
231
|
+
*/
|
|
232
|
+
function sourceLocalBuildDigest(file) {
|
|
233
|
+
if (runningFromCompiledBinary()) return undefined;
|
|
234
|
+
const sourceAddon = nodePath.join(nodePath.dirname(fileURLToPath(import.meta.url)), ARTIFACT_BASENAME);
|
|
235
|
+
if (nodePath.resolve(file) !== sourceAddon) return undefined;
|
|
236
|
+
const buildMetadata = readTrustedLocalJson(`${sourceAddon}.build.json`);
|
|
237
|
+
if (buildMetadata?.profile !== "local") return undefined;
|
|
238
|
+
|
|
239
|
+
const localManifest = readTrustedLocalJson(
|
|
240
|
+
nodePath.join(nodePath.dirname(sourceAddon), "diagnostic-artifact.local.json"),
|
|
241
|
+
);
|
|
242
|
+
if (localManifest?.schema !== "gjc.diagnostic-artifact") return null;
|
|
243
|
+
if (localManifest.version !== packageManifest.version) return null;
|
|
244
|
+
const artifacts = localManifest.artifacts;
|
|
245
|
+
if (typeof artifacts !== "object" || artifacts === null || Array.isArray(artifacts)) return null;
|
|
246
|
+
const digest = /** @type {Record<string, unknown>} */ (artifacts)[ARTIFACT_BASENAME];
|
|
247
|
+
if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest)) return null;
|
|
248
|
+
if (buildMetadata.version !== undefined) {
|
|
249
|
+
return buildMetadata.version === packageManifest.version ? digest : null;
|
|
250
|
+
}
|
|
251
|
+
return trustedRecordHasDigest(trustedArtifacts, packageManifest.version, ARTIFACT_BASENAME, digest) ? digest : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
141
254
|
/**
|
|
142
255
|
* Select the single applicable layout. Exactly one distribution shape applies per
|
|
143
256
|
* install, and a failure of the selected layout is never a reason to try another.
|
|
@@ -674,7 +787,7 @@ function consumeHandle(handle) {
|
|
|
674
787
|
* @returns {{ ok: true, token: string } | { ok: false, reason: "unsupported" }}
|
|
675
788
|
*/
|
|
676
789
|
export function verifyDiagnosticArtifact(file) {
|
|
677
|
-
const expectedDigest = trustedDigest();
|
|
790
|
+
const expectedDigest = trustedDigest(file);
|
|
678
791
|
if (expectedDigest === null) return unsupported;
|
|
679
792
|
let fd = -1;
|
|
680
793
|
try {
|
package/native/index.d.ts
CHANGED
|
@@ -687,7 +687,7 @@ export declare function __piNativesPublishOutcomeV1(): void
|
|
|
687
687
|
* `packages/natives/native/index.js` (which derives the name from
|
|
688
688
|
* `package.json#version`).
|
|
689
689
|
*/
|
|
690
|
-
export declare function
|
|
690
|
+
export declare function __piNativesV0_18_12(): void
|
|
691
691
|
|
|
692
692
|
/**
|
|
693
693
|
* Apply conservative pre-execution rewrites to a bash command.
|
|
@@ -1239,9 +1239,10 @@ export declare function exactReplacePath(sourcePath: string, destinationPath: st
|
|
|
1239
1239
|
* single-link ownership; hard-linked or symlinked source/destination are
|
|
1240
1240
|
* rejected) before anything is mutated. On Windows a pre-mutation
|
|
1241
1241
|
* `STATUS_SHARING_VIOLATION` on the destination open is reported as a
|
|
1242
|
-
* distinct `sharing_violation` code
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1242
|
+
* distinct `sharing_violation` code, while `STATUS_ACCESS_DENIED` on a
|
|
1243
|
+
* retained open or rename is reported as `access_denied`. Pre-mutation
|
|
1244
|
+
* failures include the original `windows_error_code`; only known-safe rename
|
|
1245
|
+
* operations retry transient access-denied or sharing-violation statuses.
|
|
1245
1246
|
*/
|
|
1246
1247
|
export declare function exactReplaceRetained(sourcePath: string, destinationPath: string, backupName: string, expectedSource: NativeExactFileIdentity, expectedDestination: NativeExactFileIdentity): NativeExactUnlinkResult
|
|
1247
1248
|
|
|
@@ -2578,16 +2579,22 @@ export interface PtyStartOptions {
|
|
|
2578
2579
|
export declare function ptyTimeoutCount(): bigint
|
|
2579
2580
|
|
|
2580
2581
|
/**
|
|
2581
|
-
* Rasterize SVG/SVGZ bytes into a
|
|
2582
|
+
* Rasterize bounded SVG/SVGZ bytes into a PNG without resolving local files.
|
|
2583
|
+
*
|
|
2584
|
+
* DTD/entity declarations and SVG image elements are rejected rather than
|
|
2585
|
+
* allowing unbounded expansion or silently returning an incomplete preview.
|
|
2582
2586
|
*
|
|
2583
2587
|
* Conversion runs on the native blocking pool so parsing and rendering do not
|
|
2584
2588
|
* stall the JavaScript event loop.
|
|
2585
2589
|
*
|
|
2590
|
+
* When both cell dimensions are provided, the PNG is padded on a transparent
|
|
2591
|
+
* canvas to whole terminal cells without embedding or decoding another image.
|
|
2592
|
+
*
|
|
2586
2593
|
* # Errors
|
|
2587
|
-
* Returns an error for invalid SVG data, zero
|
|
2588
|
-
* failure, or PNG encoding failure.
|
|
2594
|
+
* Returns an error for invalid SVG data, mismatched/zero cell dimensions,
|
|
2595
|
+
* zero/oversized limits, allocation failure, or PNG encoding failure.
|
|
2589
2596
|
*/
|
|
2590
|
-
export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number): Promise<Uint8Array>
|
|
2597
|
+
export declare function rasterizeSvg(input: Uint8Array, maxWidthPx: number, maxHeightPx: number, cellWidthPx?: number | undefined | null, cellHeightPx?: number | undefined | null): Promise<Uint8Array>
|
|
2591
2598
|
|
|
2592
2599
|
/**
|
|
2593
2600
|
* Read an image from the system clipboard.
|
package/native/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export const TtyWriter = nativeBindings.TtyWriter;
|
|
|
35
35
|
// functions
|
|
36
36
|
export const __gjcInstallTokioRuntime = nativeBindings.__gjcInstallTokioRuntime;
|
|
37
37
|
export const __piNativesPublishOutcomeV1 = nativeBindings.__piNativesPublishOutcomeV1;
|
|
38
|
-
export const
|
|
38
|
+
export const __piNativesV0_18_12 = nativeBindings.__piNativesV0_18_12;
|
|
39
39
|
export const applyBashFixups = nativeBindings.applyBashFixups;
|
|
40
40
|
export const applyOwnerOnlyFdSecurity = nativeBindings.applyOwnerOnlyFdSecurity;
|
|
41
41
|
export const applyOwnerOnlyPathSecurity = nativeBindings.applyOwnerOnlyPathSecurity;
|
package/native/loader-state.d.ts
CHANGED
|
@@ -88,7 +88,9 @@ export function loadFromCandidates<T>(input: LoadFromCandidatesInput<T>): LoadFr
|
|
|
88
88
|
export interface CachedEmbeddedExtractionIsFreshInput {
|
|
89
89
|
targetPath: string;
|
|
90
90
|
embeddedPath: string;
|
|
91
|
+
expectedDigest: string | null | undefined;
|
|
91
92
|
sizeOf: (path: string) => number | null;
|
|
93
|
+
digestOf: (path: string) => string | null;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export function cachedEmbeddedExtractionIsFresh(input: CachedEmbeddedExtractionIsFreshInput): boolean;
|
|
@@ -136,6 +138,7 @@ export interface LoadNativeOptions {
|
|
|
136
138
|
acquireStagedCandidateLease?: (candidate: string) => () => void;
|
|
137
139
|
requireCandidate?: (candidate: string) => Record<string, unknown>;
|
|
138
140
|
validateCandidate?: (bindings: Record<string, unknown>, candidate: string) => void;
|
|
141
|
+
loadNativeLibrary?: (runtime: string) => bigint | number | null;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
export function loadNative(options?: LoadNativeOptions): Record<string, unknown>;
|
package/native/loader-state.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import embeddedDiagnosticArtifacts from "./diagnostic-artifact-embedded.json" with { type: "json" };
|
|
8
9
|
import packageJson from "../package.json" with { type: "json" };
|
|
9
10
|
import { embeddedAddon } from "./embedded-addon.js";
|
|
10
11
|
|
|
@@ -32,13 +33,14 @@ import { embeddedAddon } from "./embedded-addon.js";
|
|
|
32
33
|
* post-build `--reset` stub) is the authoritative compiled-mode signal.
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
|
-
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
36
|
+
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64"];
|
|
36
37
|
const OPTIONAL_PACKAGE_BY_PLATFORM_TAG = {
|
|
37
38
|
"darwin-arm64": "@gajae-code/natives-darwin-arm64",
|
|
38
39
|
"darwin-x64": "@gajae-code/natives-darwin-x64",
|
|
39
40
|
"linux-arm64": "@gajae-code/natives-linux-arm64",
|
|
40
41
|
"linux-x64": "@gajae-code/natives-linux-x64",
|
|
41
42
|
"win32-x64": "@gajae-code/natives-win32-x64",
|
|
43
|
+
"win32-arm64": "@gajae-code/natives-win32-arm64",
|
|
42
44
|
};
|
|
43
45
|
const WINDOWS_GENERIC_READ = 0x80000000;
|
|
44
46
|
const WINDOWS_FILE_SHARE_READ = 0x00000001;
|
|
@@ -138,6 +140,32 @@ function validateStagedCandidate(ctx, candidate) {
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
function preloadSiblingNativeRuntime(ctx, candidate, loadLibrary) {
|
|
144
|
+
if (process.platform !== "win32") return;
|
|
145
|
+
// Staging copies only the .node into the versioned cache and drops the
|
|
146
|
+
// package directory from the candidate list. The DLL stays beside the
|
|
147
|
+
// recorded source addon, not beside that cache copy or bun.exe.
|
|
148
|
+
const sourcePath = ctx.stagedSourceSnapshots?.get(candidate)?.sourcePath;
|
|
149
|
+
const runtimes = [
|
|
150
|
+
typeof sourcePath === "string" ? path.join(path.dirname(sourcePath), "libunwind.dll") : null,
|
|
151
|
+
path.join(path.dirname(candidate), "libunwind.dll"),
|
|
152
|
+
path.join(path.dirname(process.execPath), "libunwind.dll"),
|
|
153
|
+
];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
for (const runtime of runtimes) {
|
|
156
|
+
if (!runtime || seen.has(runtime) || !fs.existsSync(runtime)) continue;
|
|
157
|
+
seen.add(runtime);
|
|
158
|
+
const handle = loadLibrary(runtime);
|
|
159
|
+
if (handle !== 0 && handle !== 0n && handle != null) return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadSiblingNativeLibrary(runtime) {
|
|
164
|
+
const widePath = new Uint16Array(runtime.length + 1);
|
|
165
|
+
for (let index = 0; index < runtime.length; index++) widePath[index] = runtime.charCodeAt(index);
|
|
166
|
+
return windowsKernel32Bindings().symbols.LoadLibraryW(ptr(widePath));
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
function windowsKernel32Bindings() {
|
|
142
170
|
if (windowsKernel32) return windowsKernel32;
|
|
143
171
|
windowsKernel32 = dlopen("kernel32.dll", {
|
|
@@ -147,6 +175,7 @@ function windowsKernel32Bindings() {
|
|
|
147
175
|
},
|
|
148
176
|
CloseHandle: { args: ["i64"], returns: "u32" },
|
|
149
177
|
GetLastError: { args: [], returns: "u32" },
|
|
178
|
+
LoadLibraryW: { args: ["ptr"], returns: "ptr" },
|
|
150
179
|
});
|
|
151
180
|
return windowsKernel32;
|
|
152
181
|
}
|
|
@@ -368,18 +397,60 @@ export function loadFromCandidates({ candidates, requireCandidate, validateCandi
|
|
|
368
397
|
/**
|
|
369
398
|
* Decide whether a previously extracted embedded addon may be reused. A cached
|
|
370
399
|
* extraction from an earlier build of the same version carries the same version
|
|
371
|
-
* sentinel
|
|
372
|
-
*
|
|
373
|
-
* a
|
|
374
|
-
*
|
|
400
|
+
* sentinel and may have the same byte size, so freshness also requires its digest
|
|
401
|
+
* to match the package-owned embedded build record. `sizeOf` returns a path's
|
|
402
|
+
* byte size; `digestOf` returns a SHA-256 hex digest. Both return `null` when the
|
|
403
|
+
* path cannot be inspected.
|
|
404
|
+
* @param {{ targetPath: string; embeddedPath: string; expectedDigest: string | null | undefined; sizeOf: (path: string) => number | null; digestOf: (path: string) => string | null }} input
|
|
375
405
|
* @returns {boolean}
|
|
376
406
|
*/
|
|
377
|
-
export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, sizeOf }) {
|
|
407
|
+
export function cachedEmbeddedExtractionIsFresh({ targetPath, embeddedPath, expectedDigest, sizeOf, digestOf }) {
|
|
408
|
+
if (typeof expectedDigest !== "string" || !/^[0-9a-f]{64}$/.test(expectedDigest)) return false;
|
|
378
409
|
const cachedSize = sizeOf(targetPath);
|
|
379
410
|
if (cachedSize === null) return false;
|
|
380
411
|
const embeddedSize = sizeOf(embeddedPath);
|
|
381
412
|
if (embeddedSize === null) return false;
|
|
382
|
-
|
|
413
|
+
if (cachedSize !== embeddedSize) return false;
|
|
414
|
+
return digestOf(targetPath) === expectedDigest;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function sha256File(file) {
|
|
418
|
+
let fd = -1;
|
|
419
|
+
try {
|
|
420
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
421
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
422
|
+
const before = fs.fstatSync(fd);
|
|
423
|
+
if (!before.isFile()) return null;
|
|
424
|
+
const hash = createHash("sha256");
|
|
425
|
+
const buffer = Buffer.allocUnsafe(1 << 20);
|
|
426
|
+
let offset = 0;
|
|
427
|
+
while (offset < before.size) {
|
|
428
|
+
const read = fs.readSync(fd, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
|
|
429
|
+
if (read === 0) return null;
|
|
430
|
+
hash.update(buffer.subarray(0, read));
|
|
431
|
+
offset += read;
|
|
432
|
+
}
|
|
433
|
+
const after = fs.fstatSync(fd);
|
|
434
|
+
if (
|
|
435
|
+
after.dev !== before.dev ||
|
|
436
|
+
after.ino !== before.ino ||
|
|
437
|
+
after.size !== before.size ||
|
|
438
|
+
after.mtimeMs !== before.mtimeMs
|
|
439
|
+
) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return hash.digest("hex");
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
} finally {
|
|
446
|
+
if (fd >= 0) {
|
|
447
|
+
try {
|
|
448
|
+
fs.closeSync(fd);
|
|
449
|
+
} catch {
|
|
450
|
+
// A failed close cannot make an unverified digest admissible.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
383
454
|
}
|
|
384
455
|
|
|
385
456
|
// =========================================================================
|
|
@@ -549,12 +620,16 @@ function maybeExtractEmbeddedAddons(ctx, errors) {
|
|
|
549
620
|
const extracted = [];
|
|
550
621
|
for (const embeddedFile of embeddedAddonCandidates(ctx.selectedVariant)) {
|
|
551
622
|
const targetPath = path.join(ctx.versionedDir, embeddedFile.filename);
|
|
623
|
+
const expectedDigest =
|
|
624
|
+
embeddedDiagnosticArtifacts?.schema === "gjc.diagnostic-artifact" &&
|
|
625
|
+
embeddedDiagnosticArtifacts?.version === embeddedAddon.version
|
|
626
|
+
? embeddedDiagnosticArtifacts?.artifacts?.[embeddedFile.filename]
|
|
627
|
+
: undefined;
|
|
552
628
|
if (fs.existsSync(targetPath)) {
|
|
553
629
|
// Guard against intra-version drift: a cached extraction written by an earlier
|
|
554
|
-
// build of the same version carries the same version sentinel
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// embedded payload size and re-extract otherwise.
|
|
630
|
+
// build of the same version carries the same version sentinel and may even have
|
|
631
|
+
// the same byte size. The embedded digest is the source of truth, so reuse the
|
|
632
|
+
// cached file only when its bytes match that exact build.
|
|
558
633
|
const sizeOf = candidate => {
|
|
559
634
|
try {
|
|
560
635
|
return fs.statSync(candidate).size;
|
|
@@ -562,7 +637,15 @@ function maybeExtractEmbeddedAddons(ctx, errors) {
|
|
|
562
637
|
return null;
|
|
563
638
|
}
|
|
564
639
|
};
|
|
565
|
-
if (
|
|
640
|
+
if (
|
|
641
|
+
cachedEmbeddedExtractionIsFresh({
|
|
642
|
+
targetPath,
|
|
643
|
+
embeddedPath: embeddedFile.filePath,
|
|
644
|
+
expectedDigest,
|
|
645
|
+
sizeOf,
|
|
646
|
+
digestOf: sha256File,
|
|
647
|
+
})
|
|
648
|
+
) {
|
|
566
649
|
extracted.push(targetPath);
|
|
567
650
|
continue;
|
|
568
651
|
}
|
|
@@ -910,6 +993,7 @@ export function loadNative(options = {}) {
|
|
|
910
993
|
}
|
|
911
994
|
try {
|
|
912
995
|
validateStagedCandidate(ctx, candidate);
|
|
996
|
+
preloadSiblingNativeRuntime(ctx, candidate, options.loadNativeLibrary ?? loadSiblingNativeLibrary);
|
|
913
997
|
const bindings = options.requireCandidate ? options.requireCandidate(candidate) : require_(candidate);
|
|
914
998
|
if (typeof bindings.__gjcInstallTokioRuntime === "function") {
|
|
915
999
|
bindings.__gjcInstallTokioRuntime();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gajae-code/natives",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.12",
|
|
4
4
|
"description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "bun scripts/build-native.ts",
|
|
33
33
|
"check": "biome check . && bun run check:types",
|
|
34
|
-
"check:types": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"check:types": "bun ../coding-agent/scripts/generate-docs-index.ts && tsc -p tsconfig.json --noEmit",
|
|
35
35
|
"lint": "biome lint .",
|
|
36
36
|
"test": "bun test",
|
|
37
37
|
"fix": "biome check --write --unsafe .",
|
|
@@ -61,15 +61,16 @@
|
|
|
61
61
|
"native/diagnostic-loader.d.ts",
|
|
62
62
|
"native/diagnostic-loader.js",
|
|
63
63
|
"native/diagnostic-artifact.json",
|
|
64
|
+
"native/diagnostic-artifact-embedded.json",
|
|
64
65
|
"native/embedded-addon.js",
|
|
65
66
|
"README.md"
|
|
66
67
|
],
|
|
67
68
|
"optionalDependencies": {
|
|
68
|
-
"@gajae-code/natives-darwin-arm64": "0.18.
|
|
69
|
-
"@gajae-code/natives-darwin-x64": "0.18.
|
|
70
|
-
"@gajae-code/natives-linux-arm64": "0.18.
|
|
71
|
-
"@gajae-code/natives-linux-x64": "0.18.
|
|
72
|
-
"@gajae-code/natives-win32-x64": "0.18.
|
|
69
|
+
"@gajae-code/natives-darwin-arm64": "0.18.12",
|
|
70
|
+
"@gajae-code/natives-darwin-x64": "0.18.12",
|
|
71
|
+
"@gajae-code/natives-linux-arm64": "0.18.12",
|
|
72
|
+
"@gajae-code/natives-linux-x64": "0.18.12",
|
|
73
|
+
"@gajae-code/natives-win32-x64": "0.18.12"
|
|
73
74
|
},
|
|
74
75
|
"exports": {
|
|
75
76
|
".": {
|