Compare commits

...

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c9a9b1175a Fix float32_mode integer parsing: preserve int64 precision for integer-only tokens
Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/syoyo/tinygltf/sessions/a77fd614-00f3-49c1-bb4a-0498771cc63b
2026-03-20 21:24:37 +00:00
copilot-swe-agent[bot]
c870bd5fd6 Initial plan 2026-03-20 21:15:36 +00:00
Syoyo Fujita
f7bd377a69 Update tiny_gltf_v3.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-21 05:36:13 +09:00
Syoyo Fujita
5d6984b9fd Update tiny_gltf_v3.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-21 05:35:53 +09:00
Syoyo Fujita
3331c6cee2 Update tinygltf_json.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-21 05:35:34 +09:00
3 changed files with 84 additions and 20 deletions

View File

@@ -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 */

View File

@@ -1750,7 +1750,25 @@ static int tg3__parse_int(tg3__parse_ctx *ctx, const tg3__json &o,
"Field '%s' must be a number", key);
return 0;
}
*out = it->get<int>();
if (it->is_number_integer()) {
int64_t v = it->get<int64_t>();
if (v < (int64_t)INT32_MIN || v > (int64_t)INT32_MAX) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
TG3_ERR_JSON_TYPE_MISMATCH, parent,
"Field '%s' value %" PRId64 " is out of range for int32", key, v);
return 0;
}
*out = (int32_t)v;
} else {
double d = it->get<double>();
if (d < (double)INT32_MIN || d > (double)INT32_MAX) {
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
TG3_ERR_JSON_TYPE_MISMATCH, parent,
"Field '%s' value %f is out of range for int32", key, d);
return 0;
}
*out = (int32_t)d;
}
return 1;
}
@@ -2315,17 +2333,22 @@ static int tg3__parse_buffer(tg3__parse_ctx *ctx, const tg3__json &o,
/* Load buffer data */
if (ctx->is_binary && buf_idx == 0 && buf->uri.len == 0) {
/* GLB: first buffer uses binary chunk */
if (ctx->bin_data && ctx->bin_size >= byte_length) {
uint8_t *data = (uint8_t *)tg3__arena_alloc(ctx->arena, (size_t)byte_length);
if (!data) {
tg3__error_push(ctx->errors, TG3_SEVERITY_ERROR,
TG3_ERR_OUT_OF_MEMORY, "OOM for buffer data", NULL, -1);
return 0;
}
memcpy(data, ctx->bin_data, (size_t)byte_length);
buf->data.data = data;
buf->data.count = byte_length;
if (!ctx->bin_data || ctx->bin_size < byte_length) {
tg3__error_push(ctx->errors, TG3_SEVERITY_ERROR,
TG3_ERR_BUFFER_SIZE_MISMATCH,
"GLB BIN chunk missing or smaller than buffer.byteLength",
NULL, -1);
return 0;
}
uint8_t *data = (uint8_t *)tg3__arena_alloc(ctx->arena, (size_t)byte_length);
if (!data) {
tg3__error_push(ctx->errors, TG3_SEVERITY_ERROR,
TG3_ERR_OUT_OF_MEMORY, "OOM for buffer data", NULL, -1);
return 0;
}
memcpy(data, ctx->bin_data, (size_t)byte_length);
buf->data.data = data;
buf->data.count = byte_length;
} else if (buf->uri.len > 0) {
if (tg3_is_data_uri(buf->uri.data, buf->uri.len)) {
/* Data URI */

View File

@@ -297,8 +297,9 @@ static const char *cj_scan_str(const char *p, const char *end) {
* FAST NUMBER PARSING (C-style)
*
* Uses Clinger's fast path for float conversion, avoiding strtod() for the
* vast majority of JSON numbers. This is locale-independent and typically
* 4-10x faster than strtod.
* vast majority of JSON numbers. The fast path itself is locale-independent
* and typically 4-10x faster than strtod; however, rare fallback paths may
* still invoke the C library's strtod(), which can be locale-dependent.
*
* Optional float32 mode (CJ_FLOAT32_MODE flag in cj_parse_number):
* Parses floating-point values to float (single) precision and stores
@@ -421,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. */
@@ -443,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') {
@@ -452,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 */
@@ -473,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--;
}