From 1385a9bbeff11a3bb1d0fdf81ff9163363ff7508 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Tue, 11 Aug 2026 12:05:19 +0900 Subject: [PATCH] Reject out-of-range integers instead of silently truncating parseHook for unsigned integers accumulated into a uint64 and converted with `type(v)(v2)`, and Nim's unsigned conversions wrap modulo with no range check even in debug, so `"256".fromJson(uint8)` returned 0 and huge numbers wrapped silently. The signed path relied on a `try/except` around the conversion, but Nim's range checks are disabled under -d:release, so over-large values wrapped there too (e.g. `"128".fromJson(int8)` == -128). Range-check both paths explicitly against the target type's bounds (the unsigned check also guards the uint64 accumulation itself), raising the existing "Number type to small to contain the number." error. The signed negative branch handles the extra low(T) value (e.g. int8 reaches -128). Fixes #109 --- src/jsony.nim | 21 ++++++++++++++++----- tests/test_numbers.nim | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/jsony.nim b/src/jsony.nim index 4621185..7c57b7c 100644 --- a/src/jsony.nim +++ b/src/jsony.nim @@ -105,7 +105,12 @@ proc parseHook*(s: string, i: var int, v: var SomeUnsignedInt) = v2: uint64 = 0 startI = i while i < s.len and s[i] in {'0'..'9'}: - v2 = v2 * 10 + (s[i].ord - '0'.ord).uint64 + let digit = (s[i].ord - '0'.ord).uint64 + # Reject numbers that do not fit the target type instead of silently + # wrapping modulo (unsigned conversions are unchecked). See issue #109. + if v2 > (uint64(high(type(v))) - digit) div 10: + error("Number type to small to contain the number.", i) + v2 = v2 * 10 + digit inc i if startI == i: error("Number expected.", i) @@ -123,14 +128,20 @@ proc parseHook*(s: string, i: var int, v: var SomeSignedInt) = var v2: uint64 inc i parseHook(s, i, v2) - v = -type(v)(v2) + # The negative range extends one past high(T) (e.g. int8 reaches -128). + # Range-check explicitly; release builds do not raise. See issue #109. + if v2 > uint64(high(type(v))) + 1: + error("Number type to small to contain the number.", i) + elif v2 == uint64(high(type(v))) + 1: + v = low(type(v)) + else: + v = -type(v)(v2) else: var v2: uint64 parseHook(s, i, v2) - try: - v = type(v)(v2) - except: + if v2 > uint64(high(type(v))): error("Number type to small to contain the number.", i) + v = type(v)(v2) proc parseHook*(s: string, i: var int, v: var SomeFloat) = ## Will parse float32 and float64. diff --git a/tests/test_numbers.nim b/tests/test_numbers.nim index 867e79d..6c4dc48 100644 --- a/tests/test_numbers.nim +++ b/tests/test_numbers.nim @@ -51,3 +51,18 @@ block: @["hi", "bye", "maybe"] doAssert """[["hi", "bye"], ["maybe"], []]""".fromJson(seq[seq[string]]) == @[@["hi", "bye"], @["maybe"], @[]] + +block: + # Out-of-range integers must raise instead of silently wrapping/truncating. + # See https://github.com/treeform/jsony/issues/109 + doAssertRaises(JsonError): discard "256".fromJson(uint8) + doAssertRaises(JsonError): discard "99999999999999999999999999".fromJson(uint64) + doAssertRaises(JsonError): discard "128".fromJson(int8) + doAssertRaises(JsonError): discard "-129".fromJson(int8) + doAssertRaises(JsonError): discard "300".fromJson(uint8) + # Boundary values still parse correctly. + doAssert "255".fromJson(uint8) == 255'u8 + doAssert "127".fromJson(int8) == 127'i8 + doAssert "-128".fromJson(int8) == -128'i8 + doAssert "65535".fromJson(uint16) == 65535'u16 + doAssert "18446744073709551615".fromJson(uint64) == 18446744073709551615'u64