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

fix(ext/node): Add fs.lutimes / fs.lutimesSync (#23172)

Part of #18218


- Adds `fs.lutimes` and `fs.lutimesSync` to our node polyfills. To do
this I added methods to the `FileSystem` trait + ops to expose the
functionality to JS.
- Exports `fs._toUnixTimestamp`. Node exposes an internal util
`toUnixTimestamp` from the fs module to be used by unit tests (so we
need it for the unit test to pass unmodified). It's weird because it's
only supposed to be used internally but it's still publicly accessible
- Matches up error handling and timestamp handling for fs.futimes and
fs.utimes with node
- Enables the node_compat utimes test - this exercises futimes, lutimes,
and utimes.
This commit is contained in:
Nathan Whitaker 2024-07-02 19:33:32 -07:00 committed by GitHub
parent 3324d7203e
commit dadc606419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 511 additions and 19 deletions

View file

@ -349,4 +349,29 @@ impl FileSystem for DenoCompileFileSystem {
.utime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
.await
}
fn lutime_sync(
&self,
path: &Path,
atime_secs: i64,
atime_nanos: u32,
mtime_secs: i64,
mtime_nanos: u32,
) -> FsResult<()> {
self.error_if_in_vfs(path)?;
RealFs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
}
async fn lutime_async(
&self,
path: PathBuf,
atime_secs: i64,
atime_nanos: u32,
mtime_secs: i64,
mtime_nanos: u32,
) -> FsResult<()> {
self.error_if_in_vfs(&path)?;
RealFs
.lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
.await
}
}

View file

