From fe368b72c1dd2f2d17b7b7e5965b9e3d9ca61c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 7 Mar 2023 05:13:44 -0400 Subject: refactor: Add "deno_fs" extension crate (#18040) This commit factors out APIs related to file system from "runtime/" to a separate "deno_fs" extension crate. --- Cargo.lock | 18 + Cargo.toml | 2 + cli/build.rs | 1 + ext/fs/30_fs.js | 899 +++++++++++++ ext/fs/Cargo.toml | 31 + ext/fs/README.md | 3 + ext/fs/lib.rs | 2281 ++++++++++++++++++++++++++++++++ ext/node/lib.rs | 2 +- ext/node/polyfills/_process/process.ts | 2 +- runtime/Cargo.toml | 2 + runtime/build.rs | 38 +- runtime/js/30_fs.js | 899 ------------- runtime/js/40_process.js | 2 +- runtime/js/90_deno_ns.js | 2 +- runtime/lib.rs | 1 + runtime/ops/fs.rs | 2214 ------------------------------- runtime/ops/mod.rs | 1 - runtime/permissions/mod.rs | 2 +- runtime/web_worker.rs | 2 +- runtime/worker.rs | 2 +- 20 files changed, 3282 insertions(+), 3122 deletions(-) create mode 100644 ext/fs/30_fs.js create mode 100644 ext/fs/Cargo.toml create mode 100644 ext/fs/README.md create mode 100644 ext/fs/lib.rs delete mode 100644 runtime/js/30_fs.js delete mode 100644 runtime/ops/fs.rs diff --git a/Cargo.lock b/Cargo.lock index 8b9dd991f..adf3706b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1115,6 +1115,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "deno_fs" +version = "0.1.0" +dependencies = [ + "deno_core", + "deno_crypto", + "deno_io", + "filetime", + "fs3", + "libc", + "log", + "nix", + "serde", + "tokio", + "winapi", +] + [[package]] name = "deno_graph" version = "0.44.3" @@ -1276,6 +1293,7 @@ dependencies = [ "deno_fetch", "deno_ffi", "deno_flash", + "deno_fs", "deno_http", "deno_io", "deno_napi", diff --git a/Cargo.toml b/Cargo.toml index fdcef9848..1fbdd5c65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "ext/fetch", "ext/flash", "ext/ffi", + "ext/fs", "ext/http", "ext/io", "ext/net", @@ -62,6 +63,7 @@ deno_crypto = { version = "0.105.0", path = "./ext/crypto" } deno_fetch = { version = "0.115.0", path = "./ext/fetch" } deno_ffi = { version = "0.78.0", path = "./ext/ffi" } deno_flash = { version = "0.27.0", path = "./ext/flash" } +deno_fs = { version = "0.1.0", path = "./ext/fs" } deno_http = { version = "0.86.0", path = "./ext/http" } deno_io = { version = "0.1.0", path = "./ext/io" } deno_net = { version = "0.83.0", path = "./ext/net" } diff --git a/cli/build.rs b/cli/build.rs index 3baafb522..9d133d99a 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -343,6 +343,7 @@ fn create_cli_snapshot(snapshot_path: PathBuf) { false, // No --unstable. ), deno_io::init(Default::default()), + deno_fs::init::(false), deno_node::init::(None), // No --unstable. deno_node::init_polyfill(), deno_ffi::init::(false), diff --git a/ext/fs/30_fs.js b/ext/fs/30_fs.js new file mode 100644 index 000000000..19e7f372b --- /dev/null +++ b/ext/fs/30_fs.js @@ -0,0 +1,899 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +const core = globalThis.Deno.core; +const ops = core.ops; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypeFilter, + Date, + DatePrototype, + Error, + Function, + MathTrunc, + ObjectEntries, + ObjectPrototypeIsPrototypeOf, + ObjectValues, + SymbolAsyncIterator, + SymbolIterator, + Uint32Array, +} = primordials; +import { read, readSync, write, writeSync } from "internal:deno_io/12_io.js"; +import * as abortSignal from "internal:deno_web/03_abort_signal.js"; +import { + readableStreamForRid, + ReadableStreamPrototype, + writableStreamForRid, +} from "internal:deno_web/06_streams.js"; +import { pathFromURL } from "internal:deno_web/00_infra.js"; + +function chmodSync(path, mode) { + ops.op_chmod_sync(pathFromURL(path), mode); +} + +async function chmod(path, mode) { + await core.opAsync("op_chmod_async", pathFromURL(path), mode); +} + +function chownSync( + path, + uid, + gid, +) { + ops.op_chown_sync(pathFromURL(path), uid, gid); +} + +async function chown( + path, + uid, + gid, +) { + await core.opAsync( + "op_chown_async", + pathFromURL(path), + uid, + gid, + ); +} + +function copyFileSync( + fromPath, + toPath, +) { + ops.op_copy_file_sync( + pathFromURL(fromPath), + pathFromURL(toPath), + ); +} + +async function copyFile( + fromPath, + toPath, +) { + await core.opAsync( + "op_copy_file_async", + pathFromURL(fromPath), + pathFromURL(toPath), + ); +} + +function cwd() { + return ops.op_cwd(); +} + +function chdir(directory) { + ops.op_chdir(pathFromURL(directory)); +} + +function makeTempDirSync(options = {}) { + return ops.op_make_temp_dir_sync(options); +} + +function makeTempDir(options = {}) { + return core.opAsync("op_make_temp_dir_async", options); +} + +function makeTempFileSync(options = {}) { + return ops.op_make_temp_file_sync(options); +} + +function makeTempFile(options = {}) { + return core.opAsync("op_make_temp_file_async", options); +} + +function mkdirArgs(path, options) { + const args = { path: pathFromURL(path), recursive: false }; + if (options != null) { + if (typeof options.recursive == "boolean") { + args.recursive = options.recursive; + } + if (options.mode) { + args.mode = options.mode; + } + } + return args; +} + +function mkdirSync(path, options) { + ops.op_mkdir_sync(mkdirArgs(path, options)); +} + +async function mkdir( + path, + options, +) { + await core.opAsync("op_mkdir_async", mkdirArgs(path, options)); +} + +function readDirSync(path) { + return ops.op_read_dir_sync(pathFromURL(path))[ + SymbolIterator + ](); +} + +function readDir(path) { + const array = core.opAsync( + "op_read_dir_async", + pathFromURL(path), + ); + return { + async *[SymbolAsyncIterator]() { + yield* await array; + }, + }; +} + +function readLinkSync(path) { + return ops.op_read_link_sync(pathFromURL(path)); +} + +function readLink(path) { + return core.opAsync("op_read_link_async", pathFromURL(path)); +} + +function realPathSync(path) { + return ops.op_realpath_sync(pathFromURL(path)); +} + +function realPath(path) { + return core.opAsync("op_realpath_async", pathFromURL(path)); +} + +function removeSync( + path, + options = {}, +) { + ops.op_remove_sync( + pathFromURL(path), + !!options.recursive, + ); +} + +async function remove( + path, + options = {}, +) { + await core.opAsync( + "op_remove_async", + pathFromURL(path), + !!options.recursive, + ); +} + +function renameSync(oldpath, newpath) { + ops.op_rename_sync( + pathFromURL(oldpath), + pathFromURL(newpath), + ); +} + +async function rename(oldpath, newpath) { + await core.opAsync( + "op_rename_async", + pathFromURL(oldpath), + pathFromURL(newpath), + ); +} + +// Extract the FsStat object from the encoded buffer. +// See `runtime/ops/fs.rs` for the encoder. +// +// This is not a general purpose decoder. There are 4 types: +// +// 1. date +// offset += 4 +// 1/0 | extra padding | high u32 | low u32 +// if date[0] == 1, new Date(u64) else null +// +// 2. bool +// offset += 2 +// 1/0 | extra padding +// +// 3. u64 +// offset += 2 +// high u32 | low u32 +// +// 4. ?u64 converts a zero u64 value to JS null on Windows. +function createByteStruct(types) { + // types can be "date", "bool" or "u64". + // `?` prefix means optional on windows. + let offset = 0; + let str = + 'const unix = Deno.build.os === "darwin" || Deno.build.os === "linux"; return {'; + const typeEntries = ObjectEntries(types); + for (let i = 0; i < typeEntries.length; ++i) { + let { 0: name, 1: type } = typeEntries[i]; + + const optional = type.startsWith("?"); + if (optional) type = type.slice(1); + + if (type == "u64") { + if (!optional) { + str += `${name}: view[${offset}] + view[${offset + 1}] * 2**32,`; + } else { + str += `${name}: (unix ? (view[${offset}] + view[${ + offset + 1 + }] * 2**32) : (view[${offset}] + view[${ + offset + 1 + }] * 2**32) || null),`; + } + } else if (type == "date") { + str += `${name}: view[${offset}] === 0 ? null : new Date(view[${ + offset + 2 + }] + view[${offset + 3}] * 2**32),`; + offset += 2; + } else { + str += `${name}: !!(view[${offset}] + view[${offset + 1}] * 2**32),`; + } + offset += 2; + } + str += "};"; + // ...so you don't like eval huh? don't worry, it only executes during snapshot :) + return [new Function("view", str), new Uint32Array(offset)]; +} + +const { 0: statStruct, 1: statBuf } = createByteStruct({ + isFile: "bool", + isDirectory: "bool", + isSymlink: "bool", + size: "u64", + mtime: "date", + atime: "date", + birthtime: "date", + dev: "?u64", + ino: "?u64", + mode: "?u64", + nlink: "?u64", + uid: "?u64", + gid: "?u64", + rdev: "?u64", + blksize: "?u64", + blocks: "?u64", +}); + +function parseFileInfo(response) { + const unix = core.build.os === "darwin" || core.build.os === "linux"; + return { + isFile: response.isFile, + isDirectory: response.isDirectory, + isSymlink: response.isSymlink, + size: response.size, + mtime: response.mtimeSet !== null ? new Date(response.mtime) : null, + atime: response.atimeSet !== null ? new Date(response.atime) : null, + birthtime: response.birthtimeSet !== null + ? new Date(response.birthtime) + : null, + // Only non-null if on Unix + dev: unix ? response.dev : null, + ino: unix ? response.ino : null, + mode: unix ? response.mode : null, + nlink: unix ? response.nlink : null, + uid: unix ? response.uid : null, + gid: unix ? response.gid : null, + rdev: unix ? response.rdev : null, + blksize: unix ? response.blksize : null, + blocks: unix ? response.blocks : null, + }; +} + +function fstatSync(rid) { + ops.op_fstat_sync(rid, statBuf); + return statStruct(statBuf); +} + +async function fstat(rid) { + return parseFileInfo(await core.opAsync("op_fstat_async", rid)); +} + +async function lstat(path) { + const res = await core.opAsync("op_stat_async", { + path: pathFromURL(path), + lstat: true, + }); + return parseFileInfo(res); +} + +function lstatSync(path) { + ops.op_stat_sync( + pathFromURL(path), + true, + statBuf, + ); + return statStruct(statBuf); +} + +async function stat(path) { + const res = await core.opAsync("op_stat_async", { + path: pathFromURL(path), + lstat: false, + }); + return parseFileInfo(res); +} + +function statSync(path) { + ops.op_stat_sync( + pathFromURL(path), + false, + statBuf, + ); + return statStruct(statBuf); +} + +function coerceLen(len) { + if (len == null || len < 0) { + return 0; + } + + return len; +} + +function ftruncateSync(rid, len) { + ops.op_ftruncate_sync(rid, coerceLen(len)); +} + +async function ftruncate(rid, len) { + await core.opAsync("op_ftruncate_async", rid, coerceLen(len)); +} + +function truncateSync(path, len) { + ops.op_truncate_sync(path, coerceLen(len)); +} + +async function truncate(path, len) { + await core.opAsync("op_truncate_async", path, coerceLen(len)); +} + +function umask(mask) { + return ops.op_umask(mask); +} + +function linkSync(oldpath, newpath) { + ops.op_link_sync(oldpath, newpath); +} + +async function link(oldpath, newpath) { + await core.opAsync("op_link_async", oldpath, newpath); +} + +function toUnixTimeFromEpoch(value) { + if (ObjectPrototypeIsPrototypeOf(DatePrototype, value)) { + const time = value.valueOf(); + const seconds = MathTrunc(time / 1e3); + const nanoseconds = MathTrunc(time - (seconds * 1e3)) * 1e6; + + return [ + seconds, + nanoseconds, + ]; + } + + const seconds = value; + const nanoseconds = 0; + + return [ + seconds, + nanoseconds, + ]; +} + +function futimeSync( + rid, + atime, + mtime, +) { + const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); + const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); + ops.op_futime_sync(rid, atimeSec, atimeNsec, mtimeSec, mtimeNsec); +} + +async function futime( + rid, + atime, + mtime, +) { + const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); + const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); + await core.opAsync( + "op_futime_async", + rid, + atimeSec, + atimeNsec, + mtimeSec, + mtimeNsec, + ); +} + +function utimeSync( + path, + atime, + mtime, +) { + const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); + const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); + ops.op_utime_sync( + pathFromURL(path), + atimeSec, + atimeNsec, + mtimeSec, + mtimeNsec, + ); +} + +async function utime( + path, + atime, + mtime, +) { + const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); + const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); + await core.opAsync( + "op_utime_async", + pathFromURL(path), + atimeSec, + atimeNsec, + mtimeSec, + mtimeNsec, + ); +} + +function symlinkSync( + oldpath, + newpath, + options, +) { + ops.op_symlink_sync( + pathFromURL(oldpath), + pathFromURL(newpath), + options?.type, + ); +} + +async function symlink( + oldpath, + newpath, + options, +) { + await core.opAsync( + "op_symlink_async", + pathFromURL(oldpath), + pathFromURL(newpath), + options?.type, + ); +} + +function fdatasyncSync(rid) { + ops.op_fdatasync_sync(rid); +} + +async function fdatasync(rid) { + await core.opAsync("op_fdatasync_async", rid); +} + +function fsyncSync(rid) { + ops.op_fsync_sync(rid); +} + +async function fsync(rid) { + await core.opAsync("op_fsync_async", rid); +} + +function flockSync(rid, exclusive) { + ops.op_flock_sync(rid, exclusive === true); +} + +async function flock(rid, exclusive) { + await core.opAsync("op_flock_async", rid, exclusive === true); +} + +function funlockSync(rid) { + ops.op_funlock_sync(rid); +} + +async function funlock(rid) { + await core.opAsync("op_funlock_async", rid); +} + +function seekSync( + rid, + offset, + whence, +) { + return ops.op_seek_sync({ rid, offset, whence }); +} + +function seek( + rid, + offset, + whence, +) { + return core.opAsync("op_seek_async", { rid, offset, whence }); +} + +function openSync( + path, + options, +) { + if (options) checkOpenOptions(options); + const mode = options?.mode; + const rid = ops.op_open_sync( + pathFromURL(path), + options, + mode, + ); + + return new FsFile(rid); +} + +async function open( + path, + options, +) { + if (options) checkOpenOptions(options); + const mode = options?.mode; + const rid = await core.opAsync( + "op_open_async", + pathFromURL(path), + options, + mode, + ); + + return new FsFile(rid); +} + +function createSync(path) { + return openSync(path, { + read: true, + write: true, + truncate: true, + create: true, + }); +} + +function create(path) { + return open(path, { + read: true, + write: true, + truncate: true, + create: true, + }); +} + +class FsFile { + #rid = 0; + + #readable; + #writable; + + constructor(rid) { + this.#rid = rid; + } + + get rid() { + return this.#rid; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + truncate(len) { + return ftruncate(this.rid, len); + } + + truncateSync(len) { + return ftruncateSync(this.rid, len); + } + + read(p) { + return read(this.rid, p); + } + + readSync(p) { + return readSync(this.rid, p); + } + + seek(offset, whence) { + return seek(this.rid, offset, whence); + } + + seekSync(offset, whence) { + return seekSync(this.rid, offset, whence); + } + + stat() { + return fstat(this.rid); + } + + statSync() { + return fstatSync(this.rid); + } + + close() { + core.close(this.rid); + } + + get readable() { + if (this.#readable === undefined) { + this.#readable = readableStreamForRid(this.rid); + } + return this.#readable; + } + + get writable() { + if (this.#writable === undefined) { + this.#writable = writableStreamForRid(this.rid); + } + return this.#writable; + } +} + +function checkOpenOptions(options) { + if ( + ArrayPrototypeFilter( + ObjectValues(options), + (val) => val === true, + ).length === 0 + ) { + throw new Error("OpenOptions requires at least one option to be true"); + } + + if (options.truncate && !options.write) { + throw new Error("'truncate' option requires 'write' option"); + } + + const createOrCreateNewWithoutWriteOrAppend = + (options.create || options.createNew) && + !(options.write || options.append); + + if (createOrCreateNewWithoutWriteOrAppend) { + throw new Error( + "'create' or 'createNew' options require 'write' or 'append' option", + ); + } +} + +const File = FsFile; + +function readFileSync(path) { + return ops.op_readfile_sync(pathFromURL(path)); +} + +async function readFile(path, options) { + let cancelRid; + let abortHandler; + if (options?.signal) { + options.signal.throwIfAborted(); + cancelRid = ops.op_cancel_handle(); + abortHandler = () => core.tryClose(cancelRid); + options.signal[abortSignal.add](abortHandler); + } + + try { + const read = await core.opAsync( + "op_readfile_async", + pathFromURL(path), + cancelRid, + ); + return read; + } finally { + if (options?.signal) { + options.signal[abortSignal.remove](abortHandler); + + // always throw the abort error when aborted + options.signal.throwIfAborted(); + } + } +} + +function readTextFileSync(path) { + return ops.op_readfile_text_sync(pathFromURL(path)); +} + +async function readTextFile(path, options) { + let cancelRid; + let abortHandler; + if (options?.signal) { + options.signal.throwIfAborted(); + cancelRid = ops.op_cancel_handle(); + abortHandler = () => core.tryClose(cancelRid); + options.signal[abortSignal.add](abortHandler); + } + + try { + const read = await core.opAsync( + "op_readfile_text_async", + pathFromURL(path), + cancelRid, + ); + return read; + } finally { + if (options?.signal) { + options.signal[abortSignal.remove](abortHandler); + + // always throw the abort error when aborted + options.signal.throwIfAborted(); + } + } +} + +function writeFileSync( + path, + data, + options = {}, +) { + options.signal?.throwIfAborted(); + ops.op_write_file_sync( + pathFromURL(path), + options.mode, + options.append ?? false, + options.create ?? true, + options.createNew ?? false, + data, + ); +} + +async function writeFile( + path, + data, + options = {}, +) { + let cancelRid; + let abortHandler; + if (options.signal) { + options.signal.throwIfAborted(); + cancelRid = ops.op_cancel_handle(); + abortHandler = () => core.tryClose(cancelRid); + options.signal[abortSignal.add](abortHandler); + } + try { + if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) { + const file = await open(path, { + mode: options.mode, + append: options.append ?? false, + create: options.create ?? true, + createNew: options.createNew ?? false, + write: true, + }); + await data.pipeTo(file.writable, { + signal: options.signal, + }); + } else { + await core.opAsync( + "op_write_file_async", + pathFromURL(path), + options.mode, + options.append ?? false, + options.create ?? true, + options.createNew ?? false, + data, + cancelRid, + ); + } + } finally { + if (options.signal) { + options.signal[abortSignal.remove](abortHandler); + + // always throw the abort error when aborted + options.signal.throwIfAborted(); + } + } +} + +function writeTextFileSync( + path, + data, + options = {}, +) { + const encoder = new TextEncoder(); + return writeFileSync(path, encoder.encode(data), options); +} + +function writeTextFile( + path, + data, + options = {}, +) { + if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) { + return writeFile( + path, + data.pipeThrough(new TextEncoderStream()), + options, + ); + } else { + const encoder = new TextEncoder(); + return writeFile(path, encoder.encode(data), options); + } +} + +export { + chdir, + chmod, + chmodSync, + chown, + chownSync, + copyFile, + copyFileSync, + create, + createSync, + cwd, + fdatasync, + fdatasyncSync, + File, + flock, + flockSync, + FsFile, + fstat, + fstatSync, + fsync, + fsyncSync, + ftruncate, + ftruncateSync, + funlock, + funlockSync, + futime, + futimeSync, + link, + linkSync, + lstat, + lstatSync, + makeTempDir, + makeTempDirSync, + makeTempFile, + makeTempFileSync, + mkdir, + mkdirSync, + open, + openSync, + readDir, + readDirSync, + readFile, + readFileSync, + readLink, + readLinkSync, + readTextFile, + readTextFileSync, + realPath, + realPathSync, + remove, + removeSync, + rename, + renameSync, + seek, + seekSync, + stat, + statSync, + symlink, + symlinkSync, + truncate, + truncateSync, + umask, + utime, + utimeSync, + writeFile, + writeFileSync, + writeTextFile, + writeTextFileSync, +}; diff --git a/ext/fs/Cargo.toml b/ext/fs/Cargo.toml new file mode 100644 index 000000000..279788efa --- /dev/null +++ b/ext/fs/Cargo.toml @@ -0,0 +1,31 @@ +# Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_fs" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true +description = "Ops for interacting with the file system" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core.workspace = true +deno_crypto.workspace = true +deno_io.workspace = true +filetime = "0.2.16" +fs3 = "0.5.0" +libc.workspace = true +log.workspace = true +serde.workspace = true +tokio.workspace = true + +[target.'cfg(unix)'.dependencies] +nix.workspace = true + +[target.'cfg(windows)'.dependencies] +winapi = { workspace = true, features = ["winbase"] } diff --git a/ext/fs/README.md b/ext/fs/README.md new file mode 100644 index 000000000..1fe7c1843 --- /dev/null +++ b/ext/fs/README.md @@ -0,0 +1,3 @@ +# deno_fs + +This crate provides ops for interacting with the file system. diff --git a/ext/fs/lib.rs b/ext/fs/lib.rs new file mode 100644 index 000000000..7551408a9 --- /dev/null +++ b/ext/fs/lib.rs @@ -0,0 +1,2281 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Some deserializer fields are only used on Unix and Windows build fails without it + +use deno_core::error::custom_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::include_js_files; +use deno_core::op; +use deno_core::CancelFuture; +use deno_core::CancelHandle; +use deno_core::Extension; +use deno_core::OpState; +use deno_core::ResourceId; +use deno_core::ZeroCopyBuf; +use deno_crypto::rand::thread_rng; +use deno_crypto::rand::Rng; +use deno_io::StdFileResource; +use log::debug; +use serde::Deserialize; +use serde::Serialize; +use std::borrow::Cow; +use std::cell::RefCell; +use std::convert::From; +use std::env::current_dir; +use std::env::set_current_dir; +use std::env::temp_dir; +use std::io; +use std::io::Error; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +/// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. +fn canonicalize_path(path: &Path) -> Result { + let mut canonicalized_path = path.canonicalize()?; + if cfg!(windows) { + canonicalized_path = PathBuf::from( + canonicalized_path + .display() + .to_string() + .trim_start_matches("\\\\?\\"), + ); + } + Ok(canonicalized_path) +} + +/// A utility function to map OsStrings to Strings +fn into_string(s: std::ffi::OsString) -> Result { + s.into_string().map_err(|s| { + let message = format!("File name or path {s:?} is not valid UTF-8"); + custom_error("InvalidData", message) + }) +} + +#[cfg(unix)] +pub fn get_nix_error_class(error: &nix::Error) -> &'static str { + match error { + nix::Error::ECHILD => "NotFound", + nix::Error::EINVAL => "TypeError", + nix::Error::ENOENT => "NotFound", + nix::Error::ENOTTY => "BadResource", + nix::Error::EPERM => "PermissionDenied", + nix::Error::ESRCH => "NotFound", + nix::Error::UnknownErrno => "Error", + &nix::Error::ENOTSUP => unreachable!(), + _ => "Error", + } +} + +struct UnstableChecker { + pub unstable: bool, +} + +impl UnstableChecker { + // NOTE(bartlomieju): keep in sync with `cli/program_state.rs` + pub fn check_unstable(&self, api_name: &str) { + if !self.unstable { + eprintln!( + "Unstable API '{api_name}'. The --unstable flag must be provided." + ); + std::process::exit(70); + } + } +} + +/// Helper for checking unstable features. Used for sync ops. +fn check_unstable(state: &OpState, api_name: &str) { + state.borrow::().check_unstable(api_name) +} + +/// Helper for checking unstable features. Used for async ops. +fn check_unstable2(state: &Rc>, api_name: &str) { + let state = state.borrow(); + state.borrow::().check_unstable(api_name) +} + +pub trait FsPermissions { + fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; + fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>; + fn check_read_blind( + &mut self, + p: &Path, + display: &str, + api_name: &str, + ) -> Result<(), AnyError>; + fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; + fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError>; +} + +#[cfg(not(unix))] +use deno_core::error::generic_error; +#[cfg(not(unix))] +use deno_core::error::not_supported; + +pub fn init(unstable: bool) -> Extension { + Extension::builder("deno_fs") + .esm(include_js_files!("30_fs.js",)) + .state(move |state| { + state.put(UnstableChecker { unstable }); + Ok(()) + }) + .ops(vec![ + op_open_sync::decl::

(), + op_open_async::decl::

(), + op_write_file_sync::decl::

(), + op_write_file_async::decl::

(), + op_seek_sync::decl(), + op_seek_async::decl(), + op_fdatasync_sync::decl(), + op_fdatasync_async::decl(), + op_fsync_sync::decl(), + op_fsync_async::decl(), + op_fstat_sync::decl(), + op_fstat_async::decl(), + op_flock_sync::decl(), + op_flock_async::decl(), + op_funlock_sync::decl(), + op_funlock_async::decl(), + op_umask::decl(), + op_chdir::decl::

(), + op_mkdir_sync::decl::

(), + op_mkdir_async::decl::

(), + op_chmod_sync::decl::

(), + op_chmod_async::decl::

(), + op_chown_sync::decl::

(), + op_chown_async::decl::

(), + op_remove_sync::decl::

(), + op_remove_async::decl::

(), + op_copy_file_sync::decl::

(), + op_copy_file_async::decl::

(), + op_stat_sync::decl::

(), + op_stat_async::decl::

(), + op_realpath_sync::decl::

(), + op_realpath_async::decl::

(), + op_read_dir_sync::decl::

(), + op_read_dir_async::decl::

(), + op_rename_sync::decl::

(), + op_rename_async::decl::

(), + op_link_sync::decl::

(), + op_link_async::decl::

(), + op_symlink_sync::decl::

(), + op_symlink_async::decl::

(), + op_read_link_sync::decl::

(), + op_read_link_async::decl::

(), + op_ftruncate_sync::decl(), + op_ftruncate_async::decl(), + op_truncate_sync::decl::

(), + op_truncate_async::decl::

(), + op_make_temp_dir_sync::decl::

(), + op_make_temp_dir_async::decl::

(), + op_make_temp_file_sync::decl::

(), + op_make_temp_file_async::decl::

(), + op_cwd::decl::

(), + op_futime_sync::decl(), + op_futime_async::decl(), + op_utime_sync::decl::

(), + op_utime_async::decl::

(), + op_readfile_sync::decl::

(), + op_readfile_text_sync::decl::

(), + op_readfile_async::decl::

(), + op_readfile_text_async::decl::

(), + ]) + .build() +} + +fn default_err_mapper(err: Error, desc: String) -> Error { + Error::new(err.kind(), format!("{err}, {desc}")) +} + +#[derive(Deserialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct OpenOptions { + read: bool, + write: bool, + create: bool, + truncate: bool, + append: bool, + create_new: bool, +} + +#[inline] +fn open_helper

