@scriptc/runtime 0.0.23 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/scr_lib.c CHANGED
@@ -49,13 +49,14 @@
49
49
  #include <ws2tcpip.h> /* inet_ntop, sockaddr_in6 */
50
50
  #include <iphlpapi.h> /* GetAdaptersAddresses (os.networkInterfaces) */
51
51
  #include <windows.h>
52
+ #include <winioctl.h> /* FSCTL_GET_REPARSE_POINT */
52
53
  #include <lmcons.h> /* UNLEN for GetUserNameA */
54
+ #include "scr_win_stats.h"
53
55
 
54
56
 
55
- /* CRT stat has no symlink view lstat degrades to stat (divergence: a
56
- * compiled binary never reports isSymbolicLink() true on Windows; Node
57
- * does for real symlinks/junctions). Mechanical fix: GetFileAttributesW +
58
- * FILE_ATTRIBUTE_REPARSE_POINT. */
57
+ /* The CRT has no symlink view, so its internal lstat users degrade to stat.
58
+ * The public Stats path below bypasses this seam and opens the final component
59
+ * as a reparse point for lstatSync, matching Node's no-follow split. */
59
60
  #define lstat stat
60
61
 
61
62
  /* Windows has no directory mode bits; the CRT mkdir takes one argument. */
@@ -76,6 +77,7 @@
76
77
  #include <ifaddrs.h>
77
78
  #include <net/if.h>
78
79
  #include <netinet/in.h>
80
+ #include <pthread.h>
79
81
  #include <pwd.h>
80
82
  #include <sys/ioctl.h>
81
83
  #include <sys/socket.h>
@@ -1111,6 +1113,23 @@ static double scr_filetime_us(FILETIME ft) {
1111
1113
  v.HighPart = ft.dwHighDateTime;
1112
1114
  return (double)(v.QuadPart / 10);
1113
1115
  }
1116
+
1117
+ /* A filesystem FILETIME is 100ns ticks since 1601. Match libuv's split into
1118
+ * Unix seconds + nanoseconds before doing Node's millisecond arithmetic; the
1119
+ * split matters for the last-bit rounding of dates far from the epoch. */
1120
+ static double scr_filetime_unix_ms(FILETIME ft) {
1121
+ ULARGE_INTEGER raw;
1122
+ raw.LowPart = ft.dwLowDateTime;
1123
+ raw.HighPart = ft.dwHighDateTime;
1124
+ int64_t ticks = (int64_t)raw.QuadPart - INT64_C(116444736000000000);
1125
+ int64_t sec = ticks / INT64_C(10000000);
1126
+ int64_t rem = ticks % INT64_C(10000000);
1127
+ if (rem < 0) {
1128
+ sec--;
1129
+ rem += INT64_C(10000000);
1130
+ }
1131
+ return (double)sec * 1000.0 + (double)rem / 10000.0;
1132
+ }
1114
1133
  double scr_cpu_user(void) {
1115
1134
  FILETIME c, e, k, u;
1116
1135
  if (!GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u)) return 0;
@@ -1380,11 +1399,23 @@ static const char *scr_errno_name(int e, char *fallback, size_t cap) {
1380
1399
  case ENOENT: return "ENOENT";
1381
1400
  case EEXIST: return "EEXIST";
1382
1401
  case EACCES: return "EACCES";
1402
+ case EBUSY: return "EBUSY";
1403
+ case EINVAL: return "EINVAL";
1404
+ case EIO: return "EIO";
1405
+ case ENAMETOOLONG: return "ENAMETOOLONG";
1406
+ case ENOMEM: return "ENOMEM";
1407
+ case ENOSPC: return "ENOSPC";
1383
1408
  case ENOTDIR: return "ENOTDIR";
1384
1409
  case EISDIR: return "EISDIR";
1385
1410
  case ENOTEMPTY: return "ENOTEMPTY";
1386
1411
  case EPERM: return "EPERM";
1412
+ case EROFS: return "EROFS";
1413
+ case EXDEV: return "EXDEV";
1387
1414
  case EBADF: return "EBADF";
1415
+ case EAGAIN: return "EAGAIN";
1416
+ case EFBIG: return "EFBIG";
1417
+ case EPIPE: return "EPIPE";
1418
+ case ESPIPE: return "ESPIPE";
1388
1419
  default:
1389
1420
  snprintf(fallback, cap, "E%d", e);
1390
1421
  return fallback;
@@ -1396,11 +1427,23 @@ static const char *scr_errno_text(int e) {
1396
1427
  case ENOENT: return "no such file or directory";
1397
1428
  case EEXIST: return "file already exists";
1398
1429
  case EACCES: return "permission denied";
1430
+ case EBUSY: return "resource busy or locked";
1431
+ case EINVAL: return "invalid argument";
1432
+ case EIO: return "i/o error";
1433
+ case ENAMETOOLONG: return "name too long";
1434
+ case ENOMEM: return "not enough memory";
1435
+ case ENOSPC: return "no space left on device";
1399
1436
  case ENOTDIR: return "not a directory";
1400
1437
  case EISDIR: return "illegal operation on a directory";
1401
1438
  case ENOTEMPTY: return "directory not empty";
1402
1439
  case EPERM: return "operation not permitted";
1440
+ case EROFS: return "read-only file system";
1441
+ case EXDEV: return "cross-device link not permitted";
1403
1442
  case EBADF: return "bad file descriptor";
1443
+ case EAGAIN: return "resource temporarily unavailable";
1444
+ case EFBIG: return "file too large";
1445
+ case EPIPE: return "broken pipe";
1446
+ case ESPIPE: return "invalid seek";
1404
1447
  default: return strerror(e);
1405
1448
  }
1406
1449
  }
