Files
scriptc/packages/runtime/src/scr_util_style.c
T
Chris Tate 9835ad0d75 Move scriptc to Node 24.22.0
Pin Node v24.22.0 (commit f14cd3aa7bf9, Undici 7.29.1, OpenSSL 3.5.8) in
.node-version, Dockerfile.sandbox, the docs matrix fallback, the
compatibility pin, the fetch profile, and process.version. A node-pin test
checks that these pins agree. The Node compatibility snapshot is
regenerated from the v24.22.0 API docs; the new stream_iter chapter maps
to node:stream/iter, and the SuiteContext rows no longer match the
TestContext evidence.

Runtime and compiler changes follow Node 24.22 behavior:

- fs.read, fs.readSync, and FileHandle.read validate the position before
  the zero-length return.
- fs.statfs results include frsize; Windows reads
  FileFsFullSizeInformation and reports bfree and bavail separately.
- fs.utimes, futimes, and lutimes keep nanosecond precision.
- ServerResponse sets writableFinished where 'finish' is emitted and runs
  end() callbacks before the 'finish' listeners while the socket is still
  attached; a request read to 'end' closes before its response.
- socket.write(string, 'buffer') throws ERR_UNKNOWN_ENCODING.
- URL protocol-setter rejections keep the current URL, the host setter
  no longer leaves a "/." escape when it adds a port to a "//" path, and
  new URLSearchParams(null) parses "null".
- util.stripVTControlCharacters uses Node 24.22's ANSI pattern,
  util.styleText accepts #RGB and #RRGGBB colors, and
  util.types.isDataView has its JS name and length.
- structuredClone and AbortSignal.any use the "cannot be converted to
  sequence" message.
- crypto.getCiphers, getHashes, and constants match Node 24.22.
- dns.lookup, net.connect, and tls.connect throw ERR_INVALID_ARG_VALUE for
  hosts with null bytes.
- fs.readFile validates options.buffer and refuses its valid forms;
  httpValidation and certificateCompression refuse by name.
- url.domainToASCII, url.domainToUnicode, and url.parse hostnames follow
  ada 4.0: ASCII domains are lowercased without checking their xn--
  labels, the RFC 5893 rule checks how LTR labels end and how RTL labels
  start, a Punycode label whose decoded form begins with "xn--" fails, and
  ToUnicode decodes only labels that pass the ToASCII checks. The url
  legacy test cases are regenerated with Node 24.22.

Corpus programs 5275-5279 cover the new behavior. 1640, 2283, 2599, and
3136 are updated where they pinned Node 24.15 output.
2026-10-14 17:27:02 -05:00

390 lines
16 KiB
C

