Add fs.cpSync, fs.cp and fs/promises cp (#1166)

This commit is contained in:
Chris Tate
2026-10-17 02:55:40 -05:00
committed by GitHub
46 changed files with 2398 additions and 98 deletions
+2
View File
@@ -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
}
+11 -11
View File
@@ -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"] },
+47
View File
@@ -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 } },
+12
View File
@@ -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": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5568-fs-cp-errors.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5569-fs-cp-symlinks.ts"
],
"diags": []
}
@@ -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",
+19
View File
@@ -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;
}
+6
View File
@@ -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 */
+52 -50
View File
@@ -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, &times) < 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. */
+5 -1
View File
@@ -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);
+10
View File
@@ -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
+22
View File
@@ -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 */
+6
View File
@@ -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);
+28 -5
View File
@@ -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));
+65 -1
View File
@@ -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);
+14
View File
@@ -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
+1
View File
@@ -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 */
+1
View File
@@ -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"),
+1
View File
@@ -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",
+1
View File
@@ -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",
+116
View File
@@ -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 });
+115
View File
@@ -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 });
+93
View File
@@ -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 });
+101
View File
@@ -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 });
+42
View File
@@ -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 });