@ -473,8 +473,8 @@ function toUnixTimeFromEpoch(value) {
];
}
const seconds = value;
const nanoseconds = 0;
const seconds = MathTrunc(value);
const nanoseconds = MathTrunc((value * 1e3) - (seconds * 1e3)) * 1e6;
return [
seconds,

View file

@ -350,6 +350,27 @@ impl FileSystem for InMemoryFs {
self.utime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
}
fn lutime_sync(
&self,
_path: &Path,
_atime_secs: i64,
_atime_nanos: u32,
_mtime_secs: i64,
_mtime_nanos: u32,
) -> FsResult<()> {
Err(FsError::NotSupported)
}
async fn lutime_async(
&self,
path: PathBuf,
atime_secs: i64,
atime_nanos: u32,
mtime_secs: i64,
mtime_nanos: u32,
) -> FsResult<()> {
self.lutime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
}
fn write_file_sync(
&self,
path: &Path,

View file

@ -221,6 +221,23 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync {
mtime_nanos: u32,
) -> FsResult<()>;
fn lutime_sync(
&self,
path: &Path,
atime_secs: i64,
atime_nanos: u32,
mtime_secs: i64,
mtime_nanos: u32,
) -> FsResult<()>;
async fn lutime_async(
&self,
path: PathBuf,
atime_secs: i64,
atime_nanos: u32,
mtime_secs: i64,
mtime_nanos: u32,
) -> FsResult<()>;
fn write_file_sync(
&self,
path: &Path,

View file

@ -274,6 +274,35 @@ impl FileSystem for RealFs {
.await?
}
fn lutime_sync(
&self,
path: &Path,
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: PathBuf,
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 write_file_sync(
&self,
path: &Path,
@ -927,9 +956,14 @@ fn open_with_access_check(
};
(*access_check)(true, &path, &options)?;
// For windows
#[allow(unused_mut)]
let mut opts: fs::OpenOptions = open_options(options);
#[cfg(windows)]
{
// allow opening directories
use std::os::windows::fs::OpenOptionsExt;
opts.custom_flags(winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS);
}
#[cfg(unix)]
{
// Don't follow symlinks on open -- we must always pass fully-resolved files
@ -943,7 +977,15 @@ fn open_with_access_check(
Ok(opts.open(&path)?)
} else {
let opts = open_options(options);
// for unix
#[allow(unused_mut)]
let mut opts = open_options(options);
#[cfg(windows)]
{
// allow opening directories
use std::os::windows::fs::OpenOptionsExt;
opts.custom_flags(winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS);
}
Ok(opts.open(path)?)
}
}

View file

@ -315,6 +315,8 @@ deno_core::extension!(deno_node,
ops::fs::op_node_fs_exists_sync<P>,
ops::fs::op_node_cp_sync<P>,
ops::fs::op_node_cp<P>,
ops::fs::op_node_lutimes_sync<P>,
ops::fs::op_node_lutimes<P>,
ops::fs::op_node_statfs<P>,
ops::winerror::op_node_sys_to_uv_error,
ops::v8::op_v8_cached_data_version_tag,
@ -426,6 +428,7 @@ deno_core::extension!(deno_node,
"_fs/_fs_futimes.ts",
"_fs/_fs_link.ts",
"_fs/_fs_lstat.ts",
"_fs/_fs_lutimes.ts",
"_fs/_fs_mkdir.ts",
"_fs/_fs_mkdtemp.ts",
"_fs/_fs_open.ts",

View file

@ -222,3 +222,54 @@ where
Err(anyhow!("Unsupported platform."))
}
}
#[op2(fast)]
pub fn op_node_lutimes_sync<P>(
state: &mut OpState,
#[string] path: &str,
#[number] atime_secs: i64,
#[smi] atime_nanos: u32,
#[number] mtime_secs: i64,
#[smi] mtime_nanos: u32,
) -> Result<(), AnyError>
where
P: NodePermissions + 'static,
{
let path = Path::new(path);
state
.borrow_mut::<P>()
.check_write_with_api_name(path, Some("node:fs.lutimes"))?;
let fs = state.borrow::<FileSystemRc>();
fs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)?;
Ok(())
}
#[op2(async)]
pub async fn op_node_lutimes<P>(
state: Rc<RefCell<OpState>>,
#[string] path: String,
#[number] atime_secs: i64,
#[smi] atime_nanos: u32,
#[number] mtime_secs: i64,
#[smi] mtime_nanos: u32,
) -> Result<(), AnyError>
where
P: NodePermissions + 'static,
{
let path = PathBuf::from(path);
let fs = {
let mut state = state.borrow_mut();
state
.borrow_mut::<P>()
.check_write_with_api_name(&path, Some("node:fs.lutimesSync"))?;
state.borrow::<FileSystemRc>().clone()
};
fs.lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)
.await?;
Ok(())
}

View file

@ -5,6 +5,9 @@
import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts";
import { FsFile } from "ext:deno_fs/30_fs.js";
import { validateInteger } from "ext:deno_node/internal/validators.mjs";
import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts";
import { toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs";
function getValidTime(
time: number | string | Date,
@ -23,7 +26,7 @@ function getValidTime(
);
}
return time;
return toUnixTimestamp(time);
}
export function futimes(
@ -35,6 +38,11 @@ export function futimes(
if (!callback) {
throw new Deno.errors.InvalidData("No callback function supplied");
}
if (typeof fd !== "number") {
throw new ERR_INVALID_ARG_TYPE("fd", "number", fd);
}
validateInteger(fd, "fd", 0, 2147483647);
atime = getValidTime(atime, "atime");
mtime = getValidTime(mtime, "mtime");
@ -51,6 +59,12 @@ export function futimesSync(
atime: number | string | Date,
mtime: number | string | Date,
) {
if (typeof fd !== "number") {
throw new ERR_INVALID_ARG_TYPE("fd", "number", fd);
}
validateInteger(fd, "fd", 0, 2147483647);
atime = getValidTime(atime, "atime");
mtime = getValidTime(mtime, "mtime");

View file

@ -0,0 +1,85 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file prefer-primordials
import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts";
import { type Buffer } from "node:buffer";
import { primordials } from "ext:core/mod.js";
import { op_node_lutimes, op_node_lutimes_sync } from "ext:core/ops";
import { promisify } from "ext:deno_node/internal/util.mjs";
import {
getValidatedPath,
toUnixTimestamp,
} from "ext:deno_node/internal/fs/utils.mjs";
const { MathTrunc } = primordials;
type TimeLike = number | string | Date;
type PathLike = string | Buffer | URL;
function getValidUnixTime(
value: TimeLike,
name: string,
): [number, number] {
if (typeof value === "string") {
value = Number(value);
}
if (
typeof value === "number" &&
(Number.isNaN(value) || !Number.isFinite(value))
) {
throw new Deno.errors.InvalidData(
`invalid ${name}, must not be infinity or NaN`,
);
}
const unixSeconds = toUnixTimestamp(value);
const seconds = MathTrunc(unixSeconds);
const nanoseconds = MathTrunc((unixSeconds * 1e3) - (seconds * 1e3)) * 1e6;
return [
seconds,
nanoseconds,
];
}
export function lutimes(
path: PathLike,
atime: TimeLike,
mtime: TimeLike,
callback: CallbackWithError,
): void {
if (!callback) {
throw new Error("No callback function supplied");
}
const [atimeSecs, atimeNanos] = getValidUnixTime(atime, "atime");
const [mtimeSecs, mtimeNanos] = getValidUnixTime(mtime, "mtime");
path = getValidatedPath(path).toString();
op_node_lutimes(path, atimeSecs, atimeNanos, mtimeSecs, mtimeNanos).then(
() => callback(null),
callback,
);
}
export function lutimesSync(
path: PathLike,
atime: TimeLike,
mtime: TimeLike,
): void {
const { 0: atimeSecs, 1: atimeNanos } = getValidUnixTime(atime, "atime");
const { 0: mtimeSecs, 1: mtimeNanos } = getValidUnixTime(mtime, "mtime");
path = getValidatedPath(path).toString();
op_node_lutimes_sync(path, atimeSecs, atimeNanos, mtimeSecs, mtimeNanos);
}
export const lutimesPromise = promisify(lutimes) as (
path: PathLike,
atime: TimeLike,
mtime: TimeLike,
) => Promise<void>;

View file

@ -4,13 +4,16 @@
// deno-lint-ignore-file prefer-primordials
import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts";
import { pathFromURL } from "ext:deno_web/00_infra.js";
import { promisify } from "ext:deno_node/internal/util.mjs";
import {
getValidatedPath,
toUnixTimestamp,
} from "ext:deno_node/internal/fs/utils.mjs";
function getValidTime(
time: number | string | Date,
name: string,
): number | Date {
): number {
if (typeof time === "string") {
time = Number(time);
}
@ -24,7 +27,7 @@ function getValidTime(
);
}
return time;
return toUnixTimestamp(time);
}
export function utimes(
@ -33,7 +36,7 @@ export function utimes(
mtime: number | string | Date,
callback: CallbackWithError,
) {
path = path instanceof URL ? pathFromURL(path) : path;
path = getValidatedPath(path).toString();
if (!callback) {
throw new Deno.errors.InvalidData("No callback function supplied");
@ -56,7 +59,7 @@ export function utimesSync(
atime: number | string | Date,
mtime: number | string | Date,
) {
path = path instanceof URL ? pathFromURL(path) : path;
path = getValidatedPath(path).toString();
atime = getValidTime(atime, "atime");
mtime = getValidTime(mtime, "mtime");

View file

@ -29,6 +29,11 @@ import { ftruncate, ftruncateSync } from "ext:deno_node/_fs/_fs_ftruncate.ts";
import { futimes, futimesSync } from "ext:deno_node/_fs/_fs_futimes.ts";
import { link, linkPromise, linkSync } from "ext:deno_node/_fs/_fs_link.ts";
import { lstat, lstatPromise, lstatSync } from "ext:deno_node/_fs/_fs_lstat.ts";
import {
lutimes,
lutimesPromise,
lutimesSync,
} from "ext:deno_node/_fs/_fs_lutimes.ts";
import { mkdir, mkdirPromise, mkdirSync } from "ext:deno_node/_fs/_fs_mkdir.ts";
import {
mkdtemp,
@ -123,6 +128,7 @@ import {
ReadStream,
WriteStream,
} from "ext:deno_node/internal/fs/streams.mjs";
import { toUnixTimestamp as _toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs";
const {
F_OK,
@ -170,7 +176,7 @@ const promises = {
// lchown: promisify(lchown),
chown: chownPromise,
utimes: utimesPromise,
// lutimes = promisify(lutimes),
lutimes: lutimesPromise,
realpath: realpathPromise,
mkdtemp: mkdtempPromise,
writeFile: writeFilePromise,
@ -216,6 +222,8 @@ export default {
linkSync,
lstat,
lstatSync,
lutimes,
lutimesSync,
mkdir,
mkdirSync,
mkdtemp,
@ -284,9 +292,13 @@ export default {
WriteStream,
writeSync,
X_OK,
// For tests
_toUnixTimestamp,
};
export {
// For tests
_toUnixTimestamp,
access,
accessSync,
appendFile,
@ -323,6 +335,8 @@ export {
linkSync,
lstat,
lstatSync,
lutimes,
lutimesSync,
mkdir,
mkdirSync,
mkdtemp,

View file

@ -5,6 +5,8 @@
"use strict";
import { primordials } from "ext:core/mod.js";
const { DatePrototypeGetTime } = primordials;
import { Buffer } from "node:buffer";
import {
ERR_FS_EISDIR,
@ -698,7 +700,7 @@ export function toUnixTimestamp(time, name = "time") {
}
if (isDate(time)) {
// Convert to 123.456 UNIX timestamp
return Date.getTime(time) / 1000;
return DatePrototypeGetTime(time) / 1000;
}
throw new ERR_INVALID_ARG_TYPE(name, ["Date", "Time in seconds"], time);
}

View file

@ -359,6 +359,7 @@
"test-fs-rmdir-recursive-warns-on-file.js",
"test-fs-rmdir-recursive.js",
"test-fs-rmdir-type-check.js",
"test-fs-utimes.js",
"test-fs-watchfile.js",
"test-fs-write-buffer.js",
"test-fs-write-file-buffer.js",

View file

@ -899,7 +899,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
- [parallel/test-fs-util-validateoffsetlength.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-util-validateoffsetlength.js)
- [parallel/test-fs-utils-get-dirents.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-utils-get-dirents.js)
- [parallel/test-fs-utimes-y2K38.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-utimes-y2K38.js)
- [parallel/test-fs-utimes.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-utimes.js)
- [parallel/test-fs-watch-abort-signal.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-watch-abort-signal.js)
- [parallel/test-fs-watch-close-when-destroyed.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-watch-close-when-destroyed.js)
- [parallel/test-fs-watch-encoding.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-fs-watch-encoding.js)

View file

@ -86,6 +86,7 @@ async function runTest(t: Deno.TestContext, path: string): Promise<void> {
//"--unsafely-ignore-certificate-errors",
"--unstable-unsafe-proto",
"--unstable-bare-node-builtins",
"--unstable-fs",
"--v8-flags=" + v8Flags.join(),
];
if (usesNodeTest) {

View file

@ -0,0 +1,218 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
const assert = require('assert');
const util = require('util');
const fs = require('fs');
const url = require('url');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const lpath = `${tmpdir.path}/symlink`;
fs.symlinkSync('unoent-entry', lpath);
function stat_resource(resource, statSync = fs.statSync) {
if (typeof resource === 'string') {
return statSync(resource);
}
const stats = fs.fstatSync(resource);
// Ensure mtime has been written to disk
// except for directories on AIX where it cannot be synced
if ((common.isAIX || common.isIBMi) && stats.isDirectory())
return stats;
fs.fsyncSync(resource);
return fs.fstatSync(resource);
}
function check_mtime(resource, mtime, statSync) {
mtime = fs._toUnixTimestamp(mtime);
const stats = stat_resource(resource, statSync);
const real_mtime = fs._toUnixTimestamp(stats.mtime);
return mtime - real_mtime;
}
function expect_errno(syscall, resource, err, errno) {
assert(
err && (err.code === errno || err.code === 'ENOSYS'),
`FAILED: expect_errno ${util.inspect(arguments)}`
);
}
function expect_ok(syscall, resource, err, atime, mtime, statSync) {
const mtime_diff = check_mtime(resource, mtime, statSync);
assert(
// Check up to single-second precision.
// Sub-second precision is OS and fs dependant.
!err && (mtime_diff < 2) || err && err.code === 'ENOSYS',
`FAILED: expect_ok ${util.inspect(arguments)}
check_mtime: ${mtime_diff}`
);
}
const stats = fs.statSync(tmpdir.path);
const asPath = (path) => path;
const asUrl = (path) => url.pathToFileURL(path);
const cases = [
[asPath, new Date('1982-09-10 13:37')],
[asPath, new Date()],
[asPath, 123456.789],
[asPath, stats.mtime],
[asPath, '123456', -1],
[asPath, new Date('2017-04-08T17:59:38.008Z')],
[asUrl, new Date()],
];
runTests(cases.values());
function runTests(iter) {
const { value, done } = iter.next();
if (done) return;
// Support easy setting same or different atime / mtime values.
const [pathType, atime, mtime = atime] = value;
let fd;
//
// test async code paths
//
fs.utimes(pathType(tmpdir.path), atime, mtime, common.mustCall((err) => {
expect_ok('utimes', tmpdir.path, err, atime, mtime);
fs.lutimes(pathType(lpath), atime, mtime, common.mustCall((err) => {
expect_ok('lutimes', lpath, err, atime, mtime, fs.lstatSync);
fs.utimes(pathType('foobarbaz'), atime, mtime, common.mustCall((err) => {
expect_errno('utimes', 'foobarbaz', err, 'ENOENT');
// don't close this fd
if (common.isWindows) {
fd = fs.openSync(tmpdir.path, 'r+');
} else {
fd = fs.openSync(tmpdir.path, 'r');
}
fs.futimes(fd, atime, mtime, common.mustCall((err) => {
expect_ok('futimes', fd, err, atime, mtime);
syncTests();
setImmediate(common.mustCall(runTests), iter);
}));
}));
}));
}));
//
// test synchronized code paths, these functions throw on failure
//
function syncTests() {
fs.utimesSync(pathType(tmpdir.path), atime, mtime);
expect_ok('utimesSync', tmpdir.path, undefined, atime, mtime);
fs.lutimesSync(pathType(lpath), atime, mtime);
expect_ok('lutimesSync', lpath, undefined, atime, mtime, fs.lstatSync);
// Some systems don't have futimes
// if there's an error, it should be ENOSYS
try {
fs.futimesSync(fd, atime, mtime);
expect_ok('futimesSync', fd, undefined, atime, mtime);
} catch (ex) {
expect_errno('futimesSync', fd, ex, 'ENOSYS');
}
let err;
try {
fs.utimesSync(pathType('foobarbaz'), atime, mtime);
} catch (ex) {
err = ex;
}
expect_errno('utimesSync', 'foobarbaz', err, 'ENOENT');
err = undefined;
}
}
const expectTypeError = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
};
// utimes-only error cases
{
assert.throws(
() => fs.utimes(0, new Date(), new Date(), common.mustNotCall()),
expectTypeError
);
assert.throws(
() => fs.utimesSync(0, new Date(), new Date()),
expectTypeError
);
}
// shared error cases
[false, {}, [], null, undefined].forEach((i) => {
assert.throws(
() => fs.utimes(i, new Date(), new Date(), common.mustNotCall()),
expectTypeError
);
assert.throws(
() => fs.utimesSync(i, new Date(), new Date()),
expectTypeError
);
assert.throws(
() => fs.futimes(i, new Date(), new Date(), common.mustNotCall()),
expectTypeError
);
assert.throws(
() => fs.futimesSync(i, new Date(), new Date()),
expectTypeError
);
});
const expectRangeError = {
code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
message: 'The value of "fd" is out of range. ' +
'It must be >= 0 && <= 2147483647. Received -1'
};
// futimes-only error cases
{
assert.throws(
() => fs.futimes(-1, new Date(), new Date(), common.mustNotCall()),
expectRangeError
);
assert.throws(
() => fs.futimesSync(-1, new Date(), new Date()),
expectRangeError
);
}

View file

@ -172,11 +172,7 @@ Deno.test(
try {
await Deno.readFile("tests/testdata/assets/");
} catch (e) {
if (Deno.build.os === "windows") {
assertEquals(e.code, "EPERM");
} else {
assertEquals(e.code, "EISDIR");
}
}
},
);