Merge current main into native array loop improvements

- Retain the new filesystem link coverage.
- Apply consistent sanitizer output handling.
This commit is contained in:
Chris Tate
2026-10-04 10:06:02 -05:00
29 changed files with 1139 additions and 146 deletions
+4
View File
@@ -482,6 +482,10 @@ jobs:
env:
SCRIPTC_NO_CACHE: "0"
run: pnpm exec vitest run packages/cli/test/windows-subsystem.test.ts
- name: Windows filesystem links differential
run: >-
pnpm exec vitest run tests/harness/differential.test.ts tests/harness/project-config.test.ts
--testNamePattern "node24-fs-links|filesystem links"
- name: Windows CA-store differential
shell: pwsh
run: |
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "e731e2807c40f14ba1b1",
"artifactVersion": "4236932a217647103f1b",
"rowCount": 3662
}
+26 -26
View File
@@ -458,9 +458,9 @@
"apiEntries": 276,
"static": {
"supported": 0,
"partial": 86,
"partial": 94,
"refused": 0,
"not-implemented": 190,
"not-implemented": 182,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -36105,9 +36105,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -36357,9 +36357,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -36553,9 +36553,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -37337,9 +37337,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Creates hard or symbolic links and reads link targets using string, Buffer, or file URL paths, with encoded results and deferred error-first callbacks; complete filesystem error metadata remains unsupported.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -37673,9 +37673,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Creates hard or symbolic links and reads link targets using string, Buffer, or file URL paths, with encoded results and deferred error-first callbacks; complete filesystem error metadata remains unsupported.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -37925,9 +37925,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Creates hard or symbolic links and reads link targets using string, Buffer, or file URL paths, with encoded results and deferred error-first callbacks; complete filesystem error metadata remains unsupported.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -39073,9 +39073,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
},
"dynamic": {
"status": "partial",
@@ -39381,9 +39381,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 42,
"verify-gap": 2466,
"audit-partial": 705,
"verify-gap": 2458,
"audit-partial": 713,
"classify": 2,
"implement": 80
},
@@ -35505,12 +35505,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.promises.link",
"tests": [
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -35839,12 +35843,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.promises.readlink",
"tests": [
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "partial",
@@ -36105,12 +36114,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.promises.symlink",
"tests": [
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -37103,12 +37117,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.link",
"tests": [
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -37526,12 +37545,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.readlink",
"tests": [
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "partial",
@@ -37851,12 +37875,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.symlink",
"tests": [
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -39033,7 +39062,10 @@
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.linkSync",
"tests": [
"tests/corpus/2950-fs-descriptor-publication.ts"
"tests/corpus/2950-fs-descriptor-publication.ts",
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
@@ -39374,12 +39406,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.readlinkSync",
"tests": [
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "partial",
@@ -39803,12 +39840,17 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.symlinkSync",
"tests": [
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -507,9 +507,9 @@
"apiEntries": 276,
"static": {
"supported": 0,
"partial": 86,
"partial": 94,
"refused": 0,
"not-implemented": 190,
"not-implemented": 182,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -50948,11 +50948,18 @@
"tests/corpus/3136-fs-file-handle-options.ts",
"tests/corpus/3137-fs-vector-callbacks.cjs",
"tests/corpus/fs-native-realpath.ts",
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-timestamps-callbacks.cjs",
"tests/corpus/node24-fs-timestamps-errors.cjs",
"tests/corpus/node24-fs-timestamps-promises.ts",
"tests/corpus/node24-fs-timestamps-symlinks.ts",
"tests/corpus/node24-fs-timestamps.ts",
"tests/fixtures/node-types/fs-links.ts",
"tests/fixtures/node-types/fs-timestamps.ts"
]
},
@@ -52268,8 +52275,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.promises.link",
"tests": [
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -52559,8 +52571,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.promises.readlink",
"tests": [
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "partial",
@@ -52793,8 +52811,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.promises.symlink",
"tests": [
"tests/corpus/node24-fs-links-promises.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-promise-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -53701,8 +53725,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.link",
"tests": [
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -54115,8 +54145,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.readlink",
"tests": [
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "partial",
@@ -54395,8 +54431,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.symlink",
"tests": [
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/corpus/node24-fs-links-options.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
@@ -55553,7 +55595,10 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.linkSync",
"tests": [
"tests/corpus/2950-fs-descriptor-publication.ts"
"tests/corpus/2950-fs-descriptor-publication.ts",
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
@@ -55856,8 +55901,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.readlinkSync",
"tests": [
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "partial",
@@ -56237,8 +56288,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.symlinkSync",
"tests": [
"tests/corpus/node24-fs-links.ts",
"tests/corpus/node24-fs-links-options.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
"tests/fixtures/node-types/fs-links.ts"
]
},
"dynamic": {
"status": "not-implemented",
+3
View File
@@ -601,6 +601,9 @@ function publicDetail(tier) {
}
if (source.startsWith("compiler-dedicated:")) return "Implemented by a dedicated static compiler/runtime path.";
if (source.startsWith("compiler-feature:")) {
if (/^compiler-feature:fs\.fs\.(?:link|symlink|readlink)$/.test(source)) {
return "Creates hard or symbolic links and reads link targets using string, Buffer, or file URL paths, with encoded results and deferred error-first callbacks; complete filesystem error metadata remains unsupported.";
}
if (/^compiler-feature:fs\.(?:fs\.(?:utimes|futimes|lutimes)|filehandle\.utimes)$/.test(source)) {
return "Changes timestamps using numeric seconds, numeric strings, and Dates with tested argument validation; link variants update the symlink itself.";
}
+7 -1
View File
@@ -65,7 +65,12 @@
"node-builtin.fs.readv": ["tests/corpus/3137-fs-vector-callbacks.cjs"],
"node-builtin.fs.writev": ["tests/corpus/3137-fs-vector-callbacks.cjs"],
"node-builtin.fs/promises.open": ["tests/corpus/2686-fs-file-handle.ts", "tests/corpus/3135-fs-file-handle-controls.ts"],
"node-builtin.fs.linkSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
"node-builtin.fs.linkSync": ["tests/corpus/2950-fs-descriptor-publication.ts", "tests/corpus/node24-fs-links.ts", "tests/corpus/node24-fs-links-errors.cjs", "tests/fixtures/node-types/fs-links.ts"],
"node-builtin.fs.symlinkSync": ["tests/corpus/node24-fs-links.ts", "tests/corpus/node24-fs-links-options.cjs", "tests/corpus/node24-fs-links-errors.cjs", "tests/fixtures/node-types/fs-links.ts"],
"node-builtin.fs.readlinkSync": ["tests/corpus/node24-fs-links.ts", "tests/corpus/node24-fs-links-options.cjs", "tests/corpus/node24-fs-links-errors.cjs", "tests/fixtures/node-types/fs-links.ts"],
"node-builtin.fs.promises.link": ["tests/corpus/node24-fs-links-promises.ts", "tests/corpus/node24-fs-links-promise-errors.cjs", "tests/fixtures/node-types/fs-links.ts"],
"node-builtin.fs.promises.symlink": ["tests/corpus/node24-fs-links-promises.ts", "tests/corpus/node24-fs-links-options.cjs", "tests/corpus/node24-fs-links-promise-errors.cjs", "tests/fixtures/node-types/fs-links.ts"],
"node-builtin.fs.promises.readlink": ["tests/corpus/node24-fs-links-promises.ts", "tests/corpus/node24-fs-links-options.cjs", "tests/corpus/node24-fs-links-promise-errors.cjs", "tests/fixtures/node-types/fs-links.ts"],
"node-builtin.http": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2700-http-static-trailers.ts", "tests/corpus/2701-http-static-trailer-wire.ts", "tests/corpus/2702-http-static-trailer-end.ts", "tests/corpus/2957-http-static-response-controls.ts", "tests/fixtures/server/cases/http-trailers/main.ts"],
"node-builtin.http2": ["tests/corpus/2260-http2-constants.cjs"],
"node-builtin.https": ["tests/corpus/2645-https-client-url-argument.ts", "tests/corpus/2690-https-server-timeout-option.ts"],
@@ -126,6 +131,7 @@
{ "chapter": "fs", "symbols": ["FileHandle", "filehandle.fd", "filehandle.close", "filehandle.read", "filehandle.write", "filehandle.readFile", "filehandle.writeFile", "filehandle.appendFile", "filehandle.stat"], "status": "partial", "evidence": ["tests/corpus/2686-fs-file-handle.ts", "tests/corpus/3136-fs-file-handle-options.ts"] },
{ "chapter": "fs", "symbols": ["filehandle.sync", "filehandle.datasync", "filehandle.truncate", "filehandle.chmod", "filehandle.readv", "filehandle.writev"], "status": "partial", "evidence": ["tests/corpus/3135-fs-file-handle-controls.ts"] },
{ "chapter": "fs", "symbols": ["filehandle.utimes"], "status": "partial", "evidence": ["tests/corpus/node24-fs-timestamps-promises.ts", "tests/fixtures/node-types/fs-timestamps.ts"] },
{ "chapter": "fs", "symbols": ["fs.link", "fs.symlink", "fs.readlink"], "status": "partial", "evidence": ["tests/corpus/node24-fs-links-callbacks.cjs", "tests/corpus/node24-fs-links-errors.cjs", "tests/corpus/node24-fs-links-options.cjs", "tests/fixtures/node-types/fs-links.ts"] },
{ "chapter": "fs", "symbols": ["fs.utimes", "fs.futimes", "fs.lutimes"], "status": "partial", "evidence": ["tests/corpus/node24-fs-timestamps-callbacks.cjs", "tests/corpus/node24-fs-timestamps-symlinks.ts", "tests/corpus/node24-fs-timestamps-errors.cjs"] },
{ "chapter": "process", "signatures": ["`process.hrtime([time])`", "`process.hrtime.bigint()`"], "status": "partial", "evidence": ["tests/corpus/process-hrtime.js"] },
{ "chapter": "errors", "signatures": ["`stackTraceLimit` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/error-stack-limit.ts"] },
+17 -1
View File
@@ -1099,7 +1099,18 @@ declare module "node:fs" {
export function chmodSync(path: string, mode: number): void;
export function fchmodSync(fd: number, mode: number): void;
export function fsyncSync(fd: number): void;
export function linkSync(existingPath: string, newPath: string): void;
export function linkSync(existingPath: string | Uint8Array | URL, newPath: string | Uint8Array | URL): void;
export function symlinkSync(target: string | Uint8Array | URL, path: string | Uint8Array | URL, type?: string | null): void;
export function symlink(target: string | Uint8Array | URL, path: string | Uint8Array | URL, callback: (err: NodeJS.ErrnoException | null) => void): void;
export function symlink(target: string | Uint8Array | URL, path: string | Uint8Array | URL, type: string | null | undefined, callback: (err: NodeJS.ErrnoException | null) => void): void;
export function link(existingPath: string | Uint8Array | URL, newPath: string | Uint8Array | URL, callback: (err: NodeJS.ErrnoException | null) => void): void;
export function readlink(path: string | Uint8Array | URL, callback: (err: NodeJS.ErrnoException | null, target: string) => void): void;
export function readlink(path: string | Uint8Array | URL, options: "buffer" | { encoding: "buffer" }, callback: (err: NodeJS.ErrnoException | null, target: Buffer) => void): void;
export function readlink(path: string | Uint8Array | URL, options: BufferEncoding | { encoding?: BufferEncoding | null } | null, callback: (err: NodeJS.ErrnoException | null, target: string) => void): void;
export function readlink(path: string | Uint8Array | URL, options: string | { encoding?: string | null } | null, callback: (err: NodeJS.ErrnoException | null, target: string | Buffer) => void): void;
export function readlinkSync(path: string | Uint8Array | URL, options: "buffer" | { encoding: "buffer" }): Buffer;
export function readlinkSync(path: string | Uint8Array | URL, options?: BufferEncoding | { encoding?: BufferEncoding | null } | null): string;
export function readlinkSync(path: string | Uint8Array | URL, options: string | { encoding?: string | null } | null): string | Buffer;
export function chownSync(path: string, uid: number, gid: number): void;
/* The 2-argument form only (Node's mode flags have no lowering). The
* destination is created or truncated carrying the SOURCE's mode. */
@@ -1324,6 +1335,11 @@ declare module "fs/promises" {
[Symbol.asyncDispose](): Promise<void>;
}
export function open(path: string, flags?: string, mode?: number): Promise<FileHandle>;
export function link(existingPath: string | Uint8Array | URL, newPath: string | Uint8Array | URL): Promise<void>;
export function symlink(target: string | Uint8Array | URL, path: string | Uint8Array | URL, type?: string | null): Promise<void>;
export function readlink(path: string | Uint8Array | URL, options: "buffer" | { encoding: "buffer" }): Promise<Buffer>;
export function readlink(path: string | Uint8Array | URL, options?: BufferEncoding | { encoding?: BufferEncoding | null } | null): Promise<string>;
export function readlink(path: string | Uint8Array | URL, options: string | { encoding?: string | null } | null): Promise<string | Buffer>;
export function utimes(path: string | Uint8Array | URL, atime: number | string | Date, mtime: number | string | Date): Promise<void>;
export function lutimes(path: string | Uint8Array | URL, atime: number | string | Date, mtime: number | string | Date): Promise<void>;
export function readFile(path: string, encoding: "utf8" | "utf-8"): Promise<string>;
@@ -355,7 +355,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fs.ftruncateSync": "scr_fs_ftruncate",
"fs.readvSync": "scr_fs_readv_sync",
"fs.writevSync": "scr_fs_writev_sync",
"fs.linkSync": "scr_fs_link",
"fs.linkSync": "scr_fs_link_checked",
"fs.symlinkSync": "scr_fs_symlink_checked",
"fs.readlinkSync": "scr_fs_readlink_dyn",
"fs.readlinkSyncStr": "scr_fs_readlink_str",
"fs.readlinkSyncBuffer": "scr_fs_readlink_buffer",
"fs.openSync": "scr_fs_open",
"fs.openNumericSync": "scr_fs_open_numeric",
"fs.readSync": "scr_fs_read_sync",
@@ -545,6 +549,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fsp.mkdirRecursiveMode": "scr_fsp_mkdir_recursive_mode",
"fsp.unlink": "scr_fsp_unlink",
"fsp.chmod": "scr_fsp_chmod",
"fsp.link": "scr_fsp_link",
"fsp.symlink": "scr_fsp_symlink",
"fsp.readlink": "scr_fsp_readlink_dyn",
"fsp.readlinkStr": "scr_fsp_readlink_str",
"fsp.readlinkBuffer": "scr_fsp_readlink_buffer",
"fsp.utimes": "scr_fsp_utimes",
"fsp.lutimes": "scr_fsp_lutimes",
"fsp.rename": "scr_fsp_rename",
@@ -1672,6 +1672,27 @@ function lowerFsSyncBufferWindow(
? lowerFsTimestampValue(lowerer, arg, loc) : lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
return { kind: "libCall", fn: "fs.callbackCall", args: [{ kind: "strLit", value: bi.member, type: STRING, loc }, args], type: DYN, loc };
}
if ((bi.module === "fs" && ["linkSync", "symlinkSync", "readlinkSync"].includes(bi.member)) ||
(bi.module === "fs/promises" && ["link", "symlink", "readlink"].includes(bi.member))) {
const symbolic = bi.member === "symlinkSync" || bi.member === "symlink";
const reading = bi.member === "readlinkSync" || bi.member === "readlink";
const arity = symbolic ? 3 : 2;
if (expr.arguments.length > arity || expr.arguments.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr);
const args = Array.from({ length: arity }, (_, index) => expr.arguments[index]
? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) : dynUndefinedExpr(loc));
if (reading) {
const promise = bi.module === "fs/promises";
const result = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? fn.result;
const inner = result.kind === "promise" ? result.inner : result;
const bytes = inner.kind === "bytes";
const text = inner.kind === "string";
const selected = promise ? bytes ? "fsp.readlinkBuffer" : text ? "fsp.readlinkStr" : "fsp.readlink"
: bytes ? "fs.readlinkSyncBuffer" : text ? "fs.readlinkSyncStr" : "fs.readlinkSync";
const valueType = bytes ? BYTES_U8 : text ? STRING : DYN;
return { kind: "libCall", fn: selected, args, type: promise ? { kind: "promise", inner: valueType } : valueType, loc };
}
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
}
if ((bi.module === "fs" && ["utimesSync", "futimesSync", "lutimesSync"].includes(bi.member)) ||
(bi.module === "fs/promises" && ["utimes", "lutimes"].includes(bi.member))) {
if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr);
@@ -728,7 +728,13 @@ export const BUILTIN_MODULE_FNS: Record<string, Record<string, BuiltinModuleFn |
lutimesSync: { fn: "fs.lutimesSync", params: [DYN, DYN, DYN], result: VOID },
readvSync: { fn: "fs.readvSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 },
writevSync: { fn: "fs.writevSync", params: [F64, arrayOf(BYTES_U8), F64], result: F64 },
linkSync: { fn: "fs.linkSync", params: [STRING, STRING], result: VOID, valueParams: exactValueParams(STRING, STRING) },
linkSync: { fn: "fs.linkSync", params: [DYN, DYN], result: VOID, valueParams: exactValueParams(DYN, DYN) },
symlinkSync: { fn: "fs.symlinkSync", params: [DYN, DYN, DYN], result: VOID, valueParams: [
...exactValueParams(DYN, DYN), { mode: "optional", type: DYN, defaultValue: undefined },
] },
readlinkSync: { fn: "fs.readlinkSync", params: [DYN, DYN], result: DYN, valueParams: [
...exactValueParams(DYN), { mode: "optional", type: DYN, defaultValue: undefined },
] },
// realpath(3) — Node's realpathSync (failures spell syscall "lstat",
// Node's own message shape).
realpathSync: { fn: "fs.realpathSync", params: [STRING], result: STRING },
@@ -767,6 +773,13 @@ export const BUILTIN_MODULE_FNS: Record<string, Record<string, BuiltinModuleFn |
lstat: { fn: "fsp.lstat", params: [STRING], result: { kind: "promise", inner: STATS_T } },
unlink: { fn: "fsp.unlink", params: [STRING], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(STRING) },
chmod: { fn: "fsp.chmod", params: [STRING, F64], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(STRING, F64) },
link: { fn: "fsp.link", params: [DYN, DYN], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(DYN, DYN) },
symlink: { fn: "fsp.symlink", params: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID }, valueParams: [
...exactValueParams(DYN, DYN), { mode: "optional", type: DYN, defaultValue: undefined },
] },
readlink: { fn: "fsp.readlink", params: [DYN, DYN], result: { kind: "promise", inner: DYN }, valueParams: [
...exactValueParams(DYN), { mode: "optional", type: DYN, defaultValue: undefined },
] },
utimes: { fn: "fsp.utimes", params: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } },
lutimes: { fn: "fsp.lutimes", params: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } },
rename: { fn: "fsp.rename", params: [STRING, STRING], result: { kind: "promise", inner: VOID }, valueParams: exactValueParams(STRING, STRING) },
@@ -1072,6 +1085,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<string, Record<string, readonly I
// The Buffer form (no encoding), the fd forms (readFileSync(fd[,
// "utf8"])), and the checked-dynamic encoding form.
readFileSync: ["fs.readFileSyncBuf", "fs.readFileSyncBytes", "fs.readFileSyncDyn", "fs.readFdSync", "fs.readFdSyncBytes"],
readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"],
// The bytes-data form and the { mode } options form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
appendFileSync: ["fs.appendFileSyncBytes"],
@@ -1097,6 +1111,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<string, Record<string, readonly I
"fs/promises": {
// The Buffer form (no encoding).
readFile: ["fsp.readFileBytes"],
readlink: ["fsp.readlinkStr", "fsp.readlinkBuffer"],
// The string-data { mode } options form.
writeFile: ["fsp.writeFileMode"],
// The { withFileTypes: true } Dirent form.
+13
View File
@@ -3667,6 +3667,11 @@ export type IrLibFn =
| "fsp.mkdirRecursiveMode"
| "fsp.unlink"
| "fsp.chmod"
| "fsp.link"
| "fsp.symlink"
| "fsp.readlink"
| "fsp.readlinkStr"
| "fsp.readlinkBuffer"
| "fsp.utimes"
| "fsp.lutimes"
| "fsp.rename"
@@ -4850,6 +4855,10 @@ export type IrLibFn =
| "fs.readvSync"
| "fs.writevSync"
| "fs.linkSync"
| "fs.symlinkSync"
| "fs.readlinkSync"
| "fs.readlinkSyncStr"
| "fs.readlinkSyncBuffer"
/** fs.openSync(path, flags) → the raw fd as f64; fs.readSync/fs.writeSync
* over Buffer windows perform sequential I/O when position is -1 and
* offset-preserving positioned I/O otherwise; fs.writeStrSync is the
@@ -8065,6 +8074,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.readvSync",
"fs.writevSync",
"fs.linkSync",
"fs.symlinkSync",
"fs.readlinkSync",
"fs.readlinkSyncStr",
"fs.readlinkSyncBuffer",
"fs.writeFileModeSync",
"fs.mkdirModeSync",
"fs.mkdirRecursiveModeSync",
+10 -1
View File
@@ -455,7 +455,11 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"fs.lutimesSync": { argTypes: [DYN, DYN, DYN], result: VOID },
"fs.readvSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 },
"fs.writevSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 },
"fs.linkSync": { argTypes: [STRING, STRING], result: VOID },
"fs.linkSync": { argTypes: [DYN, DYN], result: VOID },
"fs.symlinkSync": { argTypes: [DYN, DYN, DYN], result: VOID },
"fs.readlinkSync": { argTypes: [DYN, DYN], result: DYN },
"fs.readlinkSyncStr": { argTypes: [DYN, DYN], result: STRING },
"fs.readlinkSyncBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 },
"fs.openSync": { argTypes: [STRING, STRING], result: F64 },
"fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 },
"fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 },
@@ -1097,6 +1101,11 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"fsp.mkdirRecursiveMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } },
"fsp.unlink": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
"fsp.chmod": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } },
"fsp.link": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } },
"fsp.symlink": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } },
"fsp.readlink": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"fsp.readlinkStr": { argTypes: [DYN, DYN], result: { kind: "promise", inner: STRING } },
"fsp.readlinkBuffer": { argTypes: [DYN, DYN], result: { kind: "promise", inner: BYTES_U8 } },
"fsp.utimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } },
"fsp.lutimes": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } },
"fsp.rename": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } },
+30
View File
@@ -1004,6 +1004,12 @@
"name": "fs/promises.chmod",
"status": "static"
},
{
"id": "node-builtin.fs.promises.link",
"kind": "node-builtin",
"name": "fs/promises.link",
"status": "static"
},
{
"id": "node-builtin.fs.promises.lstat",
"kind": "node-builtin",
@@ -1040,6 +1046,12 @@
"name": "fs/promises.readdir",
"status": "static"
},
{
"id": "node-builtin.fs.promises.readlink",
"kind": "node-builtin",
"name": "fs/promises.readlink",
"status": "static"
},
{
"id": "node-builtin.fs.promises.realpath",
"kind": "node-builtin",
@@ -1064,6 +1076,12 @@
"name": "fs/promises.stat",
"status": "static"
},
{
"id": "node-builtin.fs.promises.symlink",
"kind": "node-builtin",
"name": "fs/promises.symlink",
"status": "static"
},
{
"id": "node-builtin.fs.promises.unlink",
"kind": "node-builtin",
@@ -1100,6 +1118,12 @@
"name": "fs.readdirSync",
"status": "static"
},
{
"id": "node-builtin.fs.readlinkSync",
"kind": "node-builtin",
"name": "fs.readlinkSync",
"status": "static"
},
{
"id": "node-builtin.fs.readvSync",
"kind": "node-builtin",
@@ -1148,6 +1172,12 @@
"name": "fs.statSync",
"status": "static"
},
{
"id": "node-builtin.fs.symlinkSync",
"kind": "node-builtin",
"name": "fs.symlinkSync",
"status": "static"
},
{
"id": "node-builtin.fs.unlinkSync",
"kind": "node-builtin",
@@ -10662,6 +10662,42 @@
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-links-callbacks.cjs": {
"order": [
"<repo>/tests/corpus/node24-fs-links-callbacks.cjs"
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-links-errors.cjs": {
"order": [
"<repo>/tests/corpus/node24-fs-links-errors.cjs"
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-links-options.cjs": {
"order": [
"<repo>/tests/corpus/node24-fs-links-options.cjs"
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-links-promise-errors.cjs": {
"order": [
"<repo>/tests/corpus/node24-fs-links-promise-errors.cjs"
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-links-promises.ts": {
"order": [
"<repo>/tests/corpus/node24-fs-links-promises.ts"
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-links.ts": {
"order": [
"<repo>/tests/corpus/node24-fs-links.ts"
],
"diags": []
},
"<repo>/tests/corpus/node24-fs-timestamps-callbacks.cjs": {
"order": [
"<repo>/tests/corpus/node24-fs-timestamps-callbacks.cjs"
@@ -13319,6 +13355,12 @@
],
"diags": []
},
"<repo>/tests/fixtures/node-types/fs-links.ts": {
"order": [
"<repo>/tests/fixtures/node-types/fs-links.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/fs-timestamps.ts": {
"order": [
"<repo>/tests/fixtures/node-types/fs-timestamps.ts"
+29
View File
@@ -1763,6 +1763,35 @@ ScrPromise *scr_fsp_utimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_link(const ScrDyn *existing, const ScrDyn *destination) {
scr_fs_link_checked(existing, destination);
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_symlink(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type) {
scr_fs_symlink_promise_checked(target, destination, type);
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_readlink_dyn(const ScrDyn *path, const ScrDyn *options) {
ScrDyn *result = scr_fs_readlink_checked(path, options, true);
return scr_promise_settled_ref(result, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
}
ScrPromise *scr_fsp_readlink_str(const ScrDyn *path, const ScrDyn *options) {
ScrDyn *result = scr_fs_readlink_checked(path, options, true);
ScrStr *text = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL;
scr_dyn_release(result);
return scr_promise_settled_str(text);
}
ScrPromise *scr_fsp_readlink_buffer(const ScrDyn *path, const ScrDyn *options) {
ScrDyn *result = scr_fs_readlink_checked(path, options, true);
ScrBytes *bytes = result ? scr_dyn_bytes_unbox(result) : NULL;
scr_dyn_release(result);
return scr_promise_settled_ref(bytes, scr_bytes_retain_v, scr_bytes_release_v, NULL);
}
ScrPromise *scr_fsp_lutimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime) {
scr_fs_lutimes_checked(path, atime, mtime);
return scr_promise_settled_void();
+202 -31
View File
@@ -279,24 +279,180 @@ static ScrStr *scr_fs_cb_path(const ScrDyn *value, const char *name) {
ScrStr *path = value->kind == SCR_DYN_STR ? scr_str_retain(value->v.str)
: scr_str_new((const char *)value->v.bytes->data, value->v.bytes->len);
if (memchr(path->data, 0, path->len)) {
if (!strcmp(name, "path")) scr_fs_invalid_path(value, path);
else scr_dyn_arg_value_fail(name, "must be a string, Uint8Array, or URL without null bytes", value);
scr_fs_invalid_named_path(value, path, name);
scr_str_release(path);
return NULL;
}
return path;
}
static ScrStr *scr_fs_timestamp_path(const ScrDyn *value) {
if (!scr_dyn_native_url_is(value)) return scr_fs_cb_path(value, "path");
static ScrStr *scr_fs_checked_path(const ScrDyn *value, const char *name) {
if (!scr_dyn_native_url_is(value)) return scr_fs_cb_path(value, name);
ScrStr *path = scr_url_checked_to_path(value);
if (!path) return NULL;
ScrDyn input = { .kind = SCR_DYN_STR, .v.str = path };
ScrStr *checked = scr_fs_cb_path(&input, "path");
ScrStr *checked = scr_fs_cb_path(&input, name);
scr_str_release(path);
return checked;
}
static ScrStr *scr_fs_timestamp_path(const ScrDyn *value) {
return scr_fs_checked_path(value, "path");
}
/* validateOneOf runs before either path, even on POSIX where the type has
* no syscall effect. -1 means infer the target's kind on Windows. */
static int scr_fs_symlink_type(const ScrDyn *type) {
if (scr_fs_dyn_absent(type)) return -1;
if (type->kind == SCR_DYN_STR) {
const ScrStr *s = type->v.str;
if (s->len == 4 && !memcmp(s->data, "file", 4)) return 0;
if (s->len == 3 && !memcmp(s->data, "dir", 3)) return 1;
if (s->len == 8 && !memcmp(s->data, "junction", 8)) return 2;
}
scr_dyn_arg_value_fail("type", "must be one of: 'dir', 'file', 'junction', null, undefined", type);
return -1;
}
void scr_fs_link_checked(const ScrDyn *existing, const ScrDyn *destination) {
ScrStr *from = scr_fs_checked_path(existing, "existingPath");
if (!from) return;
ScrStr *to = scr_fs_checked_path(destination, "newPath");
if (to) scr_fs_link(from, to);
scr_str_release(from); scr_str_release(to);
}
static void scr_fs_symlink_checked_core(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type, bool promise) {
int kind = scr_fs_symlink_type(type);
if (scr_exc_pending()) return;
#ifdef _WIN32
if (kind == -1) {
// Sync/promise inference stringifies the raw destination and target before
// getValidatedPath. Sync stat exposes null-byte errors in the resolved path;
// the promise form catches inference failures and continues as a file link.
ScrStr *link = scr_dyn_string_coerce_js(destination);
ScrStr *source = !scr_exc_pending() ? scr_dyn_string_coerce_js(target) : NULL;
ScrStr *absolute = NULL;
if (link && source && !scr_exc_pending()) {
ScrArr *parts = scr_arr_new_ref(scr_str_retain_v, scr_str_release_v, NULL, 3);
scr_arr_push_ref(parts, scr_str_retain(link));
scr_arr_push_ref(parts, scr_str_new("..", 2));
scr_arr_push_ref(parts, scr_str_retain(source));
absolute = scr_path_win32_resolve(parts);
scr_arr_release(parts);
}
kind = 0;
if (absolute && !scr_exc_pending()) {
ScrDyn input = { .kind = SCR_DYN_STR, .v.str = absolute };
ScrStr *checked = scr_fs_cb_path(&input, "path");
if (checked) kind = scr_fs_symlink_infer(checked);
scr_str_release(checked);
}
scr_str_release(link); scr_str_release(source); scr_str_release(absolute);
if (scr_exc_pending()) {
if (!promise) return;
scr_caught_release(scr_exc_take());
}
}
#else
(void)promise;
#endif
ScrStr *from = scr_fs_checked_path(target, "target");
if (!from) return;
ScrStr *to = scr_fs_checked_path(destination, "path");
#ifdef _WIN32
if (to && kind == 2 && scr_dyn_bytes_is(destination, SCR_BYTES_U8)) {
scr_dyn_arg_type_fail("paths[0]", "of type string", destination);
scr_str_release(to); to = NULL;
}
#endif
if (to) scr_fs_symlink(from, to, kind);
scr_str_release(from); scr_str_release(to);
}
void scr_fs_symlink_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type) {
scr_fs_symlink_checked_core(target, destination, type, false);
}
void scr_fs_symlink_promise_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type) {
scr_fs_symlink_checked_core(target, destination, type, true);
}
/* getOptions accepts functions as defaults and validates signals without
* cancelling readlink. Unlike readFile, an aborted signal is ignored. */
static ScrDyn *scr_fs_readlink_encoding(const ScrDyn *options) {
if (scr_fs_dyn_absent(options) || options->kind == SCR_DYN_FUNC) return scr_dyn_retain(scr_dyn_undefined());
if (options->kind != SCR_DYN_STR && options->kind != SCR_DYN_OBJ &&
options->kind != SCR_DYN_ARR && options->kind != SCR_DYN_TYPED_REF &&
options->kind != SCR_DYN_HANDLE && options->kind != SCR_DYN_BYTES) {
scr_dyn_arg_type_fail("options", "one of type string or object", options);
return NULL;
}
ScrDyn *encoding = options->kind == SCR_DYN_STR ? scr_dyn_retain((ScrDyn *)options) : scr_fs_cb_option(options, "encoding");
if (!encoding) return NULL;
bool buffer = encoding->kind == SCR_DYN_STR && encoding->v.str->len == 6 && !memcmp(encoding->v.str->data, "buffer", 6);
if (!buffer && scr_dyn_truthy(encoding) &&
(encoding->kind != SCR_DYN_STR || !scr_bytes_is_encoding(encoding->v.str))) {
scr_dyn_arg_value_fail("encoding", "is invalid encoding", encoding);
scr_dyn_release(encoding);
return NULL;
}
if (options->kind != SCR_DYN_STR) {
ScrDyn *signal = scr_fs_cb_option(options, "signal");
if (!signal) { scr_dyn_release(encoding); return NULL; }
bool valid = signal->kind == SCR_DYN_UNDEF || (signal->kind == SCR_DYN_HANDLE && signal->v.handle.tag == SCR_DYNH_ABORT_SIGNAL);
if (!valid) scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal);
scr_dyn_release(signal);
if (!valid) { scr_dyn_release(encoding); return NULL; }
}
return encoding;
}
static ScrDyn *scr_fs_readlink_result(ScrStr *path, const ScrDyn *encoding) {
ScrBytes *bytes = scr_fs_readlink_bytes(path);
if (!bytes) return NULL;
ScrDyn *result;
if (encoding->kind == SCR_DYN_STR && encoding->v.str->len == 6 && !memcmp(encoding->v.str->data, "buffer", 6)) {
result = scr_dyn_new_buffer(bytes);
}
else {
ScrStr *codec = encoding->kind == SCR_DYN_STR && encoding->v.str->len ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4);
ScrStr *text = scr_bytes_to_str(bytes, codec);
scr_str_release(codec);
result = text ? scr_dyn_new_str(text) : NULL;
scr_str_release(text);
}
scr_bytes_release(bytes);
return result;
}
ScrDyn *scr_fs_readlink_checked(const ScrDyn *input, const ScrDyn *options, bool promise) {
ScrDyn *encoding = scr_fs_readlink_encoding(options);
if (!encoding) return NULL;
ScrStr *path = scr_fs_checked_path(input, promise ? "oldPath" : "path");
ScrDyn *result = path ? scr_fs_readlink_result(path, encoding) : NULL;
scr_str_release(path); scr_dyn_release(encoding);
return result;
}
ScrDyn *scr_fs_readlink_dyn(const ScrDyn *path, const ScrDyn *options) {
return scr_fs_readlink_checked(path, options, false);
}
ScrStr *scr_fs_readlink_str(const ScrDyn *path, const ScrDyn *options) {
ScrDyn *result = scr_fs_readlink_dyn(path, options);
ScrStr *text = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL;
scr_dyn_release(result);
return text;
}
ScrBytes *scr_fs_readlink_buffer(const ScrDyn *path, const ScrDyn *options) {
ScrDyn *result = scr_fs_readlink_dyn(path, options);
ScrBytes *bytes = result ? scr_dyn_bytes_unbox(result) : NULL;
scr_dyn_release(result);
return bytes;
}
static ScrDyn *scr_fs_cb_string(ScrStr *text) {
if (!text) return NULL;
ScrDyn *value = scr_dyn_new_str(text);
@@ -517,6 +673,46 @@ static ScrArr *scr_fs_cb_vectors(const ScrDyn *buffers) {
static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc) {
const char *op = member->data;
if (!strcmp(op, "link") || !strcmp(op, "symlink") || !strcmp(op, "readlink")) {
const ScrDyn *first = scr_fs_cb_arg(args, argc, 0);
const ScrDyn *second = scr_fs_cb_arg(args, argc, 1);
bool symbolic = !strcmp(op, "symlink"), reading = !strcmp(op, "readlink");
const ScrDyn *third = scr_fs_cb_arg(args, argc, 2);
const ScrDyn *fourth = scr_fs_cb_arg(args, argc, 3);
const ScrDyn *callback = symbolic && fourth->kind != SCR_DYN_UNDEF ? fourth
: reading && second->kind == SCR_DYN_FUNC ? second : third;
int kind = -1;
if (symbolic && fourth->kind != SCR_DYN_UNDEF) {
kind = scr_fs_symlink_type(third);
if (scr_exc_pending()) return NULL;
} else if (!scr_fs_cb_chk(callback, "cb")) return NULL;
ScrDyn *encoding = reading ? scr_fs_readlink_encoding(second) : NULL;
if (reading && !encoding) return NULL;
ScrStr *from = scr_fs_checked_path(first, symbolic ? "target" : reading ? "path" : "existingPath");
if (!from) { scr_dyn_release(encoding); return NULL; }
ScrStr *to = reading ? NULL : scr_fs_checked_path(second, symbolic ? "path" : "newPath");
if ((!reading && !to) || (symbolic && fourth->kind != SCR_DYN_UNDEF && !scr_fs_cb_chk(callback, "cb"))) {
scr_str_release(from); scr_str_release(to); scr_dyn_release(encoding); return NULL;
}
#ifdef _WIN32
if (symbolic) {
if (kind == 2 && scr_dyn_bytes_is(second, SCR_BYTES_U8)) {
scr_dyn_arg_type_fail("paths[0]", "of type string", second);
scr_str_release(from); scr_str_release(to); return NULL;
}
// Callback inference passes validated paths directly to path.resolve;
// Buffer paths fail that probe and fall back to a file link.
if (kind == -1 && (scr_dyn_bytes_is(first, SCR_BYTES_U8) || scr_dyn_bytes_is(second, SCR_BYTES_U8))) kind = 0;
}
#endif
ScrDyn *result = NULL;
if (reading) result = scr_fs_readlink_result(from, encoding);
else if (symbolic) scr_fs_symlink(from, to, kind);
else scr_fs_link(from, to);
scr_str_release(from); scr_str_release(to); scr_dyn_release(encoding);
scr_fs_cb_schedule(callback, result, NULL);
return scr_dyn_retain(scr_dyn_undefined());
}
bool vector = !strcmp(op, "readv") || !strcmp(op, "writev");
bool timestamp = !strcmp(op, "utimes") || !strcmp(op, "lutimes") || !strcmp(op, "futimes");
bool timestamp_fd = !strcmp(op, "futimes");
@@ -673,11 +869,10 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
scr_dyn_release(ownedBuffer); scr_str_release(path);
return scr_dyn_retain(scr_dyn_undefined());
}
} else if (!strcmp(op, "copyFile") || !strcmp(op, "rename") || !strcmp(op, "link") || !strcmp(op, "symlink")) {
} else if (!strcmp(op, "copyFile") || !strcmp(op, "rename")) {
ScrStr *destination = scr_fs_cb_path(ARG(1), "dest");
if (destination) {
if (!strcmp(op, "rename")) scr_fs_rename(path, destination);
else if (!strcmp(op, "link")) scr_fs_link(path, destination);
else if (!strcmp(op, "copyFile")) {
double flags = scr_fs_cb_number(ARG(2), "mode", 0);
if (flags == 1 && scr_fs_exists(destination)) scr_fs_throw(EEXIST, "copyfile", destination);
@@ -686,13 +881,6 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
} else if (!scr_exc_pending()) scr_fs_copyfile(path, destination);
}
else {
#if !defined(_WIN32)
if (symlink(path->data, destination->data) < 0) scr_fs_throw(errno, "symlink", path);
#else
scr_fs_throw(ENOSYS, "symlink", path);
#endif
}
scr_str_release(destination);
}
} else if (!strcmp(op, "truncate") || !strcmp(op, "ftruncate")) {
@@ -701,23 +889,6 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
if (descriptor) scr_fs_ftruncate(fd, length);
else if (truncate(path->data, (off_t)length) < 0) scr_fs_throw(errno, "truncate", path);
}
} else if (!strcmp(op, "readlink")) {
#ifndef _WIN32
size_t capacity = 256;
char *text = NULL;
for (;;) {
char *grown = realloc(text, capacity);
if (!grown) scr_bytes_io_oom();
text = grown;
ssize_t size = readlink(path->data, text, capacity);
if (size < 0) { scr_fs_throw(errno, "readlink", path); break; }
if ((size_t)size < capacity) { result = scr_fs_cb_string(scr_str_new(text, (size_t)size)); break; }
capacity *= 2;
}
free(text);
#else
scr_fs_throw(ENOSYS, "readlink", path);
#endif
} else if (timestamp) {
if (timestamp_fd) scr_fs_futimes(fd, access, modified);
else scr_fs_utimes(path, access, modified, !strcmp(op, "lutimes"));
+227 -11
View File
@@ -55,6 +55,8 @@
static WCHAR *scr_fs_win_wide(const ScrStr *path);
static int scr_fs_win_errno(DWORD error);
static bool scr_stats_is_link_tag(DWORD tag);
static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr **text_out);
static ScrStr *scr_fs_win_utf8(const WCHAR *text, size_t length) {
if (length > INT_MAX) { SetLastError(ERROR_FILENAME_EXCED_RANGE); return NULL; }
@@ -869,10 +871,12 @@ ScrDyn *scr_util_parse_env(const ScrDyn *content) {
return scr_env_parse_content(content->v.str, true);
}
void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path) {
void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const char *name) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "The argument 'path' must be a string, Uint8Array, or URL without null bytes. Received ");
scr_jb_puts(&b, "The argument '");
scr_jb_puts(&b, name);
scr_jb_puts(&b, "' must be a string, Uint8Array, or URL without null bytes. Received ");
if (value->kind == SCR_DYN_BYTES) {
const ScrBytes *bytes = value->v.bytes;
if (value->buffer) {
@@ -916,6 +920,10 @@ void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path) {
scr_str_release(message);
}
void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path) {
scr_fs_invalid_named_path(value, path, "path");
}
void scr_process_load_env_file(const ScrDyn *value) {
ScrStr *path;
if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) path = scr_str_new("./.env", 6);
@@ -2501,7 +2509,45 @@ void scr_fs_mkdir_mode(ScrStr *path, double mode) {
if (scr_sys_mkdir(path->data, (mode_t)mode) != 0) scr_fs_throw(errno, "mkdir", path);
}
#ifdef _WIN32
/* The CRT's stat follows junctions. Recognize real link reparse records
* before unlink/rm dispatch so a tree walk never descends into their targets.
* Return 1 for a removed link, 0 for an ordinary entry, -1 on failure. */
static int scr_fs_win_remove_link(const char *path, size_t len) {
ScrStr *text = scr_str_new(path, len);
WCHAR *wide = scr_fs_win_wide(text);
scr_str_release(text);
if (!wide) { errno = scr_fs_win_errno(GetLastError()); return -1; }
DWORD attributes = GetFileAttributesW(wide);
if (attributes == INVALID_FILE_ATTRIBUTES || !(attributes & FILE_ATTRIBUTE_REPARSE_POINT)) { free(wide); return 0; }
HANDLE h = CreateFileW(wide, FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (h == INVALID_HANDLE_VALUE) { DWORD error = GetLastError(); free(wide); errno = scr_fs_win_errno(error); return -1; }
FILE_ATTRIBUTE_TAG_INFO tagged;
if (!GetFileInformationByHandleEx(h, FileAttributeTagInfo, &tagged, sizeof tagged)) {
DWORD error = GetLastError(); CloseHandle(h); free(wide); errno = scr_fs_win_errno(error); return -1;
}
double size;
bool link = scr_stats_is_link_tag(tagged.ReparseTag);
// Mount-point tags also represent mounted volumes. Only junction payloads
// are links; keep the existing ordinary-directory behavior for volumes.
if (link && tagged.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) link = scr_stats_link_target(h, tagged.ReparseTag, &size, NULL);
CloseHandle(h);
if (!link) { free(wide); return 0; }
BOOL ok = attributes & FILE_ATTRIBUTE_DIRECTORY ? RemoveDirectoryW(wide) : DeleteFileW(wide);
DWORD error = ok ? 0 : GetLastError();
free(wide);
if (!ok) { errno = scr_fs_win_errno(error); return -1; }
return 1;
}
#endif
void scr_fs_unlink(ScrStr *path) {
#ifdef _WIN32
int link = scr_fs_win_remove_link(path->data, path->len);
if (link != 0) { if (link < 0) scr_fs_throw(errno, "unlink", path); return; }
#endif
if (unlink(path->data) != 0) scr_fs_throw(errno, "unlink", path);
}
@@ -3239,18 +3285,15 @@ void scr_fs_link(ScrStr *source, ScrStr *dest) {
WCHAR *from = scr_fs_win_wide(source);
WCHAR *to = scr_fs_win_wide(dest);
if (!from || !to) {
DWORD error = GetLastError();
free(from); free(to);
scr_fs_throw2(EINVAL, "link", source, dest);
scr_fs_throw2(scr_fs_win_errno(error), "link", source, dest);
return;
}
BOOL ok = CreateHardLinkW(to, from, NULL);
DWORD error = ok ? 0 : GetLastError();
free(from); free(to);
if (!ok) {
int code = error == ERROR_ALREADY_EXISTS || error == ERROR_FILE_EXISTS ? EEXIST :
error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND ? ENOENT : EACCES;
scr_fs_throw2(code, "link", source, dest);
}
if (!ok) scr_fs_throw2(scr_fs_win_errno(error), "link", source, dest);
#else
if (link(source->data, dest->data) != 0) scr_fs_throw2(errno, "link", source, dest);
#endif
@@ -3421,6 +3464,10 @@ void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath) {
static int scr_rm_unlink(const char *path, size_t len);
void scr_fs_rm(ScrStr *path) {
#ifdef _WIN32
int link = scr_fs_win_remove_link(path->data, path->len);
if (link != 0) { if (link < 0) scr_fs_throw(errno, "unlink", path); return; }
#endif
/* Node's rmSync: lstat first (a missing path reports the lstat syscall),
* refuse directories (Node requires `recursive`, which the scriptc
* surface doesn't declare — the message wording diverges from Node's
@@ -3575,6 +3622,10 @@ static int scr_rm_unlink(const char *path, size_t len) {
/* Post-order tree removal for rmSync's recursive form. Stops at (and
* records) the first failure, with the failing path and syscall name. */
static void scr_rm_tree_e(const char *path, size_t len, ScrRmFail *f) {
#ifdef _WIN32
int link = scr_fs_win_remove_link(path, len);
if (link != 0) { if (link < 0) scr_rm_fail_set(f, errno, "unlink", path, len); return; }
#endif
struct stat st;
if (lstat(path, &st) != 0) {
scr_rm_fail_set(f, errno, "lstat", path, len);
@@ -3615,6 +3666,10 @@ static void scr_rm_tree_e(const char *path, size_t len, ScrRmFail *f) {
* force's ENOENT swallow, the non-recursive directory rejection, and the
* tree walk — failures recorded in `f`, never thrown. */
static void scr_fs_rm_attempt(ScrStr *path, bool recursive, bool force, ScrRmFail *f) {
#ifdef _WIN32
int link = scr_fs_win_remove_link(path->data, path->len);
if (link != 0) { if (link < 0) scr_rm_fail_set(f, errno, "unlink", path->data, path->len); return; }
#endif
struct stat st;
if (lstat(path->data, &st) != 0) {
if (force && errno == ENOENT) return; /* Node: force swallows ENOENT */
@@ -4068,7 +4123,7 @@ typedef struct {
/* Node/libuv reports a Windows link's target-text length as st_size. The
* ordinary handle information does not carry that value, so read the reparse
* payload while the no-follow handle is live. */
static bool scr_stats_link_size(HANDLE h, DWORD tag, double *size_out) {
static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr **text_out) {
union {
ScrStatsReparseData align;
unsigned char bytes[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
@@ -4113,6 +4168,7 @@ static bool scr_stats_link_size(HANDLE h, DWORD tag, double *size_out) {
/* WSL's LX symlink payload is a version word followed by UTF-8 bytes. */
if (data->data_len < sizeof(ULONG)) return false;
*size_out = (double)(data->data_len - sizeof(ULONG));
if (text_out) *text_out = scr_str_new((const char *)data->body.generic.bytes + sizeof(ULONG), data->data_len - sizeof(ULONG));
return true;
} else {
/* App execution links carry a counted UTF-16 string list; the third
@@ -4141,6 +4197,10 @@ static bool scr_stats_link_size(HANDLE h, DWORD tag, double *size_out) {
double size = scr_stats_utf16_len(target, len);
if (size < 0) return false;
*size_out = size;
if (text_out) {
*text_out = scr_fs_win_utf8(target, len);
if (!*text_out) return false;
}
return true;
}
@@ -4166,6 +4226,10 @@ static bool scr_stats_link_size(HANDLE h, DWORD tag, double *size_out) {
double size = scr_stats_utf16_len(target, len);
if (size < 0) return false;
*size_out = size;
if (text_out) {
*text_out = scr_fs_win_utf8(target, len);
if (!*text_out) return false;
}
return true;
}
@@ -4179,8 +4243,21 @@ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op,
HANDLE h = CreateFileW(wide, FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, flags, NULL);
if (h == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
DWORD attributes = GetFileAttributesW(wide);
free(wide);
/* CRT stat can report the link entry when its target cannot be opened,
* hiding dangling links and Windows file/directory type mismatches.
* libuv preserves the failed follow operation for reparse points. */
if (!no_follow && attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
scr_fs_throw(scr_fs_win_errno(error), op, path);
return NULL;
}
return scr_stats_crt_fallback(path, op);
}
free(wide);
if (h == INVALID_HANDLE_VALUE) return scr_stats_crt_fallback(path, op);
DWORD file_type = GetFileType(h);
if (file_type != FILE_TYPE_DISK) {
@@ -4204,7 +4281,7 @@ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op,
CloseHandle(h);
return scr_stats_of_path(path, op, false);
}
if (!scr_stats_link_size(h, tagged.ReparseTag, &link_size)) {
if (!scr_stats_link_target(h, tagged.ReparseTag, &link_size, NULL)) {
/* A mount-point tag may name a mounted volume rather than a junction.
* libuv treats those (and malformed/unsupported link payloads) as
* ordinary reparse points and retries with the final component
@@ -4281,6 +4358,145 @@ static ScrStats *scr_stats_of(const struct stat *st) {
}
#endif
/* Symbolic link targets are stored verbatim on POSIX. Windows junctions
* instead carry an absolute NT path in a bounded mount-point reparse record. */
#ifdef _WIN32
static DWORD scr_fs_win_junction(const WCHAR *target, const WCHAR *destination) {
size_t len = wcslen(target);
/* libuv permits drive-absolute junctions, not UNC mount targets. */
size_t skip = len >= 4 && !wcsncmp(target, L"\\\\?\\", 4) ? 4 : 0;
const WCHAR *plain = target + skip;
size_t chars = len - skip;
if (chars < 3 || !((plain[0] >= L'A' && plain[0] <= L'Z') || (plain[0] >= L'a' && plain[0] <= L'z')) || plain[1] != L':' || plain[2] != L'\\') return ERROR_INVALID_PARAMETER;
size_t fixed = offsetof(ScrStatsReparseData, body.mount.path);
size_t total = fixed + (4 + chars + 1 + chars + 1) * sizeof(WCHAR);
if (total > MAXIMUM_REPARSE_DATA_BUFFER_SIZE) return ERROR_FILENAME_EXCED_RANGE;
ScrStatsReparseData *data = calloc(1, total);
if (!data) return ERROR_NOT_ENOUGH_MEMORY;
data->tag = IO_REPARSE_TAG_MOUNT_POINT;
data->data_len = (USHORT)(total - offsetof(ScrStatsReparseData, body));
data->body.mount.substitute_offset = 0;
data->body.mount.substitute_len = (USHORT)((4 + chars) * sizeof(WCHAR));
data->body.mount.print_offset = (USHORT)((4 + chars + 1) * sizeof(WCHAR));
data->body.mount.print_len = (USHORT)(chars * sizeof(WCHAR));
memcpy(data->body.mount.path, L"\\??\\", 4 * sizeof(WCHAR));
memcpy(data->body.mount.path + 4, plain, chars * sizeof(WCHAR));
memcpy(data->body.mount.path + 4 + chars + 1, plain, chars * sizeof(WCHAR));
DWORD error = 0, used;
bool created = CreateDirectoryW(destination, NULL) != 0;
HANDLE h = INVALID_HANDLE_VALUE;
if (!created) error = GetLastError();
else {
h = CreateFileW(destination, GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (h == INVALID_HANDLE_VALUE) error = GetLastError();
else if (!DeviceIoControl(h, FSCTL_SET_REPARSE_POINT, data, (DWORD)total, NULL, 0, &used, NULL)) error = GetLastError();
}
if (h != INVALID_HANDLE_VALUE) CloseHandle(h);
if (error && created) RemoveDirectoryW(destination);
free(data);
return error;
}
#endif
#ifdef _WIN32
/* libuv keeps this fallback for later calls once Windows rejects the flag. */
static DWORD scr_fs_symlink_usermode_flag = 0x2; /* ALLOW_UNPRIVILEGED_CREATE */
#endif
int scr_fs_symlink_infer(ScrStr *absolute) {
#ifdef _WIN32
WCHAR *probe = scr_fs_win_wide(absolute);
DWORD attributes = probe ? GetFileAttributesW(probe) : INVALID_FILE_ATTRIBUTES;
free(probe);
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0;
#else
(void)absolute;
return 0;
#endif
}
void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind) {
#ifdef _WIN32
ScrArr *parts = scr_arr_new_ref(scr_str_retain_v, scr_str_release_v, NULL, 3);
scr_arr_push_ref(parts, scr_str_retain(destination));
scr_arr_push_ref(parts, scr_str_new("..", 2));
scr_arr_push_ref(parts, scr_str_retain(target));
ScrStr *absolute = scr_path_win32_resolve(parts);
scr_arr_release(parts);
if (kind == -1) kind = scr_fs_symlink_infer(absolute);
ScrStr *stored;
if (kind == 2) stored = scr_path_win32_to_namespaced_path(absolute);
else if (scr_path_win32_is_absolute(target)) stored = scr_path_win32_to_namespaced_path(target);
else {
char *text = malloc(target->len ? target->len : 1);
if (!text) scr_trap("scriptc: out of memory\n");
for (size_t i = 0; i < target->len; i++) text[i] = target->data[i] == '/' ? '\\' : target->data[i];
stored = scr_str_new(text, target->len);
free(text);
}
scr_str_release(absolute);
/* Node's binding namespaces the link path before passing it to libuv. */
ScrStr *link_path = scr_path_win32_to_namespaced_path(destination);
WCHAR *from = scr_fs_win_wide(stored), *to = scr_fs_win_wide(link_path);
DWORD error = !from || !to ? GetLastError() : 0;
if (!error && kind == 2) error = scr_fs_win_junction(from, to);
else if (!error) {
DWORD flags = (kind == 1 ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | scr_fs_symlink_usermode_flag;
if (!CreateSymbolicLinkW(to, from, flags)) {
error = GetLastError();
if (error == ERROR_INVALID_PARAMETER && scr_fs_symlink_usermode_flag) {
scr_fs_symlink_usermode_flag = 0;
error = CreateSymbolicLinkW(to, from, flags & ~0x2u) ? 0 : GetLastError();
}
}
}
free(from); free(to); scr_str_release(stored); scr_str_release(link_path);
if (error) scr_fs_throw2(scr_fs_win_errno(error), "symlink", target, destination);
#else
(void)kind;
if (symlink(target->data, destination->data) != 0) scr_fs_throw2(errno, "symlink", target, destination);
#endif
}
ScrBytes *scr_fs_readlink_bytes(ScrStr *path) {
#ifdef _WIN32
WCHAR *wide = scr_fs_win_wide(path);
if (!wide) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "readlink", path); return NULL; }
HANDLE h = CreateFileW(wide, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
free(wide);
if (h == INVALID_HANDLE_VALUE) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "readlink", path); return NULL; }
FILE_ATTRIBUTE_TAG_INFO tagged;
ScrStr *text = NULL;
double size;
bool ok = GetFileInformationByHandleEx(h, FileAttributeTagInfo, &tagged, sizeof tagged) &&
scr_stats_link_target(h, tagged.ReparseTag, &size, &text);
CloseHandle(h);
if (!ok || !text) { scr_str_release(text); scr_fs_throw(EINVAL, "readlink", path); return NULL; }
ScrBytes *bytes = scr_bytes_from_data((const uint8_t *)text->data, text->len);
scr_str_release(text);
return bytes;
#else
size_t capacity = 256;
uint8_t *text = NULL;
for (;;) {
uint8_t *grown = realloc(text, capacity);
if (!grown) { free(text); scr_trap("scriptc: out of memory\n"); }
text = grown;
ssize_t size = readlink(path->data, (char *)text, capacity);
if (size < 0) { int error = errno; free(text); scr_fs_throw(error, "readlink", path); return NULL; }
if ((size_t)size < capacity) {
ScrBytes *bytes = scr_bytes_from_data(text, (size_t)size);
free(text);
return bytes;
}
if (capacity > SIZE_MAX / 2) { free(text); scr_fs_throw(ENAMETOOLONG, "readlink", path); return NULL; }
capacity *= 2;
}
#endif
}
ScrStats *scr_fs_stat(ScrStr *path) {
#ifdef _WIN32
return scr_stats_of_path(path, "stat", false);
+16
View File
@@ -2638,8 +2638,24 @@ void scr_fs_fdatasync(double fd);
void scr_fs_ftruncate(double fd, double length);
void scr_fs_utimes(ScrStr *path, double atime, double mtime, bool nofollow);
void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path);
void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const char *name);
void scr_fs_futimes(double fd, double atime, double mtime);
void scr_fs_link(ScrStr *source, ScrStr *dest);
void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind);
int scr_fs_symlink_infer(ScrStr *absolute);
ScrBytes *scr_fs_readlink_bytes(ScrStr *path);
void scr_fs_link_checked(const ScrDyn *existing, const ScrDyn *destination);
void scr_fs_symlink_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type);
void scr_fs_symlink_promise_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type);
ScrDyn *scr_fs_readlink_checked(const ScrDyn *path, const ScrDyn *options, bool promise);
ScrDyn *scr_fs_readlink_dyn(const ScrDyn *path, const ScrDyn *options);
ScrStr *scr_fs_readlink_str(const ScrDyn *path, const ScrDyn *options);
ScrBytes *scr_fs_readlink_buffer(const ScrDyn *path, const ScrDyn *options);
ScrPromise *scr_fsp_link(const ScrDyn *existing, const ScrDyn *destination);
ScrPromise *scr_fsp_symlink(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type);
ScrPromise *scr_fsp_readlink_dyn(const ScrDyn *path, const ScrDyn *options);
ScrPromise *scr_fsp_readlink_str(const ScrDyn *path, const ScrDyn *options);
ScrPromise *scr_fsp_readlink_buffer(const ScrDyn *path, const ScrDyn *options);
ScrStats *scr_stats_retain(ScrStats *s);
void scr_stats_release(ScrStats *s);
void *scr_stats_retain_v(void *p);
@@ -0,0 +1,53 @@
// Direct and captured callbacks validate synchronously and complete later.
const fs = require("node:fs");
const { pathToFileURL } = require("node:url");
const { join } = require("node:path");
const root = fs.mkdtempSync(join(require("node:os").tmpdir(), "scriptc-cb-links-"));
const call = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, result) => err ? reject(err) : resolve(result)));
async function main() {
const file = join(root, "file");
fs.writeFileSync(file, "shared");
fs.mkdirSync(join(root, "dir"));
try {
let immediate = true;
await new Promise((resolve, reject) => {
fs.link(Buffer.from(file), pathToFileURL(join(root, "hard")), err => {
console.log("deferred", immediate, err === null);
if (err) reject(err); else resolve(undefined);
}, "ignored");
immediate = false;
});
await call(fs.link, pathToFileURL(file), Buffer.from(join(root, "stored-hard")));
console.log("hard", fs.readFileSync(join(root, "stored-hard"), "utf8"));
await call(fs.symlink, "dir", pathToFileURL(join(root, "junction")), "junction");
const target = await call(fs.readlink, Buffer.from(join(root, "junction")));
const bytes = await call(fs.readlink, pathToFileURL(join(root, "junction")), { encoding: "buffer" });
console.log("stored readlink", typeof target, Buffer.isBuffer(bytes), bytes.toString("utf8") === target);
await new Promise((resolve, reject) => fs.readlink(join(root, "junction"), "hex", (err, value) => {
if (err) reject(err); else { console.log("hex", value === bytes.toString("hex")); resolve(undefined); }
}, "ignored"));
// String and URL paths participate in Windows's directory inference.
try {
await call(fs.symlink, "dir", join(root, "inferred-string"));
console.log("inferred string", fs.statSync(join(root, "inferred-string")).isDirectory());
await call(fs.symlink, "dir", pathToFileURL(join(root, "inferred-url")), null);
console.log("inferred URL", fs.statSync(join(root, "inferred-url")).isDirectory());
await call(fs.symlink, Buffer.from("file"), join(root, "buffer-file"));
console.log("buffer file", fs.statSync(join(root, "buffer-file")).isFile());
} catch (e) { if (process.platform === "win32" && e.code === "EPERM") console.log("inference privilege boundary"); else throw e; }
// Buffer paths bypass that probe and use the file-link fallback.
try {
await call(fs.symlink, "dir", Buffer.from(join(root, "inferred")));
console.log("inferred link", fs.lstatSync(join(root, "inferred")).isSymbolicLink());
try { console.log("inferred stat", fs.statSync(join(root, "inferred")).isDirectory()); }
catch (e) { console.log("inferred stat", e.name, e.code, e.message.includes("stat")); }
} catch (e) { if (process.platform === "win32" && e.code === "EPERM") console.log("symlink privilege boundary"); else throw e; }
let synchronous = true;
await new Promise(resolve => {
fs.readlink(file, (err) => { console.log("error deferred", synchronous, err.name, err.code); resolve(undefined); });
synchronous = false;
});
try { await call(fs.link, file + ".missing", join(root, "missing")); } catch (e) { console.log("missing", e.name, e.code, e.message.includes(" -> ")); }
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}
main();
+43
View File
@@ -0,0 +1,43 @@
// Validation order is observable before any filesystem operation.
const fs = require("node:fs");
const cwd = process.cwd().replaceAll("\\", "\\\\");
const report = (label, action) => {
try { action(); console.log(label, "unexpected success"); }
catch (e) { console.log(label, e.name, e.code, e.message.replaceAll(cwd, "<cwd>")); }
};
// Object inspection in Received tails remains a separate runtime limitation.
const reportObjectEncoding = (label, encoding) => {
try { fs.readlinkSync(null, { encoding }); console.log(label, "unexpected success"); }
catch (e) { console.log(label, e.name, e.code, e.message.startsWith("The argument 'encoding' is invalid encoding. Received ")); }
};
report("symlink type first", () => fs.symlinkSync(null, null, "other"));
if (process.platform === "win32") report("junction buffer destination", () => fs.symlinkSync("target", Buffer.from("link"), "junction"));
report("numeric type", () => fs.symlinkSync(null, null, 1));
report("symlink target", () => fs.symlinkSync(null, null));
report("symlink path", () => fs.symlinkSync("target", null, "file"));
report("nul target", () => fs.symlinkSync("a\0b", "link"));
report("nul buffer target", () => fs.symlinkSync(Buffer.from("a\0b"), "link"));
report("hard source", () => fs.linkSync(null, null));
report("hard destination", () => fs.linkSync("existing", null));
report("nul source", () => fs.linkSync("a\0b", "new"));
report("nul destination", () => fs.linkSync("existing", Buffer.from("a\0b")));
report("scheme", () => fs.symlinkSync(new URL("https://example.com/target"), "link"));
report("encoded slash", () => fs.linkSync(new URL("file:///tmp/a%2Fb"), "new"));
report("readlink options first", () => fs.readlinkSync(null, 1));
report("readlink encoding first", () => fs.readlinkSync(null, "invalid"));
report("readlink numeric encoding", () => fs.readlinkSync(null, { encoding: 1 }));
reportObjectEncoding("readlink object encoding", {});
reportObjectEncoding("readlink nested encoding", { encoding: "hex" });
report("readlink signal", () => fs.readlinkSync(null, { signal: null }));
report("readlink path", () => fs.readlinkSync(null, { encoding: "buffer" }));
report("readlink nul", () => fs.readlinkSync(Buffer.from("a\0b")));
report("link callback first", () => fs.link(null, null, null));
report("link fixed callback", () => fs.link("existing", "new"));
report("symlink omitted callback", () => fs.symlink(null, null));
report("symlink bad callback", () => fs.symlink(null, null, null));
report("symlink callback type first", () => fs.symlink(null, null, "other", () => {}));
report("symlink callback target", () => fs.symlink(null, null, () => {}));
report("readlink callback first", () => fs.readlink(null, 1, null));
report("readlink callback options", () => fs.readlink(null, 1, () => {}));
report("readlink callback encoding", () => fs.readlink(null, "invalid", () => {}));
report("readlink callback path", () => fs.readlink(null, () => {}));
+37
View File
@@ -0,0 +1,37 @@
// Runtime encodings, falsy defaults, and valid aborted signals retain readlink semantics.
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const { join } = require("node:path");
const root = fs.mkdtempSync(join(require("node:os").tmpdir(), "scriptc-link-options-"));
async function main() {
fs.mkdirSync(join(root, "dir"));
const path = join(root, "link");
try {
const create = fs.symlinkSync;
create("dir", path, "junction");
const read = fs.readlinkSync;
const expected = read(path);
for (const options of [null, undefined, "", {}, [], { encoding: null }, { encoding: false }, { encoding: 0 }]) {
console.log("default", fs.readlinkSync(path, options) === expected);
}
for (const encoding of ["utf8", "hex", "base64", "base64url", "latin1", "ascii", "utf16le", "buffer"]) {
const result = fs.readlinkSync(path, { encoding });
const raw = fs.readlinkSync(path, "buffer");
console.log(encoding, encoding === "buffer" ? Buffer.isBuffer(result) && result.toString("hex") === raw.toString("hex") : result === raw.toString(encoding));
}
const controller = new AbortController();
controller.abort();
const options = { signal: controller.signal, encoding: "utf8" };
console.log("signal sync", fs.readlinkSync(path, options) === expected);
console.log("signal promise", await fsp.readlink(path, options) === expected);
await new Promise((resolve, reject) => fs.readlink(path, options, (err, target) => {
if (err) reject(err); else { console.log("signal callback", target === expected); resolve(undefined); }
}));
const storedPromise = fsp.readlink;
console.log("stored promise", await storedPromise(path) === expected);
const storedCreate = fsp.symlink;
await storedCreate("dir", join(root, "stored"), "junction");
console.log("stored create", fs.statSync(join(root, "stored")).isDirectory());
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}
main();
@@ -0,0 +1,24 @@
// Promise validation rejects, including options-first and oldPath naming.
const fsp = require("node:fs/promises");
const report = async (label, action) => {
let promise;
try { promise = action(); console.log(label, "returned promise"); }
catch (e) { console.log(label, "synchronous throw", e.name); return; }
try { await promise; console.log(label, "unexpected success"); }
catch (e) { console.log(label, e.name, e.code, e.message); }
};
async function main() {
await report("type", () => fsp.symlink(null, null, "other"));
await report("target", () => fsp.symlink(null, null));
await report("destination", () => fsp.symlink("target", null));
await report("source", () => fsp.link(null, null));
await report("new path", () => fsp.link("existing", null));
await report("options", () => fsp.readlink(null, 1));
await report("encoding", () => fsp.readlink(null, "invalid"));
await report("old path", () => fsp.readlink(null));
await report("signal", () => fsp.readlink(null, { signal: 1 }));
// Captured functions preserve promise rejection behavior too.
const stored = fsp.link;
await report("stored", () => stored("existing", null));
}
main();
+27
View File
@@ -0,0 +1,27 @@
// Promise adapters fulfill typed string/Buffer results and reject argument errors.
import * as fs from "node:fs";
import { link, symlink, readlink } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
import { tmpdir } from "node:os";
async function main(): Promise<void> {
const root = fs.mkdtempSync(join(tmpdir(), "scriptc-promise-links-"));
const file = join(root, "file");
fs.writeFileSync(file, "contents");
fs.mkdirSync(join(root, "directory"));
try {
await link(Buffer.from(file), pathToFileURL(join(root, "hard")));
console.log("link fulfilled");
console.log("hard", fs.readFileSync(join(root, "hard"), "utf8"));
await symlink(Buffer.from("directory"), pathToFileURL(join(root, "junction")), "junction");
console.log("symlink fulfilled");
const target = await readlink(pathToFileURL(join(root, "junction")));
const bytes = await readlink(Buffer.from(join(root, "junction")), "buffer");
console.log("readlink", target === (process.platform === "win32" ? join(root, "directory") : "directory"), bytes.toString("utf8") === target);
console.log("hex", await readlink(join(root, "junction"), { encoding: "hex" }) === bytes.toString("hex"));
try { await link(file, join(root, "hard")); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("existing", err.name, err.code); }
try { await readlink(file); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("regular", err.name, err.code); }
try { await symlink("directory", join(root, "junction"), "junction"); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("symlink existing", err.name, err.code); }
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}
main();
+69
View File
@@ -0,0 +1,69 @@
// Relative targets stay relative to the link's parent; hard links share contents.
import * as fs from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
const root = fs.mkdtempSync(join(tmpdir(), "scriptc-links-"));
const outside = fs.mkdtempSync(join(tmpdir(), "scriptc-link-target-"));
fs.writeFileSync(join(outside, "keep"), "preserved");
const file = join(root, "target.txt");
const hard = join(root, "hard.txt");
const nested = join(root, "nested");
fs.writeFileSync(file, "first");
fs.mkdirSync(nested);
try {
fs.linkSync(pathToFileURL(file), Buffer.from(hard));
const storedLink = fs.linkSync;
storedLink(Buffer.from(file), join(root, "stored-hard"));
fs.writeFileSync(hard, "shared");
console.log("hard", fs.readFileSync(file, "utf8"), fs.statSync(file).ino === fs.statSync(hard).ino);
try { fs.linkSync(file, hard); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("existing", err.name, err.code, err.message.includes("link")); }
try { fs.linkSync(file + ".missing", hard + ".missing"); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("missing", err.name, err.code, err.message.includes(" -> ")); }
const relative = join(nested, "relative");
let canSymlink = true;
try { fs.symlinkSync("../target.txt", relative); }
catch (e) { const err = e as NodeJS.ErrnoException; if (process.platform === "win32" && err.code === "EPERM") { canSymlink = false; console.log("symlink privilege boundary"); } else throw e; }
if (canSymlink) {
const text = fs.readlinkSync(pathToFileURL(relative));
const bytes = fs.readlinkSync(Buffer.from(relative), { encoding: "buffer" });
console.log("relative", text === (process.platform === "win32" ? "..\\target.txt" : "../target.txt"), fs.readFileSync(relative, "utf8"), bytes.toString("utf8") === text);
console.log("encodings", fs.readlinkSync(relative, "hex") === bytes.toString("hex"), fs.readlinkSync(relative, { encoding: "base64" }) === bytes.toString("base64"), fs.readlinkSync(relative, "latin1") === bytes.toString("latin1"));
fs.symlinkSync(pathToFileURL(file), join(root, "absolute"), "file");
console.log("absolute", fs.readlinkSync(join(root, "absolute")) === file);
fs.symlinkSync("missing-target", join(root, "dangling"), null);
console.log("dangling", fs.readlinkSync(join(root, "dangling")), fs.lstatSync(join(root, "dangling")).isSymbolicLink());
try { fs.statSync(join(root, "dangling")); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("dangling stat", err.name, err.code); }
fs.symlinkSync("nested", join(root, "directory"), "dir");
console.log("directory", fs.statSync(join(root, "directory")).isDirectory());
fs.symlinkSync("nested", join(root, "file-directory"), "file");
console.log("file-directory link", fs.lstatSync(join(root, "file-directory")).isSymbolicLink(), fs.readlinkSync(join(root, "file-directory")) === "nested");
try { console.log("file-directory stat", fs.statSync(join(root, "file-directory")).isDirectory()); }
catch (e) { const err = e as NodeJS.ErrnoException; console.log("file-directory stat", err.name, err.code); }
fs.symlinkSync("target.txt", join(root, "directory-file"), "dir");
console.log("directory-file link", fs.lstatSync(join(root, "directory-file")).isSymbolicLink());
try { console.log("directory-file stat", fs.statSync(join(root, "directory-file")).isFile()); }
catch (e) { const err = e as NodeJS.ErrnoException; console.log("directory-file stat", err.name, err.code); }
if (process.platform !== "win32") {
fs.symlinkSync("x".repeat(600), join(root, "long"), "file");
console.log("long", fs.readlinkSync(join(root, "long")).length);
fs.symlinkSync(Buffer.from([0x61, 0xff, 0x62]), Buffer.from(join(root, "raw")));
console.log("raw", fs.readlinkSync(join(root, "raw"), "buffer").toString("hex"), fs.readlinkSync(join(root, "raw")));
}
}
// Junctions do not require Windows symlink privileges. POSIX ignores type.
fs.symlinkSync("nested", join(root, "junction"), "junction");
console.log("junction", fs.statSync(join(root, "junction")).isDirectory(), fs.readlinkSync(join(root, "junction")) === (process.platform === "win32" ? nested : "nested"));
fs.unlinkSync(join(root, "junction"));
console.log("unlink directory link", fs.statSync(nested).isDirectory());
fs.symlinkSync("nested", join(root, "remove"), "junction");
fs.rmSync(join(root, "remove"));
console.log("remove directory link", fs.statSync(nested).isDirectory());
fs.symlinkSync(outside, join(root, "outside"), "junction");
fs.unlinkSync(file);
console.log("survives", fs.readFileSync(hard, "utf8"));
} finally {
try {
fs.rmSync(root, { recursive: true, force: true });
console.log("no-follow cleanup", fs.readFileSync(join(outside, "keep"), "utf8"));
} finally { fs.rmSync(outside, { recursive: true, force: true }); }
}
+2 -1
View File
@@ -29,7 +29,8 @@ try {
const before = Date.now() - 2000;
utimesSync(file, -1, "Infinity");
const current = statSync(file);
console.log("current", current.atimeMs >= before && current.atimeMs <= Date.now(), current.mtimeMs >= before && current.mtimeMs <= Date.now());
// UTIME_NOW can preserve submillisecond precision; Date.now is integral.
console.log("current", current.atimeMs >= before && current.atimeMs <= Date.now() + 1, current.mtimeMs >= before && current.mtimeMs <= Date.now() + 1);
utimesSync(file, "-Infinity", new Date(NaN));
console.log("negative infinity", statSync(file).atimeMs >= before);
let order = "";
+29
View File
@@ -0,0 +1,29 @@
import { symlinkSync, readlinkSync, linkSync, symlink, readlink, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
import { symlink as symlinkPromise, readlink as readlinkPromise, link as linkPromise } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
import { tmpdir } from "node:os";
async function main(): Promise<void> {
const root = mkdtempSync(join(tmpdir(), "scriptc-typed-links-"));
const file = join(root, "file");
writeFileSync(file, "typed");
mkdirSync(join(root, "dir"));
try {
linkSync(pathToFileURL(file), Buffer.from(join(root, "hard")));
await linkPromise(Buffer.from(file), pathToFileURL(join(root, "promise-hard")));
console.log("hard", readFileSync(join(root, "promise-hard"), "utf8"));
const path = join(root, "junction");
symlinkSync("dir", pathToFileURL(path), "junction");
const text: string = readlinkSync(Buffer.from(path));
const bytes: Buffer = readlinkSync(pathToFileURL(path), { encoding: "buffer" });
console.log("sync", text === bytes.toString());
await symlinkPromise(Buffer.from("dir"), pathToFileURL(join(root, "promise")), "junction");
const promised: Buffer = await readlinkPromise(join(root, "promise"), "buffer");
console.log("promise", await readlinkPromise(join(root, "promise")) === promised.toString());
await new Promise<void>((resolve, reject) => symlink("dir", join(root, "callback"), "junction", err => err ? reject(err) : resolve()));
await new Promise<void>((resolve, reject) => readlink(join(root, "callback"), "buffer", (err, target) => {
if (err) reject(err); else { console.log("callback", target.toString() === text); resolve(); }
}));
} finally { rmSync(root, { recursive: true, force: true }); }
}
main();
+1 -1
View File
@@ -352,7 +352,7 @@ async function compileAndRun(file: string): Promise<RunResult> {
const outDir = join(cacheDir, key);
mkdirSync(outDir, { recursive: true });
const result = await compile(file, {
outPath: join(outDir, "program"),
outPath: join(outDir, process.platform === "win32" ? "program.exe" : "program"),
outDir,
sanitize,
dynamic,
+11
View File
@@ -31,6 +31,17 @@ function comparableStderr(text: string): string {
: text;
}
test("node-types: filesystem links accept URL and Buffer paths and typed readlink results under @types/node", async () => {
const outDir = outDirFor("fs-links");
const entry = join(nodeTypesDir, "fs-links.ts");
const result = await compile(entry, { outPath: join(outDir, process.platform === "win32" ? "fs-links.exe" : "fs-links"), outDir, sanitize });
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const [native, node] = await Promise.all([execFileAsync(result.binaryPath), execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry])]);
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: filesystem timestamps accept Date, Buffer, URL, and FileHandle inputs under @types/node", async () => {
const outDir = outDirFor("fs-timestamps");
const entry = join(nodeTypesDir, "fs-timestamps.ts");