-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
fs: use signed types for stat
data
#43714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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)) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a repro for that? I can understand why cast is needed for native JS numbers but not when it's transferred as I've tried to put numbers from #13255 in // both PR and v18.4.0:
+ 1713037251359.999
- 1713037251360
// only v18.4.0, PR passes successfully:
+ 1.8446744071562068e+22
- -2147483648000 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After reading those fully:
Overall it seems that precision loss is inevitable and accepted, and the solution is to simply use I've ran a dumb test to see precision regression between Of course if I'm missing something else, it can be reverted to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You were right, it did indeed bring back "overflow" from #13255 on win32. Any chance for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Breaking, I'm afraid. 1 Gah, I even pointed it out but this particular instance slipped through. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It leaves us with a choice on windows: we either support negative half of int32, or upper half of uint32. Y2038 is pretty close and we have an explicit test that it works: node/test/parallel/test-fs-utimes-y2K38.js Lines 45 to 48 in acd2a32
In current state of this PR, it fixes the issue for all platforms except windows, which is IMO the best solution for now. On a larger scale, if Node.js will be ready to migrate to newer ABI, I'd like to see more appropriate types. In JS part, I think it would be reasonable to drop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I completely forgot but I tried fixing pretty much the exact same issue in pretty much the exact same way in #32408... and looks like I ran into pretty much the exact same issues as you do now. 🤦 You can add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, I guess searching for previous issues could save quite a time here. 😅 |
||||||||||
#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); | ||||||||||
} | ||||||||||
|
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 | ||
} |
Uh oh!
There was an error while loading. Please reload this page.