mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Add fs.cpSync, fs.cp and fs/promises cp (#1166)
This commit is contained in:
@@ -76,6 +76,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/copy.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/glob.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/filehandle-and-descriptor-i-o.mdx</include>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **Copy:** `fs.cpSync`, `fs.cp`, and `fs/promises` `cp` support every option and follow Node's two implementations: `cpSync` reports the native binding's errors, and the callback and promise forms report `SystemError`s. Files, directories, and symbolic links are copied in directory order, as in Node. The callback and promise forms complete the copy before the callback runs or the promise settles. An async `filter` must return a promise that is already settled; a pending one throws `SC2020`. On Windows, the three functions throw `SC2020`.
|
||||
@@ -15,4 +15,4 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
|
||||
|
||||
Synchronous and promise filesystem APIs with trailing arguments that affect behavior remain call-only. This includes writers, open, directory creation/removal, metadata readers, and directory readers. Passing them as values could discard modes, flags, encodings, or options during function-signature adaptation.
|
||||
|
||||
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy supports regular files and recursive directories. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Symbolic-link copy, copy filters and clone modes, recursive directory reads, and Dirent results report a native limitation. `fs.glob` compiles only as a direct call; a stored `fs.glob` value throws `SC2020`.
|
||||
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy follows `fs.cp`. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Recursive directory reads and Dirent results report a native limitation. `fs.glob` compiles only as a direct call; a stored `fs.glob` value throws `SC2020`.
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "fba8a4c99de7c5424f50",
|
||||
"artifactVersion": "ae3ec89c0c8935ca6c07",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -314,9 +314,9 @@
|
||||
"apiEntries": 280,
|
||||
"static": {
|
||||
"supported": 7,
|
||||
"partial": 195,
|
||||
"partial": 198,
|
||||
"refused": 0,
|
||||
"not-implemented": 78,
|
||||
"not-implemented": 75,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -29667,9 +29667,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 documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -30449,9 +30449,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 documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -31714,9 +31714,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 documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"tierItemCount": 3472,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1058,
|
||||
"verify-gap": 2287,
|
||||
"audit-partial": 1061,
|
||||
"verify-gap": 2284,
|
||||
"replace-refusal": 40,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
@@ -26214,12 +26214,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.fsPromises.cp",
|
||||
"tests": [
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -27168,12 +27173,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.cp",
|
||||
"tests": [
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -28700,12 +28710,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.cpSync",
|
||||
"tests": [
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -326,9 +326,9 @@
|
||||
"apiEntries": 280,
|
||||
"static": {
|
||||
"supported": 7,
|
||||
"partial": 195,
|
||||
"partial": 198,
|
||||
"refused": 0,
|
||||
"not-implemented": 78,
|
||||
"not-implemented": 75,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -44021,6 +44021,10 @@
|
||||
"tests/corpus/5548-fs-callback-arguments.cjs",
|
||||
"tests/corpus/5549-fs-callback-option-checks.cjs",
|
||||
"tests/corpus/5554-fs-streams-typed.ts",
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts",
|
||||
"tests/corpus/fs-native-realpath.ts",
|
||||
"tests/corpus/node24-fs-links-callbacks.cjs",
|
||||
"tests/corpus/node24-fs-links-errors.cjs",
|
||||
@@ -45067,8 +45071,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:fs"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:fs.fsPromises.cp",
|
||||
"tests": [
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -46027,8 +46037,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:fs"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:fs.fs.cp",
|
||||
"tests": [
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -47655,8 +47671,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:fs"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:fs.fs.cpSync",
|
||||
"tests": [
|
||||
"tests/corpus/5567-fs-cpsync.ts",
|
||||
"tests/corpus/5568-fs-cp-errors.js",
|
||||
"tests/corpus/5569-fs-cp-symlinks.ts",
|
||||
"tests/corpus/5570-fs-cp-async.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
{ "chapter": "child_process", "signatures": ["`stdin` Type: {stream.Writable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2894-child-stdin-backpressure.ts", "tests/corpus/2895-child-stdin-write-after-end.ts", "tests/corpus/2896-child-stdin-write-after-finish.ts", "tests/corpus/2897-child-stdin-destroy.ts", "tests/corpus/2898-child-stdin-spawn-failure.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fsPromises.lstat(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fs.globSync(pattern[, options])`", "`fs.glob(pattern[, options], callback)`", "`fsPromises.glob(pattern[, options])`"], "status": "partial", "evidence": ["tests/corpus/5448-fs-glob-patterns.ts", "tests/corpus/5449-fs-glob-options.ts", "tests/corpus/5450-fs-glob-validation.js", "tests/corpus/5451-fs-promises-glob.ts", "tests/corpus/5452-fs-glob-callback.ts", "tests/fixtures/node-types/fs-glob.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fs.cpSync(src, dest[, options])`", "`fs.cp(src, dest[, options], callback)`", "`fsPromises.cp(src, dest[, options])`"], "status": "partial", "evidence": ["tests/corpus/5567-fs-cpsync.ts", "tests/corpus/5568-fs-cp-errors.js", "tests/corpus/5569-fs-cp-symlinks.ts", "tests/corpus/5570-fs-cp-async.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
|
||||
{ "chapter": "fs", "symbols": ["fs.Stats", "stats.isBlockDevice", "stats.isCharacterDevice", "stats.isDirectory", "stats.isFIFO", "stats.isFile", "stats.isSocket", "stats.isSymbolicLink"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] },
|
||||
{ "chapter": "fs", "parent": "Class: `fs.Stats`", "signatures": ["`mode` Type: {number|bigint}", "`nlink` Type: {number|bigint}", "`uid` Type: {number|bigint}", "`gid` Type: {number|bigint}", "`rdev` Type: {number|bigint}", "`size` Type: {number|bigint}", "`blksize` Type: {number|bigint}", "`blocks` Type: {number|bigint}", "`atimeMs` Type: {number|bigint}", "`mtimeMs` Type: {number|bigint}", "`birthtimeMs` Type: {number|bigint}", "`atimeNs` Type: {bigint}", "`mtimeNs` Type: {bigint}", "`ctimeNs` Type: {bigint}", "`birthtimeNs` Type: {bigint}", "`atime` Type: {Date}", "`mtime` Type: {Date}", "`ctime` Type: {Date}", "`birthtime` Type: {Date}"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] },
|
||||
|
||||
@@ -1251,6 +1251,45 @@ declare module "node:fs" {
|
||||
path: string | Uint8Array | URL,
|
||||
type?: string | null,
|
||||
): void;
|
||||
/* fs.cpSync / fs.cp: copies a file, symlink or (with recursive) a
|
||||
* directory tree. filter decides per path; the callback and promise
|
||||
* forms complete the copy before reporting. */
|
||||
export interface CopySyncOptions {
|
||||
dereference?: boolean;
|
||||
errorOnExist?: boolean;
|
||||
filter?: (source: string, destination: string) => boolean;
|
||||
force?: boolean;
|
||||
mode?: number;
|
||||
preserveTimestamps?: boolean;
|
||||
recursive?: boolean;
|
||||
verbatimSymlinks?: boolean;
|
||||
}
|
||||
export interface CopyOptions {
|
||||
dereference?: boolean;
|
||||
errorOnExist?: boolean;
|
||||
filter?: (source: string, destination: string) => boolean | Promise<boolean>;
|
||||
force?: boolean;
|
||||
mode?: number;
|
||||
preserveTimestamps?: boolean;
|
||||
recursive?: boolean;
|
||||
verbatimSymlinks?: boolean;
|
||||
}
|
||||
export function cpSync(
|
||||
source: string | URL,
|
||||
destination: string | URL,
|
||||
options?: CopySyncOptions,
|
||||
): void;
|
||||
export function cp(
|
||||
source: string | URL,
|
||||
destination: string | URL,
|
||||
callback: (err: NodeJS.ErrnoException | null) => void,
|
||||
): void;
|
||||
export function cp(
|
||||
source: string | URL,
|
||||
destination: string | URL,
|
||||
options: CopyOptions,
|
||||
callback: (err: NodeJS.ErrnoException | null) => void,
|
||||
): void;
|
||||
export function symlink(
|
||||
target: string | Uint8Array | URL,
|
||||
path: string | Uint8Array | URL,
|
||||
@@ -1441,6 +1480,9 @@ declare module "node:fs" {
|
||||
readonly O_NONBLOCK: number;
|
||||
readonly O_TRUNC: number;
|
||||
readonly O_APPEND: number;
|
||||
readonly COPYFILE_EXCL: number;
|
||||
readonly COPYFILE_FICLONE: number;
|
||||
readonly COPYFILE_FICLONE_FORCE: number;
|
||||
};
|
||||
/* A stat(2) snapshot (statSync follows symlinks, lstatSync does not —
|
||||
* Node's split) — immutable; the supported surface is exactly these
|
||||
@@ -1789,6 +1831,11 @@ declare module "fs/promises" {
|
||||
path: string | Uint8Array | URL,
|
||||
type?: string | null,
|
||||
): Promise<void>;
|
||||
export function cp(
|
||||
source: string | URL,
|
||||
destination: string | URL,
|
||||
options?: import("node:fs").CopyOptions,
|
||||
): Promise<void>;
|
||||
export function readlink(
|
||||
path: string | Uint8Array | URL,
|
||||
options: "buffer" | { encoding: "buffer" },
|
||||
|
||||
@@ -461,6 +461,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fs.writevSync": "scr_fs_writev_sync",
|
||||
"fs.linkSync": "scr_fs_link_checked",
|
||||
"fs.symlinkSync": "scr_fs_symlink_checked",
|
||||
"fs.cpSync": "scr_fs_cp_sync",
|
||||
"fs.readlinkSync": "scr_fs_readlink_dyn",
|
||||
"fs.statfsSync": "scr_fs_statfs_checked",
|
||||
"fs.readlinkSyncStr": "scr_fs_readlink_str",
|
||||
@@ -712,6 +713,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.rmdir": "scr_fsp_rmdir",
|
||||
"fsp.link": "scr_fsp_link",
|
||||
"fsp.symlink": "scr_fsp_symlink",
|
||||
"fsp.cp": "scr_fsp_cp",
|
||||
"fsp.readlink": "scr_fsp_readlink_dyn",
|
||||
"fsp.statfs": "scr_fsp_statfs",
|
||||
"fsp.readlinkStr": "scr_fsp_readlink_str",
|
||||
|
||||
@@ -46,6 +46,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_array.c",
|
||||
"scr_bytes.c",
|
||||
"scr_bytes_io.c",
|
||||
"scr_fs_cp.c",
|
||||
"scr_map.c",
|
||||
"scr_closure.c",
|
||||
"scr_ffi.c",
|
||||
|
||||
@@ -106,6 +106,26 @@ export function lowerFsLinkOrStatfsCall(
|
||||
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
|
||||
}
|
||||
|
||||
/** fs.cpSync / fs/promises cp: src, dest and the optional options cross
|
||||
* as dyn values (an omitted argument is undefined); scr_fs_cp_begin
|
||||
* validates them in Node's order. */
|
||||
export function lowerFsCpCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
fn: BuiltinModuleFn,
|
||||
loc: SrcLoc,
|
||||
name: string,
|
||||
): IrExpr {
|
||||
if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement))
|
||||
lowerer.noLowering(`${name} with this argument shape`, expr);
|
||||
const args = [0, 1, 2].map((index) =>
|
||||
expr.arguments[index]
|
||||
? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN)
|
||||
: dynUndefinedExpr(loc),
|
||||
);
|
||||
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
|
||||
}
|
||||
|
||||
export function lowerFsTimestampCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { lowerDiagnosticsChannelCall } from "./async-context.js";
|
||||
import { lowerTimersPromisesCall } from "./timers.js";
|
||||
import {
|
||||
lowerFsCallbackCall,
|
||||
lowerFsCpCall,
|
||||
lowerFsLinkOrStatfsCall,
|
||||
lowerFsTimestampCall,
|
||||
lowerFsVectorOrTruncateCall,
|
||||
@@ -72,6 +73,11 @@ export function lowerBuiltinModuleCall(
|
||||
if (fn.fn === "fs.callbackCall" && bi.member !== "rename") {
|
||||
return lowerFsCallbackCall(lowerer, expr, bi, loc);
|
||||
}
|
||||
if (
|
||||
(bi.module === "fs" && bi.member === "cpSync") ||
|
||||
(bi.module === "fs/promises" && bi.member === "cp")
|
||||
)
|
||||
return lowerFsCpCall(lowerer, expr, fn, loc, name);
|
||||
if (
|
||||
(bi.module === "fs" &&
|
||||
["linkSync", "symlinkSync", "readlinkSync", "statfsSync"].includes(bi.member)) ||
|
||||
|
||||
@@ -793,6 +793,10 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void {
|
||||
{ name: "%systemCall", type: STRING },
|
||||
{ name: "%systemPath", type: STRING },
|
||||
{ name: "%systemDest", type: STRING },
|
||||
{ name: "%systemMessage", type: STRING },
|
||||
{ name: "%systemCode", type: STRING },
|
||||
{ name: "%nativePlain", type: BOOL },
|
||||
{ name: "%errnoOrder", type: BOOL },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -1222,6 +1222,16 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
result: VOID,
|
||||
valueParams: exactValueParams(DYN, DYN),
|
||||
},
|
||||
// Options and paths are checked by the runtime (scr_fs_cp_begin).
|
||||
cpSync: {
|
||||
fn: "fs.cpSync",
|
||||
params: [DYN, DYN, DYN],
|
||||
result: VOID,
|
||||
valueParams: [
|
||||
...exactValueParams(DYN, DYN),
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
symlinkSync: {
|
||||
fn: "fs.symlinkSync",
|
||||
params: [DYN, DYN, DYN],
|
||||
@@ -1362,6 +1372,15 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
result: { kind: "promise", inner: VOID },
|
||||
valueParams: exactValueParams(DYN, DYN),
|
||||
},
|
||||
cp: {
|
||||
fn: "fsp.cp",
|
||||
params: [DYN, DYN, DYN],
|
||||
result: { kind: "promise", inner: VOID },
|
||||
valueParams: [
|
||||
...exactValueParams(DYN, DYN),
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
symlink: {
|
||||
fn: "fsp.symlink",
|
||||
params: [DYN, DYN, DYN],
|
||||
|
||||
@@ -813,6 +813,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fs.writevSync",
|
||||
"fs.linkSync",
|
||||
"fs.symlinkSync",
|
||||
"fs.cpSync",
|
||||
"fs.readlinkSync",
|
||||
"fs.statfsSync",
|
||||
"fs.readlinkSyncStr",
|
||||
|
||||
@@ -979,6 +979,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"fs.writevSync": { argTypes: [F64, arrayOf(BYTES_U8), F64], result: F64 },
|
||||
"fs.linkSync": { argTypes: [DYN, DYN], result: VOID },
|
||||
"fs.symlinkSync": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
/** fs.cpSync / fs/promises cp (scr_fs_cp.c): src, dest and options are
|
||||
* dyn values the runtime validates in Node's order. */
|
||||
"fs.cpSync": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
"fs.readlinkSync": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fs.statfsSync": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fs.readlinkSyncStr": { argTypes: [DYN, DYN], result: STRING },
|
||||
@@ -2682,6 +2685,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"fsp.rmdir": { argTypes: [STRING], 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.cp": { argTypes: [DYN, DYN, DYN], result: { kind: "promise", inner: VOID } },
|
||||
"fsp.readlink": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"fsp.statfs": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"fsp.readlinkStr": { argTypes: [DYN, DYN], result: { kind: "promise", inner: STRING } },
|
||||
|
||||
@@ -867,6 +867,12 @@
|
||||
"name": "fs.copyFileSync",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.fs.cpSync",
|
||||
"kind": "node-builtin",
|
||||
"name": "fs.cpSync",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.fs.createReadStream",
|
||||
"kind": "node-builtin",
|
||||
@@ -1018,6 +1024,12 @@
|
||||
"name": "fs/promises.copyFile",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.fs.promises.cp",
|
||||
"kind": "node-builtin",
|
||||
"name": "fs/promises.cp",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.fs.promises.glob",
|
||||
"kind": "node-builtin",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5567-fs-cpsync.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5568-fs-cp-errors.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5569-fs-cp-symlinks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5570-fs-cp-async.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5571-fs-cp-deep.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -21,6 +21,7 @@ const BASE_RUNTIME_SOURCES = [
|
||||
"scr_array.c",
|
||||
"scr_bytes.c",
|
||||
"scr_bytes_io.c",
|
||||
"scr_fs_cp.c",
|
||||
"scr_map.c",
|
||||
"scr_closure.c",
|
||||
"scr_ffi.c",
|
||||
|
||||
@@ -19,6 +19,7 @@ const BASE_RUNTIME_SOURCES = [
|
||||
"scr_array.c",
|
||||
"scr_bytes.c",
|
||||
"scr_bytes_io.c",
|
||||
"scr_fs_cp.c",
|
||||
"scr_map.c",
|
||||
"scr_closure.c",
|
||||
"scr_ffi.c",
|
||||
|
||||
@@ -2311,6 +2311,25 @@ static bool scr_await_settled(ScrPromise *p) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scr_promise_settled_now(ScrPromise *p) {
|
||||
if (p->state == SCR_PROM_PENDING) return false;
|
||||
scr_prom_observe(p);
|
||||
if (p->state == SCR_PROM_REJECTED) scr_promise_rethrow(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scr_promise_payload_truthy(const ScrPromise *p) {
|
||||
switch (p->payload_kind) {
|
||||
case SCR_EXC_NONE: return false;
|
||||
case SCR_EXC_F64: return p->f64 != 0 && p->f64 == p->f64;
|
||||
case SCR_EXC_BOOL: return p->b;
|
||||
case SCR_EXC_STR: return p->payload && ((const ScrStr *)p->payload)->len > 0;
|
||||
default:
|
||||
if (p->retain_fn == scr_dyn_retain_v) return p->payload && scr_dyn_truthy(p->payload);
|
||||
return p->payload != NULL;
|
||||
}
|
||||
}
|
||||
|
||||
double scr_await_f64(ScrPromise *p) {
|
||||
return scr_await_settled(p) ? p->f64 : 0;
|
||||
}
|
||||
|
||||
@@ -318,6 +318,12 @@ bool scr_promise_payload_is_dyn(const ScrPromise *p);
|
||||
double scr_promise_payload_num(const ScrPromise *p);
|
||||
bool scr_promise_payload_flag(const ScrPromise *p);
|
||||
bool scr_promise_await_settled(ScrPromise *p); /* park/hop; false = rethrown */
|
||||
/* An already-settled promise's outcome without awaiting (no job hop, no
|
||||
* parking): false while p is pending; otherwise true, with the rejection
|
||||
* rethrown when p rejected. payload_truthy is the fulfillment value's
|
||||
* JavaScript truthiness. */
|
||||
bool scr_promise_settled_now(ScrPromise *p);
|
||||
bool scr_promise_payload_truthy(const ScrPromise *p);
|
||||
void scr_promise_payload_release(const ScrPromise *p, void *v);
|
||||
|
||||
void scr_set_timeout(ScrClosure *cb, double ms); /* cb ownership moves in */
|
||||
|
||||
@@ -19,7 +19,6 @@ static const ScrArrOps scr_bytes_io_bytes_ops = { scr_bytes_retain_v, scr_bytes_
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <utime.h>
|
||||
#ifndef O_SYNC
|
||||
#define O_SYNC 0 /* Windows CRT opens have no separate synchronous flag. */
|
||||
#endif
|
||||
@@ -331,6 +330,10 @@ static ScrStr *scr_fs_checked_path(const ScrDyn *value, const char *name) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
ScrStr *scr_fs_validated_path(const ScrDyn *value, const char *name) { return scr_fs_checked_path(value, name); }
|
||||
|
||||
ScrDyn *scr_fs_option_get(const ScrDyn *options, const char *name) { return scr_fs_cb_option(options, name); }
|
||||
|
||||
static ScrStr *scr_fs_timestamp_path(const ScrDyn *value) {
|
||||
return scr_fs_checked_path(value, "path");
|
||||
}
|
||||
@@ -554,44 +557,34 @@ static ScrStr *scr_fs_cb_join(const ScrStr *directory, const char *name) {
|
||||
return path;
|
||||
}
|
||||
|
||||
static void scr_fs_cb_copy(const ScrStr *source, const ScrStr *destination, bool recursive, bool force, bool preserve) {
|
||||
struct stat status;
|
||||
if (
|
||||
#ifdef _WIN32
|
||||
stat(source->data, &status)
|
||||
#else
|
||||
lstat(source->data, &status)
|
||||
#endif
|
||||
< 0) { scr_fs_throw(errno, "stat", source); return; }
|
||||
#ifndef _WIN32
|
||||
if (S_ISLNK(status.st_mode)) {
|
||||
const char *message = "Symbolic-link cp entries have no native lowering";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (S_ISDIR(status.st_mode)) {
|
||||
if (!recursive) { scr_fs_throw(EISDIR, "cp", source); return; }
|
||||
scr_fs_mkdir_recursive_mode((ScrStr *)destination, status.st_mode & 0777);
|
||||
if (scr_exc_pending()) return;
|
||||
DIR *directory = opendir(source->data);
|
||||
if (!directory) { scr_fs_throw(errno, "opendir", source); return; }
|
||||
struct dirent *entry;
|
||||
while (!scr_exc_pending() && (entry = readdir(directory))) {
|
||||
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
|
||||
ScrStr *from = scr_fs_cb_join(source, entry->d_name), *to = scr_fs_cb_join(destination, entry->d_name);
|
||||
scr_fs_cb_copy(from, to, true, force, preserve);
|
||||
scr_str_release(from); scr_str_release(to);
|
||||
}
|
||||
closedir(directory);
|
||||
} else {
|
||||
if (!force && scr_fs_exists((ScrStr *)destination)) return;
|
||||
scr_fs_copyfile((ScrStr *)source, (ScrStr *)destination);
|
||||
}
|
||||
if (preserve && !scr_exc_pending()) {
|
||||
struct utimbuf times = { status.st_atime, status.st_mtime };
|
||||
if (utime(destination->data, ×) < 0) scr_fs_throw(errno, "utime", destination);
|
||||
}
|
||||
static ScrDyn *scr_fs_cb_stat_test(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
(void)args; (void)argc;
|
||||
return scr_dyn_new_bool(scr_box_get_bool(closure->caps[0]));
|
||||
}
|
||||
|
||||
static void scr_fs_cb_stat_method(ScrDyn *object, const char *name, bool answer) {
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
closure->caps[0] = scr_box_new(SCR_BOX_BOOL);
|
||||
scr_box_set_bool(closure->caps[0], answer);
|
||||
scr_dyn_obj_set(object, name, strlen(name), scr_dyn_new_func(closure, scr_fs_cb_stat_test, 0, "native:fs.Stats", name));
|
||||
}
|
||||
|
||||
static void scr_fs_cb_stat_number(ScrDyn *object, const char *name, int64_t number, bool bigint) {
|
||||
ScrDyn *value;
|
||||
if (bigint) {
|
||||
ScrBigInt *integer = scr_bigint_from_i64(number);
|
||||
value = scr_dyn_new_bigint(integer);
|
||||
scr_bigint_release(integer);
|
||||
} else value = scr_dyn_new_num((double)number);
|
||||
scr_dyn_obj_set(object, name, strlen(name), value);
|
||||
}
|
||||
|
||||
static void scr_fs_cb_stat_date(ScrDyn *object, const char *name, double milliseconds) {
|
||||
ScrDyn *arguments = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(arguments, scr_dyn_new_num(milliseconds));
|
||||
ScrDyn *date = scr_dyn_native_date_new(arguments);
|
||||
scr_dyn_release(arguments);
|
||||
if (date) scr_dyn_obj_set(object, name, strlen(name), date);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_fs_cb_stats(const ScrStr *path, double fd, const ScrDyn *options, bool nofollow) {
|
||||
@@ -765,6 +758,24 @@ 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, "cp")) {
|
||||
/* fs.cp(src, dest[, options], callback): makeCallback, then the option
|
||||
* and path validation (thrown), then the copy, whose error reaches the
|
||||
* callback as callbackify delivers it. */
|
||||
const ScrDyn *options = scr_fs_cb_arg(args, argc, 2);
|
||||
const ScrDyn *callback = scr_fs_cb_arg(args, argc, 3);
|
||||
if (options->kind == SCR_DYN_FUNC) {
|
||||
callback = options;
|
||||
options = scr_dyn_undefined();
|
||||
}
|
||||
if (!scr_fs_cb_chk(callback, "cb")) return NULL;
|
||||
ScrFsCp *job = scr_fs_cp_begin(scr_fs_cb_arg(args, argc, 0), scr_fs_cb_arg(args, argc, 1), options);
|
||||
if (!job) return NULL;
|
||||
scr_fs_cp_run(job, true);
|
||||
scr_fs_cp_free(job);
|
||||
scr_fs_cb_schedule(callback, scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()), NULL);
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
if (!strcmp(op, "statfs")) {
|
||||
const ScrDyn *options = scr_fs_cb_arg(args, argc, 1);
|
||||
const ScrDyn *callback = options->kind == SCR_DYN_FUNC ? options : scr_fs_cb_arg(args, argc, 2);
|
||||
@@ -833,8 +844,8 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
|
||||
if (vector && !scr_fs_int_range_chk(scr_fs_cb_arg(args, argc, 0), "fd", 0, 2147483647.0, ">= 0 && <= 2147483647")) return NULL;
|
||||
ScrArr *vectors = vector ? scr_fs_cb_vectors(scr_fs_cb_arg(args, argc, 1)) : NULL;
|
||||
if (vector && !vectors) return NULL;
|
||||
/* cp, glob and rm keep the last argument as their callback. */
|
||||
bool legacy = !strcmp(op, "cp") || !strcmp(op, "glob") || !strcmp(op, "rm");
|
||||
/* glob and rm keep the last argument as their callback. */
|
||||
bool legacy = !strcmp(op, "glob") || !strcmp(op, "rm");
|
||||
size_t slot = timestamp ? 3 : legacy ? (argc ? argc - 1 : 0) : scr_fs_cb_slot(op, args, argc);
|
||||
const ScrDyn *callback = scr_fs_cb_arg(args, argc, slot);
|
||||
/* These check their callback after the arguments Node validates first
|
||||
@@ -1020,15 +1031,6 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
|
||||
else if (!strcmp(op, "open")) {
|
||||
double opened = scr_fs_cb_open(path, ARG(1), open_mode, "r");
|
||||
if (!scr_exc_pending()) result = scr_dyn_new_num(opened);
|
||||
} else if (!strcmp(op, "cp")) {
|
||||
if (scr_fs_cb_refuse_option(ARG(2), "filter") || scr_fs_cb_refuse_option(ARG(2), "dereference") || scr_fs_cb_refuse_option(ARG(2), "verbatimSymlinks") || scr_fs_cb_refuse_option(ARG(2), "errorOnExist") || scr_fs_cb_refuse_option(ARG(2), "mode")) goto done;
|
||||
ScrStr *destination = scr_fs_cb_path(ARG(1), "dest");
|
||||
if (destination) {
|
||||
ScrDyn *force = scr_fs_cb_option(ARG(2), "force");
|
||||
scr_fs_cb_copy(path, destination, scr_fs_cb_option_bool(ARG(2), "recursive"),
|
||||
force && (force->kind == SCR_DYN_UNDEF || scr_dyn_truthy(force)), scr_fs_cb_option_bool(ARG(2), "preserveTimestamps"));
|
||||
scr_dyn_release(force); scr_str_release(destination);
|
||||
}
|
||||
} else if (!strcmp(op, "glob")) {
|
||||
/* Direct fs.glob calls lower to scr_fs_glob_callback (scr_glob.c); a
|
||||
* stored fs.glob value is not supported. */
|
||||
|
||||
@@ -53,6 +53,8 @@ static void scr_error_gcfree(void *obj) {
|
||||
scr_str_release(e->system_call);
|
||||
scr_str_release(e->system_path);
|
||||
scr_str_release(e->system_dest);
|
||||
scr_str_release(e->system_message);
|
||||
scr_str_release(e->system_code);
|
||||
if (e->error_cause && scr_error_cause_drop) scr_error_cause_drop(obj);
|
||||
scr_obj_free_note();
|
||||
scr_cyc_free(e);
|
||||
@@ -102,6 +104,8 @@ static void scr_error_destroy(void *obj) {
|
||||
scr_str_release(e->system_call);
|
||||
scr_str_release(e->system_path);
|
||||
scr_str_release(e->system_dest);
|
||||
scr_str_release(e->system_message);
|
||||
scr_str_release(e->system_code);
|
||||
if (e->error_cause && scr_error_cause_drop) scr_error_cause_drop(obj);
|
||||
scr_obj_free_note();
|
||||
if (scr_error_traced) scr_cyc_free(e);
|
||||
@@ -284,7 +288,7 @@ static ScrStr *scr_error_format(ScrError *e, ScrStr *name, ScrStr *message) {
|
||||
* the code-prefix test is exact for runtime-thrown errors; a user
|
||||
* assigning an ERR_* code onto a plain Error would bracket here where
|
||||
* Node would not (documented — user code owns that spelling). */
|
||||
bool assertion = name && e->code && e->code->len >= 4 &&
|
||||
bool assertion = !e->native_plain && name && e->code && e->code->len >= 4 &&
|
||||
memcmp(e->code->data, "ERR_", 4) == 0;
|
||||
if (!name || name->len == 0) {
|
||||
return message ? scr_str_retain(message) : scr_str_new("", 0);
|
||||
|
||||
@@ -106,6 +106,12 @@ typedef struct ScrError {
|
||||
ScrStr *system_call;
|
||||
ScrStr *system_path;
|
||||
ScrStr *system_dest;
|
||||
/* Node's SystemError (internal/errors.js): the context message and errno
|
||||
* name behind `info`; NULL for every other error. */
|
||||
ScrStr *system_message;
|
||||
ScrStr *system_code;
|
||||
bool native_plain; /* a native binding's coded Error: "Name: message", no "[code]" */
|
||||
bool errno_order; /* ErrnoException key order: errno, code, path, syscall */
|
||||
} ScrError;
|
||||
|
||||
enum {
|
||||
@@ -150,6 +156,10 @@ typedef struct ScrDomException {
|
||||
ScrStr *system_call;
|
||||
ScrStr *system_path;
|
||||
ScrStr *system_dest;
|
||||
ScrStr *system_message;
|
||||
ScrStr *system_code;
|
||||
bool native_plain;
|
||||
bool errno_order;
|
||||
double dom_code; /* the WebIDL legacy code (0 when the name is off-table) */
|
||||
bool has_cause; /* the options form carried a `cause` member */
|
||||
struct ScrDyn *cause; /* owned; NULL when has_cause is false */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
/* scriptc runtime — fs.cpSync, fs.cp and fs/promises cp (scr_fs_cp.c).
|
||||
* Included by scr_runtime.h, which says where new declarations go. */
|
||||
#ifndef SCR_FS_CP_H
|
||||
#define SCR_FS_CP_H
|
||||
|
||||
#include "scr_async.h"
|
||||
#include "scr_dyn.h"
|
||||
|
||||
/* scr_fs_cp_begin runs validateCpOptions(options) and getValidatedPath for
|
||||
* src and dest (all borrowed) and answers the copy job, or NULL with the
|
||||
* validation error pending. scr_fs_cp_run copies with cpSync's semantics,
|
||||
* or with fs/promises cp's when promise_form is set, leaving any copy
|
||||
* error pending. scr_fs_cp_sync is fs.cpSync; scr_fsp_cp is fs/promises
|
||||
* cp, settled before it returns (validation errors reject it). */
|
||||
typedef struct ScrFsCp ScrFsCp;
|
||||
ScrFsCp *scr_fs_cp_begin(const ScrDyn *src, const ScrDyn *dest, const ScrDyn *options);
|
||||
void scr_fs_cp_run(ScrFsCp *job, bool promise_form);
|
||||
void scr_fs_cp_free(ScrFsCp *job); /* NULL-tolerant */
|
||||
void scr_fs_cp_sync(const ScrDyn *src, const ScrDyn *dest, const ScrDyn *options);
|
||||
ScrPromise *scr_fsp_cp(const ScrDyn *src, const ScrDyn *dest, const ScrDyn *options);
|
||||
|
||||
#endif /* SCR_FS_CP_H */
|
||||
@@ -105,6 +105,12 @@ ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args);
|
||||
* becomes [err], otherwise [null, result(, buffer)]; the callback runs on
|
||||
* a later loop turn. result moves in. */
|
||||
void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *buffer);
|
||||
/* getValidatedPath(value, name): a string, Buffer or file: URL path
|
||||
* without NUL bytes (+1), or NULL with Node's TypeError pending. */
|
||||
ScrStr *scr_fs_validated_path(const ScrDyn *value, const char *name);
|
||||
/* options[name] for an options object (+1; undefined when options is
|
||||
* nullish), or NULL with an exception pending. */
|
||||
ScrDyn *scr_fs_option_get(const ScrDyn *options, const char *name);
|
||||
ScrBytes *scr_fs_read_fd_bytes(double fd); /* +1; the fd form (scr_lib.c) */
|
||||
void scr_fs_write_file_bytes(ScrStr *path, const ScrBytes *data);
|
||||
void scr_fs_append_file_bytes(ScrStr *path, const ScrBytes *data);
|
||||
|
||||
@@ -5658,12 +5658,35 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable;
|
||||
}
|
||||
if (e->stack) scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack));
|
||||
/* errno rides fs errors (with syscall) and zlib errors (without). */
|
||||
if (e->system_call || e->system_errno != 0)
|
||||
if (e->system_message) {
|
||||
/* SystemError: code, the read-only `info` context ({ message, path,
|
||||
* syscall, errno, code }), then errno, syscall, path and dest. */
|
||||
if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
|
||||
ScrDyn *info = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(info, "message", 7, scr_dyn_new_str(e->system_message));
|
||||
if (e->system_path) scr_dyn_obj_set(info, "path", 4, scr_dyn_new_str(e->system_path));
|
||||
if (e->system_dest) scr_dyn_obj_set(info, "dest", 4, scr_dyn_new_str(e->system_dest));
|
||||
if (e->system_call) scr_dyn_obj_set(info, "syscall", 7, scr_dyn_new_str(e->system_call));
|
||||
scr_dyn_obj_set(info, "errno", 5, scr_dyn_new_num(e->system_errno));
|
||||
if (e->system_code) scr_dyn_obj_set(info, "code", 4, scr_dyn_new_str(e->system_code));
|
||||
scr_dyn_obj_set(d, "info", 4, info);
|
||||
d->v.obj.entries[d->v.obj.len - 1].writable = false;
|
||||
scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno));
|
||||
if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
|
||||
if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call));
|
||||
if (e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path));
|
||||
if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call));
|
||||
} else if (e->errno_order) {
|
||||
/* ErrnoException: errno, code, path, syscall. */
|
||||
if (e->system_call) scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno));
|
||||
if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
|
||||
if (e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path));
|
||||
if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call));
|
||||
} else {
|
||||
/* errno rides fs errors (with syscall) and zlib errors (without). */
|
||||
if (e->system_call || e->system_errno != 0)
|
||||
scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno));
|
||||
if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code));
|
||||
if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call));
|
||||
}
|
||||
if (!e->errno_order && e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path));
|
||||
if (e->system_dest) scr_dyn_obj_set(d, "dest", 4, scr_dyn_new_str(e->system_dest));
|
||||
if (e->error_cause) {
|
||||
scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause));
|
||||
|
||||
@@ -2914,6 +2914,7 @@ static const char *scr_errno_name(int e, char *fallback, size_t cap) {
|
||||
case ESPIPE: return "ESPIPE";
|
||||
case ELOOP: return "ELOOP";
|
||||
case ENOSYS: return "ENOSYS";
|
||||
case ENOTSUP: return "ENOTSUP";
|
||||
default:
|
||||
snprintf(fallback, cap, "E%d", e);
|
||||
return fallback;
|
||||
@@ -3027,6 +3028,69 @@ void scr_fs_throw(int e, const char *op, const ScrStr *path) {
|
||||
scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* A native binding's coded Error (Node's THROW_ERR_* macros): "Error:
|
||||
* message" with an own `code`, and no "[code]" in its name. */
|
||||
void scr_fs_throw_native_coded(const char *code, const char *message, size_t length) {
|
||||
ScrStr *text = scr_str_new(message, length);
|
||||
ScrError *error = scr_error_new(SCR_ERR_ERROR, text);
|
||||
scr_str_release(text);
|
||||
scr_error_set_code(error, code);
|
||||
error->native_plain = true;
|
||||
scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* Node's ErrnoException (env->ThrowErrnoException, used for std::error_code
|
||||
* failures): "EEXIST, File exists 'path'" with errno (the positive errno),
|
||||
* code, path and syscall. */
|
||||
void scr_fs_throw_errno_exception(int e, const char *syscall, const ScrStr *path) {
|
||||
char namebuf[16];
|
||||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||||
const char *text = strerror(e);
|
||||
size_t cap = strlen(name) + strlen(text) + (path ? path->len : 0) + 8;
|
||||
char *msg = malloc(cap);
|
||||
if (!msg) scr_trap("scriptc: out of memory\n");
|
||||
int len = path ? snprintf(msg, cap, "%s, %s '%s'", name, text, path->data) : snprintf(msg, cap, "%s, %s", name, text);
|
||||
ScrStr *message = scr_str_new(msg, (size_t)len);
|
||||
free(msg);
|
||||
ScrError *error = scr_error_new(SCR_ERR_ERROR, message);
|
||||
scr_str_release(message);
|
||||
scr_error_set_code(error, name);
|
||||
error->system_errno = e;
|
||||
error->system_call = scr_str_new(syscall, strlen(syscall));
|
||||
error->system_path = path ? scr_str_retain((ScrStr *)path) : NULL;
|
||||
error->errno_order = true;
|
||||
scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* Node's SystemError (internal/errors.js) with a { message, path, syscall,
|
||||
* errno, code } context: "<prefix>: <syscall> returned <errno name>
|
||||
* (<message>) <path>", named SystemError, with code, info, errno, syscall
|
||||
* and path own properties. */
|
||||
void scr_fs_throw_system_error(const char *code, const char *prefix, int e, const char *syscall,
|
||||
const ScrStr *message, const ScrStr *path) {
|
||||
char namebuf[16];
|
||||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||||
size_t cap = strlen(prefix) + strlen(syscall) + strlen(name) + message->len + (path ? path->len : 0) + 32;
|
||||
char *msg = malloc(cap);
|
||||
if (!msg) scr_trap("scriptc: out of memory\n");
|
||||
int len = snprintf(msg, cap, "%s: %s returned %s (%s)", prefix, syscall, name, message->data);
|
||||
if (path) len += snprintf(msg + len, cap - (size_t)len, " %s", path->data);
|
||||
ScrStr *text = scr_str_new(msg, (size_t)len);
|
||||
free(msg);
|
||||
ScrError *error = scr_error_new(SCR_ERR_ERROR, text);
|
||||
scr_str_release(text);
|
||||
scr_str_release(error->name);
|
||||
error->name = scr_str_new("SystemError", 11);
|
||||
error->name_present = true;
|
||||
scr_error_set_code(error, code);
|
||||
error->system_errno = e;
|
||||
error->system_call = scr_str_new(syscall, strlen(syscall));
|
||||
error->system_path = path ? scr_str_retain((ScrStr *)path) : NULL;
|
||||
error->system_message = scr_str_retain((ScrStr *)message);
|
||||
error->system_code = scr_str_new(name, strlen(name));
|
||||
scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* ── fs operations ───────────────────────────────────────────────────── */
|
||||
|
||||
static FILE *scr_fs_fopen(const ScrStr *path, const char *mode) {
|
||||
@@ -4236,7 +4300,7 @@ int scr_fs_raw_close(int fd) {
|
||||
|
||||
/* The two-path fs error shape — Node's copyfile errors quote both ends:
|
||||
* "ENOENT: no such file or directory, copyfile 'src' -> 'dest'". */
|
||||
static void scr_fs_throw2(int e, const char *op, const ScrStr *src, const ScrStr *dest) {
|
||||
void scr_fs_throw2(int e, const char *op, const ScrStr *src, const ScrStr *dest) {
|
||||
char namebuf[16];
|
||||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||||
const char *text = scr_errno_text(e);
|
||||
|
||||
@@ -288,6 +288,20 @@ bool scr_fs_access_mode_valid(double mode);
|
||||
bool scr_fs_truncate_len_valid(double length);
|
||||
void scr_fs_truncate(ScrStr *path, double length);
|
||||
void scr_fs_copyfile(ScrStr *src, ScrStr *dest);
|
||||
/* The two-path form of scr_fs_throw: "ENOENT: no such file or directory,
|
||||
* copyfile 'src' -> 'dest'", with path and dest own properties. */
|
||||
void scr_fs_throw2(int e, const char *op, const ScrStr *src, const ScrStr *dest);
|
||||
/* The error shapes Node's native fs bindings throw besides the libuv one
|
||||
* (scr_fs_throw): a THROW_ERR_* coded Error ("Error: message", own `code`,
|
||||
* no "[code]" in its name); an ErrnoException from a std::error_code
|
||||
* ("EEXIST, File exists 'path'", positive errno, keys errno, code, path,
|
||||
* syscall); and internal/errors.js's SystemError ("<prefix>: <syscall>
|
||||
* returned <errno name> (<message>) <path>", named SystemError, keys code,
|
||||
* info, errno, syscall, path). */
|
||||
void scr_fs_throw_native_coded(const char *code, const char *message, size_t length);
|
||||
void scr_fs_throw_errno_exception(int e, const char *syscall, const ScrStr *path);
|
||||
void scr_fs_throw_system_error(const char *code, const char *prefix, int e, const char *syscall,
|
||||
const ScrStr *message, const ScrStr *path);
|
||||
void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath);
|
||||
/* The callback rename worker uses the non-throwing syscall seam off the
|
||||
* runtime thread, then materializes any Node-shaped Error back on the main
|
||||
|
||||
@@ -73,5 +73,6 @@
|
||||
#include "scr_test.h"
|
||||
#include "scr_assert.h"
|
||||
#include "scr_console.h"
|
||||
#include "scr_fs_cp.h"
|
||||
|
||||
#endif /* SCR_RUNTIME_H */
|
||||
|
||||
@@ -34,6 +34,7 @@ beforeAll(async () => {
|
||||
join(testDir, "../src/scr_bytes.c"),
|
||||
join(testDir, "../src/scr_shared.c"),
|
||||
join(testDir, "../src/scr_bytes_io.c"),
|
||||
join(testDir, "../src/scr_fs_cp.c"),
|
||||
join(testDir, "../src/scr_zlib.c"),
|
||||
join(testDir, "../src/scr_lib.c"),
|
||||
join(testDir, "../src/scr_hash.c"),
|
||||
|
||||
@@ -14,6 +14,7 @@ const runtimeSources = [
|
||||
"scr_bytes.c",
|
||||
"scr_bytes.test.c",
|
||||
"scr_bytes_io.c",
|
||||
"scr_fs_cp.c",
|
||||
"scr_zlib.c",
|
||||
"scr_lib.c",
|
||||
"scr_hash.c",
|
||||
|
||||
@@ -16,6 +16,7 @@ const RUNTIME_SOURCES = [
|
||||
"scr_bytes.c",
|
||||
"scr_shared.c",
|
||||
"scr_bytes_io.c",
|
||||
"scr_fs_cp.c",
|
||||
"scr_map.c",
|
||||
"scr_closure.c",
|
||||
"scr_ffi.c",
|
||||
|
||||
@@ -14,6 +14,7 @@ const runtimeSources = [
|
||||
"scr_bytes.c",
|
||||
"scr_bytes.test.c",
|
||||
"scr_bytes_io.c",
|
||||
"scr_fs_cp.c",
|
||||
"scr_zlib.c",
|
||||
"scr_hash.c",
|
||||
"scr_string.c",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// fs.cpSync: files, recursive directories, the force / errorOnExist /
|
||||
// mode / filter / preserveTimestamps options, and directory merges.
|
||||
// Paths are relative to a scratch directory so messages are stable.
|
||||
import * as fs from "node:fs";
|
||||
|
||||
function tail(path: string): string {
|
||||
let i = path.length - 1;
|
||||
while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1;
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const root = `/tmp/scr-cpsync-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`;
|
||||
if (fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(root);
|
||||
const home = process.cwd();
|
||||
process.chdir(root);
|
||||
|
||||
function tree(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
const walk = (d: string): void => {
|
||||
const names = fs.readdirSync(d).sort();
|
||||
for (const name of names) {
|
||||
const p = `${d}/${name}`;
|
||||
const st = fs.lstatSync(p);
|
||||
if (st.isDirectory()) {
|
||||
out.push(`${p}/`);
|
||||
walk(p);
|
||||
} else if (st.isSymbolicLink()) {
|
||||
out.push(`${p} -> ${fs.readlinkSync(p).split(root).join("<root>")}`);
|
||||
} else {
|
||||
out.push(`${p} = ${fs.readFileSync(p, "utf8")}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
return out;
|
||||
}
|
||||
|
||||
function attempt(label: string, run: () => void): void {
|
||||
try {
|
||||
run();
|
||||
console.log(label, "ok");
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
console.log(label, err.name, err.code, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync("src/sub/deep", { recursive: true });
|
||||
fs.writeFileSync("src/a.txt", "a");
|
||||
fs.writeFileSync("src/sub/b.txt", "b");
|
||||
fs.writeFileSync("src/sub/deep/c.txt", "c");
|
||||
fs.writeFileSync("file.txt", "file");
|
||||
|
||||
// A single file, into a missing parent directory.
|
||||
fs.cpSync("file.txt", "out/nested/file.txt");
|
||||
console.log(tree("out"));
|
||||
|
||||
// A directory tree.
|
||||
fs.cpSync("src", "copy", { recursive: true });
|
||||
console.log(tree("copy"));
|
||||
|
||||
// force (the default) overwrites; force: false keeps the existing file.
|
||||
fs.writeFileSync("other.txt", "other");
|
||||
fs.cpSync("other.txt", "out/nested/file.txt");
|
||||
console.log(fs.readFileSync("out/nested/file.txt", "utf8"));
|
||||
fs.writeFileSync("other.txt", "changed");
|
||||
fs.cpSync("other.txt", "out/nested/file.txt", { force: false });
|
||||
console.log(fs.readFileSync("out/nested/file.txt", "utf8"));
|
||||
attempt("errorOnExist", () =>
|
||||
fs.cpSync("other.txt", "out/nested/file.txt", { force: false, errorOnExist: true }),
|
||||
);
|
||||
|
||||
// Merging into an existing directory: force overwrites, force: false skips.
|
||||
fs.writeFileSync("src/a.txt", "a2");
|
||||
fs.writeFileSync("copy/extra.txt", "extra");
|
||||
fs.cpSync("src", "copy", { recursive: true, force: false });
|
||||
console.log(tree("copy"));
|
||||
fs.cpSync("src", "copy", { recursive: true });
|
||||
console.log(tree("copy"));
|
||||
attempt("dir errorOnExist", () =>
|
||||
fs.cpSync("src", "copy", { recursive: true, force: false, errorOnExist: true }),
|
||||
);
|
||||
|
||||
// COPYFILE_EXCL through mode.
|
||||
attempt("mode excl", () => fs.cpSync("other.txt", "out/nested/file.txt", { mode: fs.constants.COPYFILE_EXCL }));
|
||||
fs.cpSync("other.txt", "fresh.txt", { mode: fs.constants.COPYFILE_EXCL });
|
||||
console.log(fs.readFileSync("fresh.txt", "utf8"));
|
||||
|
||||
// filter: called for the top-level paths and each entry, in order.
|
||||
const seen: string[] = [];
|
||||
fs.cpSync("src", "filtered", {
|
||||
recursive: true,
|
||||
filter: (from: string, to: string) => {
|
||||
seen.push(`${from} -> ${to}`);
|
||||
return !from.endsWith("b.txt");
|
||||
},
|
||||
});
|
||||
console.log(seen.sort());
|
||||
console.log(tree("filtered"));
|
||||
fs.cpSync("src", "skipped", { recursive: true, filter: () => false });
|
||||
console.log(fs.existsSync("skipped"));
|
||||
|
||||
// preserveTimestamps keeps the source mtime on files.
|
||||
const stamp = new Date("2020-01-02T03:04:05Z");
|
||||
fs.utimesSync("file.txt", stamp, stamp);
|
||||
fs.cpSync("file.txt", "stamped.txt", { preserveTimestamps: true });
|
||||
console.log(new Date(fs.statSync("stamped.txt").mtimeMs).toISOString());
|
||||
fs.cpSync("file.txt", "unstamped.txt");
|
||||
console.log(fs.statSync("unstamped.txt").mtimeMs === stamp.getTime());
|
||||
|
||||
// A file: URL source.
|
||||
fs.cpSync(new URL(`file://${root}/file.txt`), "url.txt");
|
||||
console.log(fs.readFileSync("url.txt", "utf8"));
|
||||
|
||||
process.chdir(home);
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
@@ -0,0 +1,115 @@
|
||||
// fs.cpSync and fs/promises cp errors: option validation, path
|
||||
// validation, and the copy errors, whose class, message, code and own
|
||||
// keys differ between the synchronous and the promise form, as in Node.
|
||||
import * as fs from "node:fs";
|
||||
import * as fsp from "node:fs/promises";
|
||||
|
||||
const any = (value) => /** @type {any} */ (value);
|
||||
|
||||
function tail(path) {
|
||||
let i = path.length - 1;
|
||||
while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1;
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const root = `/tmp/scr-cp-errors-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`;
|
||||
if (fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(root);
|
||||
const home = process.cwd();
|
||||
process.chdir(root);
|
||||
fs.mkdirSync("src/sub", { recursive: true });
|
||||
fs.writeFileSync("src/a.txt", "a");
|
||||
fs.writeFileSync("file.txt", "f");
|
||||
fs.writeFileSync("b.txt", "b");
|
||||
|
||||
function describe(label, e) {
|
||||
const keys = Object.keys(e);
|
||||
console.log(label, e instanceof Error, e.name, e.code, JSON.stringify(e.message), keys);
|
||||
if (keys.includes("info")) console.log(" info", e.info, e.errno, e.syscall, e.path, String(e));
|
||||
else if (keys.includes("errno")) console.log(" errno", e.errno, e.syscall, e.path);
|
||||
}
|
||||
function sync(label, run) {
|
||||
try {
|
||||
run();
|
||||
console.log(label, "ok");
|
||||
} catch (e) {
|
||||
describe(label, e);
|
||||
}
|
||||
}
|
||||
async function promised(label, run) {
|
||||
try {
|
||||
await run();
|
||||
console.log(label, "ok");
|
||||
} catch (e) {
|
||||
describe(label, e);
|
||||
}
|
||||
}
|
||||
|
||||
sync("options type", () => fs.cpSync("file.txt", "x.txt", any(5)));
|
||||
sync("options null", () => fs.cpSync("file.txt", "x.txt", any(null)));
|
||||
sync("recursive type", () => fs.cpSync("file.txt", "x.txt", any({ recursive: 1 })));
|
||||
sync("force type", () => fs.cpSync("file.txt", "x.txt", any({ force: "yes" })));
|
||||
sync("mode range", () => fs.cpSync("file.txt", "x.txt", { mode: 99 }));
|
||||
sync("mode type", () => fs.cpSync("file.txt", "x.txt", any({ mode: "1" })));
|
||||
sync("option pair", () => fs.cpSync("file.txt", "x.txt", { dereference: true, verbatimSymlinks: true }));
|
||||
sync("filter type", () => fs.cpSync("file.txt", "x.txt", any({ filter: 1 })));
|
||||
sync("filter promise", () => fs.cpSync("file.txt", "x.txt", any({ filter: async () => true })));
|
||||
sync("filter throws", () =>
|
||||
fs.cpSync("file.txt", "x.txt", {
|
||||
filter: () => {
|
||||
throw new Error("no copy");
|
||||
},
|
||||
}),
|
||||
);
|
||||
sync("src type", () => fs.cpSync(any(5), "x.txt"));
|
||||
sync("dest type", () => fs.cpSync("file.txt", any({})));
|
||||
sync("src nul", () => fs.cpSync("file\0.txt", "x.txt"));
|
||||
sync("validation order", () => fs.cpSync(any(5), any(6), any({ force: 1 })));
|
||||
|
||||
sync("same", () => fs.cpSync("src", "src", { recursive: true }));
|
||||
sync("not recursive", () => fs.cpSync("src", "out"));
|
||||
sync("dir to file", () => fs.cpSync("src", "file.txt", { recursive: true }));
|
||||
sync("file to dir", () => fs.cpSync("file.txt", "src"));
|
||||
sync("into self", () => fs.cpSync("src", "src/sub/inner", { recursive: true }));
|
||||
sync("missing", () => fs.cpSync("missing.txt", "x.txt"));
|
||||
sync("exists", () => fs.cpSync("file.txt", "b.txt", { force: false, errorOnExist: true }));
|
||||
fs.cpSync("src", "copy", { recursive: true });
|
||||
sync("dir file exists", () => fs.cpSync("src", "copy", { recursive: true, force: false, errorOnExist: true }));
|
||||
fs.rmSync("copy/a.txt");
|
||||
fs.mkdirSync("copy/a.txt");
|
||||
sync("dir entry kind", () => fs.cpSync("src", "copy", { recursive: true }));
|
||||
|
||||
await promised("p options type", () => fsp.cp("file.txt", "x.txt", any(5)));
|
||||
await promised("p src type", () => fsp.cp(any(5), "x.txt"));
|
||||
await promised("p same", () => fsp.cp("src", "src", { recursive: true }));
|
||||
await promised("p not recursive", () => fsp.cp("src", "out"));
|
||||
await promised("p dir to file", () => fsp.cp("src", "file.txt", { recursive: true }));
|
||||
await promised("p file to dir", () => fsp.cp("file.txt", "src"));
|
||||
await promised("p into self", () => fsp.cp("src", "src/sub/inner", { recursive: true }));
|
||||
await promised("p missing", () => fsp.cp("missing.txt", "x.txt"));
|
||||
await promised("p exists", () => fsp.cp("file.txt", "b.txt", { force: false, errorOnExist: true }));
|
||||
await promised("p dir exists", () => fsp.cp("src", "copy", { recursive: true, force: false, errorOnExist: true }));
|
||||
await promised("p dir entry kind", () => fsp.cp("src", "copy", { recursive: true }));
|
||||
|
||||
// The copy takes the source's mode, and a Buffer path works.
|
||||
function access(path) {
|
||||
const can = (mode) => {
|
||||
try {
|
||||
fs.accessSync(path, mode);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return [can(fs.constants.R_OK), can(fs.constants.W_OK), can(fs.constants.X_OK)];
|
||||
}
|
||||
fs.chmodSync("b.txt", 0o500);
|
||||
fs.cpSync("b.txt", any(Buffer.from("moded.txt")));
|
||||
console.log(access("moded.txt"));
|
||||
fs.chmodSync("src/sub", 0o500);
|
||||
fs.cpSync("src", "moded-dir", { recursive: true });
|
||||
console.log(access("moded-dir/sub"));
|
||||
fs.chmodSync("src/sub", 0o700);
|
||||
fs.chmodSync("moded-dir/sub", 0o700);
|
||||
|
||||
process.chdir(home);
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
@@ -0,0 +1,93 @@
|
||||
// fs.cpSync and fs/promises cp with symbolic links: a top-level link,
|
||||
// links inside a copied directory (relative and absolute targets),
|
||||
// verbatimSymlinks, dereference, and replacing an existing link.
|
||||
import * as fs from "node:fs";
|
||||
import * as fsp from "node:fs/promises";
|
||||
|
||||
function tail(path: string): string {
|
||||
let i = path.length - 1;
|
||||
while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1;
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const root = `/tmp/scr-cp-links-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`;
|
||||
if (fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(root);
|
||||
const home = process.cwd();
|
||||
process.chdir(root);
|
||||
const real = fs.realpathSync(root);
|
||||
|
||||
function show(path: string): string {
|
||||
const st = fs.lstatSync(path);
|
||||
if (st.isSymbolicLink()) return `${path} -> ${fs.readlinkSync(path).split(real).join("<root>")}`;
|
||||
if (st.isDirectory()) return `${path}/`;
|
||||
return `${path} = ${fs.readFileSync(path, "utf8")}`;
|
||||
}
|
||||
function tree(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
const walk = (d: string): void => {
|
||||
for (const name of fs.readdirSync(d).sort()) {
|
||||
const p = `${d}/${name}`;
|
||||
out.push(show(p));
|
||||
if (!fs.lstatSync(p).isSymbolicLink() && fs.statSync(p).isDirectory()) walk(p);
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
return out;
|
||||
}
|
||||
function attempt(label: string, run: () => void): void {
|
||||
try {
|
||||
run();
|
||||
console.log(label, "ok");
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
console.log(label, err.name, err.code, err.message.split(real).join("<root>"));
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync("src/sub", { recursive: true });
|
||||
fs.writeFileSync("src/a.txt", "a");
|
||||
fs.writeFileSync("src/sub/b.txt", "b");
|
||||
fs.writeFileSync("target.txt", "t");
|
||||
fs.symlinkSync("a.txt", "src/rel-link");
|
||||
fs.symlinkSync("sub", "src/dir-link");
|
||||
fs.symlinkSync(`${real}/target.txt`, "src/abs-link");
|
||||
fs.symlinkSync("target.txt", "top-link");
|
||||
|
||||
// A top-level link copies as a link; its relative target is resolved.
|
||||
fs.cpSync("top-link", "copied-link");
|
||||
console.log(show("copied-link"));
|
||||
fs.cpSync("top-link", "verbatim-link", { verbatimSymlinks: true });
|
||||
console.log(show("verbatim-link"));
|
||||
// With dereference, the binding's path check treats a link as a directory.
|
||||
attempt("deref file link", () => fs.cpSync("top-link", "deref.txt", { dereference: true }));
|
||||
fs.cpSync("top-link", "deref.txt", { dereference: true, recursive: true });
|
||||
console.log(show("deref.txt"));
|
||||
|
||||
// Links inside a directory: the synchronous copy points each one at its
|
||||
// canonical absolute target.
|
||||
fs.cpSync("src", "sync-copy", { recursive: true });
|
||||
console.log(tree("sync-copy"));
|
||||
fs.cpSync("src", "sync-verbatim", { recursive: true, verbatimSymlinks: true });
|
||||
console.log(tree("sync-verbatim"));
|
||||
|
||||
// The promise form resolves relative targets against the link's directory.
|
||||
await fsp.cp("src", "async-copy", { recursive: true });
|
||||
console.log(tree("async-copy"));
|
||||
await fsp.cp("src", "async-verbatim", { recursive: true, verbatimSymlinks: true });
|
||||
console.log(tree("async-verbatim"));
|
||||
await fsp.cp("src", "async-deref", { recursive: true, dereference: true });
|
||||
console.log(tree("async-deref"));
|
||||
|
||||
// Replacing an existing link, and a link onto a regular file.
|
||||
fs.symlinkSync("src/a.txt", "other-link");
|
||||
fs.cpSync("top-link", "other-link");
|
||||
console.log(show("other-link"));
|
||||
attempt("link onto file", () => fs.cpSync("top-link", "target.txt"));
|
||||
attempt("sync link in dir onto file", () => {
|
||||
fs.mkdirSync("merge", { recursive: true });
|
||||
fs.writeFileSync("merge/rel-link", "file");
|
||||
fs.cpSync("src", "merge", { recursive: true });
|
||||
});
|
||||
|
||||
process.chdir(home);
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
@@ -0,0 +1,101 @@
|
||||
// fs.cp (callback) and fs/promises cp: results and errors reach the
|
||||
// callback after the calling code finishes; the promise form merges
|
||||
// directories, takes async filters, and keeps timestamps.
|
||||
import * as fs from "node:fs";
|
||||
import * as fsp from "node:fs/promises";
|
||||
|
||||
function tail(path: string): string {
|
||||
let i = path.length - 1;
|
||||
while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1;
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const root = `/tmp/scr-cp-async-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`;
|
||||
if (fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(root);
|
||||
const home = process.cwd();
|
||||
process.chdir(root);
|
||||
|
||||
function tree(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
const walk = (d: string): void => {
|
||||
for (const name of fs.readdirSync(d).sort()) {
|
||||
const p = `${d}/${name}`;
|
||||
if (fs.statSync(p).isDirectory()) {
|
||||
out.push(`${p}/`);
|
||||
walk(p);
|
||||
} else {
|
||||
out.push(`${p} = ${fs.readFileSync(p, "utf8")}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
return out;
|
||||
}
|
||||
|
||||
fs.mkdirSync("src/sub", { recursive: true });
|
||||
fs.writeFileSync("src/a.txt", "a");
|
||||
fs.writeFileSync("src/sub/b.txt", "b");
|
||||
fs.writeFileSync("file.txt", "file");
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
fs.cp("file.txt", "cb.txt", (err) => {
|
||||
console.log("callback", err, fs.readFileSync("cb.txt", "utf8"));
|
||||
resolve();
|
||||
});
|
||||
console.log("after call");
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
fs.cp("src", "cb-dir", { recursive: true }, (err) => {
|
||||
console.log("dir callback", err, tree("cb-dir"));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
fs.cp("src", "nope", (err) => {
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
console.log("error callback", e.name, e.code, e.message);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
fs.cp("file.txt", "x.txt", { recursive: true }, undefined as unknown as () => void);
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
console.log("missing callback", err.name, err.code, err.message);
|
||||
}
|
||||
|
||||
// The promise form: filters may be async, merges overwrite by default.
|
||||
const seen: string[] = [];
|
||||
await fsp.cp("src", "p-dir", {
|
||||
recursive: true,
|
||||
filter: async (from: string) => {
|
||||
seen.push(from);
|
||||
return !from.endsWith("b.txt");
|
||||
},
|
||||
});
|
||||
console.log(seen.sort(), tree("p-dir"));
|
||||
|
||||
fs.writeFileSync("src/a.txt", "a2");
|
||||
await fsp.cp("src", "p-dir", { recursive: true, force: false });
|
||||
console.log(tree("p-dir"));
|
||||
await fsp.cp("src", "p-dir", { recursive: true });
|
||||
console.log(tree("p-dir"));
|
||||
|
||||
const stamp = new Date("2021-05-06T07:08:09.123Z");
|
||||
fs.utimesSync("file.txt", stamp, stamp);
|
||||
await fsp.cp("file.txt", "p-stamped.txt", { preserveTimestamps: true });
|
||||
console.log(new Date(fs.statSync("p-stamped.txt").mtimeMs).toISOString());
|
||||
await fsp.cp("src", "p-stamped-dir", { recursive: true, preserveTimestamps: true });
|
||||
console.log(fs.statSync("p-stamped-dir").mtimeMs === fs.statSync("src").mtimeMs);
|
||||
|
||||
const rejected = await fsp.cp("missing", "x").then(
|
||||
() => "fulfilled",
|
||||
(e: NodeJS.ErrnoException) => `${e.code} ${e.message}`,
|
||||
);
|
||||
console.log(rejected);
|
||||
|
||||
process.chdir(home);
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
@@ -0,0 +1,42 @@
|
||||
// fs.cpSync and fs/promises cp over a directory chain 400 levels deep (its
|
||||
// deepest path stays under macOS's 1024-byte PATH_MAX), from a module with
|
||||
// top-level await (its code runs on a fiber stack).
|
||||
import * as fs from "node:fs";
|
||||
import * as fsp from "node:fs/promises";
|
||||
|
||||
function tail(path: string): string {
|
||||
let i = path.length - 1;
|
||||
while (i >= 0 && path.charAt(i) !== "/" && path.charAt(i) !== "\\") i = i - 1;
|
||||
return path.slice(i + 1);
|
||||
}
|
||||
const root = `/tmp/scr-cp-deep-${tail(process.argv[1] === undefined ? "x" : process.argv[1])}`;
|
||||
if (fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(root);
|
||||
const home = process.cwd();
|
||||
process.chdir(root);
|
||||
|
||||
const depth = 400;
|
||||
let path = "src";
|
||||
for (let i = 0; i < depth; i++) path += "/d";
|
||||
fs.mkdirSync(path, { recursive: true });
|
||||
fs.writeFileSync(`${path}/leaf.txt`, "leaf");
|
||||
|
||||
function count(dir: string): number {
|
||||
let levels = 0;
|
||||
let at = dir;
|
||||
while (fs.existsSync(`${at}/d`)) {
|
||||
at += "/d";
|
||||
levels += 1;
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
fs.cpSync("src", "sync", { recursive: true });
|
||||
console.log("sync", count("sync"), fs.readFileSync(`sync${path.slice(3)}/leaf.txt`, "utf8"));
|
||||
fs.cpSync("src", "filtered", { recursive: true, filter: () => true });
|
||||
console.log("filtered", count("filtered"));
|
||||
await fsp.cp("src", "promise", { recursive: true });
|
||||
console.log("promise", count("promise"));
|
||||
|
||||
process.chdir(home);
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
Reference in New Issue
Block a user