@nifrajs/node 4.0.2 → 5.0.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 +17 -4
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +216 -76
- package/dist/index.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +64 -0
- package/dist/open-resolved.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +241 -77
- package/src/open-resolved.ts +66 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Kept identical to packages/web/src/open-resolved.ts (a web test compares them); this package
|
|
2
|
+
// has no dependency through which to share it.
|
|
3
|
+
import { constants as FS } from "node:fs"
|
|
4
|
+
import { type FileHandle, open, readlink, realpath, stat } from "node:fs/promises"
|
|
5
|
+
|
|
6
|
+
// Darwin's open(2) flag refusing a symlink in ANY component; `O_NOFOLLOW` covers only the last one.
|
|
7
|
+
const O_NOFOLLOW_ANY = 0x20000000
|
|
8
|
+
// Deno's node:fs rejects flags it does not know with EINVAL; the first refusal turns the flag off.
|
|
9
|
+
let noFollowAny: boolean | undefined
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Open a name `realpath` already resolved, refusing it when any component became a symlink since.
|
|
13
|
+
* A local writer inside the served tree can swap an intermediate directory for a link after the
|
|
14
|
+
* containment check; `O_NOFOLLOW` alone would follow it. Darwin refuses such a path in the kernel;
|
|
15
|
+
* Linux reads back the name the descriptor holds. Elsewhere, Windows included, no file API can open
|
|
16
|
+
* without following a middle link or name an open descriptor's path, so the check in
|
|
17
|
+
* {@link opensAt} is a platform limit: it narrows the race to repeated swaps but cannot close it.
|
|
18
|
+
*/
|
|
19
|
+
export async function openResolved(realFile: string): Promise<FileHandle> {
|
|
20
|
+
// Read per open, not at load: this module can sit in a client bundle graph, where `process` is
|
|
21
|
+
// undefined.
|
|
22
|
+
if (noFollowAny ?? process.platform === "darwin") {
|
|
23
|
+
try {
|
|
24
|
+
return await open(realFile, FS.O_RDONLY | O_NOFOLLOW_ANY)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!(error instanceof Error && "code" in error && error.code === "EINVAL")) throw error
|
|
27
|
+
noFollowAny = false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const handle = await open(realFile, FS.O_RDONLY | FS.O_NOFOLLOW)
|
|
31
|
+
try {
|
|
32
|
+
if (await opensAt(handle, realFile)) return handle
|
|
33
|
+
} catch {}
|
|
34
|
+
await handle.close()
|
|
35
|
+
throw Object.assign(new Error(`${realFile} moved while it was opened`), { code: "ELOOP" })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether `handle` is the file at `realFile` with no symlink in its path. */
|
|
39
|
+
export async function opensAt(
|
|
40
|
+
handle: Pick<FileHandle, "fd" | "stat">,
|
|
41
|
+
realFile: string,
|
|
42
|
+
): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
return (await readlink(`/proc/self/fd/${handle.fd}`)) === realFile
|
|
45
|
+
} catch {
|
|
46
|
+
return sameFileAt(handle, realFile)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* {@link opensAt} where no descriptor's path can be read. Exported for its test, which runs it on
|
|
52
|
+
* every host.
|
|
53
|
+
*/
|
|
54
|
+
export async function sameFileAt(
|
|
55
|
+
handle: Pick<FileHandle, "stat">,
|
|
56
|
+
realFile: string,
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
// One step after another, so passing takes a swap back and a swap again between steps: the name
|
|
59
|
+
// resolves to itself, then to the opened file, then to itself again. Identities are compared as
|
|
60
|
+
// bigints because a Windows file index can exceed 2^53.
|
|
61
|
+
if ((await realpath(realFile)) !== realFile) return false
|
|
62
|
+
const opened = await handle.stat({ bigint: true })
|
|
63
|
+
const named = await stat(realFile, { bigint: true })
|
|
64
|
+
if (opened.dev !== named.dev || opened.ino !== named.ino) return false
|
|
65
|
+
return (await realpath(realFile)) === realFile
|
|
66
|
+
}
|