#include "scr_runtime.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Standard Node 24 inspect colors, including its non-enumerable aliases.
* Native inspect.colors mutation has no static surface. */
typedef struct ScrTextStyle { const char *name; unsigned open, close; } ScrTextStyle;
static const ScrTextStyle util_text_styles[] = {
{ "reset", 0, 0 },
{ "bold", 1, 22 },
{ "dim", 2, 22 },
{ "italic", 3, 23 },
{ "underline", 4, 24 },
{ "blink", 5, 25 },
{ "inverse", 7, 27 },
{ "hidden", 8, 28 },
{ "strikethrough", 9, 29 },
{ "doubleunderline", 21, 24 },
{ "black", 30, 39 },
{ "red", 31, 39 },
{ "green", 32, 39 },
{ "yellow", 33, 39 },
{ "blue", 34, 39 },
{ "magenta", 35, 39 },
{ "cyan", 36, 39 },
{ "white", 37, 39 },
{ "bgBlack", 40, 49 },
{ "bgRed", 41, 49 },
{ "bgGreen", 42, 49 },
{ "bgYellow", 43, 49 },
{ "bgBlue", 44, 49 },
{ "bgMagenta", 45, 49 },
{ "bgCyan", 46, 49 },
{ "bgWhite", 47, 49 },
{ "framed", 51, 54 },
{ "overlined", 53, 55 },
{ "gray", 90, 39 },
{ "redBright", 91, 39 },
{ "greenBright", 92, 39 },
{ "yellowBright", 93, 39 },
{ "blueBright", 94, 39 },
{ "magentaBright", 95, 39 },
{ "cyanBright", 96, 39 },
{ "whiteBright", 97, 39 },
{ "bgGray", 100, 49 },
{ "bgRedBright", 101, 49 },
{ "bgGreenBright", 102, 49 },
{ "bgYellowBright", 103, 49 },
{ "bgBlueBright", 104, 49 },
{ "bgMagentaBright", 105, 49 },
{ "bgCyanBright", 106, 49 },
{ "bgWhiteBright", 107, 49 },
{ "grey", 90, 39 },
{ "blackBright", 90, 39 },
{ "bgGrey", 100, 49 },
{ "bgBlackBright", 100, 49 },
{ "faint", 2, 22 },
{ "crossedout", 9, 29 },
{ "strikeThrough", 9, 29 },
{ "crossedOut", 9, 29 },
{ "conceal", 8, 28 },
{ "swapColors", 7, 27 },
{ "swapcolors", 7, 27 },
{ "doubleUnderline", 21, 24 },
};
static ScrDyn *util_style_view(const ScrDyn *value) {
return value->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(value)
: scr_dyn_retain((ScrDyn *)value);
}
static ScrDyn *util_style_member(const ScrDyn *value, const char *key) {
if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL)
return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *view = util_style_view(value);
if (!view) return NULL;
ScrDyn *result = scr_dyn_obj_read(view, key, strlen(key));
scr_dyn_release(view);
return result;
}
static const ScrTextStyle *util_style_lookup(const ScrDyn *key) {
if (key->kind == SCR_DYN_SYMBOL) return NULL;
ScrStr *name = scr_dyn_string_constructor(key);
if (!name) return NULL;
const ScrTextStyle *result = NULL;
for (size_t i = 0; i < sizeof util_text_styles / sizeof util_text_styles[0]; i++) {
if (name->len == strlen(util_text_styles[i].name) &&
!memcmp(name->data, util_text_styles[i].name, name->len)) {
result = &util_text_styles[i]; break;
}
}
scr_str_release(name);
return result;
}
static bool util_style_none(const ScrDyn *key) {
return key->kind == SCR_DYN_STR && key->v.str->len == 4 &&
!memcmp(key->v.str->data, "none", 4);
}
static void util_style_invalid(const ScrDyn *key, bool hex) {
ScrDyn *view = util_style_view(key);
if (!view) return;
ScrStr *shown = scr_insp_dyn(view, 0, 2);
scr_dyn_release(view);
if (!shown) return;
ScrJsonBuf message;
scr_jb_init(&message);
if (hex) {
scr_jb_puts(&message, "The argument 'format' must be a valid hex color (#RGB or #RRGGBB)");
} else {
scr_jb_puts(&message, "The argument 'format' must be one of: ");
for (size_t i = 0; i < sizeof util_text_styles / sizeof util_text_styles[0]; i++) {
if (i) scr_jb_puts(&message, ", ");
scr_jb_putc(&message, '\'');
scr_jb_puts(&message, util_text_styles[i].name);
scr_jb_putc(&message, '\'');
}
}
scr_jb_puts(&message, ". Received ");
scr_jb_put_str(&message, shown);
ScrStr *text = scr_jb_finish(&message);
scr_throw_error_msg_code(SCR_ERR_TYPE, text->data, text->len, "ERR_INVALID_ARG_VALUE");
scr_str_release(text); scr_str_release(shown);
}
/* A string format starting with '#' names a 24-bit foreground color:
* 1 writes its open sequence for #RGB/#RRGGBB, -1 rejects other '#'
* strings, and 0 leaves every other format to the named styles. */
static int util_style_hex(const ScrDyn *key, char open[32]) {
if (key->kind != SCR_DYN_STR || key->v.str->len == 0 || key->v.str->data[0] != '#') return 0;
const char *hex = key->v.str->data + 1;
size_t digits = key->v.str->len - 1;
if (digits != 3 && digits != 6) return -1;
unsigned value[6];
for (size_t i = 0; i < digits; i++) {
char c = hex[i];
if (c >= '0' && c <= '9') value[i] = (unsigned)(c - '0');
else if (c >= 'a' && c <= 'f') value[i] = (unsigned)(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') value[i] = (unsigned)(c - 'A' + 10);
else return -1;
}
unsigned rgb[3];
for (size_t i = 0; i < 3; i++)
rgb[i] = digits == 3 ? value[i] * 17 : value[2 * i] * 16 + value[2 * i + 1];
snprintf(open, 32, "\x1b[38;2;%u;%u;%um", rgb[0], rgb[1], rgb[2]);
return 1;
}
/* Reopen an outer style when a nested fragment closes it. Bold and dim
* share 22, so preserve that reset before restoring either attribute.
* A close at the very end is kept intact, matching Node's replacement. */
static void util_style_putn(ScrJsonBuf *buffer, const char *data, size_t length) {
scr_jb_write(buffer, data, length);
}
static ScrStr *util_style_replace(const ScrStr *text, const char *close,
const char *open, bool keep_close) {
size_t close_len = strlen(close), at = 0;
ScrJsonBuf result;
scr_jb_init(&result);
for (size_t i = 0; i + close_len < text->len;) {
if (!memcmp(text->data + i, close, close_len)) {
util_style_putn(&result, text->data + at, i - at);
if (keep_close) scr_jb_puts(&result, close);
scr_jb_puts(&result, open);
i += close_len; at = i;
} else i++;
}
util_style_putn(&result, text->data + at, text->len - at);
return scr_jb_finish(&result);
}
static bool util_style_env_nonempty(const char *name) {
const char *value = scr_getenv(name);
return value && *value;
}
/* Node recognizes structural stream implementations as well as branded
* instances. Keep that valid shape outside the native stdio boundary. */
static bool util_style_custom_stream(const ScrDyn *stream) {
static const char *const keys[] = { "_readableState", "_writableState", "write", "on", "pipe", "pipeThrough", "getReader", "cancel", "getWriter", "abort" };
ScrDyn *members[sizeof keys / sizeof keys[0]] = {0};
bool result = false;
for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++) {
members[i] = util_style_member(stream, keys[i]);
if (!members[i] || scr_exc_pending()) goto done;
}
result = scr_dyn_truthy(members[0]) || scr_dyn_truthy(members[1]) ||
(members[3]->kind == SCR_DYN_FUNC && (members[2]->kind == SCR_DYN_FUNC || members[4]->kind == SCR_DYN_FUNC)) ||
(members[5]->kind == SCR_DYN_FUNC && members[6]->kind == SCR_DYN_FUNC && members[7]->kind == SCR_DYN_FUNC) ||
(members[8]->kind == SCR_DYN_FUNC && members[9]->kind == SCR_DYN_FUNC);
done:
for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++) scr_dyn_release(members[i]);
return result;
}
/* styleText only needs the color/no-color distinction, rather than the
* full 4/8/24-bit depth. Follow the ordering of Node 24 internal/tty. */
bool scr_util_terminal_colors(void) {
const char *force = scr_getenv("FORCE_COLOR");
if (force) {
bool enabled = !strcmp(force, "") || !strcmp(force, "true") ||
!strcmp(force, "1") || !strcmp(force, "2") || !strcmp(force, "3");
static SCR_TL bool warned = false;
const char *disabled = util_style_env_nonempty("NO_COLOR") ? "NO_COLOR" :
util_style_env_nonempty("NODE_DISABLE_COLORS") ? "NODE_DISABLE_COLORS" : NULL;
if (enabled && disabled && !warned) {
ScrJsonBuf message;
scr_jb_init(&message);
scr_jb_puts(&message, "The '"); scr_jb_puts(&message, disabled);
scr_jb_puts(&message, "' env is ignored due to the 'FORCE_COLOR' env being set.");
ScrStr *text = scr_jb_finish(&message);
scr_emit_warning("Warning", NULL, text);
scr_str_release(text); warned = true;
}
return enabled;
}
const char *term = scr_getenv("TERM");
if (util_style_env_nonempty("NO_COLOR") || util_style_env_nonempty("NODE_DISABLE_COLORS") ||
(term && !strcmp(term, "dumb"))) return false;
#ifdef _WIN32
return true;
#endif
if (util_style_env_nonempty("TMUX")) return true;
if (scr_getenv("TF_BUILD") && scr_getenv("AGENT_NAME")) return true;
if (scr_getenv("CI")) {
static const char *const ci[] = { "APPVEYOR", "BUILDKITE", "CIRCLECI", "DRONE", "GITEA_ACTIONS", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS" };
for (size_t i = 0; i < sizeof ci / sizeof ci[0]; i++) if (scr_getenv(ci[i])) return true;
const char *name = scr_getenv("CI_NAME");
return name && !strcmp(name, "codeship");
}
const char *teamcity = scr_getenv("TEAMCITY_VERSION");
if (teamcity) {
const char *p = teamcity;
if (!strncmp(p, "9.", 2)) {
p += 2; while (*p == '0') p++;
if (*p < '1' || *p > '9') return false;
while (*p >= '0' && *p <= '9') p++;
return *p == '.';
}
while (*p >= '0' && *p <= '9') p++;
return p - teamcity >= 2 && *p == '.';
}
const char *program = scr_getenv("TERM_PROGRAM");
if (program && (!strcmp(program, "iTerm.app") || !strcmp(program, "HyperTerm") ||
!strcmp(program, "MacTerm") || !strcmp(program, "Apple_Terminal"))) return true;
const char *colorterm = scr_getenv("COLORTERM");
if (colorterm && (!strcmp(colorterm, "truecolor") || !strcmp(colorterm, "24bit"))) return true;
if (term) {
if (strstr(term, "truecolor") || !strncmp(term, "xterm-256", 9)) return true;
char *lower = malloc(strlen(term) + 1);
if (!lower) abort();
for (size_t i = 0; i <= strlen(term); i++) lower[i] = term[i] >= 'A' && term[i] <= 'Z' ? term[i] + ('a' - 'A') : term[i];
static const char *const terminals[] = { "eterm", "cons25", "console", "cygwin", "dtterm", "gnome", "hurd", "jfbterm", "konsole", "kterm", "mlterm", "mosh", "putty", "st", "rxvt-unicode-24bit", "terminator", "xterm-kitty" };
bool found = false;
for (size_t i = 0; i < sizeof terminals / sizeof terminals[0]; i++) found |= !strcmp(lower, terminals[i]);
found |= strstr(lower, "ansi") || strstr(lower, "color") || strstr(lower, "linux") || strstr(lower, "direct") ||
!strncmp(lower, "rxvt", 4) || !strncmp(lower, "screen", 6) || !strncmp(lower, "xterm", 5) ||
!strncmp(lower, "vt100", 5) || !strncmp(lower, "vt220", 5);
if (!strncmp(lower, "con", 3)) {
const char *p = lower + 3;
while (*p >= '0' && *p <= '9') p++;
if (*p == 'x' && p[1] >= '0' && p[1] <= '9') found = true;
}
free(lower);
if (found) return true;
}
return colorterm && *colorterm;
}
ScrStr *scr_util_style_text(const ScrDyn *format, const ScrDyn *text, const ScrDyn *options) {
ScrDyn *validate = util_style_member(options, "validateStream");
if (!validate) return NULL;
if (validate->kind == SCR_DYN_UNDEF || validate->kind == SCR_DYN_NULL) {
scr_dyn_release(validate); validate = scr_dyn_new_bool(true);
}
const ScrTextStyle *fast = NULL;
if (!scr_dyn_truthy(validate) && format->kind == SCR_DYN_STR && text->kind == SCR_DYN_STR) {
if (util_style_none(format)) { scr_dyn_release(validate); return scr_str_retain(text->v.str); }
fast = util_style_lookup(format);
char open[32];
if (!fast && util_style_hex(format, open) == 1) {
static const char close[] = "\x1b[39m";
ScrStr *processed = util_style_replace(text->v.str, close, open, false);
ScrJsonBuf styled;
scr_jb_init(&styled);
scr_jb_puts(&styled, open);
scr_jb_put_str(&styled, processed);
scr_jb_puts(&styled, close);
scr_str_release(processed);
scr_dyn_release(validate);
return scr_jb_finish(&styled);
}
}
bool colorize = true;
ScrDyn *view = NULL, *stream = NULL;
ScrStr *result = NULL, *processed = NULL;
if (!fast) {
if (text->kind != SCR_DYN_STR) { scr_dyn_arg_type_fail("text", "of type string", text); goto done; }
view = util_style_view(options);
if (!view) goto done;
if (view->kind != SCR_DYN_UNDEF && view->kind != SCR_DYN_OBJ) {
scr_dyn_arg_type_fail("options", "of type object", view); goto done;
}
if (validate->kind != SCR_DYN_BOOL) {
scr_dyn_prop_type_fail("options.validateStream", "of type boolean", validate); goto done;
}
if (validate->v.b) {
stream = util_style_member(options, "stream");
if (!stream) goto done;
if (stream->kind == SCR_DYN_TYPED_REF && !strncmp(stream->v.typed_ref.type_key, "record:", 7)) {
ScrDyn *native = util_style_view(stream);
scr_dyn_release(stream); stream = native;
if (!stream) goto done;
}
double fd = 1;
if (stream->kind != SCR_DYN_UNDEF && stream->kind != SCR_DYN_NULL) {
if (stream->kind == SCR_DYN_HANDLE && stream->v.handle.tag == SCR_DYNH_STDIO) {
ScrStr *key = scr_str_new("fd", 2);
ScrDyn *value = scr_dyn_handle_key_get(stream, key);
scr_str_release(key);
if (!value) goto done;
fd = value->v.num; scr_dyn_release(value);
} else if (stream->kind == SCR_DYN_HANDLE || stream->kind == SCR_DYN_TYPED_REF ||
(stream->kind == SCR_DYN_OBJ && util_style_custom_stream(stream))) {
static const char message[] = "util.styleText over native custom streams is not supported yet";
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "SC2020"); goto done;
} else {
if (scr_exc_pending()) goto done;
scr_dyn_arg_type_fail("stream", "an instance of ReadableStream, WritableStream, or Stream", stream); goto done;
}
}
colorize = scr_getenv("FORCE_COLOR") ? scr_util_terminal_colors() :
scr_process_is_tty(fd) && scr_util_terminal_colors();
}
}
scr_dyn_release(view); view = util_style_view(format);
if (!view) goto done;
processed = scr_str_retain(text->v.str);
ScrJsonBuf opens, closes;
scr_jb_init(&opens); scr_jb_init(&closes);
size_t count = view->kind == SCR_DYN_ARR ? view->v.arr.len : 1;
for (size_t i = 0; i < count; i++) {
const ScrDyn *key = view->kind == SCR_DYN_ARR ? view->v.arr.items[i] : view;
if (util_style_none(key)) continue;
char open[32], close[16];
int hex = util_style_hex(key, open);
if (hex < 0) {
util_style_invalid(key, true);
scr_str_release(scr_jb_finish(&opens)); scr_str_release(scr_jb_finish(&closes)); goto done;
}
const ScrTextStyle *style = hex ? NULL : util_style_lookup(key);
if (hex) {
/* Node skips a hex color's sequences when the stream does not
* colorize; later formats are still validated. */
if (!colorize) continue;
snprintf(close, sizeof close, "\x1b[39m");
} else if (!style) {
if (!scr_exc_pending()) util_style_invalid(key, false);
scr_str_release(scr_jb_finish(&opens)); scr_str_release(scr_jb_finish(&closes)); goto done;
} else {
snprintf(open, sizeof open, "\x1b[%um", style->open);
snprintf(close, sizeof close, "\x1b[%um", style->close);
}
scr_jb_puts(&opens, open);
size_t n = strlen(close);
scr_jb_puts(&closes, close);
/* Prepend each closing sequence so the outer formats close last. */
memmove(closes.data + n, closes.data, closes.len - n);
memcpy(closes.data, close, n);
ScrStr *next = util_style_replace(processed, close, open,
style != NULL && (style->open == 1 || style->open == 2));
scr_str_release(processed); processed = next;
}
if (colorize) {
scr_jb_put_str(&opens, processed);
util_style_putn(&opens, closes.data, closes.len);
result = scr_jb_finish(&opens);
} else { scr_str_release(scr_jb_finish(&opens)); result = scr_str_retain(text->v.str); }
scr_str_release(scr_jb_finish(&closes));
done:
scr_dyn_release(validate); scr_dyn_release(view); scr_dyn_release(stream);
scr_str_release(processed);
return result;
}