lib/fmt: encode rune args as UTF-8, not a truncated low byte (#66)

writeone and formatraw's rune arm did `buf[0] = r: u8; putbytes(...,1)`,
emitting only the low byte — invalid UTF-8 for any rune > 0x7F, with a
success return. rawlen's rune arm hardcoded 1, desyncing width-padding
for multibyte runes. Route both write arms through utf8.encoderune and
rawlen through utf8.runesz (ref/hare/fmt/print.ha:84). Uses the current
in-tree encoderune(out: []u8, r) caller-buffer signature; report-#168's
signature realignment is a separate item, not folded here.

fmttest gains fprintrune (1/2/3/4-byte runes) + fprintf_rune_width
(multibyte pad).
This commit is contained in:
2026-06-13 10:36:11 +09:00
parent 69e31355ce
commit bccd111a16
2 changed files with 60 additions and 5 deletions

View File

@@ -303,6 +303,46 @@ fn errsource() io.stream = {
match (c) { case void => {}; case let eioe: io.error => abort(); };
};
// ---- fprint: rune args emit full UTF-8, not a truncated byte (#66) -----
// writeone/formatraw's rune arm did `buf[0] = r: u8; putbytes(...,1)`,
// emitting only the low byte (invalid UTF-8 for r > 0x7F). The fix routes
// through utf8.encoderune (ref/hare/fmt/print.ha:84). Edge runes: é (2B),
// € (3B), 😀 (4B), A (1B).
@test fn fprintrune() void = {
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;
let r: (size | io.error) = fmt.fprint(s, 233: rune, 0x20AC: rune,
0x1F600: rune, 65: rune);
match (r) {
case let n: size => { assert(!(n: i32 != 13)); }; // 2+1+3+1+4+1+1
case let eioe: io.error => abort();
};
assert(!(!streq(memio.string(&mem), "é € 😀 A")));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let eioe: io.error => abort(); };
};
// rawlen's rune arm hardcoded 1, desyncing width-padding for a multibyte
// rune. With runesz it counts the encoded length: € (3B) in width 5 pads
// to " €" (2 spaces + 3 bytes = 5).
@test fn fprintf_rune_width() void = {
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;
let r: (size | io.error) = fmt.fprintf(s, "{:5}", 0x20AC: rune);
match (r) {
case let n: size => { assert(!(n: i32 != 5)); };
case let eioe: io.error => abort();
};
assert(!(!streq(memio.string(&mem), " €")));
let c: (void | io.error) = io.close(s);
match (c) { case void => {}; case let eioe: io.error => abort(); };
};
@test fn fprintf_pad_underscore() void = {
let mem: memio.stream = memio.dynamic();
let s: io.stream = &mem.vt;