Skip to content

Commit

Permalink
fs: use signed types for stat data
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
LiviaMedeiros authored and danielleadams committed Jul 26, 2022
1 parent e89e0b4 commit 100f6de
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 14 deletions.
17 changes: 10 additions & 7 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
NumberIsFinite,
NumberIsInteger,
MathMin,
MathRound,
ObjectIs,
ObjectPrototypeHasOwnProperty,
ObjectSetPrototypeOf,
Expand Down Expand Up @@ -39,9 +40,9 @@ const {
} = require('internal/errors');
const {
isArrayBufferView,
isUint8Array,
isBigInt64Array,
isDate,
isBigUint64Array
isUint8Array,
} = require('internal/util/types');
const {
kEmptyObject,
Expand Down Expand Up @@ -454,14 +455,16 @@ function nsFromTimeSpecBigInt(sec, nsec) {
return sec * kNsPerSecBigInt + nsec;
}

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

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

/**
* @param {Float64Array | BigUint64Array} stats
* @param {Float64Array | BigInt64Array} stats
* @param {number} offset
* @returns {BigIntStats | Stats}
*/
function getStatsFromBinding(stats, offset = 0) {
if (isBigUint64Array(stats)) {
if (isBigInt64Array(stats)) {
return new BigIntStats(
stats[0 + offset], stats[1 + offset], stats[2 + offset],
stats[3 + offset], stats[4 + offset], stats[5 + offset],
Expand Down
2 changes: 1 addition & 1 deletion src/aliased_buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ typedef AliasedBufferBase<int32_t, v8::Int32Array> AliasedInt32Array;
typedef AliasedBufferBase<uint8_t, v8::Uint8Array> AliasedUint8Array;
typedef AliasedBufferBase<uint32_t, v8::Uint32Array> AliasedUint32Array;
typedef AliasedBufferBase<double, v8::Float64Array> AliasedFloat64Array;
typedef AliasedBufferBase<uint64_t, v8::BigUint64Array> AliasedBigUint64Array;
typedef AliasedBufferBase<int64_t, v8::BigInt64Array> AliasedBigInt64Array;
} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
Expand Down
19 changes: 14 additions & 5 deletions src/node_file-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,22 @@ template <typename NativeT, typename V8T>
void FillStatsArray(AliasedBufferBase<NativeT, V8T>* fields,
const uv_stat_t* s,
const size_t offset) {
#define SET_FIELD_WITH_STAT(stat_offset, stat) \
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
#define SET_FIELD_WITH_STAT(stat_offset, stat) \
fields->SetValue(offset + static_cast<size_t>(FsStatsOffset::stat_offset), \
static_cast<NativeT>(stat))

#define SET_FIELD_WITH_TIME_STAT(stat_offset, stat) \
/* NOLINTNEXTLINE(runtime/int) */ \
// 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 Expand Up @@ -233,7 +242,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo<v8::Value>& args,
Environment* env = binding_data->env();
if (value->StrictEquals(env->fs_use_promises_symbol())) {
if (use_bigint) {
return FSReqPromise<AliasedBigUint64Array>::New(binding_data, use_bigint);
return FSReqPromise<AliasedBigInt64Array>::New(binding_data, use_bigint);
} else {
return FSReqPromise<AliasedFloat64Array>::New(binding_data, use_bigint);
}
Expand Down
2 changes: 1 addition & 1 deletion src/node_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class BindingData : public SnapshotableObject {
explicit BindingData(Environment* env, v8::Local<v8::Object> wrap);

AliasedFloat64Array stats_field_array;
AliasedBigUint64Array stats_field_bigint_array;
AliasedBigInt64Array stats_field_bigint_array;

std::vector<BaseObjectPtr<FileHandleReadWrap>>
file_handle_read_wrap_freelist;
Expand Down
79 changes: 79 additions & 0 deletions test/parallel/test-fs-stat-date.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as common from '../common/index.mjs';

// Test timestamps returned by fsPromises.stat and fs.statSync

import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert';
import tmpdir from '../common/tmpdir.js';

// On some platforms (for example, ppc64) boundaries are tighter
// than usual. If we catch these errors, skip corresponding test.
const ignoredErrors = new Set(['EINVAL', 'EOVERFLOW']);

tmpdir.refresh();
const filepath = path.resolve(tmpdir.path, 'timestamp');

await (await fsPromises.open(filepath, 'w')).close();

// Date might round down timestamp
function closeEnough(actual, expected, margin) {
// On ppc64, value is rounded to seconds
if (process.arch === 'ppc64') {
margin += 1000;
}
assert.ok(Math.abs(Number(actual - expected)) < margin,
`expected ${expected} ± ${margin}, got ${actual}`);
}

async function runTest(atime, mtime, margin = 0) {
margin += Number.EPSILON;
try {
await fsPromises.utimes(filepath, new Date(atime), new Date(mtime));
} catch (e) {
if (ignoredErrors.has(e.code)) return;
throw e;
}

const stats = await fsPromises.stat(filepath);
closeEnough(stats.atimeMs, atime, margin);
closeEnough(stats.mtimeMs, mtime, margin);
closeEnough(stats.atime.getTime(), new Date(atime).getTime(), margin);
closeEnough(stats.mtime.getTime(), new Date(mtime).getTime(), margin);

const statsBigint = await fsPromises.stat(filepath, { bigint: true });
closeEnough(statsBigint.atimeMs, BigInt(atime), margin);
closeEnough(statsBigint.mtimeMs, BigInt(mtime), margin);
closeEnough(statsBigint.atime.getTime(), new Date(atime).getTime(), margin);
closeEnough(statsBigint.mtime.getTime(), new Date(mtime).getTime(), margin);

const statsSync = fs.statSync(filepath);
closeEnough(statsSync.atimeMs, atime, margin);
closeEnough(statsSync.mtimeMs, mtime, margin);
closeEnough(statsSync.atime.getTime(), new Date(atime).getTime(), margin);
closeEnough(statsSync.mtime.getTime(), new Date(mtime).getTime(), margin);

const statsSyncBigint = fs.statSync(filepath, { bigint: true });
closeEnough(statsSyncBigint.atimeMs, BigInt(atime), margin);
closeEnough(statsSyncBigint.mtimeMs, BigInt(mtime), margin);
closeEnough(statsSyncBigint.atime.getTime(), new Date(atime).getTime(), margin);
closeEnough(statsSyncBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
}

// Too high/low numbers produce too different results on different platforms
{
// TODO(LiviaMedeiros): investigate outdated stat time on FreeBSD.
// On Windows, filetime is stored and handled differently. Supporting dates
// after Y2038 is preferred over supporting dates before 1970-01-01.
if (!common.isFreeBSD && !common.isWindows) {
await runTest(-40691, -355, 1); // Potential precision loss on 32bit
await runTest(-355, -40691, 1); // Potential precision loss on 32bit
await runTest(-1, -1);
}
await runTest(0, 0);
await runTest(1, 1);
await runTest(355, 40691, 1); // Precision loss on 32bit
await runTest(40691, 355, 1); // Precision loss on 32bit
await runTest(1713037251360, 1713037251360, 1); // Precision loss
}

0 comments on commit 100f6de

Please sign in to comment.