mirror of
https://github.com/denoland/deno.git
synced 2025-09-27 04:39:10 +00:00
fix(ext/node): implement fchmod
on windows (#30704)
- Also fixes `node:fs.fstat` because it was incorrectly showing the file mode on windows, which is needed by the node compat test. - Moves the `stat_extra` function from `ext/fs/std_fs.rs` (deno_fs) into `ext/io/lib.rs` (deno_io), because it's more convenient to export it from there letting us to use the function on `deno_fs` and `deno_io`, and currently `deno_fs` had already used `deno_io` as a dependency. - Allows [parallel/test-fs-chmod-mask.js](https://github.com/nodejs/node/blob/v24.2.0/test/parallel/test-fs-chmod-mask.js) and [parallel/test-fs-chmod.js](https://github.com/nodejs/node/blob/v24.2.0/test/parallel/test-fs-chmod.js) tests to pass.
This commit is contained in:
parent
d8c186196c
commit
fc6f0d38a3
4 changed files with 207 additions and 138 deletions
126
ext/fs/std_fs.rs
126
ext/fs/std_fs.rs
|
@ -862,7 +862,7 @@ fn stat(path: &Path) -> FsResult<FsStat> {
|
||||||
let file = opts.open(path)?;
|
let file = opts.open(path)?;
|
||||||
let metadata = file.metadata()?;
|
let metadata = file.metadata()?;
|
||||||
let mut fsstat = FsStat::from_std(metadata);
|
let mut fsstat = FsStat::from_std(metadata);
|
||||||
stat_extra(&file, &mut fsstat)?;
|
deno_io::stat_extra(&file, &mut fsstat)?;
|
||||||
Ok(fsstat)
|
Ok(fsstat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -885,132 +885,10 @@ fn lstat(path: &Path) -> FsResult<FsStat> {
|
||||||
let file = opts.open(path)?;
|
let file = opts.open(path)?;
|
||||||
let metadata = file.metadata()?;
|
let metadata = file.metadata()?;
|
||||||
let mut fsstat = FsStat::from_std(metadata);
|
let mut fsstat = FsStat::from_std(metadata);
|
||||||
stat_extra(&file, &mut fsstat)?;
|
deno_io::stat_extra(&file, &mut fsstat)?;
|
||||||
Ok(fsstat)
|
Ok(fsstat)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn stat_extra(file: &std::fs::File, fsstat: &mut FsStat) -> FsResult<()> {
|
|
||||||
use std::os::windows::io::AsRawHandle;
|
|
||||||
|
|
||||||
unsafe fn get_dev(
|
|
||||||
handle: winapi::shared::ntdef::HANDLE,
|
|
||||||
) -> std::io::Result<u64> {
|
|
||||||
use winapi::shared::minwindef::FALSE;
|
|
||||||
use winapi::um::fileapi::BY_HANDLE_FILE_INFORMATION;
|
|
||||||
use winapi::um::fileapi::GetFileInformationByHandle;
|
|
||||||
|
|
||||||
// SAFETY: winapi calls
|
|
||||||
unsafe {
|
|
||||||
let info = {
|
|
||||||
let mut info =
|
|
||||||
std::mem::MaybeUninit::<BY_HANDLE_FILE_INFORMATION>::zeroed();
|
|
||||||
if GetFileInformationByHandle(handle, info.as_mut_ptr()) == FALSE {
|
|
||||||
return Err(std::io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
|
|
||||||
info.assume_init()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(info.dwVolumeSerialNumber as u64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WINDOWS_TICK: i64 = 10_000; // 100-nanosecond intervals in a millisecond
|
|
||||||
const SEC_TO_UNIX_EPOCH: i64 = 11_644_473_600; // Seconds between Windows epoch and Unix epoch
|
|
||||||
|
|
||||||
fn windows_time_to_unix_time_msec(windows_time: &i64) -> i64 {
|
|
||||||
let milliseconds_since_windows_epoch = windows_time / WINDOWS_TICK;
|
|
||||||
milliseconds_since_windows_epoch - SEC_TO_UNIX_EPOCH * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
use windows_sys::Wdk::Storage::FileSystem::FILE_ALL_INFORMATION;
|
|
||||||
use windows_sys::Win32::Foundation::NTSTATUS;
|
|
||||||
|
|
||||||
unsafe fn query_file_information(
|
|
||||||
handle: winapi::shared::ntdef::HANDLE,
|
|
||||||
) -> Result<FILE_ALL_INFORMATION, NTSTATUS> {
|
|
||||||
use windows_sys::Wdk::Storage::FileSystem::NtQueryInformationFile;
|
|
||||||
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
|
|
||||||
use windows_sys::Win32::Foundation::RtlNtStatusToDosError;
|
|
||||||
use windows_sys::Win32::System::IO::IO_STATUS_BLOCK;
|
|
||||||
|
|
||||||
// SAFETY: winapi calls
|
|
||||||
unsafe {
|
|
||||||
let mut info = std::mem::MaybeUninit::<FILE_ALL_INFORMATION>::zeroed();
|
|
||||||
let mut io_status_block =
|
|
||||||
std::mem::MaybeUninit::<IO_STATUS_BLOCK>::zeroed();
|
|
||||||
let status = NtQueryInformationFile(
|
|
||||||
handle as _,
|
|
||||||
io_status_block.as_mut_ptr(),
|
|
||||||
info.as_mut_ptr() as *mut _,
|
|
||||||
std::mem::size_of::<FILE_ALL_INFORMATION>() as _,
|
|
||||||
18, /* FileAllInformation */
|
|
||||||
);
|
|
||||||
|
|
||||||
if status < 0 {
|
|
||||||
let converted_status = RtlNtStatusToDosError(status);
|
|
||||||
|
|
||||||
// If error more data is returned, then it means that the buffer is too small to get full filename information
|
|
||||||
// to have that we should retry. However, since we only use BasicInformation and StandardInformation, it is fine to ignore it
|
|
||||||
// since struct is populated with other data anyway.
|
|
||||||
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile#remarksdd
|
|
||||||
if converted_status != ERROR_MORE_DATA {
|
|
||||||
return Err(converted_status as NTSTATUS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(info.assume_init())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: winapi calls
|
|
||||||
unsafe {
|
|
||||||
let file_handle = file.as_raw_handle();
|
|
||||||
|
|
||||||
fsstat.dev = get_dev(file_handle)?;
|
|
||||||
|
|
||||||
if let Ok(file_info) = query_file_information(file_handle) {
|
|
||||||
fsstat.ctime = Some(windows_time_to_unix_time_msec(
|
|
||||||
&file_info.BasicInformation.ChangeTime,
|
|
||||||
) as u64);
|
|
||||||
|
|
||||||
if file_info.BasicInformation.FileAttributes
|
|
||||||
& winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT
|
|
||||||
!= 0
|
|
||||||
{
|
|
||||||
fsstat.is_symlink = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if file_info.BasicInformation.FileAttributes
|
|
||||||
& winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY
|
|
||||||
!= 0
|
|
||||||
{
|
|
||||||
fsstat.mode |= libc::S_IFDIR as u32;
|
|
||||||
fsstat.size = 0;
|
|
||||||
} else {
|
|
||||||
fsstat.mode |= libc::S_IFREG as u32;
|
|
||||||
fsstat.size = file_info.StandardInformation.EndOfFile as u64;
|
|
||||||
}
|
|
||||||
|
|
||||||
if file_info.BasicInformation.FileAttributes
|
|
||||||
& winapi::um::winnt::FILE_ATTRIBUTE_READONLY
|
|
||||||
!= 0
|
|
||||||
{
|
|
||||||
fsstat.mode |=
|
|
||||||
(libc::S_IREAD | (libc::S_IREAD >> 3) | (libc::S_IREAD >> 6)) as u32;
|
|
||||||
} else {
|
|
||||||
fsstat.mode |= ((libc::S_IREAD | libc::S_IWRITE)
|
|
||||||
| ((libc::S_IREAD | libc::S_IWRITE) >> 3)
|
|
||||||
| ((libc::S_IREAD | libc::S_IWRITE) >> 6))
|
|
||||||
as u32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exists(path: &Path) -> bool {
|
fn exists(path: &Path) -> bool {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
|
|
@ -39,3 +39,4 @@ rand.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
windows-sys.workspace = true
|
windows-sys.workspace = true
|
||||||
deno_subprocess_windows.workspace = true
|
deno_subprocess_windows.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
|
|
216
ext/io/lib.rs
216
ext/io/lib.rs
|
@ -828,29 +828,63 @@ impl crate::fs::File for StdFileResourceInner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chmod_sync(self: Rc<Self>, _mode: u32) -> FsResult<()> {
|
fn chmod_sync(self: Rc<Self>, mode: u32) -> FsResult<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::prelude::PermissionsExt;
|
use std::os::unix::prelude::PermissionsExt;
|
||||||
self.with_sync(|file| {
|
self.with_sync(|file| {
|
||||||
Ok(file.set_permissions(std::fs::Permissions::from_mode(_mode))?)
|
Ok(file.set_permissions(std::fs::Permissions::from_mode(mode))?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
self.with_sync(|file| {
|
||||||
|
let mut permissions = file.metadata()?.permissions();
|
||||||
|
if mode & libc::S_IWRITE as u32 > 0 {
|
||||||
|
// clippy warning should only be applicable to Unix platforms
|
||||||
|
// https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false
|
||||||
|
#[allow(clippy::permissions_set_readonly_false)]
|
||||||
|
permissions.set_readonly(false);
|
||||||
|
} else {
|
||||||
|
permissions.set_readonly(true);
|
||||||
|
}
|
||||||
|
file.set_permissions(permissions)?;
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
|
||||||
Err(FsError::NotSupported)
|
|
||||||
}
|
}
|
||||||
async fn chmod_async(self: Rc<Self>, _mode: u32) -> FsResult<()> {
|
async fn chmod_async(self: Rc<Self>, mode: u32) -> FsResult<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::prelude::PermissionsExt;
|
use std::os::unix::prelude::PermissionsExt;
|
||||||
self
|
self
|
||||||
.with_inner_blocking_task(move |file| {
|
.with_inner_blocking_task(move |file| {
|
||||||
Ok(file.set_permissions(std::fs::Permissions::from_mode(_mode))?)
|
Ok(file.set_permissions(std::fs::Permissions::from_mode(mode))?)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(windows)]
|
||||||
Err(FsError::NotSupported)
|
{
|
||||||
|
self
|
||||||
|
.with_inner_blocking_task(move |file| {
|
||||||
|
let mut permissions = file.metadata()?.permissions();
|
||||||
|
if mode & libc::S_IWRITE as u32 > 0 {
|
||||||
|
// clippy warning should only be applicable to Unix platforms
|
||||||
|
// https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false
|
||||||
|
#[allow(clippy::permissions_set_readonly_false)]
|
||||||
|
permissions.set_readonly(false);
|
||||||
|
} else {
|
||||||
|
permissions.set_readonly(true);
|
||||||
|
}
|
||||||
|
file.set_permissions(permissions)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
Err(FsError::NotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chown_sync(
|
fn chown_sync(
|
||||||
|
@ -922,14 +956,46 @@ impl crate::fs::File for StdFileResourceInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stat_sync(self: Rc<Self>) -> FsResult<FsStat> {
|
fn stat_sync(self: Rc<Self>) -> FsResult<FsStat> {
|
||||||
self.with_sync(|file| Ok(file.metadata().map(FsStat::from_std)?))
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
self.with_sync(|file| Ok(file.metadata().map(FsStat::from_std)?))
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
self.with_sync(|file| {
|
||||||
|
let mut fs_stat = file.metadata().map(FsStat::from_std)?;
|
||||||
|
stat_extra(file, &mut fs_stat)?;
|
||||||
|
Ok(fs_stat)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
Err(FsError::NotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async fn stat_async(self: Rc<Self>) -> FsResult<FsStat> {
|
async fn stat_async(self: Rc<Self>) -> FsResult<FsStat> {
|
||||||
self
|
#[cfg(unix)]
|
||||||
.with_inner_blocking_task(|file| {
|
{
|
||||||
Ok(file.metadata().map(FsStat::from_std)?)
|
self
|
||||||
})
|
.with_inner_blocking_task(|file| {
|
||||||
.await
|
Ok(file.metadata().map(FsStat::from_std)?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.with_inner_blocking_task(|file| {
|
||||||
|
let mut fs_stat = file.metadata().map(FsStat::from_std)?;
|
||||||
|
stat_extra(file, &mut fs_stat)?;
|
||||||
|
Ok(fs_stat)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
Err(FsError::NotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lock_sync(self: Rc<Self>, exclusive: bool) -> FsResult<()> {
|
fn lock_sync(self: Rc<Self>, exclusive: bool) -> FsResult<()> {
|
||||||
|
@ -1122,3 +1188,125 @@ pub fn op_print(
|
||||||
.map_err(JsErrorBox::from_err)
|
.map_err(JsErrorBox::from_err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn stat_extra(file: &std::fs::File, fsstat: &mut FsStat) -> FsResult<()> {
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
|
||||||
|
unsafe fn get_dev(
|
||||||
|
handle: winapi::shared::ntdef::HANDLE,
|
||||||
|
) -> std::io::Result<u64> {
|
||||||
|
use winapi::shared::minwindef::FALSE;
|
||||||
|
use winapi::um::fileapi::BY_HANDLE_FILE_INFORMATION;
|
||||||
|
use winapi::um::fileapi::GetFileInformationByHandle;
|
||||||
|
|
||||||
|
// SAFETY: winapi calls
|
||||||
|
unsafe {
|
||||||
|
let info = {
|
||||||
|
let mut info =
|
||||||
|
std::mem::MaybeUninit::<BY_HANDLE_FILE_INFORMATION>::zeroed();
|
||||||
|
if GetFileInformationByHandle(handle, info.as_mut_ptr()) == FALSE {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
info.assume_init()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(info.dwVolumeSerialNumber as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINDOWS_TICK: i64 = 10_000; // 100-nanosecond intervals in a millisecond
|
||||||
|
const SEC_TO_UNIX_EPOCH: i64 = 11_644_473_600; // Seconds between Windows epoch and Unix epoch
|
||||||
|
|
||||||
|
fn windows_time_to_unix_time_msec(windows_time: &i64) -> i64 {
|
||||||
|
let milliseconds_since_windows_epoch = windows_time / WINDOWS_TICK;
|
||||||
|
milliseconds_since_windows_epoch - SEC_TO_UNIX_EPOCH * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
use windows_sys::Wdk::Storage::FileSystem::FILE_ALL_INFORMATION;
|
||||||
|
use windows_sys::Win32::Foundation::NTSTATUS;
|
||||||
|
|
||||||
|
unsafe fn query_file_information(
|
||||||
|
handle: winapi::shared::ntdef::HANDLE,
|
||||||
|
) -> Result<FILE_ALL_INFORMATION, NTSTATUS> {
|
||||||
|
use windows_sys::Wdk::Storage::FileSystem::NtQueryInformationFile;
|
||||||
|
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
|
||||||
|
use windows_sys::Win32::Foundation::RtlNtStatusToDosError;
|
||||||
|
use windows_sys::Win32::System::IO::IO_STATUS_BLOCK;
|
||||||
|
|
||||||
|
// SAFETY: winapi calls
|
||||||
|
unsafe {
|
||||||
|
let mut info = std::mem::MaybeUninit::<FILE_ALL_INFORMATION>::zeroed();
|
||||||
|
let mut io_status_block =
|
||||||
|
std::mem::MaybeUninit::<IO_STATUS_BLOCK>::zeroed();
|
||||||
|
let status = NtQueryInformationFile(
|
||||||
|
handle as _,
|
||||||
|
io_status_block.as_mut_ptr(),
|
||||||
|
info.as_mut_ptr() as *mut _,
|
||||||
|
std::mem::size_of::<FILE_ALL_INFORMATION>() as _,
|
||||||
|
18, /* FileAllInformation */
|
||||||
|
);
|
||||||
|
|
||||||
|
if status < 0 {
|
||||||
|
let converted_status = RtlNtStatusToDosError(status);
|
||||||
|
|
||||||
|
// If error more data is returned, then it means that the buffer is too small to get full filename information
|
||||||
|
// to have that we should retry. However, since we only use BasicInformation and StandardInformation, it is fine to ignore it
|
||||||
|
// since struct is populated with other data anyway.
|
||||||
|
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile#remarksdd
|
||||||
|
if converted_status != ERROR_MORE_DATA {
|
||||||
|
return Err(converted_status as NTSTATUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info.assume_init())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: winapi calls
|
||||||
|
unsafe {
|
||||||
|
let file_handle = file.as_raw_handle();
|
||||||
|
|
||||||
|
fsstat.dev = get_dev(file_handle)?;
|
||||||
|
|
||||||
|
if let Ok(file_info) = query_file_information(file_handle) {
|
||||||
|
fsstat.ctime = Some(windows_time_to_unix_time_msec(
|
||||||
|
&file_info.BasicInformation.ChangeTime,
|
||||||
|
) as u64);
|
||||||
|
|
||||||
|
if file_info.BasicInformation.FileAttributes
|
||||||
|
& winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT
|
||||||
|
!= 0
|
||||||
|
{
|
||||||
|
fsstat.is_symlink = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_info.BasicInformation.FileAttributes
|
||||||
|
& winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY
|
||||||
|
!= 0
|
||||||
|
{
|
||||||
|
fsstat.mode |= libc::S_IFDIR as u32;
|
||||||
|
fsstat.size = 0;
|
||||||
|
} else {
|
||||||
|
fsstat.mode |= libc::S_IFREG as u32;
|
||||||
|
fsstat.size = file_info.StandardInformation.EndOfFile as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_info.BasicInformation.FileAttributes
|
||||||
|
& winapi::um::winnt::FILE_ATTRIBUTE_READONLY
|
||||||
|
!= 0
|
||||||
|
{
|
||||||
|
fsstat.mode |=
|
||||||
|
(libc::S_IREAD | (libc::S_IREAD >> 3) | (libc::S_IREAD >> 6)) as u32;
|
||||||
|
} else {
|
||||||
|
fsstat.mode |= ((libc::S_IREAD | libc::S_IWRITE)
|
||||||
|
| ((libc::S_IREAD | libc::S_IWRITE) >> 3)
|
||||||
|
| ((libc::S_IREAD | libc::S_IWRITE) >> 6))
|
||||||
|
as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -426,6 +426,8 @@
|
||||||
"parallel/test-file-write-stream5.js" = {}
|
"parallel/test-file-write-stream5.js" = {}
|
||||||
"parallel/test-finalization-registry-shutdown.js" = {}
|
"parallel/test-finalization-registry-shutdown.js" = {}
|
||||||
"parallel/test-fs-buffertype-writesync.js" = {}
|
"parallel/test-fs-buffertype-writesync.js" = {}
|
||||||
|
"parallel/test-fs-chmod-mask.js" = {}
|
||||||
|
"parallel/test-fs-chmod.js" = {}
|
||||||
"parallel/test-fs-chown-type-check.js" = {}
|
"parallel/test-fs-chown-type-check.js" = {}
|
||||||
"parallel/test-fs-close.js" = {}
|
"parallel/test-fs-close.js" = {}
|
||||||
# TODO(bartlomieju): appears that part of the test that's broke was removed in Node 24.0
|
# TODO(bartlomieju): appears that part of the test that's broke was removed in Node 24.0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue