mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-12 02:13:04 +08:00
7769 lines
293 KiB
C
7769 lines
293 KiB
C
/* Standard library: the process global + synchronous node:fs (scr_runtime.h
|
||
* has the API contract). Everything here is called through compiler-emitted
|
||
* `libCall` IR.
|
||
*
|
||
* - process.argv is ONE interned array, built lazily on first read and
|
||
* retained per read — identity (`process.argv === process.argv`) and
|
||
* mutation persistence match Node's stable process.argv. The atexit
|
||
* cleanup registered by scr_lib_init releases the interned values before
|
||
* the RC audit runs (atexit is LIFO; scr_init registered the audit
|
||
* first), so they never count as leaks.
|
||
* - fs failures THROW through the exception cell (scr_throw_error — the
|
||
* payload is a catchable Error instance whose message is shaped like
|
||
* Node's fs error messages) and return a dummy; the compiler emits
|
||
* pending checks after every fs call (the MAY_THROW_LIB_FNS seed).
|
||
* scr_fs_exists never throws, like Node's existsSync.
|
||
*/
|
||
#include "scr_runtime.h"
|
||
#include "scr_system_errors.h"
|
||
|
||
#include <ctype.h>
|
||
#ifdef SCR_WORKERS
|
||
#include <stdatomic.h>
|
||
static atomic_uint scr_process_mask;
|
||
static void scr_process_mask_init(void);
|
||
#endif
|
||
#include <dirent.h>
|
||
#include <errno.h>
|
||
#include <fcntl.h>
|
||
#include <limits.h>
|
||
#include <math.h>
|
||
#include <signal.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#if defined(__linux__)
|
||
#include <sys/vfs.h>
|
||
#elif defined(__APPLE__) || defined(__FreeBSD__)
|
||
#include <sys/mount.h>
|
||
#elif !defined(_WIN32) && !defined(__wasi__)
|
||
#include <sys/statvfs.h>
|
||
#endif
|
||
#include <time.h>
|
||
|
||
#ifdef _WIN32
|
||
/* ── the Windows arm's system surface ─────────────────────────────────
|
||
* mingw-w64's CRT covers most of sync fs (open/read/write/stat/dirent —
|
||
* the seams below note what it lacks: d_type, lstat, two-arg mkdir,
|
||
* mkdtemp, O_SYNC) and the Win32 API covers the process/os surface
|
||
* (GetTempPathA, GetUserNameA, RtlGetVersion, GetConsoleScreenBufferInfo).
|
||
* What has NO arm yet is stubbed honestly at its seam: process.kill and
|
||
* getuid/getgid (needs-design: OpenProcess/TerminateProcess vs Node's
|
||
* uv_kill; no uids exist on Windows — Node omits the members there),
|
||
* setRawMode's raw arm (mechanical: SetConsoleMode).
|
||
* os.networkInterfaces HAS its arm: GetAdaptersAddresses below, libuv's
|
||
* exact row selection. */
|
||
#include <direct.h> /* _mkdir */
|
||
#include <io.h> /* _isatty, _access, open/read/write/close */
|
||
#include <process.h> /* getpid */
|
||
#include <unistd.h> /* mingw-w64 ships one: getcwd, access, isatty, ... */
|
||
#include <winsock2.h> /* BEFORE windows.h (which pulls winsock 1 otherwise) */
|
||
#include <ws2tcpip.h> /* inet_ntop, sockaddr_in6 */
|
||
#include <iphlpapi.h> /* GetAdaptersAddresses (os.networkInterfaces) */
|
||
#include <windows.h>
|
||
#include <winioctl.h> /* FSCTL_GET_REPARSE_POINT */
|
||
#include <lmcons.h> /* UNLEN for GetUserNameA */
|
||
#include "scr_win_stats.h"
|
||
|
||
static WCHAR *scr_fs_win_wide(const ScrStr *path);
|
||
static int scr_fs_win_errno(DWORD error);
|
||
static bool scr_stats_is_link_tag(DWORD tag);
|
||
static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr **text_out);
|
||
|
||
static ScrStr *scr_fs_win_utf8(const WCHAR *text, size_t length) {
|
||
if (length > INT_MAX) { SetLastError(ERROR_FILENAME_EXCED_RANGE); return NULL; }
|
||
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text, (int)length, NULL, 0, NULL, NULL);
|
||
if (size <= 0) return length == 0 ? scr_str_new("", 0) : NULL;
|
||
char *bytes = malloc((size_t)size);
|
||
if (!bytes) scr_trap("scriptc: out of memory\n");
|
||
if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text, (int)length, bytes, size, NULL, NULL) != size) {
|
||
DWORD error = GetLastError();
|
||
free(bytes);
|
||
SetLastError(error);
|
||
return NULL;
|
||
}
|
||
ScrStr *result = scr_str_new(bytes, (size_t)size);
|
||
free(bytes);
|
||
return result;
|
||
}
|
||
|
||
/* The CRT has no symlink view, so its internal lstat users degrade to stat.
|
||
* The public Stats path below bypasses this seam and opens the final component
|
||
* as a reparse point for lstatSync, matching Node's no-follow split. */
|
||
#define lstat stat
|
||
|
||
/* Windows has no directory mode bits; the CRT mkdir takes one argument. */
|
||
#define scr_sys_mkdir(p, m) ((void)(m), mkdir(p))
|
||
|
||
/* openSync's "rs"/"sa" flags: no O_SYNC on the CRT — degrade to non-sync
|
||
* opens (Node on Windows maps O_SYNC to FILE_FLAG_WRITE_THROUGH; the
|
||
* difference is durability, not observable output). */
|
||
#define O_SYNC 0
|
||
|
||
#ifndef INET6_ADDRSTRLEN
|
||
#define INET6_ADDRSTRLEN 46 /* ws2tcpip.h's value; only sizes the row bufs */
|
||
#endif
|
||
|
||
#else /* !_WIN32 */
|
||
|
||
#include <arpa/inet.h>
|
||
#include <ifaddrs.h>
|
||
#include <net/if.h>
|
||
#include <netinet/in.h>
|
||
#include <pthread.h>
|
||
#include <pwd.h>
|
||
#include <sys/ioctl.h>
|
||
#include <sys/socket.h>
|
||
#include <sys/utsname.h>
|
||
#include <termios.h>
|
||
#include <unistd.h>
|
||
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
||
#include <net/if_dl.h>
|
||
#elif defined(__linux__)
|
||
#include <inttypes.h>
|
||
#include <netpacket/packet.h>
|
||
#include <sys/sysinfo.h>
|
||
#endif
|
||
#if defined(__linux__)
|
||
#include <sched.h> /* sched_getaffinity, CPU_COUNT (os.availableParallelism) */
|
||
#elif defined(__APPLE__)
|
||
#include <sys/sysctl.h> /* sysctlbyname (os.availableParallelism) */
|
||
#endif
|
||
|
||
#define scr_sys_mkdir(p, m) mkdir((p), (m))
|
||
|
||
extern char **environ; /* env snapshot (scr_env_pairs) */
|
||
|
||
#endif /* _WIN32 */
|
||
|
||
/* O_BINARY: Windows-only (CRT text mode would translate \n on fd writes);
|
||
* zero elsewhere so the POSIX open flags are unchanged. */
|
||
#ifndef O_BINARY
|
||
#define O_BINARY 0
|
||
#endif
|
||
|
||
/* ── process ─────────────────────────────────────────────────────────── */
|
||
|
||
static SCR_TL int scr_lib_argc = 0;
|
||
static SCR_TL char **scr_lib_argv = NULL;
|
||
static SCR_TL char **scr_lib_argv_owned = NULL;
|
||
#if defined(_WIN32) && !defined(SCR_LIB)
|
||
static SCR_TL char *scr_lib_argv_utf8 = NULL;
|
||
#endif
|
||
static SCR_TL bool scr_lib_has_fork = false;
|
||
static SCR_TL double scr_lib_fork_target_id = -1;
|
||
static SCR_TL uintptr_t scr_lib_fork_read_handle = 0;
|
||
static SCR_TL uintptr_t scr_lib_fork_write_handle = 0;
|
||
static SCR_TL ScrArr *scr_argv_arr = NULL; /* interned process.argv */
|
||
static SCR_TL ScrStr *scr_platform_str = NULL; /* interned process.platform */
|
||
static SCR_TL ScrStr *scr_exec_path_str = NULL; /* interned process.execPath */
|
||
static SCR_TL ScrStr *scr_arch_str = NULL; /* interned process.arch */
|
||
static SCR_TL ScrStr *scr_versions_node_str = NULL; /* interned process.versions.node */
|
||
static SCR_TL ScrStr *scr_versions_openssl_str = NULL; /* interned process.versions.openssl */
|
||
|
||
typedef struct {
|
||
ScrStr *filename;
|
||
ScrStr *id;
|
||
ScrStr *path;
|
||
ScrArr *paths;
|
||
ScrArr *children;
|
||
double parent;
|
||
size_t cache_order;
|
||
bool in_cache;
|
||
bool loaded;
|
||
bool is_main;
|
||
} ScrModuleRecord;
|
||
|
||
static SCR_TL ScrModuleRecord *scr_module_records = NULL;
|
||
static SCR_TL size_t scr_module_record_count = 0;
|
||
static SCR_TL size_t scr_module_next_cache_order = 1;
|
||
|
||
static void scr_module_registry_cleanup(void) {
|
||
for (size_t i = 0; i < scr_module_record_count; i++) {
|
||
ScrModuleRecord *m = &scr_module_records[i];
|
||
scr_str_release(m->filename);
|
||
scr_str_release(m->id);
|
||
scr_str_release(m->path);
|
||
scr_arr_release(m->paths);
|
||
scr_arr_release(m->children);
|
||
}
|
||
free(scr_module_records);
|
||
scr_module_records = NULL;
|
||
scr_module_record_count = 0;
|
||
scr_module_next_cache_order = 1;
|
||
}
|
||
|
||
/* Keep lazy process values out of the startup cleanup root. The executable
|
||
* linker can discard an otherwise-unused getter, but an unconditional atexit
|
||
* callback that mentions every cache would still retain each cache cell (and
|
||
* its symbol) in a tiny hello-world. Each getter below instead registers its
|
||
* own cleanup only when the value is first materialized. That is equivalent
|
||
* at process exit and retains the RC-audit cleanup guarantee for the values a
|
||
* program actually observes. */
|
||
static void scr_lib_cleanup(void) {
|
||
scr_module_registry_cleanup();
|
||
scr_arr_release(scr_argv_arr);
|
||
scr_argv_arr = NULL;
|
||
free(scr_lib_argv_owned);
|
||
scr_lib_argv_owned = NULL;
|
||
#if defined(_WIN32) && !defined(SCR_LIB)
|
||
free(scr_lib_argv_utf8);
|
||
scr_lib_argv_utf8 = NULL;
|
||
#endif
|
||
scr_lib_argc = 0;
|
||
scr_lib_argv = NULL;
|
||
scr_lib_has_fork = false;
|
||
scr_lib_fork_target_id = -1;
|
||
scr_lib_fork_read_handle = 0;
|
||
scr_lib_fork_write_handle = 0;
|
||
}
|
||
|
||
static void scr_process_platform_cleanup(void) {
|
||
scr_str_release(scr_platform_str);
|
||
scr_platform_str = NULL;
|
||
}
|
||
|
||
static void scr_process_exec_path_cleanup(void) {
|
||
scr_str_release(scr_exec_path_str);
|
||
scr_exec_path_str = NULL;
|
||
}
|
||
|
||
static void scr_process_arch_cleanup(void) {
|
||
scr_str_release(scr_arch_str);
|
||
scr_arch_str = NULL;
|
||
}
|
||
|
||
static void scr_process_versions_node_cleanup(void) {
|
||
scr_str_release(scr_versions_node_str);
|
||
scr_versions_node_str = NULL;
|
||
}
|
||
|
||
static void scr_process_versions_openssl_cleanup(void) {
|
||
scr_str_release(scr_versions_openssl_str);
|
||
scr_versions_openssl_str = NULL;
|
||
}
|
||
|
||
static bool scr_lib_same_executable_arg(const char *a, const char *b) {
|
||
if (strcmp(a, b) == 0) return true;
|
||
char resolved_a[PATH_MAX], resolved_b[PATH_MAX];
|
||
#ifdef _WIN32
|
||
const char *use_a = _fullpath(resolved_a, a, sizeof resolved_a) != NULL ? resolved_a : a;
|
||
const char *use_b = _fullpath(resolved_b, b, sizeof resolved_b) != NULL ? resolved_b : b;
|
||
return _stricmp(use_a, use_b) == 0;
|
||
#else
|
||
const char *use_a = realpath(a, resolved_a) != NULL ? resolved_a : a;
|
||
const char *use_b = realpath(b, resolved_b) != NULL ? resolved_b : b;
|
||
return strcmp(use_a, use_b) == 0;
|
||
#endif
|
||
}
|
||
|
||
#if defined(_WIN32) && !defined(SCR_LIB)
|
||
/* C main's narrow argv has already lost characters outside the active ANSI
|
||
* code page. Recover the original Unicode command line before publishing
|
||
* process.argv to either execution tier. Keep this lazy: programs that do
|
||
* not read arguments need no converted strings or shell32 initialization.
|
||
* Load the documented OS parser from System32 without adding an executable-
|
||
* only shell dependency to the embedding runtime's link contract. */
|
||
static void scr_lib_prepare_utf8_argv(void) {
|
||
#ifdef SCR_WORKERS
|
||
/* Worker arguments were copied as UTF-8 before their native thread began. */
|
||
if (!scr_context_is_main()) return;
|
||
#endif
|
||
if (scr_lib_argv == NULL || scr_lib_argv_utf8 != NULL) return;
|
||
HMODULE shell = LoadLibraryExW(L"shell32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||
if (shell == NULL) scr_trap("could not load Windows command-line parser");
|
||
typedef LPWSTR *(WINAPI *ScrParseCommandLine)(LPCWSTR, int *);
|
||
ScrParseCommandLine parse = (ScrParseCommandLine)(void *)GetProcAddress(shell, "CommandLineToArgvW");
|
||
if (parse == NULL) {
|
||
FreeLibrary(shell);
|
||
scr_trap("Windows command-line parser is unavailable");
|
||
}
|
||
int count = 0;
|
||
LPWSTR *wide = parse(GetCommandLineW(), &count);
|
||
FreeLibrary(shell);
|
||
if (wide == NULL || count < 1) scr_trap("could not parse Windows command line");
|
||
size_t bytes = 0;
|
||
for (int i = 0; i < count; i++) {
|
||
int size = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL);
|
||
if (size == 0 || bytes > SIZE_MAX - (size_t)size) {
|
||
LocalFree(wide);
|
||
scr_trap("could not encode Windows argument");
|
||
}
|
||
bytes += (size_t)size;
|
||
}
|
||
char *text = malloc(bytes);
|
||
char **args = calloc((size_t)count + 1, sizeof(char *));
|
||
if (text == NULL || args == NULL) {
|
||
free(text); free(args); LocalFree(wide);
|
||
scr_trap("out of memory");
|
||
}
|
||
char *next = text;
|
||
int used = 0;
|
||
for (int i = 0; i < count; i++) {
|
||
// scr_lib_init already consumed this private ASCII fork marker. Do not
|
||
// reintroduce it when replacing the narrow argument storage.
|
||
if (i == 1 && scr_lib_has_fork) continue;
|
||
int size = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL);
|
||
if (size == 0 || WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, next, size, NULL, NULL) != size) {
|
||
free(text); free(args); LocalFree(wide);
|
||
scr_trap("could not encode Windows argument");
|
||
}
|
||
args[used++] = next;
|
||
next += size;
|
||
}
|
||
LocalFree(wide);
|
||
free(scr_lib_argv_owned);
|
||
scr_lib_argv_owned = args;
|
||
scr_lib_argv_utf8 = text;
|
||
scr_lib_argv = args;
|
||
scr_lib_argc = used;
|
||
}
|
||
#endif
|
||
|
||
/* A child_process call spelling Node's self-reexec shape:
|
||
* spawn(process.execPath, [process.argv[1], ...args]). The parent still
|
||
* knows these two path-like arguments are the executable and script marker,
|
||
* so its argv builder can collapse the marker without guessing from an
|
||
* unrelated child's raw user arguments. Library sessions have no argv and
|
||
* always answer false. */
|
||
bool scr_lib_should_collapse_reexec_arg(ScrStr *cmd, ScrArr *args) {
|
||
if (scr_lib_arg_count() < 1 || scr_arr_len(args) < 1) return false;
|
||
ScrStr *first = (ScrStr *)scr_arr_get_ref(args, 0);
|
||
bool collapse = scr_lib_same_executable_arg(cmd->data, scr_lib_argv[0]) &&
|
||
scr_lib_same_executable_arg(first->data, scr_lib_argv[0]);
|
||
scr_str_release(first);
|
||
return collapse;
|
||
}
|
||
|
||
#ifndef SCR_LIB
|
||
/* Executable lane only: a library artifact has no argv and registers no
|
||
* atexit handlers (the emitted library init never calls this; keeping it out
|
||
* the archive's objects free of any atexit reference — the K8 ambient
|
||
* audit's bar). */
|
||
void scr_lib_init(int argc, char **argv) {
|
||
#ifdef SCR_WORKERS
|
||
if (scr_context_is_main()) scr_process_mask_init();
|
||
#endif
|
||
scr_lib_argc = argc;
|
||
scr_lib_argv = argv;
|
||
if (argc >= 2 && strncmp(argv[1], "--scriptc-fork=", 15) == 0) {
|
||
unsigned long long target = 0, read_handle = 0, write_handle = 0;
|
||
char tail = '\0';
|
||
if (sscanf(argv[1] + 15, "%llu,%llu,%llu%c", &target, &read_handle,
|
||
&write_handle, &tail) == 3) {
|
||
scr_lib_has_fork = true;
|
||
scr_lib_fork_target_id = (double)target;
|
||
scr_lib_fork_read_handle = (uintptr_t)read_handle;
|
||
scr_lib_fork_write_handle = (uintptr_t)write_handle;
|
||
scr_lib_argv_owned = malloc((size_t)argc * sizeof(char *));
|
||
if (!scr_lib_argv_owned) scr_trap("out of memory");
|
||
scr_lib_argv_owned[0] = argv[0];
|
||
for (int i = 2; i < argc; i++) scr_lib_argv_owned[i - 1] = argv[i];
|
||
scr_lib_argv_owned[argc - 1] = NULL;
|
||
scr_lib_argc = argc - 1;
|
||
scr_lib_argv = scr_lib_argv_owned;
|
||
}
|
||
}
|
||
scr_atexit(scr_lib_cleanup);
|
||
}
|
||
#endif /* !SCR_LIB */
|
||
|
||
#ifdef SCR_LIB
|
||
/* Library builds never call scr_lib_init (a library artifact has no argv and registers no
|
||
* atexit handlers); the interned process values above still intern lazily
|
||
* on first read, so the library reset seam releases them here instead —
|
||
* scr_library_reset (scr_library.c) calls this every session reset. */
|
||
void scr_lib_session_cleanup(void) {
|
||
scr_lib_cleanup();
|
||
scr_process_platform_cleanup();
|
||
scr_process_exec_path_cleanup();
|
||
scr_process_arch_cleanup();
|
||
scr_process_versions_node_cleanup();
|
||
scr_process_versions_openssl_cleanup();
|
||
}
|
||
#endif
|
||
|
||
static size_t scr_module_index(double module_id) {
|
||
if (!isfinite(module_id) || module_id < 0 || trunc(module_id) != module_id ||
|
||
module_id >= (double)scr_module_record_count) {
|
||
scr_trap("invalid CommonJS module handle");
|
||
}
|
||
return (size_t)module_id;
|
||
}
|
||
|
||
static ScrModuleRecord *scr_module_record(double module_id) {
|
||
return &scr_module_records[scr_module_index(module_id)];
|
||
}
|
||
|
||
void scr_module_registry_init(double count_value) {
|
||
if (!isfinite(count_value) || count_value < 0 || trunc(count_value) != count_value ||
|
||
count_value > (double)SIZE_MAX) {
|
||
scr_trap("invalid CommonJS module registry size");
|
||
}
|
||
scr_module_registry_cleanup();
|
||
scr_module_record_count = (size_t)count_value;
|
||
if (scr_module_record_count == 0) return;
|
||
scr_module_records = (ScrModuleRecord *)calloc(scr_module_record_count, sizeof(*scr_module_records));
|
||
if (!scr_module_records) scr_trap("out of memory");
|
||
for (size_t i = 0; i < scr_module_record_count; i++) {
|
||
scr_module_records[i].parent = -1;
|
||
}
|
||
}
|
||
|
||
void scr_module_define(double module_id, ScrStr *filename, ScrStr *id,
|
||
ScrStr *path, ScrArr *paths, bool is_main) {
|
||
ScrModuleRecord *m = scr_module_record(module_id);
|
||
scr_str_release(m->filename);
|
||
scr_str_release(m->id);
|
||
scr_str_release(m->path);
|
||
scr_arr_release(m->paths);
|
||
scr_arr_release(m->children);
|
||
m->filename = scr_str_retain(filename);
|
||
m->id = scr_str_retain(id);
|
||
m->path = scr_str_retain(path);
|
||
m->paths = scr_arr_retain(paths);
|
||
m->children = scr_arr_new(SCR_ELEM_F64, 4);
|
||
m->parent = is_main ? -1 : -2;
|
||
m->cache_order = 0;
|
||
m->in_cache = false;
|
||
m->loaded = false;
|
||
m->is_main = is_main;
|
||
}
|
||
|
||
void scr_module_enter(double module_id) {
|
||
ScrModuleRecord *m = scr_module_record(module_id);
|
||
if (!m->in_cache) {
|
||
m->in_cache = true;
|
||
m->cache_order = scr_module_next_cache_order++;
|
||
}
|
||
m->loaded = false;
|
||
}
|
||
|
||
void scr_module_link(double parent_id, double child_id) {
|
||
ScrModuleRecord *parent = scr_module_record(parent_id);
|
||
ScrModuleRecord *child = scr_module_record(child_id);
|
||
if (!child->is_main && child->parent < 0) child->parent = parent_id;
|
||
if (!scr_arr_includes_f64(parent->children, child_id)) {
|
||
scr_arr_push_f64(parent->children, child_id);
|
||
}
|
||
}
|
||
|
||
void scr_module_finish(double module_id) {
|
||
ScrModuleRecord *m = scr_module_record(module_id);
|
||
m->loaded = true;
|
||
}
|
||
|
||
void scr_module_fail(double module_id) {
|
||
ScrModuleRecord *m = scr_module_record(module_id);
|
||
if (m->parent >= 0) {
|
||
ScrModuleRecord *parent = scr_module_record(m->parent);
|
||
double at = scr_arr_index_of_f64(parent->children, module_id);
|
||
if (at >= 0) {
|
||
ScrArr *removed = scr_arr_splice(parent->children, at, 1);
|
||
scr_arr_release(removed);
|
||
}
|
||
}
|
||
m->parent = m->is_main ? -1 : -2;
|
||
m->cache_order = 0;
|
||
m->in_cache = false;
|
||
m->loaded = false;
|
||
}
|
||
|
||
ScrStr *scr_module_filename(double module_id) {
|
||
return scr_str_retain(scr_module_record(module_id)->filename);
|
||
}
|
||
|
||
ScrStr *scr_module_id(double module_id) {
|
||
return scr_str_retain(scr_module_record(module_id)->id);
|
||
}
|
||
|
||
ScrStr *scr_module_path(double module_id) {
|
||
return scr_str_retain(scr_module_record(module_id)->path);
|
||
}
|
||
|
||
ScrArr *scr_module_paths(double module_id) {
|
||
return scr_arr_retain(scr_module_record(module_id)->paths);
|
||
}
|
||
|
||
ScrArr *scr_module_children(double module_id) {
|
||
return scr_arr_retain(scr_module_record(module_id)->children);
|
||
}
|
||
|
||
double scr_module_parent(double module_id) {
|
||
return scr_module_record(module_id)->parent;
|
||
}
|
||
|
||
bool scr_module_loaded(double module_id) {
|
||
return scr_module_record(module_id)->loaded;
|
||
}
|
||
|
||
static bool scr_module_filename_equal(const ScrModuleRecord *m, const ScrStr *filename) {
|
||
return m->filename && m->filename->len == filename->len &&
|
||
memcmp(m->filename->data, filename->data, filename->len) == 0;
|
||
}
|
||
|
||
double scr_module_cache_get(ScrStr *filename) {
|
||
for (size_t i = 0; i < scr_module_record_count; i++) {
|
||
const ScrModuleRecord *m = &scr_module_records[i];
|
||
if (m->in_cache && scr_module_filename_equal(m, filename)) return (double)i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
bool scr_module_cache_has(ScrStr *filename) {
|
||
return scr_module_cache_get(filename) >= 0;
|
||
}
|
||
|
||
ScrArr *scr_module_cache_keys(void) {
|
||
ScrArr *keys = scr_arr_new(SCR_ELEM_STR, scr_module_record_count);
|
||
for (size_t order = 1; order < scr_module_next_cache_order; order++) {
|
||
for (size_t i = 0; i < scr_module_record_count; i++) {
|
||
ScrModuleRecord *m = &scr_module_records[i];
|
||
if (m->in_cache && m->cache_order == order) {
|
||
scr_arr_push_ref(keys, scr_str_retain(m->filename));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
int scr_lib_arg_count(void) {
|
||
#if defined(_WIN32) && !defined(SCR_LIB)
|
||
scr_lib_prepare_utf8_argv();
|
||
#endif
|
||
return scr_lib_argc;
|
||
}
|
||
const char *scr_lib_arg(int i) {
|
||
#if defined(_WIN32) && !defined(SCR_LIB)
|
||
scr_lib_prepare_utf8_argv();
|
||
#endif
|
||
return scr_lib_argv[i];
|
||
}
|
||
|
||
bool scr_lib_fork_info(double *target, uintptr_t *read_handle,
|
||
uintptr_t *write_handle) {
|
||
if (!scr_lib_has_fork) return false;
|
||
*target = scr_lib_fork_target_id;
|
||
*read_handle = scr_lib_fork_read_handle;
|
||
*write_handle = scr_lib_fork_write_handle;
|
||
return true;
|
||
}
|
||
|
||
ScrArr *scr_process_argv(void) {
|
||
if (!scr_argv_arr) {
|
||
/* ["scriptc", argv[0], argv[1], ...]: positions and length line up
|
||
* with Node's [node-path, script-path, ...args]; the argv[0]/argv[1]
|
||
* VALUES diverge (SEMANTICS.md). */
|
||
int argc = scr_lib_arg_count();
|
||
scr_argv_arr = scr_arr_new(SCR_ELEM_STR, (size_t)argc + 1);
|
||
scr_arr_push_ref(scr_argv_arr, scr_str_new("scriptc", 7));
|
||
for (int i = 0; i < argc; i++) {
|
||
const char *a = scr_lib_arg(i);
|
||
scr_arr_push_ref(scr_argv_arr, scr_str_new(a, strlen(a)));
|
||
}
|
||
}
|
||
return scr_arr_retain(scr_argv_arr);
|
||
}
|
||
|
||
ScrStr *scr_process_platform(void) {
|
||
if (!scr_platform_str) {
|
||
#if defined(__APPLE__)
|
||
scr_platform_str = scr_str_new("darwin", 6);
|
||
#elif defined(__linux__)
|
||
scr_platform_str = scr_str_new("linux", 5);
|
||
#elif defined(__wasi__)
|
||
scr_platform_str = scr_str_new("wasi", 4);
|
||
#elif defined(_WIN32)
|
||
scr_platform_str = scr_str_new("win32", 5);
|
||
#else
|
||
scr_platform_str = scr_str_new("unknown", 7);
|
||
#endif
|
||
#ifndef SCR_LIB
|
||
scr_atexit(scr_process_platform_cleanup);
|
||
#endif
|
||
}
|
||
return scr_str_retain(scr_platform_str);
|
||
}
|
||
|
||
/* process.arch — the compiled binary's OWN architecture, spelled the way
|
||
* Node spells its own build's arch. Interned like process.platform. */
|
||
ScrStr *scr_process_arch(void) {
|
||
if (!scr_arch_str) {
|
||
#if defined(__wasm32__)
|
||
scr_arch_str = scr_str_new("wasm32", 6);
|
||
#elif defined(__wasm64__)
|
||
scr_arch_str = scr_str_new("wasm64", 6);
|
||
#elif defined(__aarch64__) || defined(_M_ARM64)
|
||
scr_arch_str = scr_str_new("arm64", 5);
|
||
#elif defined(__x86_64__) || defined(_M_X64)
|
||
scr_arch_str = scr_str_new("x64", 3);
|
||
#else
|
||
scr_arch_str = scr_str_new("unknown", 7);
|
||
#endif
|
||
#ifndef SCR_LIB
|
||
scr_atexit(scr_process_arch_cleanup);
|
||
#endif
|
||
}
|
||
return scr_str_retain(scr_arch_str);
|
||
}
|
||
|
||
/* process.versions.node — the runtime's Node COMPATIBILITY TARGET. There
|
||
* is no Node under a compiled binary; this is the version whose semantics
|
||
* SEMANTICS.md verifies the runtime against (divergence 60, the execPath
|
||
* stance: answer for the world that actually exists). */
|
||
#define SCR_NODE_COMPAT_VERSION "24.15.0"
|
||
ScrStr *scr_process_versions_node(void) {
|
||
if (!scr_versions_node_str) {
|
||
scr_versions_node_str =
|
||
scr_str_new(SCR_NODE_COMPAT_VERSION, sizeof(SCR_NODE_COMPAT_VERSION) - 1);
|
||
#ifndef SCR_LIB
|
||
scr_atexit(scr_process_versions_node_cleanup);
|
||
#endif
|
||
}
|
||
return scr_str_retain(scr_versions_node_str);
|
||
}
|
||
|
||
/* process.versions.openssl — the compat target's crypto-provider version
|
||
* string (the versions.node stance): Boolean(process.versions.openssl) is
|
||
* Node's own "is crypto available" idiom, and the runtime DOES ship a
|
||
* crypto module (mbedTLS-backed; unsupported members fence at their call
|
||
* sites). Reading undefined here made every hasCrypto test self-skip —
|
||
* measuring nothing (SEMANTICS.md; the divergence entry documents that
|
||
* the string names the compat target's OpenSSL, not a linked library). */
|
||
#define SCR_OPENSSL_COMPAT_VERSION "3.5.5"
|
||
ScrStr *scr_process_versions_openssl(void) {
|
||
if (!scr_versions_openssl_str) {
|
||
scr_versions_openssl_str =
|
||
scr_str_new(SCR_OPENSSL_COMPAT_VERSION, sizeof(SCR_OPENSSL_COMPAT_VERSION) - 1);
|
||
#ifndef SCR_LIB
|
||
scr_atexit(scr_process_versions_openssl_cleanup);
|
||
#endif
|
||
}
|
||
return scr_str_retain(scr_versions_openssl_str);
|
||
}
|
||
|
||
static SCR_TL ScrDyn *scr_versions_object;
|
||
|
||
static void scr_versions_object_cleanup(void) {
|
||
scr_dyn_release(scr_versions_object);
|
||
scr_versions_object = NULL;
|
||
}
|
||
|
||
ScrDyn *scr_process_versions(void) {
|
||
if (!scr_versions_object) {
|
||
scr_versions_object = scr_dyn_new_obj();
|
||
ScrStr *node = scr_process_versions_node();
|
||
ScrStr *openssl = scr_process_versions_openssl();
|
||
scr_dyn_obj_set(scr_versions_object, "node", 4, scr_dyn_new_str(node));
|
||
scr_dyn_obj_set(scr_versions_object, "openssl", 7, scr_dyn_new_str(openssl));
|
||
scr_str_release(node);
|
||
scr_str_release(openssl);
|
||
for (size_t i = 0; i < scr_versions_object->v.obj.len; i++) {
|
||
scr_versions_object->v.obj.entries[i].writable = false;
|
||
}
|
||
scr_atexit(&scr_versions_object_cleanup);
|
||
}
|
||
return scr_dyn_retain(scr_versions_object);
|
||
}
|
||
|
||
/* process.execPath — the running binary's own resolved absolute path,
|
||
* exactly what Node computes for ITS executable (uv_exepath + realpath):
|
||
* _NSGetExecutablePath on macOS, /proc/self/exe on Linux, argv[0] as the
|
||
* last resort. Interned like process.platform; +1 per read. */
|
||
ScrStr *scr_process_exec_path(void) {
|
||
if (!scr_exec_path_str) {
|
||
char raw[4096];
|
||
raw[0] = '\0';
|
||
#if defined(__APPLE__)
|
||
uint32_t size = sizeof(raw);
|
||
extern int _NSGetExecutablePath(char *buf, uint32_t *bufsize);
|
||
if (_NSGetExecutablePath(raw, &size) != 0) raw[0] = '\0';
|
||
#elif defined(__linux__)
|
||
ssize_t got = readlink("/proc/self/exe", raw, sizeof(raw) - 1);
|
||
if (got > 0) raw[got] = '\0';
|
||
else raw[0] = '\0';
|
||
#elif defined(_WIN32)
|
||
/* uv_exepath's source of truth on Windows; already absolute, and
|
||
* spelled with backslashes like Node's own execPath there. */
|
||
DWORD got = GetModuleFileNameA(NULL, raw, sizeof(raw) - 1);
|
||
raw[got < sizeof(raw) ? got : 0] = '\0';
|
||
#endif
|
||
if (raw[0] == '\0' && scr_lib_argc > 0) {
|
||
snprintf(raw, sizeof(raw), "%s", scr_lib_argv[0]);
|
||
}
|
||
char resolved[PATH_MAX];
|
||
#ifdef _WIN32
|
||
const char *use = _fullpath(resolved, raw, sizeof resolved) != NULL ? resolved : raw;
|
||
#else
|
||
const char *use = realpath(raw, resolved) != NULL ? resolved : raw;
|
||
#endif
|
||
scr_exec_path_str = scr_str_new(use, strlen(use));
|
||
#ifndef SCR_LIB
|
||
scr_atexit(scr_process_exec_path_cleanup);
|
||
#endif
|
||
}
|
||
return scr_str_retain(scr_exec_path_str);
|
||
}
|
||
|
||
ScrStr *scr_env_get(const ScrStr *name) {
|
||
#ifdef SCR_WORKERS
|
||
if (!scr_context_is_main()) {
|
||
const char *value = scr_context_getenv(name->data);
|
||
return value ? scr_str_new(value, strlen(value)) : NULL;
|
||
}
|
||
#endif
|
||
/* ScrStr data is NUL-terminated (like the fs paths below). A fresh copy
|
||
* per read: getenv's buffer is not ours to alias, and Node's process.env
|
||
* reads snapshot the value too. Absent → NULL (the compiler's undefined
|
||
* arm), never a throw. */
|
||
#ifdef _WIN32
|
||
/* The WIN32 environment, not the CRT's startup snapshot — libuv's choice
|
||
* too, and the one CreateProcess children inherit. Case-insensitive,
|
||
* like Node's process.env on Windows. */
|
||
DWORD need = GetEnvironmentVariableA(name->data, NULL, 0);
|
||
if (need == 0) return NULL; /* absent (an empty value still needs its NUL) */
|
||
char *buf = malloc(need);
|
||
if (!buf) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
DWORD got = GetEnvironmentVariableA(name->data, buf, need);
|
||
ScrStr *s = scr_str_new(buf, got);
|
||
free(buf);
|
||
return s;
|
||
#else
|
||
const char *v = scr_getenv(name->data);
|
||
return v ? scr_str_new(v, strlen(v)) : NULL;
|
||
#endif
|
||
}
|
||
|
||
/* util.debuglog matches sections against NODE_DEBUG as the process
|
||
* started, so the first write to that variable records the value it
|
||
* replaces. */
|
||
static SCR_TL bool scr_node_debug_captured = false;
|
||
static SCR_TL char *scr_node_debug_value = NULL;
|
||
|
||
static void scr_node_debug_teardown(void) {
|
||
free(scr_node_debug_value);
|
||
scr_node_debug_value = NULL;
|
||
}
|
||
|
||
const char *scr_node_debug_initial(void) {
|
||
if (!scr_node_debug_captured) {
|
||
scr_node_debug_captured = true;
|
||
const char *value = scr_getenv("NODE_DEBUG");
|
||
if (value) {
|
||
size_t len = strlen(value);
|
||
scr_node_debug_value = malloc(len + 1);
|
||
if (!scr_node_debug_value) scr_trap("scriptc: out of memory\n");
|
||
memcpy(scr_node_debug_value, value, len + 1);
|
||
scr_atexit(scr_node_debug_teardown);
|
||
}
|
||
}
|
||
return scr_node_debug_value;
|
||
}
|
||
|
||
static void scr_node_debug_capture(const ScrStr *name) {
|
||
if (name->len == 10 && memcmp(name->data, "NODE_DEBUG", 10) == 0) (void)scr_node_debug_initial();
|
||
}
|
||
|
||
/* process.env.NAME = v — setenv(3): later scr_env_get reads and spawned
|
||
* children (posix_spawn inherits environ) observe the write, like Node.
|
||
* Both args borrowed (NUL-terminated ScrStr data); setenv copies. */
|
||
void scr_env_set(const ScrStr *name, const ScrStr *value) {
|
||
scr_node_debug_capture(name);
|
||
/* The environment holds UTF-8: a lone surrogate is stored as U+FFFD. */
|
||
char *name_owned, *value_owned;
|
||
const char *k = scr_str_utf8(name, &name_owned), *v = scr_str_utf8(value, &value_owned);
|
||
#ifdef SCR_WORKERS
|
||
if (scr_context_env_set(k, v)) goto done;
|
||
#endif
|
||
#ifdef _WIN32
|
||
/* The WIN32 environment (see scr_env_get): an empty value stays a
|
||
* present-but-empty variable, and children inherit the write. */
|
||
SetEnvironmentVariableA(k, v);
|
||
#else
|
||
setenv(k, v, 1);
|
||
#endif
|
||
#ifdef SCR_WORKERS
|
||
done:
|
||
#endif
|
||
scr_str_utf8_end(name_owned);
|
||
scr_str_utf8_end(value_owned);
|
||
}
|
||
|
||
/* `delete process.env.NAME` — unsetenv(3): later reads answer absent and
|
||
* spawned children lose the variable, like Node. Borrowed. */
|
||
void scr_env_unset(const ScrStr *name) {
|
||
scr_node_debug_capture(name);
|
||
#ifdef SCR_WORKERS
|
||
if (scr_context_env_set(name->data, NULL)) return;
|
||
#endif
|
||
#ifdef _WIN32
|
||
SetEnvironmentVariableA(name->data, NULL);
|
||
#else
|
||
unsetenv(name->data);
|
||
#endif
|
||
}
|
||
|
||
static ScrStr *scr_fs_read_file_core(ScrStr *path, bool env_file);
|
||
|
||
/* Node's dotenv parser trims ASCII space/tab/newline and removes every CR,
|
||
* including CR inside quoted values. Quoted values can span lines; only
|
||
* double quotes expand backslash-n. Preserve the pinned parser's ordering
|
||
* of empty-value and export-prefix handling, including its unusual forms. */
|
||
static bool scr_env_space(char c) { return c == ' ' || c == '\t' || c == '\n'; }
|
||
|
||
static void scr_env_trim(const char *text, size_t *start, size_t *end) {
|
||
while (*start < *end && scr_env_space(text[*start])) (*start)++;
|
||
while (*end > *start && scr_env_space(text[*end - 1])) (*end)--;
|
||
}
|
||
|
||
static void scr_env_store(ScrDyn *out, const char *text, size_t ks, size_t ke,
|
||
size_t vs, size_t ve, bool expand) {
|
||
ScrJsonBuf value;
|
||
scr_jb_init(&value);
|
||
for (size_t i = vs; i < ve; i++) {
|
||
if (expand && text[i] == '\\' && i + 1 < ve && text[i + 1] == 'n') {
|
||
scr_jb_putc(&value, '\n');
|
||
i++;
|
||
} else scr_jb_putc(&value, text[i]);
|
||
}
|
||
ScrStr *s = scr_jb_finish(&value);
|
||
scr_dyn_obj_set(out, text + ks, ke - ks, scr_dyn_new_str(s));
|
||
scr_str_release(s);
|
||
}
|
||
|
||
static int scr_env_entry_order(const void *left, const void *right) {
|
||
const ScrDynEntry *a = *(const ScrDynEntry *const *)left;
|
||
const ScrDynEntry *b = *(const ScrDynEntry *const *)right;
|
||
size_t n = a->key_len < b->key_len ? a->key_len : b->key_len;
|
||
int cmp = memcmp(a->key, b->key, n);
|
||
return cmp ? cmp : (a->key_len > b->key_len) - (a->key_len < b->key_len);
|
||
}
|
||
|
||
static ScrDyn *scr_env_parse_content(const ScrStr *input, bool ordinary) {
|
||
char *text = malloc(input->len + 1);
|
||
if (!text) scr_trap("scriptc: out of memory\n");
|
||
size_t end = 0;
|
||
for (size_t i = 0; i < input->len; i++) if (input->data[i] != '\r') text[end++] = input->data[i];
|
||
text[end] = '\0';
|
||
size_t pos = 0;
|
||
scr_env_trim(text, &pos, &end);
|
||
ScrDyn *entries = scr_dyn_new_obj_null_proto();
|
||
while (pos < end) {
|
||
if (text[pos] == '\n' || text[pos] == '#') {
|
||
while (pos < end && text[pos] != '\n') pos++;
|
||
if (pos < end) pos++;
|
||
continue;
|
||
}
|
||
size_t equal = pos;
|
||
while (equal < end && text[equal] != '=' && text[equal] != '\n') equal++;
|
||
if (equal == end) break;
|
||
if (text[equal] == '\n') {
|
||
pos = equal + 1;
|
||
scr_env_trim(text, &pos, &end);
|
||
continue;
|
||
}
|
||
size_t ks = pos, ke = equal;
|
||
scr_env_trim(text, &ks, &ke);
|
||
pos = equal + 1;
|
||
if (pos == end || text[pos] == '\n') {
|
||
scr_env_store(entries, text, ks, ke, pos, pos, false);
|
||
continue;
|
||
}
|
||
scr_env_trim(text, &pos, &end);
|
||
if (ks == ke) continue;
|
||
if (ke - ks >= 7 && memcmp(text + ks, "export ", 7) == 0) {
|
||
ks += 7;
|
||
scr_env_trim(text, &ks, &ke);
|
||
}
|
||
if (pos == end) {
|
||
scr_env_store(entries, text, ks, ke, pos, pos, false);
|
||
break;
|
||
}
|
||
char quote = text[pos];
|
||
if (quote == '\'' || quote == '"' || quote == '`') {
|
||
size_t closing = pos + 1;
|
||
while (closing < end && text[closing] != quote) closing++;
|
||
if (closing < end) {
|
||
scr_env_store(entries, text, ks, ke, pos + 1, closing, quote == '"');
|
||
pos = closing + 1;
|
||
while (pos < end && text[pos] != '\n') pos++;
|
||
if (pos < end) pos++;
|
||
continue;
|
||
}
|
||
size_t newline = pos;
|
||
while (newline < end && text[newline] != '\n') newline++;
|
||
scr_env_store(entries, text, ks, ke, pos, newline, false);
|
||
pos = newline < end ? newline + 1 : end;
|
||
} else {
|
||
size_t newline = pos;
|
||
while (newline < end && text[newline] != '\n') newline++;
|
||
size_t vs = pos, ve = pos;
|
||
while (ve < newline && text[ve] != '#') ve++;
|
||
scr_env_trim(text, &vs, &ve);
|
||
scr_env_store(entries, text, ks, ke, vs, ve, false);
|
||
pos = newline < end ? newline + 1 : end;
|
||
}
|
||
scr_env_trim(text, &pos, &end);
|
||
}
|
||
free(text);
|
||
/* Node's std::map orders UTF-8 keys before installing object properties.
|
||
* Ordinary objects ignore string-valued __proto__ assignments; loading
|
||
* the native environment retains that name as an ordinary variable. */
|
||
size_t count = entries->v.obj.len;
|
||
const ScrDynEntry **sorted = count ? malloc(count * sizeof(*sorted)) : NULL;
|
||
if (count && !sorted) scr_trap("scriptc: out of memory\n");
|
||
for (size_t i = 0; i < count; i++) sorted[i] = &entries->v.obj.entries[i];
|
||
if (count > 1) qsort(sorted, count, sizeof(*sorted), scr_env_entry_order);
|
||
ScrDyn *result = ordinary ? scr_dyn_new_obj() : scr_dyn_new_obj_null_proto();
|
||
for (size_t i = 0; i < count; i++) {
|
||
const ScrDynEntry *entry = sorted[i];
|
||
if (ordinary && entry->key_len == 9 && memcmp(entry->key, "__proto__", 9) == 0) continue;
|
||
if (!ordinary) {
|
||
scr_dyn_obj_set(result, entry->key, entry->key_len, scr_dyn_retain(entry->value));
|
||
continue;
|
||
}
|
||
/* Convert sorted entries to JS strings after parsing. */
|
||
ScrStr *key = scr_str_from_utf8_lossy((const uint8_t *)entry->key, entry->key_len);
|
||
const ScrStr *raw = entry->value->v.str;
|
||
ScrStr *value = scr_str_from_utf8_lossy((const uint8_t *)raw->data, raw->len);
|
||
scr_dyn_obj_set(result, key->data, key->len, scr_dyn_new_str(value));
|
||
scr_str_release(key);
|
||
scr_str_release(value);
|
||
}
|
||
free(sorted);
|
||
scr_dyn_release(entries);
|
||
return result;
|
||
}
|
||
|
||
ScrDyn *scr_util_parse_env(const ScrDyn *content) {
|
||
if (content->kind != SCR_DYN_STR) {
|
||
scr_dyn_arg_type_fail("content", "of type string", content);
|
||
return NULL;
|
||
}
|
||
/* The parser works on UTF-8, as Node's does: a lone surrogate in the
|
||
* string arrives as U+FFFD. */
|
||
ScrStr *text = scr_str_to_well_formed(content->v.str);
|
||
ScrDyn *entries = scr_env_parse_content(text, true);
|
||
scr_str_release(text);
|
||
return entries;
|
||
}
|
||
|
||
void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const char *name) {
|
||
ScrJsonBuf b;
|
||
scr_jb_init(&b);
|
||
scr_jb_puts(&b, "The argument '");
|
||
scr_jb_puts(&b, name);
|
||
scr_jb_puts(&b, "' must be a string, Uint8Array, or URL without null bytes. Received ");
|
||
if (value->kind == SCR_DYN_BYTES) {
|
||
const ScrBytes *bytes = value->v.bytes;
|
||
SCR_SHARED_GUARD(bytes, NULL);
|
||
if (value->buffer) {
|
||
scr_jb_puts(&b, "<Buffer");
|
||
for (size_t i = 0; i < bytes->len; i++) {
|
||
char hex[4];
|
||
snprintf(hex, sizeof hex, " %02x", bytes->data[i]);
|
||
scr_jb_puts(&b, hex);
|
||
}
|
||
scr_jb_putc(&b, '>');
|
||
} else {
|
||
char length[48];
|
||
snprintf(length, sizeof length, "Uint8Array(%zu) [ ", bytes->len);
|
||
scr_jb_puts(&b, length);
|
||
for (size_t i = 0; i < bytes->len; i++) {
|
||
char number[8];
|
||
snprintf(number, sizeof number, "%s%u", i ? ", " : "", bytes->data[i]);
|
||
scr_jb_puts(&b, number);
|
||
}
|
||
scr_jb_puts(&b, " ]");
|
||
}
|
||
} else {
|
||
char quote = memchr(path->data, '\'', path->len) ? '"' : '\'';
|
||
scr_jb_putc(&b, quote);
|
||
for (size_t i = 0; i < path->len; i++) {
|
||
unsigned char c = (unsigned char)path->data[i];
|
||
if (c == (unsigned char)quote || c == '\\') { scr_jb_putc(&b, '\\'); scr_jb_putc(&b, (char)c); }
|
||
else if (c == '\n') scr_jb_puts(&b, "\\n");
|
||
else if (c == '\r') scr_jb_puts(&b, "\\r");
|
||
else if (c == '\t') scr_jb_puts(&b, "\\t");
|
||
else if (c < 0x20 || c == 0x7f) {
|
||
char escape[5];
|
||
snprintf(escape, sizeof escape, "\\x%02X", c);
|
||
scr_jb_puts(&b, escape);
|
||
} else scr_jb_putc(&b, (char)c);
|
||
}
|
||
scr_jb_putc(&b, quote);
|
||
}
|
||
ScrStr *message = scr_jb_finish(&b);
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_VALUE");
|
||
scr_str_release(message);
|
||
}
|
||
|
||
void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path) {
|
||
scr_fs_invalid_named_path(value, path, "path");
|
||
}
|
||
|
||
void scr_process_load_env_file(const ScrDyn *value) {
|
||
ScrStr *path;
|
||
if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) path = scr_str_new("./.env", 6);
|
||
else if (value->kind == SCR_DYN_STR) path = scr_str_retain(value->v.str);
|
||
else if (scr_dyn_native_url_is(value)) path = scr_url_to_path(value->v.handle.ptr);
|
||
else if (scr_dyn_bytes_is(value, SCR_BYTES_U8)) path = scr_bytes_string(value->v.bytes, 0, value->v.bytes->len);
|
||
else {
|
||
scr_dyn_arg_type_fail("path", "of type string or an instance of Buffer or URL", value);
|
||
return;
|
||
}
|
||
if (!path) return;
|
||
if (memchr(path->data, 0, path->len)) {
|
||
scr_fs_invalid_path(value, path);
|
||
scr_str_release(path);
|
||
return;
|
||
}
|
||
ScrStr *content = scr_fs_read_file_core(path, true);
|
||
scr_str_release(path);
|
||
if (!content) return;
|
||
ScrDyn *entries = scr_env_parse_content(content, false);
|
||
scr_str_release(content);
|
||
for (size_t i = 0; i < entries->v.obj.len; i++) {
|
||
const ScrDynEntry *entry = &entries->v.obj.entries[i];
|
||
/* Node normalizes each raw sorted entry immediately before checking
|
||
* the environment. Distinct invalid byte keys can normalize alike;
|
||
* the first absent name wins, rather than the last parsed entry. */
|
||
ScrStr *key = scr_str_from_utf8_lossy((const uint8_t *)entry->key, entry->key_len);
|
||
ScrStr *existing = scr_env_get(key);
|
||
if (!existing) {
|
||
const ScrStr *raw = entry->value->v.str;
|
||
ScrStr *value = scr_str_from_utf8_lossy((const uint8_t *)raw->data, raw->len);
|
||
scr_env_set(key, value);
|
||
scr_str_release(value);
|
||
}
|
||
scr_str_release(existing);
|
||
scr_str_release(key);
|
||
}
|
||
scr_dyn_release(entries);
|
||
}
|
||
|
||
/* The whole environment as one fresh string[] of alternating
|
||
* [k0, v0, k1, v1, ...] entries in environ order — the raw material of the
|
||
* compiler's process.env snapshot record (insertion order = environ order,
|
||
* which is Node's own Object.keys(process.env) order). Entries without '='
|
||
* (not producible by setenv) are skipped; the value is everything after
|
||
* the FIRST '='. +1 array. */
|
||
ScrArr *scr_env_pairs(void) {
|
||
#ifdef SCR_WORKERS
|
||
ScrArr *local = scr_context_env_pairs();
|
||
if (local) return local;
|
||
#endif
|
||
ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0);
|
||
#ifdef _WIN32
|
||
/* The WIN32 environment block (see scr_env_get): NUL-separated
|
||
* "K=V" entries, double-NUL terminated. Entries whose first byte is
|
||
* '=' are the hidden per-drive cwd variables ("=C:=..."), which libuv
|
||
* (and so Node's process.env) also skips. */
|
||
char *block = GetEnvironmentStringsA();
|
||
if (block != NULL) {
|
||
for (char *e = block; *e != '\0'; e += strlen(e) + 1) {
|
||
const char *eq = strchr(e, '=');
|
||
if (!eq || eq == e) continue;
|
||
scr_arr_push_ref(out, scr_str_new(e, (size_t)(eq - e)));
|
||
scr_arr_push_ref(out, scr_str_new(eq + 1, strlen(eq + 1)));
|
||
}
|
||
FreeEnvironmentStringsA(block);
|
||
}
|
||
#else
|
||
for (char **e = environ; *e != NULL; e++) {
|
||
const char *eq = strchr(*e, '=');
|
||
if (!eq || eq == *e) continue;
|
||
scr_arr_push_ref(out, scr_str_new(*e, (size_t)(eq - *e)));
|
||
scr_arr_push_ref(out, scr_str_new(eq + 1, strlen(eq + 1)));
|
||
}
|
||
#endif
|
||
return out;
|
||
}
|
||
|
||
/* ── process.pid / process.getuid / process.kill ─────────────────────── */
|
||
|
||
double scr_process_pid(void) { return (double)getpid(); }
|
||
|
||
#if defined(_WIN32) || defined(__wasi__)
|
||
/* No uids/gids exist on Windows: Node's process object simply has no
|
||
* getuid/getgid members there, so a call is the property-access TypeError
|
||
* below — thrown catchably, exactly what `process.getuid()` does under
|
||
* Windows Node. WASI has no uid/gid process model either. */
|
||
double scr_process_getuid(void) {
|
||
scr_throw_error_msg(SCR_ERR_TYPE, "process.getuid is not a function", 32);
|
||
return 0;
|
||
}
|
||
|
||
double scr_process_getgid(void) {
|
||
scr_throw_error_msg(SCR_ERR_TYPE, "process.getgid is not a function", 32);
|
||
return 0;
|
||
}
|
||
#else
|
||
double scr_process_getuid(void) { return (double)getuid(); }
|
||
|
||
double scr_process_getgid(void) { return (double)getgid(); }
|
||
#endif
|
||
|
||
/* Node's signal-name table (the names uv exposes), resolved to the HOST's
|
||
* numbers via the POSIX constants. Node accepts names with the SIG prefix
|
||
* only. On Windows the C runtime defines only the ANSI six (INT, ILL,
|
||
* ABRT, FPE, SEGV, TERM) plus SIGBREAK — libuv fills in the numbers below
|
||
* for the handful more it emulates, and Node's os.constants.signals shows
|
||
* exactly that union, so the same defines keep the two tables' Windows
|
||
* rows Node-identical; every other row #ifdefs away like SIGIO/SIGINFO
|
||
* always did. */
|
||
#ifdef _WIN32
|
||
#define SIGHUP 1 /* uv's emulated numbers (uv-win.h) */
|
||
#define SIGQUIT 3
|
||
#define SIGKILL 9
|
||
#define SIGWINCH 28
|
||
#endif
|
||
|
||
static int scr_signal_by_name(const char *name) {
|
||
static const struct { const char *name; int sig; } SIGS[] = {
|
||
{"SIGHUP", SIGHUP}, {"SIGINT", SIGINT}, {"SIGQUIT", SIGQUIT},
|
||
{"SIGILL", SIGILL}, {"SIGABRT", SIGABRT},
|
||
{"SIGIOT", SIGABRT}, {"SIGFPE", SIGFPE},
|
||
{"SIGKILL", SIGKILL}, {"SIGSEGV", SIGSEGV},
|
||
{"SIGTERM", SIGTERM}, {"SIGWINCH", SIGWINCH},
|
||
#ifdef SIGTRAP
|
||
{"SIGTRAP", SIGTRAP},
|
||
#endif
|
||
#ifdef SIGBUS
|
||
{"SIGBUS", SIGBUS},
|
||
#endif
|
||
#ifdef SIGUSR1
|
||
{"SIGUSR1", SIGUSR1}, {"SIGUSR2", SIGUSR2},
|
||
#endif
|
||
#ifdef SIGPIPE
|
||
{"SIGPIPE", SIGPIPE},
|
||
#endif
|
||
#ifdef SIGALRM
|
||
{"SIGALRM", SIGALRM},
|
||
#endif
|
||
#ifdef SIGCHLD
|
||
{"SIGCHLD", SIGCHLD}, {"SIGCONT", SIGCONT},
|
||
{"SIGSTOP", SIGSTOP}, {"SIGTSTP", SIGTSTP}, {"SIGTTIN", SIGTTIN},
|
||
{"SIGTTOU", SIGTTOU}, {"SIGURG", SIGURG}, {"SIGXCPU", SIGXCPU},
|
||
{"SIGXFSZ", SIGXFSZ}, {"SIGVTALRM", SIGVTALRM}, {"SIGPROF", SIGPROF},
|
||
{"SIGSYS", SIGSYS},
|
||
#endif
|
||
#ifdef SIGBREAK
|
||
{"SIGBREAK", SIGBREAK},
|
||
#endif
|
||
#ifdef SIGIO
|
||
{"SIGIO", SIGIO},
|
||
#endif
|
||
#ifdef SIGINFO
|
||
{"SIGINFO", SIGINFO},
|
||
#endif
|
||
};
|
||
for (size_t i = 0; i < sizeof SIGS / sizeof SIGS[0]; i++) {
|
||
if (strcmp(SIGS[i].name, name) == 0) return SIGS[i].sig;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* The table above for other units (scr_child.c's child.kill shares Node's
|
||
* one signal-name story): the resolved number, or -1 for unknown names. */
|
||
int scr_signal_from_name(const ScrStr *signal) {
|
||
return strlen(signal->data) == signal->len ? scr_signal_by_name(signal->data) : -1;
|
||
}
|
||
|
||
/* The reverse walk, for spawnSync's result.signal: the FIRST name with
|
||
* the number wins (SIGABRT precedes its SIGIOT alias — Node's spelling),
|
||
* NULL for numbers outside the table. Static storage; never freed. */
|
||
const char *scr_signal_name(int sig) {
|
||
static const struct { const char *name; int signo; } SIGS[] = {
|
||
{"SIGHUP", SIGHUP}, {"SIGINT", SIGINT}, {"SIGQUIT", SIGQUIT},
|
||
{"SIGILL", SIGILL},
|
||
#ifdef SIGTRAP
|
||
{"SIGTRAP", SIGTRAP},
|
||
#endif
|
||
{"SIGABRT", SIGABRT},
|
||
#ifdef SIGBUS
|
||
{"SIGBUS", SIGBUS},
|
||
#endif
|
||
{"SIGFPE", SIGFPE}, {"SIGKILL", SIGKILL},
|
||
#ifdef SIGUSR1
|
||
{"SIGUSR1", SIGUSR1},
|
||
#endif
|
||
{"SIGSEGV", SIGSEGV},
|
||
#ifdef SIGUSR2
|
||
{"SIGUSR2", SIGUSR2},
|
||
#endif
|
||
#ifdef SIGPIPE
|
||
{"SIGPIPE", SIGPIPE},
|
||
#endif
|
||
#ifdef SIGALRM
|
||
{"SIGALRM", SIGALRM},
|
||
#endif
|
||
{"SIGTERM", SIGTERM},
|
||
#ifdef SIGCHLD
|
||
{"SIGCHLD", SIGCHLD}, {"SIGCONT", SIGCONT}, {"SIGSTOP", SIGSTOP},
|
||
{"SIGTSTP", SIGTSTP}, {"SIGTTIN", SIGTTIN}, {"SIGTTOU", SIGTTOU},
|
||
{"SIGURG", SIGURG}, {"SIGXCPU", SIGXCPU}, {"SIGXFSZ", SIGXFSZ},
|
||
{"SIGVTALRM", SIGVTALRM}, {"SIGPROF", SIGPROF},
|
||
#endif
|
||
{"SIGWINCH", SIGWINCH},
|
||
#ifdef SIGSYS
|
||
{"SIGSYS", SIGSYS},
|
||
#endif
|
||
#ifdef SIGBREAK
|
||
{"SIGBREAK", SIGBREAK},
|
||
#endif
|
||
#ifdef SIGIO
|
||
{"SIGIO", SIGIO},
|
||
#endif
|
||
#ifdef SIGINFO
|
||
{"SIGINFO", SIGINFO},
|
||
#endif
|
||
};
|
||
for (size_t i = 0; i < sizeof SIGS / sizeof SIGS[0]; i++) {
|
||
if (SIGS[i].signo == sig) return SIGS[i].name;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* Node validates the pid as an int32 BEFORE kill(2) and throws the
|
||
* ERR_INVALID_ARG_TYPE TypeError with this exact (odd) wording. */
|
||
static bool scr_kill_pid_check(double pid) {
|
||
if (pid >= -2147483648.0 && pid <= 2147483647.0 && pid == (double)(long long)pid) {
|
||
return true; /* NaN fails both range comparisons */
|
||
}
|
||
char num[32];
|
||
scr_f64_to_str(pid, num);
|
||
char msg[96];
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The \"pid\" argument must be of type number. "
|
||
"Received type number (%s)",
|
||
num);
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
|
||
return false;
|
||
}
|
||
|
||
/* The shared kill(2) tail: signal 0 probes; failure throws Node's terse
|
||
* `kill ESRCH` / `kill EPERM` Error. Returns Node's constant true.
|
||
* Windows: uv_kill's behavior — signal 0 opens the process to probe
|
||
* liveness, anything else is TerminateProcess (no signal exists to
|
||
* deliver; the target dies with exit code 1, exactly Node-on-Windows's
|
||
* process.kill). ESRCH/EPERM map from the open failure. */
|
||
#ifdef _WIN32
|
||
static int scr_win_kill(int pid, int sig) {
|
||
HANDLE h = OpenProcess(
|
||
sig == 0 ? PROCESS_QUERY_LIMITED_INFORMATION : PROCESS_TERMINATE,
|
||
FALSE, (DWORD)pid);
|
||
if (h == NULL) {
|
||
errno = GetLastError() == ERROR_ACCESS_DENIED ? EPERM : ESRCH;
|
||
return -1;
|
||
}
|
||
BOOL ok = sig == 0 ? TRUE : TerminateProcess(h, 1);
|
||
CloseHandle(h);
|
||
if (!ok) {
|
||
errno = EPERM;
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
#define scr_sys_kill(pid, sig) scr_win_kill((int)(pid), (sig))
|
||
#elif defined(__wasi__)
|
||
static int scr_wasi_kill(int pid, int sig) {
|
||
(void)pid;
|
||
(void)sig;
|
||
errno = ENOSYS;
|
||
return -1;
|
||
}
|
||
#define scr_sys_kill(pid, sig) scr_wasi_kill((int)(pid), (sig))
|
||
#else
|
||
#define scr_sys_kill(pid, sig) kill((pid_t)(pid), (sig))
|
||
#endif
|
||
|
||
static bool scr_kill_send(int pid, int sig) {
|
||
if (scr_sys_kill(pid, sig) == 0) return true;
|
||
const char *name = errno == ESRCH ? "ESRCH"
|
||
: errno == EPERM ? "EPERM"
|
||
: errno == EINVAL ? "EINVAL"
|
||
: NULL;
|
||
char msg[32];
|
||
int len;
|
||
if (name) {
|
||
len = snprintf(msg, sizeof msg, "kill %s", name);
|
||
/* Node's errnoException carries code = the errno name. */
|
||
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, name);
|
||
} else {
|
||
len = snprintf(msg, sizeof msg, "kill E%d", errno);
|
||
scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)len);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool scr_process_kill(double pid, double signum) {
|
||
if (!scr_kill_pid_check(pid)) return false;
|
||
return scr_kill_send((int)pid, (int)signum);
|
||
}
|
||
|
||
bool scr_process_kill_named(double pid, const ScrStr *signal) {
|
||
if (!scr_kill_pid_check(pid)) return false;
|
||
int sig = scr_signal_by_name(signal->data);
|
||
if (sig < 0) {
|
||
/* Node's ERR_UNKNOWN_SIGNAL TypeError. */
|
||
size_t cap = 16 + signal->len + 1;
|
||
char *msg = malloc(cap);
|
||
if (!msg) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
int len = snprintf(msg, cap, "Unknown signal: %s", signal->data);
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
|
||
free(msg);
|
||
return false;
|
||
}
|
||
return scr_kill_send((int)pid, sig);
|
||
}
|
||
|
||
ScrStr *scr_process_cwd(void) {
|
||
#ifdef _WIN32
|
||
DWORD size = GetCurrentDirectoryW(0, NULL);
|
||
if (!size) scr_trap("scriptc: process.cwd() failed\n");
|
||
WCHAR *wide = malloc((size_t)size * sizeof *wide);
|
||
if (!wide) scr_trap("scriptc: out of memory\n");
|
||
DWORD length = GetCurrentDirectoryW(size, wide);
|
||
ScrStr *result = length > 0 && length < size ? scr_fs_win_utf8(wide, length) : NULL;
|
||
free(wide);
|
||
if (!result) scr_trap("scriptc: process.cwd() failed\n");
|
||
return result;
|
||
#else
|
||
char buf[4096];
|
||
if (!getcwd(buf, sizeof buf)) {
|
||
scr_trap("scriptc: process.cwd() failed\n");
|
||
}
|
||
return scr_str_new(buf, strlen(buf));
|
||
#endif
|
||
}
|
||
|
||
/* The raw byte writes use the SAME stdio stream as console, and each call
|
||
* flushes before returning so live consumers see Node's source order without
|
||
* an observable C buffering delay. The boolean is Node's backpressure signal
|
||
* — this synchronous runtime has no queued backpressure, so it is constantly
|
||
* true. */
|
||
bool scr_process_stdout_write(const ScrStr *data) {
|
||
return scr_stdio_write_str(1, data);
|
||
}
|
||
|
||
bool scr_process_stderr_write(const ScrStr *data) {
|
||
return scr_stdio_write_str(2, data);
|
||
}
|
||
|
||
/* The FIRST-CLASS stream write (`output.write(line)` where output is a
|
||
* WritableStream-typed value — the prefixStream idiom): the value is the
|
||
* stream's fd (1 or 2, minted by the process.stdout/stderr reads), and
|
||
* the write dispatches onto the exact stdout/stderr paths above so
|
||
* prompt submission and ordering stay identical. */
|
||
bool scr_proc_stream_write(double fd, const ScrStr *data) {
|
||
return (int)fd == 2 ? scr_process_stderr_write(data) : scr_process_stdout_write(data);
|
||
}
|
||
|
||
/* ── os ──────────────────────────────────────────────────────────────
|
||
* os.platform() lowers to scr_process_platform (one implementation).
|
||
* homedir/tmpdir follow libuv's POSIX rules, which Node delegates to.
|
||
*/
|
||
|
||
#ifdef _WIN32
|
||
/* uv_os_homedir on Windows: %USERPROFILE% first, then the profile
|
||
* directory API. The env var covers every real session; abort matches
|
||
* the POSIX arm's stance on the unreachable failure. */
|
||
ScrStr *scr_os_homedir(void) {
|
||
const char *home = scr_getenv("USERPROFILE");
|
||
if (home && home[0] != '\0') return scr_str_new(home, strlen(home));
|
||
scr_trap("scriptc: os.homedir() failed\n");
|
||
}
|
||
|
||
ScrStr *scr_os_user_name(void) {
|
||
char buf[UNLEN + 1];
|
||
DWORD n = sizeof buf;
|
||
if (!GetUserNameA(buf, &n) || n == 0) {
|
||
scr_trap("scriptc: os.userInfo() failed\n");
|
||
}
|
||
return scr_str_new(buf, n - 1); /* n counts the NUL */
|
||
}
|
||
|
||
ScrStr *scr_os_user_shell(void) {
|
||
/* Node's userInfo().shell is null on Windows; the scriptc surface types
|
||
* it string, so the closest honest spelling is "" (divergence noted in
|
||
* the windows report). */
|
||
return scr_str_new("", 0);
|
||
}
|
||
|
||
ScrStr *scr_os_user_homedir(void) {
|
||
return scr_os_homedir(); /* uv_os_get_passwd reuses uv_os_homedir on win */
|
||
}
|
||
|
||
ScrStr *scr_os_release(void) {
|
||
/* uv_os_uname on Windows: RtlGetVersion (the un-lied-to GetVersionEx),
|
||
* rendered "major.minor.build" — Node answers e.g. "10.0.26100". */
|
||
typedef LONG(WINAPI * RtlGetVersionFn)(PRTL_OSVERSIONINFOW);
|
||
RTL_OSVERSIONINFOW info;
|
||
memset(&info, 0, sizeof info);
|
||
info.dwOSVersionInfoSize = sizeof info;
|
||
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
|
||
RtlGetVersionFn fn =
|
||
ntdll ? (RtlGetVersionFn)(void *)GetProcAddress(ntdll, "RtlGetVersion") : NULL;
|
||
if (fn == NULL || fn(&info) != 0) {
|
||
scr_trap("scriptc: os.release() failed\n");
|
||
}
|
||
char buf[64];
|
||
int len = snprintf(buf, sizeof buf, "%lu.%lu.%lu", (unsigned long)info.dwMajorVersion,
|
||
(unsigned long)info.dwMinorVersion, (unsigned long)info.dwBuildNumber);
|
||
return scr_str_new(buf, (size_t)len);
|
||
}
|
||
|
||
ScrStr *scr_os_type(void) {
|
||
/* uv_os_uname's sysname on Windows is the constant "Windows_NT". */
|
||
return scr_str_new("Windows_NT", 10);
|
||
}
|
||
|
||
double scr_os_available_parallelism(void) {
|
||
/* uv_available_parallelism on Windows: GetSystemInfo's processor count. */
|
||
SYSTEM_INFO info;
|
||
GetSystemInfo(&info);
|
||
return info.dwNumberOfProcessors < 1 ? 1 : (double)info.dwNumberOfProcessors;
|
||
}
|
||
|
||
double scr_os_totalmem(void) {
|
||
MEMORYSTATUSEX ms;
|
||
memset(&ms, 0, sizeof ms);
|
||
ms.dwLength = sizeof ms;
|
||
if (!GlobalMemoryStatusEx(&ms)) return 0;
|
||
return (double)ms.ullTotalPhys;
|
||
}
|
||
|
||
ScrStr *scr_os_tmpdir(void) {
|
||
/* GetTempPathA is libuv's source (TMP → TEMP → USERPROFILE → windir),
|
||
* with Node's one-trailing-separator trim. */
|
||
char buf[MAX_PATH + 2];
|
||
DWORD n = GetTempPathA(sizeof buf, buf);
|
||
if (n == 0 || n >= sizeof buf) {
|
||
scr_trap("scriptc: os.tmpdir() failed\n");
|
||
}
|
||
size_t len = n;
|
||
if (len > 1 && (buf[len - 1] == '\\' || buf[len - 1] == '/')) len--;
|
||
return scr_str_new(buf, len);
|
||
}
|
||
#elif defined(__wasi__)
|
||
/* WASI has an inherited environment but no passwd database, uname, or
|
||
* physical-memory query. Keep the useful environment-backed answers and
|
||
* spell the platform explicitly for the rest. */
|
||
ScrStr *scr_os_homedir(void) {
|
||
const char *home = scr_getenv("HOME");
|
||
return scr_str_new(home ? home : "", home ? strlen(home) : 0);
|
||
}
|
||
ScrStr *scr_os_user_name(void) {
|
||
const char *user = scr_getenv("USER");
|
||
return scr_str_new(user ? user : "", user ? strlen(user) : 0);
|
||
}
|
||
ScrStr *scr_os_user_shell(void) { return scr_str_new("", 0); }
|
||
ScrStr *scr_os_user_homedir(void) { return scr_os_homedir(); }
|
||
ScrStr *scr_os_release(void) { return scr_str_new("", 0); }
|
||
ScrStr *scr_os_type(void) { return scr_str_new("WASI", 4); }
|
||
double scr_os_totalmem(void) { return 0; }
|
||
/* WASI modules run on one thread of execution. */
|
||
double scr_os_available_parallelism(void) { return 1; }
|
||
/* The guest temp namespace is stable across hosts. `scriptc run` preopens
|
||
* the host's /tmp at this path; other WASI hosts can provide the same
|
||
* capability without leaking a host-specific TMPDIR into the module. */
|
||
ScrStr *scr_os_tmpdir(void) { return scr_str_new("/tmp", 4); }
|
||
#else
|
||
ScrStr *scr_os_homedir(void) {
|
||
/* uv_os_homedir: $HOME when set (even empty is "set" only if non-NULL;
|
||
* libuv requires non-empty), else the passwd entry. */
|
||
const char *home = scr_getenv("HOME");
|
||
if (home && home[0] != '\0') return scr_str_new(home, strlen(home));
|
||
struct passwd pw;
|
||
struct passwd *result = NULL;
|
||
char buf[8192];
|
||
if (getpwuid_r(getuid(), &pw, buf, sizeof buf, &result) != 0 || !result || !result->pw_dir) {
|
||
scr_trap("scriptc: os.homedir() failed\n");
|
||
}
|
||
return scr_str_new(result->pw_dir, strlen(result->pw_dir));
|
||
}
|
||
|
||
/* The os.userInfo() field trio — uv_os_get_passwd's slices. One passwd
|
||
* lookup per call (three calls per userInfo record — cheap, no caching
|
||
* to invalidate). Failure aborts: Node throws a system error there, but
|
||
* no compiled program path reaches it for the running uid. */
|
||
static const struct passwd *scr_os_passwd(char *buf, size_t cap, struct passwd *pw) {
|
||
struct passwd *result = NULL;
|
||
if (getpwuid_r(getuid(), pw, buf, cap, &result) != 0 || !result) {
|
||
scr_trap("scriptc: os.userInfo() failed\n");
|
||
}
|
||
return result;
|
||
}
|
||
|
||
ScrStr *scr_os_user_name(void) {
|
||
struct passwd pw;
|
||
char buf[8192];
|
||
const struct passwd *r = scr_os_passwd(buf, sizeof buf, &pw);
|
||
return scr_str_new(r->pw_name, strlen(r->pw_name));
|
||
}
|
||
|
||
ScrStr *scr_os_user_shell(void) {
|
||
struct passwd pw;
|
||
char buf[8192];
|
||
const struct passwd *r = scr_os_passwd(buf, sizeof buf, &pw);
|
||
const char *sh = r->pw_shell ? r->pw_shell : "";
|
||
return scr_str_new(sh, strlen(sh));
|
||
}
|
||
|
||
ScrStr *scr_os_user_homedir(void) {
|
||
/* The PASSWD home (pw_dir) — Node's userInfo().homedir, distinct from
|
||
* os.homedir()'s $HOME-first cascade. */
|
||
struct passwd pw;
|
||
char buf[8192];
|
||
const struct passwd *r = scr_os_passwd(buf, sizeof buf, &pw);
|
||
return scr_str_new(r->pw_dir, strlen(r->pw_dir));
|
||
}
|
||
|
||
ScrStr *scr_os_release(void) {
|
||
/* uname(2)'s release field — Node's uv_os_uname()-backed answer. */
|
||
struct utsname u;
|
||
if (uname(&u) != 0) {
|
||
scr_trap("scriptc: os.release() failed\n");
|
||
}
|
||
return scr_str_new(u.release, strlen(u.release));
|
||
}
|
||
|
||
ScrStr *scr_os_type(void) {
|
||
/* uname(2)'s sysname field ("Darwin", "Linux") — Node's os.type(). */
|
||
struct utsname u;
|
||
if (uname(&u) != 0) {
|
||
scr_trap("scriptc: os.type() failed\n");
|
||
}
|
||
return scr_str_new(u.sysname, strlen(u.sysname));
|
||
}
|
||
|
||
#ifdef __linux__
|
||
/* libuv's uv__slurp: one read of at most cap - 1 bytes of path into buf
|
||
* (NUL-terminated); false when the file cannot be opened or read. A single
|
||
* read, like libuv, so a long /proc/self/cgroup is cut where Node cuts it. */
|
||
static bool scr_os_slurp(const char *path, char *buf, size_t cap) {
|
||
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||
if (fd < 0) return false;
|
||
ssize_t n;
|
||
do
|
||
n = read(fd, buf, cap - 1);
|
||
while (n < 0 && errno == EINTR);
|
||
close(fd);
|
||
if (n < 0) return false;
|
||
buf[n] = '\0';
|
||
return true;
|
||
}
|
||
|
||
/* libuv's uv__get_constrained_cpu (1.51): the CPU quota of the process's
|
||
* cgroup in whole CPUs. For cgroup v2 it walks cpu.max from the process's
|
||
* cgroup up to the root; like libuv, the last level with a numeric limit
|
||
* (the outermost) decides. Returns false when there is no answer. */
|
||
static bool scr_os_cgroup_cpu_quota(long long *quota) {
|
||
char cgroup[1024];
|
||
if (!scr_os_slurp("/proc/self/cgroup", cgroup, sizeof cgroup)) return false;
|
||
if (strncmp(cgroup, "0::/", 4) == 0) {
|
||
static const char mount[] = "/sys/fs/cgroup";
|
||
const char *trimmed = cgroup + 4;
|
||
int size = (int)strcspn(trimmed, "\n");
|
||
char path[256], file[256], buf[1024], value[16];
|
||
snprintf(path, sizeof path, "%s/%.*s/cgroup.controllers", mount, size, trimmed);
|
||
if (!scr_os_slurp(path, buf, sizeof buf)) return false;
|
||
snprintf(path, sizeof path, "%s/%.*s", mount, size, trimmed);
|
||
while (strncmp(path, mount, sizeof mount - 1) == 0) {
|
||
long long limit, period;
|
||
snprintf(file, sizeof file, "%s/cpu.max", path);
|
||
if (scr_os_slurp(file, value, sizeof value) && strncmp(value, "max", 3) != 0 &&
|
||
sscanf(value, "%lld %lld", &limit, &period) == 2 && period != 0)
|
||
*quota = limit / period;
|
||
char *slash = strrchr(path, '/');
|
||
if (slash == NULL || strcmp(path, mount) == 0) break;
|
||
*slash = '\0';
|
||
}
|
||
return true;
|
||
}
|
||
char *cpu = strstr(cgroup, ":cpu,");
|
||
if (cpu == NULL) return false;
|
||
cpu += sizeof ":cpu," - 1;
|
||
int size = (int)strcspn(cpu, "\n");
|
||
char path[256], buf[64];
|
||
long long per_period, period;
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/cpu.cfs_quota_us", size, cpu);
|
||
if (!scr_os_slurp(path, buf, sizeof buf) || sscanf(buf, "%lld", &per_period) != 1) return false;
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/cpu.cfs_period_us", size, cpu);
|
||
if (!scr_os_slurp(path, buf, sizeof buf) || sscanf(buf, "%lld", &period) != 1 || period == 0)
|
||
return false;
|
||
*quota = per_period / period;
|
||
return true;
|
||
}
|
||
#endif
|
||
|
||
double scr_os_available_parallelism(void) {
|
||
/* uv_available_parallelism (libuv 1.51, Node's os.availableParallelism):
|
||
* the CPUs this process may run on (Linux: its affinity mask, then any
|
||
* smaller cgroup CPU quota; Darwin: hw.activecpu, hw.logicalcpu, hw.ncpu),
|
||
* else the online processors; at least 1. */
|
||
long rc = -1;
|
||
#if defined(__linux__)
|
||
cpu_set_t set;
|
||
memset(&set, 0, sizeof set);
|
||
if (sched_getaffinity(0, sizeof set, &set) == 0) rc = CPU_COUNT(&set);
|
||
#elif defined(__APPLE__)
|
||
static const char *const mib[] = {"hw.activecpu", "hw.logicalcpu", "hw.ncpu"};
|
||
for (size_t i = 0; i < sizeof mib / sizeof mib[0]; i++) {
|
||
int nprocs = 0;
|
||
size_t len = sizeof nprocs;
|
||
if (sysctlbyname(mib[i], &nprocs, &len, NULL, 0) == 0 && len == sizeof nprocs && nprocs > 0) {
|
||
rc = nprocs;
|
||
break;
|
||
}
|
||
}
|
||
#endif
|
||
if (rc < 0) rc = sysconf(_SC_NPROCESSORS_ONLN);
|
||
#if defined(__linux__)
|
||
/* A quota below one CPU truncates to 0 and is ignored, not clamped to 1:
|
||
* libuv (1.51 through 1.52) applies only `quota > 0 && quota < rc`, so
|
||
* Node then answers the affinity count. */
|
||
long long quota = 0;
|
||
if (scr_os_cgroup_cpu_quota("a) && quota > 0 && quota < rc) rc = (long)quota;
|
||
#endif
|
||
return rc < 1 ? 1 : (double)rc;
|
||
}
|
||
|
||
double scr_os_totalmem(void) {
|
||
/* Total physical memory in bytes (sysconf pages × page size — Darwin
|
||
* and Linux both answer _SC_PHYS_PAGES). */
|
||
long pages = sysconf(_SC_PHYS_PAGES);
|
||
long psize = sysconf(_SC_PAGE_SIZE);
|
||
if (pages <= 0 || psize <= 0) return 0;
|
||
return (double)pages * (double)psize;
|
||
}
|
||
|
||
ScrStr *scr_os_tmpdir(void) {
|
||
/* Node's env cascade, with ONE trailing slash trimmed (never down to
|
||
* nothing: "/" stays "/"). */
|
||
const char *dir = scr_getenv("TMPDIR");
|
||
if (!dir || dir[0] == '\0') dir = scr_getenv("TMP");
|
||
if (!dir || dir[0] == '\0') dir = scr_getenv("TEMP");
|
||
if (!dir || dir[0] == '\0') dir = "/tmp";
|
||
size_t len = strlen(dir);
|
||
if (len > 1 && dir[len - 1] == '/') len--;
|
||
return scr_str_new(dir, len);
|
||
}
|
||
#endif /* _WIN32 */
|
||
|
||
/* ── os.networkInterfaces(): the getifaddrs(3) snapshot ────────────────
|
||
* Row selection and field semantics follow libuv (src/unix/bsd-ifaddrs.c),
|
||
* which Node delegates to: an entry contributes a row iff its interface is
|
||
* IFF_UP && IFF_RUNNING, its address is present, and its family is
|
||
* AF_INET/AF_INET6; `internal` is IFF_LOOPBACK; MACs come from the
|
||
* interface's link-level sibling entry (AF_LINK/AF_PACKET, matched by
|
||
* name), all-zeros when there is none; cidr is Node's lib/os.js
|
||
* computation — address/<contiguous netmask prefix>, null when the netmask
|
||
* is non-contiguous (never in practice). Row order is getifaddrs
|
||
* enumeration order, which is also Node's — but Node guarantees no order,
|
||
* so consumers should compare structurally. The emitter walks the snapshot
|
||
* through the accessors below and builds the typed record inline; a
|
||
* getifaddrs failure (effectively unreachable) yields an empty snapshot
|
||
* where Node would throw ERR_SYSTEM_ERROR. */
|
||
|
||
typedef struct ScrIfaddrRow {
|
||
char name[64];
|
||
char address[INET6_ADDRSTRLEN];
|
||
char netmask[INET6_ADDRSTRLEN];
|
||
char cidr[INET6_ADDRSTRLEN + 5]; /* address + "/128" */
|
||
bool has_cidr;
|
||
char mac[18]; /* "aa:bb:cc:dd:ee:ff" */
|
||
bool internal;
|
||
bool ipv6;
|
||
double scopeid;
|
||
} ScrIfaddrRow;
|
||
|
||
struct ScrIfaddrs {
|
||
size_t n;
|
||
ScrIfaddrRow *rows;
|
||
};
|
||
|
||
#ifdef _WIN32
|
||
/* The Windows arm mirrors libuv's uv_interface_addresses (src/win/util.c),
|
||
* which Node delegates to: GetAdaptersAddresses(AF_UNSPEC, INCLUDE_PREFIX
|
||
* + the SKIP_* flags libuv passes), an adapter contributes rows iff
|
||
* OperStatus == IfOperStatusUp and it has a unicast address, the row name
|
||
* is the adapter's FriendlyName (UTF-8), `internal` is the software-
|
||
* loopback interface type, MAC comes from PhysicalAddress (all-zeros when
|
||
* absent — the loopback), the netmask is built from each unicast
|
||
* address's OnLinkPrefixLength, and scopeid is the v6 sockaddr's. cidr is
|
||
* the shared prefix computation below (always contiguous here by
|
||
* construction). A snapshot failure yields the empty dict, the historical
|
||
* stance. */
|
||
ScrIfaddrs *scr_os_ifaddrs(void) {
|
||
ScrIfaddrs *s = calloc(1, sizeof *s);
|
||
if (!s) scr_trap("scriptc: out of memory\n");
|
||
ULONG flags = GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST |
|
||
GAA_FLAG_SKIP_DNS_SERVER;
|
||
ULONG size = 16 * 1024;
|
||
IP_ADAPTER_ADDRESSES *adapters = NULL;
|
||
for (int tries = 0; tries < 4; tries++) {
|
||
adapters = realloc(adapters, size);
|
||
if (!adapters) scr_trap("scriptc: out of memory\n");
|
||
ULONG rc = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, adapters, &size);
|
||
if (rc == ERROR_SUCCESS) break;
|
||
if (rc != ERROR_BUFFER_OVERFLOW) {
|
||
free(adapters);
|
||
s->rows = calloc(1, sizeof *s->rows);
|
||
if (!s->rows) scr_trap("scriptc: out of memory\n");
|
||
return s; /* empty snapshot */
|
||
}
|
||
}
|
||
size_t count = 0;
|
||
for (IP_ADAPTER_ADDRESSES *a = adapters; a != NULL; a = a->Next) {
|
||
if (a->OperStatus != IfOperStatusUp || a->FirstUnicastAddress == NULL) continue;
|
||
for (IP_ADAPTER_UNICAST_ADDRESS *u = a->FirstUnicastAddress; u != NULL; u = u->Next) {
|
||
int fam = u->Address.lpSockaddr->sa_family;
|
||
if (fam == AF_INET || fam == AF_INET6) count++;
|
||
}
|
||
}
|
||
s->rows = calloc(count ? count : 1, sizeof *s->rows);
|
||
if (!s->rows) scr_trap("scriptc: out of memory\n");
|
||
for (IP_ADAPTER_ADDRESSES *a = adapters; a != NULL; a = a->Next) {
|
||
if (a->OperStatus != IfOperStatusUp || a->FirstUnicastAddress == NULL) continue;
|
||
char name[64] = "";
|
||
WideCharToMultiByte(CP_UTF8, 0, a->FriendlyName, -1, name, sizeof name - 1, NULL, NULL);
|
||
char mac[18];
|
||
if (a->PhysicalAddressLength == 6) {
|
||
snprintf(mac, sizeof mac, "%02x:%02x:%02x:%02x:%02x:%02x", a->PhysicalAddress[0],
|
||
a->PhysicalAddress[1], a->PhysicalAddress[2], a->PhysicalAddress[3],
|
||
a->PhysicalAddress[4], a->PhysicalAddress[5]);
|
||
} else {
|
||
snprintf(mac, sizeof mac, "00:00:00:00:00:00");
|
||
}
|
||
bool internal = a->IfType == IF_TYPE_SOFTWARE_LOOPBACK;
|
||
for (IP_ADAPTER_UNICAST_ADDRESS *u = a->FirstUnicastAddress; u != NULL; u = u->Next) {
|
||
int fam = u->Address.lpSockaddr->sa_family;
|
||
if (fam != AF_INET && fam != AF_INET6) continue;
|
||
if (s->n == count) break; /* the topology raced the two passes */
|
||
ScrIfaddrRow *row = &s->rows[s->n++];
|
||
snprintf(row->name, sizeof row->name, "%s", name);
|
||
memcpy(row->mac, mac, sizeof mac);
|
||
row->internal = internal;
|
||
unsigned prefix = u->OnLinkPrefixLength;
|
||
if (fam == AF_INET6) {
|
||
const struct sockaddr_in6 *sa = (const struct sockaddr_in6 *)u->Address.lpSockaddr;
|
||
row->ipv6 = true;
|
||
row->scopeid = (double)sa->sin6_scope_id;
|
||
inet_ntop(AF_INET6, (void *)&sa->sin6_addr, row->address, sizeof row->address);
|
||
unsigned char mask[16];
|
||
if (prefix > 128) prefix = 128;
|
||
for (size_t i = 0; i < 16; i++) {
|
||
unsigned bits = prefix > 8 * i ? prefix - 8 * i : 0;
|
||
mask[i] = bits >= 8 ? 0xff : (unsigned char)(0xff00 >> bits);
|
||
}
|
||
inet_ntop(AF_INET6, mask, row->netmask, sizeof row->netmask);
|
||
row->has_cidr = true;
|
||
snprintf(row->cidr, sizeof row->cidr, "%s/%u", row->address, prefix);
|
||
} else {
|
||
const struct sockaddr_in *sa = (const struct sockaddr_in *)u->Address.lpSockaddr;
|
||
inet_ntop(AF_INET, (void *)&sa->sin_addr, row->address, sizeof row->address);
|
||
if (prefix > 32) prefix = 32;
|
||
uint32_t mask = prefix == 0 ? 0 : 0xffffffffu << (32 - prefix);
|
||
struct in_addr m;
|
||
m.s_addr = htonl(mask);
|
||
inet_ntop(AF_INET, (void *)&m, row->netmask, sizeof row->netmask);
|
||
row->has_cidr = true;
|
||
snprintf(row->cidr, sizeof row->cidr, "%s/%u", row->address, prefix);
|
||
}
|
||
}
|
||
}
|
||
free(adapters);
|
||
return s;
|
||
}
|
||
#else
|
||
/* libuv's uv__ifaddr_exclude for the address pass, plus the explicit
|
||
* INET/INET6 family filter (the only families Node's binding reports). */
|
||
static bool scr_ifaddr_row_ok(const struct ifaddrs *ent) {
|
||
if (!((ent->ifa_flags & IFF_UP) && (ent->ifa_flags & IFF_RUNNING))) return false;
|
||
if (ent->ifa_addr == NULL) return false;
|
||
int fam = ent->ifa_addr->sa_family;
|
||
return fam == AF_INET || fam == AF_INET6;
|
||
}
|
||
|
||
/* Node's getCIDR (lib/os.js): the netmask's contiguous 1-bit prefix.
|
||
* Returns -1 for a non-contiguous mask (cidr is then null). */
|
||
static int scr_netmask_prefix(const unsigned char *bytes, size_t len) {
|
||
int ones = 0;
|
||
bool zero_seen = false;
|
||
for (size_t i = 0; i < len; i++) {
|
||
unsigned char b = bytes[i];
|
||
for (int bit = 7; bit >= 0; bit--) {
|
||
if (b & (1u << bit)) {
|
||
if (zero_seen) return -1; /* a 1 after a 0: split mask */
|
||
ones++;
|
||
} else {
|
||
zero_seen = true;
|
||
}
|
||
}
|
||
}
|
||
return ones;
|
||
}
|
||
|
||
ScrIfaddrs *scr_os_ifaddrs(void) {
|
||
ScrIfaddrs *s = calloc(1, sizeof *s);
|
||
if (!s) scr_trap("scriptc: out of memory\n");
|
||
struct ifaddrs *addrs = NULL;
|
||
if (getifaddrs(&addrs) != 0) return s;
|
||
size_t count = 0;
|
||
for (struct ifaddrs *ent = addrs; ent != NULL; ent = ent->ifa_next) {
|
||
if (scr_ifaddr_row_ok(ent)) count++;
|
||
}
|
||
s->rows = calloc(count ? count : 1, sizeof *s->rows);
|
||
if (!s->rows) scr_trap("scriptc: out of memory\n");
|
||
for (struct ifaddrs *ent = addrs; ent != NULL; ent = ent->ifa_next) {
|
||
if (!scr_ifaddr_row_ok(ent)) continue;
|
||
ScrIfaddrRow *row = &s->rows[s->n++];
|
||
snprintf(row->name, sizeof row->name, "%s", ent->ifa_name);
|
||
snprintf(row->mac, sizeof row->mac, "00:00:00:00:00:00");
|
||
row->internal = (ent->ifa_flags & IFF_LOOPBACK) != 0;
|
||
unsigned char maskbytes[16];
|
||
size_t masklen = 0;
|
||
if (ent->ifa_addr->sa_family == AF_INET6) {
|
||
const struct sockaddr_in6 *sa = (const struct sockaddr_in6 *)ent->ifa_addr;
|
||
row->ipv6 = true;
|
||
row->scopeid = (double)sa->sin6_scope_id;
|
||
inet_ntop(AF_INET6, &sa->sin6_addr, row->address, sizeof row->address);
|
||
/* A NULL netmask stays zeroed, exactly libuv's memset — "::"/0. */
|
||
struct in6_addr mask;
|
||
memset(&mask, 0, sizeof mask);
|
||
if (ent->ifa_netmask != NULL) {
|
||
mask = ((const struct sockaddr_in6 *)ent->ifa_netmask)->sin6_addr;
|
||
}
|
||
inet_ntop(AF_INET6, &mask, row->netmask, sizeof row->netmask);
|
||
memcpy(maskbytes, &mask, 16);
|
||
masklen = 16;
|
||
} else {
|
||
const struct sockaddr_in *sa = (const struct sockaddr_in *)ent->ifa_addr;
|
||
inet_ntop(AF_INET, &sa->sin_addr, row->address, sizeof row->address);
|
||
struct in_addr mask;
|
||
memset(&mask, 0, sizeof mask);
|
||
if (ent->ifa_netmask != NULL) {
|
||
mask = ((const struct sockaddr_in *)ent->ifa_netmask)->sin_addr;
|
||
}
|
||
inet_ntop(AF_INET, &mask, row->netmask, sizeof row->netmask);
|
||
memcpy(maskbytes, &mask, 4);
|
||
masklen = 4;
|
||
}
|
||
int prefix = scr_netmask_prefix(maskbytes, masklen);
|
||
if (prefix >= 0) {
|
||
row->has_cidr = true;
|
||
snprintf(row->cidr, sizeof row->cidr, "%s/%d", row->address, prefix);
|
||
}
|
||
}
|
||
/* MAC pass: the link-level sibling entry, matched by interface name —
|
||
* every row of that interface gets its physical address. */
|
||
for (struct ifaddrs *ent = addrs; ent != NULL; ent = ent->ifa_next) {
|
||
if (!((ent->ifa_flags & IFF_UP) && (ent->ifa_flags & IFF_RUNNING))) continue;
|
||
if (ent->ifa_addr == NULL) continue;
|
||
const unsigned char *phys = NULL;
|
||
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
||
if (ent->ifa_addr->sa_family == AF_LINK) {
|
||
const struct sockaddr_dl *sdl = (const struct sockaddr_dl *)ent->ifa_addr;
|
||
if (sdl->sdl_alen == 6) phys = (const unsigned char *)LLADDR(sdl);
|
||
}
|
||
#elif defined(__linux__)
|
||
if (ent->ifa_addr->sa_family == AF_PACKET) {
|
||
const struct sockaddr_ll *sll = (const struct sockaddr_ll *)ent->ifa_addr;
|
||
if (sll->sll_halen == 6) phys = sll->sll_addr;
|
||
}
|
||
#endif
|
||
if (!phys) continue;
|
||
for (size_t i = 0; i < s->n; i++) {
|
||
if (strcmp(s->rows[i].name, ent->ifa_name) != 0) continue;
|
||
snprintf(s->rows[i].mac, sizeof s->rows[i].mac, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||
phys[0], phys[1], phys[2], phys[3], phys[4], phys[5]);
|
||
}
|
||
}
|
||
freeifaddrs(addrs);
|
||
return s;
|
||
}
|
||
#endif /* _WIN32 */
|
||
|
||
size_t scr_os_ifaddrs_count(const ScrIfaddrs *s) { return s->n; }
|
||
ScrStr *scr_os_ifaddrs_name(const ScrIfaddrs *s, size_t i) {
|
||
return scr_str_new(s->rows[i].name, strlen(s->rows[i].name));
|
||
}
|
||
ScrStr *scr_os_ifaddrs_address(const ScrIfaddrs *s, size_t i) {
|
||
return scr_str_new(s->rows[i].address, strlen(s->rows[i].address));
|
||
}
|
||
ScrStr *scr_os_ifaddrs_netmask(const ScrIfaddrs *s, size_t i) {
|
||
return scr_str_new(s->rows[i].netmask, strlen(s->rows[i].netmask));
|
||
}
|
||
ScrStr *scr_os_ifaddrs_family(const ScrIfaddrs *s, size_t i) {
|
||
return s->rows[i].ipv6 ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4);
|
||
}
|
||
ScrStr *scr_os_ifaddrs_mac(const ScrIfaddrs *s, size_t i) {
|
||
return scr_str_new(s->rows[i].mac, strlen(s->rows[i].mac));
|
||
}
|
||
bool scr_os_ifaddrs_internal(const ScrIfaddrs *s, size_t i) { return s->rows[i].internal; }
|
||
bool scr_os_ifaddrs_ipv6(const ScrIfaddrs *s, size_t i) { return s->rows[i].ipv6; }
|
||
/* +1 cidr string, or NULL for the null arm (split netmask). */
|
||
ScrStr *scr_os_ifaddrs_cidr(const ScrIfaddrs *s, size_t i) {
|
||
if (!s->rows[i].has_cidr) return NULL;
|
||
return scr_str_new(s->rows[i].cidr, strlen(s->rows[i].cidr));
|
||
}
|
||
double scr_os_ifaddrs_scopeid(const ScrIfaddrs *s, size_t i) { return s->rows[i].scopeid; }
|
||
void scr_os_ifaddrs_free(ScrIfaddrs *s) {
|
||
free(s->rows);
|
||
free(s);
|
||
}
|
||
|
||
/* The events-unit hooks (scr_events.c fills them at install; NULL in
|
||
* event-free binaries and in the standalone runtime C tests). */
|
||
SCR_TL void (*scr_process_exit_hook)(double code) = NULL;
|
||
SCR_TL void (*scr_warning_emit_hook)(const char *name, const char *code, ScrStr *message) = NULL;
|
||
SCR_TL void (*scr_stdin_destroy_hook)(void) = NULL;
|
||
|
||
void scr_process_set_exit_code(double code) {
|
||
if (!isfinite(code) || trunc(code) != code) {
|
||
char recv[48], msg[160];
|
||
scr_num_received(code, recv);
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"code\" is out of range. It must be an integer. Received %s", recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
if (fabs(code) > 9007199254740991.0) {
|
||
char recv[48], msg[192];
|
||
scr_num_received(code, recv);
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"code\" is out of range. It must be >= -9007199254740991 && <= 9007199254740991. Received %s", recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
/* Node converts valid safe integers to signed int32 for exitCode and
|
||
* exit listeners. The OS subsequently observes the low eight bits. */
|
||
double low = fmod(code, 4294967296.0);
|
||
if (low < 0) low += 4294967296.0;
|
||
int64_t signed_code = (int64_t)low;
|
||
if (signed_code > INT_MAX) signed_code -= 4294967296LL;
|
||
scr_exit_code_note((int)signed_code);
|
||
}
|
||
|
||
double scr_process_exit_code_or_zero(void) {
|
||
return (double)scr_exit_code_hint_get();
|
||
}
|
||
|
||
#if defined(__linux__)
|
||
#include <sched.h>
|
||
#include <signal.h>
|
||
#include <sys/syscall.h>
|
||
#ifndef SYS_close_range
|
||
#define SYS_close_range 436 /* the generic syscall table: x86_64 and aarch64 */
|
||
#endif
|
||
|
||
/* Detached teardown (`scriptc build --detached-exit`): the kernel frees an
|
||
* exiting process's memory on its critical path, 30-40 ms per GB of 4 KiB
|
||
* pages, before its parent sees it exit. A helper process that shares the
|
||
* address space (CLONE_VM) and holds no descriptor but the read end of a
|
||
* pipe keeps the memory alive past the exit, then exits itself and frees it
|
||
* off the parent's critical path (the latency mold's --fork hides). The
|
||
* kernel releases an exiting process's memory (exit_mm) before its files
|
||
* (exit_files), so EOF on the pipe means the helper holds the last
|
||
* reference. The helper keeps the executable mapped and the memory charged
|
||
* to the cgroup until then, so a program opts in at build time: the program
|
||
* object then defines scr_detached_exit = 1, overriding this weak default. */
|
||
__attribute__((weak)) const unsigned char scr_detached_exit = 0;
|
||
static char scr_teardown_stack[16384] __attribute__((aligned(64)));
|
||
|
||
static int scr_teardown_main(void *arg) {
|
||
int fd = (int)(intptr_t)arg;
|
||
/* Raw syscalls only: the helper runs on the exiting thread's TLS. */
|
||
if (fd > 0) (void)syscall(SYS_close_range, 0u, (unsigned)fd - 1u, 0u);
|
||
(void)syscall(SYS_close_range, (unsigned)fd + 1u, ~0u, 0u);
|
||
char c;
|
||
while (syscall(SYS_read, fd, &c, 1) != 0) {
|
||
}
|
||
syscall(SYS_exit, 0);
|
||
return 0;
|
||
}
|
||
|
||
static void scr_detach_teardown(void) {
|
||
if (!scr_detached_exit) return;
|
||
int fds[2];
|
||
if (pipe2(fds, O_CLOEXEC) != 0) return;
|
||
sigset_t all, old;
|
||
sigfillset(&all);
|
||
pthread_sigmask(SIG_SETMASK, &all, &old);
|
||
pid_t pid = clone(scr_teardown_main, scr_teardown_stack + sizeof scr_teardown_stack, CLONE_VM | SIGCHLD,
|
||
(void *)(intptr_t)fds[0]);
|
||
pthread_sigmask(SIG_SETMASK, &old, NULL);
|
||
(void)close(fds[0]);
|
||
if (pid < 0) (void)close(fds[1]);
|
||
/* fds[1] stays open until the process's files are released. */
|
||
}
|
||
#endif
|
||
|
||
void scr_process_exit(double code) {
|
||
/* Node runs 'exit' listeners on explicit process.exit() too — they run
|
||
* HERE, synchronously, before the teardown-free exit below. The hook is
|
||
* non-NULL only when the events unit is linked (scr_events.c). */
|
||
scr_process_in_exit = true;
|
||
if (scr_process_exit_hook != NULL) scr_process_exit_hook(code);
|
||
#ifdef SCR_WORKERS
|
||
if (!scr_context_is_main()) {
|
||
/* A nested exit or a failing exit listener already selected its code. */
|
||
if (!scr_context_stopping()) scr_context_stop((int)code);
|
||
return;
|
||
}
|
||
#endif
|
||
/* _Exit skips atexit handlers on purpose: no further code runs (matching
|
||
* Node), and the RC audit is meaningless mid-program (live values are
|
||
* expected). scr_init's flush-at-exit is also skipped — flush here. */
|
||
fflush(stdout);
|
||
#if defined(__linux__)
|
||
scr_detach_teardown();
|
||
#endif
|
||
_Exit((int)code);
|
||
}
|
||
|
||
/* ── the process introspection statics ────────────────────────────────
|
||
* process.uptime/cpuUsage/threadCpuUsage/resourceUsage/availableMemory/
|
||
* constrainedMemory — plain reads of the process's own clocks and
|
||
* counters in Node's units. uptime anchors at load time (a constructor-
|
||
* attribute monotonic stamp — the binary's own start, which is what
|
||
* "the current Node.js process" means for a compiled program). */
|
||
#ifdef _WIN32
|
||
#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)
|
||
/* The uptime anchor belongs to the host PROCESS, not to a library
|
||
* instance. InitOnce keeps the lazy Windows spelling race-free when
|
||
* several thread instances ask for the clock concurrently. */
|
||
static INIT_ONCE scr_uptime_once = INIT_ONCE_STATIC_INIT;
|
||
static double scr_uptime_t0_ms;
|
||
static BOOL CALLBACK scr_uptime_anchor_once(PINIT_ONCE once, PVOID param,
|
||
PVOID *ctx) {
|
||
(void)once;
|
||
(void)param;
|
||
(void)ctx;
|
||
scr_uptime_t0_ms = (double)GetTickCount64();
|
||
return TRUE;
|
||
}
|
||
static void scr_uptime_anchor_init(void) {
|
||
InitOnceExecuteOnce(&scr_uptime_once, scr_uptime_anchor_once, NULL, NULL);
|
||
}
|
||
#else
|
||
static SCR_TL double scr_uptime_t0_ms;
|
||
static void scr_uptime_anchor_init(void) { scr_uptime_t0_ms = (double)GetTickCount64(); }
|
||
#endif
|
||
static double scr_uptime_now_ms(void) { return (double)GetTickCount64(); }
|
||
#else
|
||
#include <sys/resource.h>
|
||
#include <sys/time.h>
|
||
#ifdef __APPLE__
|
||
#include <mach/mach.h>
|
||
#endif
|
||
/* A load-time, process-wide anchor: the constructor runs on only the
|
||
* initial thread, while thread-instanced archives are entered from worker
|
||
* threads whose TLS slots would otherwise stay zero. It is immutable once
|
||
* main starts, so sharing it adds no cross-instance mutable state. */
|
||
static double scr_uptime_t0_ms;
|
||
static double scr_uptime_now_ms(void) {
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
||
}
|
||
__attribute__((constructor)) static void scr_uptime_anchor_init(void) {
|
||
scr_uptime_t0_ms = scr_uptime_now_ms();
|
||
}
|
||
#endif
|
||
|
||
double scr_process_uptime(void) {
|
||
#if defined(_WIN32) && ((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS))
|
||
scr_uptime_anchor_init();
|
||
#elif defined(_WIN32)
|
||
if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
|
||
#endif
|
||
return (scr_uptime_now_ms() - scr_uptime_t0_ms) / 1000.0;
|
||
}
|
||
|
||
/* perf_hooks performance.now(): milliseconds since the process's own
|
||
* start (Node's timeOrigin anchor), fractional — the same monotonic
|
||
* clock and anchor as uptime, in Node's performance.now units. */
|
||
double scr_perf_now(void) {
|
||
#if defined(_WIN32) && ((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS))
|
||
scr_uptime_anchor_init();
|
||
#elif defined(_WIN32)
|
||
if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
|
||
#endif
|
||
return scr_uptime_now_ms() - scr_uptime_t0_ms;
|
||
}
|
||
|
||
uint64_t scr_hrtime_ns(void) {
|
||
#ifdef _WIN32
|
||
LARGE_INTEGER counter, frequency;
|
||
QueryPerformanceCounter(&counter);
|
||
QueryPerformanceFrequency(&frequency);
|
||
uint64_t whole = (uint64_t)(counter.QuadPart / frequency.QuadPart);
|
||
uint64_t remainder = (uint64_t)(counter.QuadPart % frequency.QuadPart);
|
||
return whole * UINT64_C(1000000000) + remainder * UINT64_C(1000000000) / (uint64_t)frequency.QuadPart;
|
||
#else
|
||
struct timespec time;
|
||
clock_gettime(CLOCK_MONOTONIC, &time);
|
||
return (uint64_t)time.tv_sec * UINT64_C(1000000000) + (uint64_t)time.tv_nsec;
|
||
#endif
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
/* GetProcessTimes/GetThreadTimes answer 100ns units; Node reports µs. */
|
||
static double scr_filetime_us(FILETIME ft) {
|
||
ULARGE_INTEGER v;
|
||
v.LowPart = ft.dwLowDateTime;
|
||
v.HighPart = ft.dwHighDateTime;
|
||
return (double)(v.QuadPart / 10);
|
||
}
|
||
|
||
/* A filesystem FILETIME is 100ns ticks since 1601. Match libuv's split into
|
||
* Unix seconds + nanoseconds before doing Node's millisecond arithmetic; the
|
||
* split matters for the last-bit rounding of dates far from the epoch. */
|
||
static double scr_filetime_unix_ms(FILETIME ft) {
|
||
ULARGE_INTEGER raw;
|
||
raw.LowPart = ft.dwLowDateTime;
|
||
raw.HighPart = ft.dwHighDateTime;
|
||
int64_t ticks = (int64_t)raw.QuadPart - INT64_C(116444736000000000);
|
||
int64_t sec = ticks / INT64_C(10000000);
|
||
int64_t rem = ticks % INT64_C(10000000);
|
||
if (rem < 0) {
|
||
sec--;
|
||
rem += INT64_C(10000000);
|
||
}
|
||
return (double)sec * 1000.0 + (double)rem / 10000.0;
|
||
}
|
||
double scr_cpu_user(void) {
|
||
FILETIME c, e, k, u;
|
||
if (!GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u)) return 0;
|
||
return scr_filetime_us(u);
|
||
}
|
||
double scr_cpu_system(void) {
|
||
FILETIME c, e, k, u;
|
||
if (!GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u)) return 0;
|
||
return scr_filetime_us(k);
|
||
}
|
||
double scr_thread_cpu_user(void) {
|
||
FILETIME c, e, k, u;
|
||
if (!GetThreadTimes(GetCurrentThread(), &c, &e, &k, &u)) return 0;
|
||
return scr_filetime_us(u);
|
||
}
|
||
double scr_thread_cpu_system(void) {
|
||
FILETIME c, e, k, u;
|
||
if (!GetThreadTimes(GetCurrentThread(), &c, &e, &k, &u)) return 0;
|
||
return scr_filetime_us(k);
|
||
}
|
||
#else
|
||
static double scr_tv_us(struct timeval tv) {
|
||
return (double)tv.tv_sec * 1e6 + (double)tv.tv_usec;
|
||
}
|
||
double scr_cpu_user(void) {
|
||
struct rusage ru;
|
||
if (getrusage(RUSAGE_SELF, &ru) != 0) return 0;
|
||
return scr_tv_us(ru.ru_utime);
|
||
}
|
||
double scr_cpu_system(void) {
|
||
struct rusage ru;
|
||
if (getrusage(RUSAGE_SELF, &ru) != 0) return 0;
|
||
return scr_tv_us(ru.ru_stime);
|
||
}
|
||
#if defined(RUSAGE_THREAD)
|
||
double scr_thread_cpu_user(void) {
|
||
struct rusage ru;
|
||
if (getrusage(RUSAGE_THREAD, &ru) != 0) return 0;
|
||
return scr_tv_us(ru.ru_utime);
|
||
}
|
||
double scr_thread_cpu_system(void) {
|
||
struct rusage ru;
|
||
if (getrusage(RUSAGE_THREAD, &ru) != 0) return 0;
|
||
return scr_tv_us(ru.ru_stime);
|
||
}
|
||
#elif defined(__APPLE__)
|
||
double scr_thread_cpu_user(void) {
|
||
thread_basic_info_data_t info;
|
||
mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
|
||
if (thread_info(mach_thread_self(), THREAD_BASIC_INFO, (thread_info_t)&info, &count) != KERN_SUCCESS) return 0;
|
||
return (double)info.user_time.seconds * 1e6 + (double)info.user_time.microseconds;
|
||
}
|
||
double scr_thread_cpu_system(void) {
|
||
thread_basic_info_data_t info;
|
||
mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
|
||
if (thread_info(mach_thread_self(), THREAD_BASIC_INFO, (thread_info_t)&info, &count) != KERN_SUCCESS) return 0;
|
||
return (double)info.system_time.seconds * 1e6 + (double)info.system_time.microseconds;
|
||
}
|
||
#else
|
||
/* No per-thread clock on this platform: the process clocks stand in (a
|
||
* single-threaded binary's thread IS the process). */
|
||
double scr_thread_cpu_user(void) { return scr_cpu_user(); }
|
||
double scr_thread_cpu_system(void) { return scr_cpu_system(); }
|
||
#endif
|
||
#endif
|
||
|
||
/* The prev-argument validation: Node checks prevValue.user then
|
||
* prevValue.system and throws the ERR_INVALID_ARG_VALUE RangeError with
|
||
* the received number, catchably. (The frontend guarantees numbers —
|
||
* non-number shapes keep compile fences.) */
|
||
static void scr_cpu_prev_check_field(const char *name, double v) {
|
||
if (v >= 0 && v <= 1.7976931348623157e308 && v == v) return; /* finite, non-negative */
|
||
char num[32];
|
||
size_t nlen = scr_f64_to_str(v, num);
|
||
num[nlen] = 0;
|
||
char msg[128];
|
||
int len = snprintf(msg, sizeof msg, "The property 'prevValue.%s' is invalid. Received %s", name, num);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_INVALID_ARG_VALUE");
|
||
}
|
||
|
||
void scr_cpu_prev_validate(double user, double system) {
|
||
scr_cpu_prev_check_field("user", user);
|
||
if (scr_exc_pending()) return;
|
||
scr_cpu_prev_check_field("system", system);
|
||
}
|
||
|
||
double scr_cpu_user_diff(double prev) { return scr_cpu_user() - prev; }
|
||
double scr_cpu_system_diff(double prev) { return scr_cpu_system() - prev; }
|
||
double scr_thread_cpu_user_diff(double prev) { return scr_thread_cpu_user() - prev; }
|
||
double scr_thread_cpu_system_diff(double prev) { return scr_thread_cpu_system() - prev; }
|
||
|
||
/* process.resourceUsage(): one field by canonical index — Node's names,
|
||
* order, and units (CPU times in µs; maxRSS in kilobytes — uv divides
|
||
* Darwin's bytes by 1024; the rest are getrusage's own counters, zero
|
||
* where the platform never fills them). */
|
||
double scr_process_rusage(double idx) {
|
||
#if defined(_WIN32) || defined(__wasi__)
|
||
/* uv fills only the CPU times and page-fault/maxRSS slice on Windows;
|
||
* everything else answers 0 — Node's own shape there. */
|
||
switch ((int)idx) {
|
||
case 0: return scr_cpu_user();
|
||
case 1: return scr_cpu_system();
|
||
default: return 0;
|
||
}
|
||
#else
|
||
struct rusage ru;
|
||
if (getrusage(RUSAGE_SELF, &ru) != 0) return 0;
|
||
switch ((int)idx) {
|
||
case 0: return scr_tv_us(ru.ru_utime); /* userCPUTime */
|
||
case 1: return scr_tv_us(ru.ru_stime); /* systemCPUTime */
|
||
case 2: /* maxRSS (kilobytes) */
|
||
#ifdef __APPLE__
|
||
return (double)(ru.ru_maxrss / 1024);
|
||
#else
|
||
return (double)ru.ru_maxrss;
|
||
#endif
|
||
case 3: return (double)ru.ru_ixrss; /* sharedMemorySize */
|
||
case 4: return (double)ru.ru_idrss; /* unsharedDataSize */
|
||
case 5: return (double)ru.ru_isrss; /* unsharedStackSize */
|
||
case 6: return (double)ru.ru_minflt; /* minorPageFault */
|
||
case 7: return (double)ru.ru_majflt; /* majorPageFault */
|
||
case 8: return (double)ru.ru_nswap; /* swappedOut */
|
||
case 9: return (double)ru.ru_inblock; /* fsRead */
|
||
case 10: return (double)ru.ru_oublock; /* fsWrite */
|
||
case 11: return (double)ru.ru_msgsnd; /* ipcSent */
|
||
case 12: return (double)ru.ru_msgrcv; /* ipcReceived */
|
||
case 13: return (double)ru.ru_nsignals; /* signalsCount */
|
||
case 14: return (double)ru.ru_nvcsw; /* voluntaryContextSwitches */
|
||
case 15: return (double)ru.ru_nivcsw; /* involuntaryContextSwitches */
|
||
default: return 0;
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/* process.availableMemory()/constrainedMemory() — libuv's numbers
|
||
* (uv_get_constrained_memory, uv_get_available_memory; libuv 1.51,
|
||
* src/unix/linux.c). On Linux the constraint is the process's own cgroup,
|
||
* named by /proc/self/cgroup: cgroup v2 reads memory.max and memory.high
|
||
* under /sys/fs/cgroup/<path>, v1 memory.limit_in_bytes and
|
||
* memory.soft_limit_in_bytes under /sys/fs/cgroup/memory/<path> (falling
|
||
* back to the controller's root). The answer is the smaller of the two;
|
||
* "max" (and v1's page-aligned LONG_MAX) reads as UINT64_MAX, an unreadable
|
||
* file as 0, which makes the answer 0. Available memory under a constraint
|
||
* at most the total is the constraint minus the cgroup's current usage;
|
||
* otherwise MemAvailable. */
|
||
#if defined(__linux__)
|
||
/* uv__slurp: up to len - 1 bytes, NUL-terminated; false when unreadable. */
|
||
static bool scr_uv_slurp(const char *path, char *buf, size_t len) {
|
||
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||
if (fd < 0) return false;
|
||
ssize_t n;
|
||
do n = read(fd, buf, len - 1);
|
||
while (n == -1 && errno == EINTR);
|
||
close(fd);
|
||
if (n < 0) return false;
|
||
buf[n] = 0;
|
||
return true;
|
||
}
|
||
|
||
/* uv__read_uint64. */
|
||
static uint64_t scr_uv_read_uint64(const char *path) {
|
||
char buf[32];
|
||
uint64_t v = 0;
|
||
if (scr_uv_slurp(path, buf, sizeof buf) && sscanf(buf, "%" SCNu64, &v) != 1 && strcmp(buf, "max\n") == 0)
|
||
v = UINT64_MAX;
|
||
return v;
|
||
}
|
||
|
||
/* uv__cgroup1_find_memory_controller: the memory controller's path without
|
||
* its leading slash, and its length in *n; NULL when there is none. */
|
||
static const char *scr_uv_cgroup1_memory(const char *buf, int *n) {
|
||
const char *p = strchr(buf, ':');
|
||
while (p != NULL && strncmp(p, ":memory:", 8) != 0) {
|
||
p = strchr(p, '\n');
|
||
if (p != NULL) p = strchr(p, ':');
|
||
}
|
||
if (p != NULL) {
|
||
p += strlen(":memory:/");
|
||
*n = (int)strcspn(p, "\n");
|
||
}
|
||
return p;
|
||
}
|
||
|
||
/* uv__get_cgroup_constrained_memory over /proc/self/cgroup's contents. */
|
||
static uint64_t scr_uv_cgroup_constrained(const char *buf) {
|
||
char path[4097];
|
||
uint64_t high = 0, max = 0;
|
||
int n = 0;
|
||
if (strncmp(buf, "0::/", 4) != 0) {
|
||
const char *p = scr_uv_cgroup1_memory(buf, &n);
|
||
bool found = false;
|
||
if (p != NULL) {
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/memory/%.*s/memory.soft_limit_in_bytes", n, p);
|
||
high = scr_uv_read_uint64(path);
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/memory/%.*s/memory.limit_in_bytes", n, p);
|
||
max = scr_uv_read_uint64(path);
|
||
found = high != 0 && max != 0;
|
||
}
|
||
if (!found) {
|
||
high = scr_uv_read_uint64("/sys/fs/cgroup/memory/memory.soft_limit_in_bytes");
|
||
max = scr_uv_read_uint64("/sys/fs/cgroup/memory/memory.limit_in_bytes");
|
||
}
|
||
uint64_t cgroup1_max = (uint64_t)(LONG_MAX & ~(sysconf(_SC_PAGESIZE) - 1));
|
||
if (high == cgroup1_max) high = UINT64_MAX;
|
||
if (max == cgroup1_max) max = UINT64_MAX;
|
||
} else {
|
||
const char *p = buf + 4;
|
||
n = (int)strcspn(p, "\n");
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/memory.max", n, p);
|
||
max = scr_uv_read_uint64(path);
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/memory.high", n, p);
|
||
high = scr_uv_read_uint64(path);
|
||
}
|
||
if (high == 0 || max == 0) return 0;
|
||
return high < max ? high : max;
|
||
}
|
||
|
||
/* uv__read_proc_meminfo: a /proc/meminfo field in bytes, or 0. */
|
||
static uint64_t scr_uv_meminfo(const char *what) {
|
||
char buf[4096];
|
||
if (!scr_uv_slurp("/proc/meminfo", buf, sizeof buf)) return 0;
|
||
const char *p = strstr(buf, what);
|
||
if (p == NULL) return 0;
|
||
uint64_t kb = 0;
|
||
sscanf(p + strlen(what), "%" SCNu64 " kB", &kb);
|
||
return kb * 1024;
|
||
}
|
||
#endif
|
||
|
||
double scr_constrained_memory(void) {
|
||
#if defined(__linux__)
|
||
char buf[1024];
|
||
if (!scr_uv_slurp("/proc/self/cgroup", buf, sizeof buf)) return 0;
|
||
return (double)scr_uv_cgroup_constrained(buf);
|
||
#else
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
double scr_available_memory(void) {
|
||
#if defined(_WIN32)
|
||
MEMORYSTATUSEX ms;
|
||
memset(&ms, 0, sizeof ms);
|
||
ms.dwLength = sizeof ms;
|
||
if (!GlobalMemoryStatusEx(&ms)) return 0;
|
||
return (double)ms.ullAvailPhys;
|
||
#elif defined(__APPLE__)
|
||
/* uv_get_available_memory falls back to the free-memory number on
|
||
* Darwin (vm_statistics' free pages). */
|
||
vm_statistics64_data_t vm;
|
||
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
|
||
if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&vm, &count) != KERN_SUCCESS) return 0;
|
||
return (double)vm.free_count * (double)vm_page_size;
|
||
#elif defined(__linux__)
|
||
/* uv_get_available_memory; its free-memory fallback is MemAvailable, then
|
||
* sysinfo's freeram. */
|
||
char buf[1024];
|
||
if (!scr_uv_slurp("/proc/self/cgroup", buf, sizeof buf)) return 0;
|
||
uint64_t free_memory = scr_uv_meminfo("MemAvailable:");
|
||
if (free_memory == 0) {
|
||
struct sysinfo info;
|
||
if (sysinfo(&info) == 0) free_memory = (uint64_t)info.freeram * info.mem_unit;
|
||
}
|
||
uint64_t constrained = scr_uv_cgroup_constrained(buf);
|
||
if (constrained == 0) return (double)free_memory;
|
||
uint64_t total = scr_uv_meminfo("MemTotal:");
|
||
if (total == 0) {
|
||
struct sysinfo info;
|
||
if (sysinfo(&info) == 0) total = (uint64_t)info.totalram * info.mem_unit;
|
||
}
|
||
if (constrained > total) return (double)free_memory;
|
||
char path[4097];
|
||
uint64_t current;
|
||
if (strncmp(buf, "0::/", 4) != 0) {
|
||
int n = 0;
|
||
const char *p = scr_uv_cgroup1_memory(buf, &n);
|
||
current = 0;
|
||
if (p != NULL) {
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/memory/%.*s/memory.usage_in_bytes", n, p);
|
||
current = scr_uv_read_uint64(path);
|
||
}
|
||
if (current == 0) current = scr_uv_read_uint64("/sys/fs/cgroup/memory/memory.usage_in_bytes");
|
||
} else {
|
||
snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/memory.current", (int)strcspn(buf + 4, "\n"), buf + 4);
|
||
current = scr_uv_read_uint64(path);
|
||
}
|
||
/* Usage can exceed the limit for short bursts. */
|
||
return constrained < current ? 0 : (double)(constrained - current);
|
||
#else
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
/* process._exiting — true once the exit sequence began (set above and by
|
||
* scr_run_exit_listeners in scr_events.c; the flag lives HERE so reading
|
||
* it never forces the events unit into the link). */
|
||
SCR_TL bool scr_process_in_exit = false;
|
||
|
||
bool scr_process_exiting(void) { return scr_process_in_exit; }
|
||
|
||
#ifdef SCR_WORKERS
|
||
/* Initialize before workers start. Reading through the cached process value
|
||
* avoids temporarily clearing the OS mask while another thread creates a file. */
|
||
static void scr_process_mask_init(void) {
|
||
#ifdef _WIN32
|
||
int current, ignored;
|
||
_umask_s(0, ¤t);
|
||
_umask_s(current, &ignored);
|
||
#else
|
||
unsigned current = umask(0);
|
||
umask((mode_t)current);
|
||
#endif
|
||
atomic_store_explicit(&scr_process_mask, (unsigned)current, memory_order_relaxed);
|
||
}
|
||
#endif
|
||
|
||
/* umask(2): mask < 0 reads without setting (set 0, restore — umask has no
|
||
* read-only form); otherwise sets and answers the previous mask. */
|
||
double scr_process_umask(double mask) {
|
||
#ifdef SCR_WORKERS
|
||
if (mask < 0) return atomic_load_explicit(&scr_process_mask, memory_order_relaxed);
|
||
if (!scr_context_is_main()) {
|
||
static const char message[] = "Setting process.umask() is not supported in workers";
|
||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_WORKER_UNSUPPORTED_OPERATION");
|
||
return 0;
|
||
}
|
||
atomic_store_explicit(&scr_process_mask, (unsigned)mask, memory_order_relaxed);
|
||
#endif
|
||
#ifdef _WIN32
|
||
/* Node on Windows accepts umask() calls; only the low bits matter. */
|
||
int prev;
|
||
if (mask < 0) {
|
||
_umask_s(0, &prev);
|
||
int ignored;
|
||
_umask_s(prev, &ignored);
|
||
} else {
|
||
_umask_s((int)mask, &prev);
|
||
}
|
||
return (double)prev;
|
||
#elif defined(__wasi__)
|
||
static SCR_TL mode_t current = 022;
|
||
mode_t prev = current;
|
||
if (mask >= 0) current = (mode_t)mask;
|
||
return (double)prev;
|
||
#else
|
||
mode_t prev;
|
||
if (mask < 0) {
|
||
prev = umask(0);
|
||
umask(prev);
|
||
} else {
|
||
prev = umask((mode_t)mask);
|
||
}
|
||
return (double)prev;
|
||
#endif
|
||
}
|
||
|
||
void scr_process_chdir(ScrStr *dir) {
|
||
#ifdef SCR_WORKERS
|
||
if (!scr_context_is_main()) {
|
||
static const char message[] = "process.chdir() is not supported in workers";
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_WORKER_UNSUPPORTED_OPERATION");
|
||
return;
|
||
}
|
||
#endif
|
||
#ifdef _WIN32
|
||
WCHAR *wide = scr_fs_win_wide(dir);
|
||
BOOL changed = wide && SetCurrentDirectoryW(wide);
|
||
DWORD error = changed ? ERROR_SUCCESS : GetLastError();
|
||
free(wide);
|
||
if (!changed) scr_fs_throw(scr_fs_win_errno(error), "chdir", dir);
|
||
#else
|
||
if (chdir(dir->data) != 0) scr_fs_throw(errno, "chdir", dir);
|
||
#endif
|
||
}
|
||
|
||
/* net's process-wide happy-eyeballs attempt budget (Node's
|
||
* getDefaultAutoSelectFamilyAttemptTimeout pair, default 250ms). Lives in
|
||
* the core unit so the knob never forces scr_net.c into the link. */
|
||
static SCR_TL double scr_net_autosel_timeout_ms = 250;
|
||
|
||
double scr_net_get_autosel_timeout(void) { return scr_net_autosel_timeout_ms; }
|
||
|
||
/* Node's setDefaultAutoSelectFamilyAttemptTimeout: validateInt32(value,
|
||
* 'value', 1), then the sub-10ms floor (Node clamps small budgets to
|
||
* 10ms). Throws ERR_OUT_OF_RANGE catchably. */
|
||
void scr_net_set_autosel_timeout(double ms) {
|
||
char recv[48], msg[160];
|
||
if (!(isfinite(ms) && trunc(ms) == ms)) {
|
||
scr_num_received(ms, recv);
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"value\" is out of range. It must be an integer. Received %s", recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
if (ms < 1 || ms > 2147483647.0) {
|
||
scr_num_received(ms, recv);
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"value\" is out of range. It must be >= 1 && <= 2147483647. Received %s", recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
scr_net_autosel_timeout_ms = ms < 10 ? 10 : ms;
|
||
}
|
||
|
||
/* ── fs error formatting ─────────────────────────────────────────────
|
||
* Node's fs errors read "<ERRNO>: <text>, <syscall> '<path>'"
|
||
* ("ENOENT: no such file or directory, open 'x'"). The common errnos get
|
||
* Node's (libuv's) exact lowercase text; anything exotic falls back to
|
||
* "E<num>: <strerror>, <op> '<path>'" — close enough, and the corpus can
|
||
* only observe messages through the runtime C tests anyway (the supported
|
||
* catch form is bindingless).
|
||
*/
|
||
|
||
static const char *scr_errno_name(int e, char *fallback, size_t cap) {
|
||
switch (e) {
|
||
case ENOENT: return "ENOENT";
|
||
case EEXIST: return "EEXIST";
|
||
case EACCES: return "EACCES";
|
||
case EBUSY: return "EBUSY";
|
||
case EINVAL: return "EINVAL";
|
||
case EIO: return "EIO";
|
||
case ENAMETOOLONG: return "ENAMETOOLONG";
|
||
case ENOMEM: return "ENOMEM";
|
||
case ENOSPC: return "ENOSPC";
|
||
case ENOTDIR: return "ENOTDIR";
|
||
case EISDIR: return "EISDIR";
|
||
case ENOTEMPTY: return "ENOTEMPTY";
|
||
case EPERM: return "EPERM";
|
||
case EROFS: return "EROFS";
|
||
case EXDEV: return "EXDEV";
|
||
case EBADF: return "EBADF";
|
||
case EAGAIN: return "EAGAIN";
|
||
case EFBIG: return "EFBIG";
|
||
case EPIPE: return "EPIPE";
|
||
case ESPIPE: return "ESPIPE";
|
||
case ELOOP: return "ELOOP";
|
||
case ENOSYS: return "ENOSYS";
|
||
default:
|
||
snprintf(fallback, cap, "E%d", e);
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
static const char *scr_errno_text(int e) {
|
||
switch (e) {
|
||
case ENOENT: return "no such file or directory";
|
||
case EEXIST: return "file already exists";
|
||
case EACCES: return "permission denied";
|
||
case EBUSY: return "resource busy or locked";
|
||
case EINVAL: return "invalid argument";
|
||
case EIO: return "i/o error";
|
||
case ENAMETOOLONG: return "name too long";
|
||
case ENOMEM: return "not enough memory";
|
||
case ENOSPC: return "no space left on device";
|
||
case ENOTDIR: return "not a directory";
|
||
case EISDIR: return "illegal operation on a directory";
|
||
case ENOTEMPTY: return "directory not empty";
|
||
case EPERM: return "operation not permitted";
|
||
case EROFS: return "read-only file system";
|
||
case EXDEV: return "cross-device link not permitted";
|
||
case EBADF: return "bad file descriptor";
|
||
case EAGAIN: return "resource temporarily unavailable";
|
||
case EFBIG: return "file too large";
|
||
case EPIPE: return "broken pipe";
|
||
case ESPIPE: return "invalid seek";
|
||
case ELOOP: return "too many levels of symbolic links";
|
||
case ENOSYS: return "function not implemented";
|
||
default: return strerror(e);
|
||
}
|
||
}
|
||
|
||
/* Node on WINDOWS reports fs error paths ABSOLUTIZED and backslashed
|
||
* (its fs binding hands the Windows API namespaced absolute paths and the
|
||
* error keeps that spelling): open("no.bin") fails with "... open
|
||
* 'C:\cwd\no.bin'". _fullpath reproduces exactly that resolution. POSIX
|
||
* Node reports the path as given — the passthrough arm. */
|
||
#ifdef _WIN32
|
||
static const char *scr_fs_err_path(const ScrStr *path, char buf[PATH_MAX]) {
|
||
return _fullpath(buf, path->data, PATH_MAX) != NULL ? buf : path->data;
|
||
}
|
||
#else
|
||
static const char *scr_fs_err_path(const ScrStr *path, char buf[PATH_MAX]) {
|
||
(void)buf;
|
||
return path->data;
|
||
}
|
||
#endif
|
||
|
||
/* Exported (scr_runtime.h): scr_bytes.c's fs Buffer forms share it. */
|
||
/* Keep metadata on the error itself until it crosses a checked-value
|
||
* boundary. Failed operations do not populate the process identity cache. */
|
||
static void scr_fs_throw_details(int e, const char *name, const char *message, size_t length,
|
||
const char *op, const ScrStr *path, const ScrStr *dest) {
|
||
double number = e > 0 ? -e : e;
|
||
#define SCR_FS_ERRNO_VALUE(code, text) if (strcmp(name, #code) == 0) number = SCR_NODE_UV__##code;
|
||
SCR_NODE_UV_ERRNO_MAP(SCR_FS_ERRNO_VALUE)
|
||
#undef SCR_FS_ERRNO_VALUE
|
||
ScrStr *text = scr_str_new(message, length);
|
||
ScrError *error = scr_error_new(SCR_ERR_ERROR, text);
|
||
scr_str_release(text);
|
||
scr_error_set_code(error, name);
|
||
error->system_errno = number;
|
||
error->system_call = scr_str_new(op, strlen(op));
|
||
error->system_path = path ? scr_str_retain((ScrStr *)path) : NULL;
|
||
error->system_dest = dest ? scr_str_retain((ScrStr *)dest) : NULL;
|
||
scr_throw_obj(error, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||
}
|
||
|
||
void scr_fs_throw(int e, const char *op, const ScrStr *path) {
|
||
#ifdef _WIN32
|
||
/* The CRT lands ERROR_ACCESS_DENIED in errno as EACCES; libuv's
|
||
* uv_translate_sys_error maps the same Win32 error to EPERM, so that
|
||
* is the code Node throws (a read-only file's write open, chmod on a
|
||
* held file). Translate at the throw seam so every fs op agrees with
|
||
* the Windows oracle. */
|
||
if (e == EACCES) e = EPERM;
|
||
#endif
|
||
char namebuf[16];
|
||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||
const char *text = scr_errno_text(e);
|
||
char pathbuf[PATH_MAX];
|
||
const char *shown = path ? scr_fs_err_path(path, pathbuf) : "";
|
||
size_t cap = strlen(name) + strlen(text) + strlen(op) + strlen(shown) + 8;
|
||
char *msg = malloc(cap);
|
||
if (!msg) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
int len = path
|
||
? snprintf(msg, cap, "%s: %s, %s '%s'", name, text, op, shown)
|
||
: snprintf(msg, cap, "%s: %s, %s", name, text, op);
|
||
scr_fs_throw_details(e, name, msg, (size_t)len, op, path, NULL);
|
||
free(msg);
|
||
}
|
||
|
||
/* ── fs operations ───────────────────────────────────────────────────── */
|
||
|
||
static FILE *scr_fs_fopen(const ScrStr *path, const char *mode) {
|
||
#ifdef _WIN32
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) { errno = scr_fs_win_errno(GetLastError()); return NULL; }
|
||
WCHAR wide_mode[4] = {0};
|
||
size_t length = strlen(mode);
|
||
if (length >= 4) scr_trap("scriptc: invalid internal file mode\n");
|
||
for (size_t i = 0; i < length; i++) wide_mode[i] = (WCHAR)mode[i];
|
||
FILE *file = _wfopen(wide, wide_mode);
|
||
int error = errno;
|
||
free(wide);
|
||
errno = error;
|
||
return file;
|
||
#else
|
||
return fopen(path->data, mode);
|
||
#endif
|
||
}
|
||
|
||
/* File sizes are hints, never EOF. Pseudo-files, pipes and concurrently
|
||
* changing files still read to EOF; descriptor reads keep their position
|
||
* and ownership. Limit individual reads for the CRT and small ssize_t. */
|
||
typedef struct {
|
||
ScrStr *text;
|
||
uint8_t *bytes;
|
||
size_t len, cap;
|
||
} ScrFileRead;
|
||
|
||
static size_t scr_file_read_hint(FILE *stream, int fd) {
|
||
if (stream) fd = fileno(stream);
|
||
#ifdef _WIN32
|
||
struct _stat64 st;
|
||
if (_fstat64(fd, &st) != 0 || !S_ISREG(st.st_mode) || st.st_size <= 0) return 4096;
|
||
int64_t position = stream ? 0 : _lseeki64(fd, 0, SEEK_CUR);
|
||
#else
|
||
struct stat st;
|
||
if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) || st.st_size <= 0) return 4096;
|
||
off_t position = stream ? 0 : lseek(fd, 0, SEEK_CUR);
|
||
#endif
|
||
if (position < 0 || position >= st.st_size) return 4096;
|
||
uintmax_t remaining = (uintmax_t)st.st_size - (uintmax_t)position;
|
||
/* A bounded initial allocation also tolerates sparse files that shrink
|
||
* between metadata and the first read without reserving their full size. */
|
||
size_t limit = 64 * 1024 * 1024;
|
||
return remaining > limit ? limit : (size_t)remaining;
|
||
}
|
||
|
||
static uint8_t *scr_file_read_data(ScrFileRead *out) {
|
||
return out->text ? (uint8_t *)out->text->data : out->bytes;
|
||
}
|
||
|
||
static void scr_file_read_grow(ScrFileRead *out) {
|
||
size_t limit = SIZE_MAX - sizeof(ScrStr) - 1;
|
||
if (out->cap >= limit) scr_trap("scriptc: file too large\n");
|
||
size_t cap = out->cap > limit / 2 ? limit : out->cap * 2;
|
||
if (out->text) {
|
||
out->text->len = out->len;
|
||
out->text = scr_str_regrow(out->text, cap);
|
||
} else {
|
||
uint8_t *grown = realloc(out->bytes, cap);
|
||
if (!grown) scr_trap("scriptc: out of memory\n");
|
||
out->bytes = grown;
|
||
}
|
||
out->cap = cap;
|
||
}
|
||
|
||
/* Fill the final allocation. At a full capacity, probe one byte before
|
||
* growing so an exact-size regular file never doubles its retained space. */
|
||
static int scr_file_read_all(FILE *stream, int fd, bool text, ScrFileRead *out) {
|
||
*out = (ScrFileRead){0};
|
||
out->cap = scr_file_read_hint(stream, fd);
|
||
if (text) {
|
||
out->text = scr_str_alloc_raw(0, out->cap);
|
||
out->cap = out->text->cap;
|
||
out->text->data[0] = '\0';
|
||
} else {
|
||
out->bytes = malloc(out->cap);
|
||
if (!out->bytes) scr_trap("scriptc: out of memory\n");
|
||
}
|
||
for (;;) {
|
||
uint8_t extra;
|
||
bool full = out->len == out->cap;
|
||
size_t room = full ? 1 : out->cap - out->len;
|
||
if (room > 512 * 1024) room = 512 * 1024;
|
||
uint8_t *dest = full ? &extra : scr_file_read_data(out) + out->len;
|
||
size_t count;
|
||
int error = 0;
|
||
if (stream) {
|
||
count = fread(dest, 1, room, stream);
|
||
if (ferror(stream)) error = errno ? errno : EIO;
|
||
} else {
|
||
ssize_t n = read(fd, dest, room);
|
||
if (n < 0) { count = 0; error = errno; }
|
||
else count = (size_t)n;
|
||
}
|
||
if (count) {
|
||
if (full) {
|
||
scr_file_read_grow(out);
|
||
scr_file_read_data(out)[out->len] = extra;
|
||
}
|
||
out->len += count;
|
||
}
|
||
if (error == EINTR) {
|
||
if (stream) clearerr(stream);
|
||
continue;
|
||
}
|
||
if (error) {
|
||
/* Raw strings must be safe for the normal cache-aware destructor. */
|
||
if (out->text) { out->text->len = out->len; out->text->data[out->len] = '\0'; }
|
||
scr_str_release(out->text);
|
||
free(out->bytes);
|
||
*out = (ScrFileRead){0};
|
||
return error;
|
||
}
|
||
if (!count) break;
|
||
}
|
||
if (out->text) { out->text->len = out->len; out->text->data[out->len] = '\0'; }
|
||
// Do not retain a large hint after truncation or a tiny pipe result.
|
||
if (out->cap > 256 && out->len < out->cap / 2) {
|
||
if (out->text) out->text = scr_str_regrow(out->text, out->len);
|
||
else {
|
||
uint8_t *trimmed = realloc(out->bytes, out->len ? out->len : 1);
|
||
if (trimmed) out->bytes = trimmed;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static ScrStr *scr_fs_read_file_core(ScrStr *path, bool env_file) {
|
||
FILE *f = scr_fs_fopen(path, "rb");
|
||
if (!f) {
|
||
scr_fs_throw(errno, "open", path);
|
||
return NULL;
|
||
}
|
||
ScrFileRead out;
|
||
int error = scr_file_read_all(f, -1, true, &out);
|
||
fclose(f);
|
||
if (error) {
|
||
if (env_file) {
|
||
ScrJsonBuf b;
|
||
scr_jb_init(&b);
|
||
scr_jb_puts(&b, "Contents of '");
|
||
scr_jb_put_str(&b, path);
|
||
scr_jb_puts(&b, "' should be a valid string.");
|
||
ScrStr *message = scr_jb_finish(&b);
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_TYPE");
|
||
scr_str_release(message);
|
||
} else scr_fs_throw(error, "read", path);
|
||
return NULL;
|
||
}
|
||
return out.text;
|
||
}
|
||
|
||
ScrBytes *scr_fs_read_file_bytes(ScrStr *path) {
|
||
FILE *f = scr_fs_fopen(path, "rb");
|
||
if (!f) { scr_fs_throw(errno, "open", path); return NULL; }
|
||
ScrFileRead out;
|
||
int error = scr_file_read_all(f, -1, false, &out);
|
||
fclose(f);
|
||
if (error) { scr_fs_throw(error, "read", path); return NULL; }
|
||
return scr_bytes_take_data(out.bytes, out.len);
|
||
}
|
||
|
||
ScrStr *scr_fs_read_file(ScrStr *path) {
|
||
return scr_fs_read_file_core(path, false);
|
||
}
|
||
|
||
static ScrStr *scr_fs_realpath_common(ScrStr *path, const char *op) {
|
||
#ifdef _WIN32
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) {
|
||
scr_fs_throw(scr_fs_win_errno(GetLastError()), op, path);
|
||
return NULL;
|
||
}
|
||
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES,
|
||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||
free(wide);
|
||
if (handle == INVALID_HANDLE_VALUE) {
|
||
scr_fs_throw(scr_fs_win_errno(GetLastError()), op, path);
|
||
return NULL;
|
||
}
|
||
DWORD needed = GetFinalPathNameByHandleW(handle, NULL, 0, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
|
||
if (needed == 0) {
|
||
DWORD error = GetLastError();
|
||
CloseHandle(handle);
|
||
scr_fs_throw(scr_fs_win_errno(error), op, path);
|
||
return NULL;
|
||
}
|
||
WCHAR *resolved = malloc(((size_t)needed + 1) * sizeof *resolved);
|
||
if (!resolved) {
|
||
CloseHandle(handle);
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
DWORD length = GetFinalPathNameByHandleW(handle, resolved, needed + 1,
|
||
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
|
||
DWORD error = length == 0 ? GetLastError() : length > needed ? ERROR_INSUFFICIENT_BUFFER : ERROR_SUCCESS;
|
||
CloseHandle(handle);
|
||
if (error != ERROR_SUCCESS) {
|
||
free(resolved);
|
||
scr_fs_throw(scr_fs_win_errno(error), op, path);
|
||
return NULL;
|
||
}
|
||
size_t prefix = 0;
|
||
bool unc = false;
|
||
if (length >= 8 && wcsncmp(resolved, L"\\\\?\\UNC\\", 8) == 0) {
|
||
prefix = 8;
|
||
unc = true;
|
||
} else if (length >= 4 && wcsncmp(resolved, L"\\\\?\\", 4) == 0) {
|
||
prefix = 4;
|
||
}
|
||
int utf8_len = WideCharToMultiByte(CP_UTF8, 0, resolved + prefix,
|
||
(int)(length - prefix), NULL, 0, NULL, NULL);
|
||
if (utf8_len <= 0) {
|
||
error = GetLastError();
|
||
free(resolved);
|
||
scr_fs_throw(scr_fs_win_errno(error), op, path);
|
||
return NULL;
|
||
}
|
||
size_t extra = unc ? 2 : 0;
|
||
char *utf8 = malloc(extra + (size_t)utf8_len);
|
||
if (!utf8) {
|
||
free(resolved);
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
if (unc) { utf8[0] = '\\'; utf8[1] = '\\'; }
|
||
(void)WideCharToMultiByte(CP_UTF8, 0, resolved + prefix, (int)(length - prefix),
|
||
utf8 + extra, utf8_len, NULL, NULL);
|
||
ScrStr *result = scr_str_new(utf8, extra + (size_t)utf8_len);
|
||
free(utf8);
|
||
free(resolved);
|
||
return result;
|
||
#else
|
||
char buf[PATH_MAX];
|
||
if (realpath(path->data, buf) == NULL) {
|
||
scr_fs_throw(errno, op, path);
|
||
return NULL;
|
||
}
|
||
return scr_str_new(buf, strlen(buf));
|
||
#endif
|
||
}
|
||
|
||
ScrStr *scr_fs_realpath(ScrStr *path) {
|
||
return scr_fs_realpath_common(path, "lstat");
|
||
}
|
||
|
||
ScrStr *scr_fs_realpath_promise(ScrStr *path) {
|
||
return scr_fs_realpath_common(path, "realpath");
|
||
}
|
||
|
||
static void scr_fs_write_common(ScrStr *path, ScrStr *data, const char *mode) {
|
||
FILE *f = scr_fs_fopen(path, mode);
|
||
if (!f) {
|
||
scr_fs_throw(errno, "open", path);
|
||
return;
|
||
}
|
||
char *owned;
|
||
const char *bytes = scr_str_utf8(data, &owned);
|
||
if (data->len > 0 && fwrite(bytes, 1, data->len, f) != data->len) {
|
||
int e = errno;
|
||
fclose(f);
|
||
scr_str_utf8_end(owned);
|
||
scr_fs_throw(e, "write", path);
|
||
return;
|
||
}
|
||
scr_str_utf8_end(owned);
|
||
if (fclose(f) != 0) scr_fs_throw(errno, "close", path);
|
||
}
|
||
|
||
void scr_fs_write_file(ScrStr *path, ScrStr *data) {
|
||
scr_fs_write_common(path, data, "wb");
|
||
}
|
||
|
||
/* writeFileSync(p, data, { mode }): the mode is open(2)'s O_CREAT
|
||
* argument — it applies at CREATION only (umask applying), and an
|
||
* existing file keeps its permissions, exactly Node (which never chmods
|
||
* here). Same error shapes as the plain form. */
|
||
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
|
||
char recv[48], msg[176];
|
||
scr_num_received(mode, recv);
|
||
if (!(isfinite(mode) && trunc(mode) == mode)) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"mode\" is out of range. It must be an integer. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
if (mode < 0 || mode > 4294967295.0) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
/* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
|
||
* \n in these byte-exact writes (fopen's "wb" path already does). */
|
||
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, (mode_t)mode);
|
||
if (fd < 0) {
|
||
scr_fs_throw(errno, "open", path);
|
||
return;
|
||
}
|
||
char *owned;
|
||
const char *bytes = scr_str_utf8(data, &owned);
|
||
size_t at = 0;
|
||
while (at < data->len) {
|
||
ssize_t wrote = write(fd, bytes + at, data->len - at);
|
||
if (wrote < 0) {
|
||
if (errno == EINTR) continue;
|
||
int e = errno;
|
||
close(fd);
|
||
scr_str_utf8_end(owned);
|
||
scr_fs_throw(e, "write", path);
|
||
return;
|
||
}
|
||
at += (size_t)wrote;
|
||
}
|
||
scr_str_utf8_end(owned);
|
||
if (close(fd) != 0) scr_fs_throw(errno, "close", path);
|
||
}
|
||
|
||
void scr_fs_append_file(ScrStr *path, ScrStr *data) {
|
||
scr_fs_write_common(path, data, "ab");
|
||
}
|
||
|
||
bool scr_fs_exists(ScrStr *path) {
|
||
/* Like Node's existsSync: any failure (missing, EACCES on a parent, ...)
|
||
* is simply false — never a throw. */
|
||
return access(path->data, F_OK) == 0;
|
||
}
|
||
|
||
void scr_fs_mkdir(ScrStr *path) {
|
||
if (scr_sys_mkdir(path->data, 0777) != 0) scr_fs_throw(errno, "mkdir", path);
|
||
}
|
||
|
||
/* mkdirSync(p, { mode }) non-recursive: mkdir(2) with the explicit mode
|
||
* (umask applies, exactly as in Node — mode is the syscall argument;
|
||
* Windows has no directory modes and drops it, like Node there). */
|
||
void scr_fs_mkdir_mode(ScrStr *path, double mode) {
|
||
if (scr_sys_mkdir(path->data, (mode_t)mode) != 0) scr_fs_throw(errno, "mkdir", path);
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
/* The CRT's stat follows junctions. Recognize real link reparse records
|
||
* before unlink/rm dispatch so a tree walk never descends into their targets.
|
||
* Return 1 for a removed link, 0 for an ordinary entry, -1 on failure. */
|
||
static int scr_fs_win_remove_link(const char *path, size_t len) {
|
||
ScrStr *text = scr_str_new(path, len);
|
||
WCHAR *wide = scr_fs_win_wide(text);
|
||
scr_str_release(text);
|
||
if (!wide) { errno = scr_fs_win_errno(GetLastError()); return -1; }
|
||
DWORD attributes = GetFileAttributesW(wide);
|
||
if (attributes == INVALID_FILE_ATTRIBUTES || !(attributes & FILE_ATTRIBUTE_REPARSE_POINT)) { free(wide); return 0; }
|
||
HANDLE h = CreateFileW(wide, FILE_READ_ATTRIBUTES,
|
||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
|
||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
|
||
if (h == INVALID_HANDLE_VALUE) { DWORD error = GetLastError(); free(wide); errno = scr_fs_win_errno(error); return -1; }
|
||
FILE_ATTRIBUTE_TAG_INFO tagged;
|
||
if (!GetFileInformationByHandleEx(h, FileAttributeTagInfo, &tagged, sizeof tagged)) {
|
||
DWORD error = GetLastError(); CloseHandle(h); free(wide); errno = scr_fs_win_errno(error); return -1;
|
||
}
|
||
double size;
|
||
bool link = scr_stats_is_link_tag(tagged.ReparseTag);
|
||
// Mount-point tags also represent mounted volumes. Only junction payloads
|
||
// are links; keep the existing ordinary-directory behavior for volumes.
|
||
if (link && tagged.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) link = scr_stats_link_target(h, tagged.ReparseTag, &size, NULL);
|
||
CloseHandle(h);
|
||
if (!link) { free(wide); return 0; }
|
||
BOOL ok = attributes & FILE_ATTRIBUTE_DIRECTORY ? RemoveDirectoryW(wide) : DeleteFileW(wide);
|
||
DWORD error = ok ? 0 : GetLastError();
|
||
free(wide);
|
||
if (!ok) { errno = scr_fs_win_errno(error); return -1; }
|
||
return 1;
|
||
}
|
||
#endif
|
||
|
||
void scr_fs_unlink(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
int link = scr_fs_win_remove_link(path->data, path->len);
|
||
if (link != 0) { if (link < 0) scr_fs_throw(errno, "unlink", path); return; }
|
||
#endif
|
||
if (unlink(path->data) != 0) scr_fs_throw(errno, "unlink", path);
|
||
}
|
||
|
||
void scr_fs_chmod(ScrStr *path, double mode) {
|
||
if (chmod(path->data, (mode_t)mode) != 0) scr_fs_throw(errno, "chmod", path);
|
||
}
|
||
|
||
void scr_fs_chown(ScrStr *path, double uid, double gid) {
|
||
#if defined(_WIN32) || defined(__wasi__)
|
||
/* libuv's uv_fs_chown on Windows is an unconditional no-op success —
|
||
* Node's chownSync "works" and changes nothing there; WASI likewise has
|
||
* no ownership model. */
|
||
(void)path; (void)uid; (void)gid;
|
||
#else
|
||
/* Node passes the ids straight to chown(2); -1 is the POSIX "leave
|
||
* unchanged" value and rides the same int cast. */
|
||
if (chown(path->data, (uid_t)(int64_t)uid, (gid_t)(int64_t)gid) != 0) {
|
||
scr_fs_throw(errno, "chown", path);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/* fs.openSync(path, flags) → the raw fd (as f64) — the pair behind
|
||
* spawn's fd-stdio form (openSync → stdio: ["ignore", fd, fd] →
|
||
* closeSync). flags is Node's string grammar; an unknown flag throws
|
||
* Node's ERR_INVALID_ARG_VALUE TypeError text, an open(2) failure the
|
||
* usual Node-shaped fs error. Mode is Node's 0666 default (the numeric
|
||
* third argument is a compile fence). */
|
||
double scr_fs_open(ScrStr *path, ScrStr *flags) {
|
||
const char *f = flags->data;
|
||
int of;
|
||
if (strcmp(f, "r") == 0) of = O_RDONLY;
|
||
else if (strcmp(f, "rs") == 0 || strcmp(f, "sr") == 0) of = O_RDONLY | O_SYNC;
|
||
else if (strcmp(f, "r+") == 0) of = O_RDWR;
|
||
else if (strcmp(f, "rs+") == 0 || strcmp(f, "sr+") == 0) of = O_RDWR | O_SYNC;
|
||
else if (strcmp(f, "w") == 0) of = O_TRUNC | O_CREAT | O_WRONLY;
|
||
else if (strcmp(f, "wx") == 0 || strcmp(f, "xw") == 0) of = O_TRUNC | O_CREAT | O_WRONLY | O_EXCL;
|
||
else if (strcmp(f, "w+") == 0) of = O_TRUNC | O_CREAT | O_RDWR;
|
||
else if (strcmp(f, "wx+") == 0 || strcmp(f, "xw+") == 0) of = O_TRUNC | O_CREAT | O_RDWR | O_EXCL;
|
||
else if (strcmp(f, "a") == 0) of = O_APPEND | O_CREAT | O_WRONLY;
|
||
else if (strcmp(f, "ax") == 0 || strcmp(f, "xa") == 0) of = O_APPEND | O_CREAT | O_WRONLY | O_EXCL;
|
||
else if (strcmp(f, "as") == 0 || strcmp(f, "sa") == 0) of = O_APPEND | O_CREAT | O_WRONLY | O_SYNC;
|
||
else if (strcmp(f, "a+") == 0) of = O_APPEND | O_CREAT | O_RDWR;
|
||
else if (strcmp(f, "ax+") == 0 || strcmp(f, "xa+") == 0) of = O_APPEND | O_CREAT | O_RDWR | O_EXCL;
|
||
else if (strcmp(f, "as+") == 0 || strcmp(f, "sa+") == 0) of = O_APPEND | O_CREAT | O_RDWR | O_SYNC;
|
||
else {
|
||
char msg[128];
|
||
int len = snprintf(msg, sizeof msg, "The argument 'flags' is invalid. Received '%s'", f);
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
|
||
return 0;
|
||
}
|
||
int fd = open(path->data, of | O_BINARY, 0666);
|
||
if (fd < 0) {
|
||
scr_fs_throw(errno, "open", path);
|
||
return 0;
|
||
}
|
||
return (double)fd;
|
||
}
|
||
|
||
/* fs.openSync(path, fs.constants.O_* [, mode]). The compiler passes a
|
||
* platform-independent mask for inline flag expressions; native flags are
|
||
* chosen here so cross-compiled programs retain the target's values. */
|
||
double scr_fs_open_numeric(ScrStr *path, double flags, double mode) {
|
||
int bits = (int)flags;
|
||
int access = bits & 3;
|
||
if (access == 3 || !(isfinite(mode) && trunc(mode) == mode && mode >= 0 && mode <= 4294967295.0)) {
|
||
scr_throw_error_msg(SCR_ERR_RANGE, "Invalid open flags or mode", 26);
|
||
return 0;
|
||
}
|
||
int native = access == 1 ? O_WRONLY : access == 2 ? O_RDWR : O_RDONLY;
|
||
if (bits & 4) native |= O_CREAT;
|
||
if (bits & 8) native |= O_EXCL;
|
||
if (bits & 64) native |= O_TRUNC;
|
||
if (bits & 128) native |= O_APPEND;
|
||
#ifdef _WIN32
|
||
/* The CRT cannot open a reparse point without following it. Fail closed
|
||
* until a handle-based no-follow open is available on this target. */
|
||
if (bits & 16) {
|
||
scr_fs_throw(ENOSYS, "open", path);
|
||
return 0;
|
||
}
|
||
#else
|
||
if (bits & 16) native |= O_NOFOLLOW;
|
||
if (bits & 32) native |= O_NONBLOCK;
|
||
#endif
|
||
int fd = open(path->data, native | O_BINARY, (mode_t)mode);
|
||
if (fd < 0) {
|
||
scr_fs_throw(errno, "open", path);
|
||
return 0;
|
||
}
|
||
return (double)fd;
|
||
}
|
||
|
||
static void scr_fs_throw_nopath(int e, const char *op);
|
||
static bool scr_fs_write_fd_valid(double fd);
|
||
|
||
double scr_fs_open_native(ScrStr *path, double flags, double mode) {
|
||
#ifdef _WIN32
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "open", path); return 0; }
|
||
int fd = _wopen(wide, (int)flags | O_BINARY, (int)mode);
|
||
free(wide);
|
||
#else
|
||
int fd = open(path->data, (int)flags | O_BINARY, (mode_t)mode);
|
||
#endif
|
||
if (fd < 0) { scr_fs_throw(errno, "open", path); return 0; }
|
||
return (double)fd;
|
||
}
|
||
|
||
void scr_fs_fchmod(double fd, double mode) {
|
||
#ifdef _WIN32
|
||
(void)fd;
|
||
(void)mode;
|
||
/* The CRT cannot enforce Unix permission bits through a descriptor.
|
||
* A silent success would misrepresent a security-sensitive operation. */
|
||
scr_fs_throw_nopath(ENOSYS, "fchmod");
|
||
#else
|
||
if (fchmod((int)fd, (mode_t)mode) != 0) scr_fs_throw_nopath(errno, "fchmod");
|
||
#endif
|
||
}
|
||
|
||
void scr_fs_fsync(double fd) {
|
||
#ifdef _WIN32
|
||
if (_commit((int)fd) != 0) scr_fs_throw_nopath(errno, "fsync");
|
||
#else
|
||
if (fsync((int)fd) != 0) scr_fs_throw_nopath(errno, "fsync");
|
||
#endif
|
||
}
|
||
|
||
void scr_fs_fdatasync(double fd) {
|
||
if (!scr_fs_write_fd_valid(fd)) return;
|
||
#if defined(_WIN32)
|
||
if (_commit((int)fd) != 0) scr_fs_throw_nopath(errno, "fdatasync");
|
||
#elif defined(__APPLE__)
|
||
/* macOS/libuv implements fdatasync with fsync. */
|
||
if (fsync((int)fd) != 0) scr_fs_throw_nopath(errno, "fdatasync");
|
||
#else
|
||
if (fdatasync((int)fd) != 0) scr_fs_throw_nopath(errno, "fdatasync");
|
||
#endif
|
||
}
|
||
|
||
void scr_fs_ftruncate(double fd, double length) {
|
||
if (!scr_fs_write_fd_valid(fd)) return;
|
||
char received[48], msg[192];
|
||
scr_num_received(length, received);
|
||
if (!(isfinite(length) && trunc(length) == length) ||
|
||
length < -9007199254740991.0 || length > 9007199254740991.0) {
|
||
const char *bound = isfinite(length) && trunc(length) == length
|
||
? ">= -9007199254740991 && <= 9007199254740991" : "an integer";
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"len\" is out of range. It must be %s. Received %s", bound, received);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return;
|
||
}
|
||
if (length < 0) length = 0;
|
||
#ifdef _WIN32
|
||
int error = (int)_chsize_s((int)fd, (__int64)length);
|
||
if (error) scr_fs_throw_nopath(error, "ftruncate");
|
||
#else
|
||
if (ftruncate((int)fd, (off_t)length) != 0) scr_fs_throw_nopath(errno, "ftruncate");
|
||
#endif
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
static bool scr_fs_set_times(HANDLE handle, double access, double modified) {
|
||
FILETIME atime, mtime, now;
|
||
if (isinf(access) || isinf(modified)) GetSystemTimeAsFileTime(&now);
|
||
double values[2] = { access, modified };
|
||
FILETIME *files[2] = { &atime, &mtime };
|
||
for (size_t i = 0; i < 2; i++) {
|
||
if (isnan(values[i])) { files[i] = NULL; continue; }
|
||
if (isinf(values[i])) { *files[i] = now; continue; }
|
||
double ticks = values[i] * 10000000.0 + 116444736000000000.0;
|
||
if (ticks < -9223372036854775808.0 || ticks >= 9223372036854775808.0) {
|
||
SetLastError(ERROR_INVALID_PARAMETER);
|
||
return false;
|
||
}
|
||
int64_t time = (int64_t)ticks;
|
||
files[i]->dwLowDateTime = (DWORD)(uint64_t)time;
|
||
files[i]->dwHighDateTime = (DWORD)((uint64_t)time >> 32);
|
||
}
|
||
return SetFileTime(handle, NULL, files[0], files[1]) != 0;
|
||
}
|
||
|
||
static DWORD scr_fs_set_path_times(WCHAR *path, double atime, double mtime, bool nofollow) {
|
||
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | (nofollow ? FILE_FLAG_OPEN_REPARSE_POINT : 0);
|
||
HANDLE handle = CreateFileW(path, FILE_WRITE_ATTRIBUTES,
|
||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, flags, NULL);
|
||
if (handle == INVALID_HANDLE_VALUE) return GetLastError();
|
||
DWORD error = scr_fs_set_times(handle, atime, mtime) ? 0 : GetLastError();
|
||
CloseHandle(handle);
|
||
return error;
|
||
}
|
||
#else
|
||
/* Match the pinned libuv conversion: microsecond truncation, normalized
|
||
* negative fractions, and the special current/unchanged time sentinels. */
|
||
static bool scr_fs_timespec(double time, struct timespec *out) {
|
||
if (isinf(time)) { *out = (struct timespec){ 0, UTIME_NOW }; return true; }
|
||
if (isnan(time)) { *out = (struct timespec){ 0, UTIME_OMIT }; return true; }
|
||
if (time < -9223372036854775808.0 || time >= 9223372036854775808.0) return false;
|
||
out->tv_sec = (time_t)time;
|
||
out->tv_nsec = (long)((time - (double)out->tv_sec) * 1000000000.0);
|
||
out->tv_nsec -= out->tv_nsec % 1000;
|
||
if (out->tv_nsec < 0) { out->tv_nsec += 1000000000; out->tv_sec--; }
|
||
return true;
|
||
}
|
||
#endif
|
||
|
||
void scr_fs_utimes(ScrStr *path, double atime, double mtime, bool nofollow) {
|
||
const char *op = nofollow ? "lutime" : "utime";
|
||
#ifdef _WIN32
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) { scr_fs_throw(scr_fs_win_errno(GetLastError()), op, path); return; }
|
||
DWORD error = scr_fs_set_path_times(wide, atime, mtime, nofollow);
|
||
if (nofollow && (error == ERROR_SYMLINK_NOT_SUPPORTED || error == ERROR_NOT_A_REPARSE_POINT))
|
||
error = scr_fs_set_path_times(wide, atime, mtime, false);
|
||
free(wide);
|
||
if (error) scr_fs_throw(scr_fs_win_errno(error), op, path);
|
||
#else
|
||
struct timespec times[2];
|
||
if (!scr_fs_timespec(atime, ×[0]) || !scr_fs_timespec(mtime, ×[1])) { scr_fs_throw(EINVAL, op, path); return; }
|
||
if (utimensat(AT_FDCWD, path->data, times, nofollow ? AT_SYMLINK_NOFOLLOW : 0) != 0) scr_fs_throw(errno, op, path);
|
||
#endif
|
||
}
|
||
|
||
void scr_fs_futimes(double fd, double atime, double mtime) {
|
||
#ifdef _WIN32
|
||
HANDLE handle = (HANDLE)_get_osfhandle((int)fd);
|
||
if (handle == INVALID_HANDLE_VALUE) { scr_fs_throw_nopath(EBADF, "futime"); return; }
|
||
if (!scr_fs_set_times(handle, atime, mtime)) scr_fs_throw_nopath(scr_fs_win_errno(GetLastError()), "futime");
|
||
#else
|
||
struct timespec times[2];
|
||
if (!scr_fs_timespec(atime, ×[0]) || !scr_fs_timespec(mtime, ×[1])) { scr_fs_throw_nopath(EINVAL, "futime"); return; }
|
||
if (futimens((int)fd, times) != 0) scr_fs_throw_nopath(errno, "futime");
|
||
#endif
|
||
}
|
||
|
||
/* fs.closeSync(fd) — close(2); failure throws Node's path-less fs error
|
||
* shape ("EBADF: bad file descriptor, close"). */
|
||
|
||
/* Offset-preserving read for fs.readSync's numeric-position form. POSIX
|
||
* supplies pread(2). Windows follows libuv's synchronous-handle recipe:
|
||
* ReadFile with an OVERLAPPED byte offset, then restore the handle position
|
||
* because synchronous ReadFile updates it even when OVERLAPPED is present. */
|
||
static ssize_t scr_fs_pread(int fd, void *data, size_t length, double position) {
|
||
#ifdef _WIN32
|
||
HANDLE handle = (HANDLE)_get_osfhandle(fd);
|
||
if (handle == INVALID_HANDLE_VALUE) {
|
||
errno = EBADF;
|
||
return -1;
|
||
}
|
||
|
||
OVERLAPPED overlapped;
|
||
memset(&overlapped, 0, sizeof overlapped);
|
||
LARGE_INTEGER at;
|
||
at.QuadPart = (LONGLONG)position;
|
||
overlapped.Offset = at.LowPart;
|
||
overlapped.OffsetHigh = at.HighPart;
|
||
|
||
LARGE_INTEGER zero;
|
||
LARGE_INTEGER original;
|
||
zero.QuadPart = 0;
|
||
BOOL restore = SetFilePointerEx(handle, zero, &original, FILE_CURRENT);
|
||
DWORD got = 0;
|
||
DWORD want = length > (size_t)UINT32_MAX ? UINT32_MAX : (DWORD)length;
|
||
BOOL ok = ReadFile(handle, data, want, &got, &overlapped);
|
||
DWORD error = ok ? ERROR_SUCCESS : GetLastError();
|
||
if (restore) (void)SetFilePointerEx(handle, original, NULL, FILE_BEGIN);
|
||
/* ReadFile may report a terminal status after still filling part of the
|
||
* caller's buffer (notably ERROR_MORE_DATA for a message-mode pipe).
|
||
* libuv/Node return those bytes and surface the status on the next read. */
|
||
if (ok || got > 0 || error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE) {
|
||
return (ssize_t)got;
|
||
}
|
||
|
||
/* The read-specific subset of libuv's Win32-to-errno translation. */
|
||
switch (error) {
|
||
case ERROR_INVALID_HANDLE:
|
||
case ERROR_ACCESS_DENIED:
|
||
errno = EBADF;
|
||
break;
|
||
case ERROR_INVALID_FUNCTION:
|
||
case ERROR_INVALID_PARAMETER:
|
||
errno = EINVAL;
|
||
break;
|
||
case ERROR_NOT_ENOUGH_MEMORY:
|
||
case ERROR_OUTOFMEMORY:
|
||
errno = ENOMEM;
|
||
break;
|
||
case ERROR_OPERATION_ABORTED:
|
||
errno = EINTR;
|
||
break;
|
||
default:
|
||
errno = EIO;
|
||
break;
|
||
}
|
||
return -1;
|
||
#else
|
||
return pread(fd, data, length, (off_t)position);
|
||
#endif
|
||
}
|
||
|
||
/* Offset-preserving write for fs.writeSync's numeric-position forms. The
|
||
* Windows arm mirrors scr_fs_pread: WriteFile receives an OVERLAPPED offset
|
||
* and the CRT descriptor's current position is restored before returning. */
|
||
static ssize_t scr_fs_pwrite(int fd, const void *data, size_t length, double position) {
|
||
#ifdef _WIN32
|
||
HANDLE handle = (HANDLE)_get_osfhandle(fd);
|
||
if (handle == INVALID_HANDLE_VALUE) {
|
||
errno = EBADF;
|
||
return -1;
|
||
}
|
||
|
||
OVERLAPPED overlapped;
|
||
memset(&overlapped, 0, sizeof overlapped);
|
||
LARGE_INTEGER at;
|
||
at.QuadPart = (LONGLONG)position;
|
||
overlapped.Offset = at.LowPart;
|
||
overlapped.OffsetHigh = at.HighPart;
|
||
|
||
LARGE_INTEGER zero;
|
||
LARGE_INTEGER original;
|
||
zero.QuadPart = 0;
|
||
BOOL restore = SetFilePointerEx(handle, zero, &original, FILE_CURRENT);
|
||
DWORD wrote = 0;
|
||
DWORD want = length > (size_t)UINT32_MAX ? UINT32_MAX : (DWORD)length;
|
||
BOOL ok = WriteFile(handle, data, want, &wrote, &overlapped);
|
||
DWORD error = ok ? ERROR_SUCCESS : GetLastError();
|
||
if (restore) (void)SetFilePointerEx(handle, original, NULL, FILE_BEGIN);
|
||
if (ok || wrote > 0) return (ssize_t)wrote;
|
||
|
||
switch (error) {
|
||
case ERROR_INVALID_HANDLE:
|
||
case ERROR_ACCESS_DENIED:
|
||
errno = EBADF;
|
||
break;
|
||
case ERROR_INVALID_FUNCTION:
|
||
case ERROR_INVALID_PARAMETER:
|
||
errno = EINVAL;
|
||
break;
|
||
case ERROR_DISK_FULL:
|
||
case ERROR_HANDLE_DISK_FULL:
|
||
errno = ENOSPC;
|
||
break;
|
||
case ERROR_FILE_TOO_LARGE:
|
||
errno = EFBIG;
|
||
break;
|
||
case ERROR_BROKEN_PIPE:
|
||
case ERROR_NO_DATA:
|
||
errno = EPIPE;
|
||
break;
|
||
case ERROR_NOT_ENOUGH_MEMORY:
|
||
case ERROR_OUTOFMEMORY:
|
||
errno = ENOMEM;
|
||
break;
|
||
case ERROR_OPERATION_ABORTED:
|
||
errno = EINTR;
|
||
break;
|
||
default:
|
||
errno = EIO;
|
||
break;
|
||
}
|
||
return -1;
|
||
#else
|
||
return pwrite(fd, data, length, (off_t)position);
|
||
#endif
|
||
}
|
||
|
||
static bool scr_fs_write_fd_valid(double fd) {
|
||
char msg[160];
|
||
char recv[48];
|
||
scr_num_received(fd, recv);
|
||
if (!(isfinite(fd) && trunc(fd) == fd)) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"fd\" is out of range. It must be an integer. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return false;
|
||
}
|
||
if (fd < 0 || fd > 2147483647.0) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"fd\" is out of range. It must be >= 0 && <= 2147483647. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* write(2) raises SIGPIPE before returning EPIPE when a pipe/socket peer has
|
||
* closed; write(2)/pwrite(2) likewise raise SIGXFSZ before returning EFBIG at
|
||
* RLIMIT_FSIZE. Node turns both into catchable fs errors. Do the same per
|
||
* calling thread rather than changing process dispositions (which spawned
|
||
* children would inherit). Consume only a signal freshly generated by this
|
||
* call, preserving any signal that was already pending. */
|
||
static ssize_t scr_fs_write_once(int fd, const void *data, size_t length,
|
||
bool positioned, double position) {
|
||
#if defined(_WIN32) || defined(__wasi__)
|
||
return positioned
|
||
? scr_fs_pwrite(fd, data, length, position)
|
||
: write(fd, data, length);
|
||
#else
|
||
sigset_t write_set, old_set, pending;
|
||
sigemptyset(&write_set);
|
||
#ifdef SIGPIPE
|
||
sigaddset(&write_set, SIGPIPE);
|
||
#endif
|
||
#ifdef SIGXFSZ
|
||
sigaddset(&write_set, SIGXFSZ);
|
||
#endif
|
||
if (pthread_sigmask(SIG_BLOCK, &write_set, &old_set) != 0) {
|
||
return positioned
|
||
? scr_fs_pwrite(fd, data, length, position)
|
||
: write(fd, data, length);
|
||
}
|
||
|
||
bool pending_known = sigpending(&pending) == 0;
|
||
#ifdef SIGPIPE
|
||
bool had_pipe = pending_known && sigismember(&pending, SIGPIPE) == 1;
|
||
#endif
|
||
#ifdef SIGXFSZ
|
||
bool had_xfsz = pending_known && sigismember(&pending, SIGXFSZ) == 1;
|
||
#endif
|
||
ssize_t n = positioned
|
||
? scr_fs_pwrite(fd, data, length, position)
|
||
: write(fd, data, length);
|
||
int write_errno = errno;
|
||
|
||
int generated = 0;
|
||
if (n < 0 && pending_known && sigpending(&pending) == 0) {
|
||
#ifdef SIGPIPE
|
||
if (write_errno == EPIPE && !had_pipe && sigismember(&pending, SIGPIPE) == 1) {
|
||
generated = SIGPIPE;
|
||
}
|
||
#endif
|
||
#ifdef SIGXFSZ
|
||
if (write_errno == EFBIG && !had_xfsz && sigismember(&pending, SIGXFSZ) == 1) {
|
||
generated = SIGXFSZ;
|
||
}
|
||
#endif
|
||
}
|
||
if (generated != 0) {
|
||
sigset_t generated_set;
|
||
sigemptyset(&generated_set);
|
||
sigaddset(&generated_set, generated);
|
||
int caught = 0;
|
||
int wait_err;
|
||
do {
|
||
wait_err = sigwait(&generated_set, &caught);
|
||
} while (wait_err == EINTR);
|
||
}
|
||
|
||
(void)pthread_sigmask(SIG_SETMASK, &old_set, NULL);
|
||
errno = write_errno;
|
||
return n;
|
||
#endif
|
||
}
|
||
|
||
static double scr_fs_write_bytes(double fd, const void *data, size_t length,
|
||
double position) {
|
||
if (!scr_fs_write_fd_valid(fd)) return 0;
|
||
/* Node/libuv treats every position other than a safe nonnegative integer
|
||
* as the current-offset sentinel for writes (unlike readSync, it does not
|
||
* throw for negative/fractional positions). */
|
||
bool positioned = isfinite(position) && trunc(position) == position &&
|
||
position >= 0 && position <= 9007199254740991.0;
|
||
ssize_t n;
|
||
do {
|
||
n = scr_fs_write_once((int)fd, data, length, positioned, position);
|
||
} while (n < 0 && errno == EINTR);
|
||
if (n < 0) {
|
||
int e = errno;
|
||
char namebuf[16];
|
||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||
const char *text = scr_errno_text(e);
|
||
char msg[160];
|
||
int len = snprintf(msg, sizeof msg, "%s: %s, write", name, text);
|
||
scr_fs_throw_details(e, name, msg, (size_t)len, "write", NULL, NULL);
|
||
return 0;
|
||
}
|
||
return (double)n;
|
||
}
|
||
|
||
/* fs.writeSync(fd, buffer, offset, length[, position]) — validates the
|
||
* caller window before touching the descriptor, then submits one write like
|
||
* Node/libuv. Positioned writes do not advance the descriptor. */
|
||
double scr_fs_write_sync(double fd, ScrBytes *buf, double offset, double length,
|
||
double position) {
|
||
SCR_BYTES_SNAPSHOT(buf);
|
||
size_t bytelen = buf->len; /* frontend admits u8 buffers only */
|
||
char msg[160];
|
||
char recv[48];
|
||
scr_num_received(offset, recv);
|
||
if (!(isfinite(offset) && trunc(offset) == offset)) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be an integer. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
if (offset < 0 || offset > 9007199254740991.0) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
if (offset > (double)bytelen) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be <= %zu. Received %s",
|
||
bytelen, recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
|
||
scr_num_received(length, recv);
|
||
if (length < 0) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"length\" is out of range. It must be >= 0. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
size_t off = (size_t)offset;
|
||
if (length > (double)(bytelen - off)) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"length\" is out of range. It must be <= %zu. Received %s",
|
||
bytelen - off, recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
if (!(isfinite(length) && trunc(length) == length)) {
|
||
int len = snprintf(msg, sizeof msg,
|
||
"The value of \"length\" is out of range. It must be an integer. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
return scr_fs_write_bytes(fd, buf->data + off, (size_t)length, position);
|
||
}
|
||
|
||
double scr_fs_write_str_sync(double fd, ScrStr *data, double position,
|
||
ScrStr *encoding) {
|
||
(void)encoding; /* frontend proves utf8; the argument still evaluates */
|
||
char *owned;
|
||
const char *bytes = scr_str_utf8(data, &owned);
|
||
double wrote = scr_fs_write_bytes(fd, bytes, data->len, position);
|
||
scr_str_utf8_end(owned);
|
||
return wrote;
|
||
}
|
||
|
||
/* fs.readSync(fd, buffer, offset, length[, position]) — the buffer form.
|
||
* Node validates offset/length against the buffer before reading and
|
||
* throws ERR_OUT_OF_RANGE; here the checks clamp to the same contract and
|
||
* throw the RangeError shape. Position -1 means the fd's current offset;
|
||
* nonnegative positions do not advance it. Returns the byte count the OS
|
||
* reports; errors carry the errno name like the other fd operations. */
|
||
double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length,
|
||
double position) {
|
||
size_t bytelen = buf->len; /* u8 buffers: elem count == byte count */
|
||
char msg[160];
|
||
int mlen;
|
||
/* Node's exact texts: offset validates against MAX_SAFE_INTEGER's range
|
||
* form, length against the remaining window (validateOffset vs the
|
||
* buffer-bounds check in fs.readSync). */
|
||
char numbuf[40];
|
||
if (!(isfinite(offset) && trunc(offset) == offset)) {
|
||
char recv[48];
|
||
scr_num_received(offset, recv);
|
||
mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be an integer. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
if (offset < 0 || offset > 9007199254740991.0) {
|
||
numbuf[scr_f64_to_str(offset, numbuf)] = 0;
|
||
mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
|
||
numbuf);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
/* Node returns after offset's intrinsic validation when the requested
|
||
* length normalizes to zero: buffer-window bounds, position, and fd are
|
||
* not consulted. Preserve the existing size_t coercion's [0, 1) case. */
|
||
if (length >= 0 && length < 1) return 0;
|
||
if (offset > (double)bytelen) {
|
||
numbuf[scr_f64_to_str(offset, numbuf)] = 0;
|
||
mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
|
||
numbuf);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
size_t off = (size_t)offset;
|
||
if (length < 0 || (double)length > (double)(bytelen - off)) {
|
||
numbuf[scr_f64_to_str(length, numbuf)] = 0;
|
||
mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"length\" is out of range. It must be <= %zu. Received %s",
|
||
bytelen - off, numbuf);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
size_t want = (size_t)length;
|
||
if (!(isfinite(position) && trunc(position) == position)) {
|
||
char recv[48];
|
||
scr_num_received(position, recv);
|
||
mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"position\" is out of range. It must be an integer. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
if (position < -1 || position > 9007199254740991.0) {
|
||
char recv[48];
|
||
scr_num_received(position, recv);
|
||
mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"position\" is out of range. It must be >= -1 && <= 9007199254740991. Received %s",
|
||
recv);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
uint8_t *bounce = buf->shared ? malloc(want ? want : 1) : NULL;
|
||
if (buf->shared && !bounce) scr_trap("scriptc: out of memory\n");
|
||
uint8_t *destination = bounce ? bounce : buf->data + off;
|
||
ssize_t n = position == -1
|
||
? read((int)fd, destination, want)
|
||
: scr_fs_pread((int)fd, destination, want, position);
|
||
int read_error = errno;
|
||
if (bounce) {
|
||
if (n > 0) scr_bytes_write(buf, off, bounce, (size_t)n);
|
||
free(bounce);
|
||
}
|
||
errno = read_error;
|
||
if (n < 0) {
|
||
int e = errno;
|
||
char namebuf[16];
|
||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||
const char *text = scr_errno_text(e);
|
||
char msg[160];
|
||
int len = snprintf(msg, sizeof msg, "%s: %s, read", name, text);
|
||
scr_fs_throw_details(e, name, msg, (size_t)len, "read", NULL, NULL);
|
||
return 0;
|
||
}
|
||
return (double)n;
|
||
}
|
||
|
||
/* Gather/scatter one contiguous window around ONE kernel operation. Multiple
|
||
* scalar calls would change short I/O and pipe atomicity. This portable seam
|
||
* also preserves positioned I/O on targets without preadv/pwritev. */
|
||
double scr_fs_read_zero(double fd, double position) {
|
||
if (!scr_fs_write_fd_valid(fd)) return 0;
|
||
uint8_t dummy;
|
||
ssize_t n = position < 0 ? read((int)fd, &dummy, 0)
|
||
: scr_fs_pread((int)fd, &dummy, 0, position);
|
||
if (n < 0) scr_fs_throw_nopath(errno, "read");
|
||
return 0;
|
||
}
|
||
|
||
static double scr_fs_vector_sync(double fd, ScrArr *buffers, double position,
|
||
bool writing) {
|
||
size_t count = (size_t)scr_arr_len(buffers);
|
||
if (writing && count == 0) return 0;
|
||
if (!scr_fs_write_fd_valid(fd)) return 0;
|
||
if (count == 0) { scr_fs_throw_nopath(EINVAL, "read"); return 0; }
|
||
size_t length = 0;
|
||
for (size_t i = 0; i < count; i++) {
|
||
ScrBytes *buf = scr_arr_get_ref(buffers, (double)i);
|
||
size_t n = buf->len;
|
||
scr_bytes_release(buf);
|
||
if (n > SIZE_MAX - length) scr_trap("scriptc: vector I/O size overflow\n");
|
||
length += n;
|
||
}
|
||
uint8_t *data = malloc(length ? length : 1);
|
||
if (!data) scr_trap("scriptc: out of memory\n");
|
||
if (writing) {
|
||
size_t at = 0;
|
||
for (size_t i = 0; i < count; i++) {
|
||
ScrBytes *buf = scr_arr_get_ref(buffers, (double)i);
|
||
scr_bytes_read(buf, 0, data + at, buf->len);
|
||
at += buf->len;
|
||
scr_bytes_release(buf);
|
||
}
|
||
}
|
||
ScrBytes window = {SIZE_MAX, length, SCR_BYTES_U8, data, NULL};
|
||
position = isfinite(position) && position >= 0 && position <= 9007199254740991.0
|
||
? trunc(position) : -1;
|
||
double transferred = writing
|
||
? scr_fs_write_sync(fd, &window, 0, (double)length, position)
|
||
: length == 0 ? scr_fs_read_zero(fd, position)
|
||
: scr_fs_read_sync(fd, &window, 0, (double)length, position);
|
||
if (!writing && !scr_exc_pending()) {
|
||
size_t at = 0, remaining = (size_t)transferred;
|
||
for (size_t i = 0; i < count && remaining; i++) {
|
||
ScrBytes *buf = scr_arr_get_ref(buffers, (double)i);
|
||
size_t n = buf->len < remaining ? buf->len : remaining;
|
||
scr_bytes_write(buf, 0, data + at, n);
|
||
at += n;
|
||
remaining -= n;
|
||
scr_bytes_release(buf);
|
||
}
|
||
}
|
||
free(data);
|
||
return transferred;
|
||
}
|
||
|
||
double scr_fs_readv_sync(double fd, ScrArr *buffers, double position) {
|
||
return scr_fs_vector_sync(fd, buffers, position, false);
|
||
}
|
||
|
||
double scr_fs_writev_sync(double fd, ScrArr *buffers, double position) {
|
||
return scr_fs_vector_sync(fd, buffers, position, true);
|
||
}
|
||
|
||
void scr_fs_close(double fd) {
|
||
if (close((int)fd) != 0) {
|
||
int e = errno;
|
||
char namebuf[16];
|
||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||
const char *text = scr_errno_text(e);
|
||
char msg[160];
|
||
int len = snprintf(msg, sizeof msg, "%s: %s, close", name, text);
|
||
scr_fs_throw_details(e, name, msg, (size_t)len, "close", NULL, NULL);
|
||
}
|
||
}
|
||
|
||
/* The two-path fs error shape — Node's copyfile errors quote both ends:
|
||
* "ENOENT: no such file or directory, copyfile 'src' -> 'dest'". */
|
||
static void scr_fs_throw2(int e, const char *op, const ScrStr *src, const ScrStr *dest) {
|
||
char namebuf[16];
|
||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||
const char *text = scr_errno_text(e);
|
||
char srcbuf[PATH_MAX], destbuf[PATH_MAX];
|
||
const char *shown_src = scr_fs_err_path(src, srcbuf);
|
||
const char *shown_dest = scr_fs_err_path(dest, destbuf);
|
||
size_t cap = strlen(name) + strlen(text) + strlen(op) + strlen(shown_src) + strlen(shown_dest) + 16;
|
||
char *msg = malloc(cap);
|
||
if (!msg) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
int len = snprintf(msg, cap, "%s: %s, %s '%s' -> '%s'", name, text, op, shown_src, shown_dest);
|
||
scr_fs_throw_details(e, name, msg, (size_t)len, op, src, dest);
|
||
free(msg);
|
||
}
|
||
|
||
void scr_fs_link(ScrStr *source, ScrStr *dest) {
|
||
#ifdef _WIN32
|
||
WCHAR *from = scr_fs_win_wide(source);
|
||
WCHAR *to = scr_fs_win_wide(dest);
|
||
if (!from || !to) {
|
||
DWORD error = GetLastError();
|
||
free(from); free(to);
|
||
scr_fs_throw2(scr_fs_win_errno(error), "link", source, dest);
|
||
return;
|
||
}
|
||
BOOL ok = CreateHardLinkW(to, from, NULL);
|
||
DWORD error = ok ? 0 : GetLastError();
|
||
free(from); free(to);
|
||
if (!ok) scr_fs_throw2(scr_fs_win_errno(error), "link", source, dest);
|
||
#else
|
||
if (link(source->data, dest->data) != 0) scr_fs_throw2(errno, "link", source, dest);
|
||
#endif
|
||
}
|
||
|
||
/* copyFileSync(src, dest): contents copied into a created-or-truncated
|
||
* destination carrying the SOURCE's permission bits — libuv's
|
||
* uv_fs_copyfile behavior behind Node's copyFileSync (umask applies at
|
||
* creation, like any open(2)). Every failure throws catchably with the
|
||
* two-path message and the syscall name Node reports ("copyfile"). */
|
||
void scr_fs_copyfile(ScrStr *src, ScrStr *dest) {
|
||
int in = open(src->data, O_RDONLY | O_BINARY);
|
||
if (in < 0) {
|
||
scr_fs_throw2(errno, "copyfile", src, dest);
|
||
return;
|
||
}
|
||
struct stat st;
|
||
if (fstat(in, &st) != 0) {
|
||
int e = errno;
|
||
close(in);
|
||
scr_fs_throw2(e, "copyfile", src, dest);
|
||
return;
|
||
}
|
||
int out = open(dest->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, st.st_mode & 07777);
|
||
if (out < 0) {
|
||
int e = errno;
|
||
close(in);
|
||
scr_fs_throw2(e, "copyfile", src, dest);
|
||
return;
|
||
}
|
||
char buf[65536];
|
||
for (;;) {
|
||
ssize_t got = read(in, buf, sizeof buf);
|
||
if (got < 0) {
|
||
if (errno == EINTR) continue;
|
||
int e = errno;
|
||
close(in);
|
||
close(out);
|
||
scr_fs_throw2(e, "copyfile", src, dest);
|
||
return;
|
||
}
|
||
if (got == 0) break;
|
||
ssize_t at = 0;
|
||
while (at < got) {
|
||
ssize_t wrote = write(out, buf + at, (size_t)(got - at));
|
||
if (wrote < 0) {
|
||
if (errno == EINTR) continue;
|
||
int e = errno;
|
||
close(in);
|
||
close(out);
|
||
scr_fs_throw2(e, "copyfile", src, dest);
|
||
return;
|
||
}
|
||
at += wrote;
|
||
}
|
||
}
|
||
close(in);
|
||
if (close(out) != 0) scr_fs_throw2(errno, "copyfile", src, dest);
|
||
}
|
||
|
||
/* renameSync(old, new): rename(2), Node's two-path error shape ("ENOENT:
|
||
* no such file or directory, rename 'a' -> 'b'"). Windows must bypass
|
||
* the CRT's rename(): it refuses an existing destination and cannot move
|
||
* directories between parents, while Node/libuv uses MoveFileExW with
|
||
* MOVEFILE_REPLACE_EXISTING. Runtime strings are UTF-8, so feed the wide
|
||
* Win32 API rather than the active-code-page `A` form. */
|
||
#ifdef _WIN32
|
||
static WCHAR *scr_fs_win_wide(const ScrStr *path) {
|
||
if (path->len > INT_MAX) {
|
||
SetLastError(ERROR_FILENAME_EXCED_RANGE);
|
||
return NULL;
|
||
}
|
||
int n = path->len > 0
|
||
? MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||
path->data, (int)path->len, NULL, 0)
|
||
: 0;
|
||
if (path->len > 0 && n == 0) return NULL;
|
||
WCHAR *wide = malloc(((size_t)n + 1) * sizeof *wide);
|
||
if (!wide) {
|
||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||
return NULL;
|
||
}
|
||
if (n > 0) {
|
||
(void)MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||
path->data, (int)path->len, wide, n);
|
||
}
|
||
wide[n] = L'\0';
|
||
return wide;
|
||
}
|
||
|
||
/* The reachable fs subset of libuv's uv_translate_sys_error table. */
|
||
static int scr_fs_win_errno(DWORD error) {
|
||
switch (error) {
|
||
case ERROR_ALREADY_EXISTS:
|
||
case ERROR_FILE_EXISTS:
|
||
return EEXIST;
|
||
case ERROR_LOCK_VIOLATION:
|
||
case ERROR_PIPE_BUSY:
|
||
case ERROR_SHARING_VIOLATION:
|
||
return EBUSY;
|
||
case ERROR_INVALID_FUNCTION:
|
||
return EISDIR;
|
||
case ERROR_INSUFFICIENT_BUFFER:
|
||
case ERROR_INVALID_DATA:
|
||
case ERROR_INVALID_PARAMETER:
|
||
return EINVAL;
|
||
case ERROR_BUFFER_OVERFLOW:
|
||
case ERROR_FILENAME_EXCED_RANGE:
|
||
return ENAMETOOLONG;
|
||
case ERROR_NOT_ENOUGH_MEMORY:
|
||
case ERROR_OUTOFMEMORY:
|
||
return ENOMEM;
|
||
case ERROR_CANNOT_MAKE:
|
||
case ERROR_DISK_FULL:
|
||
case ERROR_HANDLE_DISK_FULL:
|
||
return ENOSPC;
|
||
case ERROR_DIR_NOT_EMPTY:
|
||
return ENOTEMPTY;
|
||
case ERROR_ACCESS_DENIED:
|
||
case ERROR_PRIVILEGE_NOT_HELD:
|
||
return EPERM;
|
||
case ERROR_WRITE_PROTECT:
|
||
return EROFS;
|
||
case ERROR_NOT_SAME_DEVICE:
|
||
return EXDEV;
|
||
case ERROR_BAD_PATHNAME:
|
||
case ERROR_DIRECTORY:
|
||
case ERROR_FILE_NOT_FOUND:
|
||
case ERROR_INVALID_DRIVE:
|
||
case ERROR_INVALID_NAME:
|
||
case ERROR_PATH_NOT_FOUND:
|
||
return ENOENT;
|
||
default:
|
||
return EIO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
int scr_fs_rename_raw(const ScrStr *oldpath, const ScrStr *newpath) {
|
||
#ifdef _WIN32
|
||
WCHAR *oldwide = scr_fs_win_wide(oldpath);
|
||
if (!oldwide) return scr_fs_win_errno(GetLastError());
|
||
WCHAR *newwide = scr_fs_win_wide(newpath);
|
||
if (!newwide) {
|
||
DWORD error = GetLastError();
|
||
free(oldwide);
|
||
return scr_fs_win_errno(error);
|
||
}
|
||
BOOL ok = MoveFileExW(oldwide, newwide, MOVEFILE_REPLACE_EXISTING);
|
||
DWORD error = ok ? ERROR_SUCCESS : GetLastError();
|
||
free(oldwide);
|
||
free(newwide);
|
||
return ok ? 0 : scr_fs_win_errno(error);
|
||
#else
|
||
return rename(oldpath->data, newpath->data) == 0 ? 0 : errno;
|
||
#endif
|
||
}
|
||
|
||
void scr_fs_rename_error(int error, const ScrStr *oldpath, const ScrStr *newpath) {
|
||
scr_fs_throw2(error, "rename", oldpath, newpath);
|
||
}
|
||
|
||
void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath) {
|
||
int error = scr_fs_rename_raw(oldpath, newpath);
|
||
if (error != 0) scr_fs_rename_error(error, oldpath, newpath);
|
||
}
|
||
|
||
static int scr_rm_unlink(const char *path, size_t len);
|
||
|
||
void scr_fs_rm(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
int link = scr_fs_win_remove_link(path->data, path->len);
|
||
if (link != 0) { if (link < 0) scr_fs_throw(errno, "unlink", path); return; }
|
||
#endif
|
||
/* Node's rmSync: lstat first (a missing path reports the lstat syscall),
|
||
* refuse directories (Node requires `recursive`, which the scriptc
|
||
* surface doesn't declare — the message wording diverges from Node's
|
||
* ERR_FS_EISDIR, see SEMANTICS.md), then unlink. */
|
||
struct stat st;
|
||
if (lstat(path->data, &st) != 0) {
|
||
scr_fs_throw(errno, "lstat", path);
|
||
return;
|
||
}
|
||
if (S_ISDIR(st.st_mode)) {
|
||
scr_fs_throw(EISDIR, "rm", path);
|
||
return;
|
||
}
|
||
if (scr_rm_unlink(path->data, path->len) != 0) scr_fs_throw(errno, "unlink", path);
|
||
}
|
||
|
||
void scr_fs_rmdir(ScrStr *path) {
|
||
if (rmdir(path->data) != 0) scr_fs_throw(errno, "rmdir", path);
|
||
}
|
||
|
||
/* ── fs option forms ─────────────────────────────────────────────────
|
||
* mkdirSync(p, { recursive: true }), rmSync(p, { recursive, force }),
|
||
* mkdtempSync(prefix), accessSync(p, mode), and the readFileSync(fd)
|
||
* forms — the slice real CLIs use. All throw catchably like the rest of
|
||
* sync fs, with Node's errno/path shapes (verified against Node). */
|
||
|
||
/* Node's recursive mkdir algorithm: try mkdir; EEXIST is fine iff the
|
||
* path is a directory (a file target throws EEXIST at that path); ENOENT
|
||
* creates the parent first and retries. ENOTDIR past a file reports the
|
||
* FULL requested path, like Node. `path` is a NUL-terminated mutable
|
||
* buffer of `len` bytes. */
|
||
/* The path separators the RECURSIVE walk recognizes: win32 targets take
|
||
* both slashes (the path module hands out backslashed paths there);
|
||
* POSIX '/' only — a backslash is an ordinary filename byte. */
|
||
static bool scr_fs_sep(char c) {
|
||
#ifdef _WIN32
|
||
if (c == '\\') return true;
|
||
#endif
|
||
return c == '/';
|
||
}
|
||
|
||
static void scr_mkdir_rec(char *path, size_t len, mode_t mode) {
|
||
if (scr_sys_mkdir(path, mode) == 0) return;
|
||
int e = errno;
|
||
struct stat st;
|
||
if (e == EEXIST) {
|
||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) return;
|
||
} else if (e == ENOENT) {
|
||
/* Parent: trim trailing separators, the last component, then the
|
||
* separator run before it (keep "/" itself). */
|
||
size_t i = len;
|
||
while (i > 0 && scr_fs_sep(path[i - 1])) i--;
|
||
while (i > 0 && !scr_fs_sep(path[i - 1])) i--;
|
||
while (i > 1 && scr_fs_sep(path[i - 1])) i--;
|
||
if (i > 0 && i < len) {
|
||
char saved = path[i];
|
||
path[i] = 0;
|
||
#ifdef _WIN32
|
||
/* POSIX mkdir answers ENOTDIR itself when a path component is a
|
||
* FILE; the CRT answers ENOENT for that too. Node on Windows still
|
||
* reports ENOTDIR with the full requested path — recover the
|
||
* distinction from the parent's stat before recursing. */
|
||
if (stat(path, &st) == 0 && !S_ISDIR(st.st_mode)) {
|
||
path[i] = saved;
|
||
ScrStr *full = scr_str_new(path, len);
|
||
scr_fs_throw(ENOTDIR, "mkdir", full);
|
||
scr_str_release(full);
|
||
return;
|
||
}
|
||
#endif
|
||
scr_mkdir_rec(path, i, mode);
|
||
path[i] = saved;
|
||
if (scr_exc_pending()) return;
|
||
if (scr_sys_mkdir(path, mode) == 0) return;
|
||
e = errno;
|
||
if (e == EEXIST && stat(path, &st) == 0 && S_ISDIR(st.st_mode)) return;
|
||
}
|
||
}
|
||
ScrStr *p = scr_str_new(path, len);
|
||
scr_fs_throw(e, "mkdir", p);
|
||
scr_str_release(p);
|
||
}
|
||
|
||
void scr_fs_mkdir_recursive(ScrStr *path) {
|
||
char *buf = malloc(path->len + 1);
|
||
if (!buf) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
memcpy(buf, path->data, path->len + 1); /* ScrStr data is NUL-terminated */
|
||
scr_mkdir_rec(buf, path->len, 0777);
|
||
free(buf);
|
||
}
|
||
|
||
/* mkdirSync(p, { recursive: true, mode }): Node passes the mode to every
|
||
* directory the walk creates (existing ones keep theirs). */
|
||
void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode) {
|
||
char *buf = malloc(path->len + 1);
|
||
if (!buf) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
memcpy(buf, path->data, path->len + 1);
|
||
scr_mkdir_rec(buf, path->len, (mode_t)mode);
|
||
free(buf);
|
||
}
|
||
|
||
/* First failure of an rm walk, recorded instead of thrown so the retry
|
||
* form can decide (retryable errno + attempts left → sleep and go again)
|
||
* before anything reaches the exception cell. `path` is +1 when err != 0;
|
||
* the throwers release it after scr_fs_throw. */
|
||
typedef struct {
|
||
int err;
|
||
const char *op;
|
||
ScrStr *path;
|
||
} ScrRmFail;
|
||
|
||
static void scr_rm_fail_set(ScrRmFail *f, int err, const char *op, const char *path, size_t len) {
|
||
if (f->err != 0) return; /* first failure wins, exactly like the old unwind */
|
||
f->err = err;
|
||
f->op = op;
|
||
f->path = scr_str_new(path, len);
|
||
}
|
||
|
||
static int scr_rm_unlink(const char *path, size_t len) {
|
||
if (unlink(path) == 0) return 0;
|
||
#ifdef _WIN32
|
||
/* Node's Windows removal clears a file's read-only attribute before
|
||
* retrying. Private staged runtime objects intentionally use mode 0400. */
|
||
const int original = errno;
|
||
if (original != EACCES && original != EPERM) return -1;
|
||
ScrStr *text = scr_str_new(path, len);
|
||
WCHAR *wide = scr_fs_win_wide(text);
|
||
scr_str_release(text);
|
||
if (!wide) { errno = original; return -1; }
|
||
const DWORD attributes = GetFileAttributesW(wide);
|
||
if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) &&
|
||
SetFileAttributesW(wide, attributes & ~FILE_ATTRIBUTE_READONLY)) {
|
||
if (DeleteFileW(wide)) { free(wide); return 0; }
|
||
const DWORD error = GetLastError();
|
||
SetFileAttributesW(wide, attributes);
|
||
free(wide);
|
||
errno = scr_fs_win_errno(error);
|
||
return -1;
|
||
}
|
||
free(wide);
|
||
errno = original;
|
||
#else
|
||
(void)len;
|
||
#endif
|
||
return -1;
|
||
}
|
||
|
||
/* Post-order tree removal for rmSync's recursive form. Stops at (and
|
||
* records) the first failure, with the failing path and syscall name. */
|
||
static void scr_rm_tree_e(const char *path, size_t len, ScrRmFail *f) {
|
||
#ifdef _WIN32
|
||
int link = scr_fs_win_remove_link(path, len);
|
||
if (link != 0) { if (link < 0) scr_rm_fail_set(f, errno, "unlink", path, len); return; }
|
||
#endif
|
||
struct stat st;
|
||
if (lstat(path, &st) != 0) {
|
||
scr_rm_fail_set(f, errno, "lstat", path, len);
|
||
return;
|
||
}
|
||
if (!S_ISDIR(st.st_mode)) {
|
||
if (scr_rm_unlink(path, len) != 0) scr_rm_fail_set(f, errno, "unlink", path, len);
|
||
return;
|
||
}
|
||
DIR *d = opendir(path);
|
||
if (!d) {
|
||
scr_rm_fail_set(f, errno, "scandir", path, len);
|
||
return;
|
||
}
|
||
const struct dirent *ent;
|
||
while ((ent = readdir(d)) != NULL) {
|
||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue;
|
||
size_t namelen = strlen(ent->d_name);
|
||
char *child = malloc(len + 1 + namelen + 1);
|
||
if (!child) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
memcpy(child, path, len);
|
||
child[len] = '/';
|
||
memcpy(child + len + 1, ent->d_name, namelen + 1);
|
||
scr_rm_tree_e(child, len + 1 + namelen, f);
|
||
free(child);
|
||
if (f->err != 0) {
|
||
closedir(d);
|
||
return;
|
||
}
|
||
}
|
||
closedir(d);
|
||
if (rmdir(path) != 0) scr_rm_fail_set(f, errno, "rmdir", path, len);
|
||
}
|
||
|
||
/* One rm attempt (the shared core of both option forms): lstat dispatch,
|
||
* force's ENOENT swallow, the non-recursive directory rejection, and the
|
||
* tree walk — failures recorded in `f`, never thrown. */
|
||
static void scr_fs_rm_attempt(ScrStr *path, bool recursive, bool force, ScrRmFail *f) {
|
||
#ifdef _WIN32
|
||
int link = scr_fs_win_remove_link(path->data, path->len);
|
||
if (link != 0) { if (link < 0) scr_rm_fail_set(f, errno, "unlink", path->data, path->len); return; }
|
||
#endif
|
||
struct stat st;
|
||
if (lstat(path->data, &st) != 0) {
|
||
if (force && errno == ENOENT) return; /* Node: force swallows ENOENT */
|
||
scr_rm_fail_set(f, errno, "lstat", path->data, path->len);
|
||
return;
|
||
}
|
||
if (S_ISDIR(st.st_mode)) {
|
||
if (!recursive) {
|
||
/* Node throws ERR_FS_EISDIR here; the EISDIR-prefixed wording is
|
||
* divergence 13's documented difference. */
|
||
scr_rm_fail_set(f, EISDIR, "rm", path->data, path->len);
|
||
return;
|
||
}
|
||
scr_rm_tree_e(path->data, path->len, f);
|
||
return;
|
||
}
|
||
if (scr_rm_unlink(path->data, path->len) != 0) scr_rm_fail_set(f, errno, "unlink", path->data, path->len);
|
||
}
|
||
|
||
void scr_fs_rm_opts(ScrStr *path, bool recursive, bool force) {
|
||
ScrRmFail f = {0, NULL, NULL};
|
||
scr_fs_rm_attempt(path, recursive, force, &f);
|
||
if (f.err != 0) {
|
||
scr_fs_throw(f.err, f.op, f.path);
|
||
scr_str_release(f.path);
|
||
}
|
||
}
|
||
|
||
/* rmSync(p, { recursive, force, maxRetries, retryDelay }): Node retries
|
||
* the operation on EBUSY/EMFILE/ENFILE/ENOTEMPTY/EPERM up to maxRetries
|
||
* times, waiting retryDelay ms LONGER on each try (linear backoff — the
|
||
* fs.rmSync documented semantics). Everything else throws immediately
|
||
* with the failing path, exactly like the plain options form. */
|
||
void scr_fs_rm_opts_retry(ScrStr *path, bool recursive, bool force, double max_retries, double retry_delay) {
|
||
long tries = max_retries > 0 ? (long)max_retries : 0;
|
||
double delay_ms = retry_delay > 0 ? retry_delay : 0;
|
||
ScrRmFail f = {0, NULL, NULL};
|
||
for (long attempt = 0;; attempt++) {
|
||
f.err = 0;
|
||
f.op = NULL;
|
||
if (f.path) {
|
||
scr_str_release(f.path);
|
||
f.path = NULL;
|
||
}
|
||
scr_fs_rm_attempt(path, recursive, force, &f);
|
||
if (f.err == 0) return;
|
||
bool retryable = f.err == EBUSY || f.err == EMFILE || f.err == ENFILE ||
|
||
f.err == ENOTEMPTY || f.err == EPERM;
|
||
if (!retryable || attempt >= tries) break;
|
||
double ms = delay_ms * (double)(attempt + 1);
|
||
if (ms > 0) {
|
||
struct timespec ts;
|
||
ts.tv_sec = (time_t)(ms / 1000.0);
|
||
ts.tv_nsec = (long)((ms - (double)ts.tv_sec * 1000.0) * 1000000.0);
|
||
nanosleep(&ts, NULL);
|
||
}
|
||
}
|
||
scr_fs_throw(f.err, f.op, f.path);
|
||
scr_str_release(f.path);
|
||
}
|
||
|
||
#if defined(_WIN32) || defined(__wasi__)
|
||
/* No mkdtemp in the Windows CRT or WASI libc: libuv's own fallback shape — six random
|
||
* [a-z0-9] name characters from the CSPRNG, retried on EEXIST. */
|
||
static char *scr_portable_mkdtemp(char *tmpl) {
|
||
static const char cs[] = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||
size_t len = strlen(tmpl);
|
||
for (int tries = 0; tries < 32; tries++) {
|
||
unsigned char r[6];
|
||
arc4random_buf(r, sizeof r);
|
||
for (size_t i = 0; i < 6; i++) tmpl[len - 6 + i] = cs[r[i] % 36];
|
||
if (scr_sys_mkdir(tmpl, 0777) == 0) return tmpl;
|
||
if (errno != EEXIST) break;
|
||
}
|
||
memcpy(tmpl + len - 6, "XXXXXX", 6); /* the error message shows the template */
|
||
return NULL;
|
||
}
|
||
#define mkdtemp scr_portable_mkdtemp
|
||
#endif
|
||
|
||
ScrStr *scr_fs_mkdtemp(ScrStr *prefix) {
|
||
char *tmpl = malloc(prefix->len + 7);
|
||
if (!tmpl) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
memcpy(tmpl, prefix->data, prefix->len);
|
||
memcpy(tmpl + prefix->len, "XXXXXX", 7);
|
||
if (!mkdtemp(tmpl)) {
|
||
/* Node reports the template, X's included: mkdtemp '/nope/x-XXXXXX' */
|
||
int e = errno;
|
||
ScrStr *shown = scr_str_new(tmpl, prefix->len + 6);
|
||
scr_fs_throw(e, "mkdtemp", shown);
|
||
scr_str_release(shown);
|
||
free(tmpl);
|
||
return NULL;
|
||
}
|
||
ScrStr *out = scr_str_new(tmpl, prefix->len + 6);
|
||
free(tmpl);
|
||
return out;
|
||
}
|
||
|
||
void scr_fs_access(ScrStr *path, double mode) {
|
||
int m = (int)mode;
|
||
#ifdef _WIN32
|
||
/* The CRT access() rejects X_OK (there is no execute bit); Node on
|
||
* Windows treats X_OK as F_OK, so mask it down to the R/W bits. */
|
||
m &= 6;
|
||
#endif
|
||
if (access(path->data, m) != 0) scr_fs_throw(errno, "access", path);
|
||
}
|
||
|
||
/* The no-path variant of the fs error shape — Node's fd reads report
|
||
* "EBADF: bad file descriptor, read" with no quoted path. */
|
||
static void scr_fs_throw_nopath(int e, const char *op) {
|
||
char namebuf[16];
|
||
const char *name = scr_errno_name(e, namebuf, sizeof namebuf);
|
||
const char *text = scr_errno_text(e);
|
||
char msg[256];
|
||
int len = snprintf(msg, sizeof msg, "%s: %s, %s", name, text, op);
|
||
scr_fs_throw_details(e, name, msg, (size_t)len, op, NULL, NULL);
|
||
}
|
||
|
||
ScrStr *scr_fs_read_fd(double fd) {
|
||
ScrFileRead out;
|
||
int error = scr_file_read_all(NULL, (int)fd, true, &out);
|
||
if (error) { scr_fs_throw_nopath(error, "read"); return NULL; }
|
||
return out.text;
|
||
}
|
||
|
||
ScrBytes *scr_fs_read_fd_bytes(double fd) {
|
||
ScrFileRead out;
|
||
int error = scr_file_read_all(NULL, (int)fd, false, &out);
|
||
if (error) { scr_fs_throw_nopath(error, "read"); return NULL; }
|
||
return scr_bytes_take_data(out.bytes, out.len);
|
||
}
|
||
|
||
/* ── the tty probes ──────────────────────────────────────────────────── */
|
||
|
||
bool scr_process_is_tty(double fd) {
|
||
if (!isfinite(fd) || fd < 0 || fd > INT_MAX || trunc(fd) != fd) return false;
|
||
return isatty((int)fd) != 0;
|
||
}
|
||
|
||
/* Terminal geometry for process.stdout/stderr.columns/rows: ioctl(TIOCGWINSZ) on
|
||
* the stream's fd, exactly Node's tty.WriteStream source of truth. A
|
||
* non-TTY stream, or a terminal that refuses the ioctl, answers -1 and
|
||
* the emitter's union construction turns that into the undefined arm —
|
||
* Node's missing geometry on non-TTY streams. */
|
||
static double scr_process_dimension(double fd, bool rows) {
|
||
if (!isatty((int)fd)) return -1;
|
||
#ifdef _WIN32
|
||
/* The console buffer's visible window — libuv's uv_tty_get_winsize. */
|
||
HANDLE h = (HANDLE)_get_osfhandle((int)fd);
|
||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||
if (h == INVALID_HANDLE_VALUE || !GetConsoleScreenBufferInfo(h, &info)) return -1;
|
||
return rows ? (double)(info.srWindow.Bottom - info.srWindow.Top + 1)
|
||
: (double)(info.srWindow.Right - info.srWindow.Left + 1);
|
||
#elif defined(__wasi__)
|
||
(void)rows;
|
||
return -1;
|
||
#else
|
||
struct winsize ws;
|
||
if (ioctl((int)fd, TIOCGWINSZ, &ws) != 0) return -1;
|
||
return rows ? (double)ws.ws_row : (double)ws.ws_col;
|
||
#endif
|
||
}
|
||
|
||
double scr_process_columns(double fd) { return scr_process_dimension(fd, false); }
|
||
double scr_process_rows(double fd) { return scr_process_dimension(fd, true); }
|
||
|
||
/* process.stdin.setRawMode(mode). TTY stdin: libuv's UV_TTY_MODE_RAW
|
||
* termios flag set — exactly what Node's setRawMode(true) applies — and
|
||
* setRawMode(false) restores the termios saved at the first raw entry
|
||
* (libuv's orig_termios), a no-op when raw mode was never entered.
|
||
* NON-TTY stdin: Node's process.stdin is a Socket with no setRawMode
|
||
* member at all, so the call throws Node's exact catchable TypeError. */
|
||
#ifdef _WIN32
|
||
static SCR_TL DWORD scr_stdin_cooked;
|
||
static SCR_TL bool scr_stdin_cooked_saved = false;
|
||
|
||
void scr_process_stdin_set_raw_mode(bool raw) {
|
||
if (!isatty(0)) {
|
||
const char msg[] = "process.stdin.setRawMode is not a function";
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||
return;
|
||
}
|
||
/* libuv's uv_tty_set_mode(UV_TTY_MODE_RAW) console half: drop line
|
||
* buffering, echo, and Ctrl-C cooking; restore the entry mode on the
|
||
* way back — the termios save/restore shape, translated. */
|
||
HANDLE h = (HANDLE)_get_osfhandle(0);
|
||
DWORD mode;
|
||
if (h == INVALID_HANDLE_VALUE || !GetConsoleMode(h, &mode)) return;
|
||
if (raw) {
|
||
if (!scr_stdin_cooked_saved) {
|
||
scr_stdin_cooked = mode;
|
||
scr_stdin_cooked_saved = true;
|
||
}
|
||
mode &= ~(DWORD)(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
|
||
(void)SetConsoleMode(h, mode);
|
||
} else if (scr_stdin_cooked_saved) {
|
||
(void)SetConsoleMode(h, scr_stdin_cooked);
|
||
}
|
||
}
|
||
#elif defined(__wasi__)
|
||
void scr_process_stdin_set_raw_mode(bool raw) {
|
||
(void)raw;
|
||
const char msg[] = "process.stdin.setRawMode is not a function";
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||
}
|
||
#else
|
||
static SCR_TL struct termios scr_stdin_cooked;
|
||
static SCR_TL bool scr_stdin_cooked_saved = false;
|
||
|
||
void scr_process_stdin_set_raw_mode(bool raw) {
|
||
if (!isatty(0)) {
|
||
const char msg[] = "process.stdin.setRawMode is not a function";
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||
return;
|
||
}
|
||
if (raw) {
|
||
struct termios t;
|
||
if (tcgetattr(0, &t) != 0) return;
|
||
if (!scr_stdin_cooked_saved) {
|
||
scr_stdin_cooked = t;
|
||
scr_stdin_cooked_saved = true;
|
||
}
|
||
/* libuv uv__tty_make_raw (UV_TTY_MODE_RAW) */
|
||
t.c_iflag &= (tcflag_t)~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
|
||
t.c_oflag |= (tcflag_t)ONLCR;
|
||
t.c_cflag |= (tcflag_t)CS8;
|
||
t.c_lflag &= (tcflag_t)~(ECHO | ICANON | IEXTEN | ISIG);
|
||
t.c_cc[VMIN] = 1;
|
||
t.c_cc[VTIME] = 0;
|
||
(void)tcsetattr(0, TCSADRAIN, &t);
|
||
} else if (scr_stdin_cooked_saved) {
|
||
(void)tcsetattr(0, TCSADRAIN, &scr_stdin_cooked);
|
||
}
|
||
}
|
||
#endif /* _WIN32 */
|
||
|
||
/* Node's destroy() tears down the stream: the events unit (scr_events.c)
|
||
* drops every stdin listener, stops watching fd 0, and ends a running
|
||
* for-await — nothing fires after, and the loop stops keeping the
|
||
* process alive for stdin. With the events unit not linked there is
|
||
* nothing to tear down and the call is a no-op, as before. */
|
||
void scr_process_stdin_destroy(void) {
|
||
if (scr_stdin_destroy_hook != NULL) scr_stdin_destroy_hook();
|
||
}
|
||
|
||
/* ── Stats values ────────────────────────────────────────────────────
|
||
* An immutable snapshot of stat(2) results — the lowered type/mode, device,
|
||
* inode, size, allocation/link, and access/write/change-time slice. statSync
|
||
* THROWS like the other sync fs calls; the promise form rejects (see the fsp
|
||
* section). */
|
||
|
||
struct ScrStats {
|
||
size_t rc;
|
||
bool is_file;
|
||
bool is_dir;
|
||
bool is_symlink; /* lstat only — a followed stat never sees one */
|
||
double dev;
|
||
double ino;
|
||
double size;
|
||
double blocks; /* allocated size in 512-byte units (Node/libuv) */
|
||
double nlink;
|
||
double atime_ms;
|
||
double mtime_ms; /* milliseconds with the nanosecond fraction (Node) */
|
||
double ctime_ms;
|
||
};
|
||
|
||
ScrStats *scr_stats_retain(ScrStats *s) {
|
||
if (s->rc != SIZE_MAX) s->rc++;
|
||
return s;
|
||
}
|
||
|
||
void scr_stats_release(ScrStats *s) {
|
||
if (!s || s->rc == SIZE_MAX) return;
|
||
if (--s->rc == 0) free(s);
|
||
}
|
||
|
||
void *scr_stats_retain_v(void *p) { return scr_stats_retain(p); }
|
||
void scr_stats_release_v(void *p) { scr_stats_release(p); }
|
||
|
||
bool scr_stats_is_file(ScrStats *s) { return s->is_file; }
|
||
bool scr_stats_is_dir(ScrStats *s) { return s->is_dir; }
|
||
bool scr_stats_is_symlink(ScrStats *s) { return s->is_symlink; }
|
||
double scr_stats_dev(ScrStats *s) { return s->dev; }
|
||
double scr_stats_ino(ScrStats *s) { return s->ino; }
|
||
double scr_stats_size(ScrStats *s) { return s->size; }
|
||
double scr_stats_blocks(ScrStats *s) { return s->blocks; }
|
||
double scr_stats_nlink(ScrStats *s) { return s->nlink; }
|
||
double scr_stats_atime_ms(ScrStats *s) { return s->atime_ms; }
|
||
double scr_stats_mtime_ms(ScrStats *s) { return s->mtime_ms; }
|
||
double scr_stats_ctime_ms(ScrStats *s) { return s->ctime_ms; }
|
||
|
||
static ScrStats *scr_stats_new(void) {
|
||
ScrStats *s = malloc(sizeof(ScrStats));
|
||
if (!s) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
s->rc = 1;
|
||
return s;
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
/* The CRT fallback is deliberately complete: callers either get every field
|
||
* from this one stat() result or every field from one Win32 handle below,
|
||
* never a snapshot spliced across two path resolutions. */
|
||
static ScrStats *scr_stats_of_crt(const struct stat *st) {
|
||
ScrStats *s = scr_stats_new();
|
||
s->is_file = S_ISREG(st->st_mode);
|
||
s->is_dir = S_ISDIR(st->st_mode);
|
||
s->is_symlink = false;
|
||
s->dev = (double)st->st_dev;
|
||
s->ino = (double)st->st_ino;
|
||
s->size = (double)st->st_size;
|
||
s->blocks = st->st_size <= 0 ? 0.0 : (double)(((uint64_t)st->st_size + 511) >> 9);
|
||
s->nlink = (double)st->st_nlink;
|
||
s->atime_ms = (double)st->st_atime * 1000.0;
|
||
s->mtime_ms = (double)st->st_mtime * 1000.0;
|
||
s->ctime_ms = (double)st->st_ctime * 1000.0;
|
||
return s;
|
||
}
|
||
|
||
static ScrStats *scr_stats_crt_fallback(const ScrStr *path, const char *op) {
|
||
struct stat st;
|
||
if (stat(path->data, &st) != 0) {
|
||
scr_fs_throw(errno, op, path);
|
||
return NULL;
|
||
}
|
||
return scr_stats_of_crt(&st);
|
||
}
|
||
|
||
/* Resolve ordinary disk paths once and populate every public field from the
|
||
* resulting handle. Splitting size/type across CRT stat() and a later
|
||
* CreateFile call can mix two entries when another process replaces path. */
|
||
static bool scr_stats_is_link_tag(DWORD tag) {
|
||
return tag == IO_REPARSE_TAG_SYMLINK ||
|
||
tag == IO_REPARSE_TAG_MOUNT_POINT ||
|
||
tag == IO_REPARSE_TAG_APPEXECLINK ||
|
||
tag == UINT32_C(0xA000001D); /* IO_REPARSE_TAG_LX_SYMLINK */
|
||
}
|
||
|
||
static double scr_stats_utf16_len(const WCHAR *text, size_t len) {
|
||
if (len == 0) return 0;
|
||
if (len > INT_MAX) return -1;
|
||
int bytes = WideCharToMultiByte(
|
||
CP_UTF8, 0, text, (int)len, NULL, 0, NULL, NULL);
|
||
return bytes > 0 ? (double)bytes : -1;
|
||
}
|
||
|
||
typedef struct {
|
||
ULONG tag;
|
||
USHORT data_len;
|
||
USHORT reserved;
|
||
union {
|
||
struct {
|
||
USHORT substitute_offset;
|
||
USHORT substitute_len;
|
||
USHORT print_offset;
|
||
USHORT print_len;
|
||
ULONG flags;
|
||
WCHAR path[1];
|
||
} symlink;
|
||
struct {
|
||
USHORT substitute_offset;
|
||
USHORT substitute_len;
|
||
USHORT print_offset;
|
||
USHORT print_len;
|
||
WCHAR path[1];
|
||
} mount;
|
||
struct {
|
||
unsigned char bytes[1];
|
||
} generic;
|
||
} body;
|
||
} ScrStatsReparseData;
|
||
|
||
/* Node/libuv reports a Windows link's target-text length as st_size. The
|
||
* ordinary handle information does not carry that value, so read the reparse
|
||
* payload while the no-follow handle is live. */
|
||
static bool scr_stats_link_target(HANDLE h, DWORD tag, double *size_out, ScrStr **text_out) {
|
||
union {
|
||
ScrStatsReparseData align;
|
||
unsigned char bytes[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
|
||
} storage;
|
||
DWORD used;
|
||
if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, NULL, 0,
|
||
storage.bytes, sizeof storage.bytes, &used, NULL)) return false;
|
||
ScrStatsReparseData *data = (ScrStatsReparseData *)storage.bytes;
|
||
const size_t body_offset = offsetof(ScrStatsReparseData, body);
|
||
if (used < body_offset || data->data_len > used - body_offset ||
|
||
data->tag != tag) return false;
|
||
|
||
const WCHAR *target;
|
||
size_t len;
|
||
if (tag == IO_REPARSE_TAG_SYMLINK) {
|
||
const size_t fixed = offsetof(ScrStatsReparseData, body.symlink.path) -
|
||
body_offset;
|
||
size_t offset = data->body.symlink.substitute_offset;
|
||
size_t bytes = data->body.symlink.substitute_len;
|
||
if (data->data_len < fixed || ((offset | bytes) & 1) != 0 ||
|
||
offset > data->data_len - fixed ||
|
||
bytes > data->data_len - fixed - offset) return false;
|
||
target = (const WCHAR *)((const unsigned char *)data->body.symlink.path +
|
||
offset);
|
||
len = bytes / sizeof(WCHAR);
|
||
} else if (tag == IO_REPARSE_TAG_MOUNT_POINT) {
|
||
const size_t fixed = offsetof(ScrStatsReparseData, body.mount.path) -
|
||
body_offset;
|
||
size_t offset = data->body.mount.substitute_offset;
|
||
size_t bytes = data->body.mount.substitute_len;
|
||
if (data->data_len < fixed || ((offset | bytes) & 1) != 0 ||
|
||
offset > data->data_len - fixed ||
|
||
bytes > data->data_len - fixed - offset) return false;
|
||
target = (const WCHAR *)((const unsigned char *)data->body.mount.path +
|
||
offset);
|
||
len = bytes / sizeof(WCHAR);
|
||
_Static_assert(sizeof(WCHAR) == sizeof(uint16_t),
|
||
"Windows reparse payloads use UTF-16 code units");
|
||
if (!scr_win_stats_mount_target_is_junction(
|
||
(const uint16_t *)target, len)) return false;
|
||
} else if (tag == UINT32_C(0xA000001D)) {
|
||
/* WSL's LX symlink payload is a version word followed by UTF-8 bytes. */
|
||
if (data->data_len < sizeof(ULONG)) return false;
|
||
*size_out = (double)(data->data_len - sizeof(ULONG));
|
||
if (text_out) *text_out = scr_str_new((const char *)data->body.generic.bytes + sizeof(ULONG), data->data_len - sizeof(ULONG));
|
||
return true;
|
||
} else {
|
||
/* App execution links carry a counted UTF-16 string list; the third
|
||
* string is the target path. */
|
||
if (tag != IO_REPARSE_TAG_APPEXECLINK ||
|
||
data->data_len < sizeof(ULONG)) return false;
|
||
const unsigned char *raw = data->body.generic.bytes;
|
||
ULONG count;
|
||
memcpy(&count, raw, sizeof count);
|
||
if (count < 3 || ((data->data_len - sizeof count) & 1) != 0) return false;
|
||
target = (const WCHAR *)(raw + sizeof count);
|
||
size_t chars = (data->data_len - sizeof count) / sizeof(WCHAR);
|
||
for (size_t item = 0; item < 2; item++) {
|
||
size_t part = 0;
|
||
while (part < chars && target[part] != L'\0') part++;
|
||
if (part == 0 || part == chars) return false;
|
||
target += part + 1;
|
||
chars -= part + 1;
|
||
}
|
||
len = 0;
|
||
while (len < chars && target[len] != L'\0') len++;
|
||
if (len == 0 || len == chars || len < 3 ||
|
||
!((target[0] >= L'A' && target[0] <= L'Z') ||
|
||
(target[0] >= L'a' && target[0] <= L'z')) ||
|
||
target[1] != L':' || target[2] != L'\\') return false;
|
||
double size = scr_stats_utf16_len(target, len);
|
||
if (size < 0) return false;
|
||
*size_out = size;
|
||
if (text_out) {
|
||
*text_out = scr_fs_win_utf8(target, len);
|
||
if (!*text_out) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* Undo the NT namespace prefix CreateSymbolicLinkW stores for absolute
|
||
* DOS/UNC targets, matching libuv's readlink normalization. */
|
||
if (len >= 4 && target[0] == L'\\' && target[1] == L'?' &&
|
||
target[2] == L'?' && target[3] == L'\\') {
|
||
if (len >= 6 && target[5] == L':' &&
|
||
((target[4] >= L'A' && target[4] <= L'Z') ||
|
||
(target[4] >= L'a' && target[4] <= L'z')) &&
|
||
(len == 6 || target[6] == L'\\')) {
|
||
target += 4;
|
||
len -= 4;
|
||
} else if (len >= 8 &&
|
||
(target[4] == L'U' || target[4] == L'u') &&
|
||
(target[5] == L'N' || target[5] == L'n') &&
|
||
(target[6] == L'C' || target[6] == L'c') &&
|
||
target[7] == L'\\') {
|
||
target += 6;
|
||
len -= 6;
|
||
}
|
||
}
|
||
double size = scr_stats_utf16_len(target, len);
|
||
if (size < 0) return false;
|
||
*size_out = size;
|
||
if (text_out) {
|
||
*text_out = scr_fs_win_utf8(target, len);
|
||
if (!*text_out) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static ScrStats *scr_stats_of_path(const ScrStr *path, const char *op,
|
||
bool no_follow) {
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) return scr_stats_crt_fallback(path, op);
|
||
|
||
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS;
|
||
if (no_follow) flags |= FILE_FLAG_OPEN_REPARSE_POINT;
|
||
HANDLE h = CreateFileW(wide, FILE_READ_ATTRIBUTES,
|
||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||
NULL, OPEN_EXISTING, flags, NULL);
|
||
if (h == INVALID_HANDLE_VALUE) {
|
||
DWORD error = GetLastError();
|
||
DWORD attributes = GetFileAttributesW(wide);
|
||
free(wide);
|
||
/* CRT stat can report the link entry when its target cannot be opened,
|
||
* hiding dangling links and Windows file/directory type mismatches.
|
||
* libuv preserves the failed follow operation for reparse points. */
|
||
if (!no_follow && attributes != INVALID_FILE_ATTRIBUTES &&
|
||
(attributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
|
||
scr_fs_throw(scr_fs_win_errno(error), op, path);
|
||
return NULL;
|
||
}
|
||
return scr_stats_crt_fallback(path, op);
|
||
}
|
||
free(wide);
|
||
|
||
DWORD file_type = GetFileType(h);
|
||
if (file_type != FILE_TYPE_DISK) {
|
||
CloseHandle(h);
|
||
return scr_stats_crt_fallback(path, op);
|
||
}
|
||
BY_HANDLE_FILE_INFORMATION basic;
|
||
if (!GetFileInformationByHandle(h, &basic)) {
|
||
CloseHandle(h);
|
||
return scr_stats_crt_fallback(path, op);
|
||
}
|
||
bool is_link = false;
|
||
double link_size = -1;
|
||
if (no_follow && (basic.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
|
||
FILE_ATTRIBUTE_TAG_INFO tagged;
|
||
if (!GetFileInformationByHandleEx(
|
||
h, FileAttributeTagInfo, &tagged, sizeof tagged) ||
|
||
!scr_stats_is_link_tag(tagged.ReparseTag)) {
|
||
/* Reparse points are a general interception mechanism. Node/libuv
|
||
* follows non-link tags even for lstat, so reopen the resolved target. */
|
||
CloseHandle(h);
|
||
return scr_stats_of_path(path, op, false);
|
||
}
|
||
if (!scr_stats_link_target(h, tagged.ReparseTag, &link_size, NULL)) {
|
||
/* A mount-point tag may name a mounted volume rather than a junction.
|
||
* libuv treats those (and malformed/unsupported link payloads) as
|
||
* ordinary reparse points and retries with the final component
|
||
* followed. Do not classify from the tag alone. */
|
||
CloseHandle(h);
|
||
return scr_stats_of_path(path, op, false);
|
||
}
|
||
is_link = true;
|
||
}
|
||
FILE_STANDARD_INFO standard;
|
||
bool have_standard = GetFileInformationByHandleEx(
|
||
h, FileStandardInfo, &standard, sizeof standard);
|
||
FILE_BASIC_INFO times;
|
||
bool have_times = GetFileInformationByHandleEx(
|
||
h, FileBasicInfo, ×, sizeof times);
|
||
CloseHandle(h);
|
||
|
||
bool is_dir = (basic.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||
uint64_t size = ((uint64_t)basic.nFileSizeHigh << 32) | basic.nFileSizeLow;
|
||
ScrStats *s = scr_stats_new();
|
||
s->is_file = !is_link && (have_standard ? !standard.Directory : !is_dir);
|
||
s->is_dir = !is_link && (have_standard ? standard.Directory : is_dir);
|
||
s->is_symlink = is_link;
|
||
s->dev = (double)basic.dwVolumeSerialNumber;
|
||
s->ino = (double)basic.nFileIndexHigh * 4294967296.0 +
|
||
(double)basic.nFileIndexLow;
|
||
s->size = is_link && link_size >= 0 ? link_size : s->is_dir ? 0.0
|
||
: have_standard ? (double)standard.EndOfFile.QuadPart : (double)size;
|
||
s->blocks = have_standard
|
||
? (double)((uint64_t)standard.AllocationSize.QuadPart >> 9)
|
||
: size == 0 ? 0.0 : (double)((size + 511) >> 9);
|
||
s->nlink = have_standard
|
||
? (double)standard.NumberOfLinks
|
||
: (double)basic.nNumberOfLinks;
|
||
s->atime_ms = scr_filetime_unix_ms(basic.ftLastAccessTime);
|
||
s->mtime_ms = scr_filetime_unix_ms(basic.ftLastWriteTime);
|
||
if (have_times) {
|
||
FILETIME changed;
|
||
changed.dwLowDateTime = times.ChangeTime.LowPart;
|
||
changed.dwHighDateTime = times.ChangeTime.HighPart;
|
||
s->ctime_ms = scr_filetime_unix_ms(changed);
|
||
} else {
|
||
s->ctime_ms = s->mtime_ms;
|
||
}
|
||
return s;
|
||
}
|
||
#else
|
||
static ScrStats *scr_stats_of(const struct stat *st) {
|
||
ScrStats *s = scr_stats_new();
|
||
s->is_file = S_ISREG(st->st_mode);
|
||
s->is_dir = S_ISDIR(st->st_mode);
|
||
s->is_symlink = S_ISLNK(st->st_mode);
|
||
s->dev = (double)st->st_dev;
|
||
s->ino = (double)st->st_ino;
|
||
#if defined(__APPLE__)
|
||
s->atime_ms = (double)st->st_atimespec.tv_sec * 1000.0 +
|
||
(double)st->st_atimespec.tv_nsec / 1e6;
|
||
s->mtime_ms = (double)st->st_mtimespec.tv_sec * 1000.0 +
|
||
(double)st->st_mtimespec.tv_nsec / 1e6;
|
||
s->ctime_ms = (double)st->st_ctimespec.tv_sec * 1000.0 +
|
||
(double)st->st_ctimespec.tv_nsec / 1e6;
|
||
#else
|
||
s->atime_ms = (double)st->st_atim.tv_sec * 1000.0 +
|
||
(double)st->st_atim.tv_nsec / 1e6;
|
||
s->mtime_ms = (double)st->st_mtim.tv_sec * 1000.0 +
|
||
(double)st->st_mtim.tv_nsec / 1e6;
|
||
s->ctime_ms = (double)st->st_ctim.tv_sec * 1000.0 +
|
||
(double)st->st_ctim.tv_nsec / 1e6;
|
||
#endif
|
||
s->blocks = (double)st->st_blocks;
|
||
s->nlink = (double)st->st_nlink;
|
||
s->size = (double)st->st_size;
|
||
return s;
|
||
}
|
||
#endif
|
||
|
||
/* Symbolic link targets are stored verbatim on POSIX. Windows junctions
|
||
* instead carry an absolute NT path in a bounded mount-point reparse record. */
|
||
#ifdef _WIN32
|
||
static DWORD scr_fs_win_junction(const WCHAR *target, const WCHAR *destination) {
|
||
size_t len = wcslen(target);
|
||
/* libuv permits drive-absolute junctions, not UNC mount targets. */
|
||
size_t skip = len >= 4 && !wcsncmp(target, L"\\\\?\\", 4) ? 4 : 0;
|
||
const WCHAR *plain = target + skip;
|
||
size_t chars = len - skip;
|
||
if (chars < 3 || !((plain[0] >= L'A' && plain[0] <= L'Z') || (plain[0] >= L'a' && plain[0] <= L'z')) || plain[1] != L':' || plain[2] != L'\\') return ERROR_INVALID_PARAMETER;
|
||
size_t fixed = offsetof(ScrStatsReparseData, body.mount.path);
|
||
size_t total = fixed + (4 + chars + 1 + chars + 1) * sizeof(WCHAR);
|
||
if (total > MAXIMUM_REPARSE_DATA_BUFFER_SIZE) return ERROR_FILENAME_EXCED_RANGE;
|
||
ScrStatsReparseData *data = calloc(1, total);
|
||
if (!data) return ERROR_NOT_ENOUGH_MEMORY;
|
||
data->tag = IO_REPARSE_TAG_MOUNT_POINT;
|
||
data->data_len = (USHORT)(total - offsetof(ScrStatsReparseData, body));
|
||
data->body.mount.substitute_offset = 0;
|
||
data->body.mount.substitute_len = (USHORT)((4 + chars) * sizeof(WCHAR));
|
||
data->body.mount.print_offset = (USHORT)((4 + chars + 1) * sizeof(WCHAR));
|
||
data->body.mount.print_len = (USHORT)(chars * sizeof(WCHAR));
|
||
memcpy(data->body.mount.path, L"\\??\\", 4 * sizeof(WCHAR));
|
||
memcpy(data->body.mount.path + 4, plain, chars * sizeof(WCHAR));
|
||
memcpy(data->body.mount.path + 4 + chars + 1, plain, chars * sizeof(WCHAR));
|
||
DWORD error = 0, used;
|
||
bool created = CreateDirectoryW(destination, NULL) != 0;
|
||
HANDLE h = INVALID_HANDLE_VALUE;
|
||
if (!created) error = GetLastError();
|
||
else {
|
||
h = CreateFileW(destination, GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
|
||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
|
||
if (h == INVALID_HANDLE_VALUE) error = GetLastError();
|
||
else if (!DeviceIoControl(h, FSCTL_SET_REPARSE_POINT, data, (DWORD)total, NULL, 0, &used, NULL)) error = GetLastError();
|
||
}
|
||
if (h != INVALID_HANDLE_VALUE) CloseHandle(h);
|
||
if (error && created) RemoveDirectoryW(destination);
|
||
free(data);
|
||
return error;
|
||
}
|
||
#endif
|
||
|
||
#ifdef _WIN32
|
||
/* libuv keeps this fallback for later calls once Windows rejects the flag. */
|
||
static SCR_TL DWORD scr_fs_symlink_usermode_flag = 0x2; /* ALLOW_UNPRIVILEGED_CREATE */
|
||
#endif
|
||
|
||
int scr_fs_symlink_infer(ScrStr *absolute) {
|
||
#ifdef _WIN32
|
||
WCHAR *probe = scr_fs_win_wide(absolute);
|
||
DWORD attributes = probe ? GetFileAttributesW(probe) : INVALID_FILE_ATTRIBUTES;
|
||
free(probe);
|
||
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0;
|
||
#else
|
||
(void)absolute;
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind) {
|
||
#ifdef _WIN32
|
||
static const ScrArrOps str_ops = { scr_str_retain_v, scr_str_release_v, NULL };
|
||
ScrArr *parts = scr_arr_new_ops(&str_ops, 3);
|
||
scr_arr_push_ref(parts, scr_str_retain(destination));
|
||
scr_arr_push_ref(parts, scr_str_new("..", 2));
|
||
scr_arr_push_ref(parts, scr_str_retain(target));
|
||
ScrStr *absolute = scr_path_win32_resolve(parts);
|
||
scr_arr_release(parts);
|
||
if (kind == -1) kind = scr_fs_symlink_infer(absolute);
|
||
ScrStr *stored;
|
||
if (kind == 2) stored = scr_path_win32_to_namespaced_path(absolute);
|
||
else if (scr_path_win32_is_absolute(target)) stored = scr_path_win32_to_namespaced_path(target);
|
||
else {
|
||
char *text = malloc(target->len ? target->len : 1);
|
||
if (!text) scr_trap("scriptc: out of memory\n");
|
||
for (size_t i = 0; i < target->len; i++) text[i] = target->data[i] == '/' ? '\\' : target->data[i];
|
||
stored = scr_str_new(text, target->len);
|
||
free(text);
|
||
}
|
||
scr_str_release(absolute);
|
||
/* Node's binding namespaces the link path before passing it to libuv. */
|
||
ScrStr *link_path = scr_path_win32_to_namespaced_path(destination);
|
||
WCHAR *from = scr_fs_win_wide(stored), *to = scr_fs_win_wide(link_path);
|
||
DWORD error = !from || !to ? GetLastError() : 0;
|
||
if (!error && kind == 2) error = scr_fs_win_junction(from, to);
|
||
else if (!error) {
|
||
DWORD flags = (kind == 1 ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | scr_fs_symlink_usermode_flag;
|
||
if (!CreateSymbolicLinkW(to, from, flags)) {
|
||
error = GetLastError();
|
||
if (error == ERROR_INVALID_PARAMETER && scr_fs_symlink_usermode_flag) {
|
||
scr_fs_symlink_usermode_flag = 0;
|
||
error = CreateSymbolicLinkW(to, from, flags & ~0x2u) ? 0 : GetLastError();
|
||
}
|
||
}
|
||
}
|
||
free(from); free(to); scr_str_release(stored); scr_str_release(link_path);
|
||
if (error) scr_fs_throw2(scr_fs_win_errno(error), "symlink", target, destination);
|
||
#else
|
||
(void)kind;
|
||
if (symlink(target->data, destination->data) != 0) scr_fs_throw2(errno, "symlink", target, destination);
|
||
#endif
|
||
}
|
||
|
||
/* Keep the unsigned libuv statfs representation before selecting Number or
|
||
* BigInt. Free-space counts are live snapshots, not cached volume metadata. */
|
||
ScrDyn *scr_fs_statfs(ScrStr *path, bool bigint) {
|
||
uint64_t fields[7];
|
||
#if defined(_WIN32)
|
||
ScrStr *syscall_path = scr_path_win32_to_namespaced_path(path);
|
||
WCHAR *wide = scr_fs_win_wide(syscall_path);
|
||
scr_str_release(syscall_path);
|
||
if (!wide) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "statfs", path); return NULL; }
|
||
DWORD sectors, bytes, free_clusters, total_clusters;
|
||
BOOL ok = GetDiskFreeSpaceW(wide, §ors, &bytes, &free_clusters, &total_clusters);
|
||
DWORD error = ok ? 0 : GetLastError();
|
||
if (!ok && error == ERROR_DIRECTORY) {
|
||
/* libuv retries a file path's parent once, preserving the original path
|
||
* in errors. GetFullPathName also expands relative and drive paths. */
|
||
DWORD size = GetFullPathNameW(wide, 0, NULL, NULL);
|
||
WCHAR *absolute = size ? malloc((size_t)size * sizeof *absolute) : NULL;
|
||
if (size && !absolute) scr_trap("scriptc: out of memory\n");
|
||
WCHAR *part = NULL;
|
||
DWORD length = absolute ? GetFullPathNameW(wide, size, absolute, &part) : 0;
|
||
if (length && length < size) {
|
||
if (part) *part = L'\0';
|
||
ok = GetDiskFreeSpaceW(absolute, §ors, &bytes, &free_clusters, &total_clusters);
|
||
if (!ok) error = GetLastError();
|
||
}
|
||
free(absolute);
|
||
}
|
||
free(wide);
|
||
if (!ok) { scr_fs_throw(scr_fs_win_errno(error), "statfs", path); return NULL; }
|
||
fields[0] = 0;
|
||
fields[1] = (DWORD)(bytes * sectors);
|
||
fields[2] = total_clusters;
|
||
fields[3] = fields[4] = free_clusters;
|
||
fields[5] = fields[6] = 0;
|
||
#elif defined(__wasi__)
|
||
(void)path; (void)bigint;
|
||
static const char message[] = "Filesystem capacity queries have no native WASI syscall";
|
||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||
return NULL;
|
||
#else
|
||
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
||
struct statfs snapshot;
|
||
if (statfs(path->data, &snapshot) != 0) { scr_fs_throw(errno, "statfs", path); return NULL; }
|
||
fields[0] = (uint64_t)snapshot.f_type;
|
||
#else
|
||
struct statvfs snapshot;
|
||
if (statvfs(path->data, &snapshot) != 0) { scr_fs_throw(errno, "statfs", path); return NULL; }
|
||
fields[0] = 0;
|
||
#endif
|
||
fields[1] = (uint64_t)snapshot.f_bsize;
|
||
fields[2] = (uint64_t)snapshot.f_blocks;
|
||
fields[3] = (uint64_t)snapshot.f_bfree;
|
||
fields[4] = (uint64_t)snapshot.f_bavail;
|
||
fields[5] = (uint64_t)snapshot.f_files;
|
||
fields[6] = (uint64_t)snapshot.f_ffree;
|
||
#endif
|
||
static const char *names[] = { "type", "bsize", "blocks", "bfree", "bavail", "files", "ffree" };
|
||
ScrDyn *result = scr_dyn_new_obj();
|
||
for (size_t i = 0; i < 7; i++) {
|
||
ScrDyn *value;
|
||
if (bigint) {
|
||
ScrBigInt *number = scr_bigint_from_u64(fields[i]);
|
||
value = scr_dyn_new_bigint(number);
|
||
scr_bigint_release(number);
|
||
} else value = scr_dyn_new_num((double)fields[i]);
|
||
scr_dyn_obj_set(result, names[i], strlen(names[i]), value);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
ScrBytes *scr_fs_readlink_bytes(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "readlink", path); return NULL; }
|
||
HANDLE h = CreateFileW(wide, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
|
||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||
free(wide);
|
||
if (h == INVALID_HANDLE_VALUE) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "readlink", path); return NULL; }
|
||
FILE_ATTRIBUTE_TAG_INFO tagged;
|
||
ScrStr *text = NULL;
|
||
double size;
|
||
bool ok = GetFileInformationByHandleEx(h, FileAttributeTagInfo, &tagged, sizeof tagged) &&
|
||
scr_stats_link_target(h, tagged.ReparseTag, &size, &text);
|
||
CloseHandle(h);
|
||
if (!ok || !text) { scr_str_release(text); scr_fs_throw(EINVAL, "readlink", path); return NULL; }
|
||
ScrBytes *bytes = scr_bytes_from_data((const uint8_t *)text->data, text->len);
|
||
scr_str_release(text);
|
||
return bytes;
|
||
#else
|
||
size_t capacity = 256;
|
||
uint8_t *text = NULL;
|
||
for (;;) {
|
||
uint8_t *grown = realloc(text, capacity);
|
||
if (!grown) { free(text); scr_trap("scriptc: out of memory\n"); }
|
||
text = grown;
|
||
ssize_t size = readlink(path->data, (char *)text, capacity);
|
||
if (size < 0) { int error = errno; free(text); scr_fs_throw(error, "readlink", path); return NULL; }
|
||
if ((size_t)size < capacity) {
|
||
ScrBytes *bytes = scr_bytes_from_data(text, (size_t)size);
|
||
free(text);
|
||
return bytes;
|
||
}
|
||
if (capacity > SIZE_MAX / 2) { free(text); scr_fs_throw(ENAMETOOLONG, "readlink", path); return NULL; }
|
||
capacity *= 2;
|
||
}
|
||
#endif
|
||
}
|
||
|
||
ScrStats *scr_fs_stat(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
return scr_stats_of_path(path, "stat", false);
|
||
#else
|
||
struct stat st;
|
||
if (stat(path->data, &st) != 0) { /* follows symlinks, like Node's statSync */
|
||
scr_fs_throw(errno, "stat", path);
|
||
return NULL;
|
||
}
|
||
return scr_stats_of(&st);
|
||
#endif
|
||
}
|
||
|
||
ScrStats *scr_fs_lstat(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
return scr_stats_of_path(path, "lstat", true);
|
||
#else
|
||
struct stat st;
|
||
if (lstat(path->data, &st) != 0) { /* no follow; Node reports lstat */
|
||
scr_fs_throw(errno, "lstat", path);
|
||
return NULL;
|
||
}
|
||
return scr_stats_of(&st);
|
||
#endif
|
||
}
|
||
|
||
ScrStats *scr_fs_fstat(double fd) {
|
||
struct stat st;
|
||
if (fstat((int)fd, &st) != 0) {
|
||
scr_fs_throw_nopath(errno, "fstat");
|
||
return NULL;
|
||
}
|
||
#ifdef _WIN32
|
||
ScrStats *s = scr_stats_new();
|
||
s->is_file = S_ISREG(st.st_mode);
|
||
s->is_dir = S_ISDIR(st.st_mode);
|
||
s->is_symlink = false;
|
||
s->dev = (double)st.st_dev;
|
||
s->ino = (double)st.st_ino;
|
||
s->size = (double)st.st_size;
|
||
s->blocks = s->size <= 0 ? 0 : ceil(s->size / 512.0);
|
||
s->nlink = (double)st.st_nlink;
|
||
s->atime_ms = (double)st.st_atime * 1000.0;
|
||
s->mtime_ms = (double)st.st_mtime * 1000.0;
|
||
s->ctime_ms = (double)st.st_ctime * 1000.0;
|
||
return s;
|
||
#else
|
||
return scr_stats_of(&st);
|
||
#endif
|
||
}
|
||
|
||
ScrArr *scr_fs_readdir(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
ScrScandir *scan = scr_fs_scandir(path);
|
||
if (!scan) return NULL;
|
||
ScrArr *arr = scr_arr_new(SCR_ELEM_STR, scr_fs_scandir_count(scan));
|
||
for (size_t i = 0; i < scr_fs_scandir_count(scan); i++) scr_arr_push_ref(arr, scr_fs_scandir_name(scan, i));
|
||
scr_fs_scandir_free(scan);
|
||
return arr;
|
||
#else
|
||
DIR *d = opendir(path->data);
|
||
if (!d) {
|
||
scr_fs_throw(errno, "scandir", path); /* Node reports scandir */
|
||
return NULL;
|
||
}
|
||
ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 8);
|
||
const struct dirent *ent;
|
||
while ((ent = readdir(d)) != NULL) {
|
||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue;
|
||
scr_arr_push_ref(arr, scr_str_new(ent->d_name, strlen(ent->d_name)));
|
||
}
|
||
closedir(d);
|
||
return arr; /* OS order, exactly like Node (unsorted) */
|
||
#endif
|
||
}
|
||
|
||
/* ── the withFileTypes scandir snapshot ──────────────────────────────
|
||
* One readdir pass capturing name + entry kind for the emitter's Dirent
|
||
* assembly (scr_runtime.h has the contract). Kinds are libuv's UV_DIRENT
|
||
* encoding; DT_UNKNOWN (filesystems that don't fill d_type) falls back
|
||
* to lstat(2) on the joined path — Node's own getDirents rule (a failed
|
||
* lstat leaves 0/unknown: every is*() probe answers false, like Node's
|
||
* Dirent over an UNKNOWN row it could not stat). */
|
||
|
||
struct ScrScandir {
|
||
size_t len, cap;
|
||
ScrStr **names;
|
||
unsigned char *kinds;
|
||
};
|
||
|
||
#ifndef _WIN32
|
||
static unsigned char scr_dirent_kind_of_mode(mode_t m) {
|
||
if (S_ISREG(m)) return 1;
|
||
if (S_ISDIR(m)) return 2;
|
||
#ifdef S_ISLNK /* no symlink/socket bits in the CRT's stat */
|
||
if (S_ISLNK(m)) return 3;
|
||
#endif
|
||
if (S_ISFIFO(m)) return 4;
|
||
#ifdef S_ISSOCK
|
||
if (S_ISSOCK(m)) return 5;
|
||
#endif
|
||
if (S_ISCHR(m)) return 6;
|
||
if (S_ISBLK(m)) return 7;
|
||
return 0;
|
||
}
|
||
#endif
|
||
|
||
#ifdef _WIN32
|
||
static ScrScandir *scr_fs_scandir_windows(ScrStr *path) {
|
||
WCHAR *wide = scr_fs_win_wide(path);
|
||
if (!wide) { scr_fs_throw(scr_fs_win_errno(GetLastError()), "scandir", path); return NULL; }
|
||
DWORD attrs = GetFileAttributesW(wide);
|
||
if (attrs == INVALID_FILE_ATTRIBUTES || !(attrs & FILE_ATTRIBUTE_DIRECTORY)) {
|
||
int error = attrs == INVALID_FILE_ATTRIBUTES ? scr_fs_win_errno(GetLastError()) : ENOTDIR;
|
||
free(wide);
|
||
scr_fs_throw(error, "scandir", path);
|
||
return NULL;
|
||
}
|
||
size_t length = wcslen(wide);
|
||
WCHAR *pattern = realloc(wide, (length + 3) * sizeof *wide);
|
||
if (!pattern) { free(wide); scr_trap("scriptc: out of memory\n"); }
|
||
if (length && pattern[length - 1] != L'\\' && pattern[length - 1] != L'/') pattern[length++] = L'\\';
|
||
pattern[length++] = L'*';
|
||
pattern[length] = 0;
|
||
WIN32_FIND_DATAW entry;
|
||
HANDLE search = FindFirstFileW(pattern, &entry);
|
||
DWORD error = search == INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS;
|
||
free(pattern);
|
||
if (search == INVALID_HANDLE_VALUE && error != ERROR_FILE_NOT_FOUND) {
|
||
scr_fs_throw(scr_fs_win_errno(error), "scandir", path);
|
||
return NULL;
|
||
}
|
||
ScrScandir *scan = calloc(1, sizeof *scan);
|
||
if (!scan) { if (search != INVALID_HANDLE_VALUE) FindClose(search); scr_trap("scriptc: out of memory\n"); }
|
||
if (search == INVALID_HANDLE_VALUE) return scan;
|
||
do {
|
||
if (wcscmp(entry.cFileName, L".") == 0 || wcscmp(entry.cFileName, L"..") == 0) continue;
|
||
ScrStr *name = scr_fs_win_utf8(entry.cFileName, wcslen(entry.cFileName));
|
||
if (!name) {
|
||
error = GetLastError();
|
||
FindClose(search);
|
||
scr_fs_scandir_free(scan);
|
||
scr_fs_throw(scr_fs_win_errno(error), "scandir", path);
|
||
return NULL;
|
||
}
|
||
if (scan->len == scan->cap) {
|
||
scan->cap = scan->cap ? scan->cap * 2 : 8;
|
||
scan->names = realloc(scan->names, scan->cap * sizeof *scan->names);
|
||
scan->kinds = realloc(scan->kinds, scan->cap);
|
||
if (!scan->names || !scan->kinds) scr_trap("scriptc: out of memory\n");
|
||
}
|
||
scan->names[scan->len] = name;
|
||
scan->kinds[scan->len++] = (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&
|
||
scr_stats_is_link_tag(entry.dwReserved0) ? 3 :
|
||
(entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? 2 : 1;
|
||
} while (FindNextFileW(search, &entry));
|
||
error = GetLastError();
|
||
FindClose(search);
|
||
if (error != ERROR_NO_MORE_FILES) {
|
||
scr_fs_scandir_free(scan);
|
||
scr_fs_throw(scr_fs_win_errno(error), "scandir", path);
|
||
return NULL;
|
||
}
|
||
return scan;
|
||
}
|
||
#endif
|
||
|
||
ScrScandir *scr_fs_scandir(ScrStr *path) {
|
||
#ifdef _WIN32
|
||
return scr_fs_scandir_windows(path);
|
||
#else
|
||
DIR *d = opendir(path->data);
|
||
if (!d) {
|
||
scr_fs_throw(errno, "scandir", path); /* Node reports scandir */
|
||
return NULL;
|
||
}
|
||
ScrScandir *s = malloc(sizeof *s);
|
||
if (!s) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
s->len = 0;
|
||
s->cap = 8;
|
||
s->names = malloc(s->cap * sizeof *s->names);
|
||
s->kinds = malloc(s->cap);
|
||
if (!s->names || !s->kinds) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
const struct dirent *ent;
|
||
while ((ent = readdir(d)) != NULL) {
|
||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue;
|
||
unsigned char kind;
|
||
#ifdef DT_REG
|
||
switch (ent->d_type) {
|
||
case DT_REG: kind = 1; break;
|
||
case DT_DIR: kind = 2; break;
|
||
case DT_LNK: kind = 3; break;
|
||
case DT_FIFO: kind = 4; break;
|
||
case DT_SOCK: kind = 5; break;
|
||
case DT_CHR: kind = 6; break;
|
||
case DT_BLK: kind = 7; break;
|
||
default: { /* DT_UNKNOWN: the lstat fallback */
|
||
char buf[4096];
|
||
int w = snprintf(buf, sizeof buf, "%s/%s", path->data, ent->d_name);
|
||
struct stat st;
|
||
kind = (w > 0 && (size_t)w < sizeof buf && lstat(buf, &st) == 0)
|
||
? scr_dirent_kind_of_mode(st.st_mode)
|
||
: 0;
|
||
break;
|
||
}
|
||
}
|
||
#else
|
||
{ /* mingw dirent has no d_type: every row takes the stat fallback
|
||
* (lstat degrades to stat on Windows — see the include seam). */
|
||
char buf[4096];
|
||
int w = snprintf(buf, sizeof buf, "%s/%s", path->data, ent->d_name);
|
||
struct stat st;
|
||
kind = (w > 0 && (size_t)w < sizeof buf && lstat(buf, &st) == 0)
|
||
? scr_dirent_kind_of_mode(st.st_mode)
|
||
: 0;
|
||
}
|
||
#endif
|
||
if (s->len == s->cap) {
|
||
s->cap *= 2;
|
||
s->names = realloc(s->names, s->cap * sizeof *s->names);
|
||
s->kinds = realloc(s->kinds, s->cap);
|
||
if (!s->names || !s->kinds) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
}
|
||
s->names[s->len] = scr_str_new(ent->d_name, strlen(ent->d_name));
|
||
s->kinds[s->len] = kind;
|
||
s->len++;
|
||
}
|
||
closedir(d);
|
||
return s;
|
||
#endif
|
||
}
|
||
|
||
size_t scr_fs_scandir_count(const ScrScandir *s) { return s ? s->len : 0; }
|
||
|
||
ScrStr *scr_fs_scandir_name(const ScrScandir *s, size_t i) {
|
||
return scr_str_retain(s->names[i]);
|
||
}
|
||
|
||
double scr_fs_scandir_type(const ScrScandir *s, size_t i) { return (double)s->kinds[i]; }
|
||
|
||
void scr_fs_scandir_free(ScrScandir *s) {
|
||
if (!s) return;
|
||
for (size_t i = 0; i < s->len; i++) scr_str_release(s->names[i]);
|
||
free(s->names);
|
||
free(s->kinds);
|
||
free(s);
|
||
}
|
||
|
||
/* ── node:crypto ─────────────────────────────────────────────────────
|
||
* Randomness comes from arc4random_buf. Hash/Hmac/PBKDF2 follow below;
|
||
* Buffer allocation itself lives in scr_bytes_io.c, while this unit owns
|
||
* the shared digest implementation and utility validation ladders. */
|
||
|
||
/* ── the scalar Math statics ─────────────────────────────────────────
|
||
* Math.min/max at two arguments: the ECMA folds — C's fmin/fmax are NOT
|
||
* these (they return the non-NaN operand where JS lets NaN poison, and
|
||
* leave the ±0 order unspecified). Math.random(): a uniform double in
|
||
* [0,1) at the spec's 53-bit granularity, drawn from arc4random_buf —
|
||
* the same CSPRNG behind the crypto lowerings. Same distribution as
|
||
* Node's, necessarily a different sequence (SEMANTICS.md 62); range and
|
||
* granularity are pinned differentially by invariant, not by bytes. */
|
||
|
||
double scr_math_min(double a, double b) {
|
||
if (isnan(a) || isnan(b)) return (double)NAN;
|
||
if (a == 0.0 && b == 0.0) return signbit(a) ? a : b; /* -0 wins */
|
||
return a < b ? a : b;
|
||
}
|
||
|
||
double scr_math_max(double a, double b) {
|
||
if (isnan(a) || isnan(b)) return (double)NAN;
|
||
if (a == 0.0 && b == 0.0) return signbit(a) ? b : a; /* +0 wins */
|
||
return a > b ? a : b;
|
||
}
|
||
|
||
/* Math.round: ECMA half-toward-+Infinity. NOT C round() (half away from
|
||
* zero: round(-1.5) is -2 where JS answers -1) and NOT floor(x + 0.5)
|
||
* (the float ADD drifts at the epsilon boundary: 0.49999999999999994 +
|
||
* 0.5 == 1.0 in doubles where the exact sum is below one — JS answers
|
||
* 0). x - floor(x) is EXACT for doubles (Sterbenz), so the fraction
|
||
* comparison decides losslessly; results in (-0.5, 0] keep the sign (JS:
|
||
* Math.round(-0.3) is -0). */
|
||
double scr_math_round(double x) {
|
||
if (isnan(x) || isinf(x) || x == 0.0) return x;
|
||
double f = floor(x);
|
||
double diff = x - f;
|
||
double r = diff < 0.5 ? f : f + 1.0;
|
||
return (r == 0.0 && x < 0.0) ? -0.0 : r;
|
||
}
|
||
|
||
/* IEEE pow(±1, ±Infinity) and pow(1, NaN) return 1; ECMAScript requires
|
||
* NaN. The other special cases, including signed zero and odd integer
|
||
* powers, follow libm. */
|
||
double scr_math_pow(double base, double exponent) {
|
||
if (isnan(exponent) || (isinf(exponent) && fabs(base) == 1.0)) return NAN;
|
||
return pow(base, exponent);
|
||
}
|
||
|
||
double scr_math_clz32(double x) {
|
||
uint32_t bits = scr_to_uint32(x);
|
||
if (bits == 0) return 32.0;
|
||
unsigned count = 0;
|
||
while ((bits & UINT32_C(0x80000000)) == 0) {
|
||
bits <<= 1;
|
||
count++;
|
||
}
|
||
return (double)count;
|
||
}
|
||
|
||
double scr_math_fround(double x) {
|
||
return (double)(float)x;
|
||
}
|
||
|
||
double scr_math_imul(double a, double b) {
|
||
uint32_t bits = scr_to_uint32(a) * scr_to_uint32(b);
|
||
return bits >= UINT32_C(0x80000000)
|
||
? (double)(uint64_t)bits - 4294967296.0
|
||
: (double)bits;
|
||
}
|
||
|
||
double scr_math_random(void) {
|
||
uint64_t r;
|
||
arc4random_buf(&r, sizeof r);
|
||
/* The top 53 bits scaled by 2^-53: every representable k/2^53 in [0,1)
|
||
* is equally likely — V8's own construction. */
|
||
return (double)(r >> 11) * 0x1.0p-53;
|
||
}
|
||
|
||
ScrStr *scr_crypto_random_uuid(void) {
|
||
unsigned char b[16];
|
||
arc4random_buf(b, sizeof b);
|
||
b[6] = (unsigned char)((b[6] & 0x0f) | 0x40); /* version 4 */
|
||
b[8] = (unsigned char)((b[8] & 0x3f) | 0x80); /* variant 10xx */
|
||
char out[37];
|
||
static const char hex[] = "0123456789abcdef";
|
||
size_t o = 0;
|
||
for (size_t i = 0; i < 16; i++) {
|
||
if (i == 4 || i == 6 || i == 8 || i == 10) out[o++] = '-';
|
||
out[o++] = hex[b[i] >> 4];
|
||
out[o++] = hex[b[i] & 0x0f];
|
||
}
|
||
return scr_str_new(out, 36);
|
||
}
|
||
|
||
/* randomBytes(n).toString(enc): n truncates like Node's (1.5 → 1 byte);
|
||
* out-of-range n THROWS Node's RangeError verbatim. enc is "hex" or
|
||
* "base64" (the compiler fences other encodings at the call site). */
|
||
ScrStr *scr_crypto_random_string(double n, ScrStr *enc) {
|
||
if (!(n >= 0 && n <= 2147483647)) {
|
||
char num[32];
|
||
size_t numlen = scr_f64_to_str(n, num);
|
||
char msg[128];
|
||
int mlen = snprintf(msg, sizeof msg,
|
||
"The value of \"size\" is out of range. It must be >= 0 && <= 2147483647. Received %.*s",
|
||
(int)numlen, num);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
size_t size = (size_t)n;
|
||
unsigned char *bytes = malloc(size ? size : 1);
|
||
if (!bytes) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
arc4random_buf(bytes, size);
|
||
ScrStr *out;
|
||
if (enc->len == 3 && memcmp(enc->data, "hex", 3) == 0) {
|
||
static const char hex[] = "0123456789abcdef";
|
||
char *buf = malloc(size * 2 + 1);
|
||
if (!buf) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
for (size_t i = 0; i < size; i++) {
|
||
buf[i * 2] = hex[bytes[i] >> 4];
|
||
buf[i * 2 + 1] = hex[bytes[i] & 0x0f];
|
||
}
|
||
out = scr_str_new(buf, size * 2);
|
||
free(buf);
|
||
} else {
|
||
/* base64, standard alphabet, '=' padded — Buffer.toString("base64"). */
|
||
static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||
size_t outlen = (size + 2) / 3 * 4;
|
||
char *buf = malloc(outlen + 1);
|
||
if (!buf) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
size_t o = 0;
|
||
for (size_t i = 0; i < size; i += 3) {
|
||
unsigned v = (unsigned)bytes[i] << 16;
|
||
if (i + 1 < size) v |= (unsigned)bytes[i + 1] << 8;
|
||
if (i + 2 < size) v |= (unsigned)bytes[i + 2];
|
||
buf[o++] = b64[(v >> 18) & 63];
|
||
buf[o++] = b64[(v >> 12) & 63];
|
||
buf[o++] = i + 1 < size ? b64[(v >> 6) & 63] : '=';
|
||
buf[o++] = i + 2 < size ? b64[v & 63] : '=';
|
||
}
|
||
out = scr_str_new(buf, o);
|
||
free(buf);
|
||
}
|
||
free(bytes);
|
||
return out;
|
||
}
|
||
|
||
static const uint32_t scr_sha256_k[64] = {
|
||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
||
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
||
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
||
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
||
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
||
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
|
||
|
||
static uint32_t scr_sha256_rotr(uint32_t x, unsigned n) {
|
||
return (x >> n) | (x << (32 - n));
|
||
}
|
||
|
||
static void scr_sha256_block(uint32_t h[8], const unsigned char *p) {
|
||
uint32_t w[64];
|
||
for (int i = 0; i < 16; i++) {
|
||
w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) |
|
||
((uint32_t)p[i * 4 + 2] << 8) | (uint32_t)p[i * 4 + 3];
|
||
}
|
||
for (int i = 16; i < 64; i++) {
|
||
uint32_t s0 = scr_sha256_rotr(w[i - 15], 7) ^ scr_sha256_rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
|
||
uint32_t s1 = scr_sha256_rotr(w[i - 2], 17) ^ scr_sha256_rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||
}
|
||
uint32_t a = h[0], b = h[1], c = h[2], d = h[3];
|
||
uint32_t e = h[4], f = h[5], g = h[6], hh = h[7];
|
||
for (int i = 0; i < 64; i++) {
|
||
uint32_t s1 = scr_sha256_rotr(e, 6) ^ scr_sha256_rotr(e, 11) ^ scr_sha256_rotr(e, 25);
|
||
uint32_t ch = (e & f) ^ (~e & g);
|
||
uint32_t t1 = hh + s1 + ch + scr_sha256_k[i] + w[i];
|
||
uint32_t s0 = scr_sha256_rotr(a, 2) ^ scr_sha256_rotr(a, 13) ^ scr_sha256_rotr(a, 22);
|
||
uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
|
||
uint32_t t2 = s0 + maj;
|
||
hh = g; g = f; f = e; e = d + t1;
|
||
d = c; c = b; b = a; a = t1 + t2;
|
||
}
|
||
h[0] += a; h[1] += b; h[2] += c; h[3] += d;
|
||
h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
|
||
}
|
||
|
||
typedef struct ScrSha256Ctx {
|
||
uint32_t h[8];
|
||
uint64_t bytes;
|
||
unsigned char tail[64];
|
||
size_t tail_len;
|
||
} ScrSha256Ctx;
|
||
|
||
static void scr_sha256_init(ScrSha256Ctx *ctx) {
|
||
static const uint32_t initial[8] = {
|
||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
|
||
memcpy(ctx->h, initial, sizeof initial);
|
||
ctx->bytes = 0;
|
||
ctx->tail_len = 0;
|
||
}
|
||
|
||
static void scr_sha256_update(ScrSha256Ctx *ctx, const unsigned char *data, size_t len) {
|
||
ctx->bytes += len;
|
||
if (ctx->tail_len != 0) {
|
||
size_t take = 64 - ctx->tail_len;
|
||
if (take > len) take = len;
|
||
memcpy(ctx->tail + ctx->tail_len, data, take);
|
||
ctx->tail_len += take;
|
||
data += take;
|
||
len -= take;
|
||
if (ctx->tail_len == 64) {
|
||
scr_sha256_block(ctx->h, ctx->tail);
|
||
ctx->tail_len = 0;
|
||
}
|
||
}
|
||
while (len >= 64) {
|
||
scr_sha256_block(ctx->h, data);
|
||
data += 64;
|
||
len -= 64;
|
||
}
|
||
if (len != 0) {
|
||
memcpy(ctx->tail, data, len);
|
||
ctx->tail_len = len;
|
||
}
|
||
}
|
||
|
||
/* Final block(s): the 0x80 terminator, zero padding, and 64-bit
|
||
* big-endian bit length. Finalization reads a snapshot so Hash.copy()
|
||
* remains a plain context copy and one-shot callers share this path. */
|
||
static size_t scr_sha256_final(const ScrSha256Ctx *source, unsigned char out[32]) {
|
||
ScrSha256Ctx ctx = *source;
|
||
unsigned char tail[128];
|
||
size_t rem = ctx.tail_len;
|
||
memcpy(tail, ctx.tail, rem);
|
||
tail[rem] = 0x80;
|
||
size_t pad = (rem + 1 + 8 <= 64) ? 64 : 128;
|
||
memset(tail + rem + 1, 0, pad - rem - 1 - 8);
|
||
uint64_t bits = ctx.bytes * 8;
|
||
for (int b = 0; b < 8; b++) tail[pad - 1 - b] = (unsigned char)(bits >> (8 * b));
|
||
scr_sha256_block(ctx.h, tail);
|
||
if (pad == 128) scr_sha256_block(ctx.h, tail + 64);
|
||
for (int j = 0; j < 8; j++) {
|
||
for (int b = 0; b < 4; b++) out[j * 4 + b] = (unsigned char)(ctx.h[j] >> (24 - 8 * b));
|
||
}
|
||
return 32;
|
||
}
|
||
|
||
/* ── SHA-1 (FIPS 180-4) — the RFC 6455 Sec-WebSocket-Accept hash ────── */
|
||
|
||
static void scr_sha1_block(uint32_t h[5], const unsigned char *p) {
|
||
uint32_t w[80];
|
||
for (int i = 0; i < 16; i++) {
|
||
w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) |
|
||
((uint32_t)p[i * 4 + 2] << 8) | (uint32_t)p[i * 4 + 3];
|
||
}
|
||
for (int i = 16; i < 80; i++) {
|
||
uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
|
||
w[i] = (x << 1) | (x >> 31);
|
||
}
|
||
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
|
||
for (int i = 0; i < 80; i++) {
|
||
uint32_t f, k;
|
||
if (i < 20) { f = (b & c) | (~b & d); k = 0x5a827999; }
|
||
else if (i < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; }
|
||
else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; }
|
||
else { f = b ^ c ^ d; k = 0xca62c1d6; }
|
||
uint32_t t = ((a << 5) | (a >> 27)) + f + e + k + w[i];
|
||
e = d; d = c; c = ((b << 30) | (b >> 2)); b = a; a = t;
|
||
}
|
||
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e;
|
||
}
|
||
|
||
typedef struct ScrSha1Ctx {
|
||
uint32_t h[5];
|
||
uint64_t bytes;
|
||
unsigned char tail[64];
|
||
size_t tail_len;
|
||
} ScrSha1Ctx;
|
||
|
||
static void scr_sha1_init(ScrSha1Ctx *ctx) {
|
||
static const uint32_t initial[5] = {
|
||
0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0};
|
||
memcpy(ctx->h, initial, sizeof initial);
|
||
ctx->bytes = 0;
|
||
ctx->tail_len = 0;
|
||
}
|
||
|
||
static void scr_sha1_update(ScrSha1Ctx *ctx, const unsigned char *data, size_t len) {
|
||
ctx->bytes += len;
|
||
if (ctx->tail_len != 0) {
|
||
size_t take = 64 - ctx->tail_len;
|
||
if (take > len) take = len;
|
||
memcpy(ctx->tail + ctx->tail_len, data, take);
|
||
ctx->tail_len += take;
|
||
data += take;
|
||
len -= take;
|
||
if (ctx->tail_len == 64) {
|
||
scr_sha1_block(ctx->h, ctx->tail);
|
||
ctx->tail_len = 0;
|
||
}
|
||
}
|
||
while (len >= 64) {
|
||
scr_sha1_block(ctx->h, data);
|
||
data += 64;
|
||
len -= 64;
|
||
}
|
||
if (len != 0) {
|
||
memcpy(ctx->tail, data, len);
|
||
ctx->tail_len = len;
|
||
}
|
||
}
|
||
|
||
static size_t scr_sha1_final(const ScrSha1Ctx *source, unsigned char out[32]) {
|
||
ScrSha1Ctx ctx = *source;
|
||
unsigned char tail[128];
|
||
size_t rem = ctx.tail_len;
|
||
memcpy(tail, ctx.tail, rem);
|
||
tail[rem] = 0x80;
|
||
size_t pad = (rem + 1 + 8 <= 64) ? 64 : 128;
|
||
memset(tail + rem + 1, 0, pad - rem - 1 - 8);
|
||
uint64_t bits = ctx.bytes * 8;
|
||
for (int b = 0; b < 8; b++) tail[pad - 1 - b] = (unsigned char)(bits >> (8 * b));
|
||
scr_sha1_block(ctx.h, tail);
|
||
if (pad == 128) scr_sha1_block(ctx.h, tail + 64);
|
||
for (int j = 0; j < 5; j++) {
|
||
for (int b = 0; b < 4; b++) out[j * 4 + b] = (unsigned char)(ctx.h[j] >> (24 - 8 * b));
|
||
}
|
||
return 20;
|
||
}
|
||
|
||
static size_t scr_sha1_digest(const unsigned char *data, size_t len, unsigned char out[32]) {
|
||
ScrSha1Ctx ctx;
|
||
scr_sha1_init(&ctx);
|
||
scr_sha1_update(&ctx, data, len);
|
||
return scr_sha1_final(&ctx, out);
|
||
}
|
||
|
||
/* The digest's encoding: "hex" or "base64" (compiler-fenced literals). */
|
||
static ScrStr *scr_digest_encode(const unsigned char *d, size_t n, const ScrStr *enc) {
|
||
if (enc->len == 3 && memcmp(enc->data, "hex", 3) == 0) {
|
||
static const char hex[] = "0123456789abcdef";
|
||
char buf[128];
|
||
for (size_t i = 0; i < n; i++) {
|
||
buf[i * 2] = hex[d[i] >> 4];
|
||
buf[i * 2 + 1] = hex[d[i] & 0x0f];
|
||
}
|
||
return scr_str_new(buf, n * 2);
|
||
}
|
||
/* base64, standard alphabet, '=' padded — Buffer.toString("base64"). */
|
||
static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||
char buf[88];
|
||
size_t o = 0;
|
||
for (size_t i = 0; i < n; i += 3) {
|
||
unsigned v = (unsigned)d[i] << 16;
|
||
if (i + 1 < n) v |= (unsigned)d[i + 1] << 8;
|
||
if (i + 2 < n) v |= (unsigned)d[i + 2];
|
||
buf[o++] = b64[(v >> 18) & 63];
|
||
buf[o++] = b64[(v >> 12) & 63];
|
||
buf[o++] = i + 1 < n ? b64[(v >> 6) & 63] : '=';
|
||
buf[o++] = i + 2 < n ? b64[v & 63] : '=';
|
||
}
|
||
return scr_str_new(buf, o);
|
||
}
|
||
|
||
/* ── MD5 (RFC 1321) ───────────────────────────────────────────────── */
|
||
|
||
static const uint32_t scr_md5_k[64] = {
|
||
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a,
|
||
0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
|
||
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340,
|
||
0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
|
||
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8,
|
||
0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
|
||
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa,
|
||
0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
|
||
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92,
|
||
0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
|
||
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
|
||
static const unsigned char scr_md5_r[64] = {
|
||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};
|
||
|
||
static void scr_md5_block(uint32_t h[4], const unsigned char *p) {
|
||
uint32_t m[16];
|
||
for (int i = 0; i < 16; i++) {
|
||
m[i] = (uint32_t)p[i * 4] | ((uint32_t)p[i * 4 + 1] << 8) |
|
||
((uint32_t)p[i * 4 + 2] << 16) | ((uint32_t)p[i * 4 + 3] << 24);
|
||
}
|
||
uint32_t a = h[0], b = h[1], c = h[2], d = h[3];
|
||
for (int i = 0; i < 64; i++) {
|
||
uint32_t f, g;
|
||
if (i < 16) { f = (b & c) | (~b & d); g = (uint32_t)i; }
|
||
else if (i < 32) { f = (d & b) | (~d & c); g = (5u * i + 1) & 15; }
|
||
else if (i < 48) { f = b ^ c ^ d; g = (3u * i + 5) & 15; }
|
||
else { f = c ^ (b | ~d); g = (7u * i) & 15; }
|
||
uint32_t t = d;
|
||
d = c;
|
||
c = b;
|
||
uint32_t x = a + f + scr_md5_k[i] + m[g];
|
||
b = b + ((x << scr_md5_r[i]) | (x >> (32 - scr_md5_r[i])));
|
||
a = t;
|
||
}
|
||
h[0] += a; h[1] += b; h[2] += c; h[3] += d;
|
||
}
|
||
|
||
typedef struct ScrMd5Ctx {
|
||
uint32_t h[4];
|
||
uint64_t bytes;
|
||
unsigned char tail[64];
|
||
size_t tail_len;
|
||
} ScrMd5Ctx;
|
||
|
||
static void scr_md5_init(ScrMd5Ctx *ctx) {
|
||
static const uint32_t initial[4] = {
|
||
0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476};
|
||
memcpy(ctx->h, initial, sizeof initial);
|
||
ctx->bytes = 0;
|
||
ctx->tail_len = 0;
|
||
}
|
||
|
||
static void scr_md5_update(ScrMd5Ctx *ctx, const unsigned char *data, size_t len) {
|
||
ctx->bytes += len;
|
||
if (ctx->tail_len != 0) {
|
||
size_t take = 64 - ctx->tail_len;
|
||
if (take > len) take = len;
|
||
memcpy(ctx->tail + ctx->tail_len, data, take);
|
||
ctx->tail_len += take;
|
||
data += take;
|
||
len -= take;
|
||
if (ctx->tail_len == 64) {
|
||
scr_md5_block(ctx->h, ctx->tail);
|
||
ctx->tail_len = 0;
|
||
}
|
||
}
|
||
while (len >= 64) {
|
||
scr_md5_block(ctx->h, data);
|
||
data += 64;
|
||
len -= 64;
|
||
}
|
||
if (len != 0) {
|
||
memcpy(ctx->tail, data, len);
|
||
ctx->tail_len = len;
|
||
}
|
||
}
|
||
|
||
static size_t scr_md5_final(const ScrMd5Ctx *source, unsigned char out[32]) {
|
||
ScrMd5Ctx ctx = *source;
|
||
unsigned char tail[128];
|
||
size_t rem = ctx.tail_len;
|
||
memcpy(tail, ctx.tail, rem);
|
||
tail[rem] = 0x80;
|
||
size_t pad = (rem + 1 + 8 <= 64) ? 64 : 128;
|
||
memset(tail + rem + 1, 0, pad - rem - 1 - 8);
|
||
uint64_t bits = ctx.bytes * 8;
|
||
for (int b = 0; b < 8; b++) tail[pad - 8 + b] = (unsigned char)(bits >> (8 * b));
|
||
scr_md5_block(ctx.h, tail);
|
||
if (pad == 128) scr_md5_block(ctx.h, tail + 64);
|
||
for (int j = 0; j < 4; j++) {
|
||
for (int b = 0; b < 4; b++) out[j * 4 + b] = (unsigned char)(ctx.h[j] >> (8 * b));
|
||
}
|
||
return 16;
|
||
}
|
||
|
||
/* SHA-384/SHA-512, FIPS 180-4. The 128-byte blocks and HMAC pads differ
|
||
* from SHA-256; the digest dispatcher keeps that distinction explicit. */
|
||
typedef struct ScrSha512Ctx {
|
||
uint64_t h[8];
|
||
uint64_t bytes;
|
||
unsigned char tail[128];
|
||
size_t tail_len;
|
||
} ScrSha512Ctx;
|
||
|
||
static const uint64_t scr_sha512_k[80] = {
|
||
UINT64_C(0x428a2f98d728ae22), UINT64_C(0x7137449123ef65cd), UINT64_C(0xb5c0fbcfec4d3b2f),
|
||
UINT64_C(0xe9b5dba58189dbbc), UINT64_C(0x3956c25bf348b538), UINT64_C(0x59f111f1b605d019),
|
||
UINT64_C(0x923f82a4af194f9b), UINT64_C(0xab1c5ed5da6d8118), UINT64_C(0xd807aa98a3030242),
|
||
UINT64_C(0x12835b0145706fbe), UINT64_C(0x243185be4ee4b28c), UINT64_C(0x550c7dc3d5ffb4e2),
|
||
UINT64_C(0x72be5d74f27b896f), UINT64_C(0x80deb1fe3b1696b1), UINT64_C(0x9bdc06a725c71235),
|
||
UINT64_C(0xc19bf174cf692694), UINT64_C(0xe49b69c19ef14ad2), UINT64_C(0xefbe4786384f25e3),
|
||
UINT64_C(0x0fc19dc68b8cd5b5), UINT64_C(0x240ca1cc77ac9c65), UINT64_C(0x2de92c6f592b0275),
|
||
UINT64_C(0x4a7484aa6ea6e483), UINT64_C(0x5cb0a9dcbd41fbd4), UINT64_C(0x76f988da831153b5),
|
||
UINT64_C(0x983e5152ee66dfab), UINT64_C(0xa831c66d2db43210), UINT64_C(0xb00327c898fb213f),
|
||
UINT64_C(0xbf597fc7beef0ee4), UINT64_C(0xc6e00bf33da88fc2), UINT64_C(0xd5a79147930aa725),
|
||
UINT64_C(0x06ca6351e003826f), UINT64_C(0x142929670a0e6e70), UINT64_C(0x27b70a8546d22ffc),
|
||
UINT64_C(0x2e1b21385c26c926), UINT64_C(0x4d2c6dfc5ac42aed), UINT64_C(0x53380d139d95b3df),
|
||
UINT64_C(0x650a73548baf63de), UINT64_C(0x766a0abb3c77b2a8), UINT64_C(0x81c2c92e47edaee6),
|
||
UINT64_C(0x92722c851482353b), UINT64_C(0xa2bfe8a14cf10364), UINT64_C(0xa81a664bbc423001),
|
||
UINT64_C(0xc24b8b70d0f89791), UINT64_C(0xc76c51a30654be30), UINT64_C(0xd192e819d6ef5218),
|
||
UINT64_C(0xd69906245565a910), UINT64_C(0xf40e35855771202a), UINT64_C(0x106aa07032bbd1b8),
|
||
UINT64_C(0x19a4c116b8d2d0c8), UINT64_C(0x1e376c085141ab53), UINT64_C(0x2748774cdf8eeb99),
|
||
UINT64_C(0x34b0bcb5e19b48a8), UINT64_C(0x391c0cb3c5c95a63), UINT64_C(0x4ed8aa4ae3418acb),
|
||
UINT64_C(0x5b9cca4f7763e373), UINT64_C(0x682e6ff3d6b2b8a3), UINT64_C(0x748f82ee5defb2fc),
|
||
UINT64_C(0x78a5636f43172f60), UINT64_C(0x84c87814a1f0ab72), UINT64_C(0x8cc702081a6439ec),
|
||
UINT64_C(0x90befffa23631e28), UINT64_C(0xa4506cebde82bde9), UINT64_C(0xbef9a3f7b2c67915),
|
||
UINT64_C(0xc67178f2e372532b), UINT64_C(0xca273eceea26619c), UINT64_C(0xd186b8c721c0c207),
|
||
UINT64_C(0xeada7dd6cde0eb1e), UINT64_C(0xf57d4f7fee6ed178), UINT64_C(0x06f067aa72176fba),
|
||
UINT64_C(0x0a637dc5a2c898a6), UINT64_C(0x113f9804bef90dae), UINT64_C(0x1b710b35131c471b),
|
||
UINT64_C(0x28db77f523047d84), UINT64_C(0x32caab7b40c72493), UINT64_C(0x3c9ebe0a15c9bebc),
|
||
UINT64_C(0x431d67c49c100d4c), UINT64_C(0x4cc5d4becb3e42b6), UINT64_C(0x597f299cfc657e2a),
|
||
UINT64_C(0x5fcb6fab3ad6faec), UINT64_C(0x6c44198c4a475817),
|
||
};
|
||
|
||
static uint64_t scr_sha512_rotr(uint64_t x, unsigned n) {
|
||
return (x >> n) | (x << (64 - n));
|
||
}
|
||
|
||
static void scr_sha512_block(uint64_t h[8], const unsigned char *data) {
|
||
uint64_t w[80];
|
||
for (size_t i = 0; i < 16; i++) {
|
||
w[i] = 0;
|
||
for (size_t b = 0; b < 8; b++) w[i] = (w[i] << 8) | data[i * 8 + b];
|
||
}
|
||
for (size_t i = 16; i < 80; i++) {
|
||
uint64_t s0 = scr_sha512_rotr(w[i - 15], 1) ^ scr_sha512_rotr(w[i - 15], 8) ^ (w[i - 15] >> 7);
|
||
uint64_t s1 = scr_sha512_rotr(w[i - 2], 19) ^ scr_sha512_rotr(w[i - 2], 61) ^ (w[i - 2] >> 6);
|
||
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||
}
|
||
uint64_t a = h[0], b = h[1], c = h[2], d = h[3];
|
||
uint64_t e = h[4], f = h[5], g = h[6], v = h[7];
|
||
for (size_t i = 0; i < 80; i++) {
|
||
uint64_t s1 = scr_sha512_rotr(e, 14) ^ scr_sha512_rotr(e, 18) ^ scr_sha512_rotr(e, 41);
|
||
uint64_t t1 = v + s1 + ((e & f) ^ (~e & g)) + scr_sha512_k[i] + w[i];
|
||
uint64_t s0 = scr_sha512_rotr(a, 28) ^ scr_sha512_rotr(a, 34) ^ scr_sha512_rotr(a, 39);
|
||
uint64_t t2 = s0 + ((a & b) ^ (a & c) ^ (b & c));
|
||
v = g; g = f; f = e; e = d + t1;
|
||
d = c; c = b; b = a; a = t1 + t2;
|
||
}
|
||
h[0] += a; h[1] += b; h[2] += c; h[3] += d;
|
||
h[4] += e; h[5] += f; h[6] += g; h[7] += v;
|
||
}
|
||
|
||
static void scr_sha512_init(ScrSha512Ctx *ctx, bool sha384) {
|
||
static const uint64_t initial512[8] = {
|
||
UINT64_C(0x6a09e667f3bcc908), UINT64_C(0xbb67ae8584caa73b),
|
||
UINT64_C(0x3c6ef372fe94f82b), UINT64_C(0xa54ff53a5f1d36f1),
|
||
UINT64_C(0x510e527fade682d1), UINT64_C(0x9b05688c2b3e6c1f),
|
||
UINT64_C(0x1f83d9abfb41bd6b), UINT64_C(0x5be0cd19137e2179)};
|
||
static const uint64_t initial384[8] = {
|
||
UINT64_C(0xcbbb9d5dc1059ed8), UINT64_C(0x629a292a367cd507),
|
||
UINT64_C(0x9159015a3070dd17), UINT64_C(0x152fecd8f70e5939),
|
||
UINT64_C(0x67332667ffc00b31), UINT64_C(0x8eb44a8768581511),
|
||
UINT64_C(0xdb0c2e0d64f98fa7), UINT64_C(0x47b5481dbefa4fa4)};
|
||
memcpy(ctx->h, sha384 ? initial384 : initial512, sizeof ctx->h);
|
||
ctx->bytes = 0;
|
||
ctx->tail_len = 0;
|
||
}
|
||
|
||
static void scr_sha512_update(ScrSha512Ctx *ctx, const unsigned char *data, size_t len) {
|
||
ctx->bytes += len;
|
||
if (ctx->tail_len != 0) {
|
||
size_t take = 128 - ctx->tail_len;
|
||
if (take > len) take = len;
|
||
memcpy(ctx->tail + ctx->tail_len, data, take);
|
||
ctx->tail_len += take;
|
||
data += take;
|
||
len -= take;
|
||
if (ctx->tail_len == 128) {
|
||
scr_sha512_block(ctx->h, ctx->tail);
|
||
ctx->tail_len = 0;
|
||
}
|
||
}
|
||
while (len >= 128) {
|
||
scr_sha512_block(ctx->h, data);
|
||
data += 128;
|
||
len -= 128;
|
||
}
|
||
if (len != 0) {
|
||
memcpy(ctx->tail, data, len);
|
||
ctx->tail_len = len;
|
||
}
|
||
}
|
||
|
||
static size_t scr_sha512_final(const ScrSha512Ctx *source, unsigned char out[64], bool sha384) {
|
||
ScrSha512Ctx ctx = *source;
|
||
unsigned char tail[256] = {0};
|
||
memcpy(tail, ctx.tail, ctx.tail_len);
|
||
tail[ctx.tail_len] = 0x80;
|
||
size_t pad = ctx.tail_len < 112 ? 128 : 256;
|
||
uint64_t low = ctx.bytes << 3, high = ctx.bytes >> 61;
|
||
for (size_t b = 0; b < 8; b++) {
|
||
tail[pad - 1 - b] = (unsigned char)(low >> (8 * b));
|
||
tail[pad - 9 - b] = (unsigned char)(high >> (8 * b));
|
||
}
|
||
scr_sha512_block(ctx.h, tail);
|
||
if (pad == 256) scr_sha512_block(ctx.h, tail + 128);
|
||
size_t len = sha384 ? 48 : 64;
|
||
for (size_t i = 0; i < len; i++) out[i] = (unsigned char)(ctx.h[i / 8] >> (56 - 8 * (i % 8)));
|
||
return len;
|
||
}
|
||
|
||
typedef enum ScrDigestAlg {
|
||
SCR_DIGEST_MD5,
|
||
SCR_DIGEST_SHA1,
|
||
SCR_DIGEST_SHA224,
|
||
SCR_DIGEST_SHA256,
|
||
SCR_DIGEST_SHA384,
|
||
SCR_DIGEST_SHA512,
|
||
} ScrDigestAlg;
|
||
|
||
typedef struct ScrDigestCtx {
|
||
ScrDigestAlg alg;
|
||
union {
|
||
ScrMd5Ctx md5;
|
||
ScrSha1Ctx sha1;
|
||
ScrSha256Ctx sha256;
|
||
ScrSha512Ctx sha512;
|
||
} state;
|
||
} ScrDigestCtx;
|
||
|
||
static unsigned char scr_ascii_lower(unsigned char c) {
|
||
return c >= 'A' && c <= 'Z' ? (unsigned char)(c + ('a' - 'A')) : c;
|
||
}
|
||
|
||
static bool scr_digest_name_is(const char *name, size_t len, const char *literal) {
|
||
size_t n = strlen(literal);
|
||
if (len != n) return false;
|
||
for (size_t i = 0; i < n; i++) {
|
||
if (scr_ascii_lower((unsigned char)name[i]) != (unsigned char)literal[i]) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static bool scr_digest_alg(const char *name, size_t len, ScrDigestAlg *out) {
|
||
if (scr_digest_name_is(name, len, "md5")) *out = SCR_DIGEST_MD5;
|
||
else if (scr_digest_name_is(name, len, "sha1")) *out = SCR_DIGEST_SHA1;
|
||
else if (scr_digest_name_is(name, len, "sha224")) *out = SCR_DIGEST_SHA224;
|
||
else if (scr_digest_name_is(name, len, "sha256")) *out = SCR_DIGEST_SHA256;
|
||
else if (scr_digest_name_is(name, len, "sha384")) *out = SCR_DIGEST_SHA384;
|
||
else if (scr_digest_name_is(name, len, "sha512")) *out = SCR_DIGEST_SHA512;
|
||
else return false;
|
||
return true;
|
||
}
|
||
|
||
static void scr_digest_init(ScrDigestCtx *ctx, ScrDigestAlg alg) {
|
||
ctx->alg = alg;
|
||
switch (alg) {
|
||
case SCR_DIGEST_MD5: scr_md5_init(&ctx->state.md5); break;
|
||
case SCR_DIGEST_SHA1: scr_sha1_init(&ctx->state.sha1); break;
|
||
case SCR_DIGEST_SHA224: {
|
||
static const uint32_t initial[8] = {
|
||
0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
|
||
0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4};
|
||
scr_sha256_init(&ctx->state.sha256);
|
||
memcpy(ctx->state.sha256.h, initial, sizeof initial);
|
||
break;
|
||
}
|
||
case SCR_DIGEST_SHA256: scr_sha256_init(&ctx->state.sha256); break;
|
||
case SCR_DIGEST_SHA384: scr_sha512_init(&ctx->state.sha512, true); break;
|
||
case SCR_DIGEST_SHA512: scr_sha512_init(&ctx->state.sha512, false); break;
|
||
}
|
||
}
|
||
|
||
static void scr_digest_update(ScrDigestCtx *ctx, const unsigned char *data, size_t len) {
|
||
switch (ctx->alg) {
|
||
case SCR_DIGEST_MD5: scr_md5_update(&ctx->state.md5, data, len); break;
|
||
case SCR_DIGEST_SHA1: scr_sha1_update(&ctx->state.sha1, data, len); break;
|
||
case SCR_DIGEST_SHA224:
|
||
case SCR_DIGEST_SHA256: scr_sha256_update(&ctx->state.sha256, data, len); break;
|
||
case SCR_DIGEST_SHA384:
|
||
case SCR_DIGEST_SHA512: scr_sha512_update(&ctx->state.sha512, data, len); break;
|
||
}
|
||
}
|
||
|
||
static size_t scr_digest_final(const ScrDigestCtx *ctx, unsigned char out[64]) {
|
||
switch (ctx->alg) {
|
||
case SCR_DIGEST_MD5: return scr_md5_final(&ctx->state.md5, out);
|
||
case SCR_DIGEST_SHA1: return scr_sha1_final(&ctx->state.sha1, out);
|
||
case SCR_DIGEST_SHA224: scr_sha256_final(&ctx->state.sha256, out); return 28;
|
||
case SCR_DIGEST_SHA256: return scr_sha256_final(&ctx->state.sha256, out);
|
||
case SCR_DIGEST_SHA384: return scr_sha512_final(&ctx->state.sha512, out, true);
|
||
case SCR_DIGEST_SHA512: return scr_sha512_final(&ctx->state.sha512, out, false);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static void scr_crypto_zero(void *ptr, size_t len) {
|
||
volatile unsigned char *p = ptr;
|
||
while (len-- != 0) *p++ = 0;
|
||
}
|
||
|
||
static void scr_hmac_init(ScrDigestCtx *inner, ScrDigestCtx *outer, ScrDigestAlg alg,
|
||
const unsigned char *key, size_t keylen) {
|
||
unsigned char kblock[128] = {0};
|
||
size_t block_len = alg == SCR_DIGEST_SHA384 || alg == SCR_DIGEST_SHA512 ? 128 : 64;
|
||
unsigned char kd[64];
|
||
if (keylen > block_len) {
|
||
ScrDigestCtx key_hash;
|
||
scr_digest_init(&key_hash, alg);
|
||
scr_digest_update(&key_hash, key, keylen);
|
||
size_t n = scr_digest_final(&key_hash, kd);
|
||
memcpy(kblock, kd, n);
|
||
scr_crypto_zero(&key_hash, sizeof key_hash);
|
||
scr_crypto_zero(kd, sizeof kd);
|
||
} else if (keylen != 0) {
|
||
memcpy(kblock, key, keylen);
|
||
}
|
||
unsigned char ipad[128];
|
||
unsigned char opad[128];
|
||
for (size_t i = 0; i < block_len; i++) {
|
||
ipad[i] = (unsigned char)(kblock[i] ^ 0x36);
|
||
opad[i] = (unsigned char)(kblock[i] ^ 0x5c);
|
||
}
|
||
scr_digest_init(inner, alg);
|
||
scr_digest_update(inner, ipad, block_len);
|
||
scr_digest_init(outer, alg);
|
||
scr_digest_update(outer, opad, block_len);
|
||
scr_crypto_zero(kblock, sizeof kblock);
|
||
scr_crypto_zero(ipad, block_len);
|
||
scr_crypto_zero(opad, block_len);
|
||
}
|
||
|
||
static size_t scr_hmac_final(const ScrDigestCtx *inner, const ScrDigestCtx *outer,
|
||
unsigned char out[64]) {
|
||
unsigned char inner_digest[64];
|
||
size_t inner_len = scr_digest_final(inner, inner_digest);
|
||
ScrDigestCtx final_outer = *outer;
|
||
scr_digest_update(&final_outer, inner_digest, inner_len);
|
||
size_t n = scr_digest_final(&final_outer, out);
|
||
scr_crypto_zero(inner_digest, sizeof inner_digest);
|
||
scr_crypto_zero(&final_outer, sizeof final_outer);
|
||
return n;
|
||
}
|
||
|
||
size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t len,
|
||
unsigned char out[64]) {
|
||
ScrDigestAlg kind;
|
||
if (!scr_digest_alg(alg, strlen(alg), &kind)) return 0;
|
||
ScrDigestCtx ctx;
|
||
scr_digest_init(&ctx, kind);
|
||
scr_digest_update(&ctx, data, len);
|
||
return scr_digest_final(&ctx, out);
|
||
}
|
||
|
||
size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen,
|
||
const unsigned char *data, size_t len, unsigned char out[64]) {
|
||
ScrDigestAlg kind;
|
||
if (!scr_digest_alg(alg, strlen(alg), &kind)) return 0;
|
||
ScrDigestCtx inner, outer;
|
||
scr_hmac_init(&inner, &outer, kind, key, keylen);
|
||
scr_digest_update(&inner, data, len);
|
||
size_t n = scr_hmac_final(&inner, &outer, out);
|
||
scr_crypto_zero(&inner, sizeof inner);
|
||
scr_crypto_zero(&outer, sizeof outer);
|
||
return n;
|
||
}
|
||
|
||
struct ScrCryptoHash {
|
||
size_t rc;
|
||
bool finalized;
|
||
bool hmac;
|
||
ScrDigestCtx inner;
|
||
ScrDigestCtx outer;
|
||
};
|
||
|
||
static void scr_crypto_digest_unsupported(void) {
|
||
static const char msg[] = "Digest method not supported";
|
||
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
|
||
}
|
||
|
||
static void scr_crypto_invalid_digest(const ScrStr *alg) {
|
||
static const char prefix[] = "Invalid digest: ";
|
||
const size_t prefix_len = sizeof prefix - 1;
|
||
if (alg->len > SIZE_MAX - prefix_len) scr_trap("scriptc: out of memory\n");
|
||
const size_t msg_len = prefix_len + alg->len;
|
||
char *msg = malloc(msg_len);
|
||
if (!msg) scr_trap("scriptc: out of memory\n");
|
||
memcpy(msg, prefix, prefix_len);
|
||
memcpy(msg + prefix_len, alg->data, alg->len);
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, msg_len,
|
||
"ERR_CRYPTO_INVALID_DIGEST");
|
||
free(msg);
|
||
}
|
||
|
||
static void scr_crypto_hash_digest_unsupported(const ScrStr *alg) {
|
||
static const char prefix[] = "Digest method ";
|
||
static const char suffix[] = " is not supported";
|
||
const size_t prefix_len = sizeof prefix - 1;
|
||
const size_t suffix_len = sizeof suffix - 1;
|
||
if (alg->len > SIZE_MAX - prefix_len ||
|
||
suffix_len > SIZE_MAX - prefix_len - alg->len) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
const size_t msg_len = prefix_len + alg->len + suffix_len;
|
||
char *msg = malloc(msg_len);
|
||
if (!msg) scr_trap("scriptc: out of memory\n");
|
||
memcpy(msg, prefix, prefix_len);
|
||
memcpy(msg + prefix_len, alg->data, alg->len);
|
||
memcpy(msg + prefix_len + alg->len, suffix, suffix_len);
|
||
scr_throw_error_msg(SCR_ERR_ERROR, msg, msg_len);
|
||
free(msg);
|
||
}
|
||
|
||
static void scr_crypto_hash_finalized(void) {
|
||
static const char msg[] = "Digest already called";
|
||
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1,
|
||
"ERR_CRYPTO_HASH_FINALIZED");
|
||
}
|
||
|
||
static ScrCryptoHash *scr_crypto_hash_alloc(ScrDigestAlg alg, bool hmac,
|
||
const unsigned char *key, size_t keylen) {
|
||
ScrCryptoHash *hash = calloc(1, sizeof *hash);
|
||
if (!hash) scr_trap("scriptc: out of memory\n");
|
||
hash->rc = 1;
|
||
hash->hmac = hmac;
|
||
if (hmac) scr_hmac_init(&hash->inner, &hash->outer, alg, key, keylen);
|
||
else scr_digest_init(&hash->inner, alg);
|
||
return hash;
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg) {
|
||
ScrDigestAlg kind;
|
||
if (!scr_digest_alg(alg->data, alg->len, &kind)) {
|
||
scr_crypto_digest_unsupported();
|
||
return NULL;
|
||
}
|
||
return scr_crypto_hash_alloc(kind, false, NULL, 0);
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key) {
|
||
ScrDigestAlg kind;
|
||
if (!scr_digest_alg(alg->data, alg->len, &kind)) {
|
||
scr_crypto_invalid_digest(alg);
|
||
return NULL;
|
||
}
|
||
char *owned;
|
||
const char *bytes = scr_str_utf8(key, &owned);
|
||
ScrCryptoHash *r = scr_crypto_hash_alloc(kind, true, (const unsigned char *)bytes, key->len);
|
||
scr_str_utf8_end(owned);
|
||
return r;
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key) {
|
||
SCR_BYTES_SNAPSHOT(key);
|
||
ScrDigestAlg kind;
|
||
if (!scr_digest_alg(alg->data, alg->len, &kind)) {
|
||
scr_crypto_invalid_digest(alg);
|
||
return NULL;
|
||
}
|
||
return scr_crypto_hash_alloc(kind, true, key->data,
|
||
key->len * scr_bytes_elem_size(key->elem));
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *hash) {
|
||
if (hash && hash->rc != SIZE_MAX) hash->rc++;
|
||
return hash;
|
||
}
|
||
|
||
void scr_crypto_hash_release(ScrCryptoHash *hash) {
|
||
if (!hash || hash->rc == SIZE_MAX) return;
|
||
if (--hash->rc == 0) {
|
||
scr_crypto_zero(hash, sizeof *hash);
|
||
free(hash);
|
||
}
|
||
}
|
||
|
||
void *scr_crypto_hash_retain_v(void *p) { return scr_crypto_hash_retain(p); }
|
||
void scr_crypto_hash_release_v(void *p) { scr_crypto_hash_release(p); }
|
||
|
||
static ScrCryptoHash *scr_crypto_hash_update_raw(ScrCryptoHash *hash,
|
||
const unsigned char *data, size_t len) {
|
||
if (hash->finalized) {
|
||
scr_crypto_hash_finalized();
|
||
return NULL;
|
||
}
|
||
scr_digest_update(&hash->inner, data, len);
|
||
return scr_crypto_hash_retain(hash);
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hash_update_str(ScrCryptoHash *hash, ScrStr *data) {
|
||
char *owned;
|
||
const char *bytes = scr_str_utf8(data, &owned);
|
||
ScrCryptoHash *r = scr_crypto_hash_update_raw(hash, (const unsigned char *)bytes, data->len);
|
||
scr_str_utf8_end(owned);
|
||
return r;
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hash_update_bytes(ScrCryptoHash *hash, ScrBytes *data) {
|
||
SCR_BYTES_SNAPSHOT(data);
|
||
return scr_crypto_hash_update_raw(hash, data->data,
|
||
data->len * scr_bytes_elem_size(data->elem));
|
||
}
|
||
|
||
ScrCryptoHash *scr_crypto_hash_copy(ScrCryptoHash *hash) {
|
||
if (hash->finalized) {
|
||
scr_crypto_hash_finalized();
|
||
return NULL;
|
||
}
|
||
ScrCryptoHash *copy = malloc(sizeof *copy);
|
||
if (!copy) scr_trap("scriptc: out of memory\n");
|
||
*copy = *hash;
|
||
copy->rc = 1;
|
||
return copy;
|
||
}
|
||
|
||
static size_t scr_crypto_hash_finish(ScrCryptoHash *hash, unsigned char out[64]) {
|
||
if (hash->finalized) {
|
||
if (hash->hmac) return 0; /* Node's repeated Hmac.digest() is empty. */
|
||
scr_crypto_hash_finalized();
|
||
return 0;
|
||
}
|
||
hash->finalized = true;
|
||
return hash->hmac
|
||
? scr_hmac_final(&hash->inner, &hash->outer, out)
|
||
: scr_digest_final(&hash->inner, out);
|
||
}
|
||
|
||
ScrStr *scr_crypto_hash_digest_string(ScrCryptoHash *hash, ScrStr *enc) {
|
||
unsigned char digest[64];
|
||
size_t n = scr_crypto_hash_finish(hash, digest);
|
||
if (n == 0) return scr_exc_pending() ? NULL : scr_str_new("", 0);
|
||
ScrStr *result = scr_digest_encode(digest, n, enc);
|
||
scr_crypto_zero(digest, sizeof digest);
|
||
return result;
|
||
}
|
||
|
||
ScrBytes *scr_crypto_hash_digest_buffer(ScrCryptoHash *hash) {
|
||
unsigned char digest[64];
|
||
size_t n = scr_crypto_hash_finish(hash, digest);
|
||
if (n == 0) return scr_exc_pending() ? NULL : scr_bytes_new(SCR_BYTES_U8, 0);
|
||
ScrBytes *result = scr_bytes_new(SCR_BYTES_U8, (double)n);
|
||
memcpy(result->data, digest, n);
|
||
scr_crypto_zero(digest, sizeof digest);
|
||
return result;
|
||
}
|
||
|
||
static ScrStr *scr_hash_digest_raw(const ScrStr *alg, const unsigned char *data, size_t len,
|
||
const ScrStr *enc) {
|
||
ScrDigestAlg kind;
|
||
if (!scr_digest_alg(alg->data, alg->len, &kind)) {
|
||
scr_crypto_hash_digest_unsupported(alg);
|
||
return NULL;
|
||
}
|
||
unsigned char d[64];
|
||
ScrDigestCtx ctx;
|
||
scr_digest_init(&ctx, kind);
|
||
scr_digest_update(&ctx, data, len);
|
||
size_t n = scr_digest_final(&ctx, d);
|
||
ScrStr *result = scr_digest_encode(d, n, enc);
|
||
scr_crypto_zero(d, sizeof d);
|
||
return result;
|
||
}
|
||
|
||
/* Strings hash their UTF-8 bytes (Node's default input encoding — ScrStr
|
||
* storage IS utf8, so the bytes are the string's own). Borrowed; +1. */
|
||
ScrStr *scr_crypto_hash_digest_str(ScrStr *alg, ScrStr *data, ScrStr *enc) {
|
||
char *owned;
|
||
const char *bytes = scr_str_utf8(data, &owned);
|
||
ScrStr *r = scr_hash_digest_raw(alg, (const unsigned char *)bytes, data->len, enc);
|
||
scr_str_utf8_end(owned);
|
||
return r;
|
||
}
|
||
|
||
ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc) {
|
||
SCR_BYTES_SNAPSHOT(data);
|
||
return scr_hash_digest_raw(alg, data->data, data->len * scr_bytes_elem_size(data->elem), enc);
|
||
}
|
||
|
||
bool scr_crypto_timing_safe_equal(ScrBytes *left, ScrBytes *right) {
|
||
SCR_SHARED_GUARD(left, right);
|
||
size_t left_len = left->len * scr_bytes_elem_size(left->elem);
|
||
size_t right_len = right->len * scr_bytes_elem_size(right->elem);
|
||
if (left_len != right_len) {
|
||
static const char msg[] = "Input buffers must have the same byte length";
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1,
|
||
"ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH");
|
||
return false;
|
||
}
|
||
unsigned char diff = 0;
|
||
for (size_t i = 0; i < left_len; i++) diff |= left->data[i] ^ right->data[i];
|
||
return diff == 0;
|
||
}
|
||
|
||
ScrBytes *scr_crypto_random_fill(ScrBytes *bytes, double offset, double size) {
|
||
double length = (double)(bytes->len * scr_bytes_elem_size(bytes->elem));
|
||
if (!isfinite(offset) || floor(offset) != offset || offset < 0 || offset > length) {
|
||
char value[64], msg[160];
|
||
size_t value_len = scr_f64_to_str(offset, value);
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"offset\" is out of range. It must be >= 0 && <= %.0f. Received %.*s",
|
||
length, (int)value_len, value);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
double available = length - offset;
|
||
if (!isfinite(size) || floor(size) != size || size < 0 || size > 2147483647.0) {
|
||
char value[64], msg[160];
|
||
size_t value_len = scr_f64_to_str(size, value);
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"size\" is out of range. It must be >= 0 && <= 2147483647. Received %.*s",
|
||
(int)value_len, value);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
if (size > available) {
|
||
char value[64], msg[160];
|
||
double total = size + offset;
|
||
size_t value_len = scr_f64_to_str(total, value);
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"size + offset\" is out of range. It must be <= %.0f. Received %.*s",
|
||
length, (int)value_len, value);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
if (bytes->shared) {
|
||
uint8_t *random = malloc(size ? (size_t)size : 1);
|
||
if (!random) scr_trap("scriptc: out of memory\n");
|
||
arc4random_buf(random, (size_t)size);
|
||
scr_bytes_write(bytes, (size_t)offset, random, (size_t)size);
|
||
free(random);
|
||
} else arc4random_buf(bytes->data + (size_t)offset, (size_t)size);
|
||
return scr_bytes_retain(bytes);
|
||
}
|
||
|
||
ScrBytes *scr_crypto_random_fill_rest(ScrBytes *bytes, double offset) {
|
||
double length = (double)(bytes->len * scr_bytes_elem_size(bytes->elem));
|
||
return scr_crypto_random_fill(bytes, offset, length - offset);
|
||
}
|
||
|
||
double scr_crypto_random_int(double min, double max) {
|
||
if (!isfinite(min) || floor(min) != min || fabs(min) > 9007199254740991.0) {
|
||
static const char msg[] = "The \"min\" argument must be a safe integer.";
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
|
||
return 0;
|
||
}
|
||
if (!isfinite(max) || floor(max) != max || fabs(max) > 9007199254740991.0) {
|
||
static const char msg[] = "The \"max\" argument must be a safe integer.";
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
|
||
return 0;
|
||
}
|
||
if (max <= min) {
|
||
char min_text[64], max_text[64], msg[192];
|
||
size_t min_len = scr_f64_to_str(min, min_text);
|
||
size_t max_len = scr_f64_to_str(max, max_text);
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"max\" is out of range. It must be greater than the value of \"min\" (%.*s). Received %.*s",
|
||
(int)min_len, min_text, (int)max_len, max_text);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
double range = max - min;
|
||
if (range > 281474976710656.0) {
|
||
static const char msg[] = "The value of \"max - min\" is out of range. It must be <= 281474976710656.";
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_OUT_OF_RANGE");
|
||
return 0;
|
||
}
|
||
const uint64_t span = (uint64_t)range;
|
||
const uint64_t ceiling = UINT64_C(1) << 48;
|
||
const uint64_t limit = ceiling - (ceiling % span);
|
||
uint64_t value;
|
||
do {
|
||
uint64_t random;
|
||
arc4random_buf(&random, sizeof random);
|
||
value = random >> 16;
|
||
} while (value >= limit);
|
||
return min + (double)(value % span);
|
||
}
|
||
|
||
static ScrBytes *scr_crypto_pbkdf2_raw(const unsigned char *password, size_t password_len,
|
||
const unsigned char *salt, size_t salt_len,
|
||
uint32_t iterations, size_t keylen,
|
||
ScrDigestAlg alg) {
|
||
ScrDigestCtx empty;
|
||
unsigned char empty_digest[64];
|
||
scr_digest_init(&empty, alg);
|
||
size_t digest_len = scr_digest_final(&empty, empty_digest);
|
||
scr_crypto_zero(empty_digest, sizeof empty_digest);
|
||
ScrBytes *result = scr_bytes_new(SCR_BYTES_U8, (double)keylen);
|
||
unsigned char *block = malloc(salt_len + 4);
|
||
if (!block) scr_trap("scriptc: out of memory\n");
|
||
memcpy(block, salt, salt_len);
|
||
size_t blocks = (keylen + digest_len - 1) / digest_len;
|
||
for (size_t index = 1; index <= blocks; index++) {
|
||
block[salt_len] = (unsigned char)(index >> 24);
|
||
block[salt_len + 1] = (unsigned char)(index >> 16);
|
||
block[salt_len + 2] = (unsigned char)(index >> 8);
|
||
block[salt_len + 3] = (unsigned char)index;
|
||
ScrDigestCtx inner, outer;
|
||
unsigned char u[64], accum[64];
|
||
scr_hmac_init(&inner, &outer, alg, password, password_len);
|
||
scr_digest_update(&inner, block, salt_len + 4);
|
||
scr_hmac_final(&inner, &outer, u);
|
||
memcpy(accum, u, digest_len);
|
||
for (uint32_t round = 1; round < iterations; round++) {
|
||
scr_hmac_init(&inner, &outer, alg, password, password_len);
|
||
scr_digest_update(&inner, u, digest_len);
|
||
scr_hmac_final(&inner, &outer, u);
|
||
for (size_t i = 0; i < digest_len; i++) accum[i] ^= u[i];
|
||
}
|
||
size_t offset = (index - 1) * digest_len;
|
||
size_t take = keylen - offset < digest_len ? keylen - offset : digest_len;
|
||
memcpy(result->data + offset, accum, take);
|
||
scr_crypto_zero(&inner, sizeof inner);
|
||
scr_crypto_zero(&outer, sizeof outer);
|
||
scr_crypto_zero(u, sizeof u);
|
||
scr_crypto_zero(accum, sizeof accum);
|
||
}
|
||
scr_crypto_zero(block, salt_len + 4);
|
||
free(block);
|
||
return result;
|
||
}
|
||
|
||
ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt,
|
||
double iterations, double keylen, ScrStr *digest) {
|
||
SCR_BYTES_SNAPSHOT(password);
|
||
SCR_BYTES_SNAPSHOT(salt);
|
||
if (!isfinite(iterations) || floor(iterations) != iterations ||
|
||
iterations < 1 || iterations > 2147483647.0) {
|
||
char value[64], msg[176];
|
||
size_t value_len = scr_f64_to_str(iterations, value);
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"iterations\" is out of range. It must be >= 1 && <= 2147483647. Received %.*s",
|
||
(int)value_len, value);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
if (!isfinite(keylen) || floor(keylen) != keylen || keylen < 0 || keylen > 2147483647.0) {
|
||
char value[64], msg[176];
|
||
size_t value_len = scr_f64_to_str(keylen, value);
|
||
int n = snprintf(msg, sizeof msg,
|
||
"The value of \"keylen\" is out of range. It must be >= 0 && <= 2147483647. Received %.*s",
|
||
(int)value_len, value);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
ScrDigestAlg alg;
|
||
if (!scr_digest_alg(digest->data, digest->len, &alg)) {
|
||
scr_crypto_invalid_digest(digest);
|
||
return NULL;
|
||
}
|
||
return scr_crypto_pbkdf2_raw(password->data,
|
||
password->len * scr_bytes_elem_size(password->elem), salt->data,
|
||
salt->len * scr_bytes_elem_size(salt->elem), (uint32_t)iterations,
|
||
(size_t)keylen, alg);
|
||
}
|
||
|
||
/* RFC 5869 HKDF and RFC 7914 scrypt share the digest core in both
|
||
* execution tiers. Sensitive scratch storage is cleared before release. */
|
||
static bool scr_crypto_integer(double value, const char *name, double max) {
|
||
if (isfinite(value) && floor(value) == value && value >= 0 && value <= max) return true;
|
||
char received[64], requirement[80], message[256];
|
||
size_t n = scr_f64_to_str(value, received);
|
||
received[n] = '\0';
|
||
if (!isfinite(value) || floor(value) != value) {
|
||
snprintf(requirement, sizeof requirement, "an integer");
|
||
} else {
|
||
snprintf(requirement, sizeof requirement, ">= 0 && <= %.0f", max);
|
||
if (fabs(value) > 4294967296.0) {
|
||
char digits[40];
|
||
snprintf(digits, sizeof digits, "%.0f", value);
|
||
size_t len = strlen(digits), out = 0, start = digits[0] == '-' ? 1 : 0;
|
||
for (size_t i = 0; i < len; i++) {
|
||
if (i > start && (len - i) % 3 == 0) received[out++] = '_';
|
||
received[out++] = digits[i];
|
||
}
|
||
received[out] = '\0';
|
||
}
|
||
}
|
||
int len = snprintf(message, sizeof message,
|
||
"The value of \"%s\" is out of range. It must be %s. Received %s", name, requirement, received);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return false;
|
||
}
|
||
|
||
ScrBytes *scr_crypto_hkdf_bytes(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt,
|
||
ScrBytes *info, double keylen) {
|
||
SCR_BYTES_SNAPSHOT(ikm);
|
||
SCR_BYTES_SNAPSHOT(salt);
|
||
SCR_BYTES_SNAPSHOT(info);
|
||
if (!scr_crypto_integer(keylen, "length", 9007199254740991.0)) return NULL;
|
||
size_t info_len = info->len * scr_bytes_elem_size(info->elem);
|
||
if (info_len > 1024) {
|
||
char message[192];
|
||
int len = snprintf(message, sizeof message,
|
||
"The value of \"info\" is out of range. It must be must not contain more than 1024 bytes. Received %zu", info_len);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)len, "ERR_OUT_OF_RANGE");
|
||
return NULL;
|
||
}
|
||
ScrDigestAlg alg;
|
||
if (!scr_digest_alg(digest->data, digest->len, &alg)) {
|
||
scr_crypto_invalid_digest(digest);
|
||
return NULL;
|
||
}
|
||
ScrDigestCtx inner, outer;
|
||
unsigned char prk[64], t[64];
|
||
scr_hmac_init(&inner, &outer, alg, salt->data, salt->len * scr_bytes_elem_size(salt->elem));
|
||
scr_digest_update(&inner, ikm->data, ikm->len * scr_bytes_elem_size(ikm->elem));
|
||
size_t hash_len = scr_hmac_final(&inner, &outer, prk);
|
||
if (keylen > 255 * hash_len) {
|
||
scr_crypto_zero(prk, sizeof prk);
|
||
scr_crypto_zero(&inner, sizeof inner);
|
||
scr_crypto_zero(&outer, sizeof outer);
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, "Invalid key length", 18, "ERR_CRYPTO_INVALID_KEYLEN");
|
||
return NULL;
|
||
}
|
||
ScrBytes *result = scr_bytes_new(SCR_BYTES_U8, keylen);
|
||
size_t offset = 0, previous_len = 0;
|
||
for (unsigned counter = 1; offset < (size_t)keylen; counter++) {
|
||
scr_hmac_init(&inner, &outer, alg, prk, hash_len);
|
||
scr_digest_update(&inner, t, previous_len);
|
||
scr_digest_update(&inner, info->data, info_len);
|
||
unsigned char byte = (unsigned char)counter;
|
||
scr_digest_update(&inner, &byte, 1);
|
||
scr_hmac_final(&inner, &outer, t);
|
||
size_t take = (size_t)keylen - offset;
|
||
if (take > hash_len) take = hash_len;
|
||
memcpy(result->data + offset, t, take);
|
||
offset += take;
|
||
previous_len = hash_len;
|
||
}
|
||
scr_crypto_zero(prk, sizeof prk);
|
||
scr_crypto_zero(t, sizeof t);
|
||
scr_crypto_zero(&inner, sizeof inner);
|
||
scr_crypto_zero(&outer, sizeof outer);
|
||
return result;
|
||
}
|
||
|
||
ScrDyn *scr_crypto_hkdf(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt,
|
||
ScrBytes *info, double keylen) {
|
||
ScrBytes *bytes = scr_crypto_hkdf_bytes(digest, ikm, salt, info, keylen);
|
||
if (!bytes) return NULL;
|
||
if (bytes->len == 0) {
|
||
scr_bytes_release(bytes);
|
||
scr_throw_error_msg(SCR_ERR_ERROR, "Deriving bits failed", 20);
|
||
return NULL;
|
||
}
|
||
ScrDyn *result = scr_array_buffer_from_bytes(bytes);
|
||
scr_bytes_release(bytes);
|
||
return result;
|
||
}
|
||
|
||
static uint32_t scr_scrypt_rotl(uint32_t x, unsigned n) {
|
||
return (x << n) | (x >> (32 - n));
|
||
}
|
||
|
||
/* Salsa20/8 core, RFC 7914 section 3. Unsigned arithmetic supplies the
|
||
* specified reduction modulo 2^32 without signed-overflow UB. */
|
||
static void scr_scrypt_salsa(uint32_t block[16]) {
|
||
uint32_t x[16];
|
||
memcpy(x, block, sizeof x);
|
||
for (unsigned round = 0; round < 8; round += 2) {
|
||
#define SCR_SALSA(a, b, c, n) x[a] ^= scr_scrypt_rotl(x[b] + x[c], n)
|
||
SCR_SALSA(4,0,12,7); SCR_SALSA(8,4,0,9); SCR_SALSA(12,8,4,13); SCR_SALSA(0,12,8,18);
|
||
SCR_SALSA(9,5,1,7); SCR_SALSA(13,9,5,9); SCR_SALSA(1,13,9,13); SCR_SALSA(5,1,13,18);
|
||
SCR_SALSA(14,10,6,7); SCR_SALSA(2,14,10,9); SCR_SALSA(6,2,14,13); SCR_SALSA(10,6,2,18);
|
||
SCR_SALSA(3,15,11,7); SCR_SALSA(7,3,15,9); SCR_SALSA(11,7,3,13); SCR_SALSA(15,11,7,18);
|
||
SCR_SALSA(1,0,3,7); SCR_SALSA(2,1,0,9); SCR_SALSA(3,2,1,13); SCR_SALSA(0,3,2,18);
|
||
SCR_SALSA(6,5,4,7); SCR_SALSA(7,6,5,9); SCR_SALSA(4,7,6,13); SCR_SALSA(5,4,7,18);
|
||
SCR_SALSA(11,10,9,7); SCR_SALSA(8,11,10,9); SCR_SALSA(9,8,11,13); SCR_SALSA(10,9,8,18);
|
||
SCR_SALSA(12,15,14,7); SCR_SALSA(13,12,15,9); SCR_SALSA(14,13,12,13); SCR_SALSA(15,14,13,18);
|
||
#undef SCR_SALSA
|
||
}
|
||
for (size_t i = 0; i < 16; i++) block[i] += x[i];
|
||
scr_crypto_zero(x, sizeof x);
|
||
}
|
||
|
||
static void scr_scrypt_blockmix(uint32_t *x, uint32_t *y, size_t r) {
|
||
uint32_t block[16];
|
||
memcpy(block, x + (2 * r - 1) * 16, sizeof block);
|
||
for (size_t i = 0; i < 2 * r; i++) {
|
||
for (size_t k = 0; k < 16; k++) block[k] ^= x[i * 16 + k];
|
||
scr_scrypt_salsa(block);
|
||
size_t dest = (i / 2 + (i % 2) * r) * 16;
|
||
memcpy(y + dest, block, sizeof block);
|
||
}
|
||
memcpy(x, y, 128 * r);
|
||
scr_crypto_zero(block, sizeof block);
|
||
}
|
||
|
||
static bool scr_scrypt_params(double n, double r, double p, double maxmem, size_t *memory) {
|
||
uint64_t N = (uint64_t)n, R = (uint64_t)r, P = (uint64_t)p;
|
||
if (N < 2 || (N & (N - 1)) != 0) {
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, "Invalid scrypt params", 21, "ERR_CRYPTO_INVALID_SCRYPT_PARAMS");
|
||
return false;
|
||
}
|
||
/* Match Node's OpenSSL 3 scrypt guard, including X/Y working buffers
|
||
* and the initial PBKDF2 output; check every product before allocation. */
|
||
bool overflow = R == 0 || P == 0 || P > UINT64_C(0x3fffffff) / R;
|
||
if (!overflow && R <= 3 && N >= (UINT64_C(1) << (16 * R))) overflow = true;
|
||
uint64_t b = overflow ? 0 : 128 * R * P;
|
||
if (b > INT32_MAX || R > UINT64_MAX / 128 / (N + 2)) overflow = true;
|
||
uint64_t v = overflow ? 0 : 128 * R * (N + 2);
|
||
if (overflow || b > UINT64_MAX - v || b + v > (uint64_t)maxmem || b + v > SIZE_MAX) {
|
||
static const char message[] = "Invalid scrypt params: error:030000AC:digital envelope routines::memory limit exceeded";
|
||
scr_throw_error_msg_code(SCR_ERR_RANGE, message, sizeof message - 1, "ERR_CRYPTO_INVALID_SCRYPT_PARAMS");
|
||
return false;
|
||
}
|
||
*memory = (size_t)(b + v);
|
||
return true;
|
||
}
|
||
|
||
ScrBytes *scr_crypto_scrypt_derive(ScrBytes *password, ScrBytes *salt, double keylen,
|
||
double n, double r, double p, double maxmem) {
|
||
SCR_BYTES_SNAPSHOT(password);
|
||
SCR_BYTES_SNAPSHOT(salt);
|
||
if (!scr_crypto_integer(keylen, "keylen", 2147483647.0) ||
|
||
!scr_crypto_integer(n, "N", 4294967295.0) || !scr_crypto_integer(r, "r", 4294967295.0) ||
|
||
!scr_crypto_integer(p, "p", 4294967295.0) || !scr_crypto_integer(maxmem, "maxmem", 9007199254740991.0)) return NULL;
|
||
if (n == 0) n = 16384;
|
||
if (r == 0) r = 8;
|
||
if (p == 0) p = 1;
|
||
if (maxmem == 0) maxmem = 33554432;
|
||
size_t memory;
|
||
if (!scr_scrypt_params(n, r, p, maxmem, &memory)) return NULL;
|
||
if (keylen == 0) return scr_bytes_new(SCR_BYTES_U8, 0);
|
||
size_t words = 32 * (size_t)r, block_len = words * sizeof(uint32_t);
|
||
size_t b_len = block_len * (size_t)p;
|
||
ScrBytes *b = scr_crypto_pbkdf2_raw(password->data, password->len * scr_bytes_elem_size(password->elem),
|
||
salt->data, salt->len * scr_bytes_elem_size(salt->elem), 1, b_len, SCR_DIGEST_SHA256);
|
||
uint32_t *work = malloc(memory - b_len);
|
||
if (!work) { scr_bytes_release(b); scr_trap("scriptc: out of memory\n"); }
|
||
uint32_t *x = work, *y = x + words, *v = y + words;
|
||
for (size_t part = 0; part < (size_t)p; part++) {
|
||
unsigned char *bytes = b->data + part * block_len;
|
||
for (size_t i = 0; i < words; i++) {
|
||
x[i] = (uint32_t)bytes[i * 4] | ((uint32_t)bytes[i * 4 + 1] << 8) |
|
||
((uint32_t)bytes[i * 4 + 2] << 16) | ((uint32_t)bytes[i * 4 + 3] << 24);
|
||
}
|
||
for (size_t i = 0; i < (size_t)n; i++) {
|
||
memcpy(v + i * words, x, block_len);
|
||
scr_scrypt_blockmix(x, y, (size_t)r);
|
||
}
|
||
for (size_t i = 0; i < (size_t)n; i++) {
|
||
size_t j = (size_t)x[words - 16] & ((size_t)n - 1);
|
||
for (size_t k = 0; k < words; k++) x[k] ^= v[j * words + k];
|
||
scr_scrypt_blockmix(x, y, (size_t)r);
|
||
}
|
||
for (size_t i = 0; i < words; i++) {
|
||
for (size_t k = 0; k < 4; k++) bytes[i * 4 + k] = (unsigned char)(x[i] >> (8 * k));
|
||
}
|
||
}
|
||
ScrBytes *result = scr_crypto_pbkdf2_raw(password->data, password->len * scr_bytes_elem_size(password->elem),
|
||
b->data, b_len, 1, (size_t)keylen, SCR_DIGEST_SHA256);
|
||
scr_crypto_zero(b->data, b_len);
|
||
scr_bytes_release(b);
|
||
scr_crypto_zero(work, memory - b_len);
|
||
free(work);
|
||
return result;
|
||
}
|
||
|
||
static ScrDyn *scr_scrypt_read_option(ScrDyn *options, const char *name) {
|
||
if (options->kind != SCR_DYN_TYPED_REF) return scr_dyn_obj_read(options, name, strlen(name));
|
||
ScrDyn *view = scr_dyn_typed_ref_materialize(options);
|
||
if (!view) return NULL;
|
||
ScrDyn *value = scr_dyn_obj_read(view, name, strlen(name));
|
||
scr_dyn_release(view);
|
||
return value;
|
||
}
|
||
|
||
static bool scr_scrypt_option(ScrDyn *options, const char *name, double max, double *out, bool *present) {
|
||
ScrDyn *value = scr_scrypt_read_option(options, name);
|
||
if (scr_exc_pending()) return false;
|
||
*present = value && value->kind != SCR_DYN_UNDEF;
|
||
if (*present) {
|
||
scr_dyn_release(value);
|
||
value = scr_scrypt_read_option(options, name);
|
||
if (scr_exc_pending()) return false;
|
||
if (value->kind != SCR_DYN_NUM) {
|
||
scr_dyn_arg_type_fail(name, "number", value);
|
||
scr_dyn_release(value);
|
||
return false;
|
||
}
|
||
*out = value->v.num;
|
||
bool valid = scr_crypto_integer(*out, name, max);
|
||
scr_dyn_release(value);
|
||
return valid;
|
||
}
|
||
scr_dyn_release(value);
|
||
return true;
|
||
}
|
||
|
||
ScrBytes *scr_crypto_scrypt(ScrBytes *password, ScrBytes *salt, double keylen, ScrDyn *options) {
|
||
if (!scr_crypto_integer(keylen, "keylen", 2147483647.0)) return NULL;
|
||
double values[4] = {16384, 8, 1, 33554432};
|
||
if (options && scr_dyn_truthy(options)) {
|
||
const char *names[3] = {"N", "r", "p"};
|
||
const char *aliases[3] = {"cost", "blockSize", "parallelization"};
|
||
for (size_t i = 0; i < 3; i++) {
|
||
bool primary, alias;
|
||
if (!scr_scrypt_option(options, names[i], 4294967295.0, &values[i], &primary)) return NULL;
|
||
// Node reads the alias to test presence before checking the conflict.
|
||
ScrDyn *alias_value = scr_scrypt_read_option(options, aliases[i]);
|
||
if (scr_exc_pending()) return NULL;
|
||
alias = alias_value && alias_value->kind != SCR_DYN_UNDEF;
|
||
if (primary && alias) {
|
||
char message[160];
|
||
int len = snprintf(message, sizeof message, "Option \"%s\" cannot be used in combination with option \"%s\"", names[i], aliases[i]);
|
||
scr_dyn_release(alias_value);
|
||
scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)len, "ERR_INCOMPATIBLE_OPTION_PAIR");
|
||
return NULL;
|
||
}
|
||
if (alias) {
|
||
scr_dyn_release(alias_value);
|
||
alias_value = scr_scrypt_read_option(options, aliases[i]);
|
||
if (scr_exc_pending()) return NULL;
|
||
if (alias_value->kind != SCR_DYN_NUM) {
|
||
scr_dyn_arg_type_fail(aliases[i], "number", alias_value);
|
||
scr_dyn_release(alias_value);
|
||
return NULL;
|
||
}
|
||
values[i] = alias_value->v.num;
|
||
if (!scr_crypto_integer(values[i], aliases[i], 4294967295.0)) {
|
||
scr_dyn_release(alias_value);
|
||
return NULL;
|
||
}
|
||
}
|
||
scr_dyn_release(alias_value);
|
||
}
|
||
bool present;
|
||
if (!scr_scrypt_option(options, "maxmem", 9007199254740991.0, &values[3], &present)) return NULL;
|
||
}
|
||
return scr_crypto_scrypt_derive(password, salt, keylen, values[0], values[1], values[2], values[3]);
|
||
}
|
||
|
||
/* The composed `new crypto.X509Certificate(data).fingerprint` read, fused
|
||
* by the compiler (no certificate handle exists). Node's .fingerprint IS
|
||
* the SHA-1 of the certificate's DER bytes, uppercase colon-separated —
|
||
* pinned against Node. Accepts what Node's constructor accepts from the
|
||
* fs.readFileSync idiom: PEM (the armor's base64 body decodes to DER,
|
||
* Node-leniently — whitespace skipped) or raw DER (a leading SEQUENCE
|
||
* tag). Anything else throws Node's exact PEM error (Error, code
|
||
* ERR_OSSL_PEM_NO_START_LINE). Input borrowed; result +1. */
|
||
static ScrStr *scr_x509_fingerprint_raw(const uint8_t *in, size_t n);
|
||
|
||
ScrStr *scr_crypto_x509_fingerprint(ScrBytes *data) {
|
||
SCR_BYTES_SNAPSHOT(data);
|
||
return scr_x509_fingerprint_raw(data->data, data->len * scr_bytes_elem_size(data->elem));
|
||
}
|
||
|
||
/* The string-input form (readFileSync(path, "utf-8") — PEM text; ScrStr
|
||
* storage IS the bytes). */
|
||
ScrStr *scr_crypto_x509_fingerprint_str(ScrStr *pem) {
|
||
return scr_x509_fingerprint_raw((const uint8_t *)pem->data, pem->len);
|
||
}
|
||
|
||
/* PEM armor → DER (or raw-DER passthrough), the shared front half of
|
||
* every X509Certificate member. On success `*der`/`*der_len` hold the
|
||
* certificate bytes and `*decoded` the malloc'd base64 buffer to free
|
||
* (NULL for raw-DER input); unparseable input throws Node's exact PEM
|
||
* error and answers false with the exception pending. */
|
||
static bool scr_x509_der(const uint8_t *in, size_t n, const uint8_t **der, size_t *der_len,
|
||
uint8_t **decoded) {
|
||
static const char begin[] = "-----BEGIN CERTIFICATE-----";
|
||
static const char end[] = "-----END CERTIFICATE-----";
|
||
*der = NULL;
|
||
*der_len = 0;
|
||
*decoded = NULL;
|
||
/* PEM: base64-decode the armor's body. */
|
||
const uint8_t *b = NULL;
|
||
for (size_t i = 0; i + sizeof begin - 1 <= n; i++) {
|
||
if (memcmp(in + i, begin, sizeof begin - 1) == 0) {
|
||
b = in + i + sizeof begin - 1;
|
||
break;
|
||
}
|
||
}
|
||
if (b != NULL) {
|
||
const uint8_t *stop = in + n;
|
||
for (const uint8_t *p = b; p + sizeof end - 1 <= in + n; p++) {
|
||
if (memcmp(p, end, sizeof end - 1) == 0) {
|
||
stop = p;
|
||
break;
|
||
}
|
||
}
|
||
uint8_t *out = malloc(((size_t)(stop - b) / 4 + 1) * 3 + 3);
|
||
if (!out) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
size_t o = 0;
|
||
unsigned acc = 0;
|
||
int have = 0;
|
||
for (const uint8_t *p = b; p < stop; p++) {
|
||
int v;
|
||
uint8_t c = *p;
|
||
if (c >= 'A' && c <= 'Z') v = c - 'A';
|
||
else if (c >= 'a' && c <= 'z') v = c - 'a' + 26;
|
||
else if (c >= '0' && c <= '9') v = c - '0' + 52;
|
||
else if (c == '+') v = 62;
|
||
else if (c == '/') v = 63;
|
||
else continue; /* whitespace, '=' */
|
||
acc = (acc << 6) | (unsigned)v;
|
||
if (++have == 4) {
|
||
out[o++] = (uint8_t)(acc >> 16);
|
||
out[o++] = (uint8_t)(acc >> 8);
|
||
out[o++] = (uint8_t)acc;
|
||
acc = 0;
|
||
have = 0;
|
||
}
|
||
}
|
||
if (have == 2) out[o++] = (uint8_t)(acc >> 4);
|
||
else if (have == 3) {
|
||
out[o++] = (uint8_t)(acc >> 10);
|
||
out[o++] = (uint8_t)(acc >> 2);
|
||
}
|
||
*decoded = out;
|
||
*der = out;
|
||
*der_len = o;
|
||
return true;
|
||
}
|
||
if (n > 0 && in[0] == 0x30) {
|
||
/* Raw DER: the certificate SEQUENCE. */
|
||
*der = in;
|
||
*der_len = n;
|
||
return true;
|
||
}
|
||
scr_throw_error_msg_code(SCR_ERR_ERROR,
|
||
"error:0480006C:PEM routines::no start line",
|
||
42, "ERR_OSSL_PEM_NO_START_LINE");
|
||
return false; /* exception pending */
|
||
}
|
||
|
||
static ScrStr *scr_x509_fingerprint_raw(const uint8_t *in, size_t n) {
|
||
const uint8_t *der;
|
||
size_t der_len;
|
||
uint8_t *decoded;
|
||
if (!scr_x509_der(in, n, &der, &der_len, &decoded)) return NULL;
|
||
unsigned char digest[32];
|
||
size_t dn = scr_sha1_digest(der, der_len, digest);
|
||
free(decoded);
|
||
char buf[64];
|
||
static const char hex[] = "0123456789ABCDEF";
|
||
size_t o = 0;
|
||
for (size_t i = 0; i < dn; i++) {
|
||
if (i > 0) buf[o++] = ':';
|
||
buf[o++] = hex[digest[i] >> 4];
|
||
buf[o++] = hex[digest[i] & 0x0f];
|
||
}
|
||
return scr_str_new(buf, o);
|
||
}
|
||
|
||
/* ── the certificate's Validity window (validFrom / validTo) ─────────────
|
||
*
|
||
* A minimal DER walk to TBSCertificate.validity: Certificate ::= SEQUENCE
|
||
* { tbsCertificate SEQUENCE { version [0] OPTIONAL, serialNumber INTEGER,
|
||
* signature SEQUENCE, issuer, validity SEQUENCE { notBefore, notAfter },
|
||
* ... } }. Each Time is UTCTime (YYMMDDHHMMSSZ; RFC 5280's 50-year pivot)
|
||
* or GeneralizedTime (YYYYMMDDHHMMSSZ), rendered in Node's exact
|
||
* ASN1_TIME_print shape: "Jul 1 00:00:00 2026 GMT" (%2d space-padded
|
||
* day). Truncated/non-Zulu encodings (RFC 5280 forbids them in certs)
|
||
* and walk failures answer OpenSSL's "Bad time value". */
|
||
|
||
/* Reads one DER TL header at p (before end): tag to *tag, content length
|
||
* to *len, and answers the content pointer (NULL on malformed/overlong). */
|
||
static const uint8_t *scr_der_tl(const uint8_t *p, const uint8_t *end, uint8_t *tag, size_t *len) {
|
||
if (p == NULL || end - p < 2) return NULL;
|
||
*tag = p[0];
|
||
uint8_t l0 = p[1];
|
||
const uint8_t *content = p + 2;
|
||
size_t l;
|
||
if (l0 < 0x80) {
|
||
l = l0;
|
||
} else {
|
||
size_t nb = l0 & 0x7f;
|
||
if (nb == 0 || nb > sizeof(size_t) || end - content < (ptrdiff_t)nb) return NULL;
|
||
l = 0;
|
||
for (size_t i = 0; i < nb; i++) l = (l << 8) | content[i];
|
||
content += nb;
|
||
}
|
||
if ((size_t)(end - content) < l) return NULL;
|
||
*tag = p[0];
|
||
*len = l;
|
||
return content;
|
||
}
|
||
|
||
/* Formats one Time element (UTCTime/GeneralizedTime content bytes) in
|
||
* ASN1_TIME_print's shape into buf; answers the length (0 = bad value). */
|
||
static size_t scr_x509_time_print(uint8_t tag, const uint8_t *t, size_t n, char buf[32]) {
|
||
static const char *mon[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||
int year, mo, day, hh, mm, ss;
|
||
const uint8_t *d = t;
|
||
size_t need = tag == 0x17 ? 13 : 15; /* ...HHMMSSZ, Zulu only */
|
||
if (n != need || t[n - 1] != 'Z') return 0;
|
||
for (size_t i = 0; i + 1 < n; i++) {
|
||
if (t[i] < '0' || t[i] > '9') return 0;
|
||
}
|
||
#define SCR_DER_2D(p) (((p)[0] - '0') * 10 + ((p)[1] - '0'))
|
||
if (tag == 0x17) {
|
||
year = SCR_DER_2D(d);
|
||
year += year < 50 ? 2000 : 1900; /* RFC 5280's UTCTime pivot */
|
||
d += 2;
|
||
} else {
|
||
year = SCR_DER_2D(d) * 100;
|
||
d += 2;
|
||
year += SCR_DER_2D(d);
|
||
d += 2;
|
||
}
|
||
mo = SCR_DER_2D(d); d += 2;
|
||
day = SCR_DER_2D(d); d += 2;
|
||
hh = SCR_DER_2D(d); d += 2;
|
||
mm = SCR_DER_2D(d); d += 2;
|
||
ss = SCR_DER_2D(d);
|
||
#undef SCR_DER_2D
|
||
if (mo < 1 || mo > 12) return 0;
|
||
int r = snprintf(buf, 32, "%s %2d %02d:%02d:%02d %d GMT", mon[mo - 1], day, hh, mm, ss, year);
|
||
return r > 0 ? (size_t)r : 0;
|
||
}
|
||
|
||
static ScrStr *scr_x509_validity_raw(const uint8_t *in, size_t n, bool want_to) {
|
||
const uint8_t *der;
|
||
size_t der_len;
|
||
uint8_t *decoded;
|
||
if (!scr_x509_der(in, n, &der, &der_len, &decoded)) return NULL;
|
||
static const char bad[] = "Bad time value";
|
||
const uint8_t *end = der + der_len;
|
||
uint8_t tag;
|
||
size_t len;
|
||
char buf[32];
|
||
size_t blen = 0;
|
||
/* Certificate SEQUENCE → tbsCertificate SEQUENCE */
|
||
const uint8_t *p = scr_der_tl(der, end, &tag, &len);
|
||
if (p && tag == 0x30) {
|
||
end = p + len;
|
||
p = scr_der_tl(p, end, &tag, &len);
|
||
} else {
|
||
p = NULL;
|
||
}
|
||
if (p && tag == 0x30) {
|
||
const uint8_t *tbs_end = p + len;
|
||
/* [0] version (optional), serialNumber, signature, issuer */
|
||
const uint8_t *q = p;
|
||
for (int skip = 0; skip < 4 && q != NULL; skip++) {
|
||
const uint8_t *c = scr_der_tl(q, tbs_end, &tag, &len);
|
||
if (!c) { q = NULL; break; }
|
||
if (skip == 0 && tag != 0xA0) {
|
||
/* no version field: this element is already serialNumber */
|
||
skip++;
|
||
}
|
||
q = c + len;
|
||
}
|
||
/* validity SEQUENCE { notBefore, notAfter } */
|
||
const uint8_t *v = scr_der_tl(q, tbs_end, &tag, &len);
|
||
if (v && tag == 0x30) {
|
||
const uint8_t *v_end = v + len;
|
||
const uint8_t *t1 = scr_der_tl(v, v_end, &tag, &len);
|
||
if (t1 && (tag == 0x17 || tag == 0x18)) {
|
||
if (!want_to) {
|
||
blen = scr_x509_time_print(tag, t1, len, buf);
|
||
} else {
|
||
const uint8_t *t2 = scr_der_tl(t1 + len, v_end, &tag, &len);
|
||
if (t2 && (tag == 0x17 || tag == 0x18)) {
|
||
blen = scr_x509_time_print(tag, t2, len, buf);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
free(decoded);
|
||
if (blen == 0) return scr_str_new(bad, sizeof bad - 1);
|
||
return scr_str_new(buf, blen);
|
||
}
|
||
|
||
ScrStr *scr_crypto_x509_valid_from(ScrBytes *data) {
|
||
SCR_BYTES_SNAPSHOT(data);
|
||
return scr_x509_validity_raw(data->data, data->len * scr_bytes_elem_size(data->elem), false);
|
||
}
|
||
|
||
ScrStr *scr_crypto_x509_valid_from_str(ScrStr *pem) {
|
||
return scr_x509_validity_raw((const uint8_t *)pem->data, pem->len, false);
|
||
}
|
||
|
||
ScrStr *scr_crypto_x509_valid_to(ScrBytes *data) {
|
||
SCR_BYTES_SNAPSHOT(data);
|
||
return scr_x509_validity_raw(data->data, data->len * scr_bytes_elem_size(data->elem), true);
|
||
}
|
||
|
||
ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem) {
|
||
return scr_x509_validity_raw((const uint8_t *)pem->data, pem->len, true);
|
||
}
|
||
|
||
/* ── String surface (fromCharCode) ────────────────────────────────────── */
|
||
|
||
/* String.fromCharCode core over n UTF-16 code units read through
|
||
* `unit(src, i)` (already ToUint16'd): combine adjacent surrogate pairs,
|
||
* keep other surrogates lone, WTF-8 encode. */
|
||
static ScrStr *scr_str_from_units(size_t n, uint32_t (*unit)(void *, size_t), void *src) {
|
||
if (n == 1) return scr_str_from_char_code_one((double)unit(src, 0));
|
||
char *out = malloc(n * 3 + 1); /* worst case: 3 bytes per UTF-16 unit */
|
||
if (!out) {
|
||
scr_trap("scriptc: out of memory\n");
|
||
}
|
||
size_t o = 0;
|
||
for (size_t i = 0; i < n; i++) {
|
||
uint32_t cp = unit(src, i);
|
||
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < n) {
|
||
uint32_t lo = unit(src, i + 1);
|
||
if (lo >= 0xDC00 && lo <= 0xDFFF) {
|
||
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
|
||
i++;
|
||
}
|
||
}
|
||
o += scr_wtf8_put(out + o, cp);
|
||
}
|
||
ScrStr *s = scr_str_new(out, o);
|
||
free(out);
|
||
return s;
|
||
}
|
||
|
||
static uint32_t scr_fcc_arr_unit(void *src, size_t i) {
|
||
return scr_to_uint32(scr_arr_get_f64((ScrArr *)src, (double)i)) & 0xFFFFu;
|
||
}
|
||
|
||
/* String.fromCharCode(...codes) over ONE packed f64[]. */
|
||
ScrStr *scr_str_from_char_code(ScrArr *codes) {
|
||
return scr_str_from_units(codes->len, scr_fcc_arr_unit, codes);
|
||
}
|
||
|
||
static uint32_t scr_fcc_bytes_unit(void *src, size_t i) {
|
||
return scr_to_uint32(scr_bytes_get((const ScrBytes *)src, (double)i)) & 0xFFFFu;
|
||
}
|
||
|
||
/* String.fromCharCode(...bytes) — a spread typed array/Buffer source (the
|
||
* magic-number ASCII probe: String.fromCharCode(...data.slice(4, 8))).
|
||
* u8 elements are plain code units; u32/f32 elements ride the same
|
||
* ToUint16 the packed-array form applies. */
|
||
ScrStr *scr_str_from_char_code_bytes(ScrBytes *codes) {
|
||
return scr_str_from_units(codes->len, scr_fcc_bytes_unit, codes);
|
||
}
|
||
|
||
static double scr_fcp_array_value(void *source, size_t i) {
|
||
return scr_arr_get_number((ScrArr *)source, (double)i);
|
||
}
|
||
|
||
static double scr_fcp_bytes_value(void *source, size_t i) {
|
||
return scr_bytes_get((const ScrBytes *)source, (double)i);
|
||
}
|
||
|
||
static ScrStr *scr_str_from_points(size_t n, double (*value)(void *, size_t), void *source) {
|
||
/* Validate before allocating the result. The frontend supplies numeric
|
||
* elements, so these reads invoke no user conversions. */
|
||
for (size_t i = 0; i < n; i++) {
|
||
double point = value(source, i);
|
||
if (!isfinite(point) || point < 0 || point > 0x10ffff || trunc(point) != point) {
|
||
char number[64], message[96];
|
||
size_t len = scr_f64_to_str(point, number);
|
||
int size = snprintf(message, sizeof message, "Invalid code point %.*s", (int)len, number);
|
||
scr_throw_error_msg(SCR_ERR_RANGE, message, (size_t)size);
|
||
return NULL;
|
||
}
|
||
}
|
||
if (n > (SIZE_MAX - 1) / 4) scr_trap("scriptc: out of memory\n");
|
||
char *out = malloc(n * 4 + 1);
|
||
if (!out) scr_trap("scriptc: out of memory\n");
|
||
size_t offset = 0;
|
||
for (size_t i = 0; i < n; i++) {
|
||
uint32_t point = (uint32_t)value(source, i);
|
||
if (point >= 0xd800 && point <= 0xdbff && i + 1 < n) {
|
||
uint32_t next = (uint32_t)value(source, i + 1);
|
||
if (next >= 0xdc00 && next <= 0xdfff) {
|
||
point = 0x10000 + ((point - 0xd800) << 10) + (next - 0xdc00);
|
||
i++;
|
||
}
|
||
}
|
||
offset += scr_wtf8_put(out + offset, point);
|
||
}
|
||
ScrStr *result = scr_str_new(out, offset);
|
||
free(out);
|
||
return result;
|
||
}
|
||
|
||
ScrStr *scr_str_from_code_point(ScrArr *codes) {
|
||
return scr_str_from_points(codes->len, scr_fcp_array_value, codes);
|
||
}
|
||
|
||
ScrStr *scr_str_from_code_point_bytes(ScrBytes *codes) {
|
||
return scr_str_from_points(codes->len, scr_fcp_bytes_value, codes);
|
||
}
|
||
|
||
/* ── Date, the read-only value slice ───────────────────────────────────
|
||
* Values are TimeClip'd epoch-millisecond scalars. Identity/mutation are
|
||
* frontend-fenced; construction, storage, getters, and ISO formatting
|
||
* observe exactly this payload. */
|
||
|
||
double scr_date_now(void) {
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_REALTIME, &ts);
|
||
/* Node's Date.now() is integer milliseconds. */
|
||
return floor((double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6);
|
||
}
|
||
|
||
/* Date's TimeClip: non-finite/out-of-range values become Invalid Date,
|
||
* finite values truncate toward zero, and -0 normalizes to +0. */
|
||
double scr_date_new_ms(double ms) {
|
||
if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return NAN;
|
||
double clipped = trunc(ms);
|
||
return clipped == 0 ? 0 : clipped;
|
||
}
|
||
|
||
double scr_date_get_time(double ms) { return ms; }
|
||
|
||
/* Node's Date.prototype.toISOString over a millisecond time value:
|
||
* TimeClip's ToInteger truncation, proleptic Gregorian civil-from-days
|
||
* (Howard Hinnant's algorithm), YYYY-MM-DDTHH:mm:ss.sssZ with expanded
|
||
* ±YYYYYY years outside 0–9999, and Node's "Invalid time value"
|
||
* RangeError on NaN / out-of-range values — verified against Node over
|
||
* epoch, negative, fractional, boundary (±8.64e15), and expanded-year
|
||
* inputs. */
|
||
ScrStr *scr_date_to_iso(double ms) {
|
||
if (!(fabs(ms) <= 8640000000000000.0)) { /* NaN and out of range */
|
||
scr_throw_error_msg(SCR_ERR_RANGE, "Invalid time value", 18);
|
||
return NULL;
|
||
}
|
||
double t = trunc(ms);
|
||
double dayd = floor(t / 86400000.0);
|
||
long long msday = (long long)(t - dayd * 86400000.0);
|
||
long long z = (long long)dayd + 719468;
|
||
long long era = (z >= 0 ? z : z - 146096) / 146097;
|
||
unsigned long long doe = (unsigned long long)(z - era * 146097);
|
||
unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||
long long y = (long long)yoe + era * 400;
|
||
unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||
unsigned long long mp = (5 * doy + 2) / 153;
|
||
unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
|
||
unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
|
||
if (m <= 2) y += 1;
|
||
int hh = (int)(msday / 3600000), mi = (int)(msday / 60000 % 60),
|
||
ss = (int)(msday / 1000 % 60), sss = (int)(msday % 1000);
|
||
char buf[40];
|
||
int len;
|
||
if (y < 0) {
|
||
len = snprintf(buf, sizeof buf, "-%06lld-%02llu-%02lluT%02d:%02d:%02d.%03dZ", -y, m, d, hh, mi, ss, sss);
|
||
} else if (y > 9999) {
|
||
len = snprintf(buf, sizeof buf, "+%06lld-%02llu-%02lluT%02d:%02d:%02d.%03dZ", y, m, d, hh, mi, ss, sss);
|
||
} else {
|
||
len = snprintf(buf, sizeof buf, "%04lld-%02llu-%02lluT%02d:%02d:%02d.%03dZ", y, m, d, hh, mi, ss, sss);
|
||
}
|
||
return scr_str_new(buf, (size_t)len);
|
||
}
|
||
|
||
/* ── new Date(dateString).getTime() ──────────────────────────────────────
|
||
*
|
||
* The BOUNDED date-string parse (documented divergence — V8's parser
|
||
* accepts far more): two grammars answer a time value, everything else is
|
||
* NaN (Node's invalid-date getTime).
|
||
*
|
||
* 1. The ASN1_TIME_print shape X509Certificate.validFrom/validTo answer
|
||
* ("Jul 1 00:00:00 2026 GMT", "Jul 17 17:52:11 2026 GMT") — the
|
||
* portless cert-expiry read. Month names match case-insensitively;
|
||
* one or two spaces precede the day (%2d's padding).
|
||
* 2. ECMA's own date-time string format with an EXPLICIT offset:
|
||
* YYYY[-MM[-DD]] date-only forms (UTC, per the spec) and full
|
||
* date-times YYYY-MM-DDTHH:mm[:ss[.sss]] ending in Z or ±HH:MM.
|
||
* Offset-less date-times are LOCAL time in JS; that arm answers NaN
|
||
* here rather than guessing a zone (the divergence note).
|
||
*/
|
||
|
||
/* Howard Hinnant's days_from_civil (the to_iso walk inverted). */
|
||
static double scr_days_from_civil(long long y, int m, int d) {
|
||
y -= m <= 2;
|
||
long long era = (y >= 0 ? y : y - 399) / 400;
|
||
unsigned long long yoe = (unsigned long long)(y - era * 400);
|
||
unsigned long long doy = (unsigned long long)((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1);
|
||
unsigned long long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||
return (double)(era * 146097 + (long long)doe - 719468);
|
||
}
|
||
|
||
static double scr_date_make_ms(long long y, int mo, int d, int hh, int mi, int ss, int ms) {
|
||
/* V8 accepts days 1..31 in every month and ROLLS OVER past the month's
|
||
* end (Feb 30 → Mar 2) — days_from_civil extrapolates linearly, so the
|
||
* rollover falls out; day 0 and 32+ are NaN, like V8. */
|
||
if (mo < 1 || mo > 12 || d < 1 || d > 31) return NAN;
|
||
if (hh > 24 || mi > 59 || ss > 59 || (hh == 24 && (mi || ss || ms))) return NAN;
|
||
double t = scr_days_from_civil(y, mo, d) * 86400000.0 +
|
||
hh * 3600000.0 + mi * 60000.0 + ss * 1000.0 + ms;
|
||
return t;
|
||
}
|
||
|
||
static double scr_date_ms_of(long long y, int mo, int d, int hh, int mi, int ss, int ms) {
|
||
return scr_date_new_ms(scr_date_make_ms(y, mo, d, hh, mi, ss, ms));
|
||
}
|
||
|
||
static bool scr_date_digits(const char **p, const char *end, int n, int *out) {
|
||
int v = 0;
|
||
if (end - *p < n) return false;
|
||
for (int i = 0; i < n; i++) {
|
||
char c = (*p)[i];
|
||
if (c < '0' || c > '9') return false;
|
||
v = v * 10 + (c - '0');
|
||
}
|
||
*p += n;
|
||
*out = v;
|
||
return true;
|
||
}
|
||
|
||
double scr_date_parse_get_time(ScrStr *s) {
|
||
const char *p = s->data;
|
||
const char *end = s->data + s->len;
|
||
static const char *mon[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||
/* Grammar 1: "MMM [D]D HH:MM:SS YYYY GMT" (ASN1_TIME_print). */
|
||
if (end - p > 3 && ((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z'))) {
|
||
int mo = 0;
|
||
for (int i = 0; i < 12; i++) {
|
||
if (tolower((unsigned char)p[0]) == tolower((unsigned char)mon[i][0]) &&
|
||
tolower((unsigned char)p[1]) == tolower((unsigned char)mon[i][1]) &&
|
||
tolower((unsigned char)p[2]) == tolower((unsigned char)mon[i][2])) {
|
||
mo = i + 1;
|
||
break;
|
||
}
|
||
}
|
||
if (mo == 0) return NAN;
|
||
p += 3;
|
||
if (p < end && *p == ' ') p++;
|
||
if (p < end && *p == ' ') p++; /* %2d's padding space */
|
||
int d = 0, hh, mi, ss, y;
|
||
if (!scr_date_digits(&p, end, 1, &d)) return NAN;
|
||
int d2;
|
||
if (p < end && *p >= '0' && *p <= '9' && scr_date_digits(&p, end, 1, &d2)) d = d * 10 + d2;
|
||
if (p >= end || *p++ != ' ') return NAN;
|
||
if (!scr_date_digits(&p, end, 2, &hh) || p >= end || *p++ != ':') return NAN;
|
||
if (!scr_date_digits(&p, end, 2, &mi) || p >= end || *p++ != ':') return NAN;
|
||
if (!scr_date_digits(&p, end, 2, &ss) || p >= end || *p++ != ' ') return NAN;
|
||
if (!scr_date_digits(&p, end, 4, &y)) return NAN;
|
||
if (end - p != 4 || memcmp(p, " GMT", 4) != 0) return NAN;
|
||
return scr_date_ms_of(y, mo, d, hh, mi, ss, 0);
|
||
}
|
||
/* Grammar 2: ECMA's date-time string format. */
|
||
{
|
||
int y, mo = 1, d = 1, hh = 0, mi = 0, ss = 0, ms = 0;
|
||
bool sign_year = p < end && (*p == '+' || *p == '-');
|
||
long long yy;
|
||
if (sign_year) {
|
||
int y6;
|
||
bool neg = *p == '-';
|
||
p++;
|
||
if (!scr_date_digits(&p, end, 6, &y6)) return NAN;
|
||
if (neg && y6 == 0) return NAN; /* ECMA forbids expanded -000000 */
|
||
yy = neg ? -(long long)y6 : y6;
|
||
} else {
|
||
if (!scr_date_digits(&p, end, 4, &y)) return NAN;
|
||
yy = y;
|
||
}
|
||
if (p < end && *p == '-') {
|
||
p++;
|
||
if (!scr_date_digits(&p, end, 2, &mo)) return NAN;
|
||
if (p < end && *p == '-') {
|
||
p++;
|
||
if (!scr_date_digits(&p, end, 2, &d)) return NAN;
|
||
}
|
||
}
|
||
if (p == end) return scr_date_ms_of(yy, mo, d, 0, 0, 0, 0); /* date-only: UTC */
|
||
if (*p++ != 'T') return NAN;
|
||
if (!scr_date_digits(&p, end, 2, &hh) || p >= end || *p++ != ':') return NAN;
|
||
if (!scr_date_digits(&p, end, 2, &mi)) return NAN;
|
||
if (p < end && *p == ':') {
|
||
p++;
|
||
if (!scr_date_digits(&p, end, 2, &ss)) return NAN;
|
||
if (p < end && *p == '.') {
|
||
p++;
|
||
if (!scr_date_digits(&p, end, 3, &ms)) return NAN;
|
||
}
|
||
}
|
||
if (p == end) return NAN; /* offset-less date-time: LOCAL in JS — the divergence */
|
||
double off = 0;
|
||
if (*p == 'Z') {
|
||
p++;
|
||
} else if (*p == '+' || *p == '-') {
|
||
bool neg = *p == '-';
|
||
int oh, om;
|
||
p++;
|
||
if (!scr_date_digits(&p, end, 2, &oh) || p >= end || *p++ != ':') return NAN;
|
||
if (!scr_date_digits(&p, end, 2, &om)) return NAN;
|
||
if (oh > 23 || om > 59) return NAN;
|
||
off = (oh * 60 + om) * 60000.0;
|
||
if (neg) off = -off;
|
||
} else {
|
||
return NAN;
|
||
}
|
||
if (p != end) return NAN;
|
||
/* MakeDate can lie just beyond the TimeClip boundary while the
|
||
* explicit offset brings the final UTC instant back into range. The
|
||
* spec clips only after that offset has been applied. */
|
||
double t = scr_date_make_ms(yy, mo, d, hh, mi, ss, ms);
|
||
return scr_date_new_ms(t - off);
|
||
}
|
||
}
|
||
|
||
/* Date.UTC(year, month, date, hours, minutes, seconds, ms) — the spec's
|
||
* MakeDay/MakeTime/TimeClip pipeline over ALREADY-NUMBER arguments (tsc
|
||
* pins them; the frontend completes omitted trailing arguments with the
|
||
* spec's defaults: month 0, date 1, time parts 0). ToIntegerOrInfinity is
|
||
* trunc on finite values; any non-finite part is NaN. Years 0–99 map to
|
||
* 1900+year (the spec's MakeFullYear), out-of-range months ROLL into the
|
||
* year (Date.UTC(2017, 13) is Feb 2018) and any integer date offsets from
|
||
* day 1 of that month — days_from_civil extrapolates linearly, so both
|
||
* rollovers fall out. V8 bounds MakeDay's input year to ±1e6 and input
|
||
* month to ±1e7 before normalizing the month (kMaxYear/kMinYear and
|
||
* kMaxMonth/kMinMonth, date.h); Node answers NaN past either bound even
|
||
* when the two inputs would normalize back into range. Never throws. */
|
||
double scr_date_utc(double y, double mo, double d,
|
||
double h, double mi, double s, double ms) {
|
||
if (!isfinite(y) || !isfinite(mo) || !isfinite(d) || !isfinite(h) ||
|
||
!isfinite(mi) || !isfinite(s) || !isfinite(ms)) {
|
||
return NAN;
|
||
}
|
||
y = trunc(y);
|
||
mo = trunc(mo);
|
||
d = trunc(d);
|
||
h = trunc(h);
|
||
mi = trunc(mi);
|
||
s = trunc(s);
|
||
ms = trunc(ms);
|
||
if (fabs(y) > 1000000.0 || fabs(mo) > 10000000.0) return NAN;
|
||
if (y >= 0 && y <= 99) y += 1900;
|
||
double ym = y + floor(mo / 12.0);
|
||
int mn = (int)(mo - floor(mo / 12.0) * 12.0); /* 0..11 */
|
||
double days = scr_days_from_civil((long long)ym, mn + 1, 1) + (d - 1.0);
|
||
double t = days * 86400000.0 + h * 3600000.0 + mi * 60000.0 + s * 1000.0 + ms;
|
||
if (fabs(t) > 8640000000000000.0) return NAN; /* TimeClip */
|
||
return t == 0 ? 0 : t; /* normalize -0 (TimeClip's +0) */
|
||
}
|
||
|
||
/* ── Date calendar getters ────────────────────────────────────────────
|
||
* UTC fields use the same proleptic-Gregorian walk as toISOString, so the
|
||
* whole Date range is portable. Local fields use the host timezone via
|
||
* localtime, exactly the environment the sibling Node process observes;
|
||
* a libc that cannot represent an extreme instant answers NaN rather than
|
||
* inventing a zone. */
|
||
|
||
typedef struct ScrDateParts {
|
||
long long year;
|
||
int month, date, day, hours, minutes, seconds, milliseconds;
|
||
double timezone_offset;
|
||
} ScrDateParts;
|
||
|
||
/* Calendar decomposition itself also serves LocalTime(t), which can lie
|
||
* just outside TimeClip's UTC interval after applying a zone offset at an
|
||
* endpoint. The checked wrapper is the public UTC-getter gate; the inner
|
||
* walk accepts those bounded local-time values too. */
|
||
static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
|
||
double dayd = floor(t / 86400000.0);
|
||
long long msday = (long long)(t - dayd * 86400000.0);
|
||
long long z = (long long)dayd + 719468;
|
||
long long era = (z >= 0 ? z : z - 146096) / 146097;
|
||
unsigned long long doe = (unsigned long long)(z - era * 146097);
|
||
unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||
long long y = (long long)yoe + era * 400;
|
||
unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||
unsigned long long mp = (5 * doy + 2) / 153;
|
||
unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
|
||
unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
|
||
if (m <= 2) y++;
|
||
long long wday = ((long long)dayd + 4) % 7;
|
||
if (wday < 0) wday += 7;
|
||
out->year = y;
|
||
out->month = (int)m - 1;
|
||
out->date = (int)d;
|
||
out->day = (int)wday;
|
||
out->hours = (int)(msday / 3600000);
|
||
out->minutes = (int)(msday / 60000 % 60);
|
||
out->seconds = (int)(msday / 1000 % 60);
|
||
out->milliseconds = (int)(msday % 1000);
|
||
out->timezone_offset = 0;
|
||
}
|
||
|
||
static bool scr_date_utc_parts(double ms, ScrDateParts *out) {
|
||
if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return false;
|
||
scr_date_utc_parts_unchecked(trunc(ms), out);
|
||
return true;
|
||
}
|
||
|
||
static bool scr_date_localtime(double secd, struct tm *out) {
|
||
time_t sec = (time_t)secd;
|
||
if ((double)sec != secd) return false;
|
||
#ifdef _WIN32
|
||
return localtime_s(out, &sec) == 0;
|
||
#else
|
||
return localtime_r(&sec, out) != NULL;
|
||
#endif
|
||
}
|
||
|
||
static bool scr_date_local_parts(double ms, ScrDateParts *out) {
|
||
if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return false;
|
||
double clipped = trunc(ms);
|
||
double secd = floor(clipped / 1000.0);
|
||
struct tm tmv;
|
||
double basis_secd = secd;
|
||
if (!scr_date_localtime(basis_secd, &tmv)) {
|
||
/* Windows' _localtime64_s rejects pre-epoch instants and years after
|
||
* 3001 even though both are valid ECMAScript Dates. Query the host's
|
||
* zone rule at a calendar-equivalent surrogate year in 2000..2399:
|
||
* Gregorian weekdays/leap years repeat every 400 years. This keeps
|
||
* every valid Date finite; the OS-vs-Node historical-rule difference
|
||
* remains the documented timezone-data divergence. */
|
||
ScrDateParts utc;
|
||
scr_date_utc_parts_unchecked(clipped, &utc);
|
||
long long cycle_year = (utc.year - 2000) % 400;
|
||
if (cycle_year < 0) cycle_year += 400;
|
||
long long surrogate_year = 2000 + cycle_year;
|
||
basis_secd =
|
||
scr_days_from_civil(surrogate_year, utc.month + 1, utc.date) * 86400.0 +
|
||
utc.hours * 3600.0 + utc.minutes * 60.0 + utc.seconds;
|
||
if (!scr_date_localtime(basis_secd, &tmv)) return false;
|
||
}
|
||
/* Treat the local broken-down fields as UTC. Its distance from the real
|
||
* (or surrogate) epoch second is the zone offset. Apply that offset to
|
||
* the original instant and use the portable Gregorian walk for its
|
||
* fields; Date#getTimezoneOffset uses the
|
||
* opposite sign (UTC - local), in whole minutes. Historical local-mean
|
||
* offsets can contain seconds, which JavaScript truncates toward zero. */
|
||
double local_as_utc =
|
||
scr_days_from_civil((long long)tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday) * 86400.0 +
|
||
tmv.tm_hour * 3600.0 + tmv.tm_min * 60.0 + tmv.tm_sec;
|
||
double local_offset = local_as_utc - basis_secd;
|
||
scr_date_utc_parts_unchecked(clipped + local_offset * 1000.0, out);
|
||
double timezone_offset = trunc(-local_offset / 60.0);
|
||
out->timezone_offset = timezone_offset == 0 ? 0 : timezone_offset;
|
||
return true;
|
||
}
|
||
|
||
static bool scr_date_parts(double ms, bool utc, ScrDateParts *out) {
|
||
return utc ? scr_date_utc_parts(ms, out) : scr_date_local_parts(ms, out);
|
||
}
|
||
|
||
#define SCR_DATE_PART_GETTER(name, field) \
|
||
double scr_date_get_##name(double ms, bool utc) { \
|
||
ScrDateParts p; \
|
||
return scr_date_parts(ms, utc, &p) ? (double)p.field : NAN; \
|
||
} \
|
||
double scr_date_get_##name##_local(double ms) { \
|
||
return scr_date_get_##name(ms, false); \
|
||
} \
|
||
double scr_date_get_##name##_utc(double ms) { \
|
||
return scr_date_get_##name(ms, true); \
|
||
}
|
||
|
||
SCR_DATE_PART_GETTER(full_year, year)
|
||
SCR_DATE_PART_GETTER(month, month)
|
||
SCR_DATE_PART_GETTER(date, date)
|
||
SCR_DATE_PART_GETTER(day, day)
|
||
SCR_DATE_PART_GETTER(hours, hours)
|
||
SCR_DATE_PART_GETTER(minutes, minutes)
|
||
SCR_DATE_PART_GETTER(seconds, seconds)
|
||
|
||
#undef SCR_DATE_PART_GETTER
|
||
|
||
double scr_date_get_milliseconds(double ms) {
|
||
ScrDateParts p;
|
||
return scr_date_utc_parts(ms, &p) ? (double)p.milliseconds : NAN;
|
||
}
|
||
|
||
double scr_date_get_timezone_offset(double ms) {
|
||
ScrDateParts p;
|
||
return scr_date_local_parts(ms, &p) ? p.timezone_offset : NAN;
|
||
}
|
||
|
||
/* ── Number statics ────────────────────────────────────────────────────
|
||
* JS-exact: the ES2015 Number statics never coerce (unlike the global
|
||
* isNaN/isFinite), and the compiler routes only number-typed arguments
|
||
* here, so plain C predicates are the whole story. */
|
||
|
||
bool scr_num_is_finite(double x) { return isfinite(x) != 0; }
|
||
|
||
/* Number.prototype.toExponential() with fractionDigits UNDEFINED — the
|
||
* spec's "as many digits as necessary": the shortest correctly-rounded
|
||
* mantissa that round-trips, formatted d[.ddd]e±X with no zero-padding of
|
||
* the exponent ("7e+0", "1.5e-7"). NaN → "NaN", ±Infinity → its text,
|
||
* ±0 → "0e+0" (the spec's x = +0 arm — the sign of -0 is dropped because
|
||
* -0 < 0 is false). Verified differentially against Node. */
|
||
ScrStr *scr_num_to_exponential(double x) {
|
||
if (isnan(x)) return scr_str_new("NaN", 3);
|
||
if (isinf(x)) return x < 0 ? scr_str_new("-Infinity", 9) : scr_str_new("Infinity", 8);
|
||
if (x == 0) return scr_str_new("0e+0", 4);
|
||
char buf[64];
|
||
int len = 0;
|
||
for (int prec = 0; prec <= 17; prec++) {
|
||
len = snprintf(buf, sizeof buf, "%.*e", prec, x);
|
||
if (strtod(buf, NULL) == x) break;
|
||
}
|
||
/* Normalize C's exponent ("e+05" / "e-123") to JS's ("e+5"/"e-123"):
|
||
* drop leading zeros after the sign, keeping at least one digit. */
|
||
char out[64];
|
||
int o = 0;
|
||
const char *e = memchr(buf, 'e', (size_t)len);
|
||
const char *p = buf;
|
||
while (p < e) out[o++] = *p++;
|
||
out[o++] = 'e';
|
||
p++; /* past 'e' */
|
||
out[o++] = *p++; /* the sign — %e always emits one */
|
||
while (*p == '0' && p[1] >= '0' && p[1] <= '9') p++;
|
||
while (p < buf + len) out[o++] = *p++;
|
||
return scr_str_new(out, (size_t)o);
|
||
}
|
||
|
||
/* Number.prototype.toFixed() with fractionDigits UNDEFINED (= 0 digits) —
|
||
* the spec's pick of the integer n closest to x with ties toward the
|
||
* LARGER n on the magnitude ((2.5).toFixed() = "3", (-2.5).toFixed() =
|
||
* "-3" — printf's half-even would answer "2"), the "-0" result for
|
||
* negative fractions rounding to zero, and ToString fallback at
|
||
* |x| ≥ 1e21 (NaN and ±Infinity ride that arm's texts via ToString too,
|
||
* matching the spec's early answers). */
|
||
ScrStr *scr_num_to_fixed0(double x) {
|
||
if (isnan(x)) return scr_str_new("NaN", 3);
|
||
if (fabs(x) >= 1e21) return scr_f64_to_scrstr(x);
|
||
double a = fabs(x);
|
||
double fl = floor(a);
|
||
double n = (a - fl >= 0.5) ? fl + 1 : fl; /* exact: a < 2^70, and below
|
||
* 2^52 the fraction is exact;
|
||
* at/above it a - fl == 0 */
|
||
ScrStr *digits = scr_f64_to_scrstr(n); /* n < 1e21 → never exponent form */
|
||
if (!(x < 0)) return digits;
|
||
size_t len = digits->len;
|
||
char buf[64];
|
||
buf[0] = '-';
|
||
memcpy(buf + 1, digits->data, len);
|
||
ScrStr *r = scr_str_new(buf, len + 1);
|
||
scr_str_release(digits);
|
||
return r;
|
||
}
|
||
|
||
/* Number.prototype.toFixed(fractionDigits), with the argument already
|
||
* number-typed by the frontend. ToIntegerOrInfinity validation precedes
|
||
* the receiver's non-finite arm, as ECMA-262 requires. Invalid precision
|
||
* raises V8's catchable RangeError text; otherwise the result is +1. The
|
||
* exact-binary-value digits come from scr_f64_to_fixed (scr_number.c). */
|
||
ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
|
||
double fd = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
|
||
if (!(fd >= 0 && fd <= 100)) {
|
||
static const char msg[] =
|
||
"toFixed() digits argument must be between 0 and 100";
|
||
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
|
||
return NULL;
|
||
}
|
||
if (!isfinite(x) || fabs(x) >= 1e21) return scr_f64_to_scrstr(x);
|
||
char out[128];
|
||
size_t len = scr_f64_to_fixed(x, (int)fd, out);
|
||
return scr_str_new(out, len);
|
||
}
|
||
|
||
/* Increment a decimal digit string in place. Returns true on overflow —
|
||
* the value becomes 1 followed by len zeros (the caller folds the zeros
|
||
* into its scale); an EMPTY string increments to "1" the same way (the
|
||
* round-up-from-nothing case: 0.0005 at 3 fraction digits). */
|
||
static bool scr_dec_inc(char *d, int len) {
|
||
for (int i = len - 1; i >= 0; i--) {
|
||
if (d[i] != '9') {
|
||
d[i]++;
|
||
return false;
|
||
}
|
||
d[i] = '0';
|
||
}
|
||
d[0] = '1';
|
||
return true;
|
||
}
|
||
|
||
/* Intl.NumberFormat("en-US").format(x) / x.toLocaleString("en-US") with
|
||
* DEFAULT options: decimal notation, minimum 0 / maximum 3 fraction
|
||
* digits, "," grouping every three integer digits, "∞"/"NaN" texts, and
|
||
* "-0" whenever the input is negative or negative zero even after
|
||
* rounding to zero. Rounding is half-up ON THE SHORTEST ROUND-TRIPPING
|
||
* DECIMAL — ICU's rounding input, probed against Node: format(1.0005) is
|
||
* "1.001" although the double is 1.000499... and toFixed(3) answers
|
||
* "1.000"; format(1e23) prints the shortest form's trailing zeros, not
|
||
* the double's exact expansion. The en-US/latn symbols (",", ".", "∞",
|
||
* "NaN", group size 3) are the whole embedded locale surface. Verified
|
||
* differentially against Node. Result +1; never throws. */
|
||
ScrStr *scr_intl_num_format_en_us(double x) {
|
||
if (isnan(x)) return scr_str_new("NaN", 3);
|
||
if (isinf(x)) {
|
||
return x < 0 ? scr_str_new("-\xE2\x88\x9E", 4) : scr_str_new("\xE2\x88\x9E", 3);
|
||
}
|
||
bool neg = signbit(x) != 0;
|
||
if (x == 0) return neg ? scr_str_new("-0", 2) : scr_str_new("0", 1);
|
||
double a = neg ? -x : x;
|
||
|
||
/* Shortest digits: value = 0.d × 10^n (no trailing zeros, k ≤ 17). */
|
||
char d[18];
|
||
int n;
|
||
int k = scr_f64_digits(a, d, &n);
|
||
|
||
/* Round at 3 fraction digits: fraction position p is digit index
|
||
* n+p-1, so index n+3 is the first DROPPED digit. Half-up on the
|
||
* decimal digits — the shortest string ends right after them, so
|
||
* "first dropped digit ≥ 5" IS the whole decision. */
|
||
int keep = n + 3;
|
||
if (keep < k) {
|
||
bool up = keep >= 0 && d[keep] >= '5';
|
||
k = keep < 0 ? 0 : keep;
|
||
if (up && scr_dec_inc(d, k)) {
|
||
/* Carried out (all nines, or the round-up-from-nothing 0.0005
|
||
* case): one leading 1, the dropped nines fold into the scale. */
|
||
k = 1;
|
||
n += 1;
|
||
} else if (k == 0) {
|
||
/* Everything rounded away: ±0 with the sign preserved. */
|
||
return neg ? scr_str_new("-0", 2) : scr_str_new("0", 1);
|
||
}
|
||
}
|
||
|
||
/* Assemble: integer digits (indices [0, n)), zero-padded past k, then
|
||
* the ≤ 3 fraction digits (indices n..n+2, '0' outside [0, k)) with
|
||
* trailing zeros trimmed, then commas every three integer digits. */
|
||
char frac[3];
|
||
int flen = 0;
|
||
for (int p = 1; p <= 3; p++) {
|
||
int idx = n + p - 1;
|
||
frac[flen++] = (idx >= 0 && idx < k) ? d[idx] : '0';
|
||
}
|
||
while (flen > 0 && frac[flen - 1] == '0') flen--;
|
||
|
||
char out[512];
|
||
int o = 0;
|
||
if (neg) out[o++] = '-';
|
||
if (n <= 0) {
|
||
out[o++] = '0';
|
||
} else {
|
||
for (int i = 0; i < n; i++) {
|
||
if (i > 0 && (n - i) % 3 == 0) out[o++] = ',';
|
||
out[o++] = (i < k) ? d[i] : '0';
|
||
}
|
||
}
|
||
if (flen > 0) {
|
||
out[o++] = '.';
|
||
memcpy(out + o, frac, (size_t)flen);
|
||
o += flen;
|
||
}
|
||
return scr_str_new(out, (size_t)o);
|
||
}
|
||
|
||
bool scr_num_is_nan(double x) { return isnan(x) != 0; }
|
||
|
||
bool scr_num_is_integer(double x) { return isfinite(x) && trunc(x) == x; }
|
||
|
||
bool scr_num_is_safe_integer(double x) {
|
||
return isfinite(x) && trunc(x) == x && fabs(x) <= 9007199254740991.0;
|
||
}
|
||
|
||
/* ── checked catch-binding cast (`e as C`) ────────────────────────────
|
||
* The caught analog of the dyn boundary's checked casts: an OBJ payload
|
||
* inside the class's preorder interval extracts (retained, +1); every
|
||
* other payload throws a catchable TypeError naming the class. Node's
|
||
* `as` is erasure — the runtime check is the documented trust-but-verify
|
||
* stance for dynamic values, extended to exception payloads. */
|
||
void *scr_caught_check_obj(const ScrCaught *c, size_t pre, size_t post,
|
||
const char *cls) {
|
||
if (scr_caught_instanceof(c, pre, post)) return c->retain_fn(c->payload);
|
||
char msg[160];
|
||
int len = snprintf(msg, sizeof msg,
|
||
"caught value is not an instance of %s (checked cast)",
|
||
cls);
|
||
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
|
||
return NULL; /* callers are compiler-emitted pending checks */
|
||
}
|
||
|
||
/* ── Set → array drain ([...set]) ─────────────────────────────────────
|
||
* The live entries in insertion order (tombstones skipped — the forEach
|
||
* walk folded into one call; no user code runs mid-drain, so the
|
||
* live-iteration rules are moot). Borrows the set; the array is +1,
|
||
* string elements retained into it by the iter_key read. */
|
||
static bool scr_set_to_arr_sparse(const ScrMap *s, ScrArr *out) {
|
||
if (out->cap >= s->nlive) return false;
|
||
/* scr_arr_new reserves dense storage for the live count; keep the
|
||
* ordinary append path in case an allocation ever declines it. */
|
||
for (size_t i = 0; i < s->nentries; i++) {
|
||
if (!s->entries[i].hash) continue;
|
||
if (out->elem == SCR_ELEM_F64) scr_arr_push_f64(out, scr_map_iter_key_f64(s, (double)i));
|
||
else if (out->elem == SCR_ELEM_BOOL) scr_arr_push_bool(out, scr_map_iter_key_bool(s, (double)i));
|
||
else if (out->elem == SCR_ELEM_STR) scr_arr_push_ref(out, scr_map_iter_key_str(s, (double)i));
|
||
else scr_arr_push_ref(out, scr_map_iter_key_ref(s, (double)i));
|
||
}
|
||
return true;
|
||
}
|
||
|
||
ScrArr *scr_set_to_arr_f64(const ScrMap *s) {
|
||
ScrArr *out = scr_arr_new(SCR_ELEM_F64, s->nlive);
|
||
if (scr_set_to_arr_sparse(s, out)) return out;
|
||
for (size_t i = 0; i < s->nentries; i++) {
|
||
if (!s->entries[i].hash) continue;
|
||
out->data[out->len] = s->entries[i].key;
|
||
out->present[out->len++] = SCR_ARR_VALUE;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
ScrArr *scr_set_to_arr_bool(const ScrMap *s) {
|
||
ScrArr *out = scr_arr_new(SCR_ELEM_BOOL, s->nlive);
|
||
if (scr_set_to_arr_sparse(s, out)) return out;
|
||
for (size_t i = 0; i < s->nentries; i++) {
|
||
if (!s->entries[i].hash) continue;
|
||
scr_arr_dense_put(out, out->len, s->entries[i].key != 0);
|
||
out->present[out->len++] = SCR_ARR_VALUE;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
ScrArr *scr_set_to_arr_str(const ScrMap *s) {
|
||
ScrArr *out = scr_arr_new(SCR_ELEM_STR, s->nlive);
|
||
if (scr_set_to_arr_sparse(s, out)) return out;
|
||
for (size_t i = 0; i < s->nentries; i++) {
|
||
if (!s->entries[i].hash) continue;
|
||
scr_str_retain((ScrStr *)(uintptr_t)s->entries[i].key);
|
||
out->data[out->len] = s->entries[i].key;
|
||
out->present[out->len++] = SCR_ARR_VALUE;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) {
|
||
ScrArr *out = scr_arr_new_ops(ops, s->nlive);
|
||
if (scr_set_to_arr_sparse(s, out)) return out;
|
||
for (size_t i = 0; i < s->nentries; i++) {
|
||
if (!s->entries[i].hash) continue;
|
||
ops->retain((void *)(uintptr_t)s->entries[i].key);
|
||
out->data[out->len] = s->entries[i].key;
|
||
out->present[out->len++] = SCR_ARR_VALUE;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/* JS Date storage owns an identity; the typed read-only Date ABI continues
|
||
* to use milliseconds and explicitly extracts them for native getters. */
|
||
typedef struct { size_t rc; double milliseconds; } ScrNativeDate;
|
||
static void *scr_native_date_retain(void *ptr) { ((ScrNativeDate *)ptr)->rc++; return ptr; }
|
||
static void scr_native_date_release(void *ptr) { if (--((ScrNativeDate *)ptr)->rc == 0) free(ptr); }
|
||
|
||
bool scr_dyn_native_date_is(const ScrDyn *value) {
|
||
return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE;
|
||
}
|
||
|
||
double scr_dyn_native_date_value(const ScrDyn *value) {
|
||
if (!scr_dyn_native_date_is(value)) { scr_dyn_check_fail(NULL, "Date", value); return NAN; }
|
||
return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds;
|
||
}
|
||
|
||
static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method,
|
||
ScrDyn *const *args, size_t argc, const char *what) {
|
||
(void)self; (void)args; (void)argc; (void)what;
|
||
double ms = ((ScrNativeDate *)ptr)->milliseconds;
|
||
if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms);
|
||
if (!strcmp(method, "toISOString") || !strcmp(method, "toJSON")) {
|
||
if (!strcmp(method, "toJSON") && !isfinite(ms)) return scr_dyn_new_null();
|
||
ScrStr *text = scr_date_to_iso(ms);
|
||
if (!text) return NULL;
|
||
ScrDyn *result = scr_dyn_new_str(text);
|
||
scr_str_release(text);
|
||
return result;
|
||
}
|
||
#define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms))
|
||
DATE_GET("getFullYear", scr_date_get_full_year_local);
|
||
DATE_GET("getUTCFullYear", scr_date_get_full_year_utc);
|
||
DATE_GET("getMonth", scr_date_get_month_local);
|
||
DATE_GET("getUTCMonth", scr_date_get_month_utc);
|
||
DATE_GET("getDate", scr_date_get_date_local);
|
||
DATE_GET("getUTCDate", scr_date_get_date_utc);
|
||
DATE_GET("getDay", scr_date_get_day_local);
|
||
DATE_GET("getUTCDay", scr_date_get_day_utc);
|
||
DATE_GET("getHours", scr_date_get_hours_local);
|
||
DATE_GET("getUTCHours", scr_date_get_hours_utc);
|
||
DATE_GET("getMinutes", scr_date_get_minutes_local);
|
||
DATE_GET("getUTCMinutes", scr_date_get_minutes_utc);
|
||
DATE_GET("getSeconds", scr_date_get_seconds_local);
|
||
DATE_GET("getUTCSeconds", scr_date_get_seconds_utc);
|
||
DATE_GET("getMilliseconds", scr_date_get_milliseconds);
|
||
DATE_GET("getUTCMilliseconds", scr_date_get_milliseconds);
|
||
DATE_GET("getTimezoneOffset", scr_date_get_timezone_offset);
|
||
#undef DATE_GET
|
||
static const char message[] = "Native Date method has no lowering";
|
||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||
return NULL;
|
||
}
|
||
|
||
ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) {
|
||
double ms;
|
||
if (arguments->v.arr.len == 0) ms = scr_date_now();
|
||
else {
|
||
ScrDyn *value = arguments->v.arr.items[0];
|
||
if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value);
|
||
else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str);
|
||
else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL;
|
||
ms = scr_date_new_ms(ms);
|
||
}
|
||
static const ScrDynHandleOps ops = {
|
||
"Date", &scr_native_date_retain, &scr_native_date_release, &scr_native_date_invoke,
|
||
NULL, NULL, NULL, NULL,
|
||
};
|
||
scr_dyn_handle_install(SCR_DYNH_DATE, &ops);
|
||
ScrNativeDate *date = malloc(sizeof *date);
|
||
if (!date) scr_trap("scriptc: out of memory\n");
|
||
date->rc = 1;
|
||
date->milliseconds = ms;
|
||
ScrDyn *result = scr_dyn_new_handle(date, SCR_DYNH_DATE);
|
||
scr_native_date_release(date);
|
||
return result;
|
||
}
|