Skip to content

Commit 100f6de

Browse files
LiviaMedeirosdanielleadams
authored andcommitted
fs: use signed types for stat data
This allows to support timestamps before 1970-01-01. On Windows, it's not supported due to Y2038 issue. PR-URL: #43714 Fixes: #43707 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent e89e0b4 commit 100f6de

File tree

5 files changed

+105
-14
lines changed

5 files changed

+105
-14
lines changed

lib/internal/fs/utils.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
NumberIsFinite,
1313
NumberIsInteger,
1414
MathMin,
15+
MathRound,
1516
ObjectIs,
1617
ObjectPrototypeHasOwnProperty,
1718
ObjectSetPrototypeOf,
@@ -39,9 +40,9 @@ const {
3940
} = require('internal/errors');
4041
const {
4142
isArrayBufferView,
42-
isUint8Array,
43+
isBigInt64Array,
4344
isDate,
44-
isBigUint64Array
45+
isUint8Array,
4546
} = require('internal/util/types');
4647
const {
4748
kEmptyObject,
@@ -454,14 +455,16 @@ function nsFromTimeSpecBigInt(sec, nsec) {
454455
return sec * kNsPerSecBigInt + nsec;
455456
}
456457

457-
// The Date constructor performs Math.floor() to the timestamp.
458-
// https://www.ecma-international.org/ecma-262/#sec-timeclip
458+
// The Date constructor performs Math.floor() on the absolute value
459+
// of the timestamp: https://tc39.es/ecma262/#sec-timeclip
459460
// Since there may be a precision loss when the timestamp is
460461
// converted to a floating point number, we manually round
461462
// the timestamp here before passing it to Date().
462463
// Refs: https://github.com/nodejs/node/pull/12607
464+
// Refs: https://github.com/nodejs/node/pull/43714
463465
function dateFromMs(ms) {
464-
return new Date(Number(ms) + 0.5);
466+
// Coercing to number, ms can be bigint
467+
return new Date(MathRound(Number(ms)));
465468
}
466469

467470
function BigIntStats(dev, mode, nlink, uid, gid, rdev, blksize,
@@ -526,12 +529,12 @@ Stats.prototype._checkModeProperty = function(property) {
526529
};
527530

528531
/**
529-
* @param {Float64Array | BigUint64Array} stats
532+
* @param {Float64Array | BigInt64Array} stats
530533
* @param {number} offset
531534
* @returns {BigIntStats | Stats}
532535
*/
533536
function getStatsFromBinding(stats, offset = 0) {
534-
if (isBigUint64Array(stats)) {
537+
if (isBigInt64Array(stats)) {
535538
return new BigIntStats(
536539
stats[0 + offset], stats[1 + offset], stats[2 + offset],
537540
stats[3 + offset], stats[4 + offset], stats[5 + offset],

src/aliased_buffer.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ typedef AliasedBufferBase<int32_t, v8::Int32Array> AliasedInt32Array;
307307
typedef AliasedBufferBase<uint8_t, v8::Uint8Array> AliasedUint8Array;
308308
typedef AliasedBufferBase<uint32_t, v8::Uint32Array> AliasedUint32Array;
309309
typedef AliasedBufferBase<double, v8::Float64Array> AliasedFloat64Array;
310-
typedef AliasedBufferBase<uint64_t, v8::BigUint64Array> AliasedBigUint64Array;
310+
typedef AliasedBufferBase<int64_t, v8::BigInt64Array> AliasedBigInt64Array;
311311
} // namespace node
312312

313313
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

src/node_file-inl.h

+14-5
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,22 @@ template <typename NativeT, typename V8T>
8686
void FillStatsArray(AliasedBufferBase<NativeT, V8T>* fields,
8787
const uv_stat_t* s,
8888
const size_t offset) {
89-
#define SET_FIELD_WITH_STAT(stat_offset, stat) \
90-
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
89+
#define SET_FIELD_WITH_STAT(stat_offset, stat) \
90+
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
9191
static_cast<NativeT>(stat))
9292

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

97106
SET_FIELD_WITH_STAT(kDev, s->st_dev);
98107
SET_FIELD_WITH_STAT(kMode, s->st_mode);
@@ -233,7 +242,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo<v8::Value>& args,
233242
Environment* env = binding_data->env();
234243
if (value->StrictEquals(env->fs_use_promises_symbol())) {
235244
if (use_bigint) {
236-
return FSReqPromise<AliasedBigUint64Array>::New(binding_data, use_bigint);
245+
return FSReqPromise<AliasedBigInt64Array>::New(binding_data, use_bigint);
237246
} else {
238247
return FSReqPromise<AliasedFloat64Array>::New(binding_data, use_bigint);
239248
}

src/node_file.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class BindingData : public SnapshotableObject {
1818
explicit BindingData(Environment* env, v8::Local<v8::Object> wrap);
1919

2020
AliasedFloat64Array stats_field_array;
21-
AliasedBigUint64Array stats_field_bigint_array;
21+
AliasedBigInt64Array stats_field_bigint_array;
2222

2323
std::vector<BaseObjectPtr<FileHandleReadWrap>>
2424
file_handle_read_wrap_freelist;

test/parallel/test-fs-stat-date.mjs

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as common from '../common/index.mjs';
2+
3+
// Test timestamps returned by fsPromises.stat and fs.statSync
4+
5+
import fs from 'node:fs';
6+
import fsPromises from 'node:fs/promises';
7+
import path from 'node:path';
8+
import assert from 'node:assert';
9+
import tmpdir from '../common/tmpdir.js';
10+
11+
// On some platforms (for example, ppc64) boundaries are tighter
12+
// than usual. If we catch these errors, skip corresponding test.
13+
const ignoredErrors = new Set(['EINVAL', 'EOVERFLOW']);
14+
15+
tmpdir.refresh();
16+
const filepath = path.resolve(tmpdir.path, 'timestamp');
17+
18+
await (await fsPromises.open(filepath, 'w')).close();
19+
20+
// Date might round down timestamp
21+
function closeEnough(actual, expected, margin) {
22+
// On ppc64, value is rounded to seconds
23+
if (process.arch === 'ppc64') {
24+
margin += 1000;
25+
}
26+
assert.ok(Math.abs(Number(actual - expected)) < margin,
27+
`expected ${expected} ± ${margin}, got ${actual}`);
28+
}
29+
30+
async function runTest(atime, mtime, margin = 0) {
31+
margin += Number.EPSILON;
32+
try {
33+
await fsPromises.utimes(filepath, new Date(atime), new Date(mtime));
34+
} catch (e) {
35+
if (ignoredErrors.has(e.code)) return;
36+
throw e;
37+
}
38+
39+
const stats = await fsPromises.stat(filepath);
40+
closeEnough(stats.atimeMs, atime, margin);
41+
closeEnough(stats.mtimeMs, mtime, margin);
42+
closeEnough(stats.atime.getTime(), new Date(atime).getTime(), margin);
43+
closeEnough(stats.mtime.getTime(), new Date(mtime).getTime(), margin);
44+
45+
const statsBigint = await fsPromises.stat(filepath, { bigint: true });
46+
closeEnough(statsBigint.atimeMs, BigInt(atime), margin);
47+
closeEnough(statsBigint.mtimeMs, BigInt(mtime), margin);
48+
closeEnough(statsBigint.atime.getTime(), new Date(atime).getTime(), margin);
49+
closeEnough(statsBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
50+
51+
const statsSync = fs.statSync(filepath);
52+
closeEnough(statsSync.atimeMs, atime, margin);
53+
closeEnough(statsSync.mtimeMs, mtime, margin);
54+
closeEnough(statsSync.atime.getTime(), new Date(atime).getTime(), margin);
55+
closeEnough(statsSync.mtime.getTime(), new Date(mtime).getTime(), margin);
56+
57+
const statsSyncBigint = fs.statSync(filepath, { bigint: true });
58+
closeEnough(statsSyncBigint.atimeMs, BigInt(atime), margin);
59+
closeEnough(statsSyncBigint.mtimeMs, BigInt(mtime), margin);
60+
closeEnough(statsSyncBigint.atime.getTime(), new Date(atime).getTime(), margin);
61+
closeEnough(statsSyncBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
62+
}
63+
64+
// Too high/low numbers produce too different results on different platforms
65+
{
66+
// TODO(LiviaMedeiros): investigate outdated stat time on FreeBSD.
67+
// On Windows, filetime is stored and handled differently. Supporting dates
68+
// after Y2038 is preferred over supporting dates before 1970-01-01.
69+
if (!common.isFreeBSD && !common.isWindows) {
70+
await runTest(-40691, -355, 1); // Potential precision loss on 32bit
71+
await runTest(-355, -40691, 1); // Potential precision loss on 32bit
72+
await runTest(-1, -1);
73+
}
74+
await runTest(0, 0);
75+
await runTest(1, 1);
76+
await runTest(355, 40691, 1); // Precision loss on 32bit
77+
await runTest(40691, 355, 1); // Precision loss on 32bit
78+
await runTest(1713037251360, 1713037251360, 1); // Precision loss
79+
}

0 commit comments

Comments
 (0)