mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-06-08 19:23:50 +00:00
Compare commits
58 Commits
copilot/su
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8e41ae694 | ||
|
|
3e283c05d5 | ||
|
|
99ec16fbca | ||
|
|
fd365dddab | ||
|
|
a4b5752b1b | ||
|
|
2ff44b903c | ||
|
|
381daedaba | ||
|
|
34a166cdac | ||
|
|
0e3043f3e9 | ||
|
|
d31c16e333 | ||
|
|
36c9643981 | ||
|
|
c9b3b9c644 | ||
|
|
d20c9298e5 | ||
|
|
70a6a0c0ea | ||
|
|
cc52d8057b | ||
|
|
3a2f149458 | ||
|
|
a8fb48fa91 | ||
|
|
188d7b257b | ||
|
|
7f736d19db | ||
|
|
af09ec3405 | ||
|
|
85441bbe19 | ||
|
|
a18f41142f | ||
|
|
d8f3bd93f7 | ||
|
|
bd6db55b70 | ||
|
|
9422613562 | ||
|
|
135695e918 | ||
|
|
b163ff225a | ||
|
|
1a04c114c6 | ||
|
|
b5a962f1f4 | ||
|
|
f143766625 | ||
|
|
1215adc13a | ||
|
|
826b71cc24 | ||
|
|
dfd94f03fb | ||
|
|
131c4489fa | ||
|
|
9248070755 | ||
|
|
d8fb6cad78 | ||
|
|
e2e40f58ae | ||
|
|
306c72fce9 | ||
|
|
594c3a057b | ||
|
|
ad316367b9 | ||
|
|
1f15c2d140 | ||
|
|
1d5e721a24 | ||
|
|
c9a9b1175a | ||
|
|
5e0c5b9ada | ||
|
|
03b9db782e | ||
|
|
c99e713fab | ||
|
|
8c8cbfa0ba | ||
|
|
0949983acc | ||
|
|
c870bd5fd6 | ||
|
|
946c5a2d9b | ||
|
|
f7bd377a69 | ||
|
|
5d6984b9fd | ||
|
|
3331c6cee2 | ||
|
|
0e370ef62f | ||
|
|
12affdcc64 | ||
|
|
2c1a8be82d | ||
|
|
df3efc6453 | ||
|
|
99720ea0cc |
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.
|
||||
92
.github/instructions/copilot-instructions.md
vendored
92
.github/instructions/copilot-instructions.md
vendored
@@ -1,92 +0,0 @@
|
||||
# Copilot Review Instructions for TinyGLTF
|
||||
|
||||
This document provides guidelines for reviewing code changes in the TinyGLTF repository.
|
||||
|
||||
## Memory Safety
|
||||
|
||||
- **Buffer Overflows**: Check for proper bounds checking when accessing arrays, vectors, and buffers. Verify that buffer sizes are validated before read/write operations.
|
||||
- **Null Pointer Dereferences**: Ensure all pointers are checked for null before dereferencing, especially when handling optional glTF fields.
|
||||
- **Memory Leaks**: Verify proper resource management, including RAII patterns for file handles, image data, and dynamically allocated memory.
|
||||
- **Use-After-Free**: Check for proper lifetime management of objects, especially when dealing with callbacks and asynchronous operations.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **File I/O**: Verify that all file operations have proper error checking and meaningful error messages.
|
||||
- **JSON Parsing**: Ensure JSON parsing errors are caught and reported with helpful context about the location and nature of the error.
|
||||
- **Resource Loading**: Check that failures in loading images, buffers, and other resources are properly handled and don't cause crashes.
|
||||
- **Error Propagation**: Verify that errors are properly propagated through the call stack with appropriate error messages.
|
||||
|
||||
## glTF 2.0 Specification Compliance
|
||||
|
||||
- **Required Fields**: Ensure all required glTF fields are validated and present.
|
||||
- **Data Types**: Verify that data types match the glTF specification (e.g., component types, accessor types).
|
||||
- **Constraints**: Check that glTF constraints are enforced (e.g., valid ranges for enums, buffer stride requirements).
|
||||
- **Extensions**: Verify proper handling of glTF extensions and that unknown extensions are handled gracefully.
|
||||
- **Validation**: Ensure new features align with the glTF 2.0 specification from the Khronos Group.
|
||||
|
||||
## Cross-Platform Compatibility
|
||||
|
||||
- **Windows**: Check for proper handling of Windows-specific issues (path separators, line endings, file operations).
|
||||
- **Linux**: Verify compatibility with various Linux distributions and compilers (GCC, Clang).
|
||||
- **macOS**: Ensure macOS-specific considerations are addressed (case-sensitive filesystems, Clang compatibility).
|
||||
- **Mobile Platforms**: Consider Android and iOS compatibility where applicable.
|
||||
- **Endianness**: Verify proper handling of byte order when reading binary data.
|
||||
- **Compiler Compatibility**: Ensure code compiles with C++11 standard and supported compilers (MSVC, GCC, Clang).
|
||||
|
||||
## Edge Cases in glTF Parsing
|
||||
|
||||
- **Empty/Minimal Files**: Verify handling of minimal valid glTF files.
|
||||
- **Large Files**: Check for proper handling of large glTF files and buffers without memory exhaustion.
|
||||
- **Malformed Data**: Ensure graceful handling of malformed or invalid glTF data.
|
||||
- **Missing Optional Fields**: Verify correct behavior when optional glTF fields are absent.
|
||||
- **Edge Values**: Check handling of boundary values (e.g., maximum buffer sizes, extreme floating-point values).
|
||||
- **Base64 Encoding**: Verify proper handling of base64-encoded data URIs and invalid encodings.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- **API Changes**: Ensure public API changes maintain backwards compatibility or are properly deprecated.
|
||||
- **Breaking Changes**: Flag any breaking changes for major version updates and document migration paths.
|
||||
- **Binary Compatibility**: Consider ABI stability for header-only library changes.
|
||||
- **Default Behavior**: Verify that default behavior of existing functionality remains unchanged.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Parsing Performance**: Check for unnecessary copies, redundant allocations, and inefficient algorithms in parsing logic.
|
||||
- **Memory Usage**: Verify efficient memory usage, especially when loading large glTF files.
|
||||
- **I/O Operations**: Ensure efficient file reading and minimize unnecessary disk access.
|
||||
- **String Operations**: Check for efficient string handling (use of string_view, move semantics).
|
||||
- **STL Usage**: Verify appropriate use of STL containers and algorithms.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Public API**: Ensure all public functions, classes, and methods have clear documentation comments.
|
||||
- **Parameters**: Verify that function parameters are documented, including expected ranges and constraints.
|
||||
- **Return Values**: Document return values and possible error conditions.
|
||||
- **Examples**: Check that complex features include usage examples.
|
||||
- **Changelog**: Verify that significant changes are documented in release notes or changelog.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Test Coverage**: Ensure new features include appropriate unit tests or integration tests.
|
||||
- **Edge Cases**: Verify that tests cover edge cases and error conditions.
|
||||
- **Cross-Platform Tests**: Check that tests run on all supported platforms.
|
||||
- **Regression Tests**: Ensure bug fixes include regression tests to prevent recurrence.
|
||||
- **Sample Files**: Verify that changes are tested with various valid and invalid glTF sample files.
|
||||
|
||||
## Code Style Consistency
|
||||
|
||||
- **Header-Only Pattern**: Maintain the header-only library structure.
|
||||
- **Naming Conventions**: Follow existing naming conventions (CamelCase for types, snake_case for functions where applicable).
|
||||
- **Formatting**: Adhere to the existing code formatting style (check `.clang-format` if available).
|
||||
- **Include Guards**: Verify proper include guards and header organization.
|
||||
- **Namespace Usage**: Ensure proper use of the `tinygltf` namespace.
|
||||
- **Comments**: Maintain consistent comment style with existing code.
|
||||
- **C++11 Compliance**: Verify that code uses C++11 features appropriately and doesn't require newer standards unless specified.
|
||||
|
||||
## Additional Considerations
|
||||
|
||||
- **Third-Party Dependencies**: Minimize new dependencies; prefer existing dependencies (json.hpp, stb_image).
|
||||
- **Warnings**: Ensure code compiles without warnings on supported compilers.
|
||||
- **const Correctness**: Verify proper use of const for parameters and methods.
|
||||
- **RAII**: Prefer RAII patterns for resource management over manual cleanup.
|
||||
- **noexcept**: Use noexcept appropriately for move constructors and move assignment operators.
|
||||
36
.github/workflows/c-cpp.yml
vendored
36
.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,16 +60,18 @@ 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
|
||||
cd build
|
||||
cmake --help
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=On ..
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=On -DTINYGLTF_BUILD_TESTS=ON ..
|
||||
cd ..
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
- name: Run tests
|
||||
run: ctest --test-dir build -C Release --output-on-failure
|
||||
|
||||
|
||||
build-linux:
|
||||
@@ -78,7 +80,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 +102,23 @@ 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
|
||||
cc -I../ -std=c11 -g -O0 \
|
||||
-o tester_v3_json_c tester_v3_json_c.c
|
||||
./tester_v3_json_c
|
||||
cc -I../ -std=c11 -ffreestanding -g -O0 \
|
||||
-o tester_v3_freestanding tester_v3_freestanding.c
|
||||
./tester_v3_freestanding
|
||||
cd ..
|
||||
|
||||
|
||||
build-rapidjson-linux:
|
||||
|
||||
@@ -107,7 +126,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 +159,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
- name: Build
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -158,7 +177,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
|
||||
@@ -166,4 +185,3 @@ jobs:
|
||||
|
||||
git clone https://github.com/Tencent/rapidjson
|
||||
clang++ -DTINYGLTF_USE_RAPIDJSON -I./rapidjson/include/rapidjson -std=c++11 -g -O0 -o loader_example loader_example.cc
|
||||
|
||||
|
||||
253
.github/workflows/ci.yml
vendored
253
.github/workflows/ci.yml
vendored
@@ -23,10 +23,10 @@ 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
|
||||
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_TESTS=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
@@ -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: |
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
cmake -B build -DCMAKE_C_COMPILER=clang-21 -DCMAKE_CXX_COMPILER=clang++-21 -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
|
||||
cmake -B build -DCMAKE_C_COMPILER=clang-21 -DCMAKE_CXX_COMPILER=clang++-21 -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_TESTS=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
@@ -71,10 +71,10 @@ 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
|
||||
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_TESTS=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
@@ -91,10 +91,10 @@ 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
|
||||
run: cmake -B build -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_TESTS=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
@@ -111,13 +111,13 @@ jobs:
|
||||
name: Windows x64 (MSVC)
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off ..
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off -DTINYGLTF_BUILD_TESTS=ON ..
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
@@ -135,13 +135,13 @@ jobs:
|
||||
name: Windows x86 (MSVC)
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G "Visual Studio 17 2022" -A Win32 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off ..
|
||||
cmake -G "Visual Studio 17 2022" -A Win32 -DTINYGLTF_BUILD_LOADER_EXAMPLE=On -DTINYGLTF_BUILD_GL_EXAMPLES=Off -DTINYGLTF_BUILD_VALIDATOR_EXAMPLE=Off -DTINYGLTF_BUILD_TESTS=ON ..
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
@@ -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
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
|
||||
- name: Build with CMake
|
||||
run: |
|
||||
cmake -G"Ninja" -S . -B build
|
||||
cmake -G"Ninja" -S . -B build -DTINYGLTF_BUILD_TESTS=ON
|
||||
cmake --build build
|
||||
|
||||
- name: Run loader_example
|
||||
@@ -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,12 +244,12 @@ 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: |
|
||||
mkdir build
|
||||
cmake -B build -DTINYGLTF_HEADER_ONLY=ON -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON
|
||||
cmake -B build -DTINYGLTF_HEADER_ONLY=ON -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_TESTS=ON
|
||||
cmake --build build
|
||||
|
||||
- name: Run loader_example
|
||||
@@ -259,13 +259,37 @@ jobs:
|
||||
- name: Run tests
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
# v3 C tests through Meson on the primary desktop platforms.
|
||||
v3-c-meson:
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: v3 C Meson (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Meson
|
||||
run: python -m pip install meson ninja
|
||||
|
||||
- name: Configure
|
||||
run: meson setup build-meson -Dtests=true
|
||||
|
||||
- name: Build
|
||||
run: meson compile -C build-meson
|
||||
|
||||
- name: Run v3 C tests
|
||||
run: meson test -C build-meson --print-errorlogs
|
||||
|
||||
# Special Configuration: RapidJSON Backend
|
||||
linux-rapidjson:
|
||||
runs-on: ubuntu-latest
|
||||
name: Linux x64 (GCC) - RapidJSON Backend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Clone RapidJSON
|
||||
run: |
|
||||
@@ -273,7 +297,7 @@ jobs:
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
cmake -B build -DTINYGLTF_USE_RAPIDJSON=ON -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DCMAKE_PREFIX_PATH=$PWD/rapidjson
|
||||
cmake -B build -DTINYGLTF_USE_RAPIDJSON=ON -DTINYGLTF_BUILD_LOADER_EXAMPLE=ON -DTINYGLTF_BUILD_TESTS=ON -DCMAKE_PREFIX_PATH=$PWD/rapidjson
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
@@ -291,7 +315,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 +337,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 +352,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
|
||||
|
||||
72
.github/workflows/codeql-analysis.yml
vendored
72
.github/workflows/codeql-analysis.yml
vendored
@@ -1,72 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '21 20 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'cpp', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
# ℹ️ 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
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
12
.github/workflows/mingw-w64-msys2.yml
vendored
12
.github/workflows/mingw-w64-msys2.yml
vendored
@@ -7,7 +7,11 @@ on:
|
||||
- devel
|
||||
paths:
|
||||
- 'tiny_gltf.*'
|
||||
- 'tinygltf_json_c.h'
|
||||
- 'CMakeLists.txt'
|
||||
- 'meson.build'
|
||||
- 'meson_options.txt'
|
||||
- 'tests/tester_v3*.c'
|
||||
- '.github/workflows/mingw-w64-msys2.yml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
@@ -20,7 +24,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
|
||||
@@ -37,9 +41,13 @@ jobs:
|
||||
cmake \
|
||||
-G"Ninja" \
|
||||
-S . \
|
||||
-B build
|
||||
-B build \
|
||||
-DTINYGLTF_BUILD_TESTS=ON
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cmake --build build
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
ctest --test-dir build --output-on-failure
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
# CMake
|
||||
/build/
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
@@ -71,6 +72,7 @@ imgui.ini
|
||||
loader_example
|
||||
tests/tester
|
||||
tests/tester_noexcept
|
||||
tests/tester_intensive_customjson
|
||||
tests/issue-97.gltf
|
||||
tests/issue-261.gltf
|
||||
tests/issue-495-external.gltf
|
||||
|
||||
@@ -41,6 +41,21 @@ endif (TINYGLTF_BUILD_BUILDER_EXAMPLE)
|
||||
|
||||
if (TINYGLTF_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
function(add_tinygltf_v3_c_test target)
|
||||
add_executable(${target} ${ARGN})
|
||||
target_include_directories(${target} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
)
|
||||
set_target_properties(${target} PROPERTIES
|
||||
C_STANDARD 11
|
||||
C_STANDARD_REQUIRED ON
|
||||
C_EXTENSIONS OFF
|
||||
)
|
||||
add_test(NAME ${target} COMMAND ${target} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||
endfunction()
|
||||
|
||||
add_executable(tester tests/tester.cc)
|
||||
target_include_directories(tester PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
@@ -56,6 +71,28 @@ if (TINYGLTF_BUILD_TESTS)
|
||||
)
|
||||
target_compile_definitions(tester_customjson PRIVATE TINYGLTF_USE_CUSTOM_JSON)
|
||||
add_test(NAME tester_customjson COMMAND tester_customjson WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||
|
||||
# Intensive parser tests for the custom JSON backend
|
||||
add_executable(tester_intensive_customjson tests/tester_intensive_customjson.cc)
|
||||
target_include_directories(tester_intensive_customjson PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/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_tinygltf_v3_c_test(tester_v3_c tests/tester_v3_c.c tiny_gltf_v3.c)
|
||||
target_compile_definitions(tester_v3_c PRIVATE TINYGLTF3_ENABLE_FS)
|
||||
|
||||
add_tinygltf_v3_c_test(tester_v3_c_v1port tests/tester_v3_c_v1port.c tiny_gltf_v3.c)
|
||||
target_compile_definitions(tester_v3_c_v1port PRIVATE TINYGLTF3_ENABLE_FS)
|
||||
|
||||
add_tinygltf_v3_c_test(tester_v3_json_c tests/tester_v3_json_c.c)
|
||||
|
||||
add_tinygltf_v3_c_test(tester_v3_freestanding tests/tester_v3_freestanding.c)
|
||||
if (CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
|
||||
target_compile_options(tester_v3_freestanding PRIVATE -ffreestanding)
|
||||
endif()
|
||||
endif (TINYGLTF_BUILD_TESTS)
|
||||
|
||||
#
|
||||
@@ -98,7 +135,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
|
||||
|
||||
91
README.md
91
README.md
@@ -1,14 +1,78 @@
|
||||
# Header only C++ tiny glTF library(loader/saver).
|
||||
|
||||
`TinyGLTF` is a header only C++11 glTF 2.0 https://github.com/KhronosGroup/glTF library.
|
||||
`TinyGLTF` is a header only C++ glTF 2.0 https://github.com/KhronosGroup/glTF library.
|
||||
|
||||
`TinyGLTF` uses Niels Lohmann's json library (https://github.com/nlohmann/json), so now it requires C++11 compiler.
|
||||
(Also, you can use RadpidJSON as an JSON backend)
|
||||
If you are looking for old, C++03 version, please use `devel-picojson` branch (but not maintained anymore).
|
||||
## TinyGLTF v3 (new major release)
|
||||
|
||||
**`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
|
||||
|
||||
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_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 the `Security Considerations` block at the top of `tiny_gltf_v3.h`.
|
||||
|
||||
### Quick start (v3)
|
||||
|
||||
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.
|
||||
|
||||
```c
|
||||
#include "tiny_gltf_v3.h"
|
||||
```
|
||||
|
||||
Loading a glTF file:
|
||||
|
||||
```c
|
||||
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_error_stack_free(&errors);
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
Currently TinyGLTF is stable and maintenance mode. No drastic changes and feature additions planned.
|
||||
> ⚠️ **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**. `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.
|
||||
- v2.8.0 Add URICallbacks for custom URI handling in Buffer and Image. PR#397
|
||||
- v2.7.0 Change WriteImageDataFunction user callback function signature. PR#393
|
||||
@@ -90,23 +154,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -139,7 +139,9 @@ struct BenchResult {
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static BenchResult bench_file(const char *filename, int iterations, int warmup,
|
||||
bool quiet, int float32_mode = 0) {
|
||||
bool quiet, int float32_mode = 0,
|
||||
int skip_extras_values = 0,
|
||||
int borrow_input_buffers = 0) {
|
||||
BenchResult r = {};
|
||||
r.filename = filename;
|
||||
r.iterations = iterations;
|
||||
@@ -199,6 +201,8 @@ static BenchResult bench_file(const char *filename, int iterations, int warmup,
|
||||
opts.memory.allocator.free = tracked_free;
|
||||
opts.memory.allocator.user_data = &tracker;
|
||||
opts.parse_float32 = float32_mode;
|
||||
opts.skip_extras_values = skip_extras_values;
|
||||
opts.borrow_input_buffers = borrow_input_buffers;
|
||||
|
||||
tg3_model model;
|
||||
tg3_error_stack errors;
|
||||
@@ -340,7 +344,11 @@ static void usage() {
|
||||
" --csv Output in CSV format\n"
|
||||
" --quiet Suppress per-iteration error messages\n"
|
||||
" --batch Benchmark multiple files\n"
|
||||
" --float32 Parse JSON floats as float32 (faster, less precise)\n");
|
||||
" --float32 Parse JSON floats as float32 (faster, less precise)\n"
|
||||
" --skip-extras-values\n"
|
||||
" Skip materializing extras/unknown extension values\n"
|
||||
" --borrow-input-buffers\n"
|
||||
" Let GLB buffers reference caller-owned input bytes\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@@ -351,6 +359,8 @@ int main(int argc, char **argv) {
|
||||
bool csv = false;
|
||||
bool quiet = false;
|
||||
int float32_mode = 0;
|
||||
int skip_extras_values = 0;
|
||||
int borrow_input_buffers = 0;
|
||||
std::vector<std::string> files;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
@@ -364,6 +374,10 @@ int main(int argc, char **argv) {
|
||||
quiet = true;
|
||||
} else if (strcmp(argv[i], "--float32") == 0) {
|
||||
float32_mode = 1;
|
||||
} else if (strcmp(argv[i], "--skip-extras-values") == 0) {
|
||||
skip_extras_values = 1;
|
||||
} else if (strcmp(argv[i], "--borrow-input-buffers") == 0) {
|
||||
borrow_input_buffers = 1;
|
||||
} else if (strcmp(argv[i], "--batch") == 0) {
|
||||
/* batch mode: just collect files */
|
||||
} else if (argv[i][0] != '-') {
|
||||
@@ -379,10 +393,14 @@ int main(int argc, char **argv) {
|
||||
if (!csv && !quiet) {
|
||||
printf("Benchmarking: %s (%d iterations, %d warmup%s)\n",
|
||||
file.c_str(), iterations, warmup,
|
||||
float32_mode ? ", float32" : "");
|
||||
float32_mode ? ", float32" :
|
||||
skip_extras_values ? ", skip extras" :
|
||||
borrow_input_buffers ? ", borrow buffers" : "");
|
||||
}
|
||||
|
||||
BenchResult r = bench_file(file.c_str(), iterations, warmup, quiet, float32_mode);
|
||||
BenchResult r = bench_file(file.c_str(), iterations, warmup, quiet,
|
||||
float32_mode, skip_extras_values,
|
||||
borrow_input_buffers);
|
||||
|
||||
if (csv) {
|
||||
print_csv_row(r);
|
||||
|
||||
@@ -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;
|
||||
|
||||
52
meson.build
Normal file
52
meson.build
Normal file
@@ -0,0 +1,52 @@
|
||||
project(
|
||||
'tinygltf',
|
||||
'c',
|
||||
default_options: ['c_std=c11'],
|
||||
meson_version: '>=0.55.0',
|
||||
)
|
||||
|
||||
tinygltf_inc = include_directories('.', 'tests')
|
||||
|
||||
if get_option('tests')
|
||||
tests_workdir = join_paths(meson.current_source_dir(), 'tests')
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
tester_v3_c = executable(
|
||||
'tester_v3_c',
|
||||
['tests/tester_v3_c.c', 'tiny_gltf_v3.c'],
|
||||
include_directories: tinygltf_inc,
|
||||
c_args: ['-DTINYGLTF3_ENABLE_FS'],
|
||||
install: false,
|
||||
)
|
||||
test('tester_v3_c', tester_v3_c, workdir: tests_workdir)
|
||||
|
||||
tester_v3_c_v1port = executable(
|
||||
'tester_v3_c_v1port',
|
||||
['tests/tester_v3_c_v1port.c', 'tiny_gltf_v3.c'],
|
||||
include_directories: tinygltf_inc,
|
||||
c_args: ['-DTINYGLTF3_ENABLE_FS'],
|
||||
install: false,
|
||||
)
|
||||
test('tester_v3_c_v1port', tester_v3_c_v1port, workdir: tests_workdir)
|
||||
|
||||
tester_v3_json_c = executable(
|
||||
'tester_v3_json_c',
|
||||
'tests/tester_v3_json_c.c',
|
||||
include_directories: tinygltf_inc,
|
||||
install: false,
|
||||
)
|
||||
test('tester_v3_json_c', tester_v3_json_c, workdir: tests_workdir)
|
||||
|
||||
freestanding_args = []
|
||||
if cc.get_id() in ['clang', 'gcc']
|
||||
freestanding_args += ['-ffreestanding']
|
||||
endif
|
||||
tester_v3_freestanding = executable(
|
||||
'tester_v3_freestanding',
|
||||
'tests/tester_v3_freestanding.c',
|
||||
include_directories: tinygltf_inc,
|
||||
c_args: freestanding_args,
|
||||
install: false,
|
||||
)
|
||||
test('tester_v3_freestanding', tester_v3_freestanding, workdir: tests_workdir)
|
||||
endif
|
||||
1
meson_options.txt
Normal file
1
meson_options.txt
Normal file
@@ -0,0 +1 @@
|
||||
option('tests', type: 'boolean', value: true, description: 'Build and run tinygltf tests')
|
||||
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,6 +1,19 @@
|
||||
# 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 tester_v3_json_c tester_v3_freestanding
|
||||
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
|
||||
|
||||
tester_v3_json_c: tester_v3_json_c.c ../tinygltf_json_c.h
|
||||
clang -I../ -std=c11 -g -O0 -o tester_v3_json_c tester_v3_json_c.c
|
||||
|
||||
tester_v3_freestanding: tester_v3_freestanding.c ../tiny_gltf_v3.h ../tiny_gltf_v3.c ../tinygltf_json_c.h
|
||||
clang -I../ -std=c11 -ffreestanding -g -O0 -o tester_v3_freestanding tester_v3_freestanding.c
|
||||
|
||||
@@ -4,9 +4,14 @@ Do fuzzing test for TinyGLTF API.
|
||||
|
||||
## Supported API
|
||||
|
||||
* [x] LoadASCIIFromMemory
|
||||
* [x] LoadASCIIFromString
|
||||
* [ ] LoadBinaryFromMemory
|
||||
|
||||
### Custom JSON backend (`tinygltf_json.h`)
|
||||
|
||||
* [x] LoadASCIIFromString
|
||||
* [x] LoadBinaryFromMemory
|
||||
|
||||
## Requirements
|
||||
|
||||
* meson
|
||||
@@ -36,11 +41,17 @@ $ cd build
|
||||
$ ninja
|
||||
```
|
||||
|
||||
This builds two fuzzers:
|
||||
|
||||
* `fuzz_gltf` – default nlohmann/json backend
|
||||
* `fuzz_gltf_customjson` – custom `tinygltf_json.h` backend (tests both ASCII and binary parsing paths)
|
||||
|
||||
## How to run
|
||||
|
||||
Increase memory limit. e.g. `-rss_limit_mb=50000`
|
||||
|
||||
```
|
||||
$ ./fuzz_gltf -rss_limit_mb=20000 -jobs 4
|
||||
$ ./fuzz_gltf_customjson -rss_limit_mb=20000 -jobs 4
|
||||
```
|
||||
|
||||
|
||||
76
tests/fuzzer/fuzz_gltf_customjson.cc
Normal file
76
tests/fuzzer/fuzz_gltf_customjson.cc
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* LLVM libFuzzer harness for tinygltf with the custom JSON backend
|
||||
* (tinygltf_json.h).
|
||||
*
|
||||
* Exercises:
|
||||
* 1. LoadASCIIFromString – glTF JSON parsing
|
||||
* 2. LoadBinaryFromMemory – GLB binary parsing
|
||||
*
|
||||
* Build (clang with libFuzzer):
|
||||
* clang++ -std=c++11 -fsanitize=address,fuzzer \
|
||||
* -DTINYGLTF_USE_CUSTOM_JSON \
|
||||
* -I../../ fuzz_gltf_customjson.cc \
|
||||
* -o fuzz_gltf_customjson
|
||||
*
|
||||
* Run:
|
||||
* ./fuzz_gltf_customjson -rss_limit_mb=20000 -jobs 4
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#define TINYGLTF_IMPLEMENTATION
|
||||
#ifndef TINYGLTF_USE_CUSTOM_JSON
|
||||
#define TINYGLTF_USE_CUSTOM_JSON
|
||||
#endif
|
||||
#include "tiny_gltf.h"
|
||||
|
||||
/* Fuzz the ASCII (JSON) parser path */
|
||||
static void fuzz_ascii(const uint8_t *data, size_t size) {
|
||||
tinygltf::Model model;
|
||||
tinygltf::TinyGLTF ctx;
|
||||
std::string err;
|
||||
std::string warn;
|
||||
|
||||
const char *str = reinterpret_cast<const char *>(data);
|
||||
|
||||
bool ret =
|
||||
ctx.LoadASCIIFromString(&model, &err, &warn, str,
|
||||
static_cast<unsigned int>(size), /* base_dir */ "");
|
||||
(void)ret;
|
||||
}
|
||||
|
||||
/* Fuzz the binary (GLB) parser path */
|
||||
static void fuzz_binary(const uint8_t *data, size_t size) {
|
||||
tinygltf::Model model;
|
||||
tinygltf::TinyGLTF ctx;
|
||||
std::string err;
|
||||
std::string warn;
|
||||
|
||||
bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, data,
|
||||
static_cast<unsigned int>(size),
|
||||
/* base_dir */ "");
|
||||
(void)ret;
|
||||
}
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size == 0) return 0;
|
||||
|
||||
/* Use the lowest bit of the first byte to select the parse path.
|
||||
* The remaining bits are left for the fuzzer engine to explore;
|
||||
* additional paths (e.g. LoadASCIIFromFile, check_sections flags)
|
||||
* can be added here in the future using more selector bits. */
|
||||
uint8_t selector = data[0];
|
||||
const uint8_t *payload = data + 1;
|
||||
size_t payload_size = size - 1;
|
||||
|
||||
if (selector & 1) {
|
||||
fuzz_binary(payload, payload_size);
|
||||
} else {
|
||||
fuzz_ascii(payload, payload_size);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -7,3 +7,9 @@ executable('fuzz_gltf',
|
||||
cpp_args : '-fsanitize=address,fuzzer',
|
||||
link_args : '-fsanitize=address,fuzzer' )
|
||||
|
||||
executable('fuzz_gltf_customjson',
|
||||
'fuzz_gltf_customjson.cc',
|
||||
include_directories : incdirs,
|
||||
cpp_args : ['-fsanitize=address,fuzzer', '-DTINYGLTF_USE_CUSTOM_JSON'],
|
||||
link_args : '-fsanitize=address,fuzzer' )
|
||||
|
||||
|
||||
@@ -1249,3 +1249,37 @@ TEST_CASE("empty-images-not-written", "[issue-495]") {
|
||||
// WriteImageData should be invoked for both images
|
||||
CHECK(counter == 2);
|
||||
}
|
||||
|
||||
#ifdef TINYGLTF_USE_CUSTOM_JSON
|
||||
/* Regression test: in float32_mode, integer-only tokens with more than 9
|
||||
* digits must still be parsed as integers (is_int == 1), not floats.
|
||||
* Previously, max_sig=9 was applied to the integer part too, causing excess
|
||||
* digits to bump exp10, which broke the exp10==0 guard in the integer
|
||||
* fast-path and mis-classified the value as a float. */
|
||||
TEST_CASE("cj-float32-long-integer", "[customjson]") {
|
||||
// Values chosen to cover exactly-at, just-over, and near int64 boundaries.
|
||||
struct {
|
||||
const char *text;
|
||||
int64_t expected;
|
||||
} cases[] = {
|
||||
{ "1234567890", 1234567890LL }, /* 10 digits */
|
||||
{ "12345678901", 12345678901LL }, /* 11 digits */
|
||||
{ "1000000000000", 1000000000000LL }, /* 13 digits */
|
||||
{ "9223372036854775807", INT64_MAX }, /* max int64 (19 digits) */
|
||||
{ "-1234567890", -1234567890LL }, /* negative 10 digits */
|
||||
{ "-9223372036854775808", INT64_MIN }, /* min int64 */
|
||||
};
|
||||
|
||||
for (auto &tc : cases) {
|
||||
int is_int = 0;
|
||||
int64_t ival = 0;
|
||||
double dval = 0.0;
|
||||
const char *end = tc.text + strlen(tc.text);
|
||||
const char *ret = cj_parse_number(tc.text, end, &is_int, &ival, &dval, /*float32_mode=*/1);
|
||||
CAPTURE(tc.text);
|
||||
REQUIRE(ret != nullptr);
|
||||
CHECK(is_int == 1);
|
||||
CHECK(ival == tc.expected);
|
||||
}
|
||||
}
|
||||
#endif /* TINYGLTF_USE_CUSTOM_JSON */
|
||||
|
||||
1089
tests/tester_intensive_customjson.cc
Normal file
1089
tests/tester_intensive_customjson.cc
Normal file
File diff suppressed because it is too large
Load Diff
BIN
tests/tester_v3_c
Executable file
BIN
tests/tester_v3_c
Executable file
Binary file not shown.
1801
tests/tester_v3_c.c
Normal file
1801
tests/tester_v3_c.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
tests/tester_v3_c_v1port
Executable file
BIN
tests/tester_v3_c_v1port
Executable file
Binary file not shown.
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;
|
||||
}
|
||||
BIN
tests/tester_v3_freestanding
Executable file
BIN
tests/tester_v3_freestanding
Executable file
Binary file not shown.
75
tests/tester_v3_freestanding.c
Normal file
75
tests/tester_v3_freestanding.c
Normal file
@@ -0,0 +1,75 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static union {
|
||||
uint64_t align;
|
||||
unsigned char bytes[512 * 1024];
|
||||
} test_heap;
|
||||
static size_t test_heap_used;
|
||||
|
||||
static void *test_malloc(size_t size) {
|
||||
size_t total = (size + sizeof(size_t) + 7u) & ~(size_t)7u;
|
||||
if (test_heap_used + total > sizeof(test_heap.bytes)) return 0;
|
||||
{
|
||||
unsigned char *base = test_heap.bytes + test_heap_used;
|
||||
*((size_t *)base) = size;
|
||||
test_heap_used += total;
|
||||
return base + sizeof(size_t);
|
||||
}
|
||||
}
|
||||
|
||||
static void *test_realloc(void *ptr, size_t size) {
|
||||
unsigned char *old_base;
|
||||
size_t old_size;
|
||||
unsigned char *new_ptr;
|
||||
size_t n;
|
||||
size_t i;
|
||||
if (!ptr) return test_malloc(size);
|
||||
old_base = (unsigned char *)ptr - sizeof(size_t);
|
||||
old_size = *((size_t *)old_base);
|
||||
new_ptr = (unsigned char *)test_malloc(size);
|
||||
if (!new_ptr) return 0;
|
||||
n = old_size < size ? old_size : size;
|
||||
for (i = 0; i < n; ++i) new_ptr[i] = ((unsigned char *)ptr)[i];
|
||||
return new_ptr;
|
||||
}
|
||||
|
||||
static void test_free(void *ptr) {
|
||||
(void)ptr;
|
||||
}
|
||||
|
||||
#define TINYGLTF3_NO_STDLIB
|
||||
#define TINYGLTF3_MALLOC(sz) test_malloc(sz)
|
||||
#define TINYGLTF3_REALLOC(ptr, sz) test_realloc((ptr), (sz))
|
||||
#define TINYGLTF3_FREE(ptr) test_free(ptr)
|
||||
#define TINYGLTF3_IMPLEMENTATION
|
||||
#include "tiny_gltf_v3.h"
|
||||
|
||||
static int streq(tg3_str s, const char *lit, uint32_t len) {
|
||||
uint32_t i;
|
||||
if (!s.data || s.len != len) return 0;
|
||||
for (i = 0; i < len; ++i) {
|
||||
if (s.data[i] != lit[i]) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const uint8_t json[] =
|
||||
"{\"asset\":{\"version\":\"2.0\"},\"nodes\":[{\"name\":\"free\"}]}";
|
||||
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) return 1;
|
||||
if (model.nodes_count != 1) return 3;
|
||||
if (!streq(model.nodes[0].name, "free", 4)) return 4;
|
||||
tg3_model_free(&model);
|
||||
tg3_error_stack_free(&errors);
|
||||
return 0;
|
||||
}
|
||||
BIN
tests/tester_v3_json_c
Executable file
BIN
tests/tester_v3_json_c
Executable file
Binary file not shown.
165
tests/tester_v3_json_c.c
Normal file
165
tests/tester_v3_json_c.c
Normal file
@@ -0,0 +1,165 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TINYGLTF_JSON_C_IMPLEMENTATION
|
||||
#include "tinygltf_json_c.h"
|
||||
|
||||
static uint64_t dbl_bits(double v) {
|
||||
uint64_t bits;
|
||||
memcpy(&bits, &v, sizeof(bits));
|
||||
return bits;
|
||||
}
|
||||
|
||||
static double dbl_from_bits(uint64_t bits) {
|
||||
double v;
|
||||
memcpy(&v, &bits, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
|
||||
static int check_stringify(const char *json, const char *expected) {
|
||||
tg3json_value v;
|
||||
const char *err = NULL;
|
||||
char *out;
|
||||
size_t out_len = 0;
|
||||
int ok = 1;
|
||||
if (!tg3json_parse(json, json + strlen(json), 128, &v, &err)) {
|
||||
fprintf(stderr, "parse failed for %s at %td\n", json, err ? err - json : -1);
|
||||
return 0;
|
||||
}
|
||||
out = tg3json_stringify(&v, &out_len);
|
||||
if (!out || strcmp(out, expected) != 0) {
|
||||
fprintf(stderr, "stringify(%s) = %s, expected %s\n",
|
||||
json, out ? out : "(null)", expected);
|
||||
ok = 0;
|
||||
}
|
||||
if (out) TINYGLTF_JSON_FREE(out);
|
||||
tg3json_value_free(&v);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int check_parse_rejects(const char *json) {
|
||||
tg3json_value v;
|
||||
const char *err = NULL;
|
||||
if (tg3json_parse(json, json + strlen(json), 128, &v, &err)) {
|
||||
fprintf(stderr, "parse unexpectedly accepted %s\n", json);
|
||||
tg3json_value_free(&v);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int check_roundtrip(const char *json) {
|
||||
tg3json_value a;
|
||||
tg3json_value b;
|
||||
const char *err = NULL;
|
||||
char *out;
|
||||
size_t out_len = 0;
|
||||
int ok = 1;
|
||||
if (!tg3json_parse(json, json + strlen(json), 128, &a, &err)) return 0;
|
||||
out = tg3json_stringify(&a, &out_len);
|
||||
if (!out || !tg3json_parse(out, out + out_len, 128, &b, &err)) {
|
||||
ok = 0;
|
||||
} else if (a.type != TG3JSON_REAL ||
|
||||
!((b.type == TG3JSON_REAL && dbl_bits(a.u.real) == dbl_bits(b.u.real)) ||
|
||||
(b.type == TG3JSON_INT && dbl_bits(a.u.real) == dbl_bits((double)b.u.integer)))) {
|
||||
fprintf(stderr, "roundtrip changed bits: %s -> %s\n", json, out);
|
||||
ok = 0;
|
||||
}
|
||||
if (out) TINYGLTF_JSON_FREE(out);
|
||||
tg3json_value_free(&a);
|
||||
tg3json_value_free(&b);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int check_parse_bits(const char *json, uint64_t expected_bits) {
|
||||
tg3json_value v;
|
||||
const char *err = NULL;
|
||||
if (!tg3json_parse(json, json + strlen(json), 128, &v, &err)) {
|
||||
fprintf(stderr, "parse failed for %s at %td\n", json, err ? err - json : -1);
|
||||
return 0;
|
||||
}
|
||||
if (v.type != TG3JSON_REAL || dbl_bits(v.u.real) != expected_bits) {
|
||||
fprintf(stderr, "parse bits mismatch: %s -> 0x%llx, expected 0x%llx\n",
|
||||
json, (unsigned long long)(v.type == TG3JSON_REAL ? dbl_bits(v.u.real) : 0),
|
||||
(unsigned long long)expected_bits);
|
||||
tg3json_value_free(&v);
|
||||
return 0;
|
||||
}
|
||||
tg3json_value_free(&v);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int check_parse_float32(void) {
|
||||
static const char json[] = "0.10000000149011612";
|
||||
tg3json_parse_options opts;
|
||||
tg3json_value v;
|
||||
const char *err = NULL;
|
||||
double expected = (double)(float)0.10000000149011612;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
opts.parse_float32 = 1;
|
||||
if (!tg3json_parse_n_opts(json, strlen(json), &opts, &v, &err)) {
|
||||
fprintf(stderr, "parse_float32 parse failed\n");
|
||||
return 0;
|
||||
}
|
||||
if (v.type != TG3JSON_REAL || dbl_bits(v.u.real) != dbl_bits(expected)) {
|
||||
fprintf(stderr, "parse_float32 did not round through float\n");
|
||||
tg3json_value_free(&v);
|
||||
return 0;
|
||||
}
|
||||
tg3json_value_free(&v);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int check_nonfinite_stringifies_to_null(void) {
|
||||
tg3json_value v;
|
||||
char *out;
|
||||
size_t len = 0;
|
||||
int ok;
|
||||
tg3json_value_init_real(&v, dbl_from_bits(0x7ff0000000000000ULL));
|
||||
out = tg3json_stringify(&v, &len);
|
||||
ok = out && strcmp(out, "null") == 0;
|
||||
if (!ok) fprintf(stderr, "inf stringify = %s\n", out ? out : "(null)");
|
||||
if (out) TINYGLTF_JSON_FREE(out);
|
||||
tg3json_value_free(&v);
|
||||
if (!ok) return 0;
|
||||
|
||||
tg3json_value_init_real(&v, dbl_from_bits(0x7ff8000000000001ULL));
|
||||
out = tg3json_stringify(&v, &len);
|
||||
ok = out && strcmp(out, "null") == 0;
|
||||
if (!ok) fprintf(stderr, "nan stringify = %s\n", out ? out : "(null)");
|
||||
if (out) TINYGLTF_JSON_FREE(out);
|
||||
tg3json_value_free(&v);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int ok = 1;
|
||||
ok = check_stringify("1.0", "1") && ok;
|
||||
ok = check_stringify("-1.0", "-1") && ok;
|
||||
ok = check_stringify("0.1", "0.1") && ok;
|
||||
ok = check_stringify("0.0001", "0.0001") && ok;
|
||||
ok = check_stringify("0.00001", "1e-5") && ok;
|
||||
ok = check_stringify("1000000000000000.0", "1000000000000000") && ok;
|
||||
ok = check_stringify("10000000000000000.0", "1e16") && ok;
|
||||
ok = check_roundtrip("1.2345678901234567") && ok;
|
||||
ok = check_roundtrip("2.2250738585072014e-308") && ok;
|
||||
ok = check_roundtrip("5e-324") && ok;
|
||||
ok = check_roundtrip("-5e-324") && ok;
|
||||
ok = check_roundtrip("9007199254740993.0") && ok;
|
||||
ok = check_parse_bits("1.23456789012345678901", 0x3ff3c0ca428c59fbULL) && ok;
|
||||
ok = check_parse_bits("1.234567890123456789012345678901234567890e-100",
|
||||
0x2b31482fe620c5d2ULL) && ok;
|
||||
ok = check_parse_bits("1.7976931348623157e308", 0x7fefffffffffffffULL) && ok;
|
||||
ok = check_parse_float32() && ok;
|
||||
ok = check_nonfinite_stringifies_to_null() && ok;
|
||||
ok = check_parse_rejects("+1") && ok;
|
||||
ok = check_parse_rejects("01") && ok;
|
||||
ok = check_parse_rejects("1.") && ok;
|
||||
ok = check_parse_rejects("1e") && ok;
|
||||
ok = check_parse_rejects("1e400") && ok;
|
||||
ok = check_parse_rejects("-1e400") && ok;
|
||||
ok = check_parse_rejects("1.7976931348623159e308") && ok;
|
||||
ok = check_parse_rejects("[1,]") && ok;
|
||||
return ok ? 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
|
||||
}
|
||||
@@ -7059,7 +7059,11 @@ static void SerializeNumberProperty(const std::string &key, T number,
|
||||
detail::JsonAddMember(obj, key.c_str(), detail::json(number));
|
||||
}
|
||||
|
||||
#ifdef TINYGLTF_USE_RAPIDJSON
|
||||
#if defined(TINYGLTF_USE_RAPIDJSON) || defined(TINYGLTF_USE_CUSTOM_JSON)
|
||||
// size_t needs an explicit cast to uint64_t: on platforms where size_t is
|
||||
// neither int64_t nor uint64_t (e.g. macOS ARM64 where it is unsigned long,
|
||||
// or 32-bit targets where it is unsigned int) constructing detail::json
|
||||
// directly from a size_t is an ambiguous overload.
|
||||
template <>
|
||||
void SerializeNumberProperty(const std::string &key, size_t number,
|
||||
detail::json &obj) {
|
||||
|
||||
3653
tiny_gltf_v3.c
Normal file
3653
tiny_gltf_v3.c
Normal file
File diff suppressed because it is too large
Load Diff
150
tiny_gltf_v3.h
150
tiny_gltf_v3.h
@@ -1,9 +1,8 @@
|
||||
/*
|
||||
* 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) 2015 - Present Syoyo Fujita, Aurelien Chatelain and many
|
||||
* contributors.
|
||||
* Copyright (c) 2026 - Present: Syoyo Fujita
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -28,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)
|
||||
@@ -38,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_
|
||||
@@ -47,7 +77,7 @@
|
||||
* Section 2: Configuration Macros
|
||||
* ====================================================================== */
|
||||
|
||||
/* Build mode: define in ONE .c/.cpp translation unit */
|
||||
/* Legacy single-translation-unit build mode: define in ONE C or C++ file */
|
||||
/* #define TINYGLTF3_IMPLEMENTATION */
|
||||
|
||||
/* Opt-in features (OFF by default) */
|
||||
@@ -86,8 +116,12 @@
|
||||
|
||||
/* Assert override */
|
||||
#ifndef TINYGLTF3_ASSERT
|
||||
#ifndef TINYGLTF3_NO_STDLIB
|
||||
#include <assert.h>
|
||||
#define TINYGLTF3_ASSERT(x) assert(x)
|
||||
#else
|
||||
#define TINYGLTF3_ASSERT(x) ((void)(x))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* ======================================================================
|
||||
@@ -97,8 +131,34 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#ifndef TINYGLTF3_NO_STDLIB
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
#ifndef TINYGLTF3_MALLOC
|
||||
#ifndef TINYGLTF3_NO_STDLIB
|
||||
#define TINYGLTF3_MALLOC(sz) malloc(sz)
|
||||
#else
|
||||
#define TINYGLTF3_MALLOC(sz) NULL
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef TINYGLTF3_REALLOC
|
||||
#ifndef TINYGLTF3_NO_STDLIB
|
||||
#define TINYGLTF3_REALLOC(ptr, sz) realloc((ptr), (sz))
|
||||
#else
|
||||
#define TINYGLTF3_REALLOC(ptr, sz) NULL
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef TINYGLTF3_FREE
|
||||
#ifndef TINYGLTF3_NO_STDLIB
|
||||
#define TINYGLTF3_FREE(ptr) free(ptr)
|
||||
#else
|
||||
#define TINYGLTF3_FREE(ptr) ((void)(ptr))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* ======================================================================
|
||||
* Section 4: Constants and Enums
|
||||
@@ -388,6 +448,7 @@ typedef struct tg3_asset {
|
||||
/* --- Buffer --- */
|
||||
typedef struct tg3_buffer {
|
||||
tg3_str name;
|
||||
uint64_t byte_length; /* Declared buffer.byteLength */
|
||||
tg3_span_u8 data;
|
||||
tg3_str uri;
|
||||
tg3_extras_ext ext;
|
||||
@@ -911,10 +972,18 @@ typedef struct tg3_parse_options {
|
||||
int32_t images_as_is; /* 1 = don't decode images */
|
||||
int32_t preserve_image_channels; /* 1 = keep original channels */
|
||||
int32_t store_original_json; /* 1 = store raw JSON strings */
|
||||
int32_t skip_extras_values; /* 1 = skip materializing extras and
|
||||
* unknown extension value trees */
|
||||
int32_t borrow_input_buffers; /* 1 = GLB BIN buffer spans may point
|
||||
* into caller-owned input data */
|
||||
int32_t parse_float32; /* 1 = parse JSON floats as float32 for speed
|
||||
* (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;
|
||||
|
||||
@@ -1220,6 +1289,15 @@ 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)"
|
||||
#endif
|
||||
|
||||
/* Include JSON parser */
|
||||
#include "tinygltf_json.h"
|
||||
@@ -1227,11 +1305,9 @@ ParseGenerator tg3_parse_coro(
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
/* Implementation uses C++ features from tinygltf_json.h */
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#endif
|
||||
|
||||
/* Forward SIMD macros to tinygltf_json.h */
|
||||
#ifdef TINYGLTF3_JSON_SIMD_SSE2
|
||||
@@ -1358,10 +1434,13 @@ static void *tg3__arena_alloc(tg3_arena *arena, size_t size) {
|
||||
}
|
||||
|
||||
static char *tg3__arena_strdup(tg3_arena *arena, const char *s, size_t len) {
|
||||
if (!s || len == 0) return NULL;
|
||||
if (!s) return NULL;
|
||||
/* Allocate len+1 bytes; when len==0 this produces a 1-byte "\0" buffer so
|
||||
* that empty strings (data!=NULL, len==0) remain distinguishable from
|
||||
* absent strings (data==NULL, len==0). */
|
||||
char *dst = (char *)tg3__arena_alloc(arena, len + 1);
|
||||
if (!dst) return NULL;
|
||||
memcpy(dst, s, len);
|
||||
if (len > 0) memcpy(dst, s, len);
|
||||
dst[len] = '\0';
|
||||
return dst;
|
||||
}
|
||||
@@ -1750,7 +1829,25 @@ static int tg3__parse_int(tg3__parse_ctx *ctx, const tg3__json &o,
|
||||
"Field '%s' must be a number", key);
|
||||
return 0;
|
||||
}
|
||||
*out = it->get<int>();
|
||||
if (it->is_number_integer()) {
|
||||
int64_t v = it->get<int64_t>();
|
||||
if (v < (int64_t)INT32_MIN || v > (int64_t)INT32_MAX) {
|
||||
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
|
||||
TG3_ERR_JSON_TYPE_MISMATCH, parent,
|
||||
"Field '%s' value %" PRId64 " is out of range for int32", key, v);
|
||||
return 0;
|
||||
}
|
||||
*out = (int32_t)v;
|
||||
} else {
|
||||
double d = it->get<double>();
|
||||
if (d < (double)INT32_MIN || d > (double)INT32_MAX) {
|
||||
tg3__error_pushf(ctx->errors, ctx->arena, TG3_SEVERITY_ERROR,
|
||||
TG3_ERR_JSON_TYPE_MISMATCH, parent,
|
||||
"Field '%s' value %f is out of range for int32", key, d);
|
||||
return 0;
|
||||
}
|
||||
*out = (int32_t)d;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2315,17 +2412,22 @@ static int tg3__parse_buffer(tg3__parse_ctx *ctx, const tg3__json &o,
|
||||
/* Load buffer data */
|
||||
if (ctx->is_binary && buf_idx == 0 && buf->uri.len == 0) {
|
||||
/* GLB: first buffer uses binary chunk */
|
||||
if (ctx->bin_data && ctx->bin_size >= byte_length) {
|
||||
uint8_t *data = (uint8_t *)tg3__arena_alloc(ctx->arena, (size_t)byte_length);
|
||||
if (!data) {
|
||||
tg3__error_push(ctx->errors, TG3_SEVERITY_ERROR,
|
||||
TG3_ERR_OUT_OF_MEMORY, "OOM for buffer data", NULL, -1);
|
||||
return 0;
|
||||
}
|
||||
memcpy(data, ctx->bin_data, (size_t)byte_length);
|
||||
buf->data.data = data;
|
||||
buf->data.count = byte_length;
|
||||
if (!ctx->bin_data || ctx->bin_size < byte_length) {
|
||||
tg3__error_push(ctx->errors, TG3_SEVERITY_ERROR,
|
||||
TG3_ERR_BUFFER_SIZE_MISMATCH,
|
||||
"GLB BIN chunk missing or smaller than buffer.byteLength",
|
||||
NULL, -1);
|
||||
return 0;
|
||||
}
|
||||
uint8_t *data = (uint8_t *)tg3__arena_alloc(ctx->arena, (size_t)byte_length);
|
||||
if (!data) {
|
||||
tg3__error_push(ctx->errors, TG3_SEVERITY_ERROR,
|
||||
TG3_ERR_OUT_OF_MEMORY, "OOM for buffer data", NULL, -1);
|
||||
return 0;
|
||||
}
|
||||
memcpy(data, ctx->bin_data, (size_t)byte_length);
|
||||
buf->data.data = data;
|
||||
buf->data.count = byte_length;
|
||||
} else if (buf->uri.len > 0) {
|
||||
if (tg3_is_data_uri(buf->uri.data, buf->uri.len)) {
|
||||
/* Data URI */
|
||||
@@ -4325,7 +4427,7 @@ struct tg3_writer {
|
||||
TINYGLTF3_API tg3_writer *tg3_writer_create(
|
||||
tg3_write_chunk_fn chunk_fn, void *user_data,
|
||||
const tg3_write_options *options) {
|
||||
tg3_writer *w = (tg3_writer *)calloc(1, sizeof(tg3_writer));
|
||||
tg3_writer *w = new (std::nothrow) tg3_writer();
|
||||
if (!w) return NULL;
|
||||
w->chunk_fn = chunk_fn;
|
||||
w->user_data = user_data;
|
||||
@@ -4401,12 +4503,10 @@ TINYGLTF3_API tg3_error_code tg3_writer_end(tg3_writer *w) {
|
||||
}
|
||||
|
||||
TINYGLTF3_API void tg3_writer_destroy(tg3_writer *w) {
|
||||
if (w) {
|
||||
w->root.~tinygltf_json();
|
||||
free(w);
|
||||
}
|
||||
delete w;
|
||||
}
|
||||
|
||||
#endif /* legacy header-only v3 implementation */
|
||||
#endif /* TINYGLTF3_IMPLEMENTATION */
|
||||
|
||||
#endif /* TINY_GLTF_V3_H_ */
|
||||
|
||||
@@ -297,8 +297,9 @@ static const char *cj_scan_str(const char *p, const char *end) {
|
||||
* FAST NUMBER PARSING (C-style)
|
||||
*
|
||||
* Uses Clinger's fast path for float conversion, avoiding strtod() for the
|
||||
* vast majority of JSON numbers. This is locale-independent and typically
|
||||
* 4-10x faster than strtod.
|
||||
* vast majority of JSON numbers. The fast path itself is locale-independent
|
||||
* and typically 4-10x faster than strtod; however, rare fallback paths may
|
||||
* still invoke the C library's strtod(), which can be locale-dependent.
|
||||
*
|
||||
* Optional float32 mode (CJ_FLOAT32_MODE flag in cj_parse_number):
|
||||
* Parses floating-point values to float (single) precision and stores
|
||||
@@ -307,7 +308,8 @@ static const char *cj_scan_str(const char *p, const char *end) {
|
||||
* Breaks strict JSON/IEEE-754-double conformance.
|
||||
* ====================================================================== */
|
||||
|
||||
/* Safe double-to-int64 cast: clamp inf/NaN/out-of-range to 0. */
|
||||
/* Safe double-to-int64 cast: returns 0 for NaN; clamps +inf/out-of-range-high
|
||||
* to INT64_MAX and -inf/out-of-range-low to INT64_MIN. */
|
||||
static int64_t cj_dbl_to_i64(double d) {
|
||||
if (d != d) return 0; /* NaN */
|
||||
if (d >= (double)INT64_MAX) return INT64_MAX;
|
||||
@@ -421,9 +423,11 @@ static int cj_fast_flt_convert(uint64_t mantissa, int exp10, int neg, float *out
|
||||
* Returns pointer past the last character consumed, or NULL on error.
|
||||
*
|
||||
* float32_mode: when non-zero, floating-point values are parsed at float
|
||||
* (single) precision — fewer digits are significant, and the result is
|
||||
* stored as (double)(float)value. This is faster but not JSON-conformant
|
||||
* for high-precision doubles.
|
||||
* (single) precision — only 9 significant digits are tracked for the
|
||||
* fraction part, and the result is stored as (double)(float)value. This
|
||||
* is faster but not JSON-conformant for high-precision doubles. Integer-
|
||||
* only tokens (no '.'/'e') are always parsed at full int64 precision
|
||||
* regardless of this flag.
|
||||
*
|
||||
* Uses Clinger's fast path (no strtod) for ~99% of JSON float values.
|
||||
* Falls back to strtod only for extreme exponents or >19 significant digits. */
|
||||
@@ -443,8 +447,12 @@ static const char *cj_parse_number(const char *p, const char *end,
|
||||
int mantissa_overflow = 0; /* set if >19 significant digits */
|
||||
int has_frac = 0, has_exp = 0;
|
||||
|
||||
/* Max significant digits we track: 19 for double, 9 for float32 */
|
||||
int max_sig = float32_mode ? 9 : 19;
|
||||
/* Max significant digits we track:
|
||||
* Integer part: always 19, so integer-only tokens (no '.'/'e') are always
|
||||
* accumulated fully and can be typed as int64 regardless of float32_mode.
|
||||
* Fraction part: 9 in float32_mode (single precision), 19 otherwise. */
|
||||
int max_sig_int = 19;
|
||||
int max_sig_frac = float32_mode ? 9 : 19;
|
||||
|
||||
/* Integer part */
|
||||
if (*p == '0') {
|
||||
@@ -452,7 +460,7 @@ static const char *cj_parse_number(const char *p, const char *end,
|
||||
} else if ((unsigned)(*p - '1') <= 8u) {
|
||||
while (p < end && (unsigned)(*p - '0') <= 9u) {
|
||||
unsigned d = (unsigned)(*p - '0');
|
||||
if (ndigits < max_sig) {
|
||||
if (ndigits < max_sig_int) {
|
||||
mantissa = mantissa * 10 + d;
|
||||
} else {
|
||||
exp10++; /* excess digit: bump exponent instead */
|
||||
@@ -473,7 +481,7 @@ static const char *cj_parse_number(const char *p, const char *end,
|
||||
if (p >= end || (unsigned)(*p - '0') > 9u) return NULL;
|
||||
while (p < end && (unsigned)(*p - '0') <= 9u) {
|
||||
unsigned d = (unsigned)(*p - '0');
|
||||
if (ndigits < max_sig) {
|
||||
if (ndigits < max_sig_frac) {
|
||||
mantissa = mantissa * 10 + d;
|
||||
exp10--;
|
||||
}
|
||||
|
||||
1743
tinygltf_json_c.h
Normal file
1743
tinygltf_json_c.h
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user