1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

feat(ext/fs): add ctime to Deno.stats and use it in node compat layer (#24801)

This PR fixes #24453, by introducing a ctime (using ctime for UNIX and
ChangeTime for Windows) to Deno.stats.

Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
This commit is contained in:
Łukasz Czerniawski 2024-11-13 05:35:04 +01:00 committed by GitHub
parent 43812ee8ff
commit 7becd83a38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 88 additions and 15 deletions

View file

@ -350,6 +350,7 @@ impl<'a> VfsEntryRef<'a> {
atime: None, atime: None,
birthtime: None, birthtime: None,
mtime: None, mtime: None,
ctime: None,
blksize: 0, blksize: 0,
size: 0, size: 0,
dev: 0, dev: 0,
@ -372,6 +373,7 @@ impl<'a> VfsEntryRef<'a> {
atime: None, atime: None,
birthtime: None, birthtime: None,
mtime: None, mtime: None,
ctime: None,
blksize: 0, blksize: 0,
size: file.len, size: file.len,
dev: 0, dev: 0,
@ -394,6 +396,7 @@ impl<'a> VfsEntryRef<'a> {
atime: None, atime: None,
birthtime: None, birthtime: None,
mtime: None, mtime: None,
ctime: None,
blksize: 0, blksize: 0,
size: 0, size: 0,
dev: 0, dev: 0,

View file

@ -2971,6 +2971,10 @@ declare namespace Deno {
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
* not be available on all platforms. */ * not be available on all platforms. */
birthtime: Date | null; birthtime: Date | null;
/** The last change time of the file. This corresponds to the `ctime`
* field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may
* not be available on all platforms. */
ctime: Date | null;
/** ID of the device containing the file. */ /** ID of the device containing the file. */
dev: number; dev: number;
/** Inode number. /** Inode number.
@ -2979,8 +2983,7 @@ declare namespace Deno {
ino: number | null; ino: number | null;
/** The underlying raw `st_mode` bits that contain the standard Unix /** The underlying raw `st_mode` bits that contain the standard Unix
* permissions for this file/directory. * permissions for this file/directory.
* */
* _Linux/Mac OS only._ */
mode: number | null; mode: number | null;
/** Number of hard links pointing to this file. /** Number of hard links pointing to this file.
* *

View file

@ -346,9 +346,10 @@ const { 0: statStruct, 1: statBuf } = createByteStruct({
mtime: "date", mtime: "date",
atime: "date", atime: "date",
birthtime: "date", birthtime: "date",
ctime: "date",
dev: "u64", dev: "u64",
ino: "?u64", ino: "?u64",
mode: "?u64", mode: "u64",
nlink: "?u64", nlink: "?u64",
uid: "?u64", uid: "?u64",
gid: "?u64", gid: "?u64",
@ -377,9 +378,10 @@ function parseFileInfo(response) {
birthtime: response.birthtimeSet === true birthtime: response.birthtimeSet === true
? new Date(response.birthtime) ? new Date(response.birthtime)
: null, : null,
ctime: response.ctimeSet === true ? new Date(response.ctime) : null,
dev: response.dev, dev: response.dev,
mode: response.mode,
ino: unix ? response.ino : null, ino: unix ? response.ino : null,
mode: unix ? response.mode : null,
nlink: unix ? response.nlink : null, nlink: unix ? response.nlink : null,
uid: unix ? response.uid : null, uid: unix ? response.uid : null,
gid: unix ? response.gid : null, gid: unix ? response.gid : null,

View file

@ -229,6 +229,7 @@ impl FileSystem for InMemoryFs {
mtime: None, mtime: None,
atime: None, atime: None,
birthtime: None, birthtime: None,
ctime: None,
dev: 0, dev: 0,
ino: 0, ino: 0,
mode: 0, mode: 0,
@ -251,6 +252,7 @@ impl FileSystem for InMemoryFs {
mtime: None, mtime: None,
atime: None, atime: None,
birthtime: None, birthtime: None,
ctime: None,
dev: 0, dev: 0,
ino: 0, ino: 0,
mode: 0, mode: 0,

View file

@ -1795,6 +1795,8 @@ create_struct_writer! {
atime: u64, atime: u64,
birthtime_set: bool, birthtime_set: bool,
birthtime: u64, birthtime: u64,
ctime_set: bool,
ctime: u64,
// Following are only valid under Unix. // Following are only valid under Unix.
dev: u64, dev: u64,
ino: u64, ino: u64,
@ -1826,6 +1828,8 @@ impl From<FsStat> for SerializableStat {
atime: stat.atime.unwrap_or(0), atime: stat.atime.unwrap_or(0),
birthtime_set: stat.birthtime.is_some(), birthtime_set: stat.birthtime.is_some(),
birthtime: stat.birthtime.unwrap_or(0), birthtime: stat.birthtime.unwrap_or(0),
ctime_set: stat.ctime.is_some(),
ctime: stat.ctime.unwrap_or(0),
dev: stat.dev, dev: stat.dev,
ino: stat.ino, ino: stat.ino,

View file

@ -821,24 +821,46 @@ fn stat_extra(
Ok(info.dwVolumeSerialNumber as u64) 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::Wdk::Storage::FileSystem::FILE_ALL_INFORMATION;
use windows_sys::Win32::Foundation::NTSTATUS;
unsafe fn query_file_information( unsafe fn query_file_information(
handle: winapi::shared::ntdef::HANDLE, handle: winapi::shared::ntdef::HANDLE,
) -> std::io::Result<FILE_ALL_INFORMATION> { ) -> Result<FILE_ALL_INFORMATION, NTSTATUS> {
use windows_sys::Wdk::Storage::FileSystem::NtQueryInformationFile; use windows_sys::Wdk::Storage::FileSystem::NtQueryInformationFile;
use windows_sys::Win32::Foundation::RtlNtStatusToDosError;
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
use windows_sys::Win32::System::IO::IO_STATUS_BLOCK;
let mut info = std::mem::MaybeUninit::<FILE_ALL_INFORMATION>::zeroed(); 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( let status = NtQueryInformationFile(
handle as _, handle as _,
std::ptr::null_mut(), io_status_block.as_mut_ptr(),
info.as_mut_ptr() as *mut _, info.as_mut_ptr() as *mut _,
std::mem::size_of::<FILE_ALL_INFORMATION>() as _, std::mem::size_of::<FILE_ALL_INFORMATION>() as _,
18, /* FileAllInformation */ 18, /* FileAllInformation */
); );
if status < 0 { if status < 0 {
return Err(std::io::Error::last_os_error()); 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()) Ok(info.assume_init())
@ -862,10 +884,13 @@ fn stat_extra(
} }
let result = get_dev(file_handle); let result = get_dev(file_handle);
CloseHandle(file_handle);
fsstat.dev = result?; fsstat.dev = result?;
if let Ok(file_info) = query_file_information(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 if file_info.BasicInformation.FileAttributes
& winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT & winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT
!= 0 != 0
@ -898,6 +923,7 @@ fn stat_extra(
} }
} }
CloseHandle(file_handle);
Ok(()) Ok(())
} }
} }

