diff --git a/tests/tester.cc b/tests/tester.cc index fe5981f..845a613 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1249,3 +1249,37 @@ TEST_CASE("empty-images-not-written", "[issue-495]") { // WriteImageData should be invoked for both images CHECK(counter == 2); } + +#ifdef TINYGLTF_USE_CUSTOM_JSON +/* Regression test: in float32_mode, integer-only tokens with more than 9 + * digits must still be parsed as integers (is_int == 1), not floats. + * Previously, max_sig=9 was applied to the integer part too, causing excess + * digits to bump exp10, which broke the exp10==0 guard in the integer + * fast-path and mis-classified the value as a float. */ +TEST_CASE("cj-float32-long-integer", "[customjson]") { + // Values chosen to cover exactly-at, just-over, and near int64 boundaries. + struct { + const char *text; + int64_t expected; + } cases[] = { + { "1234567890", 1234567890LL }, /* 10 digits */ + { "12345678901", 12345678901LL }, /* 11 digits */ + { "1000000000000", 1000000000000LL }, /* 13 digits */ + { "9223372036854775807", INT64_MAX }, /* max int64 (19 digits) */ + { "-1234567890", -1234567890LL }, /* negative 10 digits */ + { "-9223372036854775808", INT64_MIN }, /* min int64 */ + }; + + for (auto &tc : cases) { + int is_int = 0; + int64_t ival = 0; + double dval = 0.0; + const char *end = tc.text + strlen(tc.text); + const char *ret = cj_parse_number(tc.text, end, &is_int, &ival, &dval, /*float32_mode=*/1); + CAPTURE(tc.text); + REQUIRE(ret != nullptr); + CHECK(is_int == 1); + CHECK(ival == tc.expected); + } +} +#endif /* TINYGLTF_USE_CUSTOM_JSON */ diff --git a/tinygltf_json.h b/tinygltf_json.h index 1260543..c33f22f 100644 --- a/tinygltf_json.h +++ b/tinygltf_json.h @@ -422,9 +422,11 @@ static int cj_fast_flt_convert(uint64_t mantissa, int exp10, int neg, float *out * Returns pointer past the last character consumed, or NULL on error. * * float32_mode: when non-zero, floating-point values are parsed at float - * (single) precision — fewer digits are significant, and the result is - * stored as (double)(float)value. This is faster but not JSON-conformant - * for high-precision doubles. + * (single) precision — only 9 significant digits are tracked for the + * fraction part, and the result is stored as (double)(float)value. This + * is faster but not JSON-conformant for high-precision doubles. Integer- + * only tokens (no '.'/'e') are always parsed at full int64 precision + * regardless of this flag. * * Uses Clinger's fast path (no strtod) for ~99% of JSON float values. * Falls back to strtod only for extreme exponents or >19 significant digits. */ @@ -444,8 +446,12 @@ static const char *cj_parse_number(const char *p, const char *end, int mantissa_overflow = 0; /* set if >19 significant digits */ int has_frac = 0, has_exp = 0; - /* Max significant digits we track: 19 for double, 9 for float32 */ - int max_sig = float32_mode ? 9 : 19; + /* Max significant digits we track: + * Integer part: always 19, so integer-only tokens (no '.'/'e') are always + * accumulated fully and can be typed as int64 regardless of float32_mode. + * Fraction part: 9 in float32_mode (single precision), 19 otherwise. */ + int max_sig_int = 19; + int max_sig_frac = float32_mode ? 9 : 19; /* Integer part */ if (*p == '0') { @@ -453,7 +459,7 @@ static const char *cj_parse_number(const char *p, const char *end, } else if ((unsigned)(*p - '1') <= 8u) { while (p < end && (unsigned)(*p - '0') <= 9u) { unsigned d = (unsigned)(*p - '0'); - if (ndigits < max_sig) { + if (ndigits < max_sig_int) { mantissa = mantissa * 10 + d; } else { exp10++; /* excess digit: bump exponent instead */ @@ -474,7 +480,7 @@ static const char *cj_parse_number(const char *p, const char *end, if (p >= end || (unsigned)(*p - '0') > 9u) return NULL; while (p < end && (unsigned)(*p - '0') <= 9u) { unsigned d = (unsigned)(*p - '0'); - if (ndigits < max_sig) { + if (ndigits < max_sig_frac) { mantissa = mantissa * 10 + d; exp10--; }