From 269d3be75643d6647d24cbb41f072908767eab38 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 7 Jun 2026 17:18:57 -0400 Subject: [PATCH] c: get-output-string + write-char honor binary data on string ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pre-existing defects in c/builtins.c made (open-output-string) unusable for binary emit: - bi_get_output_string ran the buffer through make_string_from_cstr, which calls strlen. Any 0x00 in the payload truncated body at that byte. Use port's known str_len directly via make_string. - bi_write_char ignored string-port destinations entirely — it pulled AS_PORT(p)->fp (NULL for string ports), fell back to stdout, and silently routed gate bytes to terminal output instead of the port buffer. Route to port_write_str when target port kind is PORT_STRING, matching bi_write_string's behavior. Surfaced while validating the precise-GC fix against ecdsa's Phase B emit (writes 56-byte op records full of embedded nulls through a string-output port, get-output-string at the end). With these fixes ecdsa's n+1=64 emit drops a 333 MB binary in 42.6 seconds — pre-fix it timed out at 600 s with a 17-byte header-only file (strlen truncated body at byte 1; the visible gate bytes had been escaping to stdout the whole time). Binary roundtrip test (write n bytes alternating x / 0x00 to output-string port, read back via get-output-string): n=10 len=10 ok n=1000 len=1000 ok n=100000 len=100000 ok All upstream tests still pass (88/88 c-test, 4/4 regression, 205/205 functional, zoe across tiers). --- c/builtins.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/c/builtins.c b/c/builtins.c index d81b4c5..4001cdc 100644 --- a/c/builtins.c +++ b/c/builtins.c @@ -1957,10 +1957,13 @@ static Value bi_open_output_string(Value *a, int n, Env *e) { } static Value bi_get_output_string(Value *a, int n, Env *e) { (void)e; CHECK_ARITY("get-output-string", 1); - char *s = port_get_output_string(AS_PORT(a[0])); - Value r = make_string_from_cstr(s); - ul_free(s); - return r; + /* Use port's known str_len, not strlen — emitter writes binary + * (op bytes with embedded 0x00). strlen truncates at first null. */ + ULPort *p = AS_PORT(a[0]); + if (p->kind != PORT_STRING || p->dir != PORT_OUTPUT) { + return make_string("", 0, false); + } + return make_string(p->str_buf, p->str_len, false); } static Value bi_close_port(Value *a, int n, Env *e) { (void)e; CHECK_ARITY("close-port", 1); @@ -2360,6 +2363,14 @@ static Value bi_write_string(Value *a, int n, Env *e) { static Value bi_write_char(Value *a, int n, Env *e) { (void)e; CHECK_MIN_ARITY("write-char", 1); + /* Route to string-port buffer when target is a string port — without + * this, write-char ignored string ports and wrote to stdout, breaking + * binary emit through (open-output-string) accumulators. */ + if (n > 1 && IS_PORT(a[1]) && AS_PORT(a[1])->kind == PORT_STRING) { + char ch = (char)AS_CHAR(a[0]); + port_write_str(AS_PORT(a[1]), &ch, 1); + return VAL_VOID; + } FILE *out = n > 1 && IS_PORT(a[1]) ? AS_PORT(a[1])->fp : stdout; fputc(AS_CHAR(a[0]), out ? out : stdout); fflush(out ? out : stdout);