Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2229,7 +2229,8 @@ Change the file system timestamps of the object referenced by `path`.
The `atime` and `mtime` arguments follow these rules:

* Values can be either numbers representing Unix epoch time, `Date`s, or a
numeric string like `'123456789.0'`.
numeric string like `'123456789.0'`. Negative values represent times before
the Unix epoch.
* If the value can not be converted to a number, or is `NaN`, `Infinity`, or
`-Infinity`, an `Error` will be thrown.

Expand Down Expand Up @@ -5316,7 +5317,8 @@ Change the file system timestamps of the object referenced by `path`.
The `atime` and `mtime` arguments follow these rules:

* Values can be either numbers representing Unix epoch time in seconds,
`Date`s, or a numeric string like `'123456789.0'`.
`Date`s, or a numeric string like `'123456789.0'`. Negative values represent
times before the Unix epoch.
* If the value can not be converted to a number, or is `NaN`, `Infinity`, or
`-Infinity`, an `Error` will be thrown.

Expand Down Expand Up @@ -8250,6 +8252,9 @@ The times in the stat object have the following semantics:
Prior to Node.js 0.12, the `ctime` held the `birthtime` on Windows systems. As
of 0.12, `ctime` is not "creation time", and on Unix systems, it never was.

On Windows, times are limited to the range of a signed 32-bit number of seconds
from the Unix epoch, so times after `2038-01-19T03:14:07Z` are not supported.

### Class: `fs.StatFs`

