GeezerVison is showing github.com — fs: use signed types for stat data · nodejs/node@100f6de
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/fs/utils.js +10-7Lines changed: 10 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,7 @@ const { | 12 | 12 | NumberIsFinite, | 13 | 13 | NumberIsInteger, | 14 | 14 | MathMin, | | 15 | + MathRound, | 15 | 16 | ObjectIs, | 16 | 17 | ObjectPrototypeHasOwnProperty, | 17 | 18 | ObjectSetPrototypeOf, | @@ -39,9 +40,9 @@ const { | 39 | 40 | } = require('internal/errors'); | 40 | 41 | const { | 41 | 42 | isArrayBufferView, | 42 | | - isUint8Array, | | 43 | + isBigInt64Array, | 43 | 44 | isDate, | 44 | | - isBigUint64Array | | 45 | + isUint8Array, | 45 | 46 | } = require('internal/util/types'); | 46 | 47 | const { | 47 | 48 | kEmptyObject, | @@ -454,14 +455,16 @@ function nsFromTimeSpecBigInt(sec, nsec) { | 454 | 455 | return sec * kNsPerSecBigInt + nsec; | 455 | 456 | } | 456 | 457 |
| 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 | 459 | 460 | // Since there may be a precision loss when the timestamp is | 460 | 461 | // converted to a floating point number, we manually round | 461 | 462 | // the timestamp here before passing it to Date(). | 462 | 463 | // Refs: https://github.com/nodejs/node/pull/12607 | | 464 | +// Refs: https://github.com/nodejs/node/pull/43714 | 463 | 465 | 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))); | 465 | 468 | } | 466 | 469 |
| 467 | 470 | function BigIntStats(dev, mode, nlink, uid, gid, rdev, blksize, | @@ -526,12 +529,12 @@ Stats.prototype._checkModeProperty = function(property) { | 526 | 529 | }; | 527 | 530 |
| 528 | 531 | /** | 529 | | - * @param {Float64Array | BigUint64Array} stats | | 532 | + * @param {Float64Array | BigInt64Array} stats | 530 | 533 | * @param {number} offset | 531 | 534 | * @returns {BigIntStats | Stats} | 532 | 535 | */ | 533 | 536 | function getStatsFromBinding(stats, offset = 0) { | 534 | | - if (isBigUint64Array(stats)) { | | 537 | + if (isBigInt64Array(stats)) { | 535 | 538 | return new BigIntStats( | 536 | 539 | stats[0 + offset], stats[1 + offset], stats[2 + offset], | 537 | 540 | stats[3 + offset], stats[4 + offset], stats[5 + offset], | |
Collapse file Copy file name to clipboardExpand all lines: src/aliased_buffer.h +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -307,7 +307,7 @@ typedef AliasedBufferBase<int32_t, v8::Int32Array> AliasedInt32Array; | 307 | 307 | typedef AliasedBufferBase<uint8_t, v8::Uint8Array> AliasedUint8Array; | 308 | 308 | typedef AliasedBufferBase<uint32_t, v8::Uint32Array> AliasedUint32Array; | 309 | 309 | typedef AliasedBufferBase<double, v8::Float64Array> AliasedFloat64Array; | 310 | | -typedef AliasedBufferBase<uint64_t, v8::BigUint64Array> AliasedBigUint64Array; | | 310 | +typedef AliasedBufferBase<int64_t, v8::BigInt64Array> AliasedBigInt64Array; | 311 | 311 | } // namespace node | 312 | 312 |
| 313 | 313 | #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS | |
Collapse file Copy file name to clipboardExpand all lines: src/node_file-inl.h +14-5Lines changed: 14 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -86,13 +86,22 @@ template <typename NativeT, typename V8T> | 86 | 86 | void FillStatsArray(AliasedBufferBase<NativeT, V8T>* fields, | 87 | 87 | const uv_stat_t* s, | 88 | 88 | 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), \ | 91 | 91 | static_cast<NativeT>(stat)) | 92 | 92 |
| 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) */ \ | 95 | 100 | 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 | 96 | 105 |
| 97 | 106 | SET_FIELD_WITH_STAT(kDev, s->st_dev); | 98 | 107 | SET_FIELD_WITH_STAT(kMode, s->st_mode); | @@ -233,7 +242,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo<v8::Value>& args, | 233 | 242 | Environment* env = binding_data->env(); | 234 | 243 | if (value->StrictEquals(env->fs_use_promises_symbol())) { | 235 | 244 | if (use_bigint) { | 236 | | - return FSReqPromise<AliasedBigUint64Array>::New(binding_data, use_bigint); | | 245 | + return FSReqPromise<AliasedBigInt64Array>::New(binding_data, use_bigint); | 237 | 246 | } else { | 238 | 247 | return FSReqPromise<AliasedFloat64Array>::New(binding_data, use_bigint); | 239 | 248 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/node_file.h +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -18,7 +18,7 @@ class BindingData : public SnapshotableObject { | 18 | 18 | explicit BindingData(Environment* env, v8::Local<v8::Object> wrap); | 19 | 19 |
| 20 | 20 | AliasedFloat64Array stats_field_array; | 21 | | - AliasedBigUint64Array stats_field_bigint_array; | | 21 | + AliasedBigInt64Array stats_field_bigint_array; | 22 | 22 |
| 23 | 23 | std::vector<BaseObjectPtr<FileHandleReadWrap>> | 24 | 24 | file_handle_read_wrap_freelist; | |
Collapse file Copy file name to clipboard +79Lines changed: 79 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
|