Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions src/lib/libsyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,21 @@ var SyscallsLibrary = {
var atime = stat.atimeMs ?? stat.atime.getTime();
var mtime = stat.mtimeMs ?? stat.mtime.getTime();
var ctime = stat.ctimeMs ?? stat.ctime.getTime();
{{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_sec, 'Math.floor(atime / 1000)', 'i64') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_nsec, 'Math.floor((atime % 1000) * 1_000_000)', SIZE_TYPE) }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_sec, 'Math.floor(mtime / 1000)', 'i64') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_nsec, 'Math.floor((mtime % 1000) * 1_000_000)', SIZE_TYPE) }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_sec, 'Math.floor(ctime / 1000)', 'i64') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_nsec, 'Math.floor((ctime % 1000) * 1_000_000)', SIZE_TYPE) }}};
// Round to the nearest 10 microseconds. This matches the granularity
// that __syscall_utimensat rounds down to when setting timestamps, so
// that values read via stat() round-trip through utimensat().

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if I should revert #27794 instead. Perhaps my attempt if fixing the rounding issues just made things worse?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for reverting #27794.

var atime10Us = Math.round(atime * 100);
var mtime10Us = Math.round(mtime * 100);
var ctime10Us = Math.round(ctime * 100);
var atimeSec = Math.floor(atime10Us / 100_000);
var mtimeSec = Math.floor(mtime10Us / 100_000);
var ctimeSec = Math.floor(ctime10Us / 100_000);
{{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_sec, 'atimeSec', 'i64') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_nsec, '(atime10Us - atimeSec * 100_000) * 10_000', SIZE_TYPE) }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_sec, 'mtimeSec', 'i64') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_nsec, '(mtime10Us - mtimeSec * 100_000) * 10_000', SIZE_TYPE) }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_sec, 'ctimeSec', 'i64') }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_nsec, '(ctime10Us - ctimeSec * 100_000) * 10_000', SIZE_TYPE) }}};
{{{ makeSetValue('buf', C_STRUCTS.stat.st_ino, 'stat.ino', 'i64') }}};
return 0;
},
Expand Down Expand Up @@ -1078,7 +1087,11 @@ var SyscallsLibrary = {
var nofollow = flags & {{{ cDefs.AT_SYMLINK_NOFOLLOW }}};
path = SYSCALLS.getStr(path);
path = SYSCALLS.calculateAt(dirfd, path, true);
var now = Date.now(), atime, mtime;
// Use a sub-millisecond clock for "now" if possible. This is needed to
// ensure that a later UTIME_NOW doesn't set the time earlier than an
// earlier UTIME_NOW, see test_utime_now.c
var now = globalThis.performance?.timeOrigin ? performance.timeOrigin + performance.now() : Date.now();
var atime, mtime;
if (!times) {
atime = now;
mtime = now;
Expand Down
21 changes: 15 additions & 6 deletions test/codesize/test_codesize_file_preload.expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -3047,12 +3047,21 @@ var SYSCALLS = {
var atime = stat.atimeMs ?? stat.atime.getTime();
var mtime = stat.mtimeMs ?? stat.mtime.getTime();
var ctime = stat.ctimeMs ?? stat.ctime.getTime();
HEAP64[(((buf) + (40)) >> 3)] = BigInt(Math.floor(atime / 1e3));
HEAPU32[(((buf) + (48)) >> 2)] = Math.floor((atime % 1e3) * 1e6);
HEAP64[(((buf) + (56)) >> 3)] = BigInt(Math.floor(mtime / 1e3));
HEAPU32[(((buf) + (64)) >> 2)] = Math.floor((mtime % 1e3) * 1e6);
HEAP64[(((buf) + (72)) >> 3)] = BigInt(Math.floor(ctime / 1e3));
HEAPU32[(((buf) + (80)) >> 2)] = Math.floor((ctime % 1e3) * 1e6);
// Round to the nearest 10 microseconds. This matches the granularity
// that __syscall_utimensat rounds down to when setting timestamps, so
// that values read via stat() round-trip through utimensat().
var atime10Us = Math.round(atime * 100);
var mtime10Us = Math.round(mtime * 100);
var ctime10Us = Math.round(ctime * 100);
var atimeSec = Math.floor(atime10Us / 1e5);
var mtimeSec = Math.floor(mtime10Us / 1e5);
var ctimeSec = Math.floor(ctime10Us / 1e5);
HEAP64[(((buf) + (40)) >> 3)] = BigInt(atimeSec);
HEAPU32[(((buf) + (48)) >> 2)] = (atime10Us - atimeSec * 1e5) * 1e4;
HEAP64[(((buf) + (56)) >> 3)] = BigInt(mtimeSec);
HEAPU32[(((buf) + (64)) >> 2)] = (mtime10Us - mtimeSec * 1e5) * 1e4;
HEAP64[(((buf) + (72)) >> 3)] = BigInt(ctimeSec);
HEAPU32[(((buf) + (80)) >> 2)] = (ctime10Us - ctimeSec * 1e5) * 1e4;
HEAP64[(((buf) + (88)) >> 3)] = BigInt(stat.ino);
return 0;
},
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_hello_dylink.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 26358,
"a.out.js.gz": 11227,
"a.out.js": 26400,
"a.out.js.gz": 11255,
"a.out.nodebug.wasm": 16986,
"a.out.nodebug.wasm.gz": 8676,
"total": 43344,
"total_gz": 19903,
"total": 43386,
"total_gz": 19931,
"sent": [
"__syscall_stat64",
"emscripten_resize_heap",
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 272813,
"a.out.js": 272931,
"a.out.nodebug.wasm": 587951,
"total": 860764,
"total": 860882,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down
8 changes: 8 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -5878,6 +5878,14 @@ def test_strftime_zZ(self):
def test_strptime_symmetry(self):
self.do_other_test('test_strptime_symmetry.c')

@also_with_nodefs_both
def test_utime_now(self):
self.do_runf('utime/test_utime_now.c', 'done\n')

@also_with_nodefs_both
def test_utime_roundtrip(self):
self.do_runf('utime/test_utime_roundtrip.c', 'done\n')

@also_with_wasmfs
def test_truncate_from_0(self):
self.do_other_test('test_truncate_from_0.cpp')
Expand Down
62 changes: 62 additions & 0 deletions test/utime/test_utime_now.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

// Creating a file and then touching it must never move the mtime backwards.
// Run many times in a loop to prevent flakiness.

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

static int cmp_timespec(const struct timespec* a, const struct timespec* b) {
if (a->tv_sec != b->tv_sec) {
return a->tv_sec < b->tv_sec ? -1 : 1;
}
if (a->tv_nsec != b->tv_nsec) {
return a->tv_nsec < b->tv_nsec ? -1 : 1;
}
return 0;
}

int main() {
const char* path = "touchfile";
for (int i = 0; i < 1000; i++) {
// Create the file. Filesystem assigns initial mtime.
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0666);
assert(fd >= 0);
close(fd);

// stat 1
struct stat before;
assert(stat(path, &before) == 0);

// touch
struct timespec now[2];
now[0].tv_nsec = UTIME_NOW;
now[1].tv_nsec = UTIME_NOW;
assert(utimensat(AT_FDCWD, path, now, 0) == 0);

// stat 2
struct stat after;
assert(stat(path, &after) == 0);

// assert stat 2 >= stat 1
if (cmp_timespec(&after.st_mtim, &before.st_mtim) < 0) {
printf("iteration %d: mtime went backwards: before=%lld.%09ld after=%lld.%09ld\n",
i, (long long)before.st_mtim.tv_sec, before.st_mtim.tv_nsec,
(long long)after.st_mtim.tv_sec, after.st_mtim.tv_nsec);
return 1;
}

assert(unlink(path) == 0);
}

puts("done");
return 0;
}
64 changes: 64 additions & 0 deletions test/utime/test_utime_roundtrip.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

// Reading a file's timestamps with stat() and writing them back with
// utimensat() must produce the same timestamps when read back again. Run in a
// loop to exercise different rounding cases.

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

#define ITERATIONS 2000

static int same_timespec(const struct timespec* a, const struct timespec* b) {
return a->tv_sec == b->tv_sec && a->tv_nsec == b->tv_nsec;
}

static void print_timespec(const char* label, const struct timespec* ts) {
printf("%s=%lld.%09ld", label, (long long)ts->tv_sec, ts->tv_nsec);
}

int main() {
const char* path = "roundtripfile";
for (int i = 0; i < ITERATIONS; i++) {
// Create the file. Filesystem assigns mtime.
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0666);
assert(fd >= 0);
close(fd);

struct stat before;
assert(stat(path, &before) == 0);

// Write the timestamps back exactly as we read them.
struct timespec times[2] = {before.st_atim, before.st_mtim};
assert(utimensat(AT_FDCWD, path, times, 0) == 0);


// Writing back same timestamp shouldn't have changed it.
struct stat after;
assert(stat(path, &after) == 0);

if (!same_timespec(&before.st_atim, &after.st_atim) ||
!same_timespec(&before.st_mtim, &after.st_mtim)) {
printf("iteration %d: timestamps did not round-trip: ", i);
print_timespec("atime before", &before.st_atim);
print_timespec(" after", &after.st_atim);
print_timespec(" mtime before", &before.st_mtim);
print_timespec(" after", &after.st_mtim);
printf("\n");
return 1;
}

assert(unlink(path) == 0);
}

puts("done");
return 0;
}
Loading