mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-10-03 07:25:34 +00:00
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 d2837e3: 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:
@@ -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
|
||||
|
||||
@@ -2,8 +2,218 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user