|
46 | 46 | #include <errno.h> |
47 | 47 | #include <cerrno> |
48 | 48 | #include <cstdio> |
| 49 | +#include <cstring> |
49 | 50 | #include <filesystem> |
50 | 51 |
|
51 | 52 | #if defined(__MINGW32__) || defined(_MSC_VER) |
@@ -77,6 +78,7 @@ using v8::Local; |
77 | 78 | using v8::LocalVector; |
78 | 79 | using v8::Maybe; |
79 | 80 | using v8::MaybeLocal; |
| 81 | +using v8::NewStringType; |
80 | 82 | using v8::Nothing; |
81 | 83 | using v8::Number; |
82 | 84 | using v8::Object; |
@@ -2416,6 +2418,184 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) { |
2416 | 2418 | } |
2417 | 2419 | } |
2418 | 2420 |
|
| 2421 | +// Matches lib/internal/url.js isURL(): duck-type WHATWG URL objects vs |
| 2422 | +// legacy url.parse() results (which have `auth` and `path` properties). |
| 2423 | +static bool IsUrlLike(Environment* env, Local<Value> value) { |
| 2424 | + if (!value->IsObject() || value->IsUint8Array()) { |
| 2425 | + return false; |
| 2426 | + } |
| 2427 | + |
| 2428 | + Isolate* isolate = env->isolate(); |
| 2429 | + Local<Context> context = env->context(); |
| 2430 | + Local<Object> obj = value.As<Object>(); |
| 2431 | + |
| 2432 | + Local<Value> href; |
| 2433 | + if (!obj->Get(context, env->href_string()).ToLocal(&href)) { |
| 2434 | + return false; |
| 2435 | + } |
| 2436 | + if (!href->BooleanValue(isolate)) { |
| 2437 | + return false; |
| 2438 | + } |
| 2439 | + |
| 2440 | + Local<Value> protocol; |
| 2441 | + if (!obj->Get(context, env->protocol_string()).ToLocal(&protocol)) { |
| 2442 | + return false; |
| 2443 | + } |
| 2444 | + if (!protocol->BooleanValue(isolate)) { |
| 2445 | + return false; |
| 2446 | + } |
| 2447 | + |
| 2448 | + Local<Value> auth; |
| 2449 | + if (!obj->Get(context, FIXED_ONE_BYTE_STRING(isolate, "auth")) |
| 2450 | + .ToLocal(&auth)) { |
| 2451 | + return false; |
| 2452 | + } |
| 2453 | + |
| 2454 | + Local<Value> path; |
| 2455 | + if (!obj->Get(context, env->path_string()).ToLocal(&path)) { |
| 2456 | + return false; |
| 2457 | + } |
| 2458 | + |
| 2459 | + return auth->IsUndefined() && path->IsUndefined(); |
| 2460 | +} |
| 2461 | + |
| 2462 | +static bool ContainsNul(const BufferValue& path) { |
| 2463 | + return path.length() > 0 && |
| 2464 | + std::memchr(path.out(), '\0', path.length()) != nullptr; |
| 2465 | +} |
| 2466 | + |
| 2467 | +// C++ equivalent of getValidatedPath(value, propName): accepts string, |
| 2468 | +// Uint8Array/Buffer, or WHATWG URL, rejects embedded NUL bytes, and converts |
| 2469 | +// file: URLs to paths. Returns an empty MaybeLocal and throws on failure. |
| 2470 | +static MaybeLocal<Value> GetValidatedPath(Environment* env, |
| 2471 | + Local<Value> input, |
| 2472 | + const char* prop_name) { |
| 2473 | + Isolate* isolate = env->isolate(); |
| 2474 | + |
| 2475 | + if (input->IsString() || input->IsUint8Array()) { |
| 2476 | + return input; |
| 2477 | + } |
| 2478 | + |
| 2479 | + if (IsUrlLike(env, input)) { |
| 2480 | + Local<Context> context = env->context(); |
| 2481 | + Local<Value> href; |
| 2482 | + if (!input.As<Object>()->Get(context, env->href_string()).ToLocal(&href)) { |
| 2483 | + return MaybeLocal<Value>(); |
| 2484 | + } |
| 2485 | + |
| 2486 | + Utf8Value href_utf8(isolate, href); |
| 2487 | + auto parsed = ada::parse<ada::url_aggregator>(href_utf8.ToStringView()); |
| 2488 | + if (!parsed) { |
| 2489 | + url::ThrowInvalidURL(env, href_utf8.ToStringView(), std::nullopt); |
| 2490 | + return MaybeLocal<Value>(); |
| 2491 | + } |
| 2492 | + |
| 2493 | + // Match lib/internal/url.js fileURLToPath(): non-file schemes throw |
| 2494 | + // ERR_INVALID_URL_SCHEME with the same message as JS (`file`, not `file:`). |
| 2495 | + if (parsed->type != ada::scheme::FILE) { |
| 2496 | + THROW_ERR_INVALID_URL_SCHEME(isolate, "The URL must be of scheme file"); |
| 2497 | + return MaybeLocal<Value>(); |
| 2498 | + } |
| 2499 | + |
| 2500 | + std::optional<std::string> file_path = url::FileURLToPath(env, *parsed); |
| 2501 | + if (!file_path.has_value()) { |
| 2502 | + return MaybeLocal<Value>(); |
| 2503 | + } |
| 2504 | + |
| 2505 | + if (file_path->find('\0') != std::string::npos) { |
| 2506 | + THROW_ERR_INVALID_ARG_VALUE( |
| 2507 | + env, |
| 2508 | + "The argument '%s' must be a string, Uint8Array, or URL " |
| 2509 | + "without null bytes. Received %s", |
| 2510 | + prop_name, |
| 2511 | + DetermineSpecificErrorType(env, input)); |
| 2512 | + return MaybeLocal<Value>(); |
| 2513 | + } |
| 2514 | + |
| 2515 | + Local<String> path_string; |
| 2516 | + if (!String::NewFromUtf8(isolate, |
| 2517 | + file_path->data(), |
| 2518 | + NewStringType::kNormal, |
| 2519 | + static_cast<int>(file_path->size())) |
| 2520 | + .ToLocal(&path_string)) { |
| 2521 | + return MaybeLocal<Value>(); |
| 2522 | + } |
| 2523 | + return path_string; |
| 2524 | + } |
| 2525 | + |
| 2526 | + if (isolate->HasPendingException()) { |
| 2527 | + return MaybeLocal<Value>(); |
| 2528 | + } |
| 2529 | + |
| 2530 | + THROW_ERR_INVALID_ARG_TYPE( |
| 2531 | + env, |
| 2532 | + "The \"%s\" argument must be of type string or an instance of " |
| 2533 | + "Buffer or URL. Received %s", |
| 2534 | + prop_name, |
| 2535 | + DetermineSpecificErrorType(env, input)); |
| 2536 | + return MaybeLocal<Value>(); |
| 2537 | +} |
| 2538 | + |
| 2539 | +// Full C++ implementation of fs.copyFileSync(): path validation, mode |
| 2540 | +// validation, permission checks, and the copy itself. |
| 2541 | +static void CopyFileSync(const FunctionCallbackInfo<Value>& args) { |
| 2542 | + Environment* env = Environment::GetCurrent(args); |
| 2543 | + Isolate* isolate = env->isolate(); |
| 2544 | + |
| 2545 | + CHECK_GE(args.Length(), 2); // src, dest[, mode] |
| 2546 | + |
| 2547 | + Local<Value> src_val; |
| 2548 | + if (!GetValidatedPath(env, args[0], "src").ToLocal(&src_val)) { |
| 2549 | + return; |
| 2550 | + } |
| 2551 | + Local<Value> dest_val; |
| 2552 | + if (!GetValidatedPath(env, args[1], "dest").ToLocal(&dest_val)) { |
| 2553 | + return; |
| 2554 | + } |
| 2555 | + |
| 2556 | + BufferValue src(isolate, src_val); |
| 2557 | + CHECK_NOT_NULL(*src); |
| 2558 | + if (ContainsNul(src)) { |
| 2559 | + THROW_ERR_INVALID_ARG_VALUE( |
| 2560 | + env, |
| 2561 | + "The argument 'src' must be a string, Uint8Array, or URL " |
| 2562 | + "without null bytes. Received %s", |
| 2563 | + DetermineSpecificErrorType(env, args[0])); |
| 2564 | + return; |
| 2565 | + } |
| 2566 | + |
| 2567 | + BufferValue dest(isolate, dest_val); |
| 2568 | + CHECK_NOT_NULL(*dest); |
| 2569 | + if (ContainsNul(dest)) { |
| 2570 | + THROW_ERR_INVALID_ARG_VALUE( |
| 2571 | + env, |
| 2572 | + "The argument 'dest' must be a string, Uint8Array, or URL " |
| 2573 | + "without null bytes. Received %s", |
| 2574 | + DetermineSpecificErrorType(env, args[1])); |
| 2575 | + return; |
| 2576 | + } |
| 2577 | + |
| 2578 | + Local<Value> mode = args.Length() > 2 ? args[2] : Undefined(isolate); |
| 2579 | + int flags; |
| 2580 | + if (!GetValidFileMode(env, mode, UV_FS_COPYFILE).To(&flags)) { |
| 2581 | + return; |
| 2582 | + } |
| 2583 | + |
| 2584 | + ToNamespacedPath(env, &src); |
| 2585 | + ToNamespacedPath(env, &dest); |
| 2586 | + |
| 2587 | + THROW_IF_INSUFFICIENT_PERMISSIONS( |
| 2588 | + env, permission::PermissionScope::kFileSystemRead, src.ToStringView()); |
| 2589 | + THROW_IF_INSUFFICIENT_PERMISSIONS( |
| 2590 | + env, permission::PermissionScope::kFileSystemWrite, dest.ToStringView()); |
| 2591 | + |
| 2592 | + FSReqWrapSync req_wrap_sync("copyfile", *src, *dest); |
| 2593 | + FS_SYNC_TRACE_BEGIN(copyfile); |
| 2594 | + SyncCallAndThrowOnError( |
| 2595 | + env, &req_wrap_sync, uv_fs_copyfile, *src, *dest, flags); |
| 2596 | + FS_SYNC_TRACE_END(copyfile); |
| 2597 | +} |
| 2598 | + |
2419 | 2599 | static void CopyFile(const FunctionCallbackInfo<Value>& args) { |
2420 | 2600 | Environment* env = Environment::GetCurrent(args); |
2421 | 2601 | Isolate* isolate = env->isolate(); |
@@ -4229,6 +4409,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, |
4229 | 4409 | SetMethod(isolate, target, "writeFileUtf8", WriteFileUtf8); |
4230 | 4410 | SetMethod(isolate, target, "realpath", RealPath); |
4231 | 4411 | SetMethod(isolate, target, "copyFile", CopyFile); |
| 4412 | + SetMethod(isolate, target, "copyFileSync", CopyFileSync); |
4232 | 4413 |
|
4233 | 4414 | SetMethod(isolate, target, "chmod", Chmod); |
4234 | 4415 | SetMethod(isolate, target, "fchmod", FChmod); |
@@ -4357,6 +4538,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { |
4357 | 4538 | registry->Register(WriteFileUtf8); |
4358 | 4539 | registry->Register(RealPath); |
4359 | 4540 | registry->Register(CopyFile); |
| 4541 | + registry->Register(CopyFileSync); |
4360 | 4542 |
|
4361 | 4543 | registry->Register(CpSyncCheckPaths); |
4362 | 4544 | registry->Register(CpSyncOverrideFile); |
|
0 commit comments