( + state: &mut OpState, + path: &str, + mode: Option, + options: Option<&OpenOptions>, + api_name: &str, +) -> Result<(PathBuf, std::fs::OpenOptions), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(path).to_path_buf(); + + let mut open_options = std::fs::OpenOptions::new(); + + if let Some(mode) = mode { + // mode only used if creating the file on Unix + // if not specified, defaults to 0o666 + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + open_options.mode(mode & 0o777); + } + #[cfg(not(unix))] + let _ = mode; // avoid unused warning + } + + let permissions = state.borrow_mut::

(); + + match options { + None => { + permissions.check_read(&path, api_name)?; + open_options + .read(true) + .create(false) + .write(false) + .truncate(false) + .append(false) + .create_new(false); + } + Some(options) => { + if options.read { + permissions.check_read(&path, api_name)?; + } + + if options.write || options.append { + permissions.check_write(&path, api_name)?; + } + + open_options + .read(options.read) + .create(options.create) + .write(options.write) + .truncate(options.truncate) + .append(options.append) + .create_new(options.create_new); + } + } + + Ok((path, open_options)) +} + +#[op] +fn op_open_sync

( + state: &mut OpState, + path: String, + options: Option, + mode: Option, +) -> Result +where + P: FsPermissions + 'static, +{ + let (path, open_options) = + open_helper::

(state, &path, mode, options.as_ref(), "Deno.openSync()")?; + let std_file = open_options.open(&path).map_err(|err| { + default_err_mapper(err, format!("open '{}'", path.display())) + })?; + let resource = StdFileResource::fs_file(std_file); + let rid = state.resource_table.add(resource); + Ok(rid) +} + +#[op] +async fn op_open_async

( + state: Rc>, + path: String, + options: Option, + mode: Option, +) -> Result +where + P: FsPermissions + 'static, +{ + let (path, open_options) = open_helper::

( + &mut state.borrow_mut(), + &path, + mode, + options.as_ref(), + "Deno.open()", + )?; + let std_file = tokio::task::spawn_blocking(move || { + open_options.open(&path).map_err(|err| { + default_err_mapper(err, format!("open '{}'", path.display())) + }) + }) + .await?; + let resource = StdFileResource::fs_file(std_file?); + let rid = state.borrow_mut().resource_table.add(resource); + Ok(rid) +} + +#[inline] +fn write_open_options( + create: bool, + append: bool, + create_new: bool, +) -> OpenOptions { + OpenOptions { + read: false, + write: true, + create, + truncate: !append, + append, + create_new, + } +} + +#[op] +fn op_write_file_sync

( + state: &mut OpState, + path: String, + mode: Option, + append: bool, + create: bool, + create_new: bool, + data: ZeroCopyBuf, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let (path, open_options) = open_helper::

( + state, + &path, + mode, + Some(&write_open_options(create, append, create_new)), + "Deno.writeFileSync()", + )?; + write_file(&path, open_options, mode, data) +} + +#[op] +async fn op_write_file_async

( + state: Rc>, + path: String, + mode: Option, + append: bool, + create: bool, + create_new: bool, + data: ZeroCopyBuf, + cancel_rid: Option, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let (path, open_options) = open_helper::

( + &mut state.borrow_mut(), + &path, + mode, + Some(&write_open_options(create, append, create_new)), + "Deno.writeFile()", + )?; + + let write_future = tokio::task::spawn_blocking(move || { + write_file(&path, open_options, mode, data) + }); + + let cancel_handle = cancel_rid.and_then(|rid| { + state + .borrow_mut() + .resource_table + .get::(rid) + .ok() + }); + + if let Some(cancel_handle) = cancel_handle { + let write_future_rv = write_future.or_cancel(cancel_handle).await; + + if let Some(cancel_rid) = cancel_rid { + state.borrow_mut().resource_table.close(cancel_rid).ok(); + }; + + return write_future_rv??; + } + + write_future.await? +} + +fn write_file( + path: &Path, + open_options: std::fs::OpenOptions, + _mode: Option, + data: ZeroCopyBuf, +) -> Result<(), AnyError> { + let mut std_file = open_options.open(path).map_err(|err| { + default_err_mapper(err, format!("open '{}'", path.display())) + })?; + + // need to chmod the file if it already exists and a mode is specified + #[cfg(unix)] + if let Some(mode) = _mode { + use std::os::unix::fs::PermissionsExt; + let permissions = PermissionsExt::from_mode(mode & 0o777); + std_file.set_permissions(permissions).map_err(|err| { + default_err_mapper(err, format!("chmod '{}'", path.display())) + })?; + } + + std_file.write_all(&data)?; + Ok(()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SeekArgs { + rid: ResourceId, + offset: i64, + whence: i32, +} + +fn seek_helper(args: SeekArgs) -> Result<(u32, SeekFrom), AnyError> { + let rid = args.rid; + let offset = args.offset; + let whence = args.whence as u32; + // Translate seek mode to Rust repr. + let seek_from = match whence { + 0 => SeekFrom::Start(offset as u64), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => { + return Err(type_error(format!("Invalid seek mode: {whence}"))); + } + }; + + Ok((rid, seek_from)) +} + +#[op] +fn op_seek_sync(state: &mut OpState, args: SeekArgs) -> Result { + let (rid, seek_from) = seek_helper(args)?; + StdFileResource::with_file(state, rid, |std_file| { + std_file.seek(seek_from).map_err(AnyError::from) + }) +} + +#[op] +async fn op_seek_async( + state: Rc>, + args: SeekArgs, +) -> Result { + let (rid, seek_from) = seek_helper(args)?; + + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + std_file.seek(seek_from).map_err(AnyError::from) + }) + .await +} + +#[op] +fn op_fdatasync_sync( + state: &mut OpState, + rid: ResourceId, +) -> Result<(), AnyError> { + StdFileResource::with_file(state, rid, |std_file| { + std_file.sync_data().map_err(AnyError::from) + }) +} + +#[op] +async fn op_fdatasync_async( + state: Rc>, + rid: ResourceId, +) -> Result<(), AnyError> { + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + std_file.sync_data().map_err(AnyError::from) + }) + .await +} + +#[op] +fn op_fsync_sync(state: &mut OpState, rid: ResourceId) -> Result<(), AnyError> { + StdFileResource::with_file(state, rid, |std_file| { + std_file.sync_all().map_err(AnyError::from) + }) +} + +#[op] +async fn op_fsync_async( + state: Rc>, + rid: ResourceId, +) -> Result<(), AnyError> { + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + std_file.sync_all().map_err(AnyError::from) + }) + .await +} + +#[op] +fn op_fstat_sync( + state: &mut OpState, + rid: ResourceId, + out_buf: &mut [u32], +) -> Result<(), AnyError> { + let metadata = StdFileResource::with_file(state, rid, |std_file| { + std_file.metadata().map_err(AnyError::from) + })?; + let stat = get_stat(metadata); + stat.write(out_buf); + Ok(()) +} + +#[op] +async fn op_fstat_async( + state: Rc>, + rid: ResourceId, +) -> Result { + let metadata = + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + std_file.metadata().map_err(AnyError::from) + }) + .await?; + Ok(get_stat(metadata)) +} + +#[op] +fn op_flock_sync( + state: &mut OpState, + rid: ResourceId, + exclusive: bool, +) -> Result<(), AnyError> { + use fs3::FileExt; + check_unstable(state, "Deno.flockSync"); + + StdFileResource::with_file(state, rid, |std_file| { + if exclusive { + std_file.lock_exclusive()?; + } else { + std_file.lock_shared()?; + } + Ok(()) + }) +} + +#[op] +async fn op_flock_async( + state: Rc>, + rid: ResourceId, + exclusive: bool, +) -> Result<(), AnyError> { + use fs3::FileExt; + check_unstable2(&state, "Deno.flock"); + + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + if exclusive { + std_file.lock_exclusive()?; + } else { + std_file.lock_shared()?; + } + Ok(()) + }) + .await +} + +#[op] +fn op_funlock_sync( + state: &mut OpState, + rid: ResourceId, +) -> Result<(), AnyError> { + use fs3::FileExt; + check_unstable(state, "Deno.funlockSync"); + + StdFileResource::with_file(state, rid, |std_file| { + std_file.unlock()?; + Ok(()) + }) +} + +#[op] +async fn op_funlock_async( + state: Rc>, + rid: ResourceId, +) -> Result<(), AnyError> { + use fs3::FileExt; + check_unstable2(&state, "Deno.funlock"); + + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + std_file.unlock()?; + Ok(()) + }) + .await +} + +#[op] +fn op_umask(state: &mut OpState, mask: Option) -> Result { + check_unstable(state, "Deno.umask"); + // TODO implement umask for Windows + // see https://github.com/nodejs/node/blob/master/src/node_process_methods.cc + // and https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/reference/umask?view=vs-2019 + #[cfg(not(unix))] + { + let _ = mask; // avoid unused warning. + Err(not_supported()) + } + #[cfg(unix)] + { + use nix::sys::stat::mode_t; + use nix::sys::stat::umask; + use nix::sys::stat::Mode; + let r = if let Some(mask) = mask { + // If mask provided, return previous. + umask(Mode::from_bits_truncate(mask as mode_t)) + } else { + // If no mask provided, we query the current. Requires two syscalls. + let prev = umask(Mode::from_bits_truncate(0o777)); + let _ = umask(prev); + prev + }; + #[cfg(target_os = "linux")] + { + Ok(r.bits()) + } + #[cfg(target_os = "macos")] + { + Ok(r.bits() as u32) + } + } +} + +#[op] +fn op_chdir

(state: &mut OpState, directory: &str) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let d = PathBuf::from(&directory); + state.borrow_mut::

().check_read(&d, "Deno.chdir()")?; + set_current_dir(&d) + .map_err(|err| default_err_mapper(err, format!("chdir '{directory}'")))?; + Ok(()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MkdirArgs { + path: String, + recursive: bool, + mode: Option, +} + +#[op] +fn op_mkdir_sync

( + state: &mut OpState, + args: MkdirArgs, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(&args.path).to_path_buf(); + let mode = args.mode.unwrap_or(0o777) & 0o777; + state + .borrow_mut::

() + .check_write(&path, "Deno.mkdirSync()")?; + debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(args.recursive); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(mode); + } + builder.create(&path).map_err(|err| { + default_err_mapper(err, format!("mkdir '{}'", path.display())) + })?; + Ok(()) +} + +#[op] +async fn op_mkdir_async

( + state: Rc>, + args: MkdirArgs, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(&args.path).to_path_buf(); + let mode = args.mode.unwrap_or(0o777) & 0o777; + + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_write(&path, "Deno.mkdir()")?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(args.recursive); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(mode); + } + builder.create(&path).map_err(|err| { + default_err_mapper(err, format!("mkdir '{}'", path.display())) + })?; + Ok(()) + }) + .await + .unwrap() +} + +#[op] +fn op_chmod_sync

