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) <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2026-05-09 11:50:28 +09:00
parent 7f736d19db
commit 188d7b257b
6 changed files with 678 additions and 114 deletions

View File

@@ -6,7 +6,10 @@
#define STB_IMAGE_WRITE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION
#include "tiny_gltf.h" #include "tiny_gltf.h"
#include <algorithm>
#include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
@@ -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<double> &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) { int main(int argc, char **argv) {
if (argc < 2) { if (argc < 2) {
printf("Needs input.gltf\n"); printf("Needs input.gltf\n");
@@ -900,6 +1103,20 @@ int main(int argc, char **argv) {
return -1; 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); Dump(model);
return 0; return 0;

View File

@@ -2,63 +2,167 @@
import glob import glob
import os import os
import re
import subprocess 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 ----------------------- # -- config -----------------------
# Absolute path pointing to your cloned git repo of https://github.com/KhronosGroup/glTF-Sample-Models sample_model_dir = "/mnt/nfs/syoyo/glTF-Sample-Models"
sample_model_dir = "/home/syoyo/work/glTF-Sample-Models"
base_model_dir = os.path.join(sample_model_dir, "2.0") base_model_dir = os.path.join(sample_model_dir, "2.0")
# Include `glTF-Draco` when you build `loader_example` with draco support. v1_bin = "./loader_example"
kinds = [ "glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"] v3_bin = "./tests/tester_v3_c"
kinds = ["glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"]
# --------------------------------- # ---------------------------------
failed = [] COUNTS_RE = re.compile(r"^COUNTS\s+(.*)$", re.MULTILINE)
success = [] 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 "<missing>"
b = v3[i] if i < len(v3) else "<missing>"
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) 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: rc1, out1, err1 = run_binary(v1_bin, filename)
failed.append(filename) c1 = parse_counts(out1) if rc1 == 0 else None
print(stdout) d1 = parse_digest(out1) if rc1 == 0 else None
print(stderr) if c1 is None or d1 is None:
else: v1_skipped.append(filename)
success.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(): def test():
for d in sorted(os.listdir(base_model_dir)):
for d in os.listdir(base_model_dir):
p = os.path.join(base_model_dir, d) p = os.path.join(base_model_dir, d)
if os.path.isdir(p): if not os.path.isdir(p):
continue
for k in kinds: for k in kinds:
targetDir = os.path.join(p, k) targetDir = os.path.join(p, k)
g = glob.glob(targetDir + "/*.gltf") + glob.glob(targetDir + "/*.glb") g = sorted(
glob.glob(targetDir + "/*.gltf")
+ glob.glob(targetDir + "/*.glb")
)
for gltf in g: for gltf in g:
run(gltf) verify(gltf)
def main(): 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() test()
print("Success : {0}".format(len(success))) print("")
print("Failed : {0}".format(len(failed))) 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: for f, diffs in counts_diff:
print("FAIL: " + fail) 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() main()

View File

@@ -1,11 +1,10 @@
# Use this for strict compilation check(will work on clang 3.8+) # 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 #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++ -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_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++ -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 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

View File

@@ -2,8 +2,218 @@
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
/* ===== 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) { static int mem_contains(const uint8_t *data, uint64_t size, const char *needle) {
size_t needle_len = strlen(needle); size_t needle_len = strlen(needle);
uint64_t i; uint64_t i;
@@ -139,8 +349,9 @@ static int check_parse_file_failure_initializes_model(void) {
tg3_error_stack_init(&errors); tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts); tg3_parse_options_init(&opts);
err = tg3_parse_file(&model, &errors, "scene.gltf", 10, &opts); err = tg3_parse_file(&model, &errors,
if (err != TG3_ERR_FS_NOT_AVAILABLE) { "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); fprintf(stderr, "tg3_parse_file unexpected error: %d\n", (int)err);
tg3_error_stack_free(&errors); tg3_error_stack_free(&errors);
return 0; return 0;
@@ -212,7 +423,89 @@ static int check_huge_integer_field_rejected(void) {
return 1; 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()) { if (!check_minimal_parse()) {
return 1; return 1;
} }

View File

@@ -5,7 +5,11 @@
static const uint64_t FUZZ_MEMORY_BUDGET = 64ULL * 1024 * 1024; 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_model model;
tg3_error_stack errors; tg3_error_stack errors;
tg3_parse_options opts; 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_error_stack_init(&errors);
tg3_parse_options_init(&opts); tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET; opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
opts.parse_float32 = parse_float32;
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);
}
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);
tg3_model_free(&model); tg3_model_free(&model);
tg3_error_stack_free(&errors); tg3_error_stack_free(&errors);
} }
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { 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; if (size == 0) return 0;
selector = data[0] % 4; switch (data[0] % 4) {
payload = data + 1; case 0: tg3_fuzz_run(tg3_parse_auto, 0, data + 1, size - 1); break;
payload_size = size - 1; 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;
switch (selector) { case 3: tg3_fuzz_run(tg3_parse_auto, 1, data + 1, size - 1); break;
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;
} }
return 0; return 0;

View File

@@ -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) { static int tg3__json_number_to_uint64(const tg3json_value *v, uint64_t *out) {
double real; double real;
uint64_t converted; 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; const double max_safe_uint64_real = 18446744073709547520.0;
if (!tg3__json_is_number(v) || !out) return 0; if (!tg3__json_is_number(v) || !out) return 0;
if (v->type == TG3JSON_INT) { 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; 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_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; } 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); parent, "Field '%s' must be a number", key);
return 0; return 0;
} }
*out = (it->type == TG3JSON_INT) ? (double)it->u.integer : it->u.real; *out = tg3__json_number_to_double(it);
return 1; return 1;
} }
@@ -647,8 +656,7 @@ static int tg3__parse_number_array(tg3__parse_ctx *ctx, const tg3json_value *o,
return 0; return 0;
} }
for (i = 0; i < count; ++i) { for (i = 0; i < count; ++i) {
const tg3json_value *item = tg3json_array_get(it, i); arr[i] = tg3__json_number_to_double(tg3json_array_get(it, i));
arr[i] = (item && item->type == TG3JSON_REAL) ? item->u.real : (item ? (double)item->u.integer : 0.0);
} }
*out = arr; *out = arr;
*out_count = (uint32_t)count; *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) { for (i = 0; i < count; ++i) {
const tg3json_value *item = tg3json_array_get(it, i); const tg3json_value *item = tg3json_array_get(it, i);
if (!item) continue; if (!item) continue;
out[i] = (item->type == TG3JSON_REAL) ? item->u.real : (double)item->u.integer; out[i] = tg3__json_number_to_double(item);
} }
} }