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: #43714Fixes: #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‎

Lines 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
returnsec*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
functiondateFromMs(ms){
464-
returnnewDate(Number(ms)+0.5);
466+
// Coercing to number, ms can be bigint
467+
returnnewDate(MathRound(Number(ms)));
465468
}
466469

467470
functionBigIntStats(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
functiongetStatsFromBinding(stats,offset=0){
534-
if(isBigUint64Array(stats)){
537+
if(isBigInt64Array(stats)){
535538
returnnewBigIntStats(
536539
stats[0+offset],stats[1+offset],stats[2+offset],
537540
stats[3+offset],stats[4+offset],stats[5+offset],

‎src/aliased_buffer.h‎

Lines 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

‎src/node_file-inl.h‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,22 @@ template <typename NativeT, typename V8T>
8686
voidFillStatsArray(AliasedBufferBase<NativeT, V8T>* fields,
8787
constuv_stat_t* s,
8888
constsize_t offset){
89-
#defineSET_FIELD_WITH_STAT(stat_offset, stat) \
90-
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
89+
#defineSET_FIELD_WITH_STAT(stat_offset, stat) \
90+
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
9191
static_cast<NativeT>(stat))
9292

93-
#defineSET_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+
#defineSET_FIELD_WITH_TIME_STAT(stat_offset, stat) \
99+
/* NOLINTNEXTLINE(runtime/int) */ \
95100
SET_FIELD_WITH_STAT(stat_offset, static_cast<unsignedlong>(stat))
101+
#else
102+
#defineSET_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‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class BindingData : public SnapshotableObject{
1818
explicitBindingData(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;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import*ascommonfrom'../common/index.mjs';
2+
3+
// Test timestamps returned by fsPromises.stat and fs.statSync
4+
5+
importfsfrom'node:fs';
6+
importfsPromisesfrom'node:fs/promises';
7+
importpathfrom'node:path';
8+
importassertfrom'node:assert';
9+
importtmpdirfrom'../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+
constignoredErrors=newSet(['EINVAL','EOVERFLOW']);
14+
15+
tmpdir.refresh();
16+
constfilepath=path.resolve(tmpdir.path,'timestamp');
17+
18+
await(awaitfsPromises.open(filepath,'w')).close();
19+
20+
// Date might round down timestamp
21+
functioncloseEnough(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+
asyncfunctionrunTest(atime,mtime,margin=0){
31+
margin+=Number.EPSILON;
32+
try{
33+
awaitfsPromises.utimes(filepath,newDate(atime),newDate(mtime));
34+
}catch(e){
35+
if(ignoredErrors.has(e.code))return;
36+
throwe;
37+
}
38+
39+
conststats=awaitfsPromises.stat(filepath);
40+
closeEnough(stats.atimeMs,atime,margin);
41+
closeEnough(stats.mtimeMs,mtime,margin);
42+
closeEnough(stats.atime.getTime(),newDate(atime).getTime(),margin);
43+
closeEnough(stats.mtime.getTime(),newDate(mtime).getTime(),margin);
44+
45+
conststatsBigint=awaitfsPromises.stat(filepath,{bigint: true});
46+
closeEnough(statsBigint.atimeMs,BigInt(atime),margin);
47+
closeEnough(statsBigint.mtimeMs,BigInt(mtime),margin);
48+
closeEnough(statsBigint.atime.getTime(),newDate(atime).getTime(),margin);
49+
closeEnough(statsBigint.mtime.getTime(),newDate(mtime).getTime(),margin);
50+
51+
conststatsSync=fs.statSync(filepath);
52+
closeEnough(statsSync.atimeMs,atime,margin);
53+
closeEnough(statsSync.mtimeMs,mtime,margin);
54+
closeEnough(statsSync.atime.getTime(),newDate(atime).getTime(),margin);
55+
closeEnough(statsSync.mtime.getTime(),newDate(mtime).getTime(),margin);
56+
57+
conststatsSyncBigint=fs.statSync(filepath,{bigint: true});
58+
closeEnough(statsSyncBigint.atimeMs,BigInt(atime),margin);
59+
closeEnough(statsSyncBigint.mtimeMs,BigInt(mtime),margin);
60+
closeEnough(statsSyncBigint.atime.getTime(),newDate(atime).getTime(),margin);
61+
closeEnough(statsSyncBigint.mtime.getTime(),newDate(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+
awaitrunTest(-40691,-355,1);// Potential precision loss on 32bit
71+
awaitrunTest(-355,-40691,1);// Potential precision loss on 32bit
72+
awaitrunTest(-1,-1);
73+
}
74+
awaitrunTest(0,0);
75+
awaitrunTest(1,1);
76+
awaitrunTest(355,40691,1);// Precision loss on 32bit
77+
awaitrunTest(40691,355,1);// Precision loss on 32bit
78+
awaitrunTest(1713037251360,1713037251360,1);// Precision loss
79+
}

0 commit comments

Comments
(0)