@gmickel/gno 1.34.5 → 1.35.0
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 +22 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -1
- package/spec/cli.md +43 -0
- package/spec/output-schemas/mcp-job-status.schema.json +6 -2
- package/src/config/index.ts +4 -0
- package/src/config/types.ts +14 -0
- package/src/core/path-rules.ts +34 -0
- package/src/ingestion/index.ts +21 -0
- package/src/ingestion/record-container.ts +23 -1
- package/src/ingestion/source-availability/darwin-io.ts +295 -0
- package/src/ingestion/source-availability/darwin-path.ts +58 -0
- package/src/ingestion/source-availability/directory.ts +402 -0
- package/src/ingestion/source-availability/index.ts +74 -0
- package/src/ingestion/source-availability/readers.ts +360 -0
- package/src/ingestion/source-availability/resolve.ts +28 -0
- package/src/ingestion/source-availability/types.ts +170 -0
- package/src/ingestion/sync.ts +565 -108
- package/src/ingestion/types.ts +45 -3
- package/src/ingestion/walker.ts +263 -5
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +359 -0
- package/src/serve/watch-reconciliation-fallback.ts +434 -0
- package/src/serve/watch-reconciliation-shared.ts +348 -0
- package/src/serve/watch-reconciliation.ts +129 -0
- package/src/serve/watch-service-events.ts +261 -0
- package/src/serve/watch-service-flush-generation.ts +140 -0
- package/src/serve/watch-service-flush-helpers.ts +147 -0
- package/src/serve/watch-service-flush.ts +443 -0
- package/src/serve/watch-service-hosts.ts +109 -0
- package/src/serve/watch-service-lifecycle.ts +221 -0
- package/src/serve/watch-service-run-flush.ts +236 -0
- package/src/serve/watch-service-snapshot.ts +125 -0
- package/src/serve/watch-service-state.ts +146 -0
- package/src/serve/watch-service.ts +266 -306
- package/src/serve/watch-snapshot-availability.ts +51 -0
- package/src/serve/watch-snapshot-handles.ts +365 -0
- package/src/serve/watch-snapshot-libc.ts +510 -0
- package/src/serve/watch-snapshot-ops.ts +541 -0
- package/src/serve/watch-snapshot-resolve.ts +246 -0
- package/src/serve/watch-snapshot-scan.ts +300 -0
- package/src/serve/watch-snapshot-types.ts +392 -0
- package/src/serve/watch-snapshot.ts +51 -0
- package/src/store/index.ts +1 -1
- package/src/store/sqlite/adapter.ts +191 -0
- package/src/store/types.ts +66 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.5.zip.sha256 +0 -1
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Libc FFI boundary for anchored watcher directory handles.
|
|
3
|
+
*
|
|
4
|
+
* Loads system libc with platform-correct sonames, parses dirent records
|
|
5
|
+
* using d_reclen/d_namlen bounds, and clears errno around readdir.
|
|
6
|
+
*
|
|
7
|
+
* @module src/serve/watch-snapshot-libc
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// bun:ffi — no Bun high-level openat/fdopendir; libc is required for fd-relative ops
|
|
11
|
+
import { type Pointer, dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi";
|
|
12
|
+
// node:fs — flag constants for openat; no Bun equivalent
|
|
13
|
+
import { constants as fsConstants } from "node:fs";
|
|
14
|
+
|
|
15
|
+
export type LibcSymbols = {
|
|
16
|
+
openat: (dirfd: number, path: Pointer, flags: number) => number;
|
|
17
|
+
fstatat: (
|
|
18
|
+
dirfd: number,
|
|
19
|
+
path: Pointer,
|
|
20
|
+
statBuffer: Pointer,
|
|
21
|
+
flags: number
|
|
22
|
+
) => number;
|
|
23
|
+
dup: (fd: number) => number;
|
|
24
|
+
close: (fd: number) => number;
|
|
25
|
+
fdopendir: (fd: number) => Pointer | null;
|
|
26
|
+
readdir: (dirp: Pointer) => Pointer | null;
|
|
27
|
+
closedir: (dirp: Pointer) => number;
|
|
28
|
+
errnoPtr: () => Pointer;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Platform dirent field layout (little-endian). */
|
|
32
|
+
export type DirentLayout = {
|
|
33
|
+
/** Offset of d_reclen (uint16). Both Darwin and Linux: 16. */
|
|
34
|
+
dReclenOffset: number;
|
|
35
|
+
/**
|
|
36
|
+
* Offset of d_namlen (uint16) when present.
|
|
37
|
+
* Darwin: 18. Linux glibc has no d_namlen (null).
|
|
38
|
+
*/
|
|
39
|
+
dNamlenOffset: number | null;
|
|
40
|
+
/** Offset of d_name char array. Darwin: 21. Linux glibc: 19. */
|
|
41
|
+
dNameOffset: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type LoadedLibc = {
|
|
45
|
+
/** Strong reference so FFI symbols stay live for the process lifetime. */
|
|
46
|
+
library: ReturnType<typeof dlopen>;
|
|
47
|
+
symbols: LibcSymbols;
|
|
48
|
+
dirent: DirentLayout;
|
|
49
|
+
openChildFlags: number;
|
|
50
|
+
openDirFlags: number;
|
|
51
|
+
atSymlinkNoFollow: number;
|
|
52
|
+
statLayout: NativeStatLayout;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type NativeStatLayout = {
|
|
56
|
+
size: number;
|
|
57
|
+
devOffset: number;
|
|
58
|
+
devBytes: 4 | 8;
|
|
59
|
+
modeOffset: number;
|
|
60
|
+
modeBytes: 2 | 4;
|
|
61
|
+
inoOffset: number;
|
|
62
|
+
sizeOffset: number;
|
|
63
|
+
mtimeSecOffset: number;
|
|
64
|
+
mtimeNsecOffset: number;
|
|
65
|
+
ctimeSecOffset: number;
|
|
66
|
+
ctimeNsecOffset: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type FdRelativeStat = {
|
|
70
|
+
isFile(): boolean;
|
|
71
|
+
isDirectory(): boolean;
|
|
72
|
+
isSymbolicLink(): boolean;
|
|
73
|
+
dev: bigint | number;
|
|
74
|
+
ino: bigint;
|
|
75
|
+
size: bigint;
|
|
76
|
+
mtimeNs: bigint;
|
|
77
|
+
ctimeNs: bigint;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let cachedLibc: LoadedLibc | null | undefined;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deterministic libc soname candidates.
|
|
84
|
+
* Never rely solely on `libc.<suffix>` (Linux libc.so is often a linker script).
|
|
85
|
+
*/
|
|
86
|
+
export function libcLoadCandidates(
|
|
87
|
+
platform: string = process.platform
|
|
88
|
+
): string[] {
|
|
89
|
+
if (platform === "darwin") {
|
|
90
|
+
return ["libSystem.B.dylib", "libc.dylib"];
|
|
91
|
+
}
|
|
92
|
+
if (platform === "linux") {
|
|
93
|
+
return ["libc.so.6", "libc.so"];
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function direntLayoutFor(platform: string): DirentLayout {
|
|
99
|
+
if (platform === "darwin") {
|
|
100
|
+
// struct dirent: d_ino(8) d_seekoff(8) d_reclen(2) d_namlen(2) d_type(1) d_name[]
|
|
101
|
+
return { dReclenOffset: 16, dNamlenOffset: 18, dNameOffset: 21 };
|
|
102
|
+
}
|
|
103
|
+
// glibc struct dirent: d_ino(8) d_off(8) d_reclen(2) d_type(1) d_name[]
|
|
104
|
+
return { dReclenOffset: 16, dNamlenOffset: null, dNameOffset: 19 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function openLibcSymbols(
|
|
108
|
+
path: string,
|
|
109
|
+
errnoName: string
|
|
110
|
+
): ReturnType<typeof dlopen> | null {
|
|
111
|
+
try {
|
|
112
|
+
return dlopen(path, {
|
|
113
|
+
openat: {
|
|
114
|
+
args: [FFIType.i32, FFIType.cstring, FFIType.i32],
|
|
115
|
+
returns: FFIType.i32,
|
|
116
|
+
},
|
|
117
|
+
fstatat: {
|
|
118
|
+
args: [FFIType.i32, FFIType.cstring, FFIType.ptr, FFIType.i32],
|
|
119
|
+
returns: FFIType.i32,
|
|
120
|
+
},
|
|
121
|
+
dup: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
122
|
+
close: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
123
|
+
fdopendir: { args: [FFIType.i32], returns: FFIType.ptr },
|
|
124
|
+
readdir: { args: [FFIType.ptr], returns: FFIType.ptr },
|
|
125
|
+
closedir: { args: [FFIType.ptr], returns: FFIType.i32 },
|
|
126
|
+
[errnoName]: { args: [], returns: FFIType.ptr },
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function loadLibc(): LoadedLibc | null {
|
|
134
|
+
if (cachedLibc !== undefined) {
|
|
135
|
+
return cachedLibc;
|
|
136
|
+
}
|
|
137
|
+
if (process.platform === "win32") {
|
|
138
|
+
cachedLibc = null;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const platform = process.platform;
|
|
143
|
+
const errnoName = platform === "darwin" ? "__error" : "__errno_location";
|
|
144
|
+
const candidates = libcLoadCandidates(platform);
|
|
145
|
+
|
|
146
|
+
let library: ReturnType<typeof dlopen> | null = null;
|
|
147
|
+
for (const candidate of candidates) {
|
|
148
|
+
library = openLibcSymbols(candidate, errnoName);
|
|
149
|
+
if (library) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!library) {
|
|
154
|
+
cachedLibc = null;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const raw = library.symbols as Record<string, unknown>;
|
|
159
|
+
const openat = asLibcFn<LibcSymbols["openat"]>(raw.openat);
|
|
160
|
+
const fstatat = asLibcFn<LibcSymbols["fstatat"]>(raw.fstatat);
|
|
161
|
+
const dup = asLibcFn<LibcSymbols["dup"]>(raw.dup);
|
|
162
|
+
const close = asLibcFn<LibcSymbols["close"]>(raw.close);
|
|
163
|
+
const fdopendir = asLibcFn<LibcSymbols["fdopendir"]>(raw.fdopendir);
|
|
164
|
+
const readdir = asLibcFn<LibcSymbols["readdir"]>(raw.readdir);
|
|
165
|
+
const closedir = asLibcFn<LibcSymbols["closedir"]>(raw.closedir);
|
|
166
|
+
const errnoFn = asLibcFn<LibcSymbols["errnoPtr"]>(raw[errnoName]);
|
|
167
|
+
if (
|
|
168
|
+
!openat ||
|
|
169
|
+
!fstatat ||
|
|
170
|
+
!dup ||
|
|
171
|
+
!close ||
|
|
172
|
+
!fdopendir ||
|
|
173
|
+
!readdir ||
|
|
174
|
+
!closedir ||
|
|
175
|
+
!errnoFn
|
|
176
|
+
) {
|
|
177
|
+
cachedLibc = null;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const O_RDONLY = fsConstants.O_RDONLY;
|
|
182
|
+
const O_DIRECTORY = fsConstants.O_DIRECTORY;
|
|
183
|
+
const O_NOFOLLOW = fsConstants.O_NOFOLLOW;
|
|
184
|
+
|
|
185
|
+
const openDirFlags = O_RDONLY | O_DIRECTORY | O_NOFOLLOW;
|
|
186
|
+
const atSymlinkNoFollow = platform === "darwin" ? 0x20 : 0x100;
|
|
187
|
+
const statLayout = nativeStatLayout(platform, process.arch);
|
|
188
|
+
|
|
189
|
+
cachedLibc = {
|
|
190
|
+
library,
|
|
191
|
+
symbols: {
|
|
192
|
+
openat,
|
|
193
|
+
fstatat,
|
|
194
|
+
dup,
|
|
195
|
+
close,
|
|
196
|
+
fdopendir,
|
|
197
|
+
readdir,
|
|
198
|
+
closedir,
|
|
199
|
+
errnoPtr: errnoFn,
|
|
200
|
+
},
|
|
201
|
+
dirent: direntLayoutFor(platform),
|
|
202
|
+
openChildFlags: openDirFlags,
|
|
203
|
+
openDirFlags,
|
|
204
|
+
atSymlinkNoFollow,
|
|
205
|
+
statLayout,
|
|
206
|
+
};
|
|
207
|
+
return cachedLibc;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function nativeStatLayout(platform: string, arch: string): NativeStatLayout {
|
|
211
|
+
if (platform === "darwin") {
|
|
212
|
+
return {
|
|
213
|
+
size: 144,
|
|
214
|
+
devOffset: 0,
|
|
215
|
+
devBytes: 4,
|
|
216
|
+
modeOffset: 4,
|
|
217
|
+
modeBytes: 2,
|
|
218
|
+
inoOffset: 8,
|
|
219
|
+
sizeOffset: 96,
|
|
220
|
+
mtimeSecOffset: 48,
|
|
221
|
+
mtimeNsecOffset: 56,
|
|
222
|
+
ctimeSecOffset: 64,
|
|
223
|
+
ctimeNsecOffset: 72,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
size: 144,
|
|
228
|
+
devOffset: 0,
|
|
229
|
+
devBytes: 8,
|
|
230
|
+
modeOffset: arch === "x64" ? 24 : 16,
|
|
231
|
+
modeBytes: 4,
|
|
232
|
+
inoOffset: 8,
|
|
233
|
+
sizeOffset: 48,
|
|
234
|
+
mtimeSecOffset: 88,
|
|
235
|
+
mtimeNsecOffset: 96,
|
|
236
|
+
ctimeSecOffset: 104,
|
|
237
|
+
ctimeNsecOffset: 112,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function asLibcFn<T extends (...args: never[]) => unknown>(
|
|
242
|
+
value: unknown
|
|
243
|
+
): T | null {
|
|
244
|
+
if (typeof value !== "function") {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return value as T;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function errnoError(
|
|
251
|
+
errno: number,
|
|
252
|
+
syscall: string,
|
|
253
|
+
path: string
|
|
254
|
+
): NodeJS.ErrnoException {
|
|
255
|
+
const error = new Error(
|
|
256
|
+
`${syscall} failed (errno ${errno}): ${path}`
|
|
257
|
+
) as NodeJS.ErrnoException;
|
|
258
|
+
error.errno = errno;
|
|
259
|
+
error.syscall = syscall;
|
|
260
|
+
error.path = path;
|
|
261
|
+
// Map common POSIX errno values used on darwin/linux.
|
|
262
|
+
if (errno === 2) {
|
|
263
|
+
error.code = "ENOENT";
|
|
264
|
+
} else if (errno === 13) {
|
|
265
|
+
error.code = "EACCES";
|
|
266
|
+
} else if (errno === 20) {
|
|
267
|
+
error.code = "ENOTDIR";
|
|
268
|
+
} else if (errno === 40 || errno === 62) {
|
|
269
|
+
// Linux ELOOP=40, Darwin ELOOP=62
|
|
270
|
+
error.code = "ELOOP";
|
|
271
|
+
} else if (errno === 17) {
|
|
272
|
+
error.code = "EEXIST";
|
|
273
|
+
} else {
|
|
274
|
+
error.code = "EIO";
|
|
275
|
+
}
|
|
276
|
+
return error;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function readErrno(libc: LoadedLibc): number {
|
|
280
|
+
const p = libc.symbols.errnoPtr();
|
|
281
|
+
if (!p) {
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
const view = new Int32Array(toArrayBuffer(p, 0, 4));
|
|
285
|
+
return view[0] ?? 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Clear thread errno so readdir EOF (null + errno 0) is distinguishable from error. */
|
|
289
|
+
export function clearErrno(libc: LoadedLibc): void {
|
|
290
|
+
const p = libc.symbols.errnoPtr();
|
|
291
|
+
if (!p) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const view = new Int32Array(toArrayBuffer(p, 0, 4));
|
|
295
|
+
view[0] = 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Parse d_name from a dirent pointer using platform reclen/namlen bounds.
|
|
300
|
+
* Returns null for malformed records (caller treats as scan failure).
|
|
301
|
+
*/
|
|
302
|
+
export function parseDirentName(
|
|
303
|
+
ent: Pointer,
|
|
304
|
+
layout: DirentLayout
|
|
305
|
+
): string | null {
|
|
306
|
+
// Header must cover through d_name start (includes reclen and optional namlen).
|
|
307
|
+
if (layout.dNameOffset < 18) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
let header: DataView;
|
|
311
|
+
try {
|
|
312
|
+
header = new DataView(toArrayBuffer(ent, 0, layout.dNameOffset));
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const reclen = header.getUint16(layout.dReclenOffset, true);
|
|
318
|
+
// Record must at least hold the fixed header + one byte of d_name room.
|
|
319
|
+
if (reclen < layout.dNameOffset + 1) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const maxNameBytes = reclen - layout.dNameOffset;
|
|
323
|
+
// POSIX NAME_MAX is typically 255; reject absurd reclen-derived lengths.
|
|
324
|
+
if (maxNameBytes > 1024) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let nameLen: number;
|
|
329
|
+
if (layout.dNamlenOffset !== null) {
|
|
330
|
+
const namlen = header.getUint16(layout.dNamlenOffset, true);
|
|
331
|
+
if (namlen > maxNameBytes) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
nameLen = namlen;
|
|
335
|
+
} else {
|
|
336
|
+
let bytes: Uint8Array;
|
|
337
|
+
try {
|
|
338
|
+
bytes = new Uint8Array(
|
|
339
|
+
toArrayBuffer(ent, layout.dNameOffset, maxNameBytes)
|
|
340
|
+
);
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
let end = 0;
|
|
345
|
+
while (end < bytes.length && bytes[end] !== 0) {
|
|
346
|
+
end += 1;
|
|
347
|
+
}
|
|
348
|
+
// Linux dirents are NUL-terminated within d_reclen; missing NUL → malformed.
|
|
349
|
+
if (end === bytes.length) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
return Buffer.from(bytes.subarray(0, end)).toString();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (nameLen === 0) {
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const nameBytes = new Uint8Array(
|
|
360
|
+
toArrayBuffer(ent, layout.dNameOffset, nameLen)
|
|
361
|
+
);
|
|
362
|
+
return Buffer.from(nameBytes).toString();
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Enumerate child names via fdopendir/readdir, stopping after `maxNames + 1`
|
|
370
|
+
* observed entries so overflow is proven without materializing an unbounded list.
|
|
371
|
+
*/
|
|
372
|
+
export function readDirNames(
|
|
373
|
+
libc: LoadedLibc,
|
|
374
|
+
dirfd: number,
|
|
375
|
+
maxNames: number
|
|
376
|
+
): { status: "ok"; names: string[] } | { status: "overflow" } {
|
|
377
|
+
if (!Number.isInteger(maxNames) || maxNames < 0) {
|
|
378
|
+
throw Object.assign(new Error("maxNames must be a non-negative integer"), {
|
|
379
|
+
code: "EINVAL",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// fdopendir consumes the fd on success — operate on a dup so the handle stays live.
|
|
383
|
+
const dupFd = libc.symbols.dup(dirfd);
|
|
384
|
+
if (dupFd < 0) {
|
|
385
|
+
throw errnoError(readErrno(libc), "dup", "");
|
|
386
|
+
}
|
|
387
|
+
const dirp = libc.symbols.fdopendir(dupFd);
|
|
388
|
+
if (!dirp) {
|
|
389
|
+
libc.symbols.close(dupFd);
|
|
390
|
+
throw errnoError(readErrno(libc), "fdopendir", "");
|
|
391
|
+
}
|
|
392
|
+
const names: string[] = [];
|
|
393
|
+
try {
|
|
394
|
+
while (true) {
|
|
395
|
+
clearErrno(libc);
|
|
396
|
+
const ent = libc.symbols.readdir(dirp);
|
|
397
|
+
if (!ent) {
|
|
398
|
+
const err = readErrno(libc);
|
|
399
|
+
if (err !== 0) {
|
|
400
|
+
throw errnoError(err, "readdir", "");
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
const name = parseDirentName(ent, libc.dirent);
|
|
405
|
+
if (name === null) {
|
|
406
|
+
throw Object.assign(new Error("Malformed dirent record"), {
|
|
407
|
+
code: "EIO",
|
|
408
|
+
syscall: "readdir",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (name === "" || name === "." || name === "..") {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
// maxNames+1th name proves overflow; do not store it or continue.
|
|
415
|
+
if (names.length >= maxNames) {
|
|
416
|
+
return { status: "overflow" };
|
|
417
|
+
}
|
|
418
|
+
names.push(name);
|
|
419
|
+
}
|
|
420
|
+
} finally {
|
|
421
|
+
libc.symbols.closedir(dirp);
|
|
422
|
+
}
|
|
423
|
+
return { status: "ok", names };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function openatOrThrow(
|
|
427
|
+
libc: LoadedLibc,
|
|
428
|
+
dirfd: number,
|
|
429
|
+
name: string,
|
|
430
|
+
flags: number,
|
|
431
|
+
syscall: string
|
|
432
|
+
): number {
|
|
433
|
+
if (name.includes("\0") || name.includes("/") || name.includes("\\")) {
|
|
434
|
+
throw Object.assign(new Error(`Invalid directory entry name: ${name}`), {
|
|
435
|
+
code: "EINVAL",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const nameBuf = Buffer.from(`${name}\0`);
|
|
439
|
+
const fd = libc.symbols.openat(dirfd, ptr(nameBuf), flags);
|
|
440
|
+
if (fd < 0) {
|
|
441
|
+
throw errnoError(readErrno(libc), syscall, name);
|
|
442
|
+
}
|
|
443
|
+
return fd;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Read no-follow metadata for one direct child without opening its content.
|
|
448
|
+
* This avoids materializing dataless File Provider files while retaining the
|
|
449
|
+
* parent-fd anchoring and symlink identity required by watcher snapshots.
|
|
450
|
+
*/
|
|
451
|
+
export function statatNoFollowOrThrow(
|
|
452
|
+
libc: LoadedLibc,
|
|
453
|
+
dirfd: number,
|
|
454
|
+
name: string
|
|
455
|
+
): FdRelativeStat {
|
|
456
|
+
if (name.includes("\0") || name.includes("/") || name.includes("\\")) {
|
|
457
|
+
throw Object.assign(new Error(`Invalid directory entry name: ${name}`), {
|
|
458
|
+
code: "EINVAL",
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const nameBuffer = Buffer.from(`${name}\0`);
|
|
462
|
+
const statBuffer = Buffer.alloc(libc.statLayout.size);
|
|
463
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
464
|
+
const result = libc.symbols.fstatat(
|
|
465
|
+
dirfd,
|
|
466
|
+
ptr(nameBuffer),
|
|
467
|
+
ptr(statBuffer),
|
|
468
|
+
libc.atSymlinkNoFollow
|
|
469
|
+
);
|
|
470
|
+
if (result >= 0) {
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
const errno = readErrno(libc);
|
|
474
|
+
// Darwin/Bun can transiently surface ENOENT during large fd-relative stat
|
|
475
|
+
// batches. Retry the same anchored lookup once. A child that truly
|
|
476
|
+
// disappeared still fails on the second probe, so callers retain the
|
|
477
|
+
// scan-failed authority boundary instead of accepting a partial image.
|
|
478
|
+
if (errno !== 2 || attempt > 0) {
|
|
479
|
+
throw errnoError(errno, "fstatat", name);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const layout = libc.statLayout;
|
|
484
|
+
const mode =
|
|
485
|
+
layout.modeBytes === 2
|
|
486
|
+
? statBuffer.readUInt16LE(layout.modeOffset)
|
|
487
|
+
: statBuffer.readUInt32LE(layout.modeOffset);
|
|
488
|
+
const fileType = mode & 0o170_000;
|
|
489
|
+
const dev =
|
|
490
|
+
layout.devBytes === 4
|
|
491
|
+
? statBuffer.readUInt32LE(layout.devOffset)
|
|
492
|
+
: statBuffer.readBigUInt64LE(layout.devOffset);
|
|
493
|
+
const mtimeNs =
|
|
494
|
+
statBuffer.readBigInt64LE(layout.mtimeSecOffset) * 1_000_000_000n +
|
|
495
|
+
statBuffer.readBigInt64LE(layout.mtimeNsecOffset);
|
|
496
|
+
const ctimeNs =
|
|
497
|
+
statBuffer.readBigInt64LE(layout.ctimeSecOffset) * 1_000_000_000n +
|
|
498
|
+
statBuffer.readBigInt64LE(layout.ctimeNsecOffset);
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
isFile: () => fileType === 0o100_000,
|
|
502
|
+
isDirectory: () => fileType === 0o040_000,
|
|
503
|
+
isSymbolicLink: () => fileType === 0o120_000,
|
|
504
|
+
dev,
|
|
505
|
+
ino: statBuffer.readBigUInt64LE(layout.inoOffset),
|
|
506
|
+
size: statBuffer.readBigInt64LE(layout.sizeOffset),
|
|
507
|
+
mtimeNs,
|
|
508
|
+
ctimeNs,
|
|
509
|
+
};
|
|
510
|
+
}
|