View file

@ -94,6 +94,7 @@ pub struct FsStat {
pub mtime: Option<u64>, pub mtime: Option<u64>,
pub atime: Option<u64>, pub atime: Option<u64>,
pub birthtime: Option<u64>, pub birthtime: Option<u64>,
pub ctime: Option<u64>,
pub dev: u64, pub dev: u64,
pub ino: u64, pub ino: u64,
@ -153,6 +154,16 @@ impl FsStat {
} }
} }
#[inline(always)]
fn get_ctime(ctime_or_0: i64) -> Option<u64> {
if ctime_or_0 > 0 {
// ctime return seconds since epoch, but we need milliseconds
return Some(ctime_or_0 as u64 * 1000);
}
None
}
Self { Self {
is_file: metadata.is_file(), is_file: metadata.is_file(),
is_directory: metadata.is_dir(), is_directory: metadata.is_dir(),
@ -162,6 +173,7 @@ impl FsStat {
mtime: to_msec(metadata.modified()), mtime: to_msec(metadata.modified()),
atime: to_msec(metadata.accessed()), atime: to_msec(metadata.accessed()),
birthtime: to_msec(metadata.created()), birthtime: to_msec(metadata.created()),
ctime: get_ctime(unix_or_zero!(ctime)),
dev: unix_or_zero!(dev), dev: unix_or_zero!(dev),
ino: unix_or_zero!(ino), ino: unix_or_zero!(ino),

View file

@ -290,8 +290,8 @@ export function convertFileInfoToStats(origin: Deno.FileInfo): Stats {
isFIFO: () => false, isFIFO: () => false,
isCharacterDevice: () => false, isCharacterDevice: () => false,
isSocket: () => false, isSocket: () => false,
ctime: origin.mtime, ctime: origin.ctime,
ctimeMs: origin.mtime?.getTime() || null, ctimeMs: origin.ctime?.getTime() || null,
}); });
return stats; return stats;
@ -336,9 +336,9 @@ export function convertFileInfoToBigIntStats(
isFIFO: () => false, isFIFO: () => false,
isCharacterDevice: () => false, isCharacterDevice: () => false,
isSocket: () => false, isSocket: () => false,
ctime: origin.mtime, ctime: origin.ctime,
ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null, ctimeMs: origin.ctime ? BigInt(origin.ctime.getTime()) : null,
ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null, ctimeNs: origin.ctime ? BigInt(origin.ctime.getTime()) * 1000000n : null,
}); });
return stats; return stats;
} }

View file