@@ -1542,6 +1585,22 @@ void scr_fs_write_file(ScrStr *path, ScrStr *data) {
1542
1585
  * existing file keeps its permissions, exactly Node (which never chmods
1543
1586
  * here). Same error shapes as the plain form. */
1544
1587
  void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
1588
+ char recv[48], msg[176];
1589
+ scr_num_received(mode, recv);
1590
+ if (!(isfinite(mode) && trunc(mode) == mode)) {
1591
+ int len = snprintf(msg, sizeof msg,
1592
+ "The value of \"mode\" is out of range. It must be an integer. Received %s",
1593
+ recv);
1594
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1595
+ return;
1596
+ }
1597
+ if (mode < 0 || mode > 4294967295.0) {
1598
+ int len = snprintf(msg, sizeof msg,
1599
+ "The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
1600
+ recv);
1601
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1602
+ return;
1603
+ }
1545
1604
  /* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
1546
1605
  * \n in these byte-exact writes (fopen's "wb" path already does). */
1547
1606
  int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, (mode_t)mode);
@@ -1709,6 +1768,250 @@ static ssize_t scr_fs_pread(int fd, void *data, size_t length, double position)
1709
1768
  #endif
1710
1769
  }
1711
1770
 
1771
+ /* Offset-preserving write for fs.writeSync's numeric-position forms. The
1772
+ * Windows arm mirrors scr_fs_pread: WriteFile receives an OVERLAPPED offset
1773
+ * and the CRT descriptor's current position is restored before returning. */
1774
+ static ssize_t scr_fs_pwrite(int fd, const void *data, size_t length, double position) {
1775
+ #ifdef _WIN32
1776
+ HANDLE handle = (HANDLE)_get_osfhandle(fd);
1777
+ if (handle == INVALID_HANDLE_VALUE) {
1778
+ errno = EBADF;
1779
+ return -1;
1780
+ }
1781
+
1782
+ OVERLAPPED overlapped;
1783
+ memset(&overlapped, 0, sizeof overlapped);
1784
+ LARGE_INTEGER at;
1785
+ at.QuadPart = (LONGLONG)position;
1786
+ overlapped.Offset = at.LowPart;
1787
+ overlapped.OffsetHigh = at.HighPart;
1788
+
1789
+ LARGE_INTEGER zero;
1790
+ LARGE_INTEGER original;
1791
+ zero.QuadPart = 0;
1792
+ BOOL restore = SetFilePointerEx(handle, zero, &original, FILE_CURRENT);
1793
+ DWORD wrote = 0;
1794
+ DWORD want = length > (size_t)UINT32_MAX ? UINT32_MAX : (DWORD)length;
1795
+ BOOL ok = WriteFile(handle, data, want, &wrote, &overlapped);
1796
+ DWORD error = ok ? ERROR_SUCCESS : GetLastError();
1797
+ if (restore) (void)SetFilePointerEx(handle, original, NULL, FILE_BEGIN);
1798
+ if (ok || wrote > 0) return (ssize_t)wrote;
1799
+
1800
+ switch (error) {
1801
+ case ERROR_INVALID_HANDLE:
1802
+ case ERROR_ACCESS_DENIED:
1803
+ errno = EBADF;
1804
+ break;
1805
+ case ERROR_INVALID_FUNCTION:
1806
+ case ERROR_INVALID_PARAMETER:
1807
+ errno = EINVAL;
1808
+ break;
1809
+ case ERROR_DISK_FULL:
1810
+ case ERROR_HANDLE_DISK_FULL:
1811
+ errno = ENOSPC;
1812
+ break;
1813
+ case ERROR_FILE_TOO_LARGE:
1814
+ errno = EFBIG;
1815
+ break;
1816
+ case ERROR_BROKEN_PIPE:
1817
+ case ERROR_NO_DATA:
1818
+ errno = EPIPE;
1819
+ break;
1820
+ case ERROR_NOT_ENOUGH_MEMORY:
1821
+ case ERROR_OUTOFMEMORY:
1822
+ errno = ENOMEM;
1823
+ break;
1824
+ case ERROR_OPERATION_ABORTED:
1825
+ errno = EINTR;
1826
+ break;
1827
+ default:
1828
+ errno = EIO;
1829
+ break;
1830
+ }
1831
+ return -1;
1832
+ #else
1833
+ return pwrite(fd, data, length, (off_t)position);
1834
+ #endif
1835
+ }
1836
+
1837
+ static bool scr_fs_write_fd_valid(double fd) {
1838
+ char msg[160];
1839
+ char recv[48];
1840
+ scr_num_received(fd, recv);
1841
+ if (!(isfinite(fd) && trunc(fd) == fd)) {
1842
+ int len = snprintf(msg, sizeof msg,
1843
+ "The value of \"fd\" is out of range. It must be an integer. Received %s",
1844
+ recv);
1845
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1846
+ return false;
1847
+ }
1848
+ if (fd < 0 || fd > 2147483647.0) {
1849
+ int len = snprintf(msg, sizeof msg,
1850
+ "The value of \"fd\" is out of range. It must be >= 0 && <= 2147483647. Received %s",
1851
+ recv);
1852
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1853
+ return false;
1854
+ }
1855
+ return true;
1856
+ }
1857
+
1858
+ /* write(2) raises SIGPIPE before returning EPIPE when a pipe/socket peer has
1859
+ * closed; write(2)/pwrite(2) likewise raise SIGXFSZ before returning EFBIG at
1860
+ * RLIMIT_FSIZE. Node turns both into catchable fs errors. Do the same per
1861
+ * calling thread rather than changing process dispositions (which spawned
1862
+ * children would inherit). Consume only a signal freshly generated by this
1863
+ * call, preserving any signal that was already pending. */
1864
+ static ssize_t scr_fs_write_once(int fd, const void *data, size_t length,
1865
+ bool positioned, double position) {
1866
+ #ifdef _WIN32
1867
+ return positioned
1868
+ ? scr_fs_pwrite(fd, data, length, position)
1869
+ : write(fd, data, length);
1870
+ #else
1871
+ sigset_t write_set, old_set, pending;
1872
+ sigemptyset(&write_set);
1873
+ #ifdef SIGPIPE
1874
+ sigaddset(&write_set, SIGPIPE);
1875
+ #endif
1876
+ #ifdef SIGXFSZ
1877
+ sigaddset(&write_set, SIGXFSZ);
1878
+ #endif
1879
+ if (pthread_sigmask(SIG_BLOCK, &write_set, &old_set) != 0) {
1880
+ return positioned
1881
+ ? scr_fs_pwrite(fd, data, length, position)
1882
+ : write(fd, data, length);
1883
+ }
1884
+
1885
+ bool pending_known = sigpending(&pending) == 0;
1886
+ #ifdef SIGPIPE
1887
+ bool had_pipe = pending_known && sigismember(&pending, SIGPIPE) == 1;
1888
+ #endif
1889
+ #ifdef SIGXFSZ
1890
+ bool had_xfsz = pending_known && sigismember(&pending, SIGXFSZ) == 1;
1891
+ #endif
1892
+ ssize_t n = positioned
1893
+ ? scr_fs_pwrite(fd, data, length, position)
1894
+ : write(fd, data, length);
1895
+ int write_errno = errno;
1896
+
1897
+ int generated = 0;
1898
+ if (n < 0 && pending_known && sigpending(&pending) == 0) {
1899
+ #ifdef SIGPIPE
1900
+ if (write_errno == EPIPE && !had_pipe && sigismember(&pending, SIGPIPE) == 1) {
1901
+ generated = SIGPIPE;
1902
+ }
1903
+ #endif
1904
+ #ifdef SIGXFSZ
1905
+ if (write_errno == EFBIG && !had_xfsz && sigismember(&pending, SIGXFSZ) == 1) {
1906
+ generated = SIGXFSZ;
1907
+ }
1908
+ #endif
1909
+ }
1910
+ if (generated != 0) {
1911
+ sigset_t generated_set;
1912
+ sigemptyset(&generated_set);
1913
+ sigaddset(&generated_set, generated);
1914
+ int caught = 0;
1915
+ int wait_err;
1916
+ do {
1917
+ wait_err = sigwait(&generated_set, &caught);
1918
+ } while (wait_err == EINTR);
1919
+ }
1920
+
1921
+ (void)pthread_sigmask(SIG_SETMASK, &old_set, NULL);
1922
+ errno = write_errno;
1923
+ return n;
1924
+ #endif
1925
+ }
1926
+
1927
+ static double scr_fs_write_bytes(double fd, const void *data, size_t length,
1928
+ double position) {
1929
+ if (!scr_fs_write_fd_valid(fd)) return 0;
1930
+ /* Node/libuv treats every position other than a safe nonnegative integer
1931
+ * as the current-offset sentinel for writes (unlike readSync, it does not
1932
+ * throw for negative/fractional positions). */
1933
+ bool positioned = isfinite(position) && trunc(position) == position &&
1934
+ position >= 0 && position <= 9007199254740991.0;
1935
+ ssize_t n;
1936
+ do {
1937
+ n = scr_fs_write_once((int)fd, data, length, positioned, position);
1938
+ } while (n < 0 && errno == EINTR);
1939
+ if (n < 0) {
1940
+ int e = errno;
1941
+ char namebuf[16];
1942
+ const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
1943
+ const char *text = scr_errno_text(e);
1944
+ char msg[160];
1945
+ int len = snprintf(msg, sizeof msg, "%s: %s, write", name, text);
1946
+ scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name);
1947
+ return 0;
1948
+ }
1949
+ return (double)n;
1950
+ }
1951
+
1952
+ /* fs.writeSync(fd, buffer, offset, length[, position]) — validates the
1953
+ * caller window before touching the descriptor, then submits one write like
1954
+ * Node/libuv. Positioned writes do not advance the descriptor. */
1955
+ double scr_fs_write_sync(double fd, ScrBytes *buf, double offset, double length,
1956
+ double position) {
1957
+ size_t bytelen = buf->len; /* frontend admits u8 buffers only */
1958
+ char msg[160];
1959
+ char recv[48];
1960
+ scr_num_received(offset, recv);
1961
+ if (!(isfinite(offset) && trunc(offset) == offset)) {
1962
+ int len = snprintf(msg, sizeof msg,
1963
+ "The value of \"offset\" is out of range. It must be an integer. Received %s",
1964
+ recv);
1965
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1966
+ return 0;
1967
+ }
1968
+ if (offset < 0 || offset > 9007199254740991.0) {
1969
+ int len = snprintf(msg, sizeof msg,
1970
+ "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
1971
+ recv);
1972
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1973
+ return 0;
1974
+ }
1975
+ if (offset > (double)bytelen) {
1976
+ int len = snprintf(msg, sizeof msg,
1977
+ "The value of \"offset\" is out of range. It must be <= %zu. Received %s",
1978
+ bytelen, recv);
1979
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1980
+ return 0;
1981
+ }
1982
+
1983
+ scr_num_received(length, recv);
1984
+ if (length < 0) {
1985
+ int len = snprintf(msg, sizeof msg,
1986
+ "The value of \"length\" is out of range. It must be >= 0. Received %s",
1987
+ recv);
1988
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1989
+ return 0;
1990
+ }
1991
+ size_t off = (size_t)offset;
1992
+ if (length > (double)(bytelen - off)) {
1993
+ int len = snprintf(msg, sizeof msg,
1994
+ "The value of \"length\" is out of range. It must be <= %zu. Received %s",
1995
+ bytelen - off, recv);
1996
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1997
+ return 0;
1998
+ }
1999
+ if (!(isfinite(length) && trunc(length) == length)) {
2000
+ int len = snprintf(msg, sizeof msg,
2001
+ "The value of \"length\" is out of range. It must be an integer. Received %s",
2002
+ recv);
2003
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
2004
+ return 0;
2005
+ }
2006
+ return scr_fs_write_bytes(fd, buf->data + off, (size_t)length, position);
2007
+ }
2008
+
2009
+ double scr_fs_write_str_sync(double fd, ScrStr *data, double position,
2010
+ ScrStr *encoding) {
2011
+ (void)encoding; /* frontend proves utf8; the argument still evaluates */
2012
+ return scr_fs_write_bytes(fd, data->data, data->len, position);
2013
+ }
2014
+
1712
2015
  /* fs.readSync(fd, buffer, offset, length[, position]) — the buffer form.
1713
2016
  * Node validates offset/length against the buffer before reading and
1714
2017
  * throws ERR_OUT_OF_RANGE; here the checks clamp to the same contract and
@@ -1884,11 +2187,110 @@ void scr_fs_copyfile(ScrStr *src, ScrStr *dest) {
1884
2187
  }
1885
2188
 
1886
2189
  /* renameSync(old, new): rename(2), Node's two-path error shape ("ENOENT:
1887
- * no such file or directory, rename 'a' -> 'b'"). */
1888
- void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath) {
1889
- if (rename(oldpath->data, newpath->data) != 0) {
1890
- scr_fs_throw2(errno, "rename", oldpath, newpath);
2190
+ * no such file or directory, rename 'a' -> 'b'"). Windows must bypass
2191
+ * the CRT's rename(): it refuses an existing destination and cannot move
2192
+ * directories between parents, while Node/libuv uses MoveFileExW with
2193
+ * MOVEFILE_REPLACE_EXISTING. Runtime strings are UTF-8, so feed the wide
2194
+ * Win32 API rather than the active-code-page `A` form. */
2195
+ #ifdef _WIN32
2196
+ static WCHAR *scr_fs_win_wide(const ScrStr *path) {
2197
+ if (path->len > INT_MAX) {
2198
+ SetLastError(ERROR_FILENAME_EXCED_RANGE);
2199
+ return NULL;
1891
2200
  }
2201
+ int n = path->len > 0
2202
+ ? MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
2203
+ path->data, (int)path->len, NULL, 0)
2204
+ : 0;
2205
+ if (path->len > 0 && n == 0) return NULL;
2206
+ WCHAR *wide = malloc(((size_t)n + 1) * sizeof *wide);
2207
+ if (!wide) {
2208
+ SetLastError(ERROR_NOT_ENOUGH_MEMORY);
2209
+ return NULL;
2210
+ }
2211
+ if (n > 0) {
2212
+ (void)MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
2213
+ path->data, (int)path->len, wide, n);
2214
+ }
2215
+ wide[n] = L'\0';
2216
+ return wide;
2217
+ }
2218
+
2219
+ /* The reachable fs subset of libuv's uv_translate_sys_error table. */
2220
+ static int scr_fs_win_errno(DWORD error) {
2221
+ switch (error) {
2222
+ case ERROR_ALREADY_EXISTS:
2223
+ case ERROR_FILE_EXISTS:
2224
+ return EEXIST;
2225
+ case ERROR_LOCK_VIOLATION:
2226
+ case ERROR_PIPE_BUSY:
2227
+ case ERROR_SHARING_VIOLATION:
2228
+ return EBUSY;
2229
+ case ERROR_INVALID_FUNCTION:
2230
+ return EISDIR;
2231
+ case ERROR_INSUFFICIENT_BUFFER:
2232
+ case ERROR_INVALID_DATA:
2233
+ case ERROR_INVALID_PARAMETER:
2234
+ return EINVAL;
2235
+ case ERROR_BUFFER_OVERFLOW:
2236
+ case ERROR_FILENAME_EXCED_RANGE:
2237
+ return ENAMETOOLONG;
2238
+ case ERROR_NOT_ENOUGH_MEMORY:
2239
+ case ERROR_OUTOFMEMORY:
2240
+ return ENOMEM;
2241
+ case ERROR_CANNOT_MAKE:
2242
+ case ERROR_DISK_FULL:
2243
+ case ERROR_HANDLE_DISK_FULL:
2244
+ return ENOSPC;
2245
+ case ERROR_DIR_NOT_EMPTY:
2246
+ return ENOTEMPTY;
2247
+ case ERROR_ACCESS_DENIED:
2248
+ case ERROR_PRIVILEGE_NOT_HELD:
2249
+ return EPERM;
2250
+ case ERROR_WRITE_PROTECT:
2251
+ return EROFS;
2252
+ case ERROR_NOT_SAME_DEVICE:
2253
+ return EXDEV;
2254
+ case ERROR_BAD_PATHNAME:
2255
+ case ERROR_DIRECTORY:
2256
+ case ERROR_FILE_NOT_FOUND:
2257
+ case ERROR_INVALID_DRIVE:
2258
+ case ERROR_INVALID_NAME:
2259
+ case ERROR_PATH_NOT_FOUND:
2260
+ return ENOENT;
2261
+ default:
2262
+ return EIO;
2263
+ }
2264
+ }
2265
+ #endif
2266
+
2267
+ int scr_fs_rename_raw(const ScrStr *oldpath, const ScrStr *newpath) {
2268
+ #ifdef _WIN32
2269
+ WCHAR *oldwide = scr_fs_win_wide(oldpath);
2270
+ if (!oldwide) return scr_fs_win_errno(GetLastError());
2271
+ WCHAR *newwide = scr_fs_win_wide(newpath);
2272
+ if (!newwide) {
2273
+ DWORD error = GetLastError();
2274
+ free(oldwide);
2275
+ return scr_fs_win_errno(error);
2276
+ }
2277
+ BOOL ok = MoveFileExW(oldwide, newwide, MOVEFILE_REPLACE_EXISTING);
2278
+ DWORD error = ok ? ERROR_SUCCESS : GetLastError();
2279
+ free(oldwide);
2280
+ free(newwide);
2281
+ return ok ? 0 : scr_fs_win_errno(error);
2282
+ #else
2283
+ return rename(oldpath->data, newpath->data) == 0 ? 0 : errno;
2284
+ #endif
2285
+ }
2286
+
2287
+ void scr_fs_rename_error(int error, const ScrStr *oldpath, const ScrStr *newpath) {
2288
+ scr_fs_throw2(error, "rename", oldpath, newpath);
2289
+ }
2290
+
2291
+ void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath) {
2292
+ int error = scr_fs_rename_raw(oldpath, newpath);
2293
+ if (error != 0) scr_fs_rename_error(error, oldpath, newpath);
1892
2294
  }
