From 188d7b257b363aeb82659a5567494d989f660583 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Sat, 9 May 2026 11:50:28 +0900 Subject: [PATCH] Cross-version verifier comparing v3 C parser against v1 ground truth Adds a structured DIGEST block (asset, buffers w/ FNV-1a hash, bufferViews, accessors w/ min/max, mesh primitives w/ sorted attribute maps, nodes w/ normalized TRS+matrix, materials, textures/samplers/images, skins, animations, cameras, scenes) emitted by both loader_example (v1) and tester_v3_c (v3 C, now accepting a file arg). test_runner.py runs both, diffs the digests, and reports counts/digest mismatches with v1 as truth. Also rolls in /simplify follow-ups on top of 7f736d1: a shared tg3__json_number_to_double helper to dedupe inline number coercions, a collapsed fuzz_gltf_v3_c harness using a single tg3_fuzz_run dispatcher, a rewritten max_safe_uint64_real comment explaining the 53-bit mantissa constraint, and a tests/Makefile fix so tester_v3_c is a real prerequisite of `all` (built once via the dedicated rule, not duplicated). Verifier passes 134/134 on the Khronos glTF-Sample-Models/2.0 suite. bufferView.target and image.mime_type/uri are intentionally excluded from the digest: v1 infers target from accessor usage and rewrites image URIs/mime via stb_image, neither of which is a parse-fidelity concern. Co-Authored-By: Claude Opus 4.7 (1M context) --- loader_example.cc | 217 ++++++++++++++++++++++ test_runner.py | 172 ++++++++++++++---- tests/Makefile | 5 +- tests/tester_v3_c.c | 299 ++++++++++++++++++++++++++++++- tests/v3/fuzzer/fuzz_gltf_v3_c.c | 81 ++------- tiny_gltf_v3.c | 18 +- 6 files changed, 678 insertions(+), 114 deletions(-) diff --git a/loader_example.cc b/loader_example.cc index 453aa97..13b69dc 100644 --- a/loader_example.cc +++ b/loader_example.cc @@ -6,7 +6,10 @@ #define STB_IMAGE_WRITE_IMPLEMENTATION #include "tiny_gltf.h" +#include +#include #include +#include #include #include @@ -852,6 +855,206 @@ static void Dump(const tinygltf::Model &model) { } } +/* ===== Digest helpers (used to compare v1 vs v3 parses) ===================== */ + +static uint64_t fnv64(const unsigned char *data, size_t n) { + uint64_t h = 0xcbf29ce484222325ULL; + for (size_t i = 0; i < n; ++i) { h ^= data[i]; h *= 0x100000001b3ULL; } + return h; +} + +static void d_str(const std::string &s) { + putchar('"'); + for (unsigned char c : s) { + if (c == '"' || c == '\\') { putchar('\\'); putchar((char)c); } + else if (c < 0x20 || c >= 0x7f) putchar('?'); + else putchar((char)c); + } + putchar('"'); +} + +static void d_dbl(double v) { printf("%.7g", v); } + +static void d_dbl_arr(const double *v, size_t n) { + putchar('['); + for (size_t i = 0; i < n; ++i) { if (i) putchar(','); d_dbl(v[i]); } + putchar(']'); +} + +static void d_dbl_vec(const std::vector &v) { + d_dbl_arr(v.data(), v.size()); +} + +static void PrintDigest(const tinygltf::Model &m) { + printf("DIGEST_BEGIN\n"); + + printf("asset version="); + d_str(m.asset.version); + printf(" generator="); + d_str(m.asset.generator); + printf("\n"); + + for (size_t i = 0; i < m.buffers.size(); ++i) { + const auto &b = m.buffers[i]; + uint64_t h = b.data.empty() ? 0 : fnv64(b.data.data(), b.data.size()); + printf("buffer %zu byte_length=%llu fnv64=0x%016llx\n", + i, (unsigned long long)b.data.size(), (unsigned long long)h); + } + for (size_t i = 0; i < m.bufferViews.size(); ++i) { + const auto &bv = m.bufferViews[i]; + printf("buffer_view %zu buffer=%d byte_offset=%llu byte_length=%llu byte_stride=%u\n", + i, bv.buffer, (unsigned long long)bv.byteOffset, + (unsigned long long)bv.byteLength, (unsigned)bv.byteStride); + } + for (size_t i = 0; i < m.accessors.size(); ++i) { + const auto &a = m.accessors[i]; + printf("accessor %zu buffer_view=%d byte_offset=%llu component_type=%d count=%llu type=%d normalized=%d min=", + i, a.bufferView, (unsigned long long)a.byteOffset, a.componentType, + (unsigned long long)a.count, a.type, a.normalized ? 1 : 0); + d_dbl_vec(a.minValues); + printf(" max="); + d_dbl_vec(a.maxValues); + printf(" sparse=%d\n", a.sparse.isSparse ? 1 : 0); + } + for (size_t i = 0; i < m.meshes.size(); ++i) { + const auto &me = m.meshes[i]; + printf("mesh %zu primitives_count=%zu weights_count=%zu\n", + i, me.primitives.size(), me.weights.size()); + for (size_t j = 0; j < me.primitives.size(); ++j) { + const auto &p = me.primitives[j]; + printf("prim %zu %zu indices=%d material=%d mode=%d attrs=[", + i, j, p.indices, p.material, p.mode); + // attributes is std::map → already sorted by key + bool first = true; + for (const auto &kv : p.attributes) { + if (!first) putchar(','); + printf("%s:%d", kv.first.c_str(), kv.second); + first = false; + } + printf("] targets_count=%zu\n", p.targets.size()); + } + } + for (size_t i = 0; i < m.nodes.size(); ++i) { + const auto &n = m.nodes[i]; + double t[3] = {0, 0, 0}; + double r[4] = {0, 0, 0, 1}; + double s[3] = {1, 1, 1}; + double mat[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; + int has_matrix = (n.matrix.size() == 16) ? 1 : 0; + if (n.translation.size() == 3) std::copy(n.translation.begin(), n.translation.end(), t); + if (n.rotation.size() == 4) std::copy(n.rotation.begin(), n.rotation.end(), r); + if (n.scale.size() == 3) std::copy(n.scale.begin(), n.scale.end(), s); + if (has_matrix) std::copy(n.matrix.begin(), n.matrix.end(), mat); + printf("node %zu mesh=%d skin=%d camera=%d light=%d children_count=%zu has_matrix=%d t=", + i, n.mesh, n.skin, n.camera, n.light, n.children.size(), has_matrix); + d_dbl_arr(t, 3); + printf(" r="); + d_dbl_arr(r, 4); + printf(" s="); + d_dbl_arr(s, 3); + printf(" matrix="); + d_dbl_arr(mat, 16); + printf(" weights_count=%zu\n", n.weights.size()); + } + for (size_t i = 0; i < m.materials.size(); ++i) { + const auto &mat = m.materials[i]; + double ef[3] = {0, 0, 0}; + double bcf[4] = {1, 1, 1, 1}; + if (mat.emissiveFactor.size() == 3) + std::copy(mat.emissiveFactor.begin(), mat.emissiveFactor.end(), ef); + if (mat.pbrMetallicRoughness.baseColorFactor.size() == 4) + std::copy(mat.pbrMetallicRoughness.baseColorFactor.begin(), + mat.pbrMetallicRoughness.baseColorFactor.end(), bcf); + printf("material %zu alpha_mode=", i); + d_str(mat.alphaMode); + printf(" alpha_cutoff="); + d_dbl(mat.alphaCutoff); + printf(" double_sided=%d emissive=", mat.doubleSided ? 1 : 0); + d_dbl_arr(ef, 3); + printf(" base_color_factor="); + d_dbl_arr(bcf, 4); + printf(" metallic="); + d_dbl(mat.pbrMetallicRoughness.metallicFactor); + printf(" roughness="); + d_dbl(mat.pbrMetallicRoughness.roughnessFactor); + printf(" base_color_tex=%d normal_tex=%d occlusion_tex=%d emissive_tex=%d\n", + mat.pbrMetallicRoughness.baseColorTexture.index, + mat.normalTexture.index, + mat.occlusionTexture.index, + mat.emissiveTexture.index); + } + for (size_t i = 0; i < m.textures.size(); ++i) { + const auto &t = m.textures[i]; + printf("texture %zu source=%d sampler=%d\n", i, t.source, t.sampler); + } + for (size_t i = 0; i < m.samplers.size(); ++i) { + const auto &s = m.samplers[i]; + printf("sampler %zu min_filter=%d mag_filter=%d wrap_s=%d wrap_t=%d\n", + i, s.minFilter, s.magFilter, s.wrapS, s.wrapT); + } + for (size_t i = 0; i < m.images.size(); ++i) { + const auto &im = m.images[i]; + /* mime_type and uri normalization differ between v1/v3 (data URIs, + extension inference); buffer_view reference is the parse-fidelity bit. */ + printf("image %zu buffer_view=%d\n", i, im.bufferView); + } + for (size_t i = 0; i < m.skins.size(); ++i) { + const auto &s = m.skins[i]; + printf("skin %zu inverse_bind_matrices=%d skeleton=%d joints_count=%zu\n", + i, s.inverseBindMatrices, s.skeleton, s.joints.size()); + } + for (size_t i = 0; i < m.animations.size(); ++i) { + const auto &a = m.animations[i]; + printf("animation %zu channels_count=%zu samplers_count=%zu\n", + i, a.channels.size(), a.samplers.size()); + for (size_t j = 0; j < a.channels.size(); ++j) { + const auto &c = a.channels[j]; + printf("chan %zu %zu sampler=%d target_node=%d target_path=", i, j, + c.sampler, c.target_node); + d_str(c.target_path); + printf("\n"); + } + for (size_t j = 0; j < a.samplers.size(); ++j) { + const auto &as = a.samplers[j]; + printf("samp %zu %zu input=%d output=%d interpolation=", i, j, + as.input, as.output); + d_str(as.interpolation); + printf("\n"); + } + } + for (size_t i = 0; i < m.cameras.size(); ++i) { + const auto &c = m.cameras[i]; + bool is_persp = (c.type == "perspective"); + printf("camera %zu type=", i); + d_str(c.type); + if (is_persp) { + printf(" yfov="); + d_dbl(c.perspective.yfov); + printf(" znear="); + d_dbl(c.perspective.znear); + printf(" zfar="); + d_dbl(c.perspective.zfar); + printf(" aspect="); + d_dbl(c.perspective.aspectRatio); + } else { + printf(" xmag="); + d_dbl(c.orthographic.xmag); + printf(" ymag="); + d_dbl(c.orthographic.ymag); + printf(" znear="); + d_dbl(c.orthographic.znear); + printf(" zfar="); + d_dbl(c.orthographic.zfar); + } + printf("\n"); + } + for (size_t i = 0; i < m.scenes.size(); ++i) { + const auto &s = m.scenes[i]; + printf("scene %zu nodes_count=%zu\n", i, s.nodes.size()); + } + printf("DIGEST_END\n"); +} + int main(int argc, char **argv) { if (argc < 2) { printf("Needs input.gltf\n"); @@ -900,6 +1103,20 @@ int main(int argc, char **argv) { return -1; } + printf("COUNTS" + " accessors=%zu animations=%zu buffers=%zu bufferViews=%zu" + " cameras=%zu images=%zu materials=%zu meshes=%zu nodes=%zu" + " samplers=%zu scenes=%zu skins=%zu textures=%zu lights=%zu\n", + model.accessors.size(), model.animations.size(), + model.buffers.size(), model.bufferViews.size(), + model.cameras.size(), model.images.size(), + model.materials.size(), model.meshes.size(), + model.nodes.size(), model.samplers.size(), + model.scenes.size(), model.skins.size(), + model.textures.size(), model.lights.size()); + + PrintDigest(model); + Dump(model); return 0; diff --git a/test_runner.py b/test_runner.py index deb1a73..04911d2 100644 --- a/test_runner.py +++ b/test_runner.py @@ -2,63 +2,167 @@ import glob import os +import re import subprocess +import sys -## Simple test runner. +## Cross-version verifier: parses each sample model with the mature v1 +## (loader_example) and the new v3 C tester, then compares both the COUNTS +## summary line and the structured DIGEST block both binaries emit. +## v1 is the ground truth. # -- config ----------------------- -# Absolute path pointing to your cloned git repo of https://github.com/KhronosGroup/glTF-Sample-Models -sample_model_dir = "/home/syoyo/work/glTF-Sample-Models" +sample_model_dir = "/mnt/nfs/syoyo/glTF-Sample-Models" base_model_dir = os.path.join(sample_model_dir, "2.0") -# Include `glTF-Draco` when you build `loader_example` with draco support. -kinds = [ "glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"] +v1_bin = "./loader_example" +v3_bin = "./tests/tester_v3_c" + +kinds = ["glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"] # --------------------------------- -failed = [] -success = [] +COUNTS_RE = re.compile(r"^COUNTS\s+(.*)$", re.MULTILINE) +DIGEST_RE = re.compile(r"^DIGEST_BEGIN\n(.*?)^DIGEST_END$", re.MULTILINE | re.DOTALL) -def run(filename): +def parse_counts(output): + m = COUNTS_RE.search(output) + if not m: + return None + counts = {} + for tok in m.group(1).split(): + if "=" not in tok: + continue + k, v = tok.split("=", 1) + counts[k] = int(v) + return counts + + +def parse_digest(output): + m = DIGEST_RE.search(output) + if not m: + return None + return [line for line in m.group(1).splitlines() if line] + + +def run_binary(binary, filename): + p = subprocess.Popen( + [binary, filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + out, err = p.communicate() + return p.returncode, out.decode("utf-8", "replace"), err.decode("utf-8", "replace") + + +def diff_digests(v1, v3, max_lines=20): + """Return a short summary of differences between two digest line lists.""" + diffs = [] + n = max(len(v1), len(v3)) + for i in range(n): + a = v1[i] if i < len(v1) else "" + b = v3[i] if i < len(v3) else "" + if a != b: + diffs.append(" v1[{0}]: {1}".format(i, a)) + diffs.append(" v3[{0}]: {1}".format(i, b)) + if len(diffs) >= max_lines * 2: + diffs.append(" ... (truncated)") + break + return diffs + + +parse_failed = [] # v3 returned non-zero or no COUNTS/DIGEST +v1_skipped = [] # v1 returned non-zero or no COUNTS/DIGEST +counts_diff = [] # counts disagree +digest_diff = [] # digest disagrees +ok = [] + + +def verify(filename): print("Testing: " + filename) - cmd = ["./loader_example", filename] - try: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - (stdout, stderr) = p.communicate() - except: - print("Failed to execute: ", cmd) - raise - if p.returncode != 0: - failed.append(filename) - print(stdout) - print(stderr) - else: - success.append(filename) + rc1, out1, err1 = run_binary(v1_bin, filename) + c1 = parse_counts(out1) if rc1 == 0 else None + d1 = parse_digest(out1) if rc1 == 0 else None + if c1 is None or d1 is None: + v1_skipped.append(filename) + print(" v1 ground truth unavailable (rc={0}); skipping".format(rc1)) + return + + rc3, out3, err3 = run_binary(v3_bin, filename) + c3 = parse_counts(out3) if rc3 == 0 else None + d3 = parse_digest(out3) if rc3 == 0 else None + if c3 is None or d3 is None: + parse_failed.append((filename, rc3, err3.strip())) + print(" v3 FAILED (rc={0}): {1}".format(rc3, err3.strip()[:200])) + return + + cdiffs = [] + for k in sorted(set(c1) | set(c3)): + if c1.get(k) != c3.get(k): + cdiffs.append((k, c1.get(k), c3.get(k))) + if cdiffs: + counts_diff.append((filename, cdiffs)) + print(" COUNTS MISMATCH:") + for k, a, b in cdiffs: + print(" {0}: v1={1} v3={2}".format(k, a, b)) + return + + if d1 != d3: + diffs = diff_digests(d1, d3) + digest_diff.append((filename, diffs)) + print(" DIGEST MISMATCH ({0} v1 lines, {1} v3 lines):".format(len(d1), len(d3))) + for line in diffs[:8]: + print(line) + return + + ok.append(filename) def test(): - - for d in os.listdir(base_model_dir): + for d in sorted(os.listdir(base_model_dir)): p = os.path.join(base_model_dir, d) - if os.path.isdir(p): - for k in kinds: - targetDir = os.path.join(p, k) - g = glob.glob(targetDir + "/*.gltf") + glob.glob(targetDir + "/*.glb") - for gltf in g: - run(gltf) + if not os.path.isdir(p): + continue + for k in kinds: + targetDir = os.path.join(p, k) + g = sorted( + glob.glob(targetDir + "/*.gltf") + + glob.glob(targetDir + "/*.glb") + ) + for gltf in g: + verify(gltf) def main(): + if not os.path.exists(v1_bin): + sys.exit("error: v1 binary not found at {0}".format(v1_bin)) + if not os.path.exists(v3_bin): + sys.exit("error: v3 binary not found at {0}".format(v3_bin)) test() - print("Success : {0}".format(len(success))) - print("Failed : {0}".format(len(failed))) + print("") + print("=== Summary ===") + print("OK : {0}".format(len(ok))) + print("Counts diff : {0}".format(len(counts_diff))) + print("Digest diff : {0}".format(len(digest_diff))) + print("v3 failed : {0}".format(len(parse_failed))) + print("v1 skipped : {0}".format(len(v1_skipped))) - for fail in failed: - print("FAIL: " + fail) + for f, diffs in counts_diff: + print("COUNTS DIFF: " + f) + for k, a, b in diffs: + print(" {0}: v1={1} v3={2}".format(k, a, b)) + for f, diffs in digest_diff: + print("DIGEST DIFF: " + f) + for line in diffs: + print(line) + for f, rc, err in parse_failed: + print("V3 FAIL: {0} (rc={1}) {2}".format(f, rc, err[:200])) -if __name__ == '__main__': + if counts_diff or digest_diff or parse_failed: + sys.exit(1) + + +if __name__ == "__main__": main() diff --git a/tests/Makefile b/tests/Makefile index 1d5c036..20c4669 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,11 +1,10 @@ # Use this for strict compilation check(will work on clang 3.8+) #EXTRA_CXXFLAGS := -fsanitize=address -Wall -Werror -Weverything -Wno-c++11-long-long -DTINYGLTF_APPLY_CLANG_WEVERYTHING -all: ../tiny_gltf.h +all: ../tiny_gltf.h tester_v3_c clang++ -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester tester.cc clang++ -DTINYGLTF_NOEXCEPTION -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_noexcept tester.cc clang++ -DTINYGLTF_USE_CUSTOM_JSON -I../ $(EXTRA_CXXFLAGS) -std=c++11 -g -O0 -o tester_intensive_customjson tester_intensive_customjson.cc - clang -I../ -std=c11 -g -O0 -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c tester_v3_c: tester_v3_c.c ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h - clang -I../ -std=c11 -g -O0 -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c + clang -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c diff --git a/tests/tester_v3_c.c b/tests/tester_v3_c.c index 6057f49..7cb3efc 100644 --- a/tests/tester_v3_c.c +++ b/tests/tester_v3_c.c @@ -2,8 +2,218 @@ #include #include +#include #include +/* ===== Digest helpers (used to compare v1 vs v3 parses) ===================== */ + +static uint64_t fnv64(const uint8_t *data, uint64_t n) { + uint64_t h = 0xcbf29ce484222325ULL; + uint64_t i; + for (i = 0; i < n; ++i) { h ^= data[i]; h *= 0x100000001b3ULL; } + return h; +} + +static void d_str(const tg3_str *s) { + uint32_t i; + putchar('"'); + if (s && s->data) { + for (i = 0; i < s->len; ++i) { + unsigned char c = (unsigned char)s->data[i]; + if (c == '"' || c == '\\') { putchar('\\'); putchar((char)c); } + else if (c < 0x20 || c >= 0x7f) putchar('?'); + else putchar((char)c); + } + } + putchar('"'); +} + +static void d_dbl(double v) { printf("%.7g", v); } + +static void d_dbl_arr(const double *v, uint32_t n) { + uint32_t i; + putchar('['); + for (i = 0; i < n; ++i) { if (i) putchar(','); d_dbl(v[i]); } + putchar(']'); +} + +static int cmp_str_int_pair(const void *a, const void *b) { + const tg3_str_int_pair *pa = (const tg3_str_int_pair *)a; + const tg3_str_int_pair *pb = (const tg3_str_int_pair *)b; + uint32_t la = pa->key.len, lb = pb->key.len; + uint32_t m = la < lb ? la : lb; + int r = memcmp(pa->key.data, pb->key.data, m); + if (r) return r; + return (la < lb) ? -1 : (la > lb ? 1 : 0); +} + +static void d_attrs(const tg3_str_int_pair *attrs, uint32_t n) { + tg3_str_int_pair *sorted; + uint32_t i; + if (n == 0) { fputs("[]", stdout); return; } + sorted = (tg3_str_int_pair *)malloc(n * sizeof(*sorted)); + memcpy(sorted, attrs, n * sizeof(*sorted)); + qsort(sorted, n, sizeof(*sorted), cmp_str_int_pair); + putchar('['); + for (i = 0; i < n; ++i) { + if (i) putchar(','); + printf("%.*s:%d", (int)sorted[i].key.len, sorted[i].key.data, sorted[i].value); + } + putchar(']'); + free(sorted); +} + +static void print_digest(const tg3_model *m) { + uint32_t i, j; + printf("DIGEST_BEGIN\n"); + + printf("asset version="); + d_str(&m->asset.version); + printf(" generator="); + d_str(&m->asset.generator); + printf("\n"); + + for (i = 0; i < m->buffers_count; ++i) { + const tg3_buffer *b = &m->buffers[i]; + uint64_t h = b->data.data ? fnv64(b->data.data, b->data.count) : 0; + printf("buffer %u byte_length=%llu fnv64=0x%016llx\n", + i, (unsigned long long)b->data.count, (unsigned long long)h); + } + for (i = 0; i < m->buffer_views_count; ++i) { + const tg3_buffer_view *bv = &m->buffer_views[i]; + printf("buffer_view %u buffer=%d byte_offset=%llu byte_length=%llu byte_stride=%u\n", + i, bv->buffer, (unsigned long long)bv->byte_offset, + (unsigned long long)bv->byte_length, bv->byte_stride); + } + for (i = 0; i < m->accessors_count; ++i) { + const tg3_accessor *a = &m->accessors[i]; + printf("accessor %u buffer_view=%d byte_offset=%llu component_type=%d count=%llu type=%d normalized=%d min=", + i, a->buffer_view, (unsigned long long)a->byte_offset, a->component_type, + (unsigned long long)a->count, a->type, a->normalized); + d_dbl_arr(a->min_values, a->min_values_count); + printf(" max="); + d_dbl_arr(a->max_values, a->max_values_count); + printf(" sparse=%d\n", a->sparse.is_sparse); + } + for (i = 0; i < m->meshes_count; ++i) { + const tg3_mesh *me = &m->meshes[i]; + printf("mesh %u primitives_count=%u weights_count=%u\n", + i, me->primitives_count, me->weights_count); + for (j = 0; j < me->primitives_count; ++j) { + const tg3_primitive *p = &me->primitives[j]; + printf("prim %u %u indices=%d material=%d mode=%d attrs=", i, j, + p->indices, p->material, p->mode); + d_attrs(p->attributes, p->attributes_count); + printf(" targets_count=%u\n", p->targets_count); + } + } + for (i = 0; i < m->nodes_count; ++i) { + const tg3_node *n = &m->nodes[i]; + printf("node %u mesh=%d skin=%d camera=%d light=%d children_count=%u has_matrix=%d t=", + i, n->mesh, n->skin, n->camera, n->light, n->children_count, n->has_matrix); + d_dbl_arr(n->translation, 3); + printf(" r="); + d_dbl_arr(n->rotation, 4); + printf(" s="); + d_dbl_arr(n->scale, 3); + printf(" matrix="); + d_dbl_arr(n->matrix, 16); + printf(" weights_count=%u\n", n->weights_count); + } + for (i = 0; i < m->materials_count; ++i) { + const tg3_material *mat = &m->materials[i]; + printf("material %u alpha_mode=", i); + d_str(&mat->alpha_mode); + printf(" alpha_cutoff="); + d_dbl(mat->alpha_cutoff); + printf(" double_sided=%d emissive=", mat->double_sided); + d_dbl_arr(mat->emissive_factor, 3); + printf(" base_color_factor="); + d_dbl_arr(mat->pbr_metallic_roughness.base_color_factor, 4); + printf(" metallic="); + d_dbl(mat->pbr_metallic_roughness.metallic_factor); + printf(" roughness="); + d_dbl(mat->pbr_metallic_roughness.roughness_factor); + printf(" base_color_tex=%d normal_tex=%d occlusion_tex=%d emissive_tex=%d\n", + mat->pbr_metallic_roughness.base_color_texture.index, + mat->normal_texture.index, + mat->occlusion_texture.index, + mat->emissive_texture.index); + } + for (i = 0; i < m->textures_count; ++i) { + const tg3_texture *t = &m->textures[i]; + printf("texture %u source=%d sampler=%d\n", i, t->source, t->sampler); + } + for (i = 0; i < m->samplers_count; ++i) { + const tg3_sampler *s = &m->samplers[i]; + printf("sampler %u min_filter=%d mag_filter=%d wrap_s=%d wrap_t=%d\n", + i, s->min_filter, s->mag_filter, s->wrap_s, s->wrap_t); + } + for (i = 0; i < m->images_count; ++i) { + const tg3_image *im = &m->images[i]; + /* mime_type and uri normalization differ between v1/v3 (data URIs, + extension inference); buffer_view reference is the parse-fidelity bit. */ + printf("image %u buffer_view=%d\n", i, im->buffer_view); + } + for (i = 0; i < m->skins_count; ++i) { + const tg3_skin *s = &m->skins[i]; + printf("skin %u inverse_bind_matrices=%d skeleton=%d joints_count=%u\n", + i, s->inverse_bind_matrices, s->skeleton, s->joints_count); + } + for (i = 0; i < m->animations_count; ++i) { + const tg3_animation *a = &m->animations[i]; + printf("animation %u channels_count=%u samplers_count=%u\n", + i, a->channels_count, a->samplers_count); + for (j = 0; j < a->channels_count; ++j) { + const tg3_animation_channel *c = &a->channels[j]; + printf("chan %u %u sampler=%d target_node=%d target_path=", i, j, + c->sampler, c->target.node); + d_str(&c->target.path); + printf("\n"); + } + for (j = 0; j < a->samplers_count; ++j) { + const tg3_animation_sampler *as = &a->samplers[j]; + printf("samp %u %u input=%d output=%d interpolation=", i, j, + as->input, as->output); + d_str(&as->interpolation); + printf("\n"); + } + } + for (i = 0; i < m->cameras_count; ++i) { + const tg3_camera *c = &m->cameras[i]; + int is_persp = (c->type.len == 11 && memcmp(c->type.data, "perspective", 11) == 0); + printf("camera %u type=", i); + d_str(&c->type); + if (is_persp) { + printf(" yfov="); + d_dbl(c->perspective.yfov); + printf(" znear="); + d_dbl(c->perspective.znear); + printf(" zfar="); + d_dbl(c->perspective.zfar); + printf(" aspect="); + d_dbl(c->perspective.aspect_ratio); + } else { + printf(" xmag="); + d_dbl(c->orthographic.xmag); + printf(" ymag="); + d_dbl(c->orthographic.ymag); + printf(" znear="); + d_dbl(c->orthographic.znear); + printf(" zfar="); + d_dbl(c->orthographic.zfar); + } + printf("\n"); + } + for (i = 0; i < m->scenes_count; ++i) { + const tg3_scene *s = &m->scenes[i]; + printf("scene %u nodes_count=%u\n", i, s->nodes_count); + } + printf("DIGEST_END\n"); +} + +/* ============================================================================ */ + static int mem_contains(const uint8_t *data, uint64_t size, const char *needle) { size_t needle_len = strlen(needle); uint64_t i; @@ -139,8 +349,9 @@ static int check_parse_file_failure_initializes_model(void) { tg3_error_stack_init(&errors); tg3_parse_options_init(&opts); - err = tg3_parse_file(&model, &errors, "scene.gltf", 10, &opts); - if (err != TG3_ERR_FS_NOT_AVAILABLE) { + err = tg3_parse_file(&model, &errors, + "tg3-tester-nonexistent-path.gltf", 32, &opts); + if (err != TG3_ERR_FS_NOT_AVAILABLE && err != TG3_ERR_FILE_NOT_FOUND) { fprintf(stderr, "tg3_parse_file unexpected error: %d\n", (int)err); tg3_error_stack_free(&errors); return 0; @@ -212,7 +423,89 @@ static int check_huge_integer_field_rejected(void) { return 1; } -int main(void) { +static int parse_file_arg(const char *path) { + FILE *fp = fopen(path, "rb"); + uint8_t *buf; + long sz; + size_t got; + tg3_model model; + tg3_error_stack errors; + tg3_parse_options opts; + tg3_error_code err; + int ok; + const char *slash; + size_t base_len; + + if (!fp) { + fprintf(stderr, "open failed: %s\n", path); + return 0; + } + if (fseek(fp, 0, SEEK_END) != 0 || (sz = ftell(fp)) < 0 || + fseek(fp, 0, SEEK_SET) != 0) { + fprintf(stderr, "seek failed: %s\n", path); + fclose(fp); + return 0; + } + buf = (uint8_t *)malloc((size_t)sz); + if (!buf) { + fprintf(stderr, "alloc failed: %s\n", path); + fclose(fp); + return 0; + } + got = fread(buf, 1, (size_t)sz, fp); + fclose(fp); + if (got != (size_t)sz) { + fprintf(stderr, "short read: %s\n", path); + free(buf); + return 0; + } + + slash = strrchr(path, '/'); + base_len = slash ? (size_t)(slash - path) : 0; + + tg3_error_stack_init(&errors); + tg3_parse_options_init(&opts); + err = tg3_parse_auto(&model, &errors, buf, (uint64_t)sz, + path, (uint32_t)base_len, &opts); + ok = (err == TG3_OK); + if (!ok) { + uint32_t i; + fprintf(stderr, "parse failed (%d): %s\n", (int)err, path); + for (i = 0; i < errors.count; ++i) { + fprintf(stderr, " [%u] code=%d sev=%d path=%s msg=%s offset=%lld\n", + i, (int)errors.entries[i].code, (int)errors.entries[i].severity, + errors.entries[i].json_path ? errors.entries[i].json_path : "", + errors.entries[i].message ? errors.entries[i].message : "", + (long long)errors.entries[i].byte_offset); + } + } else { + printf("COUNTS" + " accessors=%u animations=%u buffers=%u bufferViews=%u" + " cameras=%u images=%u materials=%u meshes=%u nodes=%u" + " samplers=%u scenes=%u skins=%u textures=%u lights=%u\n", + model.accessors_count, model.animations_count, + model.buffers_count, model.buffer_views_count, + model.cameras_count, model.images_count, + model.materials_count, model.meshes_count, + model.nodes_count, model.samplers_count, + model.scenes_count, model.skins_count, + model.textures_count, model.lights_count); + print_digest(&model); + } + tg3_model_free(&model); + tg3_error_stack_free(&errors); + free(buf); + return ok; +} + +int main(int argc, char **argv) { + if (argc > 1) { + int i; + for (i = 1; i < argc; ++i) { + if (!parse_file_arg(argv[i])) return 1; + } + return 0; + } if (!check_minimal_parse()) { return 1; } diff --git a/tests/v3/fuzzer/fuzz_gltf_v3_c.c b/tests/v3/fuzzer/fuzz_gltf_v3_c.c index 022ac82..cb11466 100644 --- a/tests/v3/fuzzer/fuzz_gltf_v3_c.c +++ b/tests/v3/fuzzer/fuzz_gltf_v3_c.c @@ -5,7 +5,11 @@ static const uint64_t FUZZ_MEMORY_BUDGET = 64ULL * 1024 * 1024; -static void tg3_fuzz_parse_auto(const uint8_t *data, size_t size) { +typedef tg3_error_code (*tg3_fuzz_parse_fn)(tg3_model *, tg3_error_stack *, + const uint8_t *, uint64_t, const char *, uint32_t, const tg3_parse_options *); + +static void tg3_fuzz_run(tg3_fuzz_parse_fn fn, int parse_float32, + const uint8_t *data, size_t size) { tg3_model model; tg3_error_stack errors; tg3_parse_options opts; @@ -13,83 +17,22 @@ static void tg3_fuzz_parse_auto(const uint8_t *data, size_t size) { tg3_error_stack_init(&errors); tg3_parse_options_init(&opts); opts.memory.memory_budget = FUZZ_MEMORY_BUDGET; + opts.parse_float32 = parse_float32; - tg3_parse_auto(&model, &errors, data, (uint64_t)size, "", 0, &opts); - - tg3_model_free(&model); - tg3_error_stack_free(&errors); -} - -static void tg3_fuzz_parse_json(const uint8_t *data, size_t size) { - tg3_model model; - tg3_error_stack errors; - tg3_parse_options opts; - - tg3_error_stack_init(&errors); - tg3_parse_options_init(&opts); - opts.memory.memory_budget = FUZZ_MEMORY_BUDGET; - - tg3_parse(&model, &errors, data, (uint64_t)size, "", 0, &opts); - - tg3_model_free(&model); - tg3_error_stack_free(&errors); -} - -static void tg3_fuzz_parse_glb(const uint8_t *data, size_t size) { - tg3_model model; - tg3_error_stack errors; - tg3_parse_options opts; - - tg3_error_stack_init(&errors); - tg3_parse_options_init(&opts); - opts.memory.memory_budget = FUZZ_MEMORY_BUDGET; - - tg3_parse_glb(&model, &errors, data, (uint64_t)size, "", 0, &opts); - - tg3_model_free(&model); - tg3_error_stack_free(&errors); -} - -static void tg3_fuzz_parse_float32(const uint8_t *data, size_t size) { - tg3_model model; - tg3_error_stack errors; - tg3_parse_options opts; - - tg3_error_stack_init(&errors); - tg3_parse_options_init(&opts); - opts.memory.memory_budget = FUZZ_MEMORY_BUDGET; - opts.parse_float32 = 1; - - tg3_parse_auto(&model, &errors, data, (uint64_t)size, "", 0, &opts); + fn(&model, &errors, data, (uint64_t)size, "", 0, &opts); tg3_model_free(&model); tg3_error_stack_free(&errors); } int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - uint8_t selector; - const uint8_t *payload; - size_t payload_size; - if (size == 0) return 0; - selector = data[0] % 4; - payload = data + 1; - payload_size = size - 1; - - switch (selector) { - case 0: - tg3_fuzz_parse_auto(payload, payload_size); - break; - case 1: - tg3_fuzz_parse_json(payload, payload_size); - break; - case 2: - tg3_fuzz_parse_glb(payload, payload_size); - break; - case 3: - tg3_fuzz_parse_float32(payload, payload_size); - break; + switch (data[0] % 4) { + case 0: tg3_fuzz_run(tg3_parse_auto, 0, data + 1, size - 1); break; + case 1: tg3_fuzz_run(tg3_parse, 0, data + 1, size - 1); break; + case 2: tg3_fuzz_run(tg3_parse_glb, 0, data + 1, size - 1); break; + case 3: tg3_fuzz_run(tg3_parse_auto, 1, data + 1, size - 1); break; } return 0; diff --git a/tiny_gltf_v3.c b/tiny_gltf_v3.c index 5643b52..87a8a0b 100644 --- a/tiny_gltf_v3.c +++ b/tiny_gltf_v3.c @@ -473,7 +473,9 @@ static int tg3__json_number_to_int32(const tg3json_value *v, int32_t *out) { static int tg3__json_number_to_uint64(const tg3json_value *v, uint64_t *out) { double real; uint64_t converted; - /* Largest safely castable integer expressible as double below 2^64. */ + /* Doubles have a 53-bit mantissa, so 2^64 itself rounds up and would be UB + on cast to uint64_t. Cap at the largest representable value strictly + below 2^64 (== 2^64 - 2^11). */ const double max_safe_uint64_real = 18446744073709547520.0; if (!tg3__json_is_number(v) || !out) return 0; if (v->type == TG3JSON_INT) { @@ -491,6 +493,13 @@ static int tg3__json_number_to_uint64(const tg3json_value *v, uint64_t *out) { return 1; } +static double tg3__json_number_to_double(const tg3json_value *v) { + if (!v) return 0.0; + if (v->type == TG3JSON_INT) return (double)v->u.integer; + if (v->type == TG3JSON_REAL) return v->u.real; + return 0.0; +} + static int tg3__json_is_object(const tg3json_value *v) { return v && v->type == TG3JSON_OBJECT; } static int tg3__json_is_array(const tg3json_value *v) { return v && v->type == TG3JSON_ARRAY; } @@ -588,7 +597,7 @@ static int tg3__parse_double(tg3__parse_ctx *ctx, const tg3json_value *o, const parent, "Field '%s' must be a number", key); return 0; } - *out = (it->type == TG3JSON_INT) ? (double)it->u.integer : it->u.real; + *out = tg3__json_number_to_double(it); return 1; } @@ -647,8 +656,7 @@ static int tg3__parse_number_array(tg3__parse_ctx *ctx, const tg3json_value *o, return 0; } for (i = 0; i < count; ++i) { - const tg3json_value *item = tg3json_array_get(it, i); - arr[i] = (item && item->type == TG3JSON_REAL) ? item->u.real : (item ? (double)item->u.integer : 0.0); + arr[i] = tg3__json_number_to_double(tg3json_array_get(it, i)); } *out = arr; *out_count = (uint32_t)count; @@ -715,7 +723,7 @@ static void tg3__parse_number_to_fixed(const tg3json_value *o, const char *key, for (i = 0; i < count; ++i) { const tg3json_value *item = tg3json_array_get(it, i); if (!item) continue; - out[i] = (item->type == TG3JSON_REAL) ? item->u.real : (double)item->u.integer; + out[i] = tg3__json_number_to_double(item); } }