Compare commits

...

15 Commits

Author SHA1 Message Date
Syoyo Fujita
d31c16e333 CI: fix clang -Werror on unknown flag and MSVC fopen warning
run 25611531122 surfaced two failures:

- Stock Ubuntu clang errored on `-Wno-pre-c11-compat` (added in newer
  clang). Add `-Wno-unknown-warning-option` so older clang silently
  ignores warning flags it doesn't know.

- MSVC /W4 /WX failed on C4996 (fopen deprecation). Define
  `_CRT_SECURE_NO_WARNINGS` for the v3 C MSVC build; the parser uses
  fopen by design and v1 already takes the same approach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 06:00:09 +09:00
Syoyo Fujita
36c9643981 CI: bump GitHub Actions to Node.js 24 versions
Silence the deprecation warnings reported on
https://github.com/syoyo/tinygltf/actions/runs/25610558215 by upgrading
all `actions/checkout` usages from v1/v2/v3/v4 to v5 (Node 24 runtime)
and the CodeQL actions from v2 to v3.

msys2/setup-msys2 stays at v2 — it's the latest tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 05:34:39 +09:00
Syoyo Fujita
c9b3b9c644 Build v3 C under clang -Weverything and MSVC /W4
Escalate v3 C warning levels in CI to catch latent issues:
gcc/clang now run with -Werror, clang adds -Weverything (with a
small irreducible suppression list for -Wpadded, -Wunsafe-buffer-usage,
-Wcast-align, etc.), and a new MSVC job builds tester_v3_c with /W4 /WX.

Source fixes to clear the elevated warnings:
- tg3__arena_new_block: cast through void* to silence -Wcast-align.
- tg3__value_to_json: handle TG3_VALUE_BINARY explicitly and drop the
  default label so -Wswitch-enum and -Wcovered-switch-default agree.
- Drop unused tg3__json_set_value_copy.
- tinygltf_json_c.h: enumerate all tg3json_value_type cases in
  tg3json_value_free / tg3json_value_copy / tg3json__stringify_value_ex.
- tester_v3_c_v1port FAIL macro: split the format/newline prints so it
  no longer relies on the GNU `, ##__VA_ARGS__` extension.

Verified: clang -Werror -Weverything builds clean, 13/13 internal
tests, 18/18 v1-port tests, and 134/134 cross-version regression all
pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:33:21 +09:00
Syoyo Fujita
d20c9298e5 Build v3 C tests under clang too; drop GNU VA_ARGS extension
ci.yml now runs the v3 C suites under three Linux toolchains:
- v3-c-tests        (default cc / GCC, -Wall -Wextra)
- v3-c-tests-clang  (stock Ubuntu clang)
- v3-c-tests-clang21 (clang 21 from apt.llvm.org, matching local dev)

The validator helper macros TG3__IDX_BAD / TG3__CHECK_REQ /
TG3__CHECK_OPT used the GNU `, ##__VA_ARGS__` extension, which clang
flags under -Wpedantic. Every call site already passes at least one
variadic argument, so plain __VA_ARGS__ (C11) suffices.

Local verification: clang-21 -Wall -Wextra builds tester_v3_c and
tester_v3_c_v1port with zero warnings introduced by the v3 changes
(only the pre-existing tg3__json_set_value_copy unused-function
notice remains, unrelated to this work). All suites pass under gcc
13.3 and clang 21.1 alike, plus the 134-model cross-version
verifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:04:15 +09:00
Syoyo Fujita
70a6a0c0ea Run v3 C tests in GitHub CI
ci.yml: two new jobs.
  - v3-c-tests builds tester_v3_c (security regressions) and
    tester_v3_c_v1port (18 ported v1 unit tests) with the default
    system cc (gcc on ubuntu-latest) and runs both.
  - v3-c-tests-sanitizers rebuilds the same suites under clang with
    -fsanitize=address,undefined and ASAN_OPTIONS=halt_on_error=1 plus
    leak detection so memory-safety regressions break CI.

c-cpp.yml: add a v3_c_tests step to build-linux so the legacy gcc
workflow also exercises the v3 C parser end-to-end.

Both invoke the parser via parse_auto with TINYGLTF3_ENABLE_FS so the
external-file paths (and the new path-traversal/file-size guards)
are exercised end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:56:13 +09:00
Syoyo Fujita
cc52d8057b Port v1 unit tests to v3 C; enforce max_external_file_size
tests/tester_v3_c_v1port.c mirrors 18 parse/load test cases from
tester.cc against the pure-C v3 runtime: parse-error, datauri-in-glb,
extension-with-empty-object, extension-overwrite, four bounds-checking
cases, glb-invalid-length, integer-out-of-bounds,
pbr-khr-texture-transform (verifies KHR_texture_transform scale via
tg3_value introspection), image-uri-spaces (single + multiple),
empty-skeleton-id, filesize-check, load-issue-416-model,
zero-sized-bin-chunk-glb, images-as-is, inverse-bind-matrices-optional,
default-material. Header comment lists tester.cc cases skipped because
they exercise the v3 writer or v1-internal helpers (out of scope).

Wiring max_external_file_size in the parser exposed by the
filesize-check port: the option was declared in tg3_parse_options but
never enforced. tg3__load_external_file now rejects loaded files larger
than the cap with TG3_ERR_FILE_TOO_LARGE and frees the buffer the fs
callback returned to avoid a leak. The 134-model verifier and the
existing tester_v3_c security regressions still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:31:03 +09:00
Syoyo Fujita
3a2f149458 Document v3 C security model and verification layers in README
Adds a Security model subsection covering URI sanitization, index-bounds
validation, strict numeric ranges, the 1 GB memory budget, image-decoder
opt-out, and error message lifetime — pointing to the authoritative
Security Considerations block in tiny_gltf_v3.h.

Adds a Testing & verification subsection describing the three coverage
layers shipped with the v3 C runtime: internal regression tests in
tester_v3_c, the cross-version DIGEST verifier in test_runner.py, and
the libFuzzer harness with ASan+UBSan in tests/v3/fuzzer/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:16:30 +09:00
Syoyo Fujita
a8fb48fa91 Harden v3 C parser against untrusted glTF input
Threat model: parser is intended for server-side processing of attacker-
supplied glTF/GLB. Two adversarial review rounds plus a 1-hour libFuzzer
run (4 workers, ASan+UBSan, ~420M execs total, zero new artifacts) drove
this set of fixes. Concrete PoCs in tests/v3/security/ confirmed each
issue was exploitable on the prior code.

Path traversal (CRITICAL): tg3__load_external_file concatenated base_dir
with the JSON-supplied URI verbatim. A glTF with
"uri":"../../../tmp/secret" successfully loaded the file from outside
base_dir (verified by FNV64 match). New tg3__uri_is_safe rejects empty,
NUL, leading / or \\, Windows drive prefixes, and any '..' segment.
Path-buffer length checks switched to saturating subtraction so 32-bit
size_t cannot wrap.

Sign-coercion in byteStride: int32_t -1 was cast directly to uint32_t,
producing 0xFFFFFFFF and propagating into downstream count*stride math.
Restrict to glTF spec range: 0 (tightly packed) or [4, 252].

Index validation: parsed int32 index fields (accessor.bufferView,
primitive.indices/material/attributes, node.mesh/skin/camera/light,
scene.nodes[], skin.joints[], animation channel/sampler refs, MSFT_lod
ids, KHR_audio emitter/source refs, etc.) were stored unchecked. New
tg3__validate_indices walks every index field and returns
TG3_ERR_INVALID_INDEX on out-of-range. Gated by
tg3_parse_options.validate_indices, defaulting to 1.

Use-after-free on parse failure (PRE-EXISTING, surfaced by ASan during
fix verification): tg3_parse and tg3_parse_glb destroyed model->arena_
on error paths, but error messages on the user-facing tg3_error_stack
were arena-allocated. Any caller reading errors.entries[i].message
after parse failure read freed memory. tg3_model_free is now sole arena
owner; arena lives across error paths so messages stay valid until the
caller frees the model.

Other fixes:
- tg3_parse_glb: hoist tg3__model_init before header parse so callers
  can safely tg3_model_free on header failure.
- tg3__parse_primitive morph targets: when arena alloc returns NULL,
  pair with target_counts[ti]=0 so validators do not deref.
