// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::cell::RefCell; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; use deno_core::JsBuffer; use deno_core::OpState; use deno_core::ResourceId; use deno_core::ToV8; use deno_core::op2; #[cfg(feature = "sync_fs")] use deno_core::unsync::spawn_blocking; use deno_core::v8; use deno_fs::FileSystemRc; use deno_fs::FsFileType; use deno_fs::OpenOptions; use deno_io::fs::FsResult; use deno_io::fs::FsStatFs; use deno_permissions::CheckedPath; use deno_permissions::CheckedPathBuf; use deno_permissions::OpenAccessKind; use deno_permissions::PermissionsContainer; #[cfg(feature = "sync_fs")] use tokio::task::JoinError; use crate::ops::constant::UV_FS_COPYFILE_EXCL; /// Virtual file descriptors for files without a real OS fd (e.g. VFS files /// in `deno compile` binaries). These start at a high value to avoid /// collisions with real OS fds. const VIRTUAL_FD_START: i32 = 1_000_000_000; static NEXT_VIRTUAL_FD: AtomicI32 = AtomicI32::new(VIRTUAL_FD_START); fn next_virtual_fd() -> i32 { NEXT_VIRTUAL_FD .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| { Some(v.saturating_add(1)) }) .unwrap() } #[cfg(windows)] fn is_virtual_fd(fd: i32) -> bool { fd >= VIRTUAL_FD_START } /// Extract the real OS file descriptor from a File trait object. /// On Unix, this is the raw fd (already an i32). /// On Windows, this duplicates the OS HANDLE and converts it to a CRT /// file descriptor. The duplicate ensures the CRT fd and the File trait /// object own independent handles, avoiding double-close on cleanup. /// Read CRT errno and map it to a Win32 error code for std::io::Error. /// /// `open_osfhandle` (and other CRT functions) report failures via errno, /// NOT GetLastError(). Calling `std::io::Error::last_os_error()` after a /// CRT failure reads a stale Win32 error from a prior API call — e.g. /// ERROR_ALREADY_EXISTS (183) left over from CreateFileW(CREATE_ALWAYS), /// which would be misreported as EEXIST. #[cfg(windows)] fn crt_error() -> std::io::Error { // SAFETY: _errno() is a standard MSVC CRT function that returns a // pointer to the thread-local errno value. Always valid to call. unsafe extern "C" { fn _errno() -> *mut i32; } // SAFETY: _errno() returns a valid pointer to thread-local errno. let crt_errno = unsafe { *_errno() }; let win32_code = match crt_errno { libc::EMFILE => 4, // ERROR_TOO_MANY_OPEN_FILES libc::EBADF => 6, // ERROR_INVALID_HANDLE libc::ENOMEM => 8, // ERROR_NOT_ENOUGH_MEMORY libc::EINVAL => 87, // ERROR_INVALID_PARAMETER _ => 0, // Unmapped → maps to UV "UNKNOWN" }; std::io::Error::from_raw_os_error(win32_code) } /// Convert a backing OS fd/handle into a usable file descriptor. /// On Unix this is a no-op. On Windows it duplicates the OS HANDLE and /// converts it to a CRT file descriptor. fn raw_fd_from_backing_fd( handle_fd: deno_core::ResourceHandleFd, ) -> Result { #[cfg(unix)] { Ok(handle_fd) } #[cfg(windows)] { use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS; use windows_sys::Win32::Foundation::DuplicateHandle; use windows_sys::Win32::System::Threading::GetCurrentProcess; // Duplicate the OS handle so the CRT fd owns an independent copy. // This prevents double-close: dropping the Rc closes the // original handle, and libc::close(crt_fd) closes the duplicate. let mut dup_handle = std::ptr::null_mut(); // SAFETY: `handle_fd` is a valid OS handle from the file. We duplicate // it into the current process with the same access rights. let ok = unsafe { DuplicateHandle( GetCurrentProcess(), handle_fd as _, GetCurrentProcess(), &mut dup_handle, 0, 0, DUPLICATE_SAME_ACCESS, ) }; if ok == 0 { return Err(FsError::Io(std::io::Error::last_os_error())); } // SAFETY: `dup_handle` is a valid duplicated OS handle. // `open_osfhandle` associates a CRT file descriptor with it so // that node:fs callers receive a POSIX-style fd. let crt_fd = unsafe { libc::open_osfhandle(dup_handle as isize, 0) }; if crt_fd == -1 { // SAFETY: Clean up the duplicated handle on failure. unsafe { CloseHandle(dup_handle) }; return Err(FsError::Io(crt_error())); } Ok(crt_fd) } } fn ebadf() -> FsError { FsError::Io(std::io::Error::from_raw_os_error( #[cfg(unix)] libc::EBADF, #[cfg(windows)] { // Win32 ERROR_INVALID_HANDLE, which maps to Node's EBADF 6 }, )) } /// Get the File trait object for an OS file descriptor from FdTable. fn file_for_fd( state: &OpState, fd: i32, ) -> Result, FsError> { state .borrow::() .get(fd) .cloned() .ok_or_else(ebadf) } /// When `sync_fs` is enabled, `FileSystemRc` is `Arc` (Send) and we can /// offload work to a blocking thread. Otherwise, run inline. macro_rules! maybe_spawn_blocking { ($f:expr) => {{ #[cfg(feature = "sync_fs")] { spawn_blocking($f).await.unwrap() } #[cfg(not(feature = "sync_fs"))] { ($f)() } }}; } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum FsError { #[class(inherit)] #[error(transparent)] Permission(#[from] deno_permissions::PermissionCheckError), #[class(inherit)] #[error("{0}")] Io( #[from] #[inherit] std::io::Error, ), #[class(inherit)] #[error(transparent)] Fs( #[from] #[inherit] deno_io::fs::FsError, ), #[class(inherit)] #[error(transparent)] Cp(#[from] CpError), #[class(inherit)] #[error(transparent)] NodeFs(#[from] NodeFsError), #[cfg(feature = "sync_fs")] #[class(inherit)] #[error(transparent)] JoinError(#[from] JoinError), } #[derive(Debug, Default)] pub struct NodeFsErrorContext { message: Option, path: Option, dest: Option, syscall: Option, } #[derive(Debug, thiserror::Error, deno_error::JsError)] #[class(generic)] // Intentionally set the error message to be empty, so that in the JS side it will fall back to the UV error message // https://github.com/denoland/deno/blob/f123d84e7d6e8036c3c38ecec6e25deb87a8829b/ext/node/polyfills/internal/errors.ts#L268-L270 #[error("")] #[property("os_errno" = self.os_errno)] #[property("message" = self.context.message.clone().unwrap_or_default().clone())] #[property("path" = self.context.path.clone().unwrap_or_default().clone())] #[property("dest" = self.context.dest.clone().unwrap_or_default().clone())] #[property("syscall" = self.context.syscall.clone().unwrap_or_default().clone())] pub struct NodeFsError { os_errno: i32, context: NodeFsErrorContext, } fn map_fs_error_to_node_fs_error( err: deno_io::fs::FsError, context: NodeFsErrorContext, ) -> FsError { let os_errno = match err { deno_io::fs::FsError::Io(ref io_err) => io_err.raw_os_error(), _ => None, }; if let Some(os_errno) = os_errno { return NodeFsError { os_errno, context }.into(); } FsError::Fs(err) } #[derive(Debug, thiserror::Error, deno_error::JsError)] #[class(generic)] #[error("{message}")] #[property("kind" = self.kind())] #[property("message" = self.message())] #[property("path" = self.path())] pub enum CpError { EInval { message: String, path: String }, DirToNonDir { message: String, path: String }, NonDirToDir { message: String, path: String }, EExist { message: String, path: String }, EIsDir { message: String, path: String }, Socket { message: String, path: String }, Fifo { message: String, path: String }, Unknown { message: String, path: String }, SymlinkToSubdirectory { message: String, path: String }, } impl CpError { fn kind(&self) -> &'static str { match self { CpError::EInval { .. } => "EINVAL", CpError::DirToNonDir { .. } => "DIR_TO_NON_DIR", CpError::NonDirToDir { .. } => "NON_DIR_TO_DIR", CpError::EExist { .. } => "EEXIST", CpError::EIsDir { .. } => "EISDIR", CpError::Socket { .. } => "SOCKET", CpError::Fifo { .. } => "FIFO", CpError::Unknown { .. } => "UNKNOWN", CpError::SymlinkToSubdirectory { .. } => "SYMLINK_TO_SUBDIRECTORY", } } fn message(&self) -> String { match self { CpError::EInval { message, .. } => message.clone(), CpError::DirToNonDir { message, .. } => message.clone(), CpError::NonDirToDir { message, .. } => message.clone(), CpError::EExist { message, .. } => message.clone(), CpError::EIsDir { message, .. } => message.clone(), CpError::Socket { message, .. } => message.clone(), CpError::Fifo { message, .. } => message.clone(), CpError::Unknown { message, .. } => message.clone(), CpError::SymlinkToSubdirectory { message, .. } => message.clone(), } } fn path(&self) -> String { match self { CpError::EInval { path, .. } => path.clone(), CpError::DirToNonDir { path, .. } => path.clone(), CpError::NonDirToDir { path, .. } => path.clone(), CpError::EExist { path, .. } => path.clone(), CpError::EIsDir { path, .. } => path.clone(), CpError::Socket { path, .. } => path.clone(), CpError::Fifo { path, .. } => path.clone(), CpError::Unknown { path, .. } => path.clone(), CpError::SymlinkToSubdirectory { path, .. } => path.clone(), } } } #[op2(fast, stack_trace)] pub fn op_node_fs_exists_sync( state: &mut OpState, #[string] path: &str, ) -> Result { let path_or_err = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(path)), OpenAccessKind::ReadNoFollow, Some("node:fs.existsSync()"), ); match path_or_err { Ok(path) => { let fs = state.borrow::(); Ok(fs.exists_sync(&path)) } Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), Err(err) => Err(err), } } #[op2(stack_trace)] pub async fn op_node_fs_exists( state: Rc>, #[string] path: String, ) -> Result { let (fs, path_or_err) = { let mut state = state.borrow_mut(); let path_or_err = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path)), OpenAccessKind::ReadNoFollow, Some("node:fs.exists()"), ); (state.borrow::().clone(), path_or_err) }; match path_or_err { Ok(path) => Ok(fs.exists_async(path.into_owned()).await?), Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), Err(err) => Err(FsError::Permission(err)), } } fn get_open_options(flags: i32, mode: Option) -> OpenOptions { let mut options = OpenOptions::from(flags); options.mode = mode; options } fn open_options_to_access_kind(open_options: &OpenOptions) -> OpenAccessKind { let read = open_options.read; let write = open_options.write || open_options.append || open_options.create; match (read, write) { (true, true) => OpenAccessKind::ReadWrite, (false, true) => OpenAccessKind::Write, (true, false) | (false, false) => OpenAccessKind::Read, } } #[op2(fast, stack_trace)] #[smi] pub fn op_node_open_sync( state: &mut OpState, #[string] path: &str, #[smi] flags: i32, #[smi] mode: u32, ) -> Result { let path = Path::new(path); let options = get_open_options(flags, Some(mode)); let fs = state.borrow::().clone(); let path = state.borrow_mut::().check_open( Cow::Borrowed(path), open_options_to_access_kind(&options), Some("node:fs.openSync"), )?; // On Windows, opening with create + truncate uses CREATE_ALWAYS which // truncates the file to 0 bytes immediately. If the subsequent CRT fd // creation (open_osfhandle) fails, the file is left at 0 bytes — causing // permanent data loss. To prevent this, open without truncation first, // create the fd, and then truncate. #[cfg(windows)] let deferred_truncate = options.truncate && options.create && !options.create_new; #[cfg(windows)] let open_options = if deferred_truncate { OpenOptions { truncate: false, ..options } } else { options }; #[cfg(not(windows))] let open_options = options; let file = fs.open_sync(&path, open_options)?; // For VFS files (e.g. in deno compile), backing_fd() returns None. // Assign a virtual fd so the file can still be used through FdTable. let fd = match file.clone().backing_fd() { Some(backing_fd) => raw_fd_from_backing_fd(backing_fd)?, None => next_virtual_fd(), }; #[cfg(windows)] if deferred_truncate && !is_virtual_fd(fd) && let Err(e) = file.clone().truncate_sync(0) { // SAFETY: fd is a valid CRT fd just created by raw_fd_from_backing_fd. unsafe { libc::close(fd) }; return Err(e.into()); } state.borrow_mut::().register(fd, file); Ok(fd) } #[op2(stack_trace)] #[smi] pub async fn op_node_open( state: Rc>, #[string] path: String, #[smi] flags: i32, #[smi] mode: u32, ) -> Result { let path = PathBuf::from(path); let options = get_open_options(flags, Some(mode)); let (fs, path) = { let mut state = state.borrow_mut(); ( state.borrow::().clone(), state.borrow_mut::().check_open( Cow::Owned(path), open_options_to_access_kind(&options), Some("node:fs.open"), )?, ) }; // See op_node_open_sync for why we defer truncation on Windows. #[cfg(windows)] let deferred_truncate = options.truncate && options.create && !options.create_new; #[cfg(windows)] let open_options = if deferred_truncate { OpenOptions { truncate: false, ..options } } else { options }; #[cfg(not(windows))] let open_options = options; let file = fs.open_async(path.as_owned(), open_options).await?; // For VFS files (e.g. in deno compile), backing_fd() returns None. // Assign a virtual fd so the file can still be used through FdTable. let fd = match file.clone().backing_fd() { Some(backing_fd) => raw_fd_from_backing_fd(backing_fd)?, None => next_virtual_fd(), }; #[cfg(windows)] if deferred_truncate && !is_virtual_fd(fd) && let Err(e) = file.clone().truncate_sync(0) { // SAFETY: fd is a valid CRT fd just created by raw_fd_from_backing_fd. unsafe { libc::close(fd) }; return Err(e.into()); } let mut state = state.borrow_mut(); state.borrow_mut::().register(fd, file); Ok(fd) } #[derive(Debug, ToV8)] pub struct StatFs { #[to_v8(rename = "type")] pub typ: u64, pub bsize: u64, pub blocks: u64, pub bfree: u64, pub bavail: u64, pub files: u64, pub ffree: u64, } impl From for StatFs { fn from(s: FsStatFs) -> Self { StatFs { typ: s.typ, bsize: s.bsize, blocks: s.blocks, bfree: s.bfree, bavail: s.bavail, files: s.files, ffree: s.ffree, } } } #[op2(stack_trace)] pub fn op_node_statfs_sync( state: &mut OpState, #[string] path: &str, bigint: bool, ) -> Result { let path = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(path)), OpenAccessKind::ReadNoFollow, Some("node:fs.statfsSync"), )?; state .borrow_mut::() .check_sys("statfs", "node:fs.statfsSync")?; let fs = state.borrow::(); Ok(fs.statfs_sync(&path, bigint)?.into()) } #[op2(stack_trace)] pub async fn op_node_statfs( state: Rc>, #[string] path: String, bigint: bool, ) -> Result { let (fs, path) = { let mut state = state.borrow_mut(); let path = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path)), OpenAccessKind::ReadNoFollow, Some("node:fs.statfs"), )?; state .borrow_mut::() .check_sys("statfs", "node:fs.statfs")?; (state.borrow::().clone(), path) }; Ok(fs.statfs_async(path.into_owned(), bigint).await?.into()) } #[op2(fast, stack_trace)] pub fn op_node_lutimes_sync( state: &mut OpState, #[string] path: &str, #[number] atime_secs: i64, #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, ) -> Result<(), FsError> { let path = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.lutimes"), )?; let fs = state.borrow::(); fs.lutime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)?; Ok(()) } #[op2(stack_trace)] pub async fn op_node_lutimes( state: Rc>, #[string] path: String, #[number] atime_secs: i64, #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, ) -> Result<(), FsError> { let (fs, path) = { let mut state = state.borrow_mut(); let path = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.lutimesSync"), )?; (state.borrow::().clone(), path) }; fs.lutime_async( path.into_owned(), atime_secs, atime_nanos, mtime_secs, mtime_nanos, ) .await?; Ok(()) } #[op2(stack_trace)] pub fn op_node_lchown_sync( state: &mut OpState, #[string] path: &str, uid: Option, gid: Option, ) -> Result<(), FsError> { let path = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.lchownSync"), )?; let fs = state.borrow::(); fs.lchown_sync(&path, uid, gid)?; Ok(()) } #[op2(stack_trace)] pub async fn op_node_lchown( state: Rc>, #[string] path: String, uid: Option, gid: Option, ) -> Result<(), FsError> { let (fs, path) = { let mut state = state.borrow_mut(); let path = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.lchown"), )?; (state.borrow::().clone(), path) }; fs.lchown_async(path.into_owned(), uid, gid).await?; Ok(()) } #[op2(fast, stack_trace)] pub fn op_node_lchmod_sync( state: &mut OpState, #[string] path: &str, #[smi] mode: u32, ) -> Result<(), FsError> { let path = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.lchmodSync"), )?; let fs = state.borrow::(); fs.lchmod_sync(&path, mode)?; Ok(()) } #[op2(stack_trace)] pub async fn op_node_lchmod( state: Rc>, #[string] path: String, #[smi] mode: u32, ) -> Result<(), FsError> { let (fs, path) = { let mut state = state.borrow_mut(); let path = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.lchmod"), )?; (state.borrow::().clone(), path) }; fs.lchmod_async(path.into_owned(), mode).await?; Ok(()) } #[op2(stack_trace)] #[string] pub fn op_node_mkdtemp_sync( state: &mut OpState, #[string] path: &str, ) -> Result { // https://github.com/nodejs/node/blob/2ea31e53c61463727c002c2d862615081940f355/deps/uv/src/unix/os390-syscalls.c#L409 for _ in 0..libc::TMP_MAX { let path = temp_path_append_suffix(path); let checked_path = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(&path)), OpenAccessKind::WriteNoFollow, Some("node:fs.mkdtempSync()"), )?; let fs = state.borrow::(); match fs.mkdir_sync(&checked_path, false, Some(0o700)) { Ok(()) => return Ok(path), Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { continue; } Err(err) => return Err(FsError::Fs(err)), } } Err(FsError::Io(std::io::Error::new( std::io::ErrorKind::AlreadyExists, "too many temp dirs exist", ))) } #[op2(stack_trace)] #[string] pub async fn op_node_mkdtemp( state: Rc>, #[string] path: String, ) -> Result { // https://github.com/nodejs/node/blob/2ea31e53c61463727c002c2d862615081940f355/deps/uv/src/unix/os390-syscalls.c#L409 for _ in 0..libc::TMP_MAX { let path = temp_path_append_suffix(&path); let (fs, checked_path) = { let mut state = state.borrow_mut(); let checked_path = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path.clone())), OpenAccessKind::WriteNoFollow, Some("node:fs.mkdtemp()"), )?; (state.borrow::().clone(), checked_path) }; match fs .mkdir_async(checked_path.into_owned(), false, Some(0o700)) .await { Ok(()) => return Ok(path), Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { continue; } Err(err) => return Err(FsError::Fs(err)), } } Err(FsError::Io(std::io::Error::new( std::io::ErrorKind::AlreadyExists, "too many temp dirs exist", ))) } fn temp_path_append_suffix(prefix: &str) -> String { use rand::Rng; use rand::distributions::Alphanumeric; use rand::rngs::OsRng; let suffix: String = (0..6).map(|_| OsRng.sample(Alphanumeric) as char).collect(); format!("{}{}", prefix, suffix) } #[op2(fast, stack_trace)] pub fn op_node_rmdir_sync( state: &mut OpState, #[string] path: &str, ) -> Result<(), FsError> { let path = state.borrow_mut::().check_open( Cow::Borrowed(Path::new(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.rmdirSync"), )?; let fs = state.borrow::(); fs.rmdir_sync(&path)?; Ok(()) } #[op2(stack_trace)] pub async fn op_node_rmdir( state: Rc>, #[string] path: String, ) -> Result<(), FsError> { let (fs, path) = { let mut state = state.borrow_mut(); let path = state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(path)), OpenAccessKind::WriteNoFollow, Some("node:fs.rmdir"), )?; (state.borrow::().clone(), path) }; fs.rmdir_async(path.into_owned()).await?; Ok(()) } /// Create an anonymous pipe pair and return (read_fd, write_fd). /// The returned fds are NOT registered in FdTable; the caller /// is responsible for registering or closing them. #[cfg(unix)] #[op2] #[serde] pub fn op_node_create_pipe() -> Result<(i32, i32), FsError> { let mut fds = [0i32; 2]; // SAFETY: pipe() writes two valid fds into the array on success. let ret = unsafe { libc::pipe(fds.as_mut_ptr()) }; if ret != 0 { return Err(FsError::Io(std::io::Error::last_os_error())); } Ok((fds[0], fds[1])) } #[cfg(windows)] #[op2] #[serde] pub fn op_node_create_pipe() -> Result<(i32, i32), FsError> { use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::System::Pipes::CreatePipe; let mut read_handle = std::ptr::null_mut(); let mut write_handle = std::ptr::null_mut(); // SAFETY: CreatePipe writes valid handles on success. let ok = unsafe { CreatePipe(&mut read_handle, &mut write_handle, std::ptr::null(), 0) }; if ok == 0 { return Err(FsError::Io(std::io::Error::last_os_error())); } // Convert OS handles to CRT file descriptors. // SAFETY: read_handle and write_handle are valid pipe handles from // CreatePipe. open_osfhandle takes ownership of the handle on success. let read_fd = unsafe { libc::open_osfhandle(read_handle as isize, 0) }; // SAFETY: Same as above for the write handle. let write_fd = unsafe { libc::open_osfhandle(write_handle as isize, 0) }; if read_fd == -1 || write_fd == -1 { // Clean up on failure: close whichever succeeded as a CRT fd, // and close the raw OS handle for whichever failed. if read_fd != -1 { // SAFETY: read_fd is a valid CRT fd from open_osfhandle. unsafe { libc::close(read_fd); } } else { // SAFETY: read_handle is still a valid OS handle (open_osfhandle failed). unsafe { CloseHandle(read_handle); } } if write_fd != -1 { // SAFETY: write_fd is a valid CRT fd from open_osfhandle. unsafe { libc::close(write_fd); } } else { // SAFETY: write_handle is still a valid OS handle (open_osfhandle failed). unsafe { CloseHandle(write_handle); } } return Err(ebadf()); } Ok((read_fd, write_fd)) } // ============================================================ // fd-based ops for node:fs (accept real OS fd, not RID) // ============================================================ /// Set blocking or non-blocking mode on an OS file descriptor. /// Matches libuv's `uv_stream_set_blocking`. #[cfg(unix)] #[op2(fast)] #[smi] pub fn op_node_fd_set_blocking(fd: i32, blocking: bool) -> i32 { // SAFETY: fcntl with F_GETFL/F_SETFL is safe on valid fds. // Returns -1 on invalid fd, which we map to UV_EBADF. unsafe { let flags = libc::fcntl(fd, libc::F_GETFL); if flags == -1 { return -libc::EBADF; } let flags = if blocking { flags & !libc::O_NONBLOCK } else { flags | libc::O_NONBLOCK }; if libc::fcntl(fd, libc::F_SETFL, flags) == -1 { return -libc::EBADF; } 0 } } #[cfg(windows)] #[op2(fast)] #[smi] pub fn op_node_fd_set_blocking(_fd: i32, _blocking: bool) -> i32 { // On Windows, named pipes and console handles don't support // toggling blocking mode via a simple flag. The Windows-specific // behavior is handled at the I/O level instead. 0 } #[op2(fast)] pub fn op_node_fs_close(state: &mut OpState, fd: i32) -> Result<(), FsError> { // FdTable.remove() drops the Rc and cancels the cancel handle, // which will abort any in-flight async reads on this fd. let file = state .borrow_mut::() .remove(fd) .ok_or_else(ebadf)?; // For stdio fds (0/1/2), also remove the corresponding resource table // entry so that Deno.stdin/stdout/stderr see the fd as closed and // release their Rc clone of the same File. if (0..=2).contains(&fd) && let Ok(resource) = state.resource_table.take_any(fd as ResourceId) { resource.close(); } // Dropping the Rc will close the underlying OS file descriptor // when the reference count reaches zero (via std::fs::File Drop). drop(file); // On Windows, `raw_fd_from_backing_fd` creates a CRT file descriptor via // `open_osfhandle` on a duplicated OS handle. The File Drop above closes // the original handle; `libc::close` closes the duplicate and frees the // CRT fd slot. Skip this for virtual fds (VFS files) which have no CRT fd. #[cfg(windows)] if !is_virtual_fd(fd) { // SAFETY: `fd` is a valid CRT file descriptor created by // `open_osfhandle` in `raw_fd_from_backing_fd`. unsafe { libc::close(fd); } } Ok(()) } /// Read from a raw OS fd using libc. No dup, no clone -- the read uses the /// fd number directly, so closing the fd from another thread interrupts the /// Positioned read: if position >= 0, uses pread to read without moving the /// file cursor. If position < 0, reads from the current position. fn read_with_position( file: Rc, buf: &mut [u8], position: i64, ) -> Result { if position >= 0 { let nread = file.read_at_sync(buf, position as u64)?; Ok(nread as u32) } else { let nread = file.read_sync(buf)?; Ok(nread as u32) } } #[op2(fast)] #[smi] pub fn op_node_fs_read_sync( state: &mut OpState, fd: i32, #[buffer] buf: &mut [u8], #[bigint] position: i64, ) -> Result { let file = file_for_fd(state, fd)?; read_with_position(file, buf, position) } /// Async read for node:fs. Runs a blocking read on a spawned thread using /// the raw OS fd directly (no dup/clone). When the fd is closed via /// Async read for node:fs. Uses the File trait from FdTable for proper /// I/O through the file handle. #[op2] #[smi] pub async fn op_node_fs_read_deferred( state: Rc>, fd: i32, #[buffer] buf: JsBuffer, #[bigint] position: i64, ) -> Result { let file = file_for_fd(&state.borrow(), fd)?; if position >= 0 { let view = deno_core::BufMutView::from(buf); let (nread, _) = file.read_at_async(view, position as u64).await?; Ok(nread as u32) } else { let view = deno_core::BufMutView::from(buf); let (nread, _) = file.read_byob(view).await?; Ok(nread as u32) } } /// Positioned write: if position >= 0, uses pwrite to write without moving /// the file cursor. If position < 0, writes at the current position. /// Handles partial writes internally by looping until all bytes are written. fn write_with_position( file: Rc, buf: &[u8], position: i64, ) -> Result { if position >= 0 { let mut total = 0usize; while total < buf.len() { let nwritten = file .clone() .write_at_sync(&buf[total..], position as u64 + total as u64)?; total += nwritten; } Ok(total as u32) } else { let mut total = 0usize; while total < buf.len() { let nwritten = file.clone().write_sync(&buf[total..])?; total += nwritten; } Ok(total as u32) } } #[op2(fast)] #[smi] pub fn op_node_fs_write_sync( state: &mut OpState, fd: i32, #[buffer] buf: &[u8], #[number] position: i64, ) -> Result { let file = file_for_fd(state, fd)?; write_with_position(file, buf, position) } /// Async write for node:fs. Performs the write synchronously but resolves /// the promise on the next event loop tick. #[allow( clippy::unused_async, reason = "async required for deferred op scheduling" )] #[op2(async(deferred))] #[smi] pub async fn op_node_fs_write_deferred( state: Rc>, fd: i32, #[buffer] buf: JsBuffer, #[number] position: i64, ) -> Result { let file = file_for_fd(&state.borrow(), fd)?; write_with_position(file, &buf, position) } #[op2(fast)] #[number] pub fn op_node_fs_seek_sync( state: &mut OpState, fd: i32, #[number] offset: i64, #[smi] whence: i32, ) -> Result { let file = file_for_fd(state, fd)?; let seek_from = match whence { 0 => std::io::SeekFrom::Start(offset as u64), 1 => std::io::SeekFrom::Current(offset), 2 => std::io::SeekFrom::End(offset), _ => { return Err(FsError::Io(std::io::Error::new( std::io::ErrorKind::InvalidInput, "invalid whence", ))); } }; let pos = file.seek_sync(seek_from)?; Ok(pos) } #[op2] #[number] pub async fn op_node_fs_seek( state: Rc>, fd: i32, #[number] offset: i64, #[smi] whence: i32, ) -> Result { let file = file_for_fd(&state.borrow(), fd)?; let seek_from = match whence { 0 => std::io::SeekFrom::Start(offset as u64), 1 => std::io::SeekFrom::Current(offset), 2 => std::io::SeekFrom::End(offset), _ => { return Err(FsError::Io(std::io::Error::new( std::io::ErrorKind::InvalidInput, "invalid whence", ))); } }; let pos = file.seek_async(seek_from).await?; Ok(pos) } /// Stat result returned to JS. Uses f64 for numeric fields to ensure /// they always serialize as JS Number (not BigInt). BigInt conversion /// for the bigint stat API is handled on the JS side by CFISBIS. #[derive(ToV8)] pub struct NodeFsStat { pub is_file: bool, pub is_directory: bool, pub is_symlink: bool, pub size: f64, pub mtime_ms: Option, pub atime_ms: Option, pub birthtime_ms: Option, pub ctime_ms: Option, pub dev: f64, pub ino: f64, pub mode: u32, pub nlink: f64, pub uid: u32, pub gid: u32, pub rdev: f64, pub blksize: f64, pub blocks: f64, pub is_block_device: bool, pub is_char_device: bool, pub is_fifo: bool, pub is_socket: bool, } impl From for NodeFsStat { fn from(stat: deno_io::fs::FsStat) -> Self { NodeFsStat { is_file: stat.is_file, is_directory: stat.is_directory, is_symlink: stat.is_symlink, size: stat.size as f64, mtime_ms: stat.mtime.map(|v| v as f64), atime_ms: stat.atime.map(|v| v as f64), birthtime_ms: stat.birthtime.map(|v| v as f64), ctime_ms: stat.ctime.map(|v| v as f64), dev: stat.dev as f64, ino: stat.ino.unwrap_or(0) as f64, mode: stat.mode, nlink: stat.nlink.unwrap_or(0) as f64, uid: stat.uid, gid: stat.gid, rdev: stat.rdev as f64, blksize: stat.blksize as f64, blocks: stat.blocks.unwrap_or(0) as f64, is_block_device: stat.is_block_device, is_char_device: stat.is_char_device, is_fifo: stat.is_fifo, is_socket: stat.is_socket, } } } #[op2] pub fn op_node_fs_fstat_sync( state: &mut OpState, fd: i32, ) -> Result { let file = file_for_fd(state, fd)?; let stat = file.stat_sync()?; Ok(NodeFsStat::from(stat)) } #[op2] pub async fn op_node_fs_fstat( state: Rc>, fd: i32, ) -> Result { let file = file_for_fd(&state.borrow(), fd)?; let stat = file.stat_async().await?; Ok(NodeFsStat::from(stat)) } #[op2(fast)] pub fn op_node_fs_ftruncate_sync( state: &mut OpState, fd: i32, #[number] len: u64, ) -> Result<(), FsError> { let file = file_for_fd(state, fd)?; file.truncate_sync(len)?; Ok(()) } #[op2] pub async fn op_node_fs_ftruncate( state: Rc>, fd: i32, #[number] len: u64, ) -> Result<(), FsError> { let file = file_for_fd(&state.borrow(), fd)?; file.truncate_async(len).await?; Ok(()) } #[op2(fast)] pub fn op_node_fs_fsync_sync( state: &mut OpState, fd: i32, ) -> Result<(), FsError> { let file = file_for_fd(state, fd)?; file.sync_sync()?; Ok(()) } #[op2] pub async fn op_node_fs_fsync( state: Rc>, fd: i32, ) -> Result<(), FsError> { let file = file_for_fd(&state.borrow(), fd)?; file.sync_async().await?; Ok(()) } #[op2(fast)] pub fn op_node_fs_fdatasync_sync( state: &mut OpState, fd: i32, ) -> Result<(), FsError> { let file = file_for_fd(state, fd)?; file.datasync_sync()?; Ok(()) } #[op2] pub async fn op_node_fs_fdatasync( state: Rc>, fd: i32, ) -> Result<(), FsError> { let file = file_for_fd(&state.borrow(), fd)?; file.datasync_async().await?; Ok(()) } #[op2(fast)] pub fn op_node_fs_futimes_sync( state: &mut OpState, fd: i32, #[number] atime_secs: i64, #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, ) -> Result<(), FsError> { let file = file_for_fd(state, fd)?; file.utime_sync(atime_secs, atime_nanos, mtime_secs, mtime_nanos)?; Ok(()) } #[op2] pub async fn op_node_fs_futimes( state: Rc>, fd: i32, #[number] atime_secs: i64, #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, ) -> Result<(), FsError> { let file = file_for_fd(&state.borrow(), fd)?; file .utime_async(atime_secs, atime_nanos, mtime_secs, mtime_nanos) .await?; Ok(()) } #[op2(fast)] pub fn op_node_fs_fchmod_sync( state: &mut OpState, fd: i32, #[smi] mode: u32, ) -> Result<(), FsError> { let file = file_for_fd(state, fd)?; file.chmod_sync(mode)?; Ok(()) } #[op2] pub async fn op_node_fs_fchmod( state: Rc>, fd: i32, #[smi] mode: u32, ) -> Result<(), FsError> { let file = file_for_fd(&state.borrow(), fd)?; file.chmod_async(mode).await?; Ok(()) } #[op2(fast)] pub fn op_node_fs_fchown_sync( state: &mut OpState, fd: i32, #[smi] uid: u32, #[smi] gid: u32, ) -> Result<(), FsError> { let file = file_for_fd(state, fd)?; file.chown_sync(Some(uid), Some(gid))?; Ok(()) } #[op2] pub async fn op_node_fs_fchown( state: Rc>, fd: i32, #[smi] uid: u32, #[smi] gid: u32, ) -> Result<(), FsError> { let file = file_for_fd(&state.borrow(), fd)?; file.chown_async(Some(uid), Some(gid)).await?; Ok(()) } #[op2] #[buffer] pub fn op_node_fs_read_file_sync( state: &mut OpState, fd: i32, ) -> Result, FsError> { let file = file_for_fd(state, fd)?; let buf = file.read_all_sync()?; Ok(buf.to_vec()) } #[op2] #[buffer] pub async fn op_node_fs_read_file( state: Rc>, fd: i32, ) -> Result, FsError> { let file = file_for_fd(&state.borrow(), fd)?; let buf = file.read_all_async().await?; Ok(buf.to_vec()) } const CP_IS_DEST_EXISTS_FLAG: u64 = 1u64 << 32; const CP_IS_SRC_DIRECTORY_FLAG: u64 = 1u64 << 33; const CP_IS_SRC_FILE_FLAG: u64 = 1u64 << 34; const CP_IS_SRC_CHAR_DEVICE_FLAG: u64 = 1u64 << 35; const CP_IS_SRC_BLOCK_DEVICE_FLAG: u64 = 1u64 << 36; const CP_IS_SRC_SYMLINK_FLAG: u64 = 1u64 << 37; const CP_IS_SRC_SOCKET_FLAG: u64 = 1u64 << 38; const CP_IS_SRC_FIFO_FLAG: u64 = 1u64 << 39; /// Bit-packed stat metadata passed to JS for `cp` operations. /// /// Layout (u64): /// - bits 0..31 : src mode (`st_mode`) /// - bit 32 : destination exists /// - bits 33..39 : source type flags (dir, file, char/block device, symlink, socket, fifo) fn compact_stat_info( src_stat: &deno_io::fs::FsStat, is_dest_exists: bool, ) -> u64 { let mut packed = src_stat.mode as u64; if is_dest_exists { packed |= CP_IS_DEST_EXISTS_FLAG; } if src_stat.is_file { packed |= CP_IS_SRC_FILE_FLAG; } if src_stat.is_directory { packed |= CP_IS_SRC_DIRECTORY_FLAG; } if src_stat.is_char_device { packed |= CP_IS_SRC_CHAR_DEVICE_FLAG; } if src_stat.is_block_device { packed |= CP_IS_SRC_BLOCK_DEVICE_FLAG; } if src_stat.is_symlink { packed |= CP_IS_SRC_SYMLINK_FLAG; } if src_stat.is_socket { packed |= CP_IS_SRC_SOCKET_FLAG; } if src_stat.is_fifo { packed |= CP_IS_SRC_FIFO_FLAG; } packed } struct CpCheckPathsSyncResult { is_dest_exists: bool, is_src_directory: bool, is_src_file: bool, is_src_char_device: bool, is_src_block_device: bool, is_src_symlink: bool, is_src_socket: bool, is_src_fifo: bool, src_mode: u32, src_dev: u64, src_ino: u64, } // To save the cost of multiple round trips between Rust and JS, // we pack the necessary metadata for `cp` operations into a single u64 value and pass it to JS. #[derive(Debug)] struct CpCheckPathsResult { src_dev: u64, src_ino: u64, stat_info: u64, } #[derive(Clone, Copy)] struct CpSyncOptions<'a> { dereference: bool, recursive: bool, force: bool, error_on_exist: bool, preserve_timestamps: bool, verbatim_symlinks: bool, mode: u32, filter: Option>, } enum CpSyncRunStatus { Completed, JsException, } fn call_cp_sync_filter<'a>( scope: &mut v8::PinScope<'a, '_>, filter: v8::Local<'a, v8::Function>, src: &str, dest: &str, ) -> Option { v8::tc_scope!(tc_scope, scope); let recv = v8::undefined(tc_scope); let src = v8::String::new(tc_scope, src).unwrap(); let dest = v8::String::new(tc_scope, dest).unwrap(); let result = filter.call(tc_scope, recv.into(), &[src.into(), dest.into()]); if tc_scope.has_caught() || tc_scope.has_terminated() { tc_scope.rethrow(); return None; } Some(result.unwrap().boolean_value(tc_scope)) } fn cp_sync_copy_dir<'a>( state: &Rc>, fs: &FileSystemRc, scope: &mut v8::PinScope<'a, '_>, src: &str, dest: &str, opts: CpSyncOptions<'a>, ) -> Result { let src_path = check_cp_path(state, src, OpenAccessKind::ReadNoFollow)?; let entries = fs.read_dir_sync(&src_path.as_checked_path()) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.to_string()), syscall: Some("opendir".into()), ..Default::default() }, ) })?; for entry in entries { let src_item = Path::new(src) .join(&entry.name) .to_string_lossy() .to_string(); let dest_item = Path::new(dest) .join(&entry.name) .to_string_lossy() .to_string(); if let Some(filter) = opts.filter { let Some(should_copy) = call_cp_sync_filter(scope, filter, &src_item, &dest_item) else { return Ok(CpSyncRunStatus::JsException); }; if !should_copy { continue; } } let stat_info = check_paths_impl_sync( state, fs, &src_item, &dest_item, opts.dereference, )?; match cp_sync_dispatch( state, fs, scope, &stat_info, &src_item, &dest_item, opts, )? { CpSyncRunStatus::Completed => {} CpSyncRunStatus::JsException => return Ok(CpSyncRunStatus::JsException), } } Ok(CpSyncRunStatus::Completed) } fn cp_sync_mkdir_and_copy<'a>( state: &Rc>, fs: &FileSystemRc, scope: &mut v8::PinScope<'a, '_>, src_mode: u32, src: &str, dest: &str, opts: CpSyncOptions<'a>, ) -> Result { let dest_path = check_cp_path(state, dest, OpenAccessKind::Write)?; fs.mkdir_sync(&dest_path.as_checked_path(), false, None) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest.to_string()), syscall: Some("mkdir".into()), ..Default::default() }, ) })?; let result = cp_sync_copy_dir(state, fs, scope, src, dest, opts)?; if let CpSyncRunStatus::JsException = result { return Ok(result); } set_dest_mode(fs, &dest_path.as_checked_path(), src_mode)?; Ok(CpSyncRunStatus::Completed) } fn cp_sync_on_dir<'a>( state: &Rc>, fs: &FileSystemRc, scope: &mut v8::PinScope<'a, '_>, stat_info: &CpCheckPathsSyncResult, src: &str, dest: &str, opts: CpSyncOptions<'a>, ) -> Result { if !stat_info.is_dest_exists { return cp_sync_mkdir_and_copy( state, fs, scope, stat_info.src_mode, src, dest, opts, ); } cp_sync_copy_dir(state, fs, scope, src, dest, opts) } fn cp_sync_dispatch<'a>( state: &Rc>, fs: &FileSystemRc, scope: &mut v8::PinScope<'a, '_>, stat_info: &CpCheckPathsSyncResult, src: &str, dest: &str, opts: CpSyncOptions<'a>, ) -> Result { if stat_info.is_src_directory && opts.recursive { return cp_sync_on_dir(state, fs, scope, stat_info, src, dest, opts); } else if stat_info.is_src_directory { return Err( CpError::EIsDir { message: format!("{} is a directory (not copied)", src), path: src.to_string(), } .into(), ); } else if stat_info.is_src_file || stat_info.is_src_char_device || stat_info.is_src_block_device { op_node_cp_on_file_sync(state, fs, src, dest, stat_info, &opts)?; return Ok(CpSyncRunStatus::Completed); } else if stat_info.is_src_symlink { op_node_cp_on_link_sync( state, fs, src, dest, stat_info.is_dest_exists, opts.verbatim_symlinks, )?; return Ok(CpSyncRunStatus::Completed); } else if stat_info.is_src_socket { return Err( CpError::Socket { message: format!("cannot copy a socket file: {}", dest), path: dest.to_string(), } .into(), ); } else if stat_info.is_src_fifo { return Err( CpError::Fifo { message: format!("cannot copy a FIFO pipe: {}", dest), path: dest.to_string(), } .into(), ); } Err( CpError::Unknown { message: format!("cannot copy an unknown file type: {}", dest), path: dest.to_string(), } .into(), ) } /// Check if two stat results refer to the same file (same dev + ino). fn are_identical_stat( src_stat: &deno_io::fs::FsStat, dest_stat: &deno_io::fs::FsStat, ) -> bool { match (dest_stat.ino, src_stat.ino) { (Some(dest_ino), Some(src_ino)) if dest_ino > 0 && dest_stat.dev > 0 => { dest_ino == src_ino && dest_stat.dev == src_stat.dev } _ => false, } } fn is_src_subdir(src: &str, dest: &str) -> bool { let src_resolved = std::path::absolute(src).unwrap_or_else(|_| PathBuf::from(src)); let dest_resolved = std::path::absolute(dest).unwrap_or_else(|_| PathBuf::from(dest)); let src_components = src_resolved.components().collect::>(); let dest_components = dest_resolved.components().collect::>(); src_components .iter() .enumerate() .all(|(i, c)| dest_components.get(i) == Some(c)) } /// As this function attempts to get the absolute path of the target, /// callers of this function should ensure that the permissions are granted /// with at minimum using `.check_read_all()`. fn cp_symlink_type( fs: &FileSystemRc, target: &CheckedPathBuf, link_path: &CheckedPathBuf, ) -> Option { #[cfg(windows)] { // Mirror node polyfill behavior: resolve target relative to the link // path, infer dir/file from stat, and default to file on any error. // https://github.com/nodejs/node/blob/70f6b58ac655234435a99d72b857dd7b316d34bf/lib/fs.js#L1806-L1837 if let Ok(absolute_target) = std::path::absolute( Path::new(link_path.as_os_str()) .join("..") .join(target.as_os_str()), ) { let checked_target = CheckedPathBuf::unsafe_new(absolute_target); let checked_target = checked_target.as_checked_path(); if let Ok(stat) = fs.stat_sync(&checked_target) && stat.is_directory { return Some(FsFileType::Directory); } } Some(FsFileType::File) } #[cfg(not(windows))] { let _ = (fs, target, link_path); None } } #[allow(clippy::unused_async, reason = "sometimes it's async")] async fn cp_create_symlink( state: &Rc>, fs: &FileSystemRc, target: String, link_path: String, ) -> Result<(), FsError> { { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::(); permissions.check_write_all("node:fs.symlink")?; permissions.check_read_all("node:fs.symlink")?; } let fs = fs.clone(); maybe_spawn_blocking!(move || -> Result<(), FsError> { // PERMISSIONS: ok because we verified --allow-write and --allow-read above let oldpath = CheckedPathBuf::unsafe_new(PathBuf::from(&target)); let newpath = CheckedPathBuf::unsafe_new(PathBuf::from(&link_path)); let file_type = cp_symlink_type(&fs, &oldpath, &newpath); let oldpath = oldpath.as_checked_path(); let newpath = newpath.as_checked_path(); fs.symlink_sync(&oldpath, &newpath, file_type) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(target), dest: Some(link_path), syscall: Some("symlink".into()), ..Default::default() }, ) })?; Ok(()) }) } fn check_cp_path( state: &Rc>, path: &str, access_kind: OpenAccessKind, ) -> Result { let mut state = state.borrow_mut(); Ok( state .borrow_mut::() .check_open( Cow::Owned(PathBuf::from(path)), access_kind, Some("node:fs.cp"), )? .into_owned(), ) } fn check_paths_impl_sync( state: &Rc>, fs: &FileSystemRc, src: &str, dest: &str, dereference: bool, ) -> Result { let (src_stat_result, dest_result, syscall) = if dereference { let src_path = check_cp_path(state, src, OpenAccessKind::Read)?; let dest_path = check_cp_path(state, dest, OpenAccessKind::Read)?; ( fs.stat_sync(&src_path.as_checked_path()), fs.stat_sync(&dest_path.as_checked_path()), "stat".to_string(), ) } else { let src_path = check_cp_path(state, src, OpenAccessKind::ReadNoFollow)?; let dest_path = check_cp_path(state, dest, OpenAccessKind::ReadNoFollow)?; ( fs.lstat_sync(&src_path.as_checked_path()), fs.lstat_sync(&dest_path.as_checked_path()), "lstat".to_string(), ) }; let src_stat = src_stat_result.map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.to_string()), syscall: Some(syscall.to_string()), ..Default::default() }, ) })?; let dest_stat = match dest_result { Ok(stat) => Some(stat), Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, Err(e) => { return Err(map_fs_error_to_node_fs_error( e, NodeFsErrorContext { path: Some(dest.to_string()), syscall: Some(syscall), ..Default::default() }, )); } }; let src_dev = src_stat.dev; let src_ino = src_stat.ino.unwrap_or(0); if let Some(ref dest_stat) = dest_stat { if are_identical_stat(&src_stat, dest_stat) { return Err( CpError::EInval { message: "src and dest cannot be the same".to_string(), path: dest.to_string(), } .into(), ); } if src_stat.is_directory && !dest_stat.is_directory { return Err( CpError::DirToNonDir { message: format!( "cannot overwrite non-directory {} with directory {}", dest, src ), path: dest.to_string(), } .into(), ); } if !src_stat.is_directory && dest_stat.is_directory { return Err( CpError::NonDirToDir { message: format!( "cannot overwrite directory {} with non-directory {}", dest, src ), path: dest.to_string(), } .into(), ); } } if src_stat.is_directory && is_src_subdir(src, dest) { return Err( CpError::EInval { message: format!( "cannot copy {} to a subdirectory of self {}", src, dest ), path: dest.to_string(), } .into(), ); } Ok(CpCheckPathsSyncResult { src_dev, src_ino, src_mode: src_stat.mode, is_dest_exists: dest_stat.is_some(), is_src_directory: src_stat.is_directory, is_src_file: src_stat.is_file, is_src_char_device: src_stat.is_char_device, is_src_block_device: src_stat.is_block_device, is_src_symlink: src_stat.is_symlink, is_src_socket: src_stat.is_socket, is_src_fifo: src_stat.is_fifo, }) } /// Validates src and dest paths for a cp operation. /// Checks identity, directory type conflicts, and subdirectory relationships. #[allow(clippy::unused_async, reason = "sometimes async depending on cfg")] async fn check_paths_impl( state: &Rc>, fs: &FileSystemRc, src: &str, dest: &str, dereference: bool, ) -> Result { let open_access_kind = if dereference { OpenAccessKind::Read } else { OpenAccessKind::ReadNoFollow }; let src_path = check_cp_path(state, src, open_access_kind)?; let dest_path = check_cp_path(state, dest, open_access_kind)?; let fs = fs.clone(); let (src_stat_result, dest_result, syscall) = maybe_spawn_blocking!(move || -> (FsResult<_>, FsResult<_>, String) { let src_path = src_path.as_checked_path(); let dest_path = dest_path.as_checked_path(); if dereference { ( fs.stat_sync(&src_path), fs.stat_sync(&dest_path), "stat".to_string(), ) } else { ( fs.lstat_sync(&src_path), fs.lstat_sync(&dest_path), "lstat".to_string(), ) } }); let src_stat = src_stat_result.map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.to_string()), syscall: Some(syscall.clone()), ..Default::default() }, ) })?; let dest_stat = match dest_result { Ok(stat) => Some(stat), Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, Err(e) => { return Err(map_fs_error_to_node_fs_error( e, NodeFsErrorContext { path: Some(dest.to_string()), syscall: Some(syscall), ..Default::default() }, )); } }; let src_dev = src_stat.dev; let src_ino = src_stat.ino.unwrap_or(0); if let Some(ref dest_stat) = dest_stat { if are_identical_stat(&src_stat, dest_stat) { return Err( CpError::EInval { message: "src and dest cannot be the same".to_string(), path: dest.to_string(), } .into(), ); } if src_stat.is_directory && !dest_stat.is_directory { return Err( CpError::DirToNonDir { message: format!( "cannot overwrite non-directory {} with directory {}", dest, src ), path: dest.to_string(), } .into(), ); } if !src_stat.is_directory && dest_stat.is_directory { return Err( CpError::NonDirToDir { message: format!( "cannot overwrite directory {} with non-directory {}", dest, src ), path: dest.to_string(), } .into(), ); } } if src_stat.is_directory && is_src_subdir(src, dest) { return Err( CpError::EInval { message: format!( "cannot copy {} to a subdirectory of self {}", src, dest ), path: dest.to_string(), } .into(), ); } Ok(CpCheckPathsResult { src_dev, src_ino, stat_info: compact_stat_info(&src_stat, dest_stat.is_some()), }) } /// Validates src and dest paths for recursive cp operations. /// Returns stat info for the source file #[op2(stack_trace)] #[bigint] pub async fn op_node_cp_check_paths_recursive( state: Rc>, #[string] src: String, #[string] dest: String, dereference: bool, ) -> Result { let fs = { let state = state.borrow(); state.borrow::().clone() }; let result = check_paths_impl(&state, &fs, &src, &dest, dereference).await?; Ok(result.stat_info) } /// Validates src and dest paths, checks parent paths, and ensures /// parent directory exists /// Returns stat info for the source file #[op2(stack_trace)] #[bigint] pub async fn op_node_cp_validate_and_prepare( state: Rc>, #[string] src: String, #[string] dest: String, dereference: bool, ) -> Result { let fs = { let state = state.borrow(); state.borrow::().clone() }; let check_result = check_paths_impl(&state, &fs, &src, &dest, dereference).await?; check_parent_paths_impl( &state, &fs, &src, check_result.src_dev, check_result.src_ino, &dest, ) .await?; ensure_parent_dir_impl(&state, &fs, &dest).await?; Ok(check_result.stat_info) } /// Validates src and dest paths, checks parent paths, and ensures /// parent directory exists for cpSync. /// Returns stat info for the source file. fn op_node_cp_validate_and_prepare_sync( state: &Rc>, fs: &FileSystemRc, src: &str, dest: &str, dereference: bool, ) -> Result { let check_result = check_paths_impl_sync(state, fs, src, dest, dereference)?; check_parent_paths_impl_sync( state, fs, src, check_result.src_dev, check_result.src_ino, dest, )?; ensure_parent_dir_impl_sync(state, fs, dest)?; Ok(check_result) } #[op2(fast, reentrant, stack_trace)] pub fn op_node_cp_sync<'a>( state: Rc>, scope: &mut v8::PinScope<'a, '_>, #[string] src: &str, #[string] dest: &str, dereference: bool, recursive: bool, force: bool, error_on_exist: bool, preserve_timestamps: bool, verbatim_symlinks: bool, #[smi] mode: u32, filter: v8::Local<'a, v8::Value>, ) -> Result<(), FsError> { let fs = { let state = state.borrow(); state.borrow::().clone() }; let stat_info = op_node_cp_validate_and_prepare_sync(&state, &fs, src, dest, dereference)?; let filter = v8::Local::::try_from(filter).ok(); let opts = CpSyncOptions { dereference, recursive, force, error_on_exist, preserve_timestamps, verbatim_symlinks, mode, filter, }; match cp_sync_dispatch(&state, &fs, scope, &stat_info, src, dest, opts)? { // Treat JsException as success here so the pending V8 exception can propagate // naturally, preserving the original JS stack instead of rethrowing from Rust. CpSyncRunStatus::Completed | CpSyncRunStatus::JsException => Ok(()), } } /// Recursively check if dest parent is a subdirectory of src. /// It works for all file types including symlinks since it /// checks the src and dest inodes. It starts from the deepest /// parent and stops once it reaches the src parent or the root path. async fn check_parent_paths_impl( state: &Rc>, fs: &FileSystemRc, src: &str, src_dev: u64, src_ino: u64, dest: &str, ) -> Result<(), FsError> { let src_parent = Path::new(src) .parent() .map(Cow::Borrowed) .unwrap_or_default(); let src_parent = deno_path_util::strip_unc_prefix( fs.realpath_sync(&CheckedPath::unsafe_new(Cow::Borrowed(&src_parent))) .unwrap_or_else(|_| { fs.cwd() .map(|cwd| cwd.join(&src_parent)) .unwrap_or_else(|_| src_parent.into_owned()) }), ); let mut current = Path::new(dest) .parent() .map(|p| p.to_path_buf()) .unwrap_or_default(); current = deno_path_util::strip_unc_prefix( fs.realpath_sync(&CheckedPath::unsafe_new(Cow::Borrowed(¤t))) .unwrap_or_else(|_| std::path::absolute(¤t).unwrap_or(current)), ); loop { if current == src_parent { return Ok(()); } // Check if current is the root if current.parent().is_none() || current.parent() == Some(¤t) { return Ok(()); } let current_str = current.to_string_lossy(); let checked_path = match check_cp_path(state, ¤t_str, OpenAccessKind::Read) { Ok(p) => p, // When read permission is ignored, the check returns NotFound. // Treat it like a non-existent directory: stop walking. Err(FsError::Permission(e)) if e.kind() == std::io::ErrorKind::NotFound => { return Ok(()); } Err(e) => return Err(e), }; let stat_result = fs.stat_async(checked_path).await; match stat_result { Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), Err(e) => { return Err(map_fs_error_to_node_fs_error( e, NodeFsErrorContext { path: Some(current_str.to_string()), syscall: Some("stat".to_string()), ..Default::default() }, )); } Ok(dest_stat) => { // Check if src and current parent are identical (same dev + ino) if let Some(dest_ino) = dest_stat.ino && dest_ino == src_ino && dest_stat.dev == src_dev { return Err( CpError::EInval { message: format!( "cannot copy {} to a subdirectory of self {}", src, dest ), path: dest.to_string(), } .into(), ); } } } current = match current.parent() { Some(p) => p.to_path_buf(), None => return Ok(()), }; } } fn check_parent_paths_impl_sync( state: &Rc>, fs: &FileSystemRc, src: &str, src_dev: u64, src_ino: u64, dest: &str, ) -> Result<(), FsError> { let src_parent = Path::new(src) .parent() .map(Cow::Borrowed) .unwrap_or_default(); let src_parent = deno_path_util::strip_unc_prefix( fs.realpath_sync(&CheckedPath::unsafe_new(Cow::Borrowed(&src_parent))) .unwrap_or_else(|_| { fs.cwd() .map(|cwd| cwd.join(&src_parent)) .unwrap_or_else(|_| src_parent.into_owned()) }), ); let mut current = Path::new(dest) .parent() .map(|p| p.to_path_buf()) .unwrap_or_default(); current = deno_path_util::strip_unc_prefix( fs.realpath_sync(&CheckedPath::unsafe_new(Cow::Borrowed(¤t))) .unwrap_or_else(|_| std::path::absolute(¤t).unwrap_or(current)), ); loop { if current == src_parent { return Ok(()); } // Check if current is the root if current.parent().is_none() || current.parent() == Some(¤t) { return Ok(()); } let current_str = current.to_string_lossy(); let checked_path = match check_cp_path(state, ¤t_str, OpenAccessKind::Read) { Ok(p) => p, // When read permission is ignored, the check returns NotFound. // Treat it like a non-existent directory: stop walking. Err(FsError::Permission(e)) if e.kind() == std::io::ErrorKind::NotFound => { return Ok(()); } Err(e) => return Err(e), }; match fs.stat_sync(&checked_path.as_checked_path()) { Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), Err(e) => { return Err(map_fs_error_to_node_fs_error( e, NodeFsErrorContext { path: Some(current_str.to_string()), syscall: Some("stat".to_string()), ..Default::default() }, )); } Ok(dest_stat) => { // Check if src and current parent are identical (same dev + ino) if let Some(dest_ino) = dest_stat.ino && dest_ino == src_ino && dest_stat.dev == src_dev { return Err( CpError::EInval { message: format!( "cannot copy {} to a subdirectory of self {}", src, dest ), path: dest.to_string(), } .into(), ); } } } current = match current.parent() { Some(p) => p.to_path_buf(), None => return Ok(()), }; } } /// Ensures the parent directory of `dest` exists, creating it recursively /// if needed. async fn ensure_parent_dir_impl( state: &Rc>, fs: &FileSystemRc, dest: &str, ) -> Result<(), FsError> { let dest_parent = Path::new(dest) .parent() .map(|p| p.to_path_buf()) .unwrap_or_default(); let parent_str = dest_parent.to_string_lossy(); let checked_parent = check_cp_path(state, &parent_str, OpenAccessKind::Read)?; let exists = fs.exists_async(checked_parent).await.map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(parent_str.to_string()), syscall: Some("exists".into()), ..Default::default() }, ) })?; if !exists { let checked_parent = check_cp_path(state, &parent_str, OpenAccessKind::Write)?; fs.mkdir_async(checked_parent, true, None) .await .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(parent_str.to_string()), syscall: Some("mkdir".into()), ..Default::default() }, ) })?; } Ok(()) } fn ensure_parent_dir_impl_sync( state: &Rc>, fs: &FileSystemRc, dest: &str, ) -> Result<(), FsError> { let dest_parent = Path::new(dest) .parent() .map(|p| p.to_path_buf()) .unwrap_or_default(); let parent_str = dest_parent.to_string_lossy(); let checked_parent = check_cp_path(state, &parent_str, OpenAccessKind::Read)?; let exists = fs.exists_sync(&checked_parent.as_checked_path()); if !exists { let checked_parent = check_cp_path(state, &parent_str, OpenAccessKind::Write)?; fs.mkdir_sync(&checked_parent.as_checked_path(), true, None) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(parent_str.to_string()), syscall: Some("mkdir".into()), ..Default::default() }, ) })?; } Ok(()) } fn handle_timestamps_and_mode( fs: &FileSystemRc, src_path: &CheckedPath, dest_path: &CheckedPath, src_mode: u32, ) -> Result<(), FsError> { // Make sure the file is writable before setting the timestamp // otherwise open fails with EPERM when invoked with 'r+' (through utimes call) if file_is_not_writable(src_mode) { let mode = src_mode | 0o200; set_dest_mode(fs, dest_path, mode)?; } // Set timestamps from a fresh stat of src (atime is modified by read). set_dest_timestamps_and_mode(fs, src_path, dest_path, src_mode)?; Ok(()) } fn file_is_not_writable(mode: u32) -> bool { (mode & 0o200) == 0 } fn cp_mode_has_copyfile_excl(mode: u32) -> bool { let copyfile_excl = UV_FS_COPYFILE_EXCL as u32; (mode & copyfile_excl) == copyfile_excl } fn set_dest_mode( fs: &FileSystemRc, dest_path: &CheckedPath, mode: u32, ) -> Result<(), FsError> { if mode == 0 { return Ok(()); } fs.chmod_sync(dest_path, mode as _).map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest_path.to_string_lossy().to_string()), syscall: Some("chmod".into()), ..Default::default() }, ) })?; Ok(()) } fn set_dest_timestamps_and_mode( fs: &FileSystemRc, src_path: &CheckedPath, dest_path: &CheckedPath, src_mode: u32, ) -> Result<(), FsError> { // Re-stat src to get fresh atime/mtime let src_stat = fs.stat_sync(src_path).map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src_path.to_string_lossy().to_string()), syscall: Some("stat".into()), ..Default::default() }, ) })?; if let (Some(atime), Some(mtime)) = (src_stat.atime, src_stat.mtime) { // FsStat stores times as milliseconds since the Unix epoch. // utime_async expects split values: whole seconds + nanoseconds remainder. let atime_secs = (atime / 1000) as i64; // Remaining milliseconds are converted to nanoseconds. let atime_nanos = ((atime % 1000) * 1_000_000) as u32; let mtime_secs = (mtime / 1000) as i64; // Same conversion for mtime: ms remainder -> ns. let mtime_nanos = ((mtime % 1000) * 1_000_000) as u32; fs.utime_sync(dest_path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest_path.to_string_lossy().to_string()), syscall: Some("utime".into()), ..Default::default() }, ) })?; } set_dest_mode(fs, dest_path, src_mode)?; Ok(()) } /// Handle copying a single file. /// /// If dest does not exist, copies src to dest directly. /// If dest exists and force is true, removes dest and copies. /// If dest exists and error_on_exist is true, returns an EExist error. /// Otherwise does nothing. /// /// When preserve_timestamps is true, copies the timestamps from src to dest /// and ensures the file is writable before doing so. #[op2(stack_trace)] pub async fn op_node_cp_on_file( state: Rc>, #[string] src: String, #[string] dest: String, #[smi] src_mode: u32, dest_exists: bool, force: bool, error_on_exist: bool, preserve_timestamps: bool, #[smi] mode: u32, ) -> Result<(), FsError> { let fs = { let state = state.borrow(); state.borrow::().clone() }; if dest_exists { if force { // Remove dest, then copy let dest_path = check_cp_path(&state, &dest, OpenAccessKind::Write)?; fs.remove_async(dest_path, false).await.map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest.clone()), syscall: Some("unlink".into()), ..Default::default() }, ) })?; } else if error_on_exist { return Err( CpError::EExist { message: format!("{} already exists", dest), path: dest.to_string(), } .into(), ); } else { // Neither force nor errorOnExist: do nothing return Ok(()); } } // Copy file: read from src, write to dest if cp_mode_has_copyfile_excl(mode) { let dest_path = check_cp_path(&state, &dest, OpenAccessKind::ReadNoFollow)?; match fs.lstat_async(dest_path.clone()).await { Ok(_) => { return Err( CpError::EExist { message: format!("{} already exists", dest), path: dest.to_string(), } .into(), ); } Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} Err(err) => { return Err(map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest.to_string()), syscall: Some("lstat".into()), ..Default::default() }, )); } } } let src_path = check_cp_path(&state, &src, OpenAccessKind::Read)?; let dest_path = check_cp_path(&state, &dest, OpenAccessKind::Write)?; maybe_spawn_blocking!(move || -> Result<(), FsError> { fs.copy_file_sync( &src_path.as_checked_path(), &dest_path.as_checked_path(), ) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src_path.to_string_lossy().to_string()), dest: Some(dest_path.to_string_lossy().to_string()), syscall: Some("copyfile".into()), ..Default::default() }, ) })?; if preserve_timestamps { handle_timestamps_and_mode( &fs, &src_path.as_checked_path(), &dest_path.as_checked_path(), src_mode, )?; } else { set_dest_mode(&fs, &dest_path.as_checked_path(), src_mode)?; } Ok(()) }) } fn op_node_cp_on_file_sync( state: &Rc>, fs: &FileSystemRc, src: &str, dest: &str, stat_info: &CpCheckPathsSyncResult, opts: &CpSyncOptions, ) -> Result<(), FsError> { if stat_info.is_dest_exists { if opts.force { // Remove dest, then copy. let dest_path = check_cp_path(state, dest, OpenAccessKind::Write)?; fs.remove_sync(&dest_path.as_checked_path(), false) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest.to_string()), syscall: Some("unlink".into()), ..Default::default() }, ) })?; } else if opts.error_on_exist { return Err( CpError::EExist { message: format!("{} already exists", dest), path: dest.to_string(), } .into(), ); } else { // Neither force nor errorOnExist: do nothing. return Ok(()); } } if cp_mode_has_copyfile_excl(opts.mode) { let dest_path = check_cp_path(state, dest, OpenAccessKind::ReadNoFollow)?; match fs.lstat_sync(&dest_path.as_checked_path()) { Ok(_) => { return Err( CpError::EExist { message: format!("{} already exists", dest), path: dest.to_string(), } .into(), ); } Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} Err(err) => { return Err(map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest.to_string()), syscall: Some("lstat".into()), ..Default::default() }, )); } } } let src_path = check_cp_path(state, src, OpenAccessKind::Read)?; let dest_path = check_cp_path(state, dest, OpenAccessKind::Write)?; fs.copy_file_sync(&src_path.as_checked_path(), &dest_path.as_checked_path()) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.to_string()), dest: Some(dest.to_string()), syscall: Some("copyfile".into()), ..Default::default() }, ) })?; if opts.preserve_timestamps { handle_timestamps_and_mode( fs, &src_path.as_checked_path(), &dest_path.as_checked_path(), stat_info.src_mode, )?; } else { set_dest_mode(fs, &dest_path.as_checked_path(), stat_info.src_mode)?; } Ok(()) } #[op2(stack_trace)] pub async fn op_node_cp_on_link( state: Rc>, #[string] src: String, #[string] dest: String, dest_exists: bool, verbatim_symlinks: bool, ) -> Result<(), FsError> { let fs = { let state = state.borrow(); state.borrow::().clone() }; let src_path = check_cp_path(&state, &src, OpenAccessKind::ReadNoFollow)?; let resolved_src_buf = fs.read_link_async(src_path.clone()).await.map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.clone()), syscall: Some("readlink".into()), ..Default::default() }, ) })?; let mut resolved_src = resolved_src_buf.to_string_lossy().to_string(); // Resolve relative symlink targets if !verbatim_symlinks && !Path::new(&resolved_src).is_absolute() && let Some(parent) = Path::new(&src).parent() { resolved_src = parent.join(&resolved_src).to_string_lossy().to_string(); } if !dest_exists { cp_create_symlink(&state, &fs, resolved_src.to_string(), dest).await?; return Ok(()); } // Dest exists — try to read it as a symlink let dest_path = check_cp_path(&state, &dest, OpenAccessKind::ReadNoFollow)?; let resolved_dest_result = fs.read_link_async(dest_path).await; let resolved_dest = match resolved_dest_result { Ok(p) => { let s = p.to_string_lossy().to_string(); // If relative, resolve against dirname(dest) if !Path::new(&s).is_absolute() { if let Some(parent) = Path::new(&dest).parent() { parent.join(&s).to_string_lossy().to_string() } else { s } } else { s } } Err(e) => { let kind = e.kind(); // EINVAL or UNKNOWN means dest is a regular file/directory, not a symlink if kind == std::io::ErrorKind::InvalidInput || kind == std::io::ErrorKind::Other { cp_create_symlink( &state, &fs, resolved_src.to_string(), dest.to_string(), ) .await?; return Ok(()); } #[cfg(windows)] { use winapi::shared::winerror::ERROR_NOT_A_REPARSE_POINT; let os_error = e.into_io_error(); let Some(errno) = os_error.raw_os_error() else { return Err(FsError::Io(os_error)); }; let errno = errno as u32; if errno != ERROR_NOT_A_REPARSE_POINT { return Err( NodeFsError { os_errno: errno as _, context: NodeFsErrorContext { path: Some(resolved_src), dest: Some(dest), syscall: Some("symlink".into()), ..Default::default() }, } .into(), ); } return cp_create_symlink( &state, &fs, resolved_src.to_string(), dest.to_string(), ) .await; } #[cfg(not(windows))] { return Err(map_fs_error_to_node_fs_error( e, NodeFsErrorContext { path: Some(resolved_src), dest: Some(dest), syscall: Some("symlink".into()), ..Default::default() }, )); } } }; // Check subdirectory relationships let src_path = check_cp_path(&state, &src, OpenAccessKind::Read)?; let src_stat = fs.stat_async(src_path).await.map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src), syscall: Some("stat".into()), ..Default::default() }, ) })?; let src_is_dir = src_stat.is_directory; if src_is_dir && is_src_subdir(&resolved_src, &resolved_dest) { return Err( CpError::EInval { message: format!( "cannot copy {} to a subdirectory of self {}", resolved_src, resolved_dest ), path: dest.to_string(), } .into(), ); } // Do not copy if src is a subdir of dest since unlinking // dest would remove src contents and create a broken symlink. if src_is_dir && is_src_subdir(&resolved_dest, &resolved_src) { return Err( CpError::SymlinkToSubdirectory { message: format!( "cannot overwrite {} with {}", resolved_dest, resolved_src ), path: dest.to_string(), } .into(), ); } { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::(); permissions.check_write_all("node:fs.cp")?; permissions.check_read_all("node:fs.cp")?; } // Unlink dest and create new symlink maybe_spawn_blocking!(move || -> Result<(), FsError> { let src_path_buf = CheckedPathBuf::unsafe_new(PathBuf::from(&resolved_src)); let dest_path_buf = CheckedPathBuf::unsafe_new(PathBuf::from(&dest)); let src_path = src_path_buf.as_checked_path(); let dest_path = dest_path_buf.as_checked_path(); fs.remove_sync(&dest_path, false).map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest_path.to_string_lossy().to_string()), syscall: Some("unlink".into()), ..Default::default() }, ) })?; let file_type = cp_symlink_type(&fs, &src_path_buf, &dest_path_buf); fs.symlink_sync(&src_path, &dest_path, file_type) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src_path.to_string_lossy().to_string()), dest: Some(dest_path.to_string_lossy().to_string()), syscall: Some("symlink".into()), ..Default::default() }, ) }) }) } fn op_node_cp_on_link_sync( state: &Rc>, fs: &FileSystemRc, src: &str, dest: &str, dest_exists: bool, verbatim_symlinks: bool, ) -> Result<(), FsError> { let src_path = check_cp_path(state, src, OpenAccessKind::ReadNoFollow)?; let resolved_src_buf = fs .read_link_sync(&src_path.as_checked_path()) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.to_string()), syscall: Some("readlink".into()), ..Default::default() }, ) })?; let mut resolved_src = resolved_src_buf.to_string_lossy().to_string(); // Resolve relative symlink targets. if !verbatim_symlinks && !Path::new(&resolved_src).is_absolute() && let Some(parent) = Path::new(src).parent() { resolved_src = parent.join(&resolved_src).to_string_lossy().to_string(); } if !dest_exists { { let mut state = state.borrow_mut(); state .borrow_mut::() .check_write_all("node:fs.symlink")?; state .borrow_mut::() .check_read_all("node:fs.symlink")?; } let oldpath = CheckedPathBuf::unsafe_new(PathBuf::from(&resolved_src)); let newpath = CheckedPathBuf::unsafe_new(PathBuf::from(dest)); let file_type = cp_symlink_type(fs, &oldpath, &newpath); fs.symlink_sync( &oldpath.as_checked_path(), &newpath.as_checked_path(), file_type, ) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(resolved_src), dest: Some(dest.to_string()), syscall: Some("symlink".into()), ..Default::default() }, ) })?; return Ok(()); } // Dest exists — try to read it as a symlink. let dest_path = check_cp_path(state, dest, OpenAccessKind::ReadNoFollow)?; let resolved_dest_result = fs.read_link_sync(&dest_path.as_checked_path()); let resolved_dest = match resolved_dest_result { Ok(p) => { let s = p.to_string_lossy().to_string(); // If relative, resolve against dirname(dest). if !Path::new(&s).is_absolute() { if let Some(parent) = Path::new(dest).parent() { parent.join(&s).to_string_lossy().to_string() } else { s } } else { s } } Err(e) => { let kind = e.kind(); // EINVAL or UNKNOWN means dest is a regular file/directory, not a symlink. if kind == std::io::ErrorKind::InvalidInput || kind == std::io::ErrorKind::Other { { let mut state = state.borrow_mut(); state .borrow_mut::() .check_write_all("node:fs.symlink")?; state .borrow_mut::() .check_read_all("node:fs.symlink")?; } let oldpath = CheckedPathBuf::unsafe_new(PathBuf::from(&resolved_src)); let newpath = CheckedPathBuf::unsafe_new(PathBuf::from(dest)); let file_type = cp_symlink_type(fs, &oldpath, &newpath); fs.symlink_sync( &oldpath.as_checked_path(), &newpath.as_checked_path(), file_type, ) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(resolved_src.clone()), dest: Some(dest.to_string()), syscall: Some("symlink".into()), ..Default::default() }, ) })?; return Ok(()); } #[cfg(windows)] { use winapi::shared::winerror::ERROR_NOT_A_REPARSE_POINT; let os_error = e.into_io_error(); let Some(errno) = os_error.raw_os_error() else { return Err(FsError::Io(os_error)); }; let errno = errno as u32; if errno != ERROR_NOT_A_REPARSE_POINT { return Err( NodeFsError { os_errno: errno as _, context: NodeFsErrorContext { path: Some(resolved_src), dest: Some(dest.to_string()), syscall: Some("symlink".into()), ..Default::default() }, } .into(), ); } { let mut state = state.borrow_mut(); state .borrow_mut::() .check_write_all("node:fs.symlink")?; state .borrow_mut::() .check_read_all("node:fs.symlink")?; } let oldpath = CheckedPathBuf::unsafe_new(PathBuf::from(&resolved_src)); let newpath = CheckedPathBuf::unsafe_new(PathBuf::from(dest)); let file_type = cp_symlink_type(fs, &oldpath, &newpath); fs.symlink_sync( &oldpath.as_checked_path(), &newpath.as_checked_path(), file_type, ) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(resolved_src.clone()), dest: Some(dest.to_string()), syscall: Some("symlink".into()), ..Default::default() }, ) })?; return Ok(()); } #[cfg(not(windows))] { return Err(map_fs_error_to_node_fs_error( e, NodeFsErrorContext { path: Some(resolved_src), dest: Some(dest.to_string()), syscall: Some("symlink".into()), ..Default::default() }, )); } } }; // Check subdirectory relationships. let src_path = check_cp_path(state, src, OpenAccessKind::Read)?; let src_stat = fs.stat_sync(&src_path.as_checked_path()).map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src.to_string()), syscall: Some("stat".into()), ..Default::default() }, ) })?; let src_is_dir = src_stat.is_directory; if src_is_dir && is_src_subdir(&resolved_src, &resolved_dest) { return Err( CpError::EInval { message: format!( "cannot copy {} to a subdirectory of self {}", resolved_src, resolved_dest ), path: dest.to_string(), } .into(), ); } // Do not copy if src is a subdir of dest since unlinking // dest would remove src contents and create a broken symlink. if src_is_dir && is_src_subdir(&resolved_dest, &resolved_src) { return Err( CpError::SymlinkToSubdirectory { message: format!( "cannot overwrite {} with {}", resolved_dest, resolved_src ), path: dest.to_string(), } .into(), ); } let dest_path = { let mut state = state.borrow_mut(); state.borrow_mut::().check_open( Cow::Owned(PathBuf::from(dest)), OpenAccessKind::Write, Some("node:fs.rm"), )? }; fs.remove_sync(&dest_path, false).map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(dest_path.to_string_lossy().to_string()), syscall: Some("unlink".into()), ..Default::default() }, ) })?; { let mut state = state.borrow_mut(); state .borrow_mut::() .check_write_all("node:fs.symlink")?; state .borrow_mut::() .check_read_all("node:fs.symlink")?; } let src_path_buf = CheckedPathBuf::unsafe_new(PathBuf::from(&resolved_src)); let dest_path_buf = CheckedPathBuf::unsafe_new(dest_path.to_path_buf()); let src_path = src_path_buf.as_checked_path(); let file_type = cp_symlink_type(fs, &src_path_buf, &dest_path_buf); fs.symlink_sync(&src_path, &dest_path, file_type) .map_err(|err| { map_fs_error_to_node_fs_error( err, NodeFsErrorContext { path: Some(src_path.to_string_lossy().to_string()), dest: Some(dest_path.to_string_lossy().to_string()), syscall: Some("symlink".into()), ..Default::default() }, ) }) } #[cfg(test)] mod tests { use super::is_src_subdir; #[test] fn test_is_src_subdir() { let base = std::env::temp_dir().join("deno_is_src_subdir_test"); let src = base.join("src"); let child = src.join("child"); let sibling = base.join("sibling"); let src = src.to_string_lossy().into_owned(); let child = child.to_string_lossy().into_owned(); let sibling = sibling.to_string_lossy().into_owned(); assert!(is_src_subdir(&src, &child)); assert!(is_src_subdir(&src, &src)); assert!(!is_src_subdir(&src, &sibling)); assert!(!is_src_subdir(&child, &src)); } }