1893
2295
 
1894
2296
  void scr_fs_rm(ScrStr *path) {
@@ -2176,7 +2578,7 @@ static void scr_fs_throw_nopath(int e, const char *op) {
2176
2578
  const char *text = scr_errno_text(e);
2177
2579
  char msg[256];
2178
2580
  int len = snprintf(msg, sizeof msg, "%s: %s, %s", name, text, op);
2179
- scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)len);
2581
+ scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name);
2180
2582
  }
2181
2583
 
2182
2584
  /* read(2) loop to EOF from the CURRENT position — Node's
@@ -2367,8 +2769,8 @@ void scr_process_stdin_destroy(void) {
2367
2769
  }
2368
2770
 
2369
2771
  /* ── Stats values ────────────────────────────────────────────────────
2370
- * An immutable snapshot of stat(2) results — the slice the lowered
2371
- * surface exposes (isFile/isDirectory/size). statSync THROWS like the
2772
+ * An immutable snapshot of stat(2) results — the lowered type/mode, size,
2773
+ * allocation/link, and access/write-time slice. statSync THROWS like the
2372
2774
  * other sync fs calls; the promise form rejects (see the fsp section). */
2373
2775
 
2374
2776
  struct ScrStats {
@@ -2377,6 +2779,9 @@ struct ScrStats {
2377
2779
  bool is_dir;
2378
2780
  bool is_symlink; /* lstat only — a followed stat never sees one */
2379
2781
  double size;
2782
+ double blocks; /* allocated size in 512-byte units (Node/libuv) */
2783
+ double nlink;
2784
+ double atime_ms;
2380
2785
  double mtime_ms; /* milliseconds with the nanosecond fraction (Node) */
2381
2786
  };
2382
2787
 
@@ -2397,53 +2802,310 @@ bool scr_stats_is_file(ScrStats *s) { return s->is_file; }
2397
2802
  bool scr_stats_is_dir(ScrStats *s) { return s->is_dir; }
2398
2803
  bool scr_stats_is_symlink(ScrStats *s) { return s->is_symlink; }
2399
2804
  double scr_stats_size(ScrStats *s) { return s->size; }
2805
+ double scr_stats_blocks(ScrStats *s) { return s->blocks; }
2806
+ double scr_stats_nlink(ScrStats *s) { return s->nlink; }
2807
+ double scr_stats_atime_ms(ScrStats *s) { return s->atime_ms; }
2400
2808
  double scr_stats_mtime_ms(ScrStats *s) { return s->mtime_ms; }
2401
2809
 
2402
- static ScrStats *scr_stats_of(const struct stat *st) {
2810
+ static ScrStats *scr_stats_new(void) {
2403
2811
  ScrStats *s = malloc(sizeof(ScrStats));
2404
2812
  if (!s) {
2405
2813
  scr_trap("scriptc: out of memory\n");
2406
2814
  }
2407
2815
  s->rc = 1;
2816
+ return s;
2817
+ }
2818
+
2819
+ #ifdef _WIN32
2820
+ /* The CRT fallback is deliberately complete: callers either get every field
2821
+ * from this one stat() result or every field from one Win32 handle below,
2822
+ * never a snapshot spliced across two path resolutions. */
2823
+ static ScrStats *scr_stats_of_crt(const struct stat *st) {
2824
+ ScrStats *s = scr_stats_new();
2408
2825
  s->is_file = S_ISREG(st->st_mode);
2409
2826
  s->is_dir = S_ISDIR(st->st_mode);
2410
- #if defined(_WIN32)
2411
- /* CRT stat has no symlink view (lstat above degrades to stat) and no
2412
- * sub-second mtime — whole seconds where Node reads the FILETIME's
2413
- * 100ns units (divergence: mtimeMs precision; mechanical fix is
2414
- * GetFileAttributesEx). */
2415
2827
  s->is_symlink = false;
2828
+ s->size = (double)st->st_size;
2829
+ s->blocks = st->st_size <= 0 ? 0.0 : (double)(((uint64_t)st->st_size + 511) >> 9);
2830
+ s->nlink = (double)st->st_nlink;
2831
+ s->atime_ms = (double)st->st_atime * 1000.0;
2416
2832
  s->mtime_ms = (double)st->st_mtime * 1000.0;
2833
+ return s;
2834
+ }
2835
+
2836
+ static ScrStats *scr_stats_crt_fallback(const ScrStr *path, const char *op) {
2837
+ struct stat st;
2838
+ if (stat(path->data, &st) != 0) {
2839
+ scr_fs_throw(errno, op, path);
2840
+ return NULL;
2841
+ }
2842
+ return scr_stats_of_crt(&st);
2843
+ }
2844
+
2845
+ /* Resolve ordinary disk paths once and populate every public field from the
2846
+ * resulting handle. Splitting size/type across CRT stat() and a later
2847
+ * CreateFile call can mix two entries when another process replaces path. */
2848
+ static bool scr_stats_is_link_tag(DWORD tag) {
2849
+ return tag == IO_REPARSE_TAG_SYMLINK ||
2850
+ tag == IO_REPARSE_TAG_MOUNT_POINT ||
2851
+ tag == IO_REPARSE_TAG_APPEXECLINK ||
2852
+ tag == UINT32_C(0xA000001D); /* IO_REPARSE_TAG_LX_SYMLINK */
2853
+ }
2854
+
2855
+ static double scr_stats_utf16_len(const WCHAR *text, size_t len) {
2856
+ if (len == 0) return 0;
2857
+ if (len > INT_MAX) return -1;
2858
+ int bytes = WideCharToMultiByte(
2859
+ CP_UTF8, 0, text, (int)len, NULL, 0, NULL, NULL);
2860
+ return bytes > 0 ? (double)bytes : -1;
2861
+ }
2862
+
2863
+ typedef struct {
2864
+ ULONG tag;
2865
+ USHORT data_len;
2866
+ USHORT reserved;
2867
+ union {
2868
+ struct {
2869
+ USHORT substitute_offset;
2870
+ USHORT substitute_len;
2871
+ USHORT print_offset;
2872
+ USHORT print_len;
2873
+ ULONG flags;
2874
+ WCHAR path[1];
2875
+ } symlink;
2876
+ struct {
2877
+ USHORT substitute_offset;
2878
+ USHORT substitute_len;
2879
+ USHORT print_offset;
2880
+ USHORT print_len;
2881
+ WCHAR path[1];
2882
+ } mount;
2883
+ struct {
2884
+ unsigned char bytes[1];
2885
+ } generic;
2886
+ } body;
2887
+ } ScrStatsReparseData;
2888
+
2889
+ /* Node/libuv reports a Windows link's target-text length as st_size. The
2890
+ * ordinary handle information does not carry that value, so read the reparse
2891
+ * payload while the no-follow handle is live. */
2892
+ static bool scr_stats_link_size(HANDLE h, DWORD tag, double *size_out) {
2893
+ union {
2894
+ ScrStatsReparseData align;
2895
+ unsigned char bytes[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
2896
+ } storage;
2897
+ DWORD used;
2898
+ if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, NULL, 0,
2899
+ storage.bytes, sizeof storage.bytes, &used, NULL)) return false;
2900
+ ScrStatsReparseData *data = (ScrStatsReparseData *)storage.bytes;
2901
+ const size_t body_offset = offsetof(ScrStatsReparseData, body);
2902
+ if (used < body_offset || data->data_len > used - body_offset ||
2903
+ data->tag != tag) return false;
2904
+
2905
+ const WCHAR *target;
2906
+ size_t len;
2907
+ if (tag == IO_REPARSE_TAG_SYMLINK) {
2908
+ const size_t fixed = offsetof(ScrStatsReparseData, body.symlink.path) -
2909
+ body_offset;
2910
+ size_t offset = data->body.symlink.substitute_offset;
2911
+ size_t bytes = data->body.symlink.substitute_len;
2912
+ if (data->data_len < fixed || ((offset | bytes) & 1) != 0 ||
2913
+ offset > data->data_len - fixed ||
2914
+ bytes > data->data_len - fixed - offset) return false;
2915
+ target = (const WCHAR *)((const unsigned char *)data->body.symlink.path +
2916
+ offset);
2917
+ len = bytes / sizeof(WCHAR);
2918
+ } else if (tag == IO_REPARSE_TAG_MOUNT_POINT) {
2919
+ const size_t fixed = offsetof(ScrStatsReparseData, body.mount.path) -
2920
+ body_offset;
2921
+ size_t offset = data->body.mount.substitute_offset;
2922
+ size_t bytes = data->body.mount.substitute_len;
2923
+ if (data->data_len < fixed || ((offset | bytes) & 1) != 0 ||
2924
+ offset > data->data_len - fixed ||
2925
+ bytes > data->data_len - fixed - offset) return false;
2926
+ target = (const WCHAR *)((const unsigned char *)data->body.mount.path +
2927
+ offset);
2928
+ len = bytes / sizeof(WCHAR);
2929
+ _Static_assert(sizeof(WCHAR) == sizeof(uint16_t),
2930
+ "Windows reparse payloads use UTF-16 code units");
2931
+ if (!scr_win_stats_mount_target_is_junction(
2932
+ (const uint16_t *)target, len)) return false;
2933
+ } else if (tag == UINT32_C(0xA000001D)) {
2934
+ /* WSL's LX symlink payload is a version word followed by UTF-8 bytes. */
2935
+ if (data->data_len < sizeof(ULONG)) return false;
2936
+ *size_out = (double)(data->data_len - sizeof(ULONG));
2937
+ return true;
2938
+ } else {
2939
+ /* App execution links carry a counted UTF-16 string list; the third
2940
+ * string is the target path. */
2941
+ if (tag != IO_REPARSE_TAG_APPEXECLINK ||
2942
+ data->data_len < sizeof(ULONG)) return false;
2943
+ const unsigned char *raw = data->body.generic.bytes;
2944
+ ULONG count;
2945
+ memcpy(&count, raw, sizeof count);
2946
+ if (count < 3 || ((data->data_len - sizeof count) & 1) != 0) return false;
2947
+ target = (const WCHAR *)(raw + sizeof count);
2948
+ size_t chars = (data->data_len - sizeof count) / sizeof(WCHAR);
2949
+ for (size_t item = 0; item < 2; item++) {
2950
+ size_t part = 0;
2951
+ while (part < chars && target[part] != L'\0') part++;
2952
+ if (part == 0 || part == chars) return false;
2953
+ target += part + 1;
2954
+ chars -= part + 1;
2955
+ }
2956
+ len = 0;
2957
+ while (len < chars && target[len] != L'\0') len++;
2958
+ if (len == 0 || len == chars || len < 3 ||
2959
+ !((target[0] >= L'A' && target[0] <= L'Z') ||
2960
+ (target[0] >= L'a' && target[0] <= L'z')) ||
2961
+ target[1] != L':' || target[2] != L'\\') return false;
2962
+ double size = scr_stats_utf16_len(target, len);
2963
+ if (size < 0) return false;
2964
+ *size_out = size;
2965
+ return true;
2966
+ }
2967
+
2968
+ /* Undo the NT namespace prefix CreateSymbolicLinkW stores for absolute
2969
+ * DOS/UNC targets, matching libuv's readlink normalization. */
2970
+ if (len >= 4 && target[0] == L'\\' && target[1] == L'?' &&
2971
+ target[2] == L'?' && target[3] == L'\\') {
2972
+ if (len >= 6 && target[5] == L':' &&
2973
+ ((target[4] >= L'A' && target[4] <= L'Z') ||
2974
+ (target[4] >= L'a' && target[4] <= L'z')) &&
2975
+ (len == 6 || target[6] == L'\\')) {
2976
+ target += 4;
2977
+ len -= 4;
2978
+ } else if (len >= 8 &&
2979
+ (target[4] == L'U' || target[4] == L'u') &&
2980
+ (target[5] == L'N' || target[5] == L'n') &&
2981
+ (target[6] == L'C' || target[6] == L'c') &&
2982
+ target[7] == L'\\') {
2983
+ target += 6;
2984
+ len -= 6;
2985
+ }
2986
+ }
2987
+ double size = scr_stats_utf16_len(target, len);
2988
+ if (size < 0) return false;
2989
+ *size_out = size;
2990
+ return true;
2991
+ }
2992
+
2993
+ static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op,
2994
+ bool no_follow) {
2995
+ WCHAR *wide = scr_fs_win_wide(path);
2996
+ if (!wide) return scr_stats_crt_fallback(path, op);
2997
+
2998
+ DWORD flags = FILE_FLAG_BACKUP_SEMANTICS;
2999
+ if (no_follow) flags |= FILE_FLAG_OPEN_REPARSE_POINT;
3000
+ HANDLE h = CreateFileW(wide, FILE_READ_ATTRIBUTES,
3001
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
3002
+ NULL, OPEN_EXISTING, flags, NULL);
3003
+ free(wide);
3004
+ if (h == INVALID_HANDLE_VALUE) return scr_stats_crt_fallback(path, op);
3005
+
3006
+ DWORD file_type = GetFileType(h);
3007
+ if (file_type != FILE_TYPE_DISK) {
3008
+ CloseHandle(h);
3009
+ return scr_stats_crt_fallback(path, op);
3010
+ }
3011
+ BY_HANDLE_FILE_INFORMATION basic;
3012
+ if (!GetFileInformationByHandle(h, &basic)) {
3013
+ CloseHandle(h);
3014
+ return scr_stats_crt_fallback(path, op);
3015
+ }
3016
+ bool is_link = false;
3017
+ double link_size = -1;
3018
+ if (no_follow && (basic.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
3019
+ FILE_ATTRIBUTE_TAG_INFO tagged;
3020
+ if (!GetFileInformationByHandleEx(
3021
+ h, FileAttributeTagInfo, &tagged, sizeof tagged) ||
3022
+ !scr_stats_is_link_tag(tagged.ReparseTag)) {
3023
+ /* Reparse points are a general interception mechanism. Node/libuv
3024
+ * follows non-link tags even for lstat, so reopen the resolved target. */
3025
+ CloseHandle(h);
3026
+ return scr_stats_of_path(path, op, false);
3027
+ }
3028
+ if (!scr_stats_link_size(h, tagged.ReparseTag, &link_size)) {
3029
+ /* A mount-point tag may name a mounted volume rather than a junction.
3030
+ * libuv treats those (and malformed/unsupported link payloads) as
3031
+ * ordinary reparse points and retries with the final component
3032
+ * followed. Do not classify from the tag alone. */
3033
+ CloseHandle(h);
3034
+ return scr_stats_of_path(path, op, false);
3035
+ }
3036
+ is_link = true;
3037
+ }
3038
+ FILE_STANDARD_INFO standard;
3039
+ bool have_standard = GetFileInformationByHandleEx(
3040
+ h, FileStandardInfo, &standard, sizeof standard);
3041
+ CloseHandle(h);
3042
+
3043
+ bool is_dir = (basic.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
3044
+ uint64_t size = ((uint64_t)basic.nFileSizeHigh << 32) | basic.nFileSizeLow;
3045
+ ScrStats *s = scr_stats_new();
3046
+ s->is_file = !is_link && (have_standard ? !standard.Directory : !is_dir);
3047
+ s->is_dir = !is_link && (have_standard ? standard.Directory : is_dir);
3048
+ s->is_symlink = is_link;
3049
+ s->size = is_link && link_size >= 0 ? link_size : s->is_dir ? 0.0
3050
+ : have_standard ? (double)standard.EndOfFile.QuadPart : (double)size;
3051
+ s->blocks = have_standard
3052
+ ? (double)((uint64_t)standard.AllocationSize.QuadPart >> 9)
3053
+ : size == 0 ? 0.0 : (double)((size + 511) >> 9);
3054
+ s->nlink = have_standard
3055
+ ? (double)standard.NumberOfLinks
3056
+ : (double)basic.nNumberOfLinks;
3057
+ s->atime_ms = scr_filetime_unix_ms(basic.ftLastAccessTime);
3058
+ s->mtime_ms = scr_filetime_unix_ms(basic.ftLastWriteTime);
3059
+ return s;
3060
+ }
2417
3061
  #else
3062
+ static ScrStats *scr_stats_of(const struct stat *st) {
3063
+ ScrStats *s = scr_stats_new();
3064
+ s->is_file = S_ISREG(st->st_mode);
3065
+ s->is_dir = S_ISDIR(st->st_mode);
2418
3066
  s->is_symlink = S_ISLNK(st->st_mode);
2419
3067
  #if defined(__APPLE__)
3068
+ s->atime_ms = (double)st->st_atimespec.tv_sec * 1000.0 +
3069
+ (double)st->st_atimespec.tv_nsec / 1e6;
2420
3070
  s->mtime_ms = (double)st->st_mtimespec.tv_sec * 1000.0 +
2421
3071
  (double)st->st_mtimespec.tv_nsec / 1e6;
2422
3072
  #else
3073
+ s->atime_ms = (double)st->st_atim.tv_sec * 1000.0 +
3074
+ (double)st->st_atim.tv_nsec / 1e6;
2423
3075
  s->mtime_ms = (double)st->st_mtim.tv_sec * 1000.0 +
2424
3076
  (double)st->st_mtim.tv_nsec / 1e6;
2425
3077
  #endif
2426
- #endif
3078
+ s->blocks = (double)st->st_blocks;
3079
+ s->nlink = (double)st->st_nlink;
2427
3080
  s->size = (double)st->st_size;
2428
3081
  return s;
2429
3082
  }
3083
+ #endif
2430
3084
 
2431
3085
  ScrStats *scr_fs_stat(ScrStr *path) {
3086
+ #ifdef _WIN32
3087
+ return scr_stats_of_path(path, "stat", false);
3088
+ #else
2432
3089
  struct stat st;
2433
3090
  if (stat(path->data, &st) != 0) { /* follows symlinks, like Node's statSync */
2434
3091
  scr_fs_throw(errno, "stat", path);
2435
3092
  return NULL;
2436
3093
  }
2437
3094
  return scr_stats_of(&st);
3095
+ #endif
2438
3096
  }
2439
3097
 
2440
3098
  ScrStats *scr_fs_lstat(ScrStr *path) {
3099
+ #ifdef _WIN32
3100
+ return scr_stats_of_path(path, "lstat", true);
3101
+ #else
2441
3102
  struct stat st;
2442
3103
  if (lstat(path->data, &st) != 0) { /* no follow; Node reports lstat */
2443
3104
  scr_fs_throw(errno, "lstat", path);
2444
3105
  return NULL;
2445
3106
  }
2446
3107
  return scr_stats_of(&st);
3108
+ #endif
2447
3109
  }
2448
3110
 
2449
3111
  ScrArr *scr_fs_readdir(ScrStr *path) {
@@ -3222,10 +3884,6 @@ ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem) {
3222
3884
 
3223
3885
  /* ── String surface (fromCharCode / lastIndexOf) ─────────────────────── */
3224
3886
 
3225
- /* Forward declaration — scr_to_uint32 lives with the bitwise operators
3226
- * below (ToUint16 is its low 16 bits, per spec). */
3227
- static uint32_t scr_to_uint32(double d);
3228
-
3229
3887
  /* String.fromCharCode core over n UTF-16 code units read through
3230
3888
  * `unit(src, i)` (already ToUint16'd): combine adjacent surrogate pairs,
3231
3889
  * substitute U+FFFD for lone surrogates (divergence 1's storage policy —
@@ -4039,14 +4697,6 @@ bool scr_num_is_safe_integer(double x) {
4039
4697
  * narrowing casts, no UB shifts of signed values).
4040
4698
  */
4041
4699
 
4042
- static uint32_t scr_to_uint32(double d) {
4043
- if (!isfinite(d)) return 0; /* NaN, +Infinity, -Infinity */
4044
- double t = trunc(d);
4045
- t = fmod(t, 4294967296.0); /* exact for doubles; result in (-2^32, 2^32) */
4046
- if (t < 0) t += 4294967296.0;
4047
- return (uint32_t)t;
4048
- }
4049
-
4050
4700
  /* The 32 bits as a SIGNED (Int32) JS number. */
4051
4701
  static double scr_bits_as_int32(uint32_t u) {
4052
4702
  return u >= UINT32_C(0x80000000)