mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-06-08 19:23:50 +00:00
Compare commits
15 Commits
copilot/ad
...
v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d31c16e333 | ||
|
|
36c9643981 | ||
|
|
c9b3b9c644 | ||
|
|
d20c9298e5 | ||
|
|
70a6a0c0ea | ||
|
|
cc52d8057b | ||
|
|
3a2f149458 | ||
|
|
a8fb48fa91 | ||
|
|
188d7b257b | ||
|
|
7f736d19db | ||
|
|
af09ec3405 | ||
|
|
85441bbe19 | ||
|
|
a18f41142f | ||
|
|
d8f3bd93f7 | ||
|
|
bd6db55b70 |
45
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
45
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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.
|
||||
25
.github/workflows/c-cpp.yml
vendored
25
.github/workflows/c-cpp.yml
vendored
@@ -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
|
||||
|
||||
211
.github/workflows/ci.yml
vendored
211
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/mingw-w64-msys2.yml
vendored
2
.github/workflows/mingw-w64-msys2.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
83
README.md
83
README.md
@@ -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
4
SECURITY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Security Policy
|
||||
|
||||
This project manages CVE assignments exclusively through
|
||||
GitHub Security Advisories.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
172
test_runner.py
172
test_runner.py
@@ -2,63 +2,167 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
## Simple test runner.
|
||||
## Cross-version verifier: parses each sample model with the mature v1
|
||||
## (loader_example) and the new v3 C tester, then compares both the COUNTS
|
||||
## summary line and the structured DIGEST block both binaries emit.
|
||||
## v1 is the ground truth.
|
||||
|
||||
# -- config -----------------------
|
||||
|
||||
# Absolute path pointing to your cloned git repo of https://github.com/KhronosGroup/glTF-Sample-Models
|
||||
sample_model_dir = "/home/syoyo/work/glTF-Sample-Models"
|
||||
sample_model_dir = "/mnt/nfs/syoyo/glTF-Sample-Models"
|
||||
base_model_dir = os.path.join(sample_model_dir, "2.0")
|
||||
|
||||
# Include `glTF-Draco` when you build `loader_example` with draco support.
|
||||
kinds = [ "glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"]
|
||||
v1_bin = "./loader_example"
|
||||
v3_bin = "./tests/tester_v3_c"
|
||||
|
||||
kinds = ["glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"]
|
||||
# ---------------------------------
|
||||
|
||||
failed = []
|
||||
success = []
|
||||
COUNTS_RE = re.compile(r"^COUNTS\s+(.*)$", re.MULTILINE)
|
||||
DIGEST_RE = re.compile(r"^DIGEST_BEGIN\n(.*?)^DIGEST_END$", re.MULTILINE | re.DOTALL)
|
||||
|
||||
def run(filename):
|
||||
|
||||
def parse_counts(output):
|
||||
m = COUNTS_RE.search(output)
|
||||
if not m:
|
||||
return None
|
||||
counts = {}
|
||||
for tok in m.group(1).split():
|
||||
if "=" not in tok:
|
||||
continue
|
||||
k, v = tok.split("=", 1)
|
||||
counts[k] = int(v)
|
||||
return counts
|
||||
|
||||
|
||||
def parse_digest(output):
|
||||
m = DIGEST_RE.search(output)
|
||||
if not m:
|
||||
return None
|
||||
return [line for line in m.group(1).splitlines() if line]
|
||||
|
||||
|
||||
def run_binary(binary, filename):
|
||||
p = subprocess.Popen(
|
||||
[binary, filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
out, err = p.communicate()
|
||||
return p.returncode, out.decode("utf-8", "replace"), err.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def diff_digests(v1, v3, max_lines=20):
|
||||
"""Return a short summary of differences between two digest line lists."""
|
||||
diffs = []
|
||||
n = max(len(v1), len(v3))
|
||||
for i in range(n):
|
||||
a = v1[i] if i < len(v1) else "<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()
|
||||
|
||||
@@ -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
772
tests/tester_v3_c.c
Normal 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
559
tests/tester_v3_c_v1port.c
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
39
tests/v3/fuzzer/fuzz_gltf_v3_c.c
Normal file
39
tests/v3/fuzzer/fuzz_gltf_v3_c.c
Normal 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;
|
||||
}
|
||||
8
tests/v3/security/negstride.gltf
Normal file
8
tests/v3/security/negstride.gltf
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"asset": {"version": "2.0"},
|
||||
"buffers": [{"byteLength": 4}],
|
||||
"bufferViews": [
|
||||
{"buffer": 0, "byteOffset": 0, "byteLength": 4, "byteStride": -1}
|
||||
],
|
||||
"scenes": [{"nodes": []}]
|
||||
}
|
||||
9
tests/v3/security/oob_buffer_view.gltf
Normal file
9
tests/v3/security/oob_buffer_view.gltf
Normal 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": []}]
|
||||
}
|
||||
19
tests/v3/security/oob_extension_indices.gltf
Normal file
19
tests/v3/security/oob_extension_indices.gltf
Normal 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}]
|
||||
}
|
||||
}
|
||||
}
|
||||
14
tests/v3/security/traversal_relative.gltf
Normal file
14
tests/v3/security/traversal_relative.gltf
Normal 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
3278
tiny_gltf_v3.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
1024
tinygltf_json_c.h
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user