<!-- YAML
Expand Down
4 changes: 0 additions & 4 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const {
ArrayPrototypePush,
BigInt,
Date,
DateNow,
DatePrototypeGetTime,
ErrorCaptureStackTrace,
FunctionPrototypeCall,
Expand Down Expand Up @@ -867,9 +866,6 @@ function toUnixTimestamp(time, name = 'time') {
return +time;
}
if (NumberIsFinite(time)) {
if (time < 0) {
return DateNow() / 1000;
}
return time;
}
if (isDate(time)) {
Expand Down
10 changes: 0 additions & 10 deletions src/node_file-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,8 @@ void FillStatsArray(AliasedBufferBase<NativeT, V8T>* fields,
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
static_cast<NativeT>(stat))

// On win32, time is stored in uint64_t and starts from 1601-01-01.
// libuv calculates tv_sec and tv_nsec from it and converts to signed long,
// which causes Y2038 overflow. On the other platforms it is safe to treat
// negative values as pre-epoch time.
#ifdef _WIN32
#define SET_FIELD_WITH_TIME_STAT(stat_offset, stat) \
/* NOLINTNEXTLINE(runtime/int) */ \
SET_FIELD_WITH_STAT(stat_offset, static_cast<unsigned long>(stat))
#else
#define SET_FIELD_WITH_TIME_STAT(stat_offset, stat) \
SET_FIELD_WITH_STAT(stat_offset, static_cast<double>(stat))
#endif // _WIN32

SET_FIELD_WITH_STAT(kDev, s->st_dev);
SET_FIELD_WITH_STAT(kMode, s->st_mode);
Expand Down
43 changes: 18 additions & 25 deletions test/parallel/test-fs-utimes-y2K38.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,24 @@ tmpdir.refresh();
const assert = require('assert');
const fs = require('fs');

// Check for Y2K38 support. For Windows, assume it's there. Windows
// doesn't have `touch` and `date -r` which are used in the check for support.
if (!common.isWindows) {
// Ref: https://github.com/nodejs/node/issues/13255
const path = `${tmpdir.path}/test-utimes-precision`;
fs.writeFileSync(path, '');

if (common.isWindows) {
// This value would get converted to (double)1713037251359.9998
const truncate_mtime = 1713037251360;
fs.utimesSync(path, truncate_mtime / 1000, truncate_mtime / 1000);
const truncate_stats = fs.statSync(path);
assert.strictEqual(truncate_stats.mtime.getTime(), truncate_mtime);

// Libuv stores Windows timestamps in a 32-bit `long tv_sec`, so Y2K38
// times can not be represented. Negative values are pre-epoch times.
common.skip('Y2K38 is not supported on Windows');
}

// Check for Y2K38 support.
{
const testFilePath = `${tmpdir.path}/y2k38-test`;
const testFileDate = '204001020304';
const { spawnSync } = require('child_process');
Expand Down Expand Up @@ -38,29 +53,7 @@ if (!common.isWindows) {
}
}

// Ref: https://github.com/nodejs/node/issues/13255
const path = `${tmpdir.path}/test-utimes-precision`;
fs.writeFileSync(path, '');

const Y2K38_mtime = 2 ** 31;
fs.utimesSync(path, Y2K38_mtime, Y2K38_mtime);
const Y2K38_stats = fs.statSync(path);
assert.strictEqual(Y2K38_stats.mtime.getTime() / 1000, Y2K38_mtime);

if (common.isWindows) {
// This value would get converted to (double)1713037251359.9998
const truncate_mtime = 1713037251360;
fs.utimesSync(path, truncate_mtime / 1000, truncate_mtime / 1000);
const truncate_stats = fs.statSync(path);
assert.strictEqual(truncate_stats.mtime.getTime(), truncate_mtime);

// test Y2K38 for windows
// This value if treaded as a `signed long` gets converted to -2135622133469.
// POSIX systems stores timestamps in {long t_sec, long t_usec}.
// NTFS stores times in nanoseconds in a single `uint64_t`, so when libuv
// calculates (long)`uv_timespec_t.tv_sec` we get 2's complement.
const overflow_mtime = 2159345162531;
fs.utimesSync(path, overflow_mtime / 1000, overflow_mtime / 1000);
const overflow_stats = fs.statSync(path);
assert.strictEqual(overflow_stats.mtime.getTime(), overflow_mtime);
}
40 changes: 18 additions & 22 deletions test/parallel/test-fs-utimes.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ function stat_resource(resource, statSync = fs.statSync) {
return fs.fstatSync(resource);
}

function toSeconds(time) {
return typeof time === 'object' ? time.getTime() / 1000 : +time;
}

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;
return toSeconds(mtime) - toSeconds(stats.mtime);
}

function expect_errno(syscall, resource, err, errno) {
Expand All @@ -64,17 +66,12 @@ function expect_ok(syscall, resource, err, atime, mtime, statSync) {
assert(
// Check up to single-second precision.
// Sub-second precision is OS and fs dependent.
!err && (mtime_diff < 2) || err && err.code === 'ENOSYS',
!err && (Math.abs(mtime_diff) < 2) || err && err.code === 'ENOSYS',
`FAILED: expect_ok ${util.inspect(arguments)}
check_mtime: ${mtime_diff}`
);
}

function getExpectedMtime(mtime) {
// Negative numeric timestamps are normalized to "now" at call time.
return fs._toUnixTimestamp(mtime);
}

const stats = fs.statSync(tmpdir.path);

const asPath = (path) => path;
Expand All @@ -85,7 +82,12 @@ const cases = [
[asPath, new Date()],
[asPath, 123456.789],
[asPath, stats.mtime],
[asPath, '123456', -1],
// AIX rejects pre-epoch timestamps with EINVAL.
...(common.isAIX ? [] : [
[asPath, '123456', -1],
[asPath, -1234567890],
[asPath, new Date('1969-07-20T20:17:40Z')],
]),
[asPath, new Date('2017-04-08T17:59:38.008Z')],
[asUrl, new Date()],
];
Expand All @@ -103,13 +105,11 @@ function runTests(iter) {
//
// test async code paths
//
const expectedUtimesMtime = getExpectedMtime(mtime);
fs.utimes(pathType(tmpdir.path), atime, mtime, common.mustCall((err) => {
expect_ok('utimes', tmpdir.path, err, atime, expectedUtimesMtime);
expect_ok('utimes', tmpdir.path, err, atime, mtime);

const expectedLutimesMtime = getExpectedMtime(mtime);
fs.lutimes(pathType(lpath), atime, mtime, common.mustCall((err) => {
expect_ok('lutimes', lpath, err, atime, expectedLutimesMtime, fs.lstatSync);
expect_ok('lutimes', lpath, err, atime, mtime, fs.lstatSync);

fs.utimes(pathType('foobarbaz'), atime, mtime, common.mustCall((err) => {
expect_errno('utimes', 'foobarbaz', err, 'ENOENT');
Expand All @@ -121,9 +121,8 @@ function runTests(iter) {
fd = fs.openSync(tmpdir.path, 'r');
}

const expectedFutimesMtime = getExpectedMtime(mtime);
fs.futimes(fd, atime, mtime, common.mustCall((err) => {
expect_ok('futimes', fd, err, atime, expectedFutimesMtime);
expect_ok('futimes', fd, err, atime, mtime);

syncTests();

Expand All @@ -137,20 +136,17 @@ function runTests(iter) {
// test synchronized code paths, these functions throw on failure
//
function syncTests() {
const expectedUtimesMtime = getExpectedMtime(mtime);
fs.utimesSync(pathType(tmpdir.path), atime, mtime);
expect_ok('utimesSync', tmpdir.path, undefined, atime, expectedUtimesMtime);
expect_ok('utimesSync', tmpdir.path, undefined, atime, mtime);

const expectedLutimesMtime = getExpectedMtime(mtime);
fs.lutimesSync(pathType(lpath), atime, mtime);
expect_ok('lutimesSync', lpath, undefined, atime, expectedLutimesMtime, fs.lstatSync);
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 {
const expectedFutimesMtime = getExpectedMtime(mtime);
fs.futimesSync(fd, atime, mtime);
expect_ok('futimesSync', fd, undefined, atime, expectedFutimesMtime);
expect_ok('futimesSync', fd, undefined, atime, mtime);
} catch (ex) {
expect_errno('futimesSync', fd, ex, 'ENOSYS');
}
Expand Down
Loading