/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/fs/std_fs.rs
1 438 строк
40 KB
Bartek Iwańczuk
perf(fs): open files on the blocking pool in async readFile/writeFile (#36160)
27 июл 2026, 15:58
Не верифицирован
27 июл 2026, 15:58
936d900
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. #![allow(clippy::disallowed_methods, reason = "file system implementation")] use std::borrow::Cow; use std::fs; use std::io; use std::io::ErrorKind; use std::io::Read; use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::sync::Mutex; use deno_core::unsync::spawn_blocking; use deno_io::StdFileResourceInner; use deno_io::fs::File; use deno_io::fs::FsError; use deno_io::fs::FsResult; use deno_io::fs::FsStat; use deno_io::fs::FsStatFs; use deno_maybe_sync::new_rc; use deno_permissions::CheckedPath; use deno_permissions::CheckedPathBuf; use crate::FileSystem; use crate::OpenOptions; use crate::interface::FsDirEntry; use crate::interface::FsFileType; use crate::interface::FsReadDir; use crate::interface::FsReadDirRc; #[derive(Debug, Default, Clone)] pub struct RealFs; #[async_trait::async_trait(?Send)] impl FileSystem for RealFs { fn cwd(&self) -> FsResult<PathBuf> { std::env::current_dir().map_err(Into::into) } fn tmp_dir(&self) -> FsResult<PathBuf> { Ok(std::env::temp_dir()) } fn chdir(&self, path: &CheckedPath) -> FsResult<()> { std::env::set_current_dir(path).map_err(Into::into) } #[cfg(windows)] fn umask(&self, mask: Option<u32>) -> FsResult<u32> { unsafe extern "C" { fn _umask(mask: std::ffi::c_int) -> std::ffi::c_int; } // SAFETY: `_umask` is a Windows CRT function that sets the file mode // creation mask and returns the previous value. unsafe { let old = if let Some(mask) = mask { _umask(mask as std::ffi::c_int) } else { let prev = _umask(0); _umask(prev); prev }; Ok(old as u32) } } #[cfg(unix)] fn umask(&self, mask: Option<u32>) -> FsResult<u32> { use nix::sys::stat::Mode; use nix::sys::stat::mode_t; use nix::sys::stat::umask; 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(0)); let _ = umask(prev); prev }; #[cfg(any(target_os = "android", target_os = "linux"))] { Ok(r.bits()) } #[cfg(any( target_os = "macos", target_os = "openbsd", target_os = "freebsd" ))] { Ok(r.bits() as u32) } } fn open_sync( &self, path: &CheckedPath, options: OpenOptions, ) -> FsResult<Rc<dyn File>> { let std_file = open_with_checked_path(options, path)?; Ok(Rc::new(StdFileResourceInner::file( std_file, Some(path.to_path_buf()), ))) } async fn open_async<'a>( &'a self, path: CheckedPathBuf, options: OpenOptions, ) -> FsResult<Rc<dyn File>> { // Open on the blocking pool: opening a FIFO with O_RDONLY (or O_WRONLY) // blocks until the other end is opened, which would otherwise stall the // runtime thread. let std_file = spawn_blocking(move || { open_with_checked_path(options, &path.as_checked_path()) .map(|f| (f, path)) }) .await??; let (std_file, path) = std_file; Ok(Rc::new(StdFileResourceInner::file( std_file, Some(path.to_path_buf()), ))) } fn mkdir_sync( &self, path: &CheckedPath, recursive: bool, mode: Option<u32>, ) -> FsResult<()> { mkdir(path, recursive, mode) } async fn mkdir_async( &self, path: CheckedPathBuf, recursive: bool, mode: Option<u32>, ) -> FsResult<()> { spawn_blocking(move || mkdir(&path, recursive, mode)).await? } #[cfg(unix)] fn chmod_sync(&self, path: &CheckedPath, mode: u32) -> FsResult<()> { chmod(path, mode) } #[cfg(not(unix))] fn chmod_sync(&self, path: &CheckedPath, mode: i32) -> FsResult<()> { chmod(path, mode) } #[cfg(unix)] async fn chmod_async(&self, path: CheckedPathBuf, mode: u32) -> FsResult<()> { spawn_blocking(move || chmod(&path, mode)).await? } #[cfg(not(unix))] async fn chmod_async(&self, path: CheckedPathBuf, mode: i32) -> FsResult<()> { spawn_blocking(move || chmod(&path, mode)).await? } fn chown_sync( &self, path: &CheckedPath, uid: Option<u32>, gid: Option<u32>, ) -> FsResult<()> { chown(path, uid, gid) } async fn chown_async( &self, path: CheckedPathBuf, uid: Option<u32>, gid: Option<u32>, ) -> FsResult<()> { spawn_blocking(move || chown(&path, uid, gid)).await? } fn remove_sync(&self, path: &CheckedPath, recursive: bool) -> FsResult<()> { remove(path, recursive) } async fn remove_async( &self, path: CheckedPathBuf, recursive: bool, ) -> FsResult<()> { spawn_blocking(move || remove(&path, recursive)).await? } fn copy_file_sync( &self, from: &CheckedPath, to: &CheckedPath, ) -> FsResult<()> { copy_file(from, to) } async fn copy_file_async( &self, from: CheckedPathBuf, to: CheckedPathBuf, ) -> FsResult<()> { spawn_blocking(move || copy_file(&from, &to)).await? } fn cp_sync(&self, fro: &CheckedPath, to: &CheckedPath) -> FsResult<()> { cp(fro, to) } async fn cp_async( &self, fro: CheckedPathBuf, to: CheckedPathBuf, ) -> FsResult<()> { spawn_blocking(move || cp(&fro, &to)).await? } fn stat_sync(&self, path: &CheckedPath) -> FsResult<FsStat> { stat(path) } async fn stat_async(&self, path: CheckedPathBuf) -> FsResult<FsStat> { spawn_blocking(move || stat(&path)).await? } fn lstat_sync(&self, path: &CheckedPath) -> FsResult<FsStat> { lstat(path) } async fn lstat_async(&self, path: CheckedPathBuf) -> FsResult<FsStat> { spawn_blocking(move || lstat(&path)).await? } fn statfs_sync( &self, path: &CheckedPath, bigint: bool, ) -> FsResult<FsStatFs> { statfs(path, bigint) } async fn statfs_async( &self, path: CheckedPathBuf, bigint: bool, ) -> FsResult<FsStatFs> { spawn_blocking(move || statfs(&path, bigint)).await? } fn exists_sync(&self, path: &CheckedPath) -> bool { exists(path) } async fn exists_async(&self, path: CheckedPathBuf) -> FsResult<bool> { spawn_blocking(move || exists(&path)) .await .map_err(Into::into) } fn realpath_sync(&self, path: &CheckedPath) -> FsResult<PathBuf> { realpath(path) } async fn realpath_async(&self, path: CheckedPathBuf) -> FsResult<PathBuf> { spawn_blocking(move || realpath(&path)).await? } fn read_dir_sync(&self, path: &CheckedPath) -> FsResult<Vec<FsDirEntry>> { read_dir(path) } async fn read_dir_async( &self, path: CheckedPathBuf, ) -> FsResult<FsReadDirRc> { Ok(new_rc(RealFsReadDir(Mutex::new( tokio::fs::read_dir(path).await?, )))) } fn rename_sync( &self, oldpath: &CheckedPath, newpath: &CheckedPath, ) -> FsResult<()> { fs::rename(oldpath, newpath).map_err(Into::into) } async fn rename_async( &self, oldpath: CheckedPathBuf, newpath: CheckedPathBuf, ) -> FsResult<()> { spawn_blocking(move || fs::rename(oldpath, newpath)) .await? .map_err(Into::into) } fn lchmod_sync(&self, path: &CheckedPath, mode: u32) -> FsResult<()> { lchmod(path, mode) } async fn lchmod_async( &self, path: CheckedPathBuf, mode: u32, ) -> FsResult<()> { spawn_blocking(move || lchmod(&path, mode)).await? } fn link_sync( &self, oldpath: &CheckedPath, newpath: &CheckedPath, ) -> FsResult<()> { fs::hard_link(oldpath, newpath).map_err(Into::into) } async fn link_async( &self, oldpath: CheckedPathBuf, newpath: CheckedPathBuf, ) -> FsResult<()> { spawn_blocking(move || fs::hard_link(oldpath, newpath)) .await? .map_err(Into::into) } fn symlink_sync( &self, oldpath: &CheckedPath, newpath: &CheckedPath, file_type: Option<FsFileType>, ) -> FsResult<()> { symlink(oldpath, newpath, file_type) } async fn symlink_async( &self, oldpath: CheckedPathBuf, newpath: CheckedPathBuf, file_type: Option<FsFileType>, ) -> FsResult<()> { spawn_blocking(move || symlink(&oldpath, &newpath, file_type)).await? } fn read_link_sync(&self, path: &CheckedPath) -> FsResult<PathBuf> { fs::read_link(path).map_err(Into::into) } async fn read_link_async(&self, path: CheckedPathBuf) -> FsResult<PathBuf> { spawn_blocking(move || fs::read_link(path)) .await? .map_err(Into::into) } fn rmdir_sync(&self, path: &CheckedPath) -> FsResult<()> { fs::remove_dir(path).map_err(Into::into) } async fn rmdir_async(&self, path: CheckedPathBuf) -> FsResult<()> { spawn_blocking(move || fs::remove_dir(path)) .await? .map_err(Into::into) } fn truncate_sync(&self, path: &CheckedPath, len: u64) -> FsResult<()> { truncate(path, len) } async fn truncate_async( &self, path: CheckedPathBuf, len: u64, ) -> FsResult<()> { spawn_blocking(move || truncate(&path, len)).await? } fn utime_sync( &self, path: &CheckedPath, atime_secs: i64, atime_nanos: u32, mtime_secs: i64, mtime_nanos: u32, ) -> FsResult<()> { let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); filetime::set_file_times(path, atime, mtime).map_err(Into::into) } async fn utime_async( &self, path: CheckedPathBuf, atime_secs: i64, atime_nanos: u32, mtime_secs: i64, mtime_nanos: u32, ) -> FsResult<()> { let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); spawn_blocking(move || { filetime::set_file_times(path, atime, mtime).map_err(Into::into) }) .await? } fn lutime_sync( &self, path: &CheckedPath, atime_secs: i64, atime_nanos: u32, mtime_secs: i64, mtime_nanos: u32, ) -> FsResult<()> { let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); filetime::set_symlink_file_times(path, atime, mtime).map_err(Into::into) } async fn lutime_async( &self, path: CheckedPathBuf, atime_secs: i64, atime_nanos: u32, mtime_secs: i64, mtime_nanos: u32, ) -> FsResult<()> { let atime = filetime::FileTime::from_unix_time(atime_secs, atime_nanos); let mtime = filetime::FileTime::from_unix_time(mtime_secs, mtime_nanos); spawn_blocking(move || { filetime::set_symlink_file_times(path, atime, mtime).map_err(Into::into) }) .await? } fn lchown_sync( &self, path: &CheckedPath, uid: Option<u32>, gid: Option<u32>, ) -> FsResult<()> { lchown(path, uid, gid) } async fn lchown_async( &self, path: CheckedPathBuf, uid: Option<u32>, gid: Option<u32>, ) -> FsResult<()> { spawn_blocking(move || lchown(&path, uid, gid)).await? } fn write_file_sync( &self, path: &CheckedPath, options: OpenOptions, data: &[u8], ) -> FsResult<()> { let mut file = open_with_checked_path(options, path)?; #[cfg(unix)] if let Some(mode) = options.mode { use std::os::unix::fs::PermissionsExt; file.set_permissions(fs::Permissions::from_mode(mode))?; } file.write_all(data)?; Ok(()) } async fn write_file_async<'a>( &'a self, path: CheckedPathBuf, options: OpenOptions, data: Box<[u8]>, ) -> FsResult<()> { // Open on the blocking pool as well: open() can block (e.g. FIFOs, // network filesystems) and must not stall the runtime thread. spawn_blocking(move || { let mut file = open_with_checked_path(options, &path.as_checked_path())?; #[cfg(unix)] if let Some(mode) = options.mode { use std::os::unix::fs::PermissionsExt; file.set_permissions(fs::Permissions::from_mode(mode))?; } file.write_all(&data)?; Ok(()) }) .await? } fn read_file_sync( &self, path: &CheckedPath, options: OpenOptions, ) -> FsResult<Cow<'static, [u8]>> { let mut file = open_with_checked_path(options, path)?; let mut buf = Vec::new(); file.read_to_end(&mut buf)?; Ok(Cow::Owned(buf)) } async fn read_file_async<'a>( &'a self, path: CheckedPathBuf, options: OpenOptions, ) -> FsResult<Cow<'static, [u8]>> { // Open on the blocking pool as well: open() can block (e.g. FIFOs, // network filesystems) and must not stall the runtime thread. spawn_blocking(move || { let mut file = open_with_checked_path(options, &path.as_checked_path())?; let mut buf = Vec::new(); file.read_to_end(&mut buf)?; Ok::<_, FsError>(Cow::Owned(buf)) }) .await? } } fn mkdir(path: &Path, recursive: bool, mode: Option<u32>) -> FsResult<()> { let mut builder = fs::DirBuilder::new(); builder.recursive(recursive); #[cfg(unix)] if let Some(mode) = mode { use std::os::unix::fs::DirBuilderExt; builder.mode(mode); } #[cfg(not(unix))] { _ = mode; } builder.create(path).map_err(Into::into) } #[cfg(unix)] fn chmod(path: &Path, mode: u32) -> FsResult<()> { use std::os::unix::fs::PermissionsExt; let permissions = fs::Permissions::from_mode(mode); fs::set_permissions(path, permissions)?; Ok(()) } #[cfg(not(unix))] fn chmod(path: &Path, mode: i32) -> FsResult<()> { use std::os::windows::ffi::OsStrExt; // Windows chmod doesn't follow symlinks unlike the UNIX counterpart, // so we have to resolve the symlink manually let resolved_path = realpath(path)?; let wchar_path = resolved_path .as_os_str() .encode_wide() .chain(std::iter::once(0)) .collect::<Vec<_>>(); // SAFETY: `path` is a null-terminated string. let result = unsafe { libc::wchmod(wchar_path.as_ptr(), mode) }; if result != 0 { return Err(io::Error::last_os_error().into()); } Ok(()) } #[cfg(unix)] fn chown(path: &Path, uid: Option<u32>, gid: Option<u32>) -> FsResult<()> { use nix::unistd::Gid; use nix::unistd::Uid; use nix::unistd::chown; let owner = uid.map(Uid::from_raw); let group = gid.map(Gid::from_raw); let res = chown(path, owner, group); if let Err(err) = res { return Err(io::Error::from_raw_os_error(err as i32).into()); } Ok(()) } // TODO: implement chown for Windows #[cfg(not(unix))] fn chown(_path: &Path, _uid: Option<u32>, _gid: Option<u32>) -> FsResult<()> { Err(FsError::NotSupported) } #[cfg(target_os = "macos")] fn lchmod(path: &Path, mode: u32) -> FsResult<()> { use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::PermissionsExt; use libc::O_SYMLINK; let file = fs::OpenOptions::new() .write(true) .custom_flags(O_SYMLINK) .open(path)?; file.set_permissions(fs::Permissions::from_mode(mode))?; Ok(()) } #[cfg(not(target_os = "macos"))] fn lchmod(_path: &Path, _mode: u32) -> FsResult<()> { Err(FsError::NotSupported) } #[cfg(unix)] fn lchown(path: &Path, uid: Option<u32>, gid: Option<u32>) -> FsResult<()> { use std::os::unix::ffi::OsStrExt; let c_path = std::ffi::CString::new(path.as_os_str().as_bytes()).unwrap(); // -1 = leave unchanged let uid = uid .map(|uid| uid as libc::uid_t) .unwrap_or(-1i32 as libc::uid_t); let gid = gid .map(|gid| gid as libc::gid_t) .unwrap_or(-1i32 as libc::gid_t); // SAFETY: `c_path` is a valid C string and lives throughout this function call. let result = unsafe { libc::lchown(c_path.as_ptr(), uid, gid) }; if result != 0 { return Err(io::Error::last_os_error().into()); } Ok(()) } // TODO: implement lchown for Windows #[cfg(not(unix))] fn lchown(_path: &Path, _uid: Option<u32>, _gid: Option<u32>) -> FsResult<()> { Err(FsError::NotSupported) } fn remove(path: &Path, recursive: bool) -> FsResult<()> { // TODO: this is racy. This should open fds, and then `unlink` those. let metadata = fs::symlink_metadata(path)?; let file_type = metadata.file_type(); let res = if file_type.is_dir() { if recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) } } else if file_type.is_symlink() { #[cfg(unix)] { fs::remove_file(path) } #[cfg(not(unix))] { use std::os::windows::prelude::MetadataExt; use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY; if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { fs::remove_dir(path) } else { fs::remove_file(path) } } } else { fs::remove_file(path) }; res.map_err(Into::into) } fn copy_file(from: &Path, to: &Path) -> FsResult<()> { // Guard against copying a file onto itself. Otherwise the destination is // opened with truncation (or unlinked) before the source is read, which // silently empties the file. Match `cp` behavior and error instead. // // `same_file::is_same_file` compares the file identity (device + inode on // Unix, file index + volume serial via the open handle on Windows) using a // single stat per path, rather than fully canonicalizing both paths which // would `lstat`/`readlink` every component twice. It still catches // equivalent paths such as `./`, `..`, symlinks and hard links, and returns // `Err` (treated as "not the same file") in the common case where the // destination does not yet exist. if same_file::is_same_file(from, to).unwrap_or(false) { return Err( io::Error::new( io::ErrorKind::InvalidInput, "Source and destination paths refer to the same file", ) .into(), ); } #[cfg(target_os = "macos")] { use std::ffi::CString; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::PermissionsExt; use libc::clonefile; use libc::stat; use libc::unlink; let from_str = CString::new(from.as_os_str().as_encoded_bytes()) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; let to_str = CString::new(to.as_os_str().as_encoded_bytes()) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; // 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_str.as_ptr(), &mut st); if ret != 0 { return Err(io::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_str.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_str.as_ptr(), to_str.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 = fs::File::open(from)?; let perm = from_file.metadata()?.permissions(); let mut to_file = fs::OpenOptions::new() // create the file with the correct mode right away .mode(perm.mode()) .write(true) .create(true) .truncate(true) .open(to)?; 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)?; if nread == 0 { break; } to_file.write_all(&buf[..nread])?; } return Ok(()); } } // clonefile() failed, fall back to std::fs::copy(). } fs::copy(from, to)?; Ok(()) } fn cp(from: &Path, to: &Path) -> FsResult<()> { fn cp_(source_meta: fs::Metadata, from: &Path, to: &Path) -> FsResult<()> { use rayon::prelude::IntoParallelIterator; use rayon::prelude::ParallelIterator; let ty = source_meta.file_type(); if ty.is_dir() { #[allow(unused_mut, reason = "mutable on unix")] let mut builder = fs::DirBuilder::new(); #[cfg(unix)] { use std::os::unix::fs::DirBuilderExt; use std::os::unix::fs::PermissionsExt; builder.mode(fs::symlink_metadata(from)?.permissions().mode()); } // The target directory might already exists. If it does, // continue copying all entries instead of aborting. if let Err(err) = builder.create(to) && err.kind() != ErrorKind::AlreadyExists { return Err(FsError::Io(err)); } let mut entries: Vec<_> = fs::read_dir(from)? .map(|res| res.map(|e| e.file_name())) .collect::<Result<_, _>>()?; entries.shrink_to_fit(); entries .into_par_iter() .map(|file_name| { let from_path = from.join(&file_name); let to_path = to.join(&file_name); let meta = fs::symlink_metadata(&from_path).map_err(|err| { io::Error::new( err.kind(), format!( "failed to copy '{}' to '{}': {:?}", from_path.display(), to_path.display(), err, ), ) })?; cp_(meta, &from_path, &to_path).map_err(|err| { io::Error::new( err.kind(), format!( "failed to copy '{}' to '{}': {:?}", from_path.display(), to_path.display(), err, ), ) }) }) .collect::<Result<Vec<_>, _>>()?; return Ok(()); } else if ty.is_symlink() { let from = std::fs::read_link(from)?; #[cfg(unix)] std::os::unix::fs::symlink(from, to)?; #[cfg(windows)] std::os::windows::fs::symlink_file(from, to)?; return Ok(()); } #[cfg(unix)] { use std::os::unix::fs::FileTypeExt; if ty.is_socket() { return Err( io::Error::new( io::ErrorKind::InvalidInput, "sockets cannot be copied", ) .into(), ); } } // Ensure parent destination directory exists if let Some(parent) = to.parent() { fs::create_dir_all(parent)?; } copy_file(from, to) } #[cfg(target_os = "macos")] { // Just clonefile() use std::ffi::CString; use std::os::unix::ffi::OsStrExt; use libc::clonefile; use libc::unlink; let from_str = CString::new(from.as_os_str().as_bytes()) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; let to_str = CString::new(to.as_os_str().as_bytes()) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; // SAFETY: `from` and `to` are valid C strings. unsafe { // Try unlink. If it fails, we are going to try clonefile() anyway. let _ = unlink(to_str.as_ptr()); if clonefile(from_str.as_ptr(), to_str.as_ptr(), 0) == 0 { return Ok(()); } } } let source_meta = fs::symlink_metadata(from)?; #[inline] fn is_identical( source_meta: &fs::Metadata, dest_meta: &fs::Metadata, ) -> bool { #[cfg(unix)] { use std::os::unix::fs::MetadataExt; source_meta.ino() == dest_meta.ino() } #[cfg(windows)] { use std::os::windows::fs::MetadataExt; // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information // // The identifier (low and high parts) and the volume serial number uniquely identify a file on a single computer. // To determine whether two open handles represent the same file, combine the identifier and the volume serial // number for each file and compare them. // // Use this code once file_index() and volume_serial_number() is stabalized // See: https://github.com/rust-lang/rust/issues/63010 // // source_meta.file_index() == dest_meta.file_index() // && source_meta.volume_serial_number() // == dest_meta.volume_serial_number() source_meta.last_write_time() == dest_meta.last_write_time() && source_meta.creation_time() == dest_meta.creation_time() } } if let Ok(m) = fs::metadata(to) && m.is_dir() { // Only target sub dir when source is not a dir itself if let Ok(from_meta) = fs::metadata(from) && !from_meta.is_dir() { return cp_( source_meta, from, &to.join(from.file_name().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "the source path is not a valid file", ) })?), ); } } if let Ok(m) = fs::symlink_metadata(to) && is_identical(&source_meta, &m) { return Err( io::Error::new( io::ErrorKind::InvalidInput, "the source and destination are the same file", ) .into(), ); } cp_(source_meta, from, to) } #[cfg(not(windows))] fn stat(path: &Path) -> FsResult<FsStat> { let metadata = fs::metadata(path)?; Ok(FsStat::from_std(metadata)) } #[cfg(windows)] fn stat(path: &Path) -> FsResult<FsStat> { let file = open_for_stat_windows(path, false)?; let metadata = file.metadata()?; let mut fsstat = FsStat::from_std(metadata); deno_io::stat_extra(&file, &mut fsstat)?; Ok(fsstat) } #[cfg(not(windows))] fn lstat(path: &Path) -> FsResult<FsStat> { let metadata = fs::symlink_metadata(path)?; Ok(FsStat::from_std(metadata)) } #[cfg(windows)] fn lstat(path: &Path) -> FsResult<FsStat> { let file = open_for_stat_windows(path, true)?; let metadata = file.metadata()?; let mut fsstat = FsStat::from_std(metadata); deno_io::stat_extra(&file, &mut fsstat)?; Ok(fsstat) } // Some Windows file system drivers (notably ImDisk-backed memory disks) // reject `FILE_FLAG_BACKUP_SEMANTICS` for regular files and return // `ERROR_INVALID_FUNCTION` (1). Deno passes that flag so `CreateFile` can // open directories; on those drivers we transparently retry without it. // See https://github.com/denoland/deno/issues/26257. #[cfg(windows)] fn open_for_stat_windows( path: &Path, do_not_follow_symlink: bool, ) -> io::Result<fs::File> { use std::os::windows::fs::OpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT; let reparse_flag = if do_not_follow_symlink { FILE_FLAG_OPEN_REPARSE_POINT } else { 0 }; let mut opts = fs::OpenOptions::new(); opts.access_mode(0); // no read or write opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | reparse_flag); match opts.open(path) { Ok(file) => Ok(file), Err(err) if err.raw_os_error() == Some(ERROR_INVALID_FUNCTION) => { let mut fallback = fs::OpenOptions::new(); fallback.access_mode(0); fallback.custom_flags(reparse_flag); fallback.open(path).map_err(|_| err) } Err(err) => Err(err), } } #[cfg(windows)] const ERROR_INVALID_FUNCTION: i32 = 1; fn statfs(path: &Path, bigint: bool) -> FsResult<FsStatFs> { #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; let mut cpath = path.as_os_str().as_bytes().to_vec(); cpath.push(0); if bigint { #[cfg(not(any( target_os = "macos", target_os = "freebsd", target_os = "openbsd" )))] // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. let (code, result) = unsafe { let mut result: libc::statfs64 = std::mem::zeroed(); (libc::statfs64(cpath.as_ptr() as _, &mut result), result) }; #[cfg(any( target_os = "macos", target_os = "freebsd", target_os = "openbsd" ))] // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. let (code, result) = unsafe { let mut result: libc::statfs = std::mem::zeroed(); (libc::statfs(cpath.as_ptr() as _, &mut result), result) }; if code == -1 { return Err(std::io::Error::last_os_error().into()); } Ok(FsStatFs { #[cfg(not(target_os = "openbsd"))] typ: result.f_type as _, #[cfg(target_os = "openbsd")] typ: 0 as _, bsize: result.f_bsize as _, blocks: result.f_blocks as _, bfree: result.f_bfree as _, bavail: result.f_bavail as _, files: result.f_files as _, ffree: result.f_ffree as _, }) } else { // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. let (code, result) = unsafe { let mut result: libc::statfs = std::mem::zeroed(); (libc::statfs(cpath.as_ptr() as _, &mut result), result) }; if code == -1 { return Err(std::io::Error::last_os_error().into()); } Ok(FsStatFs { #[cfg(not(target_os = "openbsd"))] typ: result.f_type as _, #[cfg(target_os = "openbsd")] typ: 0 as _, bsize: result.f_bsize as _, blocks: result.f_blocks as _, bfree: result.f_bfree as _, bavail: result.f_bavail as _, files: result.f_files as _, ffree: result.f_ffree as _, }) } } #[cfg(windows)] { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceW; let _ = bigint; let path = path.canonicalize()?; let root = path.ancestors().last().ok_or_else(|| { std::io::Error::new(ErrorKind::NotFound, "Path has no root.") })?; let mut root = OsStr::new(root).encode_wide().collect::<Vec<_>>(); root.push(0); let mut sectors_per_cluster = 0; let mut bytes_per_sector = 0; let mut available_clusters = 0; let mut total_clusters = 0; let mut code = 0; let mut retries = 0; // We retry here because libuv does: https://github.com/libuv/libuv/blob/fa6745b4f26470dae5ee4fcbb1ee082f780277e0/src/win/fs.c#L2705 while code == 0 && retries < 2 { // SAFETY: Normal GetDiskFreeSpaceW usage. code = unsafe { GetDiskFreeSpaceW( root.as_ptr(), &mut sectors_per_cluster, &mut bytes_per_sector, &mut available_clusters, &mut total_clusters, ) }; retries += 1; } if code == 0 { return Err(std::io::Error::last_os_error().into()); } Ok(FsStatFs { typ: 0, bsize: (bytes_per_sector * sectors_per_cluster) as _, blocks: total_clusters as _, bfree: available_clusters as _, bavail: available_clusters as _, files: 0, ffree: 0, }) } #[cfg(not(any(unix, windows)))] { let _ = path; let _ = bigint; Err(FsError::NotSupported) } } fn exists(path: &Path) -> bool { #[cfg(unix)] { use nix::unistd::AccessFlags; use nix::unistd::access; access(path, AccessFlags::F_OK).is_ok() } #[cfg(windows)] { fs::exists(path).unwrap_or(false) } } fn realpath(path: &Path) -> FsResult<PathBuf> { Ok(deno_path_util::strip_unc_prefix(path.canonicalize()?)) } fn read_dir(path: &Path) -> FsResult<Vec<FsDirEntry>> { let entries = fs::read_dir(path)? .filter_map(|entry| { let entry = entry.ok()?; // Non-UTF-8 filenames are decoded lossily (invalid bytes become U+FFFD) // so that they still surface in directory listings; Node's default // utf8 readdir does the same. Previously these entries were silently // dropped, which made globSync/readdirSync invisibly skip such files. let name = entry.file_name().to_string_lossy().into_owned(); let metadata = entry.file_type(); macro_rules! method_or_false { ($method:ident) => { if let Ok(metadata) = &metadata { metadata.$method() } else { false } }; } Some(FsDirEntry { name, is_file: method_or_false!(is_file), is_directory: method_or_false!(is_dir), is_symlink: method_or_false!(is_symlink), }) }) .collect(); Ok(entries) } #[derive(Debug)] struct RealFsReadDir(Mutex<tokio::fs::ReadDir>); #[async_trait::async_trait(?Send)] impl FsReadDir for RealFsReadDir { #[allow( clippy::await_holding_lock, reason = "tokio::fs::ReadDir requires mutable access across next_entry().await" )] async fn next(&self) -> FsResult<Option<FsDirEntry>> { let mut read_dir = self.0.try_lock().map_err(|_| FsError::FileBusy)?; let Some(entry) = read_dir.next_entry().await? else { return Ok(None); }; Ok(Some(fs_dir_entry_from_tokio(entry).await)) } } async fn fs_dir_entry_from_tokio(entry: tokio::fs::DirEntry) -> FsDirEntry { let name = entry.file_name().to_string_lossy().into_owned(); let metadata = entry.file_type().await; macro_rules! method_or_false { ($method:ident) => { if let Ok(metadata) = &metadata { metadata.$method() } else { false } }; } FsDirEntry { name, is_file: method_or_false!(is_file), is_directory: method_or_false!(is_dir), is_symlink: method_or_false!(is_symlink), } } #[cfg(not(windows))] fn symlink( oldpath: &Path, newpath: &Path, _file_type: Option<FsFileType>, ) -> FsResult<()> { std::os::unix::fs::symlink(oldpath, newpath)?; Ok(()) } #[cfg(windows)] fn symlink( oldpath: &Path, newpath: &Path, file_type: Option<FsFileType>, ) -> FsResult<()> { let file_type = match file_type { Some(file_type) => file_type, None => { let old_meta = fs::metadata(oldpath); match old_meta { Ok(metadata) => { if metadata.is_file() { FsFileType::File } else if metadata.is_dir() { FsFileType::Directory } else { return Err(FsError::Io(io::Error::new( io::ErrorKind::InvalidInput, "On Windows the target must be a file or directory", ))); } } Err(err) if err.kind() == io::ErrorKind::NotFound => { return Err(FsError::Io(io::Error::new( io::ErrorKind::InvalidInput, "On Windows an `options` argument is required if the target does not exist", ))); } Err(err) => return Err(err.into()), } } }; match file_type { FsFileType::File => { std::os::windows::fs::symlink_file(oldpath, newpath)?; } FsFileType::Directory => { std::os::windows::fs::symlink_dir(oldpath, newpath)?; } FsFileType::Junction => { junction::create(oldpath, newpath)?; } }; Ok(()) } fn truncate(path: &Path, len: u64) -> FsResult<()> { let mut open_options = fs::OpenOptions::new(); open_options.write(true); // The permission check for truncate is performed no-follow (against the // named path, without canonicalization). Refuse to follow a terminal symlink // here so a writable symlink inside an allowed scope cannot be used to zero // out a file it points to outside that scope. O_NOFOLLOW makes the open fail // with ELOOP when the final component is a symlink. #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; open_options.custom_flags(libc::O_NOFOLLOW); } let file = open_options.open(path)?; file.set_len(len)?; Ok(()) } fn open_options(options: OpenOptions) -> fs::OpenOptions { let mut open_options = fs::OpenOptions::new(); if let Some(mode) = options.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 } if let Some(custom_flags) = options.custom_flags { #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; open_options.custom_flags(custom_flags); } #[cfg(not(unix))] let _ = custom_flags; } open_options.read(options.read); open_options.create(options.create); open_options.write(options.write); // On Windows, truncate and create_new produce conflicting // dwCreationDisposition flags (TRUNCATE_EXISTING vs CREATE_NEW). // When create_new is set, the file must not exist, so truncation // is meaningless. Passing both can cause spurious "os error 0" // (ERROR_SUCCESS) failures and file corruption on Windows. open_options.truncate(options.truncate && !options.create_new); open_options.append(options.append); open_options.create_new(options.create_new); open_options } pub fn open_with_checked_path( opts: OpenOptions, path: &CheckedPath, ) -> FsResult<std::fs::File> { // Rust's std::fs::OpenOptions requires write or append when create is set. // However, POSIX allows O_RDONLY | O_CREAT (create the file if it doesn't // exist, then open for reading). Handle this by creating the file first // if needed, then opening for read only. if opts.create && !opts.write && !opts.append { if !path.exists() { let create_opts = OpenOptions { read: false, write: true, create: true, truncate: false, append: false, create_new: opts.create_new, custom_flags: None, mode: opts.mode, }; // Create and immediately close the file drop(open_path_with_options(create_opts, path)?); } let read_opts = OpenOptions { read: true, write: false, create: false, truncate: false, append: false, create_new: false, custom_flags: opts.custom_flags, mode: None, }; return Ok(open_path_with_options(read_opts, path)?); } Ok(open_path_with_options(opts, path)?) } // Open the path using the configured options. On Windows we set // `FILE_FLAG_BACKUP_SEMANTICS` so directories can be opened, but a few // filesystem drivers (notably ImDisk-backed memory disks) reject that flag // for regular files and return `ERROR_INVALID_FUNCTION` (1). Retry without // the flag in that case so reads/writes succeed on those volumes. // See https://github.com/denoland/deno/issues/26257. fn open_path_with_options( opts: OpenOptions, path: &CheckedPath, ) -> io::Result<fs::File> { let std_opts = open_options_for_checked_path(opts, path); match std_opts.open(path) { Ok(file) => Ok(file), #[cfg(windows)] Err(err) if err.raw_os_error() == Some(ERROR_INVALID_FUNCTION) => { let fallback = open_options_for_checked_path_no_backup(opts, path); fallback.open(path).map_err(|_| err) } // A canonicalized path is opened with `O_NOFOLLOW` (see // `open_options_for_checked_path`). If the final component is still a // symlink at this point it must be a broken/dangling link: its target was // removed after canonicalization resolved the parent directory, so the link // survived as the path tail. `O_NOFOLLOW` reports it as `ELOOP` ("Too many // levels of symbolic links"), which is misleading. Translate it to // `ENOENT` so the error matches reading a nonexistent file. // See https://github.com/denoland/deno/issues/29139. #[cfg(unix)] Err(err) if path.canonicalized() && err.raw_os_error() == Some(libc::ELOOP) => { Err(io::Error::from_raw_os_error(libc::ENOENT)) } Err(err) => Err(err), } } #[cfg(windows)] fn open_options_for_checked_path_no_backup( options: OpenOptions, _path: &CheckedPath, ) -> fs::OpenOptions { // Same as open_options_for_checked_path but without // FILE_FLAG_BACKUP_SEMANTICS so drivers that reject it can still open // regular files. Cannot open directories without the flag, but for the // drivers in question that's acceptable. open_options(options) } #[inline(always)] pub fn open_options_for_checked_path( options: OpenOptions, path: &CheckedPath, ) -> fs::OpenOptions { let mut opts: fs::OpenOptions = open_options(options); #[cfg(windows)] { _ = path; // not used on windows // allow opening directories use std::os::windows::fs::OpenOptionsExt; opts.custom_flags( windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS, ); } #[cfg(unix)] if path.canonicalized() { // Don't follow symlinks on open -- we must always pass fully-resolved files // with the exception of /proc/ which is too special, and /dev/std* which might point to // proc. use std::os::unix::fs::OpenOptionsExt; match options.custom_flags { Some(flags) => { opts.custom_flags(flags | libc::O_NOFOLLOW); } None => { opts.custom_flags(libc::O_NOFOLLOW); } } } opts }