c: get-output-string + write-char honor binary data on string ports

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).
This commit is contained in:
russell@unturf.com 2026-06-07 17:18:57 -04:00
parent b841b30bc4
commit 269d3be756
No known key found for this signature in database

View file

@ -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);