Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 100f6de

Browse filesBrowse 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 <info@bnoordhuis.nl> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent e89e0b4 commit 100f6de
Copy full SHA for 100f6de

File tree

Expand file treeCollapse file tree

5 files changed

+105
-14
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+105
-14
lines changed
Open diff view settings
Collapse file

‎lib/internal/fs/utils.js‎

Copy file name to clipboardExpand all lines: lib/internal/fs/utils.js
+10-7Lines changed: 10 additions & 7 deletions
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],
Collapse file

‎src/aliased_buffer.h‎

Copy file name to clipboardExpand all lines: src/aliased_buffer.h
+1-1Lines changed: 1 addition & 1 deletion
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
Collapse file

‎src/node_file-inl.h‎

Copy file name to clipboardExpand all lines: src/node_file-inl.h
+14-5Lines changed: 14 additions & 5 deletions
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
}
Collapse file

‎src/node_file.h‎

Copy file name to clipboardExpand all lines: src/node_file.h
+1-1Lines changed: 1 addition & 1 deletion
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;
Collapse file
+79Lines changed: 79 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.