From f079fa82b2121b428c4d57cc0c51817efb8fbbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20L=C3=B3pez=20Guevara?= Date: Wed, 11 Feb 2026 19:50:16 -0300 Subject: [PATCH] fix(std-io): make uint128 decimal formatting safe (#2924) * fix(std-io): make uint128 decimal formatting safe and add all-base numeric coverage --------- Co-authored-by: Christoffer Lerno --- lib/std/io/formatter.c3 | 31 +++-- lib/std/io/formatter_private.c3 | 35 +++-- releasenotes.md | 1 + test/unit/stdlib/core/formatter.c3 | 29 +++++ test/unit/stdlib/io/printf_numbers.c3 | 181 ++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 test/unit/stdlib/io/printf_numbers.c3 diff --git a/lib/std/io/formatter.c3 b/lib/std/io/formatter.c3 index 06a6fbd5c..7571133c1 100644 --- a/lib/std/io/formatter.c3 +++ b/lib/std/io/formatter.c3 @@ -13,8 +13,7 @@ interface Printable fn usz? to_format(Formatter* formatter) @optional; } -faultdef BUFFER_EXCEEDED, INTERNAL_BUFFER_EXCEEDED, INVALID_FORMAT, - NOT_ENOUGH_ARGUMENTS, INVALID_ARGUMENT; +faultdef BUFFER_EXCEEDED, INTERNAL_BUFFER_EXCEEDED, INVALID_FORMAT, NOT_ENOUGH_ARGUMENTS, INVALID_ARGUMENT; alias OutputFn = fn void?(void* buffer, char c); alias FloatType = double; @@ -34,14 +33,14 @@ macro bool is_struct_with_default_print($Type) *> macro usz? struct_to_format(value, Formatter* f, bool $force_dump) { - var $Type = $typeof(value); - usz total = f.print("{ ")!; - $foreach $i, $member : $Type.membersof: - $if $i > 0: - total += f.print(", ")!; - $endif - $if $member.nameof != "": - total += f.printf("%s: ", $member.nameof)!; + var $Type = $typeof(value); + usz total = f.print("{ ")!; + $foreach $i, $member : $Type.membersof: + $if $i > 0: + total += f.print(", ")!; + $endif + $if $member.nameof != "": + total += f.printf("%s: ", $member.nameof)!; $endif $if ($force_dump &&& ($member.typeid.kindof == STRUCT || $member.typeid.kindof == BITSTRUCT)) ||| is_struct_with_default_print($member.typeid): @@ -351,12 +350,12 @@ macro usz? @wrap_bad(Formatter* f, #action) switch (err) { case BUFFER_EXCEEDED: - case INTERNAL_BUFFER_EXCEEDED: - return f.first_err(err)~; - default: + case INTERNAL_BUFFER_EXCEEDED: + return f.first_err(err)~; + default: err = f.first_err(INVALID_ARGUMENT); - f.out_substr("")!; - return err~; + f.out_substr("")!; + return err~; } } return len; @@ -489,7 +488,7 @@ fn usz? Formatter.vprintf(&self, String format, any[] anys) self.flags.uppercase = true; nextcase; case 'h': - char[] out @noinit; + char[] out @noinit; switch (current.type) { case char[]: diff --git a/lib/std/io/formatter_private.c3 b/lib/std/io/formatter_private.c3 index 1bf83d993..82cae987a 100644 --- a/lib/std/io/formatter_private.c3 +++ b/lib/std/io/formatter_private.c3 @@ -541,7 +541,6 @@ const char[201] DIGIT_PAIRS @private = "08182838485868788898" "09192939495969798999"; - fn usz? Formatter.ntoa(&self, uint128 value, bool negative, uint base) @private { char[PRINTF_NTOA_BUFFER_SIZE] buf @noinit; @@ -574,10 +573,10 @@ fn usz? Formatter.ntoa(&self, uint128 value, bool negative, uint base) @private while (value >= 10) { if (len + 1 >= PRINTF_NTOA_BUFFER_SIZE) return INTERNAL_BUFFER_EXCEEDED~; - char digit = (char)(value % 100); + char digit = (char)(value % 100U); buf[len:2] = DIGIT_PAIRS[2 * digit:2]; len += 2; - value /= 100; + value /= 100U; } if (value > 0) { @@ -593,16 +592,16 @@ fn usz? Formatter.ntoa(&self, uint128 value, bool negative, uint base) @private value >>= 4; } while (value); - case 8: - do - { - if (len >= PRINTF_NTOA_BUFFER_SIZE) return INTERNAL_BUFFER_EXCEEDED~; - buf[len++] = '0' + (char)value & 0x7; - value >>= 3; - } - while (value); - default: - unreachable(); + case 8: + do + { + if (len >= PRINTF_NTOA_BUFFER_SIZE) return INTERNAL_BUFFER_EXCEEDED~; + buf[len++] = '0' + (char)value & 0x7; + value >>= 3; + } + while (value); + default: + unreachable(); } } return self.ntoa_format((String)buf[:PRINTF_NTOA_BUFFER_SIZE], len, negative, base); @@ -684,9 +683,9 @@ fn usz? Formatter.out_char(&self, any arg) @private usz len = 1; // pre padding if (!self.flags.left) - { - len += self.pad(' ', self.width, len)!; - } + { + len += self.pad(' ', self.width, len)!; + } // char output Char32 c = types::any_to_int(arg, uint) ?? 0xFFFD; @@ -708,8 +707,8 @@ fn usz? Formatter.out_char(&self, any arg) @private self.out((char)(0x80 | (c & 0x3F)))!; } if (self.flags.left) - { - len += self.pad(' ', self.width, len)!; + { + len += self.pad(' ', self.width, len)!; } return len; } diff --git a/releasenotes.md b/releasenotes.md index 3a8791e15..1bf0b33e2 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -32,6 +32,7 @@ - Reallocating overaligned memory with the LibcAllocator was unsafe. - Using [] or .foo on $$ functions would not raise error but instead crash - Improved underlining errors/warnings when unicode is used. #2887 +- Fix std::io::Formatter integer issue for large uint128 decimal values. ## 0.7.9 Change list diff --git a/test/unit/stdlib/core/formatter.c3 b/test/unit/stdlib/core/formatter.c3 index e143f4041..1f8a35451 100644 --- a/test/unit/stdlib/core/formatter.c3 +++ b/test/unit/stdlib/core/formatter.c3 @@ -8,6 +8,13 @@ fn usz? Foo.to_format(&self, Formatter *f) @dynamic return f.printf("Foo[%d]", self.a); } +enum FormatterEnum +{ + ABC, + BCD, + EFG, +} + fn void test_ref() @test { Foo* f = &&(Foo){ 8 }; @@ -42,3 +49,25 @@ fn void test_ref() @test defer free(s3); assert(s3 == "1.235e+04 8.765e-04 1.235e+06 12.35 12.30 1.000 3.141592653589793"); } + +fn void test_uint128_decimal_formatting() @test +{ + uint128 v = 0xFFEEDDCC_BBAA9988_77665544_33221100; + String s = string::format(mem, "%d", v); + defer free(s); + test::eq(s, "340193404210632335760508365704335069440"); + + String s2 = string::format(mem, "%d", uint128.max); + defer free(s2); + test::eq(s2, "340282366920938463463374607431768211455"); +} + +fn void test_mixed_format_with_uint128() @test +{ + int a = 1234; + uint128 b = 0xFFEEDDCC_BBAA9988_77665544_33221100; + FormatterEnum e = BCD; + String s = string::format(mem, "a: %s, b: %d, foo: %s", a, b, e); + defer free(s); + test::eq(s, "a: 1234, b: 340193404210632335760508365704335069440, foo: BCD"); +} diff --git a/test/unit/stdlib/io/printf_numbers.c3 b/test/unit/stdlib/io/printf_numbers.c3 new file mode 100644 index 000000000..75bcd6037 --- /dev/null +++ b/test/unit/stdlib/io/printf_numbers.c3 @@ -0,0 +1,181 @@ +module std::io; + +macro void expect_d($value, $expected, $label) +{ + String s = string::format(mem, "%d", $value); + defer free(s); + assert(s == $expected, "got '%s'; want '%s' for %s", s, $expected, $label); +} + +macro void expect_x($value, $expected, $label) +{ + String s = string::format(mem, "%x", $value); + defer free(s); + assert(s == $expected, "got '%s'; want '%s' for %s", s, $expected, $label); +} + +macro void expect_o($value, $expected, $label) +{ + String s = string::format(mem, "%o", $value); + defer free(s); + assert(s == $expected, "got '%s'; want '%s' for %s", s, $expected, $label); +} + +macro void expect_b($value, $expected, $label) +{ + String s = string::format(mem, "%b", $value); + defer free(s); + assert(s == $expected, "got '%s'; want '%s' for %s", s, $expected, $label); +} + +macro void expect_all_bases($value, $d, $x, $o, $b, $label) +{ + expect_d($value, $d, $label); + expect_x($value, $x, $label); + expect_o($value, $o, $label); + expect_b($value, $b, $label); +} + +fn void printf_numbers_all_bases_per_type() @test +{ + expect_all_bases((ichar)-42, "-42", "-2a", "-52", "-101010", "ichar"); + expect_all_bases((short)-42, "-42", "-2a", "-52", "-101010", "short"); + expect_all_bases((int)-42, "-42", "-2a", "-52", "-101010", "int"); + expect_all_bases((long)-42, "-42", "-2a", "-52", "-101010", "long"); + expect_all_bases((int128)-42, "-42", "-2a", "-52", "-101010", "int128"); + expect_all_bases((iptr)-42, "-42", "-2a", "-52", "-101010", "iptr"); + expect_all_bases((isz)-42, "-42", "-2a", "-52", "-101010", "isz"); + + expect_all_bases((char)42, "42", "2a", "52", "101010", "char"); + expect_all_bases((ushort)42, "42", "2a", "52", "101010", "ushort"); + expect_all_bases((uint)42, "42", "2a", "52", "101010", "uint"); + expect_all_bases((ulong)42, "42", "2a", "52", "101010", "ulong"); + expect_all_bases((uint128)42, "42", "2a", "52", "101010", "uint128"); + expect_all_bases((uptr)42, "42", "2a", "52", "101010", "uptr"); + expect_all_bases((usz)42, "42", "2a", "52", "101010", "usz"); +} + +fn void printf_numbers_decimal_ranges() @test +{ + expect_d((ichar)-128, "-128", "ichar min"); + expect_d((ichar)127, "127", "ichar max"); + + expect_d((short)-32768, "-32768", "short min"); + expect_d((short)32767, "32767", "short max"); + + expect_d(int.min, "-2147483648", "int min"); + expect_d(int.max, "2147483647", "int max"); + + expect_d(long.min, "-9223372036854775808", "long min"); + expect_d(long.max, "9223372036854775807", "long max"); + + expect_d(int128.min, "-170141183460469231731687303715884105728", "int128 min"); + expect_d(int128.max, "170141183460469231731687303715884105727", "int128 max"); + + expect_d((char)0, "0", "char min"); + expect_d((char)255, "255", "char max"); + + expect_d((ushort)0, "0", "ushort min"); + expect_d(ushort.max, "65535", "ushort max"); + + expect_d((uint)0, "0", "uint min"); + expect_d(uint.max, "4294967295", "uint max"); + + expect_d((ulong)0, "0", "ulong min"); + expect_d(ulong.max, "18446744073709551615", "ulong max"); + + expect_d((uint128)0, "0", "uint128 min"); + expect_d(uint128.max, "340282366920938463463374607431768211455", "uint128 max"); + + if (bitsizeof(iptr) == 64) + { + expect_d(iptr.min, "-9223372036854775808", "iptr min"); + expect_d(iptr.max, "9223372036854775807", "iptr max"); + } + else + { + expect_d(iptr.min, "-2147483648", "iptr min"); + expect_d(iptr.max, "2147483647", "iptr max"); + } + + if (bitsizeof(isz) == 64) + { + expect_d(isz.min, "-9223372036854775808", "isz min"); + expect_d(isz.max, "9223372036854775807", "isz max"); + } + else + { + expect_d(isz.min, "-2147483648", "isz min"); + expect_d(isz.max, "2147483647", "isz max"); + } + + if (bitsizeof(uptr) == 64) + { + expect_d((uptr)0, "0", "uptr min"); + expect_d(uptr.max, "18446744073709551615", "uptr max"); + } + else + { + expect_d((uptr)0, "0", "uptr min"); + expect_d(uptr.max, "4294967295", "uptr max"); + } + + if (bitsizeof(usz) == 64) + { + expect_d((usz)0, "0", "usz min"); + expect_d(usz.max, "18446744073709551615", "usz max"); + } + else + { + expect_d((usz)0, "0", "usz min"); + expect_d(usz.max, "4294967295", "usz max"); + } + + expect_d(ulong.max, "18446744073709551615", "ulong max fast decimal path"); + expect_d((uint128)ulong.max + 1, "18446744073709551616", "uint128 safe decimal path"); +} + +fn void printf_numbers_radix_formats() @test +{ + expect_x((char)255, "ff", "char max hex"); + expect_o((char)255, "377", "char max oct"); + expect_b((char)255, "11111111", "char max bin"); + + expect_x((ushort)65535, "ffff", "ushort max hex"); + expect_o((ushort)65535, "177777", "ushort max oct"); + expect_b((ushort)65535, "1111111111111111", "ushort max bin"); + + expect_x(uint.max, "ffffffff", "uint max hex"); + expect_o(uint.max, "37777777777", "uint max oct"); + expect_b(uint.max, "11111111111111111111111111111111", "uint max bin"); + + expect_x(ulong.max, "ffffffffffffffff", "ulong max hex"); + expect_o(ulong.max, "1777777777777777777777", "ulong max oct"); + + expect_x(uint128.max, "ffffffffffffffffffffffffffffffff", "uint128 max hex"); + expect_o((uint128)0xFFEEDDCC_BBAA9988_77665544_33221100, + "3777355671456725231420735462524206310410400", + "uint128 sample oct"); + + expect_x((uint128)0xFFEEDDCC_BBAA9988_77665544_33221100, + "ffeeddccbbaa99887766554433221100", + "uint128 sample hex"); + + if (bitsizeof(uptr) == 64) + { + expect_x(uptr.max, "ffffffffffffffff", "uptr max hex"); + } + else + { + expect_x(uptr.max, "ffffffff", "uptr max hex"); + } + + if (bitsizeof(usz) == 64) + { + expect_x(usz.max, "ffffffffffffffff", "usz max hex"); + } + else + { + expect_x(usz.max, "ffffffff", "usz max hex"); + } +}