( + state: &mut OpState, + path: &str, + mode: u32, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(path); + let mode = mode & 0o777; + + state + .borrow_mut::

() + .check_write(path, "Deno.chmodSync()")?; + raw_chmod(path, mode) +} + +#[op] +async fn op_chmod_async

( + state: Rc>, + path: String, + mode: u32, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(&path).to_path_buf(); + let mode = mode & 0o777; + + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_write(&path, "Deno.chmod()")?; + } + + tokio::task::spawn_blocking(move || raw_chmod(&path, mode)) + .await + .unwrap() +} + +fn raw_chmod(path: &Path, _raw_mode: u32) -> Result<(), AnyError> { + let err_mapper = + |err| default_err_mapper(err, format!("chmod '{}'", path.display())); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let permissions = PermissionsExt::from_mode(_raw_mode); + std::fs::set_permissions(path, permissions).map_err(err_mapper)?; + Ok(()) + } + // TODO Implement chmod for Windows (#4357) + #[cfg(not(unix))] + { + // Still check file/dir exists on Windows + let _metadata = std::fs::metadata(path).map_err(err_mapper)?; + Err(not_supported()) + } +} + +#[op] +fn op_chown_sync

( + state: &mut OpState, + path: &str, + #[cfg_attr(windows, allow(unused_variables))] uid: Option, + #[cfg_attr(windows, allow(unused_variables))] gid: Option, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(path).to_path_buf(); + state + .borrow_mut::

() + .check_write(&path, "Deno.chownSync()")?; + #[cfg(unix)] + { + use nix::unistd::chown; + use nix::unistd::Gid; + use nix::unistd::Uid; + let nix_uid = uid.map(Uid::from_raw); + let nix_gid = gid.map(Gid::from_raw); + chown(&path, nix_uid, nix_gid).map_err(|err| { + custom_error( + get_nix_error_class(&err), + format!("{}, chown '{}'", err.desc(), path.display()), + ) + })?; + Ok(()) + } + // TODO Implement chown for Windows + #[cfg(not(unix))] + { + Err(generic_error("Not implemented")) + } +} + +#[op] +async fn op_chown_async

( + state: Rc>, + path: String, + #[cfg_attr(windows, allow(unused_variables))] uid: Option, + #[cfg_attr(windows, allow(unused_variables))] gid: Option, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = Path::new(&path).to_path_buf(); + + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_write(&path, "Deno.chown()")?; + } + + tokio::task::spawn_blocking(move || { + #[cfg(unix)] + { + use nix::unistd::chown; + use nix::unistd::Gid; + use nix::unistd::Uid; + let nix_uid = uid.map(Uid::from_raw); + let nix_gid = gid.map(Gid::from_raw); + chown(&path, nix_uid, nix_gid).map_err(|err| { + custom_error( + get_nix_error_class(&err), + format!("{}, chown '{}'", err.desc(), path.display()), + ) + })?; + Ok(()) + } + // TODO Implement chown for Windows + #[cfg(not(unix))] + Err(not_supported()) + }) + .await + .unwrap() +} + +#[op] +fn op_remove_sync

( + state: &mut OpState, + path: &str, + recursive: bool, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(path); + + state + .borrow_mut::

() + .check_write(&path, "Deno.removeSync()")?; + + #[cfg(not(unix))] + use std::os::windows::prelude::MetadataExt; + + let err_mapper = + |err| default_err_mapper(err, format!("remove '{}'", path.display())); + let metadata = std::fs::symlink_metadata(&path).map_err(err_mapper)?; + + let file_type = metadata.file_type(); + if file_type.is_file() { + std::fs::remove_file(&path).map_err(err_mapper)?; + } else if recursive { + std::fs::remove_dir_all(&path).map_err(err_mapper)?; + } else if file_type.is_symlink() { + #[cfg(unix)] + std::fs::remove_file(&path).map_err(err_mapper)?; + #[cfg(not(unix))] + { + use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + std::fs::remove_dir(&path).map_err(err_mapper)?; + } else { + std::fs::remove_file(&path).map_err(err_mapper)?; + } + } + } else if file_type.is_dir() { + std::fs::remove_dir(&path).map_err(err_mapper)?; + } else { + // pipes, sockets, etc... + std::fs::remove_file(&path).map_err(err_mapper)?; + } + Ok(()) +} + +#[op] +async fn op_remove_async

( + state: Rc>, + path: String, + recursive: bool, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + + { + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_write(&path, "Deno.remove()")?; + } + + tokio::task::spawn_blocking(move || { + #[cfg(not(unix))] + use std::os::windows::prelude::MetadataExt; + let err_mapper = + |err| default_err_mapper(err, format!("remove '{}'", path.display())); + let metadata = std::fs::symlink_metadata(&path).map_err(err_mapper)?; + + debug!("op_remove_async {} {}", path.display(), recursive); + let file_type = metadata.file_type(); + if file_type.is_file() { + std::fs::remove_file(&path).map_err(err_mapper)?; + } else if recursive { + std::fs::remove_dir_all(&path).map_err(err_mapper)?; + } else if file_type.is_symlink() { + #[cfg(unix)] + std::fs::remove_file(&path).map_err(err_mapper)?; + #[cfg(not(unix))] + { + use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + std::fs::remove_dir(&path).map_err(err_mapper)?; + } else { + std::fs::remove_file(&path).map_err(err_mapper)?; + } + } + } else if file_type.is_dir() { + std::fs::remove_dir(&path).map_err(err_mapper)?; + } else { + // pipes, sockets, etc... + std::fs::remove_file(&path).map_err(err_mapper)?; + } + Ok(()) + }) + .await + .unwrap() +} + +#[op] +fn op_copy_file_sync

( + state: &mut OpState, + from: &str, + to: &str, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let from_path = PathBuf::from(from); + let to_path = PathBuf::from(to); + + let permissions = state.borrow_mut::

(); + permissions.check_read(&from_path, "Deno.copyFileSync()")?; + permissions.check_write(&to_path, "Deno.copyFileSync()")?; + + // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput + // See https://github.com/rust-lang/rust/issues/54800 + // Once the issue is resolved, we should remove this workaround. + if cfg!(unix) && !from_path.is_file() { + return Err(custom_error( + "NotFound", + format!( + "File not found, copy '{}' -> '{}'", + from_path.display(), + to_path.display() + ), + )); + } + + let err_mapper = |err| { + default_err_mapper( + err, + format!("copy '{}' -> '{}'", from_path.display(), to_path.display()), + ) + }; + + #[cfg(target_os = "macos")] + { + use libc::clonefile; + use libc::stat; + use libc::unlink; + use std::ffi::CString; + use std::io::Read; + use std::os::unix::fs::OpenOptionsExt; + use std::os::unix::fs::PermissionsExt; + + let from = CString::new(from).unwrap(); + let to = CString::new(to).unwrap(); + + // SAFETY: `from` and `to` are valid C strings. + // std::fs::copy does open() + fcopyfile() on macOS. We try to use + // clonefile() instead, which is more efficient. + unsafe { + let mut st = std::mem::zeroed(); + let ret = stat(from.as_ptr(), &mut st); + if ret != 0 { + return Err(err_mapper(Error::last_os_error()).into()); + } + + if st.st_size > 128 * 1024 { + // Try unlink. If it fails, we are going to try clonefile() anyway. + let _ = unlink(to.as_ptr()); + // Matches rust stdlib behavior for io::copy. + // https://github.com/rust-lang/rust/blob/3fdd578d72a24d4efc2fe2ad18eec3b6ba72271e/library/std/src/sys/unix/fs.rs#L1613-L1616 + if clonefile(from.as_ptr(), to.as_ptr(), 0) == 0 { + return Ok(()); + } + } else { + // Do a regular copy. fcopyfile() is an overkill for < 128KB + // files. + let mut buf = [0u8; 128 * 1024]; + let mut from_file = + std::fs::File::open(&from_path).map_err(err_mapper)?; + let perm = from_file.metadata().map_err(err_mapper)?.permissions(); + + let mut to_file = std::fs::OpenOptions::new() + // create the file with the correct mode right away + .mode(perm.mode()) + .write(true) + .create(true) + .truncate(true) + .open(&to_path) + .map_err(err_mapper)?; + let writer_metadata = to_file.metadata()?; + if writer_metadata.is_file() { + // Set the correct file permissions, in case the file already existed. + // Don't set the permissions on already existing non-files like + // pipes/FIFOs or device nodes. + to_file.set_permissions(perm)?; + } + loop { + let nread = from_file.read(&mut buf).map_err(err_mapper)?; + if nread == 0 { + break; + } + to_file.write_all(&buf[..nread]).map_err(err_mapper)?; + } + return Ok(()); + } + } + + // clonefile() failed, fall back to std::fs::copy(). + } + + // returns size of from as u64 (we ignore) + std::fs::copy(&from_path, &to_path).map_err(err_mapper)?; + Ok(()) +} + +#[op] +async fn op_copy_file_async

( + state: Rc>, + from: String, + to: String, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let from = PathBuf::from(&from); + let to = PathBuf::from(&to); + + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::

(); + permissions.check_read(&from, "Deno.copyFile()")?; + permissions.check_write(&to, "Deno.copyFile()")?; + } + + tokio::task::spawn_blocking(move || { + // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput + // See https://github.com/rust-lang/rust/issues/54800 + // Once the issue is resolved, we should remove this workaround. + if cfg!(unix) && !from.is_file() { + return Err(custom_error( + "NotFound", + format!( + "File not found, copy '{}' -> '{}'", + from.display(), + to.display() + ), + )); + } + // returns size of from as u64 (we ignore) + std::fs::copy(&from, &to).map_err(|err| { + default_err_mapper( + err, + format!("copy '{}' -> '{}'", from.display(), to.display()), + ) + })?; + Ok(()) + }) + .await + .unwrap() +} + +fn to_msec(maybe_time: Result) -> (u64, bool) { + match maybe_time { + Ok(time) => ( + time + .duration_since(UNIX_EPOCH) + .map(|t| t.as_millis() as u64) + .unwrap_or_else(|err| err.duration().as_millis() as u64), + true, + ), + Err(_) => (0, false), + } +} + +macro_rules! create_struct_writer { + (pub struct $name:ident { $($field:ident: $type:ty),* $(,)? }) => { + impl $name { + fn write(self, buf: &mut [u32]) { + let mut offset = 0; + $( + let value = self.$field as u64; + buf[offset] = value as u32; + buf[offset + 1] = (value >> 32) as u32; + #[allow(unused_assignments)] + { + offset += 2; + } + )* + } + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct $name { + $($field: $type),* + } + }; +} + +create_struct_writer! { + pub struct FsStat { + is_file: bool, + is_directory: bool, + is_symlink: bool, + size: u64, + // In milliseconds, like JavaScript. Available on both Unix or Windows. + mtime_set: bool, + mtime: u64, + atime_set: bool, + atime: u64, + birthtime_set: bool, + birthtime: u64, + // Following are only valid under Unix. + dev: u64, + ino: u64, + mode: u32, + nlink: u64, + uid: u32, + gid: u32, + rdev: u64, + blksize: u64, + blocks: u64, + } +} + +#[inline(always)] +fn get_stat(metadata: std::fs::Metadata) -> FsStat { + // Unix stat member (number types only). 0 if not on unix. + macro_rules! usm { + ($member:ident) => {{ + #[cfg(unix)] + { + metadata.$member() + } + #[cfg(not(unix))] + { + 0 + } + }}; + } + + #[cfg(unix)] + use std::os::unix::fs::MetadataExt; + let (mtime, mtime_set) = to_msec(metadata.modified()); + let (atime, atime_set) = to_msec(metadata.accessed()); + let (birthtime, birthtime_set) = to_msec(metadata.created()); + + FsStat { + is_file: metadata.is_file(), + is_directory: metadata.is_dir(), + is_symlink: metadata.file_type().is_symlink(), + size: metadata.len(), + // In milliseconds, like JavaScript. Available on both Unix or Windows. + mtime_set, + mtime, + atime_set, + atime, + birthtime_set, + birthtime, + // Following are only valid under Unix. + dev: usm!(dev), + ino: usm!(ino), + mode: usm!(mode), + nlink: usm!(nlink), + uid: usm!(uid), + gid: usm!(gid), + rdev: usm!(rdev), + blksize: usm!(blksize), + blocks: usm!(blocks), + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StatArgs { + path: String, + lstat: bool, +} + +#[op] +fn op_stat_sync

( + state: &mut OpState, + path: &str, + lstat: bool, + out_buf: &mut [u32], +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(path); + state + .borrow_mut::

() + .check_read(&path, "Deno.statSync()")?; + let err_mapper = + |err| default_err_mapper(err, format!("stat '{}'", path.display())); + let metadata = if lstat { + std::fs::symlink_metadata(&path).map_err(err_mapper)? + } else { + std::fs::metadata(&path).map_err(err_mapper)? + }; + + let stat = get_stat(metadata); + stat.write(out_buf); + + Ok(()) +} + +#[op] +async fn op_stat_async

( + state: Rc>, + args: StatArgs, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&args.path); + let lstat = args.lstat; + + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_read(&path, "Deno.stat()")?; + } + + tokio::task::spawn_blocking(move || { + debug!("op_stat_async {} {}", path.display(), lstat); + let err_mapper = + |err| default_err_mapper(err, format!("stat '{}'", path.display())); + let metadata = if lstat { + std::fs::symlink_metadata(&path).map_err(err_mapper)? + } else { + std::fs::metadata(&path).map_err(err_mapper)? + }; + Ok(get_stat(metadata)) + }) + .await + .unwrap() +} + +#[op] +fn op_realpath_sync

( + state: &mut OpState, + path: String, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + + let permissions = state.borrow_mut::

(); + permissions.check_read(&path, "Deno.realPathSync()")?; + if path.is_relative() { + permissions.check_read_blind( + ¤t_dir()?, + "CWD", + "Deno.realPathSync()", + )?; + } + + debug!("op_realpath_sync {}", path.display()); + // corresponds to the realpath on Unix and + // CreateFile and GetFinalPathNameByHandle on Windows + let realpath = canonicalize_path(&path)?; + let realpath_str = into_string(realpath.into_os_string())?; + Ok(realpath_str) +} + +#[op] +async fn op_realpath_async

( + state: Rc>, + path: String, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::

(); + permissions.check_read(&path, "Deno.realPath()")?; + if path.is_relative() { + permissions.check_read_blind( + ¤t_dir()?, + "CWD", + "Deno.realPath()", + )?; + } + } + + tokio::task::spawn_blocking(move || { + debug!("op_realpath_async {}", path.display()); + // corresponds to the realpath on Unix and + // CreateFile and GetFinalPathNameByHandle on Windows + let realpath = canonicalize_path(&path)?; + let realpath_str = into_string(realpath.into_os_string())?; + Ok(realpath_str) + }) + .await + .unwrap() +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DirEntry { + name: String, + is_file: bool, + is_directory: bool, + is_symlink: bool, +} + +#[op] +fn op_read_dir_sync

( + state: &mut OpState, + path: String, +) -> Result, AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + + state + .borrow_mut::

() + .check_read(&path, "Deno.readDirSync()")?; + + debug!("op_read_dir_sync {}", path.display()); + + let entries: Vec<_> = std::fs::read_dir(&path) + .map_err(|err| { + default_err_mapper(err, format!("readdir '{}'", path.display())) + })? + .filter_map(|entry| { + let entry = entry.unwrap(); + // Not all filenames can be encoded as UTF-8. Skip those for now. + if let Ok(name) = into_string(entry.file_name()) { + Some(DirEntry { + name, + is_file: entry + .file_type() + .map_or(false, |file_type| file_type.is_file()), + is_directory: entry + .file_type() + .map_or(false, |file_type| file_type.is_dir()), + is_symlink: entry + .file_type() + .map_or(false, |file_type| file_type.is_symlink()), + }) + } else { + None + } + }) + .collect(); + + Ok(entries) +} + +#[op] +async fn op_read_dir_async

( + state: Rc>, + path: String, +) -> Result, AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + { + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_read(&path, "Deno.readDir()")?; + } + tokio::task::spawn_blocking(move || { + debug!("op_read_dir_async {}", path.display()); + + let entries: Vec<_> = std::fs::read_dir(&path) + .map_err(|err| { + default_err_mapper(err, format!("readdir '{}'", path.display())) + })? + .filter_map(|entry| { + let entry = entry.unwrap(); + // Not all filenames can be encoded as UTF-8. Skip those for now. + if let Ok(name) = into_string(entry.file_name()) { + Some(DirEntry { + name, + is_file: entry + .file_type() + .map_or(false, |file_type| file_type.is_file()), + is_directory: entry + .file_type() + .map_or(false, |file_type| file_type.is_dir()), + is_symlink: entry + .file_type() + .map_or(false, |file_type| file_type.is_symlink()), + }) + } else { + None + } + }) + .collect(); + + Ok(entries) + }) + .await + .unwrap() +} + +#[op] +fn op_rename_sync

( + state: &mut OpState, + oldpath: &str, + newpath: &str, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let oldpath = PathBuf::from(oldpath); + let newpath = PathBuf::from(newpath); + + let permissions = state.borrow_mut::

(); + permissions.check_read(&oldpath, "Deno.renameSync()")?; + permissions.check_write(&oldpath, "Deno.renameSync()")?; + permissions.check_write(&newpath, "Deno.renameSync()")?; + + std::fs::rename(&oldpath, &newpath).map_err(|err| { + default_err_mapper( + err, + format!("rename '{}' -> '{}'", oldpath.display(), newpath.display()), + ) + })?; + Ok(()) +} + +#[op] +async fn op_rename_async

( + state: Rc>, + oldpath: String, + newpath: String, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let oldpath = PathBuf::from(&oldpath); + let newpath = PathBuf::from(&newpath); + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::

(); + permissions.check_read(&oldpath, "Deno.rename()")?; + permissions.check_write(&oldpath, "Deno.rename()")?; + permissions.check_write(&newpath, "Deno.rename()")?; + } + + tokio::task::spawn_blocking(move || { + std::fs::rename(&oldpath, &newpath).map_err(|err| { + default_err_mapper( + err, + format!("rename '{}' -> '{}'", oldpath.display(), newpath.display()), + ) + })?; + Ok(()) + }) + .await + .unwrap() +} + +#[op] +fn op_link_sync

( + state: &mut OpState, + oldpath: &str, + newpath: &str, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let oldpath = PathBuf::from(oldpath); + let newpath = PathBuf::from(newpath); + + let permissions = state.borrow_mut::

(); + permissions.check_read(&oldpath, "Deno.linkSync()")?; + permissions.check_write(&oldpath, "Deno.linkSync()")?; + permissions.check_read(&newpath, "Deno.linkSync()")?; + permissions.check_write(&newpath, "Deno.linkSync()")?; + + std::fs::hard_link(&oldpath, &newpath).map_err(|err| { + default_err_mapper( + err, + format!("link '{}' -> '{}'", oldpath.display(), newpath.display()), + ) + })?; + Ok(()) +} + +#[op] +async fn op_link_async

( + state: Rc>, + oldpath: String, + newpath: String, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let oldpath = PathBuf::from(&oldpath); + let newpath = PathBuf::from(&newpath); + + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::

(); + permissions.check_read(&oldpath, "Deno.link()")?; + permissions.check_write(&oldpath, "Deno.link()")?; + permissions.check_read(&newpath, "Deno.link()")?; + permissions.check_write(&newpath, "Deno.link()")?; + } + + tokio::task::spawn_blocking(move || { + let err_mapper = |err| { + default_err_mapper( + err, + format!("link '{}' -> '{}'", oldpath.display(), newpath.display()), + ) + }; + std::fs::hard_link(&oldpath, &newpath).map_err(err_mapper)?; + Ok(()) + }) + .await + .unwrap() +} + +#[op] +fn op_symlink_sync

( + state: &mut OpState, + oldpath: &str, + newpath: &str, + _type: Option, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let oldpath = PathBuf::from(oldpath); + let newpath = PathBuf::from(newpath); + + state + .borrow_mut::

() + .check_write_all("Deno.symlinkSync()")?; + state + .borrow_mut::

() + .check_read_all("Deno.symlinkSync()")?; + + let err_mapper = |err| { + default_err_mapper( + err, + format!("symlink '{}' -> '{}'", oldpath.display(), newpath.display()), + ) + }; + + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(&oldpath, &newpath).map_err(err_mapper)?; + Ok(()) + } + #[cfg(not(unix))] + { + use std::os::windows::fs::symlink_dir; + use std::os::windows::fs::symlink_file; + + match _type { + Some(ty) => match ty.as_ref() { + "file" => symlink_file(&oldpath, &newpath).map_err(err_mapper)?, + "dir" => symlink_dir(&oldpath, &newpath).map_err(err_mapper)?, + _ => return Err(type_error("unsupported type")), + }, + None => { + let old_meta = std::fs::metadata(&oldpath); + match old_meta { + Ok(metadata) => { + if metadata.is_file() { + symlink_file(&oldpath, &newpath).map_err(err_mapper)? + } else if metadata.is_dir() { + symlink_dir(&oldpath, &newpath).map_err(err_mapper)? + } + } + Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), + } + } + }; + Ok(()) + } +} + +#[op] +async fn op_symlink_async

( + state: Rc>, + oldpath: String, + newpath: String, + _type: Option, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let oldpath = PathBuf::from(&oldpath); + let newpath = PathBuf::from(&newpath); + + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_write_all("Deno.symlink()")?; + state.borrow_mut::

().check_read_all("Deno.symlink()")?; + } + + tokio::task::spawn_blocking(move || { + let err_mapper = |err| default_err_mapper(err, format!( + "symlink '{}' -> '{}'", + oldpath.display(), + newpath.display() + )); + + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(&oldpath, &newpath).map_err(err_mapper)?; + Ok(()) + } + #[cfg(not(unix))] + { + use std::os::windows::fs::{symlink_dir, symlink_file}; + + match _type { + Some(ty) => match ty.as_ref() { + "file" => symlink_file(&oldpath, &newpath).map_err(err_mapper)?, + "dir" => symlink_dir(&oldpath, &newpath).map_err(err_mapper)?, + _ => return Err(type_error("unsupported type")), + }, + None => { + let old_meta = std::fs::metadata(&oldpath); + match old_meta { + Ok(metadata) => { + if metadata.is_file() { + symlink_file(&oldpath, &newpath).map_err(err_mapper)? + } else if metadata.is_dir() { + symlink_dir(&oldpath, &newpath).map_err(err_mapper)? + } + } + Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), + } + } + }; + Ok(()) + } + }) + .await + .unwrap() +} + +#[op] +fn op_read_link_sync

( + state: &mut OpState, + path: String, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + + state + .borrow_mut::

() + .check_read(&path, "Deno.readLink()")?; + + debug!("op_read_link_value {}", path.display()); + let target = std::fs::read_link(&path) + .map_err(|err| { + default_err_mapper(err, format!("readlink '{}'", path.display())) + })? + .into_os_string(); + let targetstr = into_string(target)?; + Ok(targetstr) +} + +#[op] +async fn op_read_link_async

( + state: Rc>, + path: String, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + { + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_read(&path, "Deno.readLink()")?; + } + tokio::task::spawn_blocking(move || { + debug!("op_read_link_async {}", path.display()); + let target = std::fs::read_link(&path) + .map_err(|err| { + default_err_mapper(err, format!("readlink '{}'", path.display())) + })? + .into_os_string(); + let targetstr = into_string(target)?; + Ok(targetstr) + }) + .await + .unwrap() +} + +#[op] +fn op_ftruncate_sync( + state: &mut OpState, + rid: u32, + len: i32, +) -> Result<(), AnyError> { + let len = len as u64; + StdFileResource::with_file(state, rid, |std_file| { + std_file.set_len(len).map_err(AnyError::from) + })?; + Ok(()) +} + +#[op] +async fn op_ftruncate_async( + state: Rc>, + rid: ResourceId, + len: i32, +) -> Result<(), AnyError> { + let len = len as u64; + + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + std_file.set_len(len)?; + Ok(()) + }) + .await +} + +#[op] +fn op_truncate_sync

( + state: &mut OpState, + path: &str, + len: u64, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(path); + + state + .borrow_mut::

() + .check_write(&path, "Deno.truncateSync()")?; + + debug!("op_truncate_sync {} {}", path.display(), len); + let err_mapper = + |err| default_err_mapper(err, format!("truncate '{}'", path.display())); + let f = std::fs::OpenOptions::new() + .write(true) + .open(&path) + .map_err(err_mapper)?; + f.set_len(len).map_err(err_mapper)?; + Ok(()) +} + +#[op] +async fn op_truncate_async

( + state: Rc>, + path: String, + len: u64, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + + { + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_write(&path, "Deno.truncate()")?; + } + tokio::task::spawn_blocking(move || { + debug!("op_truncate_async {} {}", path.display(), len); + let err_mapper = + |err| default_err_mapper(err, format!("truncate '{}'", path.display())); + let f = std::fs::OpenOptions::new() + .write(true) + .open(&path) + .map_err(err_mapper)?; + f.set_len(len).map_err(err_mapper)?; + Ok(()) + }) + .await + .unwrap() +} + +fn make_temp( + dir: Option<&Path>, + prefix: Option<&str>, + suffix: Option<&str>, + is_dir: bool, +) -> std::io::Result { + let prefix_ = prefix.unwrap_or(""); + let suffix_ = suffix.unwrap_or(""); + let mut buf: PathBuf = match dir { + Some(p) => p.to_path_buf(), + None => temp_dir(), + } + .join("_"); + let mut rng = thread_rng(); + loop { + let unique = rng.gen::(); + buf.set_file_name(format!("{prefix_}{unique:08x}{suffix_}")); + let r = if is_dir { + #[allow(unused_mut)] + let mut builder = std::fs::DirBuilder::new(); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(0o700); + } + builder.create(buf.as_path()) + } else { + let mut open_options = std::fs::OpenOptions::new(); + open_options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + open_options.mode(0o600); + } + open_options.open(buf.as_path())?; + Ok(()) + }; + match r { + Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Ok(_) => return Ok(buf), + Err(e) => return Err(e), + } + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MakeTempArgs { + dir: Option, + prefix: Option, + suffix: Option, +} + +#[op] +fn op_make_temp_dir_sync

( + state: &mut OpState, + args: MakeTempArgs, +) -> Result +where + P: FsPermissions + 'static, +{ + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + + state.borrow_mut::

().check_write( + dir.clone().unwrap_or_else(temp_dir).as_path(), + "Deno.makeTempDirSync()", + )?; + + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + true, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(path_str) +} + +#[op] +async fn op_make_temp_dir_async

( + state: Rc>, + args: MakeTempArgs, +) -> Result +where + P: FsPermissions + 'static, +{ + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_write( + dir.clone().unwrap_or_else(temp_dir).as_path(), + "Deno.makeTempDir()", + )?; + } + tokio::task::spawn_blocking(move || { + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + true, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(path_str) + }) + .await + .unwrap() +} + +#[op] +fn op_make_temp_file_sync

( + state: &mut OpState, + args: MakeTempArgs, +) -> Result +where + P: FsPermissions + 'static, +{ + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + + state.borrow_mut::

().check_write( + dir.clone().unwrap_or_else(temp_dir).as_path(), + "Deno.makeTempFileSync()", + )?; + + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + false, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(path_str) +} + +#[op] +async fn op_make_temp_file_async

( + state: Rc>, + args: MakeTempArgs, +) -> Result +where + P: FsPermissions + 'static, +{ + let dir = args.dir.map(|s| PathBuf::from(&s)); + let prefix = args.prefix.map(String::from); + let suffix = args.suffix.map(String::from); + { + let mut state = state.borrow_mut(); + state.borrow_mut::

().check_write( + dir.clone().unwrap_or_else(temp_dir).as_path(), + "Deno.makeTempFile()", + )?; + } + tokio::task::spawn_blocking(move || { + // TODO(piscisaureus): use byte vector for paths, not a string. + // See https://github.com/denoland/deno/issues/627. + // We can't assume that paths are always valid utf8 strings. + let path = make_temp( + // Converting Option to Option<&str> + dir.as_deref(), + prefix.as_deref(), + suffix.as_deref(), + false, + )?; + let path_str = into_string(path.into_os_string())?; + + Ok(path_str) + }) + .await + .unwrap() +} + +#[op] +fn op_futime_sync( + state: &mut OpState, + rid: ResourceId, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, +) -> Result<(), AnyError> { + let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); + let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); + + StdFileResource::with_file(state, rid, |std_file| { + filetime::set_file_handle_times(std_file, Some(atime), Some(mtime)) + .map_err(AnyError::from) + })?; + + Ok(()) +} + +#[op] +async fn op_futime_async( + state: Rc>, + rid: ResourceId, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, +) -> Result<(), AnyError> { + let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); + let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); + + StdFileResource::with_file_blocking_task(state, rid, move |std_file| { + filetime::set_file_handle_times(std_file, Some(atime), Some(mtime))?; + Ok(()) + }) + .await +} + +#[op] +fn op_utime_sync

( + state: &mut OpState, + path: &str, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(path); + let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); + let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); + + state.borrow_mut::

().check_write(&path, "Deno.utime()")?; + filetime::set_file_times(&path, atime, mtime).map_err(|err| { + default_err_mapper(err, format!("utime '{}'", path.display())) + })?; + Ok(()) +} + +#[op] +async fn op_utime_async

( + state: Rc>, + path: String, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, +) -> Result<(), AnyError> +where + P: FsPermissions + 'static, +{ + let path = PathBuf::from(&path); + let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); + let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); + + state + .borrow_mut() + .borrow_mut::

() + .check_write(&path, "Deno.utime()")?; + + tokio::task::spawn_blocking(move || { + filetime::set_file_times(&path, atime, mtime).map_err(|err| { + default_err_mapper(err, format!("utime '{}'", path.display())) + })?; + Ok(()) + }) + .await + .unwrap() +} + +#[op] +fn op_cwd

(state: &mut OpState) -> Result +where + P: FsPermissions + 'static, +{ + let path = current_dir()?; + state + .borrow_mut::

() + .check_read_blind(&path, "CWD", "Deno.cwd()")?; + let path_str = into_string(path.into_os_string())?; + Ok(path_str) +} + +#[op] +fn op_readfile_sync

( + state: &mut OpState, + path: String, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = Path::new(&path); + state + .borrow_mut::

() + .check_read(path, "Deno.readFileSync()")?; + Ok( + std::fs::read(path) + .map_err(|err| { + default_err_mapper(err, format!("readfile '{}'", path.display())) + })? + .into(), + ) +} + +#[op] +fn op_readfile_text_sync

( + state: &mut OpState, + path: String, +) -> Result +where + P: FsPermissions + 'static, +{ + let path = Path::new(&path); + state + .borrow_mut::

() + .check_read(path, "Deno.readTextFileSync()")?; + Ok(string_from_utf8_lossy(std::fs::read(path).map_err( + |err| default_err_mapper(err, format!("readfile '{}'", path.display())), + )?)) +} + +#[op] +async fn op_readfile_async

( + state: Rc>, + path: String, + cancel_rid: Option, +) -> Result +where + P: FsPermissions + 'static, +{ + { + let path = Path::new(&path); + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_read(path, "Deno.readFile()")?; + } + + let read_future = tokio::task::spawn_blocking(move || { + let path = Path::new(&path); + Ok(std::fs::read(path).map(ZeroCopyBuf::from).map_err(|err| { + default_err_mapper(err, format!("readfile '{}'", path.display())) + })?) + }); + + let cancel_handle = cancel_rid.and_then(|rid| { + state + .borrow_mut() + .resource_table + .get::(rid) + .ok() + }); + + if let Some(cancel_handle) = cancel_handle { + let read_future_rv = read_future.or_cancel(cancel_handle).await; + + if let Some(cancel_rid) = cancel_rid { + state.borrow_mut().resource_table.close(cancel_rid).ok(); + }; + + return read_future_rv??; + } + + read_future.await? +} + +#[op] +async fn op_readfile_text_async

( + state: Rc>, + path: String, + cancel_rid: Option, +) -> Result +where + P: FsPermissions + 'static, +{ + { + let path = Path::new(&path); + let mut state = state.borrow_mut(); + state + .borrow_mut::

() + .check_read(path, "Deno.readTextFile()")?; + } + + let read_future = tokio::task::spawn_blocking(move || { + let path = Path::new(&path); + Ok(string_from_utf8_lossy(std::fs::read(path).map_err( + |err| default_err_mapper(err, format!("readfile '{}'", path.display())), + )?)) + }); + + let cancel_handle = cancel_rid.and_then(|rid| { + state + .borrow_mut() + .resource_table + .get::(rid) + .ok() + }); + + if let Some(cancel_handle) = cancel_handle { + let read_future_rv = read_future.or_cancel(cancel_handle).await; + + if let Some(cancel_rid) = cancel_rid { + state.borrow_mut().resource_table.close(cancel_rid).ok(); + }; + + return read_future_rv??; + } + + read_future.await? +} + +// Like String::from_utf8_lossy but operates on owned values +fn string_from_utf8_lossy(buf: Vec) -> String { + match String::from_utf8_lossy(&buf) { + // buf contained non-utf8 chars than have been patched + Cow::Owned(s) => s, + // SAFETY: if Borrowed then the buf only contains utf8 chars, + // we do this instead of .into_owned() to avoid copying the input buf + Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(buf) }, + } +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 468280075..e1134bd03 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -323,7 +323,7 @@ pub fn init_polyfill() -> Extension { Extension::builder(env!("CARGO_PKG_NAME")) .esm(esm_files) .esm_entry_point("internal:deno_node/module_all.ts") - .dependencies(vec!["deno_io"]) + .dependencies(vec!["deno_io", "deno_fs"]) .ops(vec![ crypto::op_node_create_hash::decl(), crypto::op_node_hash_update::decl(), diff --git a/ext/node/polyfills/_process/process.ts b/ext/node/polyfills/_process/process.ts index bee65f905..7ed40492d 100644 --- a/ext/node/polyfills/_process/process.ts +++ b/ext/node/polyfills/_process/process.ts @@ -7,7 +7,7 @@ const core = globalThis.Deno.core; import { nextTick as _nextTick } from "internal:deno_node/_next_tick.ts"; import { _exiting } from "internal:deno_node/_process/exiting.ts"; -import * as fs from "internal:runtime/30_fs.js"; +import * as fs from "internal:deno_fs/30_fs.js"; /** Returns the operating system CPU architecture for which the Deno binary was compiled */ export function arch(): string { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 5a4028349..112156203 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -42,6 +42,7 @@ deno_crypto.workspace = true deno_fetch.workspace = true deno_ffi.workspace = true deno_flash.workspace = true +deno_fs.workspace = true deno_http.workspace = true deno_io.workspace = true deno_net.workspace = true @@ -70,6 +71,7 @@ deno_crypto.workspace = true deno_fetch.workspace = true deno_ffi.workspace = true deno_flash.workspace = true +deno_fs.workspace = true deno_http.workspace = true deno_io.workspace = true deno_napi.workspace = true diff --git a/runtime/build.rs b/runtime/build.rs index 76c0534bf..166421586 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -165,6 +165,41 @@ mod startup_snapshot { } } + impl deno_fs::FsPermissions for Permissions { + fn check_read( + &mut self, + _path: &Path, + _api_name: &str, + ) -> Result<(), AnyError> { + unreachable!("snapshotting!") + } + + fn check_read_blind( + &mut self, + _path: &Path, + _display: &str, + _api_name: &str, + ) -> Result<(), AnyError> { + unreachable!("snapshotting!") + } + + fn check_write( + &mut self, + _path: &Path, + _api_name: &str, + ) -> Result<(), AnyError> { + unreachable!("snapshotting!") + } + + fn check_read_all(&mut self, _api_name: &str) -> Result<(), AnyError> { + unreachable!("snapshotting!") + } + + fn check_write_all(&mut self, _api_name: &str) -> Result<(), AnyError> { + unreachable!("snapshotting!") + } + } + fn create_runtime_snapshot( snapshot_path: PathBuf, maybe_additional_extension: Option, @@ -191,6 +226,7 @@ mod startup_snapshot { "deno_http", "deno_flash", "deno_io", + "deno_fs", ]) .esm(include_js_files!( dir "js", @@ -200,7 +236,6 @@ mod startup_snapshot { "10_permissions.js", "11_workers.js", "13_buffer.js", - "30_fs.js", "30_os.js", "40_fs_events.js", "40_http.js", @@ -240,6 +275,7 @@ mod startup_snapshot { deno_napi::init::(), deno_http::init(), deno_io::init(Default::default()), + deno_fs::init::(false), deno_flash::init::(false), // No --unstable runtime_extension, // FIXME(bartlomieju): these extensions are specified last, because they diff --git a/runtime/js/30_fs.js b/runtime/js/30_fs.js deleted file mode 100644 index 19e7f372b..000000000 --- a/runtime/js/30_fs.js +++ /dev/null @@ -1,899 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -const core = globalThis.Deno.core; -const ops = core.ops; -const primordials = globalThis.__bootstrap.primordials; -const { - ArrayPrototypeFilter, - Date, - DatePrototype, - Error, - Function, - MathTrunc, - ObjectEntries, - ObjectPrototypeIsPrototypeOf, - ObjectValues, - SymbolAsyncIterator, - SymbolIterator, - Uint32Array, -} = primordials; -import { read, readSync, write, writeSync } from "internal:deno_io/12_io.js"; -import * as abortSignal from "internal:deno_web/03_abort_signal.js"; -import { - readableStreamForRid, - ReadableStreamPrototype, - writableStreamForRid, -} from "internal:deno_web/06_streams.js"; -import { pathFromURL } from "internal:deno_web/00_infra.js"; - -function chmodSync(path, mode) { - ops.op_chmod_sync(pathFromURL(path), mode); -} - -async function chmod(path, mode) { - await core.opAsync("op_chmod_async", pathFromURL(path), mode); -} - -function chownSync( - path, - uid, - gid, -) { - ops.op_chown_sync(pathFromURL(path), uid, gid); -} - -async function chown( - path, - uid, - gid, -) { - await core.opAsync( - "op_chown_async", - pathFromURL(path), - uid, - gid, - ); -} - -function copyFileSync( - fromPath, - toPath, -) { - ops.op_copy_file_sync( - pathFromURL(fromPath), - pathFromURL(toPath), - ); -} - -async function copyFile( - fromPath, - toPath, -) { - await core.opAsync( - "op_copy_file_async", - pathFromURL(fromPath), - pathFromURL(toPath), - ); -} - -function cwd() { - return ops.op_cwd(); -} - -function chdir(directory) { - ops.op_chdir(pathFromURL(directory)); -} - -function makeTempDirSync(options = {}) { - return ops.op_make_temp_dir_sync(options); -} - -function makeTempDir(options = {}) { - return core.opAsync("op_make_temp_dir_async", options); -} - -function makeTempFileSync(options = {}) { - return ops.op_make_temp_file_sync(options); -} - -function makeTempFile(options = {}) { - return core.opAsync("op_make_temp_file_async", options); -} - -function mkdirArgs(path, options) { - const args = { path: pathFromURL(path), recursive: false }; - if (options != null) { - if (typeof options.recursive == "boolean") { - args.recursive = options.recursive; - } - if (options.mode) { - args.mode = options.mode; - } - } - return args; -} - -function mkdirSync(path, options) { - ops.op_mkdir_sync(mkdirArgs(path, options)); -} - -async function mkdir( - path, - options, -) { - await core.opAsync("op_mkdir_async", mkdirArgs(path, options)); -} - -function readDirSync(path) { - return ops.op_read_dir_sync(pathFromURL(path))[ - SymbolIterator - ](); -} - -function readDir(path) { - const array = core.opAsync( - "op_read_dir_async", - pathFromURL(path), - ); - return { - async *[SymbolAsyncIterator]() { - yield* await array; - }, - }; -} - -function readLinkSync(path) { - return ops.op_read_link_sync(pathFromURL(path)); -} - -function readLink(path) { - return core.opAsync("op_read_link_async", pathFromURL(path)); -} - -function realPathSync(path) { - return ops.op_realpath_sync(pathFromURL(path)); -} - -function realPath(path) { - return core.opAsync("op_realpath_async", pathFromURL(path)); -} - -function removeSync( - path, - options = {}, -) { - ops.op_remove_sync( - pathFromURL(path), - !!options.recursive, - ); -} - -async function remove( - path, - options = {}, -) { - await core.opAsync( - "op_remove_async", - pathFromURL(path), - !!options.recursive, - ); -} - -function renameSync(oldpath, newpath) { - ops.op_rename_sync( - pathFromURL(oldpath), - pathFromURL(newpath), - ); -} - -async function rename(oldpath, newpath) { - await core.opAsync( - "op_rename_async", - pathFromURL(oldpath), - pathFromURL(newpath), - ); -} - -// Extract the FsStat object from the encoded buffer. -// See `runtime/ops/fs.rs` for the encoder. -// -// This is not a general purpose decoder. There are 4 types: -// -// 1. date -// offset += 4 -// 1/0 | extra padding | high u32 | low u32 -// if date[0] == 1, new Date(u64) else null -// -// 2. bool -// offset += 2 -// 1/0 | extra padding -// -// 3. u64 -// offset += 2 -// high u32 | low u32 -// -// 4. ?u64 converts a zero u64 value to JS null on Windows. -function createByteStruct(types) { - // types can be "date", "bool" or "u64". - // `?` prefix means optional on windows. - let offset = 0; - let str = - 'const unix = Deno.build.os === "darwin" || Deno.build.os === "linux"; return {'; - const typeEntries = ObjectEntries(types); - for (let i = 0; i < typeEntries.length; ++i) { - let { 0: name, 1: type } = typeEntries[i]; - - const optional = type.startsWith("?"); - if (optional) type = type.slice(1); - - if (type == "u64") { - if (!optional) { - str += `${name}: view[${offset}] + view[${offset + 1}] * 2**32,`; - } else { - str += `${name}: (unix ? (view[${offset}] + view[${ - offset + 1 - }] * 2**32) : (view[${offset}] + view[${ - offset + 1 - }] * 2**32) || null),`; - } - } else if (type == "date") { - str += `${name}: view[${offset}] === 0 ? null : new Date(view[${ - offset + 2 - }] + view[${offset + 3}] * 2**32),`; - offset += 2; - } else { - str += `${name}: !!(view[${offset}] + view[${offset + 1}] * 2**32),`; - } - offset += 2; - } - str += "};"; - // ...so you don't like eval huh? don't worry, it only executes during snapshot :) - return [new Function("view", str), new Uint32Array(offset)]; -} - -const { 0: statStruct, 1: statBuf } = createByteStruct({ - isFile: "bool", - isDirectory: "bool", - isSymlink: "bool", - size: "u64", - mtime: "date", - atime: "date", - birthtime: "date", - dev: "?u64", - ino: "?u64", - mode: "?u64", - nlink: "?u64", - uid: "?u64", - gid: "?u64", - rdev: "?u64", - blksize: "?u64", - blocks: "?u64", -}); - -function parseFileInfo(response) { - const unix = core.build.os === "darwin" || core.build.os === "linux"; - return { - isFile: response.isFile, - isDirectory: response.isDirectory, - isSymlink: response.isSymlink, - size: response.size, - mtime: response.mtimeSet !== null ? new Date(response.mtime) : null, - atime: response.atimeSet !== null ? new Date(response.atime) : null, - birthtime: response.birthtimeSet !== null - ? new Date(response.birthtime) - : null, - // Only non-null if on Unix - dev: unix ? response.dev : null, - ino: unix ? response.ino : null, - mode: unix ? response.mode : null, - nlink: unix ? response.nlink : null, - uid: unix ? response.uid : null, - gid: unix ? response.gid : null, - rdev: unix ? response.rdev : null, - blksize: unix ? response.blksize : null, - blocks: unix ? response.blocks : null, - }; -} - -function fstatSync(rid) { - ops.op_fstat_sync(rid, statBuf); - return statStruct(statBuf); -} - -async function fstat(rid) { - return parseFileInfo(await core.opAsync("op_fstat_async", rid)); -} - -async function lstat(path) { - const res = await core.opAsync("op_stat_async", { - path: pathFromURL(path), - lstat: true, - }); - return parseFileInfo(res); -} - -function lstatSync(path) { - ops.op_stat_sync( - pathFromURL(path), - true, - statBuf, - ); - return statStruct(statBuf); -} - -async function stat(path) { - const res = await core.opAsync("op_stat_async", { - path: pathFromURL(path), - lstat: false, - }); - return parseFileInfo(res); -} - -function statSync(path) { - ops.op_stat_sync( - pathFromURL(path), - false, - statBuf, - ); - return statStruct(statBuf); -} - -function coerceLen(len) { - if (len == null || len < 0) { - return 0; - } - - return len; -} - -function ftruncateSync(rid, len) { - ops.op_ftruncate_sync(rid, coerceLen(len)); -} - -async function ftruncate(rid, len) { - await core.opAsync("op_ftruncate_async", rid, coerceLen(len)); -} - -function truncateSync(path, len) { - ops.op_truncate_sync(path, coerceLen(len)); -} - -async function truncate(path, len) { - await core.opAsync("op_truncate_async", path, coerceLen(len)); -} - -function umask(mask) { - return ops.op_umask(mask); -} - -function linkSync(oldpath, newpath) { - ops.op_link_sync(oldpath, newpath); -} - -async function link(oldpath, newpath) { - await core.opAsync("op_link_async", oldpath, newpath); -} - -function toUnixTimeFromEpoch(value) { - if (ObjectPrototypeIsPrototypeOf(DatePrototype, value)) { - const time = value.valueOf(); - const seconds = MathTrunc(time / 1e3); - const nanoseconds = MathTrunc(time - (seconds * 1e3)) * 1e6; - - return [ - seconds, - nanoseconds, - ]; - } - - const seconds = value; - const nanoseconds = 0; - - return [ - seconds, - nanoseconds, - ]; -} - -function futimeSync( - rid, - atime, - mtime, -) { - const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); - const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); - ops.op_futime_sync(rid, atimeSec, atimeNsec, mtimeSec, mtimeNsec); -} - -async function futime( - rid, - atime, - mtime, -) { - const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); - const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); - await core.opAsync( - "op_futime_async", - rid, - atimeSec, - atimeNsec, - mtimeSec, - mtimeNsec, - ); -} - -function utimeSync( - path, - atime, - mtime, -) { - const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); - const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); - ops.op_utime_sync( - pathFromURL(path), - atimeSec, - atimeNsec, - mtimeSec, - mtimeNsec, - ); -} - -async function utime( - path, - atime, - mtime, -) { - const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime); - const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime); - await core.opAsync( - "op_utime_async", - pathFromURL(path), - atimeSec, - atimeNsec, - mtimeSec, - mtimeNsec, - ); -} - -function symlinkSync( - oldpath, - newpath, - options, -) { - ops.op_symlink_sync( - pathFromURL(oldpath), - pathFromURL(newpath), - options?.type, - ); -} - -async function symlink( - oldpath, - newpath, - options, -) { - await core.opAsync( - "op_symlink_async", - pathFromURL(oldpath), - pathFromURL(newpath), - options?.type, - ); -} - -function fdatasyncSync(rid) { - ops.op_fdatasync_sync(rid); -} - -async function fdatasync(rid) { - await core.opAsync("op_fdatasync_async", rid); -} - -function fsyncSync(rid) { - ops.op_fsync_sync(rid); -} - -async function fsync(rid) { - await core.opAsync("op_fsync_async", rid); -} - -function flockSync(rid, exclusive) { - ops.op_flock_sync(rid, exclusive === true); -} - -async function flock(rid, exclusive) { - await core.opAsync("op_flock_async", rid, exclusive === true); -} - -function funlockSync(rid) { - ops.op_funlock_sync(rid); -} - -async function funlock(rid) { - await core.opAsync("op_funlock_async", rid); -} - -function seekSync( - rid, - offset, - whence, -) { - return ops.op_seek_sync({ rid, offset, whence }); -} - -function seek( - rid, - offset, - whence, -) { - return core.opAsync("op_seek_async", { rid, offset, whence }); -} - -function openSync( - path, - options, -) { - if (options) checkOpenOptions(options); - const mode = options?.mode; - const rid = ops.op_open_sync( - pathFromURL(path), - options, - mode, - ); - - return new FsFile(rid); -} - -async function open( - path, - options, -) { - if (options) checkOpenOptions(options); - const mode = options?.mode; - const rid = await core.opAsync( - "op_open_async", - pathFromURL(path), - options, - mode, - ); - - return new FsFile(rid); -} - -function createSync(path) { - return openSync(path, { - read: true, - write: true, - truncate: true, - create: true, - }); -} - -function create(path) { - return open(path, { - read: true, - write: true, - truncate: true, - create: true, - }); -} - -class FsFile { - #rid = 0; - - #readable; - #writable; - - constructor(rid) { - this.#rid = rid; - } - - get rid() { - return this.#rid; - } - - write(p) { - return write(this.rid, p); - } - - writeSync(p) { - return writeSync(this.rid, p); - } - - truncate(len) { - return ftruncate(this.rid, len); - } - - truncateSync(len) { - return ftruncateSync(this.rid, len); - } - - read(p) { - return read(this.rid, p); - } - - readSync(p) { - return readSync(this.rid, p); - } - - seek(offset, whence) { - return seek(this.rid, offset, whence); - } - - seekSync(offset, whence) { - return seekSync(this.rid, offset, whence); - } - - stat() { - return fstat(this.rid); - } - - statSync() { - return fstatSync(this.rid); - } - - close() { - core.close(this.rid); - } - - get readable() { - if (this.#readable === undefined) { - this.#readable = readableStreamForRid(this.rid); - } - return this.#readable; - } - - get writable() { - if (this.#writable === undefined) { - this.#writable = writableStreamForRid(this.rid); - } - return this.#writable; - } -} - -function checkOpenOptions(options) { - if ( - ArrayPrototypeFilter( - ObjectValues(options), - (val) => val === true, - ).length === 0 - ) { - throw new Error("OpenOptions requires at least one option to be true"); - } - - if (options.truncate && !options.write) { - throw new Error("'truncate' option requires 'write' option"); - } - - const createOrCreateNewWithoutWriteOrAppend = - (options.create || options.createNew) && - !(options.write || options.append); - - if (createOrCreateNewWithoutWriteOrAppend) { - throw new Error( - "'create' or 'createNew' options require 'write' or 'append' option", - ); - } -} - -const File = FsFile; - -function readFileSync(path) { - return ops.op_readfile_sync(pathFromURL(path)); -} - -async function readFile(path, options) { - let cancelRid; - let abortHandler; - if (options?.signal) { - options.signal.throwIfAborted(); - cancelRid = ops.op_cancel_handle(); - abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); - } - - try { - const read = await core.opAsync( - "op_readfile_async", - pathFromURL(path), - cancelRid, - ); - return read; - } finally { - if (options?.signal) { - options.signal[abortSignal.remove](abortHandler); - - // always throw the abort error when aborted - options.signal.throwIfAborted(); - } - } -} - -function readTextFileSync(path) { - return ops.op_readfile_text_sync(pathFromURL(path)); -} - -async function readTextFile(path, options) { - let cancelRid; - let abortHandler; - if (options?.signal) { - options.signal.throwIfAborted(); - cancelRid = ops.op_cancel_handle(); - abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); - } - - try { - const read = await core.opAsync( - "op_readfile_text_async", - pathFromURL(path), - cancelRid, - ); - return read; - } finally { - if (options?.signal) { - options.signal[abortSignal.remove](abortHandler); - - // always throw the abort error when aborted - options.signal.throwIfAborted(); - } - } -} - -function writeFileSync( - path, - data, - options = {}, -) { - options.signal?.throwIfAborted(); - ops.op_write_file_sync( - pathFromURL(path), - options.mode, - options.append ?? false, - options.create ?? true, - options.createNew ?? false, - data, - ); -} - -async function writeFile( - path, - data, - options = {}, -) { - let cancelRid; - let abortHandler; - if (options.signal) { - options.signal.throwIfAborted(); - cancelRid = ops.op_cancel_handle(); - abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); - } - try { - if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) { - const file = await open(path, { - mode: options.mode, - append: options.append ?? false, - create: options.create ?? true, - createNew: options.createNew ?? false, - write: true, - }); - await data.pipeTo(file.writable, { - signal: options.signal, - }); - } else { - await core.opAsync( - "op_write_file_async", - pathFromURL(path), - options.mode, - options.append ?? false, - options.create ?? true, - options.createNew ?? false, - data, - cancelRid, - ); - } - } finally { - if (options.signal) { - options.signal[abortSignal.remove](abortHandler); - - // always throw the abort error when aborted - options.signal.throwIfAborted(); - } - } -} - -function writeTextFileSync( - path, - data, - options = {}, -) { - const encoder = new TextEncoder(); - return writeFileSync(path, encoder.encode(data), options); -} - -function writeTextFile( - path, - data, - options = {}, -) { - if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) { - return writeFile( - path, - data.pipeThrough(new TextEncoderStream()), - options, - ); - } else { - const encoder = new TextEncoder(); - return writeFile(path, encoder.encode(data), options); - } -} - -export { - chdir, - chmod, - chmodSync, - chown, - chownSync, - copyFile, - copyFileSync, - create, - createSync, - cwd, - fdatasync, - fdatasyncSync, - File, - flock, - flockSync, - FsFile, - fstat, - fstatSync, - fsync, - fsyncSync, - ftruncate, - ftruncateSync, - funlock, - funlockSync, - futime, - futimeSync, - link, - linkSync, - lstat, - lstatSync, - makeTempDir, - makeTempDirSync, - makeTempFile, - makeTempFileSync, - mkdir, - mkdirSync, - open, - openSync, - readDir, - readDirSync, - readFile, - readFileSync, - readLink, - readLinkSync, - readTextFile, - readTextFileSync, - realPath, - realPathSync, - remove, - removeSync, - rename, - renameSync, - seek, - seekSync, - stat, - statSync, - symlink, - symlinkSync, - truncate, - truncateSync, - umask, - utime, - utimeSync, - writeFile, - writeFileSync, - writeTextFile, - writeTextFileSync, -}; diff --git a/runtime/js/40_process.js b/runtime/js/40_process.js index 28bb2870c..661f972df 100644 --- a/runtime/js/40_process.js +++ b/runtime/js/40_process.js @@ -16,7 +16,7 @@ const { SymbolFor, Symbol, } = primordials; -import { FsFile } from "internal:runtime/30_fs.js"; +import { FsFile } from "internal:deno_fs/30_fs.js"; import { readAll } from "internal:deno_io/12_io.js"; import { assert, pathFromURL } from "internal:deno_web/00_infra.js"; import * as abortSignal from "internal:deno_web/03_abort_signal.js"; diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 45db05292..93d327787 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -15,7 +15,7 @@ import * as version from "internal:runtime/01_version.ts"; import * as permissions from "internal:runtime/10_permissions.js"; import * as io from "internal:deno_io/12_io.js"; import * as buffer from "internal:runtime/13_buffer.js"; -import * as fs from "internal:runtime/30_fs.js"; +import * as fs from "internal:deno_fs/30_fs.js"; import * as os from "internal:runtime/30_os.js"; import * as fsEvents from "internal:runtime/40_fs_events.js"; import * as process from "internal:runtime/40_process.js"; diff --git a/runtime/lib.rs b/runtime/lib.rs index 6bb84698d..97034bca9 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -8,6 +8,7 @@ pub use deno_crypto; pub use deno_fetch; pub use deno_ffi; pub use deno_flash; +pub use deno_fs; pub use deno_http; pub use deno_io; pub use deno_napi; diff --git a/runtime/ops/fs.rs b/runtime/ops/fs.rs deleted file mode 100644 index 546d1d68a..000000000 --- a/runtime/ops/fs.rs +++ /dev/null @@ -1,2214 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -// Some deserializer fields are only used on Unix and Windows build fails without it -use super::utils::into_string; -use crate::fs_util::canonicalize_path; -use deno_core::error::custom_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::op; -use deno_core::CancelFuture; -use deno_core::CancelHandle; -use deno_core::Extension; -use deno_core::OpState; -use deno_core::ResourceId; -use deno_core::ZeroCopyBuf; -use deno_crypto::rand::thread_rng; -use deno_crypto::rand::Rng; -use deno_io::StdFileResource; -use log::debug; -use serde::Deserialize; -use serde::Serialize; -use std::borrow::Cow; -use std::cell::RefCell; -use std::convert::From; -use std::env::current_dir; -use std::env::set_current_dir; -use std::env::temp_dir; -use std::io; -use std::io::Error; -use std::io::Seek; -use std::io::SeekFrom; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; -use std::rc::Rc; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; - -pub trait FsPermissions { - fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; - fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>; - fn check_read_blind( - &mut self, - p: &Path, - display: &str, - api_name: &str, - ) -> Result<(), AnyError>; - fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; - fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError>; -} - -#[cfg(not(unix))] -use deno_core::error::generic_error; -#[cfg(not(unix))] -use deno_core::error::not_supported; - -pub fn init() -> Extension { - Extension::builder("deno_fs") - .ops(vec![ - op_open_sync::decl::

(), - op_open_async::decl::

(), - op_write_file_sync::decl::

(), - op_write_file_async::decl::

(), - op_seek_sync::decl(), - op_seek_async::decl(), - op_fdatasync_sync::decl(), - op_fdatasync_async::decl(), - op_fsync_sync::decl(), - op_fsync_async::decl(), - op_fstat_sync::decl(), - op_fstat_async::decl(), - op_flock_sync::decl(), - op_flock_async::decl(), - op_funlock_sync::decl(), - op_funlock_async::decl(), - op_umask::decl(), - op_chdir::decl::

(), - op_mkdir_sync::decl::

(), - op_mkdir_async::decl::

(), - op_chmod_sync::decl::

(), - op_chmod_async::decl::

(), - op_chown_sync::decl::

(), - op_chown_async::decl::

(), - op_remove_sync::decl::

(), - op_remove_async::decl::

(), - op_copy_file_sync::decl::

(), - op_copy_file_async::decl::

(), - op_stat_sync::decl::

(), - op_stat_async::decl::

(), - op_realpath_sync::decl::

(), - op_realpath_async::decl::

(), - op_read_dir_sync::decl::

(), - op_read_dir_async::decl::

(), - op_rename_sync::decl::

(), - op_rename_async::decl::

(), - op_link_sync::decl::

(), - op_link_async::decl::

(), - op_symlink_sync::decl::

(), - op_symlink_async::decl::

(), - op_read_link_sync::decl::

(), - op_read_link_async::decl::

(), - op_ftruncate_sync::decl(), - op_ftruncate_async::decl(), - op_truncate_sync::decl::

(), - op_truncate_async::decl::

(), - op_make_temp_dir_sync::decl::

(), - op_make_temp_dir_async::decl::

(), - op_make_temp_file_sync::decl::

(), - op_make_temp_file_async::decl::

(), - op_cwd::decl::

(), - op_futime_sync::decl(), - op_futime_async::decl(), - op_utime_sync::decl::

(), - op_utime_async::decl::

(), - op_readfile_sync::decl::

(), - op_readfile_text_sync::decl::

(), - op_readfile_async::decl::

(), - op_readfile_text_async::decl::

(), - ]) - .build() -} - -fn default_err_mapper(err: Error, desc: String) -> Error { - Error::new(err.kind(), format!("{err}, {desc}")) -} - -#[derive(Deserialize, Default, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct OpenOptions { - read: bool, - write: bool, - create: bool, - truncate: bool, - append: bool, - create_new: bool, -} - -#[inline] -fn open_helper

( - state: &mut OpState, - path: &str, - mode: Option, - options: Option<&OpenOptions>, - api_name: &str, -) -> Result<(PathBuf, std::fs::OpenOptions), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(path).to_path_buf(); - - let mut open_options = std::fs::OpenOptions::new(); - - if let Some(mode) = mode { - // mode only used if creating the file on Unix - // if not specified, defaults to 0o666 - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - open_options.mode(mode & 0o777); - } - #[cfg(not(unix))] - let _ = mode; // avoid unused warning - } - - let permissions = state.borrow_mut::

(); - - match options { - None => { - permissions.check_read(&path, api_name)?; - open_options - .read(true) - .create(false) - .write(false) - .truncate(false) - .append(false) - .create_new(false); - } - Some(options) => { - if options.read { - permissions.check_read(&path, api_name)?; - } - - if options.write || options.append { - permissions.check_write(&path, api_name)?; - } - - open_options - .read(options.read) - .create(options.create) - .write(options.write) - .truncate(options.truncate) - .append(options.append) - .create_new(options.create_new); - } - } - - Ok((path, open_options)) -} - -#[op] -fn op_open_sync

( - state: &mut OpState, - path: String, - options: Option, - mode: Option, -) -> Result -where - P: FsPermissions + 'static, -{ - let (path, open_options) = - open_helper::

(state, &path, mode, options.as_ref(), "Deno.openSync()")?; - let std_file = open_options.open(&path).map_err(|err| { - default_err_mapper(err, format!("open '{}'", path.display())) - })?; - let resource = StdFileResource::fs_file(std_file); - let rid = state.resource_table.add(resource); - Ok(rid) -} - -#[op] -async fn op_open_async

( - state: Rc>, - path: String, - options: Option, - mode: Option, -) -> Result -where - P: FsPermissions + 'static, -{ - let (path, open_options) = open_helper::

( - &mut state.borrow_mut(), - &path, - mode, - options.as_ref(), - "Deno.open()", - )?; - let std_file = tokio::task::spawn_blocking(move || { - open_options.open(&path).map_err(|err| { - default_err_mapper(err, format!("open '{}'", path.display())) - }) - }) - .await?; - let resource = StdFileResource::fs_file(std_file?); - let rid = state.borrow_mut().resource_table.add(resource); - Ok(rid) -} - -#[inline] -fn write_open_options( - create: bool, - append: bool, - create_new: bool, -) -> OpenOptions { - OpenOptions { - read: false, - write: true, - create, - truncate: !append, - append, - create_new, - } -} - -#[op] -fn op_write_file_sync

( - state: &mut OpState, - path: String, - mode: Option, - append: bool, - create: bool, - create_new: bool, - data: ZeroCopyBuf, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let (path, open_options) = open_helper::

( - state, - &path, - mode, - Some(&write_open_options(create, append, create_new)), - "Deno.writeFileSync()", - )?; - write_file(&path, open_options, mode, data) -} - -#[op] -async fn op_write_file_async

( - state: Rc>, - path: String, - mode: Option, - append: bool, - create: bool, - create_new: bool, - data: ZeroCopyBuf, - cancel_rid: Option, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let (path, open_options) = open_helper::

( - &mut state.borrow_mut(), - &path, - mode, - Some(&write_open_options(create, append, create_new)), - "Deno.writeFile()", - )?; - - let write_future = tokio::task::spawn_blocking(move || { - write_file(&path, open_options, mode, data) - }); - - let cancel_handle = cancel_rid.and_then(|rid| { - state - .borrow_mut() - .resource_table - .get::(rid) - .ok() - }); - - if let Some(cancel_handle) = cancel_handle { - let write_future_rv = write_future.or_cancel(cancel_handle).await; - - if let Some(cancel_rid) = cancel_rid { - state.borrow_mut().resource_table.close(cancel_rid).ok(); - }; - - return write_future_rv??; - } - - write_future.await? -} - -fn write_file( - path: &Path, - open_options: std::fs::OpenOptions, - _mode: Option, - data: ZeroCopyBuf, -) -> Result<(), AnyError> { - let mut std_file = open_options.open(path).map_err(|err| { - default_err_mapper(err, format!("open '{}'", path.display())) - })?; - - // need to chmod the file if it already exists and a mode is specified - #[cfg(unix)] - if let Some(mode) = _mode { - use std::os::unix::fs::PermissionsExt; - let permissions = PermissionsExt::from_mode(mode & 0o777); - std_file.set_permissions(permissions).map_err(|err| { - default_err_mapper(err, format!("chmod '{}'", path.display())) - })?; - } - - std_file.write_all(&data)?; - Ok(()) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SeekArgs { - rid: ResourceId, - offset: i64, - whence: i32, -} - -fn seek_helper(args: SeekArgs) -> Result<(u32, SeekFrom), AnyError> { - let rid = args.rid; - let offset = args.offset; - let whence = args.whence as u32; - // Translate seek mode to Rust repr. - let seek_from = match whence { - 0 => SeekFrom::Start(offset as u64), - 1 => SeekFrom::Current(offset), - 2 => SeekFrom::End(offset), - _ => { - return Err(type_error(format!("Invalid seek mode: {whence}"))); - } - }; - - Ok((rid, seek_from)) -} - -#[op] -fn op_seek_sync(state: &mut OpState, args: SeekArgs) -> Result { - let (rid, seek_from) = seek_helper(args)?; - StdFileResource::with_file(state, rid, |std_file| { - std_file.seek(seek_from).map_err(AnyError::from) - }) -} - -#[op] -async fn op_seek_async( - state: Rc>, - args: SeekArgs, -) -> Result { - let (rid, seek_from) = seek_helper(args)?; - - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - std_file.seek(seek_from).map_err(AnyError::from) - }) - .await -} - -#[op] -fn op_fdatasync_sync( - state: &mut OpState, - rid: ResourceId, -) -> Result<(), AnyError> { - StdFileResource::with_file(state, rid, |std_file| { - std_file.sync_data().map_err(AnyError::from) - }) -} - -#[op] -async fn op_fdatasync_async( - state: Rc>, - rid: ResourceId, -) -> Result<(), AnyError> { - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - std_file.sync_data().map_err(AnyError::from) - }) - .await -} - -#[op] -fn op_fsync_sync(state: &mut OpState, rid: ResourceId) -> Result<(), AnyError> { - StdFileResource::with_file(state, rid, |std_file| { - std_file.sync_all().map_err(AnyError::from) - }) -} - -#[op] -async fn op_fsync_async( - state: Rc>, - rid: ResourceId, -) -> Result<(), AnyError> { - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - std_file.sync_all().map_err(AnyError::from) - }) - .await -} - -#[op] -fn op_fstat_sync( - state: &mut OpState, - rid: ResourceId, - out_buf: &mut [u32], -) -> Result<(), AnyError> { - let metadata = StdFileResource::with_file(state, rid, |std_file| { - std_file.metadata().map_err(AnyError::from) - })?; - let stat = get_stat(metadata); - stat.write(out_buf); - Ok(()) -} - -#[op] -async fn op_fstat_async( - state: Rc>, - rid: ResourceId, -) -> Result { - let metadata = - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - std_file.metadata().map_err(AnyError::from) - }) - .await?; - Ok(get_stat(metadata)) -} - -#[op] -fn op_flock_sync( - state: &mut OpState, - rid: ResourceId, - exclusive: bool, -) -> Result<(), AnyError> { - use fs3::FileExt; - super::check_unstable(state, "Deno.flockSync"); - - StdFileResource::with_file(state, rid, |std_file| { - if exclusive { - std_file.lock_exclusive()?; - } else { - std_file.lock_shared()?; - } - Ok(()) - }) -} - -#[op] -async fn op_flock_async( - state: Rc>, - rid: ResourceId, - exclusive: bool, -) -> Result<(), AnyError> { - use fs3::FileExt; - super::check_unstable2(&state, "Deno.flock"); - - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - if exclusive { - std_file.lock_exclusive()?; - } else { - std_file.lock_shared()?; - } - Ok(()) - }) - .await -} - -#[op] -fn op_funlock_sync( - state: &mut OpState, - rid: ResourceId, -) -> Result<(), AnyError> { - use fs3::FileExt; - super::check_unstable(state, "Deno.funlockSync"); - - StdFileResource::with_file(state, rid, |std_file| { - std_file.unlock()?; - Ok(()) - }) -} - -#[op] -async fn op_funlock_async( - state: Rc>, - rid: ResourceId, -) -> Result<(), AnyError> { - use fs3::FileExt; - super::check_unstable2(&state, "Deno.funlock"); - - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - std_file.unlock()?; - Ok(()) - }) - .await -} - -#[op] -fn op_umask(state: &mut OpState, mask: Option) -> Result { - super::check_unstable(state, "Deno.umask"); - // TODO implement umask for Windows - // see https://github.com/nodejs/node/blob/master/src/node_process_methods.cc - // and https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/reference/umask?view=vs-2019 - #[cfg(not(unix))] - { - let _ = mask; // avoid unused warning. - Err(not_supported()) - } - #[cfg(unix)] - { - use nix::sys::stat::mode_t; - use nix::sys::stat::umask; - use nix::sys::stat::Mode; - let r = if let Some(mask) = mask { - // If mask provided, return previous. - umask(Mode::from_bits_truncate(mask as mode_t)) - } else { - // If no mask provided, we query the current. Requires two syscalls. - let prev = umask(Mode::from_bits_truncate(0o777)); - let _ = umask(prev); - prev - }; - #[cfg(target_os = "linux")] - { - Ok(r.bits()) - } - #[cfg(target_os = "macos")] - { - Ok(r.bits() as u32) - } - } -} - -#[op] -fn op_chdir

(state: &mut OpState, directory: &str) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let d = PathBuf::from(&directory); - state.borrow_mut::

().check_read(&d, "Deno.chdir()")?; - set_current_dir(&d) - .map_err(|err| default_err_mapper(err, format!("chdir '{directory}'")))?; - Ok(()) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MkdirArgs { - path: String, - recursive: bool, - mode: Option, -} - -#[op] -fn op_mkdir_sync

( - state: &mut OpState, - args: MkdirArgs, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(&args.path).to_path_buf(); - let mode = args.mode.unwrap_or(0o777) & 0o777; - state - .borrow_mut::

() - .check_write(&path, "Deno.mkdirSync()")?; - debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); - let mut builder = std::fs::DirBuilder::new(); - builder.recursive(args.recursive); - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - builder.mode(mode); - } - builder.create(&path).map_err(|err| { - default_err_mapper(err, format!("mkdir '{}'", path.display())) - })?; - Ok(()) -} - -#[op] -async fn op_mkdir_async

( - state: Rc>, - args: MkdirArgs, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(&args.path).to_path_buf(); - let mode = args.mode.unwrap_or(0o777) & 0o777; - - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.mkdir()")?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_mkdir {} {:o} {}", path.display(), mode, args.recursive); - let mut builder = std::fs::DirBuilder::new(); - builder.recursive(args.recursive); - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - builder.mode(mode); - } - builder.create(&path).map_err(|err| { - default_err_mapper(err, format!("mkdir '{}'", path.display())) - })?; - Ok(()) - }) - .await - .unwrap() -} - -#[op] -fn op_chmod_sync