@ -31,6 +31,13 @@ Deno.test(
assert( assert(
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000, tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
); );
assert(tempInfo.ctime !== null && now - tempInfo.ctime.valueOf() < 1000);
const mode = tempInfo.mode! & 0o777;
if (Deno.build.os === "windows") {
assertEquals(mode, 0o666);
} else {
assertEquals(mode, 0o600);
}
const readmeInfoByUrl = Deno.statSync(pathToAbsoluteFileUrl("README.md")); const readmeInfoByUrl = Deno.statSync(pathToAbsoluteFileUrl("README.md"));
assert(readmeInfoByUrl.isFile); assert(readmeInfoByUrl.isFile);
@ -65,6 +72,10 @@ Deno.test(
tempInfoByUrl.birthtime === null || tempInfoByUrl.birthtime === null ||
now - tempInfoByUrl.birthtime.valueOf() < 1000, now - tempInfoByUrl.birthtime.valueOf() < 1000,
); );
assert(
tempInfoByUrl.ctime !== null &&
now - tempInfoByUrl.ctime.valueOf() < 1000,
);
Deno.removeSync(tempFile, { recursive: true }); Deno.removeSync(tempFile, { recursive: true });
Deno.removeSync(tempFileForUrl, { recursive: true }); Deno.removeSync(tempFileForUrl, { recursive: true });
@ -171,6 +182,7 @@ Deno.test(
assert( assert(
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000, tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
); );
assert(tempInfo.ctime !== null && now - tempInfo.ctime.valueOf() < 1000);
const tempFileForUrl = await Deno.makeTempFile(); const tempFileForUrl = await Deno.makeTempFile();
const tempInfoByUrl = await Deno.stat( const tempInfoByUrl = await Deno.stat(
@ -191,7 +203,10 @@ Deno.test(
tempInfoByUrl.birthtime === null || tempInfoByUrl.birthtime === null ||
now - tempInfoByUrl.birthtime.valueOf() < 1000, now - tempInfoByUrl.birthtime.valueOf() < 1000,
); );
assert(
tempInfoByUrl.ctime !== null &&
now - tempInfoByUrl.ctime.valueOf() < 1000,
);
Deno.removeSync(tempFile, { recursive: true }); Deno.removeSync(tempFile, { recursive: true });
Deno.removeSync(tempFileForUrl, { recursive: true }); Deno.removeSync(tempFileForUrl, { recursive: true });
}, },
@ -271,7 +286,6 @@ Deno.test(
const s = Deno.statSync(filename); const s = Deno.statSync(filename);
assert(s.dev !== 0); assert(s.dev !== 0);
assert(s.ino === null); assert(s.ino === null);
assert(s.mode === null);
assert(s.nlink === null); assert(s.nlink === null);
assert(s.uid === null); assert(s.uid === null);
assert(s.gid === null); assert(s.gid === null);

View file

@ -18,9 +18,11 @@ export function assertStats(actual: Stats, expected: Deno.FileInfo) {
assertEquals(actual.atime?.getTime(), expected.atime?.getTime()); assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime()); assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime()); assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
assertEquals(actual.ctime?.getTime(), expected.ctime?.getTime());
assertEquals(actual.atimeMs ?? undefined, expected.atime?.getTime()); assertEquals(actual.atimeMs ?? undefined, expected.atime?.getTime());
assertEquals(actual.mtimeMs ?? undefined, expected.mtime?.getTime()); assertEquals(actual.mtimeMs ?? undefined, expected.mtime?.getTime());
assertEquals(actual.birthtimeMs ?? undefined, expected.birthtime?.getTime()); assertEquals(actual.birthtimeMs ?? undefined, expected.birthtime?.getTime());
assertEquals(actual.ctimeMs ?? undefined, expected.ctime?.getTime());
assertEquals(actual.isFile(), expected.isFile); assertEquals(actual.isFile(), expected.isFile);
assertEquals(actual.isDirectory(), expected.isDirectory); assertEquals(actual.isDirectory(), expected.isDirectory);
assertEquals(actual.isSymbolicLink(), expected.isSymlink); assertEquals(actual.isSymbolicLink(), expected.isSymlink);
@ -49,6 +51,7 @@ export function assertStatsBigInt(
assertEquals(actual.atime?.getTime(), expected.atime?.getTime()); assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime()); assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime()); assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
assertEquals(actual.ctime?.getTime(), expected.ctime?.getTime());
assertEquals( assertEquals(
actual.atimeMs === null ? undefined : Number(actual.atimeMs), actual.atimeMs === null ? undefined : Number(actual.atimeMs),
expected.atime?.getTime(), expected.atime?.getTime(),
@ -61,6 +64,10 @@ export function assertStatsBigInt(
actual.birthtimeMs === null ? undefined : Number(actual.birthtimeMs), actual.birthtimeMs === null ? undefined : Number(actual.birthtimeMs),
expected.birthtime?.getTime(), expected.birthtime?.getTime(),
); );
assertEquals(
actual.ctimeMs === null ? undefined : Number(actual.ctimeMs),
expected.ctime?.getTime(),
);
assertEquals(actual.atimeNs === null, actual.atime === null); assertEquals(actual.atimeNs === null, actual.atime === null);
assertEquals(actual.mtimeNs === null, actual.mtime === null); assertEquals(actual.mtimeNs === null, actual.mtime === null);
assertEquals(actual.birthtimeNs === null, actual.birthtime === null); assertEquals(actual.birthtimeNs === null, actual.birthtime === null);