Skip to content

Commit

Permalink
feat(fs): support FileInfo.dev on Windows (#18073)
Browse files Browse the repository at this point in the history
This commit adds support for retrieving `dev` information
when stating files on Windows.

Additionally `Deno.FileInfo` interfaces was changed to always
return 0 for fields that we don't retrieve information for on Windows.

Closes #18053

---------

Co-authored-by: David Sherret <dsherret@gmail.com>
  • Loading branch information
bartlomieju and dsherret authored Mar 16, 2023
1 parent 92c3ac3 commit 48a0b7f
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 83 deletions.
20 changes: 10 additions & 10 deletions cli/tests/unit/stat_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,22 +291,22 @@ Deno.test(
ignore: Deno.build.os !== "windows",
permissions: { read: true, write: true },
},
function statNoUnixFields() {
function statUnixFieldsOnWindows() {
const enc = new TextEncoder();
const data = enc.encode("Hello");
const tempDir = Deno.makeTempDirSync();
const filename = tempDir + "/test.txt";
Deno.writeFileSync(filename, data, { mode: 0o666 });
const s = Deno.statSync(filename);
assert(s.dev === null);
assert(s.ino === null);
assert(s.mode === null);
assert(s.nlink === null);
assert(s.uid === null);
assert(s.gid === null);
assert(s.rdev === null);
assert(s.blksize === null);
assert(s.blocks === null);
assert(s.dev !== 0);
assert(s.ino === 0);
assert(s.mode === 0);
assert(s.nlink === 0);
assert(s.uid === 0);
assert(s.gid === 0);
assert(s.rdev === 0);
assert(s.blksize === 0);
assert(s.blocks === 0);
},
);

Expand Down
36 changes: 17 additions & 19 deletions cli/tsc/dts/lib.deno.ns.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3078,43 +3078,41 @@ declare namespace Deno {
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
* not be available on all platforms. */
birthtime: Date | null;
/** ID of the device containing the file.
*
* _Linux/Mac OS only._ */
dev: number | null;
/** ID of the device containing the file. */
dev: number;
/** Inode number.
*
* _Linux/Mac OS only._ */
ino: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
ino: number;
/** **UNSTABLE**: Match behavior with Go on Windows for `mode`.
*
* The underlying raw `st_mode` bits that contain the standard Unix
* permissions for this file/directory. */
mode: number | null;
mode: number;
/** Number of hard links pointing to this file.
*
* _Linux/Mac OS only._ */
nlink: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
nlink: number;
/** User ID of the owner of this file.
*
* _Linux/Mac OS only._ */
uid: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
uid: number;
/** Group ID of the owner of this file.
*
* _Linux/Mac OS only._ */
gid: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
gid: number;
/** Device ID of this file.
*
* _Linux/Mac OS only._ */
rdev: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
rdev: number;
/** Blocksize for filesystem I/O.
*
* _Linux/Mac OS only._ */
blksize: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
blksize: number;
/** Number of blocks allocated to the file, in 512-byte units.
*
* _Linux/Mac OS only._ */
blocks: number | null;
* _Linux/Mac OS only, always returns 0 on Windows_ */
blocks: number;
}

/** Resolves to the absolute normalized path, with symbolic links resolved.
Expand Down
59 changes: 21 additions & 38 deletions ext/fs/30_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,31 +211,16 @@ async function rename(oldpath, newpath) {
// 3. u64
// offset += 2
// high u32 | low u32
//
// 4. ?u64 converts a zero u64 value to JS null on Windows.
function createByteStruct(types) {
// types can be "date", "bool" or "u64".
// `?` prefix means optional on windows.
let offset = 0;
let str =
'const unix = Deno.build.os === "darwin" || Deno.build.os === "linux"; return {';
let str = "return {";
const typeEntries = ObjectEntries(types);
for (let i = 0; i < typeEntries.length; ++i) {
let { 0: name, 1: type } = typeEntries[i];

const optional = type.startsWith("?");
if (optional) type = type.slice(1);
const { 0: name, 1: type } = typeEntries[i];

if (type == "u64") {
if (!optional) {
str += `${name}: view[${offset}] + view[${offset + 1}] * 2**32,`;
} else {
str += `${name}: (unix ? (view[${offset}] + view[${
offset + 1
}] * 2**32) : (view[${offset}] + view[${
offset + 1
}] * 2**32) || null),`;
}
str += `${name}: view[${offset}] + view[${offset + 1}] * 2**32,`;
} else if (type == "date") {
str += `${name}: view[${offset}] === 0 ? null : new Date(view[${
offset + 2
Expand All @@ -259,19 +244,18 @@ const { 0: statStruct, 1: statBuf } = createByteStruct({
mtime: "date",
atime: "date",
birthtime: "date",
dev: "?u64",
ino: "?u64",
mode: "?u64",
nlink: "?u64",
uid: "?u64",
gid: "?u64",
rdev: "?u64",
blksize: "?u64",
blocks: "?u64",
dev: "u64",
ino: "u64",
mode: "u64",
nlink: "u64",
uid: "u64",
gid: "u64",
rdev: "u64",
blksize: "u64",
blocks: "u64",
});

function parseFileInfo(response) {
const unix = core.build.os === "darwin" || core.build.os === "linux";
return {
isFile: response.isFile,
isDirectory: response.isDirectory,
Expand All @@ -282,16 +266,15 @@ function parseFileInfo(response) {
birthtime: response.birthtimeSet !== null
? new Date(response.birthtime)
: null,
// Only non-null if on Unix
dev: unix ? response.dev : null,
ino: unix ? response.ino : null,
mode: unix ? response.mode : null,
nlink: unix ? response.nlink : null,
uid: unix ? response.uid : null,
gid: unix ? response.gid : null,
rdev: unix ? response.rdev : null,
blksize: unix ? response.blksize : null,
blocks: unix ? response.blocks : null,
dev: response.dev,
ino: response.ino,
mode: response.mode,
nlink: response.nlink,
uid: response.uid,
gid: response.gid,
rdev: response.rdev,
blksize: response.blksize,
blocks: response.blocks,
};
}

Expand Down
171 changes: 155 additions & 16 deletions ext/fs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1244,13 +1244,166 @@ fn get_stat(metadata: std::fs::Metadata) -> FsStat {
}
}

#[cfg(windows)]
#[inline(always)]
fn get_stat2(metadata: std::fs::Metadata, dev: u64) -> FsStat {
let (mtime, mtime_set) = to_msec(metadata.modified());
let (atime, atime_set) = to_msec(metadata.accessed());
let (birthtime, birthtime_set) = to_msec(metadata.created());

FsStat {
is_file: metadata.is_file(),
is_directory: metadata.is_dir(),
is_symlink: metadata.file_type().is_symlink(),
size: metadata.len(),
mtime_set,
mtime,
atime_set,
atime,
birthtime_set,
birthtime,
dev,
ino: 0,
mode: 0,
nlink: 0,
uid: 0,
gid: 0,
rdev: 0,
blksize: 0,
blocks: 0,
}
}

#[cfg(not(windows))]
#[inline(always)]
fn get_stat2(metadata: std::fs::Metadata) -> FsStat {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
let (mtime, mtime_set) = to_msec(metadata.modified());
let (atime, atime_set) = to_msec(metadata.accessed());
let (birthtime, birthtime_set) = to_msec(metadata.created());

FsStat {
is_file: metadata.is_file(),
is_directory: metadata.is_dir(),
is_symlink: metadata.file_type().is_symlink(),
size: metadata.len(),
mtime_set,
mtime,
atime_set,
atime,
birthtime_set,
birthtime,
dev: metadata.dev(),
ino: metadata.ino(),
mode: metadata.mode(),
nlink: metadata.nlink(),
uid: metadata.uid(),
gid: metadata.gid(),
rdev: metadata.rdev(),
blksize: metadata.blksize(),
blocks: metadata.blocks(),
}
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatArgs {
path: String,
lstat: bool,
}

#[cfg(not(windows))]
fn do_stat(path: PathBuf, lstat: bool) -> Result<FsStat, AnyError> {
let err_mapper =
|err| default_err_mapper(err, format!("stat '{}'", path.display()));
let metadata = if lstat {
std::fs::symlink_metadata(&path).map_err(err_mapper)?
} else {
std::fs::metadata(&path).map_err(err_mapper)?
};

Ok(get_stat2(metadata))
}

#[cfg(windows)]
fn do_stat(path: PathBuf, lstat: bool) -> Result<FsStat, AnyError> {
use std::os::windows::prelude::OsStrExt;

use winapi::um::fileapi::CreateFileW;
use winapi::um::fileapi::OPEN_EXISTING;
use winapi::um::handleapi::CloseHandle;
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS;
use winapi::um::winbase::FILE_FLAG_OPEN_REPARSE_POINT;
use winapi::um::winnt::FILE_SHARE_DELETE;
use winapi::um::winnt::FILE_SHARE_READ;
use winapi::um::winnt::FILE_SHARE_WRITE;

let err_mapper =
|err| default_err_mapper(err, format!("stat '{}'", path.display()));
let metadata = if lstat {
std::fs::symlink_metadata(&path).map_err(err_mapper)?
} else {
std::fs::metadata(&path).map_err(err_mapper)?
};

let (p, file_flags) = if lstat {
(
path,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
)
} else {
(path.canonicalize()?, FILE_FLAG_BACKUP_SEMANTICS)
};
unsafe {
let mut path: Vec<_> = p.as_os_str().encode_wide().collect();
path.push(0);
let file_handle = CreateFileW(
path.as_ptr(),
0,
FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
std::ptr::null_mut(),
OPEN_EXISTING,
file_flags,
std::ptr::null_mut(),
);
if file_handle == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error().into());
}

let result = get_dev(file_handle);
CloseHandle(file_handle);
let dev = result?;

Ok(get_stat2(metadata, dev))
}
}

#[cfg(windows)]
use winapi::um::fileapi::GetFileInformationByHandle;
#[cfg(windows)]
use winapi::um::fileapi::BY_HANDLE_FILE_INFORMATION;

#[cfg(windows)]
unsafe fn get_dev(
handle: winapi::shared::ntdef::HANDLE,
) -> std::io::Result<u64> {
use winapi::shared::minwindef::FALSE;

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)
}

#[op]
fn op_stat_sync<P>(
state: &mut OpState,
Expand All @@ -1265,15 +1418,8 @@ where
state
.borrow_mut::<P>()
.check_read(&path, "Deno.statSync()")?;
let err_mapper =
|err| default_err_mapper(err, format!("stat '{}'", path.display()));
let metadata = if lstat {
std::fs::symlink_metadata(&path).map_err(err_mapper)?
} else {
std::fs::metadata(&path).map_err(err_mapper)?
};

let stat = get_stat(metadata);
let stat = do_stat(path, lstat)?;
stat.write(out_buf);

Ok(())
Expand All @@ -1297,14 +1443,7 @@ where

tokio::task::spawn_blocking(move || {
debug!("op_stat_async {} {}", path.display(), lstat);
let err_mapper =
|err| default_err_mapper(err, format!("stat '{}'", path.display()));
let metadata = if lstat {
std::fs::symlink_metadata(&path).map_err(err_mapper)?
} else {
std::fs::metadata(&path).map_err(err_mapper)?
};
Ok(get_stat(metadata))
do_stat(path, lstat)
})
.await
.unwrap()
Expand Down

0 comments on commit 48a0b7f

Please sign in to comment.