- Defensive 'if (!tarr) continue' in the morph-target validator loop.
- New Security Considerations block in tiny_gltf_v3.h documents the
  threat model, default-on validation, fs-callback contract, and error
  message lifetime.

Verification: 13 internal tests in tester_v3_c (incl. 7 new security
regressions covering path traversal absolute and relative, fs-callback
no-call assertion, byteStride wrap, OOB index, opt-in raw mode, ext
fields, and arena-message lifetime), 134/134 Khronos sample models
match v1 ground truth digest, 1-hour ASan+UBSan fuzz on the final code
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:02:34 +09:00
Syoyo Fujita
188d7b257b 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>
2026-05-09 11:50:28 +09:00
Syoyo Fujita
7f736d19db Harden v3 numeric parsing and add C fuzz harness
Reject non-finite/out-of-range JSON numbers in int32/uint64 fields and
array/attribute elements instead of silently truncating, initialize the
model on parse-file failure, and free the partial JSON document when the
root is not an object. Adds a pure-C libFuzzer harness (fuzz_gltf_v3_c)
alongside the existing C++ one and tests covering the new failure modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:10:32 +09:00
Syoyo Fujita
af09ec3405 Mark v3 C runtime experimental
Update README.md to describe the pure-C v3 runtime accurately, fix the JSON backend reference, and mark the new C implementation as experimental.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 09:16:14 +09:00
Syoyo Fujita
85441bbe19 Add pure-C TinyGLTF v3 runtime
Introduce a C-first TinyGLTF v3 runtime in tiny_gltf_v3.c with a pure-C JSON backend, hook the public header to the new implementation, and add CMake/test coverage for parse and write round-trips.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 04:28:41 +09:00
Syoyo Fujita
a18f41142f Remove projects section from README
Removed the list of projects using TinyGLTF from the README.
2026-05-04 05:37:00 +09:00
Syoyo Fujita
d8f3bd93f7 add pr template. 2026-04-06 04:43:12 +09:00
Syoyo Fujita
bd6db55b70 Create SECURITY.md 2026-04-06 04:30:19 +09:00
23 changed files with 6492 additions and 108 deletions

45
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,45 @@
## Description
What does this PR do? Provide a brief summary of the changes.
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactoring (no functional changes)
- [ ] Documentation update
- [ ] Other (please describe):
## Checklist
### Required for All PRs
- [ ] Reproducible glTF test file(s) are included (e.g., `models/regression/`, `tests/issue-***.gltf`, etc.)
- [ ] Unit tests are written and pass locally (`cd tests && ./tester`)
### Required for Feature PRs
- [ ] Specification document is included (e.g., `docs/spec/<feature-name>.md`)
- The spec should cover: purpose, API design, usage examples, and edge cases
### Security Policy
This project manages CVE assignments exclusively through GitHub Security Advisories.
PRs that include or reference independently obtained CVE IDs or external vulnerability disclosures will be closed.
## Test Instructions
How can reviewers verify your changes?
```
# Example:
cd build && cmake .. && make test
```
## Related Issues
Link related issues:
## Additional Notes
Any other context, screenshots, or information relevant to the review.

View File

@@ -14,7 +14,7 @@ jobs:
# steps:
# - name: Checkout
# uses: actions/checkout@v1
# uses: actions/checkout@v5
# - name: Build
# run: |
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Build
run: |
@@ -60,7 +60,7 @@ jobs:
# https://help.github.com/en/actions/reference/software-installed-on-github-hosted-runners
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v5
- name: Configure
run: |
mkdir build
@@ -78,7 +78,7 @@ jobs:
name: Buld with gcc
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- name: build
run: |
g++ -std=c++11 -o loader_example loader_example.cc
@@ -100,6 +100,17 @@ jobs:
./tester_noexcept
cd ..
- name: v3_c_tests
run: |
cd tests
cc -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
./tester_v3_c
cc -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
./tester_v3_c_v1port
cd ..
build-rapidjson-linux:
@@ -107,7 +118,7 @@ jobs:
name: Buld with gcc + rapidjson
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- name: build
run: |
git clone https://github.com/Tencent/rapidjson
@@ -140,7 +151,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Build
run: |
sudo apt-get update
@@ -158,7 +169,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v5
- name: Build
run: |
clang++ -std=c++11 -g -O0 -o loader_example loader_example.cc

View File

@@ -23,7 +23,7 @@ jobs:
name: Linux x64 (GCC)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
@@ -43,7 +43,7 @@ jobs:
name: Linux x64 (Clang 21)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Clang 21
run: |
@@ -71,7 +71,7 @@ jobs:
name: Linux ARM64 (GCC)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
@@ -91,7 +91,7 @@ jobs:
name: macOS ARM64 Apple Silicon (Clang)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Configure
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
@@ -111,7 +111,7 @@ jobs:
name: Windows x64 (MSVC)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Configure
run: |
@@ -135,7 +135,7 @@ jobs:
name: Windows x86 (MSVC)
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Configure
run: |
@@ -155,7 +155,7 @@ jobs:
name: Windows ARM64 (MSVC) - Cross-compile
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Configure
run: |
@@ -175,7 +175,7 @@ jobs:
shell: msys2 {0}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
@@ -205,7 +205,7 @@ jobs:
name: Linux→Windows (MinGW Cross) - Build Only
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install MinGW
run: |
@@ -222,7 +222,7 @@ jobs:
name: Linux x64 (GCC) - No Exceptions
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Build loader_example
run: |
@@ -244,7 +244,7 @@ jobs:
name: Linux x64 (GCC) - Header-Only Mode
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Build with CMake Header-Only
run: |
@@ -265,7 +265,7 @@ jobs:
name: Linux x64 (GCC) - RapidJSON Backend
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Clone RapidJSON
run: |
@@ -291,7 +291,7 @@ jobs:
name: Linux x64 (Clang) - AddressSanitizer
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Build loader_example with ASan
run: |
@@ -313,7 +313,7 @@ jobs:
name: Linux x64 (Clang) - UndefinedBehaviorSanitizer
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Build loader_example with UBSan
run: |
@@ -328,3 +328,186 @@ jobs:
cd tests
clang++ -fsanitize=undefined -I../ -std=c++11 -g -O1 -o tester tester.cc
./tester
# v3 C runtime: internal security regression tests + ported v1 unit tests.
v3-c-tests:
runs-on: ubuntu-latest
name: v3 C tests (Linux x64, GCC)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build tester_v3_c
run: |
cd tests
cc -I../ -std=c11 -g -O0 -Wall -Wextra -Wpedantic -Werror -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port
run: |
cd tests
cc -I../ -std=c11 -g -O0 -Wall -Wextra -Wpedantic -Werror -DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c (security regressions)
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port (18 ported unit tests)
run: |
cd tests
./tester_v3_c_v1port
# v3 C runtime under stock Ubuntu clang.
v3-c-tests-clang:
runs-on: ubuntu-latest
name: v3 C tests (Linux x64, Clang)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install clang
run: |
sudo apt-get update
sudo apt-get install -y clang
- name: Build tester_v3_c
run: |
cd tests
clang -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port
run: |
cd tests
clang -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port
run: |
cd tests
./tester_v3_c_v1port
# v3 C runtime under bleeding-edge clang 21 (matches local dev environment).
v3-c-tests-clang21:
runs-on: ubuntu-24.04
name: v3 C tests (Linux x64, Clang 21)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install clang 21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
- name: Build tester_v3_c
run: |
cd tests
clang-21 -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port
run: |
cd tests
clang-21 -I../ -std=c11 -g -O0 -Werror -Weverything \
-Wno-padded -Wno-unsafe-buffer-usage -Wno-switch-default \
-Wno-format-nonliteral -Wno-float-equal -Wno-cast-align \
-Wno-declaration-after-statement -Wno-unknown-warning-option \
-Wno-pre-c11-compat \
-DTINYGLTF3_ENABLE_FS \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port
run: |
cd tests
./tester_v3_c_v1port
# v3 C runtime built with MSVC at warning-level 4 (/W4 /WX).
v3-c-tests-msvc:
runs-on: windows-latest
name: v3 C tests (Windows x64, MSVC /W4)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build and run tester_v3_c
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cd tests
cl /nologo /W4 /WX /std:c11 /Zi /Od /D_CRT_SECURE_NO_WARNINGS /DTINYGLTF3_ENABLE_FS /I.. tester_v3_c.c ..\tiny_gltf_v3.c /Fe:tester_v3_c.exe
tester_v3_c.exe
- name: Build and run tester_v3_c_v1port
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cd tests
cl /nologo /W4 /WX /std:c11 /Zi /Od /D_CRT_SECURE_NO_WARNINGS /DTINYGLTF3_ENABLE_FS /I.. tester_v3_c_v1port.c ..\tiny_gltf_v3.c /Fe:tester_v3_c_v1port.exe
tester_v3_c_v1port.exe
# v3 C runtime under ASan + UBSan for memory-safety + UB checks.
v3-c-tests-sanitizers:
runs-on: ubuntu-latest
name: v3 C tests (Clang ASan + UBSan)
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build tester_v3_c with ASan + UBSan
run: |
cd tests
clang -I../ -std=c11 -g -O1 -DTINYGLTF3_ENABLE_FS \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
- name: Build tester_v3_c_v1port with ASan + UBSan
run: |
cd tests
clang -I../ -std=c11 -g -O1 -DTINYGLTF3_ENABLE_FS \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c
- name: Run tester_v3_c
env:
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
run: |
cd tests
./tester_v3_c
- name: Run tester_v3_c_v1port
env:
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
run: |
cd tests
./tester_v3_c_v1port

