mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
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: https://github.com/nodejs/node/pull/43714 Fixes: https://github.com/nodejs/node/issues/43707 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const {
|
||||
NumberIsFinite,
|
||||
NumberIsInteger,
|
||||
MathMin,
|
||||
MathRound,
|
||||
ObjectIs,
|
||||
ObjectPrototypeHasOwnProperty,
|
||||
ObjectSetPrototypeOf,
|
||||
@@ -39,9 +40,9 @@ const {
|
||||
} = require('internal/errors');
|
||||
const {
|
||||
isArrayBufferView,
|
||||
isUint8Array,
|
||||
isBigInt64Array,
|
||||
isDate,
|
||||
isBigUint64Array
|
||||
isUint8Array,
|
||||
} = require('internal/util/types');
|
||||
const {
|
||||
kEmptyObject,
|
||||
@@ -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,
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
79
test/parallel/test-fs-stat-date.mjs
Normal file
79
test/parallel/test-fs-stat-date.mjs
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user