( - state: &mut OpState, - path: &str, - mode: u32, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(path); - let mode = mode & 0o777; - - state - .borrow_mut::

() - .check_write(path, "Deno.chmodSync()")?; - raw_chmod(path, mode) -} - -#[op] -async fn op_chmod_async

( - state: Rc>, - path: String, - mode: u32, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(&path).to_path_buf(); - let mode = mode & 0o777; - - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.chmod()")?; - } - - tokio::task::spawn_blocking(move || raw_chmod(&path, mode)) - .await - .unwrap() -} - -fn raw_chmod(path: &Path, _raw_mode: u32) -> Result<(), AnyError> { - let err_mapper = - |err| default_err_mapper(err, format!("chmod '{}'", path.display())); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let permissions = PermissionsExt::from_mode(_raw_mode); - std::fs::set_permissions(path, permissions).map_err(err_mapper)?; - Ok(()) - } - // TODO Implement chmod for Windows (#4357) - #[cfg(not(unix))] - { - // Still check file/dir exists on Windows - let _metadata = std::fs::metadata(path).map_err(err_mapper)?; - Err(not_supported()) - } -} - -#[op] -fn op_chown_sync

( - state: &mut OpState, - path: &str, - #[cfg_attr(windows, allow(unused_variables))] uid: Option, - #[cfg_attr(windows, allow(unused_variables))] gid: Option, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(path).to_path_buf(); - state - .borrow_mut::

() - .check_write(&path, "Deno.chownSync()")?; - #[cfg(unix)] - { - use crate::errors::get_nix_error_class; - use nix::unistd::chown; - use nix::unistd::Gid; - use nix::unistd::Uid; - let nix_uid = uid.map(Uid::from_raw); - let nix_gid = gid.map(Gid::from_raw); - chown(&path, nix_uid, nix_gid).map_err(|err| { - custom_error( - get_nix_error_class(&err), - format!("{}, chown '{}'", err.desc(), path.display()), - ) - })?; - Ok(()) - } - // TODO Implement chown for Windows - #[cfg(not(unix))] - { - Err(generic_error("Not implemented")) - } -} - -#[op] -async fn op_chown_async

( - state: Rc>, - path: String, - #[cfg_attr(windows, allow(unused_variables))] uid: Option, - #[cfg_attr(windows, allow(unused_variables))] gid: Option, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = Path::new(&path).to_path_buf(); - - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.chown()")?; - } - - tokio::task::spawn_blocking(move || { - #[cfg(unix)] - { - use crate::errors::get_nix_error_class; - use nix::unistd::chown; - use nix::unistd::Gid; - use nix::unistd::Uid; - let nix_uid = uid.map(Uid::from_raw); - let nix_gid = gid.map(Gid::from_raw); - chown(&path, nix_uid, nix_gid).map_err(|err| { - custom_error( - get_nix_error_class(&err), - format!("{}, chown '{}'", err.desc(), path.display()), - ) - })?; - Ok(()) - } - // TODO Implement chown for Windows - #[cfg(not(unix))] - Err(not_supported()) - }) - .await - .unwrap() -} - -#[op] -fn op_remove_sync

( - state: &mut OpState, - path: &str, - recursive: bool, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(path); - - state - .borrow_mut::

() - .check_write(&path, "Deno.removeSync()")?; - - #[cfg(not(unix))] - use std::os::windows::prelude::MetadataExt; - - let err_mapper = - |err| default_err_mapper(err, format!("remove '{}'", path.display())); - let metadata = std::fs::symlink_metadata(&path).map_err(err_mapper)?; - - let file_type = metadata.file_type(); - if file_type.is_file() { - std::fs::remove_file(&path).map_err(err_mapper)?; - } else if recursive { - std::fs::remove_dir_all(&path).map_err(err_mapper)?; - } else if file_type.is_symlink() { - #[cfg(unix)] - std::fs::remove_file(&path).map_err(err_mapper)?; - #[cfg(not(unix))] - { - use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; - if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { - std::fs::remove_dir(&path).map_err(err_mapper)?; - } else { - std::fs::remove_file(&path).map_err(err_mapper)?; - } - } - } else if file_type.is_dir() { - std::fs::remove_dir(&path).map_err(err_mapper)?; - } else { - // pipes, sockets, etc... - std::fs::remove_file(&path).map_err(err_mapper)?; - } - Ok(()) -} - -#[op] -async fn op_remove_async

( - state: Rc>, - path: String, - recursive: bool, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - - { - let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_write(&path, "Deno.remove()")?; - } - - tokio::task::spawn_blocking(move || { - #[cfg(not(unix))] - use std::os::windows::prelude::MetadataExt; - let err_mapper = - |err| default_err_mapper(err, format!("remove '{}'", path.display())); - let metadata = std::fs::symlink_metadata(&path).map_err(err_mapper)?; - - debug!("op_remove_async {} {}", path.display(), recursive); - let file_type = metadata.file_type(); - if file_type.is_file() { - std::fs::remove_file(&path).map_err(err_mapper)?; - } else if recursive { - std::fs::remove_dir_all(&path).map_err(err_mapper)?; - } else if file_type.is_symlink() { - #[cfg(unix)] - std::fs::remove_file(&path).map_err(err_mapper)?; - #[cfg(not(unix))] - { - use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY; - if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { - std::fs::remove_dir(&path).map_err(err_mapper)?; - } else { - std::fs::remove_file(&path).map_err(err_mapper)?; - } - } - } else if file_type.is_dir() { - std::fs::remove_dir(&path).map_err(err_mapper)?; - } else { - // pipes, sockets, etc... - std::fs::remove_file(&path).map_err(err_mapper)?; - } - Ok(()) - }) - .await - .unwrap() -} - -#[op] -fn op_copy_file_sync

( - state: &mut OpState, - from: &str, - to: &str, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let from_path = PathBuf::from(from); - let to_path = PathBuf::from(to); - - let permissions = state.borrow_mut::

(); - permissions.check_read(&from_path, "Deno.copyFileSync()")?; - permissions.check_write(&to_path, "Deno.copyFileSync()")?; - - // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput - // See https://github.com/rust-lang/rust/issues/54800 - // Once the issue is resolved, we should remove this workaround. - if cfg!(unix) && !from_path.is_file() { - return Err(custom_error( - "NotFound", - format!( - "File not found, copy '{}' -> '{}'", - from_path.display(), - to_path.display() - ), - )); - } - - let err_mapper = |err| { - default_err_mapper( - err, - format!("copy '{}' -> '{}'", from_path.display(), to_path.display()), - ) - }; - - #[cfg(target_os = "macos")] - { - use libc::clonefile; - use libc::stat; - use libc::unlink; - use std::ffi::CString; - use std::io::Read; - use std::os::unix::fs::OpenOptionsExt; - use std::os::unix::fs::PermissionsExt; - - let from = CString::new(from).unwrap(); - let to = CString::new(to).unwrap(); - - // SAFETY: `from` and `to` are valid C strings. - // std::fs::copy does open() + fcopyfile() on macOS. We try to use - // clonefile() instead, which is more efficient. - unsafe { - let mut st = std::mem::zeroed(); - let ret = stat(from.as_ptr(), &mut st); - if ret != 0 { - return Err(err_mapper(Error::last_os_error()).into()); - } - - if st.st_size > 128 * 1024 { - // Try unlink. If it fails, we are going to try clonefile() anyway. - let _ = unlink(to.as_ptr()); - // Matches rust stdlib behavior for io::copy. - // https://github.com/rust-lang/rust/blob/3fdd578d72a24d4efc2fe2ad18eec3b6ba72271e/library/std/src/sys/unix/fs.rs#L1613-L1616 - if clonefile(from.as_ptr(), to.as_ptr(), 0) == 0 { - return Ok(()); - } - } else { - // Do a regular copy. fcopyfile() is an overkill for < 128KB - // files. - let mut buf = [0u8; 128 * 1024]; - let mut from_file = - std::fs::File::open(&from_path).map_err(err_mapper)?; - let perm = from_file.metadata().map_err(err_mapper)?.permissions(); - - let mut to_file = std::fs::OpenOptions::new() - // create the file with the correct mode right away - .mode(perm.mode()) - .write(true) - .create(true) - .truncate(true) - .open(&to_path) - .map_err(err_mapper)?; - let writer_metadata = to_file.metadata()?; - if writer_metadata.is_file() { - // Set the correct file permissions, in case the file already existed. - // Don't set the permissions on already existing non-files like - // pipes/FIFOs or device nodes. - to_file.set_permissions(perm)?; - } - loop { - let nread = from_file.read(&mut buf).map_err(err_mapper)?; - if nread == 0 { - break; - } - to_file.write_all(&buf[..nread]).map_err(err_mapper)?; - } - return Ok(()); - } - } - - // clonefile() failed, fall back to std::fs::copy(). - } - - // returns size of from as u64 (we ignore) - std::fs::copy(&from_path, &to_path).map_err(err_mapper)?; - Ok(()) -} - -#[op] -async fn op_copy_file_async

( - state: Rc>, - from: String, - to: String, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let from = PathBuf::from(&from); - let to = PathBuf::from(&to); - - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::

(); - permissions.check_read(&from, "Deno.copyFile()")?; - permissions.check_write(&to, "Deno.copyFile()")?; - } - - tokio::task::spawn_blocking(move || { - // On *nix, Rust reports non-existent `from` as ErrorKind::InvalidInput - // See https://github.com/rust-lang/rust/issues/54800 - // Once the issue is resolved, we should remove this workaround. - if cfg!(unix) && !from.is_file() { - return Err(custom_error( - "NotFound", - format!( - "File not found, copy '{}' -> '{}'", - from.display(), - to.display() - ), - )); - } - // returns size of from as u64 (we ignore) - std::fs::copy(&from, &to).map_err(|err| { - default_err_mapper( - err, - format!("copy '{}' -> '{}'", from.display(), to.display()), - ) - })?; - Ok(()) - }) - .await - .unwrap() -} - -fn to_msec(maybe_time: Result) -> (u64, bool) { - match maybe_time { - Ok(time) => ( - time - .duration_since(UNIX_EPOCH) - .map(|t| t.as_millis() as u64) - .unwrap_or_else(|err| err.duration().as_millis() as u64), - true, - ), - Err(_) => (0, false), - } -} - -macro_rules! create_struct_writer { - (pub struct $name:ident { $($field:ident: $type:ty),* $(,)? }) => { - impl $name { - fn write(self, buf: &mut [u32]) { - let mut offset = 0; - $( - let value = self.$field as u64; - buf[offset] = value as u32; - buf[offset + 1] = (value >> 32) as u32; - #[allow(unused_assignments)] - { - offset += 2; - } - )* - } - } - - #[derive(Serialize)] - #[serde(rename_all = "camelCase")] - struct $name { - $($field: $type),* - } - }; -} - -create_struct_writer! { - pub struct FsStat { - is_file: bool, - is_directory: bool, - is_symlink: bool, - size: u64, - // In milliseconds, like JavaScript. Available on both Unix or Windows. - mtime_set: bool, - mtime: u64, - atime_set: bool, - atime: u64, - birthtime_set: bool, - birthtime: u64, - // Following are only valid under Unix. - dev: u64, - ino: u64, - mode: u32, - nlink: u64, - uid: u32, - gid: u32, - rdev: u64, - blksize: u64, - blocks: u64, - } -} - -#[inline(always)] -fn get_stat(metadata: std::fs::Metadata) -> FsStat { - // Unix stat member (number types only). 0 if not on unix. - macro_rules! usm { - ($member:ident) => {{ - #[cfg(unix)] - { - metadata.$member() - } - #[cfg(not(unix))] - { - 0 - } - }}; - } - - #[cfg(unix)] - use std::os::unix::fs::MetadataExt; - let (mtime, mtime_set) = to_msec(metadata.modified()); - let (atime, atime_set) = to_msec(metadata.accessed()); - let (birthtime, birthtime_set) = to_msec(metadata.created()); - - FsStat { - is_file: metadata.is_file(), - is_directory: metadata.is_dir(), - is_symlink: metadata.file_type().is_symlink(), - size: metadata.len(), - // In milliseconds, like JavaScript. Available on both Unix or Windows. - mtime_set, - mtime, - atime_set, - atime, - birthtime_set, - birthtime, - // Following are only valid under Unix. - dev: usm!(dev), - ino: usm!(ino), - mode: usm!(mode), - nlink: usm!(nlink), - uid: usm!(uid), - gid: usm!(gid), - rdev: usm!(rdev), - blksize: usm!(blksize), - blocks: usm!(blocks), - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StatArgs { - path: String, - lstat: bool, -} - -#[op] -fn op_stat_sync

( - state: &mut OpState, - path: &str, - lstat: bool, - out_buf: &mut [u32], -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(path); - state - .borrow_mut::

() - .check_read(&path, "Deno.statSync()")?; - let err_mapper = - |err| default_err_mapper(err, format!("stat '{}'", path.display())); - let metadata = if lstat { - std::fs::symlink_metadata(&path).map_err(err_mapper)? - } else { - std::fs::metadata(&path).map_err(err_mapper)? - }; - - let stat = get_stat(metadata); - stat.write(out_buf); - - Ok(()) -} - -#[op] -async fn op_stat_async

( - state: Rc>, - args: StatArgs, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&args.path); - let lstat = args.lstat; - - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_read(&path, "Deno.stat()")?; - } - - tokio::task::spawn_blocking(move || { - debug!("op_stat_async {} {}", path.display(), lstat); - let err_mapper = - |err| default_err_mapper(err, format!("stat '{}'", path.display())); - let metadata = if lstat { - std::fs::symlink_metadata(&path).map_err(err_mapper)? - } else { - std::fs::metadata(&path).map_err(err_mapper)? - }; - Ok(get_stat(metadata)) - }) - .await - .unwrap() -} - -#[op] -fn op_realpath_sync

( - state: &mut OpState, - path: String, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - - let permissions = state.borrow_mut::

(); - permissions.check_read(&path, "Deno.realPathSync()")?; - if path.is_relative() { - permissions.check_read_blind( - ¤t_dir()?, - "CWD", - "Deno.realPathSync()", - )?; - } - - debug!("op_realpath_sync {}", path.display()); - // corresponds to the realpath on Unix and - // CreateFile and GetFinalPathNameByHandle on Windows - let realpath = canonicalize_path(&path)?; - let realpath_str = into_string(realpath.into_os_string())?; - Ok(realpath_str) -} - -#[op] -async fn op_realpath_async

( - state: Rc>, - path: String, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::

(); - permissions.check_read(&path, "Deno.realPath()")?; - if path.is_relative() { - permissions.check_read_blind( - ¤t_dir()?, - "CWD", - "Deno.realPath()", - )?; - } - } - - tokio::task::spawn_blocking(move || { - debug!("op_realpath_async {}", path.display()); - // corresponds to the realpath on Unix and - // CreateFile and GetFinalPathNameByHandle on Windows - let realpath = canonicalize_path(&path)?; - let realpath_str = into_string(realpath.into_os_string())?; - Ok(realpath_str) - }) - .await - .unwrap() -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DirEntry { - name: String, - is_file: bool, - is_directory: bool, - is_symlink: bool, -} - -#[op] -fn op_read_dir_sync

( - state: &mut OpState, - path: String, -) -> Result, AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - - state - .borrow_mut::

() - .check_read(&path, "Deno.readDirSync()")?; - - debug!("op_read_dir_sync {}", path.display()); - - let entries: Vec<_> = std::fs::read_dir(&path) - .map_err(|err| { - default_err_mapper(err, format!("readdir '{}'", path.display())) - })? - .filter_map(|entry| { - let entry = entry.unwrap(); - // Not all filenames can be encoded as UTF-8. Skip those for now. - if let Ok(name) = into_string(entry.file_name()) { - Some(DirEntry { - name, - is_file: entry - .file_type() - .map_or(false, |file_type| file_type.is_file()), - is_directory: entry - .file_type() - .map_or(false, |file_type| file_type.is_dir()), - is_symlink: entry - .file_type() - .map_or(false, |file_type| file_type.is_symlink()), - }) - } else { - None - } - }) - .collect(); - - Ok(entries) -} - -#[op] -async fn op_read_dir_async

( - state: Rc>, - path: String, -) -> Result, AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - { - let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_read(&path, "Deno.readDir()")?; - } - tokio::task::spawn_blocking(move || { - debug!("op_read_dir_async {}", path.display()); - - let entries: Vec<_> = std::fs::read_dir(&path) - .map_err(|err| { - default_err_mapper(err, format!("readdir '{}'", path.display())) - })? - .filter_map(|entry| { - let entry = entry.unwrap(); - // Not all filenames can be encoded as UTF-8. Skip those for now. - if let Ok(name) = into_string(entry.file_name()) { - Some(DirEntry { - name, - is_file: entry - .file_type() - .map_or(false, |file_type| file_type.is_file()), - is_directory: entry - .file_type() - .map_or(false, |file_type| file_type.is_dir()), - is_symlink: entry - .file_type() - .map_or(false, |file_type| file_type.is_symlink()), - }) - } else { - None - } - }) - .collect(); - - Ok(entries) - }) - .await - .unwrap() -} - -#[op] -fn op_rename_sync

( - state: &mut OpState, - oldpath: &str, - newpath: &str, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let oldpath = PathBuf::from(oldpath); - let newpath = PathBuf::from(newpath); - - let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.renameSync()")?; - permissions.check_write(&oldpath, "Deno.renameSync()")?; - permissions.check_write(&newpath, "Deno.renameSync()")?; - - std::fs::rename(&oldpath, &newpath).map_err(|err| { - default_err_mapper( - err, - format!("rename '{}' -> '{}'", oldpath.display(), newpath.display()), - ) - })?; - Ok(()) -} - -#[op] -async fn op_rename_async

( - state: Rc>, - oldpath: String, - newpath: String, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let oldpath = PathBuf::from(&oldpath); - let newpath = PathBuf::from(&newpath); - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.rename()")?; - permissions.check_write(&oldpath, "Deno.rename()")?; - permissions.check_write(&newpath, "Deno.rename()")?; - } - - tokio::task::spawn_blocking(move || { - std::fs::rename(&oldpath, &newpath).map_err(|err| { - default_err_mapper( - err, - format!("rename '{}' -> '{}'", oldpath.display(), newpath.display()), - ) - })?; - Ok(()) - }) - .await - .unwrap() -} - -#[op] -fn op_link_sync

( - state: &mut OpState, - oldpath: &str, - newpath: &str, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let oldpath = PathBuf::from(oldpath); - let newpath = PathBuf::from(newpath); - - let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.linkSync()")?; - permissions.check_write(&oldpath, "Deno.linkSync()")?; - permissions.check_read(&newpath, "Deno.linkSync()")?; - permissions.check_write(&newpath, "Deno.linkSync()")?; - - std::fs::hard_link(&oldpath, &newpath).map_err(|err| { - default_err_mapper( - err, - format!("link '{}' -> '{}'", oldpath.display(), newpath.display()), - ) - })?; - Ok(()) -} - -#[op] -async fn op_link_async

( - state: Rc>, - oldpath: String, - newpath: String, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let oldpath = PathBuf::from(&oldpath); - let newpath = PathBuf::from(&newpath); - - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.link()")?; - permissions.check_write(&oldpath, "Deno.link()")?; - permissions.check_read(&newpath, "Deno.link()")?; - permissions.check_write(&newpath, "Deno.link()")?; - } - - tokio::task::spawn_blocking(move || { - let err_mapper = |err| { - default_err_mapper( - err, - format!("link '{}' -> '{}'", oldpath.display(), newpath.display()), - ) - }; - std::fs::hard_link(&oldpath, &newpath).map_err(err_mapper)?; - Ok(()) - }) - .await - .unwrap() -} - -#[op] -fn op_symlink_sync

( - state: &mut OpState, - oldpath: &str, - newpath: &str, - _type: Option, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let oldpath = PathBuf::from(oldpath); - let newpath = PathBuf::from(newpath); - - state - .borrow_mut::

() - .check_write_all("Deno.symlinkSync()")?; - state - .borrow_mut::

() - .check_read_all("Deno.symlinkSync()")?; - - let err_mapper = |err| { - default_err_mapper( - err, - format!("symlink '{}' -> '{}'", oldpath.display(), newpath.display()), - ) - }; - - #[cfg(unix)] - { - use std::os::unix::fs::symlink; - symlink(&oldpath, &newpath).map_err(err_mapper)?; - Ok(()) - } - #[cfg(not(unix))] - { - use std::os::windows::fs::symlink_dir; - use std::os::windows::fs::symlink_file; - - match _type { - Some(ty) => match ty.as_ref() { - "file" => symlink_file(&oldpath, &newpath).map_err(err_mapper)?, - "dir" => symlink_dir(&oldpath, &newpath).map_err(err_mapper)?, - _ => return Err(type_error("unsupported type")), - }, - None => { - let old_meta = std::fs::metadata(&oldpath); - match old_meta { - Ok(metadata) => { - if metadata.is_file() { - symlink_file(&oldpath, &newpath).map_err(err_mapper)? - } else if metadata.is_dir() { - symlink_dir(&oldpath, &newpath).map_err(err_mapper)? - } - } - Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), - } - } - }; - Ok(()) - } -} - -#[op] -async fn op_symlink_async

( - state: Rc>, - oldpath: String, - newpath: String, - _type: Option, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let oldpath = PathBuf::from(&oldpath); - let newpath = PathBuf::from(&newpath); - - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write_all("Deno.symlink()")?; - state.borrow_mut::

().check_read_all("Deno.symlink()")?; - } - - tokio::task::spawn_blocking(move || { - let err_mapper = |err| default_err_mapper(err, format!( - "symlink '{}' -> '{}'", - oldpath.display(), - newpath.display() - )); - - #[cfg(unix)] - { - use std::os::unix::fs::symlink; - symlink(&oldpath, &newpath).map_err(err_mapper)?; - Ok(()) - } - #[cfg(not(unix))] - { - use std::os::windows::fs::{symlink_dir, symlink_file}; - - match _type { - Some(ty) => match ty.as_ref() { - "file" => symlink_file(&oldpath, &newpath).map_err(err_mapper)?, - "dir" => symlink_dir(&oldpath, &newpath).map_err(err_mapper)?, - _ => return Err(type_error("unsupported type")), - }, - None => { - let old_meta = std::fs::metadata(&oldpath); - match old_meta { - Ok(metadata) => { - if metadata.is_file() { - symlink_file(&oldpath, &newpath).map_err(err_mapper)? - } else if metadata.is_dir() { - symlink_dir(&oldpath, &newpath).map_err(err_mapper)? - } - } - Err(_) => return Err(type_error("you must pass a `options` argument for non-existent target path in windows".to_string())), - } - } - }; - Ok(()) - } - }) - .await - .unwrap() -} - -#[op] -fn op_read_link_sync

( - state: &mut OpState, - path: String, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - - state - .borrow_mut::

() - .check_read(&path, "Deno.readLink()")?; - - debug!("op_read_link_value {}", path.display()); - let target = std::fs::read_link(&path) - .map_err(|err| { - default_err_mapper(err, format!("readlink '{}'", path.display())) - })? - .into_os_string(); - let targetstr = into_string(target)?; - Ok(targetstr) -} - -#[op] -async fn op_read_link_async

( - state: Rc>, - path: String, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - { - let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_read(&path, "Deno.readLink()")?; - } - tokio::task::spawn_blocking(move || { - debug!("op_read_link_async {}", path.display()); - let target = std::fs::read_link(&path) - .map_err(|err| { - default_err_mapper(err, format!("readlink '{}'", path.display())) - })? - .into_os_string(); - let targetstr = into_string(target)?; - Ok(targetstr) - }) - .await - .unwrap() -} - -#[op] -fn op_ftruncate_sync( - state: &mut OpState, - rid: u32, - len: i32, -) -> Result<(), AnyError> { - let len = len as u64; - StdFileResource::with_file(state, rid, |std_file| { - std_file.set_len(len).map_err(AnyError::from) - })?; - Ok(()) -} - -#[op] -async fn op_ftruncate_async( - state: Rc>, - rid: ResourceId, - len: i32, -) -> Result<(), AnyError> { - let len = len as u64; - - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - std_file.set_len(len)?; - Ok(()) - }) - .await -} - -#[op] -fn op_truncate_sync

( - state: &mut OpState, - path: &str, - len: u64, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(path); - - state - .borrow_mut::

() - .check_write(&path, "Deno.truncateSync()")?; - - debug!("op_truncate_sync {} {}", path.display(), len); - let err_mapper = - |err| default_err_mapper(err, format!("truncate '{}'", path.display())); - let f = std::fs::OpenOptions::new() - .write(true) - .open(&path) - .map_err(err_mapper)?; - f.set_len(len).map_err(err_mapper)?; - Ok(()) -} - -#[op] -async fn op_truncate_async

( - state: Rc>, - path: String, - len: u64, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - - { - let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_write(&path, "Deno.truncate()")?; - } - tokio::task::spawn_blocking(move || { - debug!("op_truncate_async {} {}", path.display(), len); - let err_mapper = - |err| default_err_mapper(err, format!("truncate '{}'", path.display())); - let f = std::fs::OpenOptions::new() - .write(true) - .open(&path) - .map_err(err_mapper)?; - f.set_len(len).map_err(err_mapper)?; - Ok(()) - }) - .await - .unwrap() -} - -fn make_temp( - dir: Option<&Path>, - prefix: Option<&str>, - suffix: Option<&str>, - is_dir: bool, -) -> std::io::Result { - let prefix_ = prefix.unwrap_or(""); - let suffix_ = suffix.unwrap_or(""); - let mut buf: PathBuf = match dir { - Some(p) => p.to_path_buf(), - None => temp_dir(), - } - .join("_"); - let mut rng = thread_rng(); - loop { - let unique = rng.gen::(); - buf.set_file_name(format!("{prefix_}{unique:08x}{suffix_}")); - let r = if is_dir { - #[allow(unused_mut)] - let mut builder = std::fs::DirBuilder::new(); - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - builder.mode(0o700); - } - builder.create(buf.as_path()) - } else { - let mut open_options = std::fs::OpenOptions::new(); - open_options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - open_options.mode(0o600); - } - open_options.open(buf.as_path())?; - Ok(()) - }; - match r { - Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, - Ok(_) => return Ok(buf), - Err(e) => return Err(e), - } - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MakeTempArgs { - dir: Option, - prefix: Option, - suffix: Option, -} - -#[op] -fn op_make_temp_dir_sync

( - state: &mut OpState, - args: MakeTempArgs, -) -> Result -where - P: FsPermissions + 'static, -{ - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - - state.borrow_mut::

().check_write( - dir.clone().unwrap_or_else(temp_dir).as_path(), - "Deno.makeTempDirSync()", - )?; - - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - true, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(path_str) -} - -#[op] -async fn op_make_temp_dir_async

( - state: Rc>, - args: MakeTempArgs, -) -> Result -where - P: FsPermissions + 'static, -{ - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write( - dir.clone().unwrap_or_else(temp_dir).as_path(), - "Deno.makeTempDir()", - )?; - } - tokio::task::spawn_blocking(move || { - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - true, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(path_str) - }) - .await - .unwrap() -} - -#[op] -fn op_make_temp_file_sync

( - state: &mut OpState, - args: MakeTempArgs, -) -> Result -where - P: FsPermissions + 'static, -{ - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - - state.borrow_mut::

().check_write( - dir.clone().unwrap_or_else(temp_dir).as_path(), - "Deno.makeTempFileSync()", - )?; - - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - false, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(path_str) -} - -#[op] -async fn op_make_temp_file_async

( - state: Rc>, - args: MakeTempArgs, -) -> Result -where - P: FsPermissions + 'static, -{ - let dir = args.dir.map(|s| PathBuf::from(&s)); - let prefix = args.prefix.map(String::from); - let suffix = args.suffix.map(String::from); - { - let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write( - dir.clone().unwrap_or_else(temp_dir).as_path(), - "Deno.makeTempFile()", - )?; - } - tokio::task::spawn_blocking(move || { - // TODO(piscisaureus): use byte vector for paths, not a string. - // See https://github.com/denoland/deno/issues/627. - // We can't assume that paths are always valid utf8 strings. - let path = make_temp( - // Converting Option to Option<&str> - dir.as_deref(), - prefix.as_deref(), - suffix.as_deref(), - false, - )?; - let path_str = into_string(path.into_os_string())?; - - Ok(path_str) - }) - .await - .unwrap() -} - -#[op] -fn op_futime_sync( - state: &mut OpState, - rid: ResourceId, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, -) -> Result<(), AnyError> { - let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); - let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); - - StdFileResource::with_file(state, rid, |std_file| { - filetime::set_file_handle_times(std_file, Some(atime), Some(mtime)) - .map_err(AnyError::from) - })?; - - Ok(()) -} - -#[op] -async fn op_futime_async( - state: Rc>, - rid: ResourceId, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, -) -> Result<(), AnyError> { - let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); - let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); - - StdFileResource::with_file_blocking_task(state, rid, move |std_file| { - filetime::set_file_handle_times(std_file, Some(atime), Some(mtime))?; - Ok(()) - }) - .await -} - -#[op] -fn op_utime_sync

( - state: &mut OpState, - path: &str, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(path); - let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); - let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); - - state.borrow_mut::

().check_write(&path, "Deno.utime()")?; - filetime::set_file_times(&path, atime, mtime).map_err(|err| { - default_err_mapper(err, format!("utime '{}'", path.display())) - })?; - Ok(()) -} - -#[op] -async fn op_utime_async

( - state: Rc>, - path: String, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, -) -> Result<(), AnyError> -where - P: FsPermissions + 'static, -{ - let path = PathBuf::from(&path); - let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); - let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); - - state - .borrow_mut() - .borrow_mut::

() - .check_write(&path, "Deno.utime()")?; - - tokio::task::spawn_blocking(move || { - filetime::set_file_times(&path, atime, mtime).map_err(|err| { - default_err_mapper(err, format!("utime '{}'", path.display())) - })?; - Ok(()) - }) - .await - .unwrap() -} - -#[op] -fn op_cwd

(state: &mut OpState) -> Result -where - P: FsPermissions + 'static, -{ - let path = current_dir()?; - state - .borrow_mut::

() - .check_read_blind(&path, "CWD", "Deno.cwd()")?; - let path_str = into_string(path.into_os_string())?; - Ok(path_str) -} - -#[op] -fn op_readfile_sync

( - state: &mut OpState, - path: String, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = Path::new(&path); - state - .borrow_mut::

() - .check_read(path, "Deno.readFileSync()")?; - Ok( - std::fs::read(path) - .map_err(|err| { - default_err_mapper(err, format!("readfile '{}'", path.display())) - })? - .into(), - ) -} - -#[op] -fn op_readfile_text_sync

( - state: &mut OpState, - path: String, -) -> Result -where - P: FsPermissions + 'static, -{ - let path = Path::new(&path); - state - .borrow_mut::

() - .check_read(path, "Deno.readTextFileSync()")?; - Ok(string_from_utf8_lossy(std::fs::read(path).map_err( - |err| default_err_mapper(err, format!("readfile '{}'", path.display())), - )?)) -} - -#[op] -async fn op_readfile_async

( - state: Rc>, - path: String, - cancel_rid: Option, -) -> Result -where - P: FsPermissions + 'static, -{ - { - let path = Path::new(&path); - let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_read(path, "Deno.readFile()")?; - } - - let read_future = tokio::task::spawn_blocking(move || { - let path = Path::new(&path); - Ok(std::fs::read(path).map(ZeroCopyBuf::from).map_err(|err| { - default_err_mapper(err, format!("readfile '{}'", path.display())) - })?) - }); - - let cancel_handle = cancel_rid.and_then(|rid| { - state - .borrow_mut() - .resource_table - .get::(rid) - .ok() - }); - - if let Some(cancel_handle) = cancel_handle { - let read_future_rv = read_future.or_cancel(cancel_handle).await; - - if let Some(cancel_rid) = cancel_rid { - state.borrow_mut().resource_table.close(cancel_rid).ok(); - }; - - return read_future_rv??; - } - - read_future.await? -} - -#[op] -async fn op_readfile_text_async

( - state: Rc>, - path: String, - cancel_rid: Option, -) -> Result -where - P: FsPermissions + 'static, -{ - { - let path = Path::new(&path); - let mut state = state.borrow_mut(); - state - .borrow_mut::

() - .check_read(path, "Deno.readTextFile()")?; - } - - let read_future = tokio::task::spawn_blocking(move || { - let path = Path::new(&path); - Ok(string_from_utf8_lossy(std::fs::read(path).map_err( - |err| default_err_mapper(err, format!("readfile '{}'", path.display())), - )?)) - }); - - let cancel_handle = cancel_rid.and_then(|rid| { - state - .borrow_mut() - .resource_table - .get::(rid) - .ok() - }); - - if let Some(cancel_handle) = cancel_handle { - let read_future_rv = read_future.or_cancel(cancel_handle).await; - - if let Some(cancel_rid) = cancel_rid { - state.borrow_mut().resource_table.close(cancel_rid).ok(); - }; - - return read_future_rv??; - } - - read_future.await? -} - -// Like String::from_utf8_lossy but operates on owned values -fn string_from_utf8_lossy(buf: Vec) -> String { - match String::from_utf8_lossy(&buf) { - // buf contained non-utf8 chars than have been patched - Cow::Owned(s) => s, - // SAFETY: if Borrowed then the buf only contains utf8 chars, - // we do this instead of .into_owned() to avoid copying the input buf - Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(buf) }, - } -} diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs index 48c22ca92..5cb7dcbff 100644 --- a/runtime/ops/mod.rs +++ b/runtime/ops/mod.rs @@ -1,6 +1,5 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -pub mod fs; pub mod fs_events; pub mod http; pub mod os; diff --git a/runtime/permissions/mod.rs b/runtime/permissions/mod.rs index 5edfbe8f7..a954387e5 100644 --- a/runtime/permissions/mod.rs +++ b/runtime/permissions/mod.rs @@ -1915,7 +1915,7 @@ impl deno_websocket::WebSocketPermissions for PermissionsContainer { } } -impl crate::ops::fs::FsPermissions for PermissionsContainer { +impl deno_fs::FsPermissions for PermissionsContainer { fn check_read( &mut self, path: &Path, diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 06b594dd1..cce69fabb 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -425,7 +425,7 @@ impl WebWorker { ), // Extensions providing Deno.* features ops::fs_events::init(), - ops::fs::init::(), + deno_fs::init::(unstable), deno_io::init(options.stdio), deno_tls::init(), deno_net::init::( diff --git a/runtime/worker.rs b/runtime/worker.rs index 66497489d..1a6b6f2fc 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -255,7 +255,7 @@ impl MainWorker { options.format_js_error_fn.clone(), ), ops::fs_events::init(), - ops::fs::init::(), + deno_fs::init::(unstable), deno_io::init(options.stdio), deno_tls::init(), deno_net::init::( -- cgit v1.2.3