View File

@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@@ -20,7 +20,7 @@ jobs:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Install core & build dependencies
uses: msys2/setup-msys2@v2

View File

@@ -65,6 +65,21 @@ if (TINYGLTF_BUILD_TESTS)
)
target_compile_definitions(tester_intensive_customjson PRIVATE TINYGLTF_USE_CUSTOM_JSON)
add_test(NAME tester_intensive_customjson COMMAND tester_intensive_customjson WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
add_executable(tester_v3_c
tests/tester_v3_c.c
tiny_gltf_v3.c
)
target_include_directories(tester_v3_c PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
)
set_target_properties(tester_v3_c PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED ON
C_EXTENSIONS OFF
)
add_test(NAME tester_v3_c COMMAND tester_v3_c WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
endif (TINYGLTF_BUILD_TESTS)
#
@@ -107,7 +122,10 @@ if (TINYGLTF_INSTALL)
INSTALL ( FILES
tiny_gltf.h
tiny_gltf_v3.h
tiny_gltf_v3.c
tinygltf_json.h
tinygltf_json_c.h
${TINYGLTF_EXTRA_SOUECES}
DESTINATION
include

View File

@@ -4,7 +4,8 @@
## TinyGLTF v3 (new major release)
**`tiny_gltf_v3.h`** is the new major version of TinyGLTF and the recommended API for new projects.
**`tiny_gltf_v3.h`** is the new major version of TinyGLTF.
The new C implementation (`tiny_gltf_v3.c` + `tinygltf_json_c.h`) is currently **experimental**.
### What's new in v3
@@ -13,43 +14,76 @@ v3 is a ground-up rewrite with a C-centric, low-overhead design:
- **Pure C POD structs** — no STL containers in the public API; easy to bind to other languages.
- **Arena-based memory management** — all parse-time allocations come from a single arena; a single `tg3_model_free()` frees everything.
- **Structured error reporting** — `tg3_error_stack` provides machine-readable errors with severity levels and source locations.
- **Custom JSON backend** — backed by `tinygltf_json.h`, a high-performance, locale-independent JSON parser with optional SIMD acceleration (SSE2 / AVX2 / NEON) and a float32 fast-path.
- **Custom JSON backend** — backed by `tinygltf_json_c.h`, a locale-independent pure-C JSON parser/serializer used by the v3 runtime.
- **Streaming callbacks** — opt-in streaming parse/write via user-supplied callbacks.
- **No RTTI, no exceptions required** — suitable for embedded and game-engine use.
- **Opt-in filesystem and image I/O** — `TINYGLTF3_ENABLE_FS` / `TINYGLTF3_ENABLE_STB_IMAGE` are off by default; you control when and how assets are loaded.
- **C++20 coroutine facade** (optional, auto-detected). C17/C++17 default.
- **Hardened against untrusted input** — URI sanitization, post-parse index-bounds validation (default-on, opt-out via `tg3_parse_options.validate_indices = 0`), strict numeric range checks; exercised by a libFuzzer harness and by a cross-version verifier that compares parsed output against the v1 C++ reference loader. See *Security model* below and the `Security Considerations` block at the top of `tiny_gltf_v3.h`.
### Quick start (v3)
Copy `tiny_gltf_v3.h` and `tinygltf_json.h` to your project. In **one** `.cpp` file:
Copy `tiny_gltf_v3.h`, `tiny_gltf_v3.c`, and `tinygltf_json_c.h` to your project.
Compile `tiny_gltf_v3.c` as C11 or newer. Define `TINYGLTF3_ENABLE_FS` when
building `tiny_gltf_v3.c` if you want `tg3_parse_file()` to use stdio-backed
filesystem helpers. The legacy `TINYGLTF3_IMPLEMENTATION` include path remains
available for compatibility.
```cpp
#define TINYGLTF3_IMPLEMENTATION
#define TINYGLTF3_ENABLE_FS // enable file I/O
#define TINYGLTF3_ENABLE_STB_IMAGE // enable image decoding
```c
#include "tiny_gltf_v3.h"
```
Loading a glTF file:
```c
tg3_load_options_t opts = tg3_load_options_default();
tg3_error_stack_t errors = {0};
tg3_model_t *model = tg3_load_from_file("scene.gltf", &opts, &errors);
if (!model) {
for (int i = 0; i < errors.count; i++)
fprintf(stderr, "[%s] %s\n", tg3_severity_str(errors.items[i].severity),
errors.items[i].message);
tg3_parse_options opts;
tg3_error_stack errors;
tg3_model model;
tg3_parse_options_init(&opts);
tg3_error_stack_init(&errors);
tg3_error_code err = tg3_parse_file(&model, &errors, "scene.gltf", 10, &opts);
if (err != TG3_OK) {
for (uint32_t i = 0; i < errors.count; i++) {
fprintf(stderr, "[%d] %s\n", (int)errors.entries[i].severity,
errors.entries[i].message ? errors.entries[i].message : "(null)");
}
}
// ... use model ...
tg3_model_free(model);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
```
### Security model (v3 C runtime)
The v3 C runtime is built for processing **untrusted glTF/GLB input** (server-side asset pipelines, user uploads, etc.) and ships hardened by default:
- **URI sanitization** — external buffer/image URIs are rejected before any filesystem call if they are empty, contain NUL bytes, begin with `/` or `\`, look like a Windows drive prefix (`X:`), or contain a `..` segment. Production callers SHOULD still provide a custom `tg3_fs_callbacks.read_file` that confines reads to a known directory (e.g. via `openat` plus a `realpath` prefix check) when the input is attacker-controlled.
- **Index bounds validation** — every `int32_t` index field populated from JSON (accessor.bufferView, primitive.indices/material/attributes, scene.nodes[], skin.joints[], animation channel/sampler refs, KHR_audio + MSFT_lod refs, …) is checked after the structural parse. Out-of-range indices produce `TG3_ERR_INVALID_INDEX`. Default `tg3_parse_options.validate_indices = 1`; set to `0` only when you need raw round-trip and have your own validator.
- **Strict numeric range checks** — JSON numbers feeding integer fields go through finite/round-trip-validated coercion (`tg3__json_number_to_int32` / `_uint64`). `byteStride` is restricted to 0 or [4, 252].
- **Memory budget** — the arena is capped at `TINYGLTF3_MAX_MEMORY_BYTES` (1 GB by default; configurable via `tg3_memory_config`).
- **Image decoding off by default** — the parser does not decode image bytes; use `tg3_parse_options.images_as_is = 1` to skip any decoder entirely when handling untrusted input.
- **Error message lifetime** — error strings on `tg3_error_stack` are arena-allocated and remain valid until `tg3_model_free()`. Read or copy them BEFORE freeing the model.
See the `Security Considerations` block at the top of `tiny_gltf_v3.h` for the authoritative threat-model summary.
### Testing & verification
The v3 C runtime ships with three layers of automated coverage:
- **`tests/tester_v3_c.c`** — internal unit checks plus security regression tests (path traversal, negative `byteStride`, OOB indices, error-message lifetime, …). Build via `make` in `tests/`; run `./tester_v3_c` for the internal suite or `./tester_v3_c <file.gltf|file.glb>` to parse a single asset.
- **`test_runner.py`** — a cross-version verifier that runs the v1 C++ reference loader (`loader_example`) and the v3 C tester against every model in `glTF-Sample-Models/2.0`, then diffs a structured DIGEST block (buffer FNV64 hashes, accessor/bufferView fields, primitive attribute maps, node TRS, material PBR factors, skin/animation/scene topology, …). v1 is the ground truth.
- **`tests/v3/fuzzer/`** — libFuzzer harness with ASan + UBSan (`make run` builds and runs `fuzz_gltf_v3_c`). Crafted regression inputs live in `tests/v3/security/` and are seeded into `tests/v3/fuzzer/corpus/`.
## Status
> ⚠️ **v2 deprecation notice:** `tiny_gltf.h` (v2) remains fully functional and is still supported,
> but it is now in **maintenance mode only** — no new features will be added.
> v2 will be **sunset after mid-2026**. New projects should use `tiny_gltf_v3.h`.
> v2 will be **sunset after mid-2026**. `tiny_gltf_v3.h` is the intended successor, but the new C v3 runtime is still **experimental**.
TinyGLTF v3's C runtime (`tiny_gltf_v3.h` + `tiny_gltf_v3.c`) is available for evaluation and early adoption,
but its API/behavior may still change while the implementation matures.
Currently TinyGLTF v2 is stable and in maintenance mode. No drastic changes and feature additions planned.
- v2.9.0 Various fixes and improvements. Filesystem callback API change.
@@ -133,23 +167,6 @@ Users who want to run TinyGLTF securely and safely(e.g. need to handle malcious
I recommend to build TinyGLTF for WASM target.
WASI build example is located in [wasm](wasm) .
## Projects using TinyGLTF
* px_render Single header C++ Libraries for Thread Scheduling, Rendering, and so on... https://github.com/pplux/px
* Physical based rendering with Vulkan using glTF 2.0 models https://github.com/SaschaWillems/Vulkan-glTF-PBR
* GLTF loader plugin for OGRE 2.1. Support for PBR materials via HLMS/PBS https://github.com/Ybalrid/Ogre_glTF
* [TinyGltfImporter](http://doc.magnum.graphics/magnum/classMagnum_1_1Trade_1_1TinyGltfImporter.html) plugin for [Magnum](https://github.com/mosra/magnum), a lightweight and modular C++11/C++14 graphics middleware for games and data visualization.
* [Diligent Engine](https://github.com/DiligentGraphics/DiligentEngine) - A modern cross-platform low-level graphics library and rendering framework
* Lighthouse 2: a rendering framework for real-time ray tracing / path tracing experiments. https://github.com/jbikker/lighthouse2
* [QuickLook GLTF](https://github.com/toshiks/glTF-quicklook) - quicklook plugin for macos. Also SceneKit wrapper for tinygltf.
* [GlslViewer](https://github.com/patriciogonzalezvivo/glslViewer) - live GLSL coding for MacOS and Linux
* [Vulkan-Samples](https://github.com/KhronosGroup/Vulkan-Samples) - The Vulkan Samples is collection of resources to help you develop optimized Vulkan applications.
* [TDME2](https://github.com/andreasdr/tdme2) - TDME2 - ThreeDeeMiniEngine2 is a lightweight 3D engine including tools suited for 3D game development using C++11
* [SanityEngine](https://github.com/DethRaid/SanityEngine) - A C++/D3D12 renderer focused on the personal and professional development of its developer
* [Open3D](http://www.open3d.org/) - A Modern Library for 3D Data Processing
* [Supernova Engine](https://github.com/supernovaengine/supernova) - Game engine for 2D and 3D projects with Lua or C++ in data oriented design.
* [Wicked Engine<img src="https://github.com/turanszkij/WickedEngine/blob/master/Content/logo_small.png" width="28px" align="center"/>](https://github.com/turanszkij/WickedEngine) - 3D engine with modern graphics
* Your projects here! (Please send PR)
## TODOs

4
SECURITY.md Normal file
View File

@@ -0,0 +1,4 @@
# Security Policy
This project manages CVE assignments exclusively through
GitHub Security Advisories.

View File

@@ -28,7 +28,7 @@ all: $(GEN) $(BENCH_V3)
$(GEN): gen_synthetic.cpp
$(CXX) $(CXXFLAGS) -o $@ $<
$(BENCH_V3): bench_v3.cpp ../tiny_gltf_v3.h ../tinygltf_json.h
$(BENCH_V3): bench_v3.cpp ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $<
# Generate synthetic scenes of varying sizes

View File

@@ -6,7 +6,10 @@
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "tiny_gltf.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#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) {
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;

View File

@@ -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 "<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)
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()

View File

@@ -1,7 +1,13 @@
# 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 tester_v3_c_v1port
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
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 -DTINYGLTF3_ENABLE_FS -o tester_v3_c tester_v3_c.c ../tiny_gltf_v3.c
tester_v3_c_v1port: tester_v3_c_v1port.c ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
clang -I../ -std=c11 -g -O0 -DTINYGLTF3_ENABLE_FS -o tester_v3_c_v1port tester_v3_c_v1port.c ../tiny_gltf_v3.c

772
tests/tester_v3_c.c Normal file
View File

@@ -0,0 +1,772 @@
#include "tiny_gltf_v3.h"
#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;
if (needle_len == 0 || size < (uint64_t)needle_len) {
return 0;
}
for (i = 0; i + (uint64_t)needle_len <= size; ++i) {
if (memcmp(data + i, needle, needle_len) == 0) {
return 1;
}
}
return 0;
}
static int check_minimal_parse(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"scene\":0,"
"\"scenes\":[{\"nodes\":[0]}],"
"\"nodes\":[{\"name\":\"root\"}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse_auto(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0,
&opts);
if (err != TG3_OK) {
fprintf(stderr, "tg3_parse_auto failed: %d\n", (int)err);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != 0 || model.scenes_count != 1 || model.nodes_count != 1) {
fprintf(stderr, "unexpected parsed model shape\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (!model.nodes || !model.nodes[0].name.data ||
!tg3_str_equals_cstr(model.nodes[0].name, "root")) {
fprintf(stderr, "node name mismatch\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_minimal_write_roundtrip(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"scene\":0,"
"\"scenes\":[{\"nodes\":[0]}],"
"\"nodes\":[{\"name\":\"root\"}]}";
tg3_model model;
tg3_model roundtrip;
tg3_error_stack errors;
tg3_parse_options parse_opts;
tg3_write_options write_opts;
tg3_error_code err;
uint8_t *out = NULL;
uint64_t out_size = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&parse_opts);
tg3_write_options_init(&write_opts);
err = tg3_parse_auto(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0,
&parse_opts);
if (err != TG3_OK) {
fprintf(stderr, "initial parse failed: %d\n", (int)err);
tg3_error_stack_free(&errors);
return 0;
}
err = tg3_write_to_memory(&model, &errors, &out, &out_size, &write_opts);
if (err != TG3_OK || !out || out_size == 0) {
fprintf(stderr, "tg3_write_to_memory failed: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (!mem_contains(out, out_size, "\"asset\"") ||
!mem_contains(out, out_size, "\"root\"")) {
fprintf(stderr, "serialized JSON missing expected fields\n");
tg3_write_free(out, &write_opts);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
err = tg3_parse_auto(&roundtrip, &errors, out, out_size, "", 0, &parse_opts);
tg3_write_free(out, &write_opts);
if (err != TG3_OK) {
fprintf(stderr, "roundtrip parse failed: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (roundtrip.default_scene != 0 || roundtrip.nodes_count != 1 ||
!roundtrip.nodes || !roundtrip.nodes[0].name.data ||
!tg3_str_equals_cstr(roundtrip.nodes[0].name, "root")) {
fprintf(stderr, "roundtrip model mismatch\n");
tg3_model_free(&roundtrip);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&roundtrip);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_parse_file_failure_initializes_model(void) {
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
memset(&model, 0xA5, sizeof(model));
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
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;
}
if (model.default_scene != -1) {
fprintf(stderr, "tg3_parse_file did not initialize model on failure\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_non_object_root_rejected(void) {
static const uint8_t json[] = "\"not an object\"";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_JSON_PARSE) {
fprintf(stderr, "non-object root returned unexpected error: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (model.default_scene != -1) {
fprintf(stderr, "non-object root left model in unexpected state\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_huge_integer_field_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},\"scene\":6.66667e+70}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_JSON_PARSE) {
fprintf(stderr, "huge integer-like field returned unexpected error: %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
/* ===== Security regression tests ============================================ */
/* fs read_file callback that records calls into *(int *)user_data and never
* succeeds — used to verify path-traversal URIs never reach the filesystem. */
static int32_t recording_read_file(uint8_t **out_data, uint64_t *out_size,
const char *path, uint32_t path_len,
void *user_data) {
(void)out_data; (void)out_size; (void)path; (void)path_len;
if (user_data) *(int *)user_data += 1;
return 0;
}
static int check_path_traversal_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"uri\":\"../../etc/passwd\",\"byteLength\":4}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
int fs_calls = 0;
uint32_t i;
int saw = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.fs.read_file = recording_read_file;
opts.fs.user_data = &fs_calls;
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1),
"/some/base", 10, &opts);
if (err == TG3_OK) {
fprintf(stderr, "path traversal NOT rejected\n");
goto fail;
}
if (fs_calls != 0) {
fprintf(stderr, "fs.read_file called %d times for traversal URI\n", fs_calls);
goto fail;
}
for (i = 0; i < errors.count; ++i) {
if (errors.entries[i].code == TG3_ERR_INVALID_VALUE) saw = 1;
}
if (!saw) {
fprintf(stderr, "expected TG3_ERR_INVALID_VALUE on traversal\n");
goto fail;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
fail:
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
static int check_absolute_uri_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"uri\":\"/etc/passwd\",\"byteLength\":4}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
int fs_calls = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.fs.read_file = recording_read_file;
opts.fs.user_data = &fs_calls;
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1),
"/base", 5, &opts);
if (err == TG3_OK || fs_calls != 0) {
fprintf(stderr, "absolute URI not rejected (rc=%d, fs_calls=%d)\n",
(int)err, fs_calls);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_negative_byte_stride_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4,\"byteStride\":-1}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err == TG3_OK) {
fprintf(stderr, "negative byteStride NOT rejected\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_oob_index_rejected(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4}],"
"\"accessors\":[{\"bufferView\":1000000,\"componentType\":5121,"
"\"count\":1,\"type\":\"SCALAR\"}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_INVALID_INDEX) {
fprintf(stderr, "OOB index expected TG3_ERR_INVALID_INDEX, got %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_oob_index_opt_in(void) {
/* When validate_indices=0, parse should accept the same out-of-range index. */
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4}],"
"\"accessors\":[{\"bufferView\":1000000,\"componentType\":5121,"
"\"count\":1,\"type\":\"SCALAR\"}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.validate_indices = 0;
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_OK) {
fprintf(stderr, "validate_indices=0 should accept OOB index, got %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
if (model.accessors_count != 1 || model.accessors[0].buffer_view != 1000000) {
fprintf(stderr, "OOB index not preserved when validation off\n");
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_extension_index_oob_rejected(void) {
/* MSFT_lod and KHR_audio index fields must be validated when
* validate_indices=1, otherwise downstream consumers can read OOB. */
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"nodes\":[{\"extensions\":{\"MSFT_lod\":{\"ids\":[99999]}}}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_ERR_INVALID_INDEX) {
fprintf(stderr, "MSFT_lod OOB index expected TG3_ERR_INVALID_INDEX, got %d\n", (int)err);
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
}
static int check_error_messages_survive_parse_failure(void) {
/* Regression: parse failure must not invalidate arena-allocated error
* message strings on the user's tg3_error_stack before model_free. */
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},"
"\"buffers\":[{\"byteLength\":4}],"
"\"bufferViews\":[{\"buffer\":0,\"byteLength\":4,\"byteStride\":-1}]}";
tg3_model model;
tg3_error_stack errors;
tg3_parse_options opts;
tg3_error_code err;
uint32_t i;
int seen_stride_msg = 0;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
err = tg3_parse(&model, &errors, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err == TG3_OK) goto fail;
for (i = 0; i < errors.count; ++i) {
const char *m = errors.entries[i].message;
if (m && strstr(m, "byteStride")) seen_stride_msg = 1;
}
if (!seen_stride_msg) {
fprintf(stderr, "byteStride error message missing or unreadable after parse\n");
goto fail;
}
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 1;
fail:
tg3_model_free(&model);
tg3_error_stack_free(&errors);
return 0;
}
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;
}
if (!check_minimal_write_roundtrip()) {
return 1;
}
if (!check_parse_file_failure_initializes_model()) {
return 1;
}
if (!check_non_object_root_rejected()) {
return 1;
}
if (!check_huge_integer_field_rejected()) {
return 1;
}
if (!check_path_traversal_rejected()) {
return 1;
}
if (!check_absolute_uri_rejected()) {
return 1;
}
if (!check_negative_byte_stride_rejected()) {
return 1;
}
if (!check_oob_index_rejected()) {
return 1;
}
if (!check_oob_index_opt_in()) {
return 1;
}
if (!check_extension_index_oob_rejected()) {
return 1;
}
if (!check_error_messages_survive_parse_failure()) {
return 1;
}
return 0;
}

559
tests/tester_v3_c_v1port.c Normal file
View File

@@ -0,0 +1,559 @@
/*
* tester_v3_c_v1port.c — Parse/load tests ported from tester.cc (v1) to the
* pure-C v3 runtime. Each PORT_CASE corresponds to a TEST_CASE in tester.cc;
* the comment marks the original test name.
*
* Skipped from tester.cc (require v3 writer or v1-internal helpers, out of
* scope for this port):
* serialize-empty-material, serialize-empty-node, serialize-light-index,
* serialize-empty-scene, serialize-node-emitter, serialize-lods,
* serialize-const-image, serialize-image-callback, serialize-image-failure,
* write-image-issue, empty-images-not-written, empty-bin-buffer,
* parse-integer, parse-unsigned, parse-integer-array,
* expandpath-utf-8, cj-float32-long-integer
*
* Build (run from tests/):
* make tester_v3_c_v1port
* Run:
* ./tester_v3_c_v1port
*/
#include "tiny_gltf_v3.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* --- thin helpers ---------------------------------------------------------- */
static int tg3_str_eq(const tg3_str *s, const char *cstr) {
size_t n = strlen(cstr);
return s->data && s->len == (uint32_t)n &&
memcmp(s->data, cstr, n) == 0;
}
static int tg3_str_contains(const tg3_str *s, char ch) {
uint32_t i;
if (!s->data) return 0;
for (i = 0; i < s->len; ++i) {
if (s->data[i] == ch) return 1;
}
return 0;
}
static int has_extension(const tg3_extras_ext *e, const char *name) {
uint32_t i;
if (!e || !e->extensions) return 0;
for (i = 0; i < e->extensions_count; ++i) {
if (tg3_str_eq(&e->extensions[i].name, name)) return 1;
}
return 0;
}
static const tg3_extension *find_extension(const tg3_extras_ext *e, const char *name) {
uint32_t i;
if (!e || !e->extensions) return NULL;
for (i = 0; i < e->extensions_count; ++i) {
if (tg3_str_eq(&e->extensions[i].name, name)) return &e->extensions[i];
}
return NULL;
}
static const tg3_value *value_get(const tg3_value *v, const char *key) {
uint32_t i;
if (!v || v->type != TG3_VALUE_OBJECT || !v->object_data) return NULL;
for (i = 0; i < v->object_count; ++i) {
if (tg3_str_eq(&v->object_data[i].key, key)) {
return &v->object_data[i].value;
}
}
return NULL;
}
/* Read a glTF/GLB file into a heap buffer; returns 1 on success. */
static int slurp(const char *path, uint8_t **out, size_t *out_len) {
FILE *fp = fopen(path, "rb");
long sz;
size_t got;
uint8_t *buf;
if (!fp) return 0;
if (fseek(fp, 0, SEEK_END) != 0 || (sz = ftell(fp)) < 0 ||
fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); return 0; }
buf = (uint8_t *)malloc((size_t)sz);
if (!buf) { fclose(fp); return 0; }
got = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
if (got != (size_t)sz) { free(buf); return 0; }
*out = buf;
*out_len = (size_t)sz;
return 1;
}
static const char *base_dir_of(const char *path, uint32_t *out_len) {
const char *slash = strrchr(path, '/');
if (slash) {
*out_len = (uint32_t)(slash - path);
} else {
*out_len = 0;
}
return path;
}
static int parse_path(tg3_model *m, tg3_error_stack *e,
tg3_parse_options *opts,
const char *path, tg3_error_code *err_out) {
uint8_t *buf = NULL;
size_t sz = 0;
uint32_t base_len = 0;
const char *base_dir;
if (!slurp(path, &buf, &sz)) {
fprintf(stderr, " slurp failed: %s\n", path);
return 0;
}
base_dir = base_dir_of(path, &base_len);
*err_out = tg3_parse_auto(m, e, buf, (uint64_t)sz,
base_dir, base_len, opts);
free(buf);
return 1;
}
static int errstack_contains(const tg3_error_stack *e, const char *needle) {
uint32_t i;
size_t nl = strlen(needle);
for (i = 0; i < e->count; ++i) {
const char *m = e->entries[i].message;
if (m && strstr(m, needle) != NULL) return 1;
(void)nl;
}
return 0;
}
/* --- test cases ------------------------------------------------------------ */
#define PASS_OR_RET(label) do { \
fprintf(stdout, " [PASS] %s\n", label); \
return 1; \
} while (0)
#define FAIL(...) do { \
fprintf(stderr, " [FAIL] "); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
goto fail; \
} while (0)
/* TEST_CASE("parse-error", "[parse]") */
static int t_parse_error(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
static const uint8_t bad[] = "bora";
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
err = tg3_parse(&m, &e, bad, (uint64_t)(sizeof(bad) - 1), "", 0, &opts);
if (err == TG3_OK) FAIL("expected parse failure on garbage input");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("parse-error");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("datauri-in-glb", "[issue-79]") */
static int t_datauri_in_glb(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "../models/box01.glb", &err)) goto fail;
if (err != TG3_OK) FAIL("box01.glb load failed err=%d", (int)err);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("datauri-in-glb");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("extension-with-empty-object", "[issue-97]") */
static int t_extension_empty_object(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "../models/Extensions-issue97/test.gltf", &err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.extensions_used_count != 1)
FAIL("extensionsUsed.size %u != 1", m.extensions_used_count);
if (!tg3_str_eq(&m.extensions_used[0], "VENDOR_material_some_ext"))
FAIL("unexpected extensionsUsed[0]");
if (m.materials_count != 1) FAIL("materials.size %u != 1", m.materials_count);
if (!has_extension(&m.materials[0].ext, "VENDOR_material_some_ext"))
FAIL("material missing VENDOR_material_some_ext extension");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("extension-with-empty-object");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("extension-overwrite", "[issue-261]") */
static int t_extension_overwrite(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
uint32_t i;
int has_lights = 0;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/Extensions-overwrite-issue261/issue-261.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.extensions_used_count != 3)
FAIL("extensionsUsed.size %u != 3", m.extensions_used_count);
for (i = 0; i < m.extensions_used_count; ++i) {
if (tg3_str_eq(&m.extensions_used[i], "KHR_lights_punctual")) {
has_lights = 1; break;
}
}
if (!has_lights) FAIL("KHR_lights_punctual missing in extensionsUsed");
if (!has_extension(&m.ext, "NV_MDL")) FAIL("model.extensions missing NV_MDL");
if (!has_extension(&m.ext, "KHR_lights_punctual"))
FAIL("model.extensions missing KHR_lights_punctual");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("extension-overwrite");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("invalid-primitive-indices", "[bounds-checking]") */
static int t_invalid_primitive_indices(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/invalid-primitive-indices.gltf",
&err)) goto fail;
if (err == TG3_OK)
FAIL("invalid-primitive-indices unexpectedly succeeded");
/* v3 reports TG3_ERR_INVALID_INDEX from validate_indices. v1 reports
* "primitive indices accessor out of bounds" at parse time; either is
* acceptable as long as the parser does not crash and rejects the model. */
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("invalid-primitive-indices");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("invalid-buffer-view-index", "[bounds-checking]") */
static int t_invalid_buffer_view_index(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/invalid-buffer-view-index.gltf",
&err)) goto fail;
if (err == TG3_OK) FAIL("invalid-buffer-view-index unexpectedly succeeded");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("invalid-buffer-view-index");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("invalid-buffer-index", "[bounds-checking]") */
static int t_invalid_buffer_index(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/invalid-buffer-index.gltf",
&err)) goto fail;
if (err == TG3_OK) FAIL("invalid-buffer-index unexpectedly succeeded");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("invalid-buffer-index");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("glb-invalid-length", "[bounds-checking]") */
static int t_glb_invalid_length(void) {
/* 'glTF' magic, version=2, length=0x0000666c (way larger than provided
* data), JSON chunk with empty {}. */
static const unsigned char glb[] = "glTF"
"\x02\x00\x00\x00" "\x6c\x66\x00\x00"
"\x02\x00\x00\x00" "\x4a\x53\x4f\x4e{}";
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
err = tg3_parse_glb(&m, &e, glb, (uint64_t)(sizeof(glb) - 1), "", 0, &opts);
if (err == TG3_OK) FAIL("invalid-length GLB unexpectedly accepted");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("glb-invalid-length");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("integer-out-of-bounds", "[bounds-checking]") */
static int t_integer_out_of_bounds(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/BoundsChecking/integer-out-of-bounds.gltf",
&err)) goto fail;
if (err == TG3_OK) FAIL("integer-out-of-bounds unexpectedly succeeded");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("integer-out-of-bounds");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("pbr-khr-texture-transform", "[material]") */
static int t_pbr_khr_texture_transform(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
const tg3_extension *ext;
const tg3_value *scale, *s0, *s1;
double v0, v1;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/Cube-texture-ext/Cube-textransform.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.materials_count != 2) FAIL("materials.size %u != 2", m.materials_count);
ext = find_extension(&m.materials[0].emissive_texture.ext,
"KHR_texture_transform");
if (!ext) FAIL("KHR_texture_transform missing on emissiveTexture");
if (ext->value.type != TG3_VALUE_OBJECT)
FAIL("KHR_texture_transform value not an object");
scale = value_get(&ext->value, "scale");
if (!scale || scale->type != TG3_VALUE_ARRAY || scale->array_count != 2)
FAIL("scale not an array of 2");
s0 = &scale->array_data[0];
s1 = &scale->array_data[1];
v0 = (s0->type == TG3_VALUE_INT) ? (double)s0->int_val : s0->real_val;
v1 = (s1->type == TG3_VALUE_INT) ? (double)s1->int_val : s1->real_val;
if (v0 != 1.0) FAIL("scale[0] %g != 1.0", v0);
if (v1 != -1.0) FAIL("scale[1] %g != -1.0", v1);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("pbr-khr-texture-transform");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("image-uri-spaces", "[issue-236]") */
static int t_image_uri_spaces(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
/* Single-spaces variant. */
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/CubeImageUriSpaces/CubeImageUriSpaces.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("CubeImageUriSpaces load failed err=%d", (int)err);
if (m.images_count != 1) FAIL("images.size %u != 1", m.images_count);
if (!tg3_str_contains(&m.images[0].uri, ' '))
FAIL("image.uri does not contain a space");
tg3_model_free(&m); tg3_error_stack_free(&e);
/* Multiple-spaces variant. */
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/CubeImageUriSpaces/CubeImageUriMultipleSpaces.gltf",
&err)) goto fail2;
if (err != TG3_OK) FAIL("MultipleSpaces load failed err=%d", (int)err);
if (m.images_count != 1) FAIL("images.size %u != 1", m.images_count);
if (m.images[0].uri.len < 2 || m.images[0].uri.data[0] != ' ')
FAIL("image.uri does not start with a space");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("image-uri-spaces");
fail2:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("empty-skeleton-id", "[issue-321]") */
static int t_empty_skeleton_id(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/regression/unassigned-skeleton.gltf",
&err)) goto fail;
if (err != TG3_OK) FAIL("load failed err=%d", (int)err);
if (m.skins_count != 1) FAIL("skins.size %u != 1", m.skins_count);
if (m.skins[0].skeleton != -1)
FAIL("skin.skeleton %d != -1", m.skins[0].skeleton);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("empty-skeleton-id");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("filesize-check", "[issue-416]") */
static int t_filesize_check(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
opts.max_external_file_size = 10; /* 10 bytes — texture image will exceed */
if (!parse_path(&m, &e, &opts, "../models/Cube/Cube.gltf", &err)) goto fail;
if (err == TG3_OK)
FAIL("expected load failure due to oversized external file");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("filesize-check");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("load-issue-416-model", "[issue-416]") */
static int t_load_issue_416_model(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "issue-416.gltf", &err)) goto fail;
/* External file is missing, but the parser should still accept the glTF
* structurally. v1 returns true; v3 returns TG3_OK or a non-fatal error
* about the missing file. */
(void)err; /* tolerate either outcome */
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("load-issue-416-model");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("zero-sized-bin-chunk-glb", "[issue-440]") */
static int t_zero_sized_bin_chunk_glb(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts,
"../models/regression/zero-sized-bin-chunk-issue-440.glb",
&err)) goto fail;
if (err != TG3_OK) FAIL("zero-sized-bin-chunk failed err=%d", (int)err);
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("zero-sized-bin-chunk-glb");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("images-as-is", "[issue-487]") */
static int t_images_as_is(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
uint32_t i;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
opts.images_as_is = 1;
if (!parse_path(&m, &e, &opts, "../models/Cube/Cube.gltf", &err)) goto fail;
if (err != TG3_OK) FAIL("Cube load failed err=%d", (int)err);
if (m.images_count == 0) FAIL("no images parsed");
for (i = 0; i < m.images_count; ++i) {
if (m.images[i].as_is != 1) FAIL("image[%u].as_is != 1", i);
if (m.images[i].uri.len == 0) FAIL("image[%u].uri empty", i);
}
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("images-as-is");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("inverse-bind-matrices-optional", "[issue-492]") */
static int t_ibm_optional(void) {
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
if (!parse_path(&m, &e, &opts, "issue-492.glb", &err)) goto fail;
if (err != TG3_OK) FAIL("issue-492.glb load failed err=%d", (int)err);
/* No "error" entries (warnings are not errors). */
if (errstack_contains(&e, "error"))
FAIL("unexpected error message present");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("inverse-bind-matrices-optional");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* TEST_CASE("default-material", "[issue-459]") — black-box parse of a
* minimal model with a default material; verify v3 populates the same
* default-PBR field values v1 documents. */
static int t_default_material(void) {
static const uint8_t json[] =
"{\"asset\":{\"version\":\"2.0\"},\"materials\":[{}]}";
tg3_model m; tg3_error_stack e; tg3_parse_options opts;
tg3_error_code err;
const tg3_material *mat;
tg3_error_stack_init(&e); tg3_parse_options_init(&opts);
err = tg3_parse(&m, &e, json, (uint64_t)(sizeof(json) - 1), "", 0, &opts);
if (err != TG3_OK) FAIL("minimal default-material parse failed err=%d", (int)err);
if (m.materials_count != 1) FAIL("materials.size %u != 1", m.materials_count);
mat = &m.materials[0];
if (!tg3_str_eq(&mat->alpha_mode, "OPAQUE")) FAIL("alpha_mode default mismatch");
if (mat->alpha_cutoff != 0.5) FAIL("alpha_cutoff default %g != 0.5", mat->alpha_cutoff);
if (mat->double_sided != 0) FAIL("double_sided default != 0");
if (mat->emissive_factor[0] != 0.0 ||
mat->emissive_factor[1] != 0.0 ||
mat->emissive_factor[2] != 0.0) FAIL("emissive_factor default mismatch");
if (mat->pbr_metallic_roughness.base_color_factor[0] != 1.0 ||
mat->pbr_metallic_roughness.base_color_factor[1] != 1.0 ||
mat->pbr_metallic_roughness.base_color_factor[2] != 1.0 ||
mat->pbr_metallic_roughness.base_color_factor[3] != 1.0)
FAIL("base_color_factor default mismatch");
if (mat->pbr_metallic_roughness.metallic_factor != 1.0)
FAIL("metallic_factor default %g != 1.0",
mat->pbr_metallic_roughness.metallic_factor);
if (mat->pbr_metallic_roughness.roughness_factor != 1.0)
FAIL("roughness_factor default %g != 1.0",
mat->pbr_metallic_roughness.roughness_factor);
if (mat->normal_texture.index != -1) FAIL("normal_texture.index != -1");
if (mat->occlusion_texture.index != -1) FAIL("occlusion_texture.index != -1");
if (mat->emissive_texture.index != -1) FAIL("emissive_texture.index != -1");
tg3_model_free(&m); tg3_error_stack_free(&e);
PASS_OR_RET("default-material");
fail:
tg3_model_free(&m); tg3_error_stack_free(&e);
return 0;
}
/* --- main ------------------------------------------------------------------ */
int main(void) {
struct { const char *name; int (*fn)(void); } cases[] = {
{"parse-error", t_parse_error},
{"datauri-in-glb", t_datauri_in_glb},
{"extension-with-empty-object", t_extension_empty_object},
{"extension-overwrite", t_extension_overwrite},
{"invalid-primitive-indices", t_invalid_primitive_indices},
{"invalid-buffer-view-index", t_invalid_buffer_view_index},
{"invalid-buffer-index", t_invalid_buffer_index},
{"glb-invalid-length", t_glb_invalid_length},
{"integer-out-of-bounds", t_integer_out_of_bounds},
{"pbr-khr-texture-transform", t_pbr_khr_texture_transform},
{"image-uri-spaces", t_image_uri_spaces},
{"empty-skeleton-id", t_empty_skeleton_id},
{"filesize-check", t_filesize_check},
{"load-issue-416-model", t_load_issue_416_model},
{"zero-sized-bin-chunk-glb", t_zero_sized_bin_chunk_glb},
{"images-as-is", t_images_as_is},
{"inverse-bind-matrices-optional", t_ibm_optional},
{"default-material", t_default_material},
};
size_t total = sizeof(cases) / sizeof(cases[0]);
size_t passed = 0;
size_t i;
for (i = 0; i < total; ++i) {
fprintf(stdout, "RUN %s\n", cases[i].name);
if (cases[i].fn()) ++passed;
}
fprintf(stdout, "\n=== v1 port: %zu/%zu passed ===\n", passed, total);
return passed == total ? 0 : 1;
}

View File

@@ -1,19 +1,23 @@
# tests/v3/fuzzer/Makefile — Build libFuzzer harness for tinygltf v3
# tests/v3/fuzzer/Makefile — Build libFuzzer harnesses for tinygltf v3
#
# Requires: clang++ with libFuzzer support
# Requires: clang/clang++ with libFuzzer support
#
# Targets:
# make — build fuzzer with ASan + UBSan
# make run — run fuzzer with default settings
# make — build both harnesses with ASan + UBSan
# make run — run the dedicated pure-C v3 harness
# make run-cpp — run the legacy header-implementation harness
# make seed — generate seed corpus from test models
# make clean — remove binaries and corpus
CC = clang
CXX = clang++
CCFLAGS = -g -O1 -std=c11
CXXFLAGS = -g -O1 -std=c++17 -fno-rtti -fno-exceptions
SANITIZE = -fsanitize=fuzzer,address,undefined
INCLUDES = -I../../..
FUZZER = fuzz_gltf_v3
FUZZER_C = fuzz_gltf_v3_c
CORPUS = corpus
ARTIFACTS = artifacts
@@ -21,16 +25,28 @@ ARTIFACTS = artifacts
MAX_LEN ?= 65536
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
MAX_TIME ?= 0
FUZZ_ENV ?= LSAN_OPTIONS=detect_leaks=0
.PHONY: all run seed clean
.PHONY: all run run-cpp seed clean
all: $(FUZZER)
all: $(FUZZER) $(FUZZER_C)
$(FUZZER): fuzz_gltf_v3.cc ../../../tiny_gltf_v3.h ../../../tinygltf_json.h
$(FUZZER): fuzz_gltf_v3.cc ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h
$(CXX) $(CXXFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $<
run: $(FUZZER) | $(CORPUS) $(ARTIFACTS)
./$(FUZZER) $(CORPUS) \
$(FUZZER_C): fuzz_gltf_v3_c.c ../../../tiny_gltf_v3.h ../../../tiny_gltf_v3.c ../../../tinygltf_json_c.h
$(CC) $(CCFLAGS) $(SANITIZE) $(INCLUDES) -o $@ $< ../../../tiny_gltf_v3.c
run: $(FUZZER_C) | $(CORPUS) $(ARTIFACTS)
$(FUZZ_ENV) ./$(FUZZER_C) $(CORPUS) \
-artifact_prefix=$(ARTIFACTS)/ \
-max_len=$(MAX_LEN) \
-jobs=$(JOBS) \
-workers=$(JOBS) \
$(if $(filter-out 0,$(MAX_TIME)),-max_total_time=$(MAX_TIME))
run-cpp: $(FUZZER) | $(CORPUS) $(ARTIFACTS)
$(FUZZ_ENV) ./$(FUZZER) $(CORPUS) \
-artifact_prefix=$(ARTIFACTS)/ \
-max_len=$(MAX_LEN) \
-jobs=$(JOBS) \
@@ -63,5 +79,5 @@ $(ARTIFACTS):
mkdir -p $(ARTIFACTS)
clean:
rm -f $(FUZZER)
rm -f $(FUZZER) $(FUZZER_C)
rm -rf $(CORPUS) $(ARTIFACTS)

View File

@@ -0,0 +1,39 @@
#include "tiny_gltf_v3.h"
#include <stddef.h>
#include <stdint.h>
static const uint64_t FUZZ_MEMORY_BUDGET = 64ULL * 1024 * 1024;
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;
tg3_error_stack_init(&errors);
tg3_parse_options_init(&opts);
opts.memory.memory_budget = FUZZ_MEMORY_BUDGET;
opts.parse_float32 = parse_float32;
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) {
if (size == 0) return 0;
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;
}

View File

@@ -0,0 +1,8 @@
{
"asset": {"version": "2.0"},
"buffers": [{"byteLength": 4}],
"bufferViews": [
{"buffer": 0, "byteOffset": 0, "byteLength": 4, "byteStride": -1}
],
"scenes": [{"nodes": []}]
}

View File

@@ -0,0 +1,9 @@
{
"asset": {"version": "2.0"},
"buffers": [{"byteLength": 4}],
"bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 4}],
"accessors": [
{"bufferView": 1000000, "byteOffset": 0, "componentType": 5121, "count": 1, "type": "SCALAR"}
],
"scenes": [{"nodes": []}]
}

View File

@@ -0,0 +1,19 @@
{
"asset": {"version": "2.0"},
"nodes": [
{"extensions": {"KHR_audio": {"emitter": 2147483647},
"MSFT_lod": {"ids": [-1, 9999]}}}
],
"materials": [
{"extensions": {"MSFT_lod": {"ids": [-5]}}}
],
"scenes": [
{"extensions": {"KHR_audio": {"emitters": [12345]}}}
],
"extensions": {
"KHR_audio": {
"sources": [{"bufferView": -1}],
"emitters": [{"source": 99999}]
}
}
}

View File

@@ -0,0 +1,14 @@
{
"asset": {"version": "2.0"},
"buffers": [
{"uri": "../../../../../../../../tmp/tg3-poc-secret.txt", "byteLength": 32}
],
"bufferViews": [
{"buffer": 0, "byteOffset": 0, "byteLength": 32}
],
"accessors": [
{"bufferView": 0, "byteOffset": 0, "componentType": 5121, "count": 32, "type": "SCALAR"}
],
"scenes": [{"nodes": []}],
"scene": 0
}

3278
tiny_gltf_v3.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
/*
* tiny_gltf_v3.h - Header-only C glTF 2.0 loader and writer (v3)
* tiny_gltf_v3.h - C-first glTF 2.0 loader and writer API (v3)
*
* The MIT License (MIT)
* Copyright (c) 2026 - Present: Syoyo Fujita
@@ -27,7 +27,7 @@
* Version: v3.0.0-alpha
*
* Ground-up C-centric API rewrite of tinygltf.
* Uses tinygltf_json.h as the sole JSON backend.
* The default runtime implementation lives in tiny_gltf_v3.c.
*
* Key differences from v2:
* - Pure C POD structs (no STL containers in public API)
@@ -37,6 +37,37 @@
* - Streaming parse/write via callbacks
* - No RTTI, no exceptions required
* - C++20 coroutine facade (optional)
*
* Security considerations (read before processing untrusted glTF):
*
* 1. External URI loading. When TINYGLTF3_ENABLE_FS is defined and no custom
* tg3_fs_callbacks are supplied, the parser opens external buffer/image
* URIs through the libc default fopen(). The parser rejects URIs that
* contain '..' segments, leading '/' or '\\', Windows drive prefixes
* (e.g. "C:"), or NUL bytes — but it does NOT chroot or canonicalize the
* result. Production callers SHOULD provide a tg3_fs_callbacks with a
* read_file callback that confines reads to a known directory (e.g. via
* openat(AT_FDCWD, path, O_NOFOLLOW) plus a realpath() prefix check) when
* the input glTF is attacker-controlled.
*
* 2. Index validation. Many glTF fields are integer indices into model
* arrays (accessor.bufferView, primitive.material, scene.nodes[], etc.).
* With opts.validate_indices = 1 (the default) the parser rejects every
* out-of-range index after the structural parse and returns
* TG3_ERR_INVALID_INDEX. Set opts.validate_indices = 0 only when you
* need to round-trip raw or extension data and have your own validator.
*
* 3. Image decoding. The parser does not decode image bytes by default.
* Set opts.images_as_is = 1 (already the safe default for untrusted
* input) to skip any decoder and store raw bytes only.
*
* 4. Memory budget. The arena is capped at TINYGLTF3_MAX_MEMORY_BYTES
* (1 GB by default; configurable per-parse via tg3_memory_config).
* The parser returns TG3_ERR_OUT_OF_MEMORY rather than overcommitting.
*
* 5. Error message lifetime. Error strings on tg3_error_stack are
* arena-allocated and remain valid until tg3_model_free() is called.
* Read or copy them BEFORE freeing the model.
*/
#ifndef TINY_GLTF_V3_H_
@@ -46,7 +77,7 @@
* Section 2: Configuration Macros
* ====================================================================== */
/* Build mode: define in ONE C++ translation unit (.cpp) */
/* Legacy single-translation-unit build mode: define in ONE C or C++ file */
/* #define TINYGLTF3_IMPLEMENTATION */
/* Opt-in features (OFF by default) */
@@ -914,6 +945,10 @@ typedef struct tg3_parse_options {
* (breaks strict double-precision conformance
* but sufficient for glTF data which is
* typically single-precision anyway) */
int32_t validate_indices; /* 1 = reject out-of-range index fields
* after parse so naive consumers cannot
* dereference attacker-controlled indices.
* Default: 1. Set to 0 to skip (raw mode). */
uint64_t max_external_file_size; /* 0 = no limit */
} tg3_parse_options;
@@ -1219,6 +1254,11 @@ ParseGenerator tg3_parse_coro(
* ====================================================================== */
#ifdef TINYGLTF3_IMPLEMENTATION
#define TINYGLTF3_SOURCE_INCLUDED_FROM_HEADER 1
#include "tiny_gltf_v3.c"
#undef TINYGLTF3_SOURCE_INCLUDED_FROM_HEADER
#if 0
#if !defined(__cplusplus)
#error "TINYGLTF3_IMPLEMENTATION requires a C++ translation unit (compile as .cpp)"
@@ -4431,6 +4471,7 @@ TINYGLTF3_API void tg3_writer_destroy(tg3_writer *w) {
delete w;
}
#endif /* legacy header-only v3 implementation */
#endif /* TINYGLTF3_IMPLEMENTATION */
#endif /* TINY_GLTF_V3_H_ */

1024
tinygltf_json_c.h Normal file

File diff suppressed because it is too large Load Diff