Repository: jcrist/msgspec (language: python)

Issue:
Incorrect decoding of 65-bit integer
### Description

There seems to be a case where 65 bit integers are improperly decoded. I discovered this in a large dynamically generated json file at seemingly random times using the msgspec lib until I tracked down and isolated an example (below). Both larger and smaller encoded integers do not seem to have any issue. I expected the value of `19933688932870350000` to be consistently encoded and decoded as such, but instead received the number `1482881526185800828` as the output. As I was trying to figure out what was going on, I tried using the standard library `json.loads()` and got the expected value out. When examining the binary representation of the number, I noticed the incorrect value was equivalent to the correct one, besides the leading 4 bits:

| | Decimal | Binary value | Bits |
| - | -------- | ------------ | ---- |
| Input | 19933688932870350000 | 10001010010010100010000001110010000110100010010110011010001111100 | 65 |
| Output (incorrect)  | 1482881526185800828 |  1010010010100010000001110010000110100010010110011010001111100 | 61 |


Here is a minimally reproducible example:
```python
import json
import msgspec

# incorrect value
encoded_json = msgspec.json.encode({"test_value": 19933688932870350000})
decoded_json =  msgspec.json.decode(encoded_json)
print(decoded_json)

# correct value
comp_encoded_json = msgspec.json.encode({"test_value": 19933688932870350000})
comp_decoded_json = json.loads(comp_encoded_json)
print(comp_decoded_json)
```


Discussion / hints:
I suppose that this piece of code does not detect the overflow correctly:
https://github.com/jcrist/msgspec/blob/2c37da090b2c5fcb2ecca9ae00274c67fabb85cf/msgspec/_core.c#L11902-L11909

Apparently the problem appears with the numbers starting from `(1 << 64) + (10 ** 18) + 1`, for which the above code will have `mantissa = ONE_E18 + 1`:
```
>>> test_value = (1 << 64) + (10 ** 18)
>>> msgspec.json.decode(json.dumps(test_value)), test_value
(19446744073709551616, 19446744073709551616)

>>> test_value = (1 << 64) + (10 ** 18) + 1
>>> msgspec.json.decode(json.dumps(test_value)), test_value
(1000000000000000001, 19446744073709551617)
```
and continues up to `19999999999999999999` (then at `20000000000000000000` the `*integer_start != '1'` part triggers, and the code starts behaving correctly again):
```
>>> test_value = 19999999999999999999
>>> msgspec.json.decode(json.dumps(test_value)), test_value
(1553255926290448383, 19999999999999999999)

>>> test_value = 19999999999999999999 + 1
>>> msgspec.json.decode(json.dumps(test_value)), test_value
(20000000000000000000, 20000000000000000000)
```

Fix patch:
diff --git a/msgspec/_core.c b/msgspec/_core.c
index caf60279..d07ef87d 100644
--- a/msgspec/_core.c
+++ b/msgspec/_core.c
@@ -11649,6 +11649,7 @@ ms_post_decode_float(
  *************************************************************************/
 
 #define ONE_E18 1000000000000000000ULL
+#define ONE_E19_MINUS_ONE 9999999999999999999ULL
 
 static MS_NOINLINE PyObject *
 parse_number_fallback(
@@ -11948,7 +11949,7 @@ parse_number_inline(
                 (is_float) ||
                 ((integer_end - integer_start) != 20) ||
                 (*integer_start != '1') ||
-                (mantissa <= ONE_E18)
+                (mantissa <= ONE_E19_MINUS_ONE)
             )
         ) {
             /* We overflowed. Redo parsing, truncating at 19 digits */


Test patch:
diff --git a/tests/test_json.py b/tests/test_json.py
index 2eee346e..d3a88f24 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -1113,17 +1113,30 @@ def test_decode_int_boundaries(self, x):
         assert isinstance(x2, int)
         assert x2 == x
 
-    @pytest.mark.parametrize("x", [-(2**63) - 1, 2**64])
-    def test_decode_big_int_as_any(self, x):
-        s = str(x).encode()
-        x2 = msgspec.json.decode(s)
-        assert isinstance(x2, int)
-        assert x2 == x
-
-    @pytest.mark.parametrize("x", [-(2**63) - 1, 2**64])
-    def test_decode_big_int_as_int(self, x):
+    @pytest.mark.parametrize(
+        "x",
+        [
+            -(2**63) - 1,
+            2**64,
+            2**64 + 10**18,
+            2**64 + 10**18 + 1,
+            19999999999999999999,
+            20000000000000000000,
+            2**64 + 10**19,  # mantissa overflows to 20 digits
+            2**64 + 10**19 - 1,
+            -(2**64),
+            -(2**64) + 10**18,
+            -(2**64) + 10**18 + 1,
+            -19999999999999999999,
+            -20000000000000000000,
+            -(2**64 + 10**19),
+            -(2**64 + 10**19 - 1),
+        ],
+    )
+    @pytest.mark.parametrize("type", [Any, int])
+    def test_decode_big_int(self, x, type):
         s = str(x).encode()
-        x2 = msgspec.json.decode(s, type=int)
+        x2 = msgspec.json.decode(s, type=type)
         assert isinstance(x2, int)
         assert x2 == x