mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-06-08 03:03:50 +00:00
1087 lines
33 KiB
C++
1087 lines
33 KiB
C++
/*
|
|
* Intensive parser unit tests for tinygltf with the custom JSON backend
|
|
* (tinygltf_json.h).
|
|
*
|
|
* These tests exercise the custom JSON parser and glTF loader with a wide
|
|
* range of edge-case inputs: deeply nested structures, malformed JSON,
|
|
* truncated inputs, unicode escapes, number edge cases, and round-trip
|
|
* serialisation/deserialisation.
|
|
*
|
|
* Build:
|
|
* c++ -std=c++11 -DTINYGLTF_USE_CUSTOM_JSON -I.. -I. -g -O0 \
|
|
* -o tester_intensive_customjson tester_intensive_customjson.cc
|
|
*/
|
|
|
|
#define TINYGLTF_IMPLEMENTATION
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#ifndef TINYGLTF_USE_CUSTOM_JSON
|
|
#define TINYGLTF_USE_CUSTOM_JSON
|
|
#endif
|
|
#include "tiny_gltf.h"
|
|
|
|
#define CATCH_CONFIG_MAIN
|
|
#include "catch.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cassert>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <cstring>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
/* ---------- helpers --------------------------------------------------------*/
|
|
|
|
/* Convenience: parse JSON string through the custom backend. */
|
|
static tinygltf::detail::JsonDocument JsonConstruct(const char *str) {
|
|
tinygltf::detail::JsonDocument doc;
|
|
tinygltf::detail::JsonParse(doc, str, strlen(str));
|
|
return doc;
|
|
}
|
|
|
|
/* Helper: access the i-th element of an array-type json node. */
|
|
static tinygltf::detail::json &JsonArrayAt(tinygltf::detail::json &arr,
|
|
size_t idx) {
|
|
auto it = arr.begin();
|
|
for (size_t i = 0; i < idx; ++i) ++it;
|
|
return *it;
|
|
}
|
|
|
|
/* Load a glTF JSON string through TinyGLTF and return success. */
|
|
static bool LoadGltfFromString(const char *json,
|
|
tinygltf::Model *model = nullptr,
|
|
std::string *err = nullptr,
|
|
std::string *warn = nullptr) {
|
|
tinygltf::Model tmp_model;
|
|
std::string tmp_err, tmp_warn;
|
|
if (!model) model = &tmp_model;
|
|
if (!err) err = &tmp_err;
|
|
if (!warn) warn = &tmp_warn;
|
|
tinygltf::TinyGLTF ctx;
|
|
return ctx.LoadASCIIFromString(model, err, warn, json,
|
|
static_cast<unsigned int>(strlen(json)),
|
|
/* base_dir */ "");
|
|
}
|
|
|
|
/* ===== Section 1: Custom JSON parser edge-cases =========================== */
|
|
|
|
TEST_CASE("cj-parse-empty-input", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-parse-whitespace-only", "[customjson][parse]") {
|
|
auto doc = JsonConstruct(" \t\n\r ");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-parse-null", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("null");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-parse-true", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("true");
|
|
REQUIRE(doc.is_boolean());
|
|
REQUIRE(doc.get<bool>() == true);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-false", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("false");
|
|
REQUIRE(doc.is_boolean());
|
|
REQUIRE(doc.get<bool>() == false);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-integer-zero", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("0");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<int>() == 0);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-negative-zero", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("-0");
|
|
REQUIRE(doc.is_number());
|
|
// -0 should still compare equal to 0
|
|
REQUIRE(doc.get<double>() == 0.0);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-positive-integer", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("42");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<int>() == 42);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-negative-integer", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("-999");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<int>() == -999);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-large-integer", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("2147483647");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<int>() == 2147483647);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-double", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("3.14159");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<double>() == Approx(3.14159));
|
|
}
|
|
|
|
TEST_CASE("cj-parse-double-exponent", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("1.5e10");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<double>() == Approx(1.5e10));
|
|
}
|
|
|
|
TEST_CASE("cj-parse-double-negative-exponent", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("2.5e-3");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<double>() == Approx(0.0025));
|
|
}
|
|
|
|
TEST_CASE("cj-parse-double-positive-exponent", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("1E+2");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<double>() == Approx(100.0));
|
|
}
|
|
|
|
TEST_CASE("cj-parse-simple-string", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("\"hello world\"");
|
|
REQUIRE(doc.is_string());
|
|
REQUIRE(doc.get<std::string>() == "hello world");
|
|
}
|
|
|
|
TEST_CASE("cj-parse-empty-string", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("\"\"");
|
|
REQUIRE(doc.is_string());
|
|
REQUIRE(doc.get<std::string>().empty());
|
|
}
|
|
|
|
TEST_CASE("cj-parse-string-escapes", "[customjson][parse]") {
|
|
// Test all standard JSON escape sequences
|
|
auto doc = JsonConstruct("\"a\\nb\\tc\\rd\\\\e\\\"/f\"");
|
|
REQUIRE(doc.is_string());
|
|
std::string expected = "a\nb\tc\rd\\e\"/f";
|
|
REQUIRE(doc.get<std::string>() == expected);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-string-unicode-escape", "[customjson][parse]") {
|
|
// \u0041 == 'A'
|
|
auto doc = JsonConstruct("\"\\u0041\"");
|
|
REQUIRE(doc.is_string());
|
|
REQUIRE(doc.get<std::string>() == "A");
|
|
}
|
|
|
|
TEST_CASE("cj-parse-string-unicode-non-ascii", "[customjson][parse]") {
|
|
// \u00E9 == é (UTF-8: 0xC3 0xA9)
|
|
auto doc = JsonConstruct("\"caf\\u00E9\"");
|
|
REQUIRE(doc.is_string());
|
|
REQUIRE(doc.get<std::string>() == "caf\xC3\xA9");
|
|
}
|
|
|
|
TEST_CASE("cj-parse-string-unicode-cjk", "[customjson][parse]") {
|
|
// \u4E16 == 世 (UTF-8: 0xE4 0xB8 0x96)
|
|
auto doc = JsonConstruct("\"\\u4E16\"");
|
|
REQUIRE(doc.is_string());
|
|
std::string expected;
|
|
expected += '\xE4';
|
|
expected += '\xB8';
|
|
expected += '\x96';
|
|
REQUIRE(doc.get<std::string>() == expected);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-empty-array", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("[]");
|
|
REQUIRE(doc.is_array());
|
|
REQUIRE(doc.size() == 0);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-array-of-ints", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("[1, 2, 3, 4, 5]");
|
|
REQUIRE(doc.is_array());
|
|
REQUIRE(doc.size() == 5);
|
|
for (size_t i = 0; i < 5; i++) {
|
|
REQUIRE(JsonArrayAt(doc, i).get<int>() == static_cast<int>(i + 1));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("cj-parse-nested-arrays", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("[[1, 2], [3, [4, 5]]]");
|
|
REQUIRE(doc.is_array());
|
|
REQUIRE(doc.size() == 2);
|
|
REQUIRE(JsonArrayAt(doc, 0).is_array());
|
|
REQUIRE(JsonArrayAt(doc, 0).size() == 2);
|
|
REQUIRE(JsonArrayAt(doc, 1).is_array());
|
|
REQUIRE(JsonArrayAt(doc, 1).size() == 2);
|
|
REQUIRE(JsonArrayAt(JsonArrayAt(doc, 1), 1).is_array());
|
|
REQUIRE(JsonArrayAt(JsonArrayAt(doc, 1), 1).size() == 2);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-empty-object", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("{}");
|
|
REQUIRE(doc.is_object());
|
|
REQUIRE(doc.size() == 0);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-simple-object", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("{\"a\": 1, \"b\": \"hello\", \"c\": true}");
|
|
REQUIRE(doc.is_object());
|
|
REQUIRE(doc.size() == 3);
|
|
|
|
int val = 0;
|
|
REQUIRE(tinygltf::detail::GetInt(doc["a"], val));
|
|
REQUIRE(val == 1);
|
|
|
|
std::string s;
|
|
REQUIRE(tinygltf::detail::GetString(doc["b"], s));
|
|
REQUIRE(s == "hello");
|
|
|
|
REQUIRE(doc["c"].get<bool>() == true);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-nested-object", "[customjson][parse]") {
|
|
auto doc = JsonConstruct("{\"outer\": {\"inner\": {\"value\": 42}}}");
|
|
REQUIRE(doc.is_object());
|
|
REQUIRE(doc["outer"].is_object());
|
|
REQUIRE(doc["outer"]["inner"].is_object());
|
|
int v = 0;
|
|
REQUIRE(tinygltf::detail::GetInt(doc["outer"]["inner"]["value"], v));
|
|
REQUIRE(v == 42);
|
|
}
|
|
|
|
TEST_CASE("cj-parse-mixed-types-array", "[customjson][parse]") {
|
|
auto doc =
|
|
JsonConstruct("[null, true, false, 42, 3.14, \"text\", [], {}]");
|
|
REQUIRE(doc.is_array());
|
|
REQUIRE(doc.size() == 8);
|
|
REQUIRE(JsonArrayAt(doc, 0).is_null());
|
|
REQUIRE(JsonArrayAt(doc, 1).is_boolean());
|
|
REQUIRE(JsonArrayAt(doc, 2).is_boolean());
|
|
REQUIRE(JsonArrayAt(doc, 3).is_number());
|
|
REQUIRE(JsonArrayAt(doc, 4).is_number());
|
|
REQUIRE(JsonArrayAt(doc, 5).is_string());
|
|
REQUIRE(JsonArrayAt(doc, 6).is_array());
|
|
REQUIRE(JsonArrayAt(doc, 7).is_object());
|
|
}
|
|
|
|
/* ===== Section 2: Deeply nested structures ================================ */
|
|
|
|
TEST_CASE("cj-deep-nested-arrays", "[customjson][depth]") {
|
|
// Build a deeply nested array: [[[[...]]]]
|
|
const int depth = 200;
|
|
std::string json;
|
|
for (int i = 0; i < depth; i++) json += "[";
|
|
json += "1";
|
|
for (int i = 0; i < depth; i++) json += "]";
|
|
|
|
auto doc = JsonConstruct(json.c_str());
|
|
REQUIRE(doc.is_array());
|
|
|
|
// Walk down
|
|
tinygltf::detail::json *cur = &doc;
|
|
for (int i = 0; i < depth - 1; i++) {
|
|
REQUIRE(cur->is_array());
|
|
REQUIRE(cur->size() == 1);
|
|
cur = &JsonArrayAt(*cur, 0);
|
|
}
|
|
REQUIRE(cur->is_array());
|
|
REQUIRE(cur->size() == 1);
|
|
REQUIRE(JsonArrayAt(*cur, 0).get<int>() == 1);
|
|
}
|
|
|
|
TEST_CASE("cj-deep-nested-objects", "[customjson][depth]") {
|
|
const int depth = 100;
|
|
std::string json;
|
|
for (int i = 0; i < depth; i++) {
|
|
json += "{\"k\":";
|
|
}
|
|
json += "42";
|
|
for (int i = 0; i < depth; i++) {
|
|
json += "}";
|
|
}
|
|
|
|
auto doc = JsonConstruct(json.c_str());
|
|
REQUIRE(doc.is_object());
|
|
|
|
tinygltf::detail::json *cur = &doc;
|
|
for (int i = 0; i < depth - 1; i++) {
|
|
REQUIRE(cur->is_object());
|
|
cur = &((*cur)["k"]);
|
|
}
|
|
REQUIRE(cur->is_object());
|
|
int v = 0;
|
|
REQUIRE(tinygltf::detail::GetInt((*cur)["k"], v));
|
|
REQUIRE(v == 42);
|
|
}
|
|
|
|
/* ===== Section 3: Malformed input handling ================================ */
|
|
|
|
TEST_CASE("cj-malformed-truncated-object", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{\"key\":");
|
|
// Should return null on parse error
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-truncated-array", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("[1, 2,");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-trailing-comma-array", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("[1, 2, 3,]");
|
|
// Trailing comma is not valid JSON
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-trailing-comma-object", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{\"a\": 1,}");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-missing-colon", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{\"key\" \"value\"}");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-missing-comma", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("[1 2 3]");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-double-comma", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("[1,,2]");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-unquoted-key", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{key: 1}");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-single-quoted-string", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{'key': 1}");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-unterminated-string", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("\"hello");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-bad-escape", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("\"bad\\xescape\"");
|
|
// \\x is not a valid JSON escape
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-truncated-unicode-escape", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("\"trunc\\u00\"");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-incomplete-true", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("tru");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-incomplete-false", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("fals");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-incomplete-null", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("nul");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-extra-data", "[customjson][malformed]") {
|
|
// Valid JSON followed by extra non-whitespace data
|
|
auto doc = JsonConstruct("42 extra");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-trailing-whitespace-ok", "[customjson][parse]") {
|
|
// Valid JSON followed by whitespace only should be accepted
|
|
auto doc = JsonConstruct("42 \t\n ");
|
|
REQUIRE(doc.is_number());
|
|
REQUIRE(doc.get<int>() == 42);
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-just-brace", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-just-bracket", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("[");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-mismatched-brackets", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("[}");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
TEST_CASE("cj-malformed-mismatched-braces", "[customjson][malformed]") {
|
|
auto doc = JsonConstruct("{]");
|
|
REQUIRE(doc.is_null());
|
|
}
|
|
|
|
/* ===== Section 4: glTF-level loading edge-cases =========================== */
|
|
|
|
TEST_CASE("cj-gltf-empty-string", "[customjson][gltf]") {
|
|
REQUIRE(false == LoadGltfFromString(""));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-garbage-input", "[customjson][gltf]") {
|
|
REQUIRE(false == LoadGltfFromString("not json at all"));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-valid-null", "[customjson][gltf]") {
|
|
REQUIRE(false == LoadGltfFromString("null"));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-valid-array-root", "[customjson][gltf]") {
|
|
// glTF expects root to be an object, not an array
|
|
REQUIRE(false == LoadGltfFromString("[]"));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-minimal-valid", "[customjson][gltf]") {
|
|
// Minimal valid glTF 2.0
|
|
const char *json = R"({"asset":{"version":"2.0"}})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.asset.version == "2.0");
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-empty-meshes", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"meshes": []
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.meshes.empty());
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-scene-with-nodes", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"scenes": [{"nodes": [0]}],
|
|
"nodes": [{"name": "TestNode"}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.scenes.size() == 1);
|
|
REQUIRE(model.nodes.size() == 1);
|
|
REQUIRE(model.nodes[0].name == "TestNode");
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-node-with-transform", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"nodes": [{
|
|
"translation": [1.0, 2.0, 3.0],
|
|
"rotation": [0.0, 0.0, 0.0, 1.0],
|
|
"scale": [1.0, 1.0, 1.0]
|
|
}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.nodes.size() == 1);
|
|
REQUIRE(model.nodes[0].translation.size() == 3);
|
|
REQUIRE(model.nodes[0].translation[0] == Approx(1.0));
|
|
REQUIRE(model.nodes[0].translation[1] == Approx(2.0));
|
|
REQUIRE(model.nodes[0].translation[2] == Approx(3.0));
|
|
REQUIRE(model.nodes[0].rotation.size() == 4);
|
|
REQUIRE(model.nodes[0].scale.size() == 3);
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-material-pbr", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"materials": [{
|
|
"pbrMetallicRoughness": {
|
|
"baseColorFactor": [1.0, 0.5, 0.25, 1.0],
|
|
"metallicFactor": 0.8,
|
|
"roughnessFactor": 0.2
|
|
}
|
|
}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.materials.size() == 1);
|
|
auto &pbr = model.materials[0].pbrMetallicRoughness;
|
|
REQUIRE(pbr.baseColorFactor.size() == 4);
|
|
REQUIRE(pbr.baseColorFactor[0] == Approx(1.0));
|
|
REQUIRE(pbr.baseColorFactor[1] == Approx(0.5));
|
|
REQUIRE(pbr.baseColorFactor[2] == Approx(0.25));
|
|
REQUIRE(pbr.metallicFactor == Approx(0.8));
|
|
REQUIRE(pbr.roughnessFactor == Approx(0.2));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-accessors-and-bufferviews", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"buffers": [{"uri": "data:application/octet-stream;base64,AAAAAAAAAAA=", "byteLength": 8}],
|
|
"bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 8}],
|
|
"accessors": [{
|
|
"bufferView": 0,
|
|
"componentType": 5126,
|
|
"count": 2,
|
|
"type": "SCALAR",
|
|
"max": [1.0],
|
|
"min": [0.0]
|
|
}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.buffers.size() == 1);
|
|
REQUIRE(model.bufferViews.size() == 1);
|
|
REQUIRE(model.accessors.size() == 1);
|
|
REQUIRE(model.accessors[0].componentType == TINYGLTF_COMPONENT_TYPE_FLOAT);
|
|
REQUIRE(model.accessors[0].count == 2);
|
|
REQUIRE(model.accessors[0].type == TINYGLTF_TYPE_SCALAR);
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-extensions", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"extensionsUsed": ["KHR_lights_punctual"],
|
|
"extensions": {
|
|
"KHR_lights_punctual": {
|
|
"lights": [{
|
|
"color": [1.0, 1.0, 1.0],
|
|
"intensity": 5.0,
|
|
"type": "point"
|
|
}]
|
|
}
|
|
}
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.extensionsUsed.size() == 1);
|
|
REQUIRE(model.extensionsUsed[0] == "KHR_lights_punctual");
|
|
REQUIRE(model.extensions.count("KHR_lights_punctual") == 1);
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-extras", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"nodes": [{
|
|
"name": "Test",
|
|
"extras": {"custom_field": 123, "nested": {"a": [1,2,3]}}
|
|
}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.nodes.size() == 1);
|
|
// extras should be preserved as a Value
|
|
REQUIRE(model.nodes[0].extras.IsObject());
|
|
}
|
|
|
|
/* ===== Section 5: Round-trip serialisation ================================ */
|
|
|
|
TEST_CASE("cj-roundtrip-minimal", "[customjson][roundtrip]") {
|
|
const char *json = R"({"asset":{"version":"2.0"}})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
|
|
// Serialise to string
|
|
std::stringstream os;
|
|
tinygltf::TinyGLTF ctx;
|
|
REQUIRE(true == ctx.WriteGltfSceneToStream(&model, os, false, false));
|
|
|
|
// Re-parse and verify
|
|
tinygltf::Model model2;
|
|
std::string json2 = os.str();
|
|
REQUIRE(true ==
|
|
ctx.LoadASCIIFromString(&model2, &err, &warn, json2.c_str(),
|
|
static_cast<unsigned int>(json2.size()), ""));
|
|
REQUIRE(model2.asset.version == "2.0");
|
|
}
|
|
|
|
TEST_CASE("cj-roundtrip-with-nodes", "[customjson][roundtrip]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"scenes": [{"nodes": [0]}],
|
|
"nodes": [
|
|
{"name": "Root", "children": [1]},
|
|
{"name": "Child", "translation": [1.0, 2.0, 3.0]}
|
|
]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
|
|
std::stringstream os;
|
|
tinygltf::TinyGLTF ctx;
|
|
REQUIRE(true == ctx.WriteGltfSceneToStream(&model, os, false, false));
|
|
|
|
tinygltf::Model model2;
|
|
std::string json2 = os.str();
|
|
REQUIRE(true ==
|
|
ctx.LoadASCIIFromString(&model2, &err, &warn, json2.c_str(),
|
|
static_cast<unsigned int>(json2.size()), ""));
|
|
REQUIRE(model2.nodes.size() == 2);
|
|
REQUIRE(model2.nodes[0].name == "Root");
|
|
REQUIRE(model2.nodes[1].name == "Child");
|
|
REQUIRE(model2.nodes[1].translation[0] == Approx(1.0));
|
|
}
|
|
|
|
TEST_CASE("cj-roundtrip-material", "[customjson][roundtrip]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"materials": [{
|
|
"name": "TestMat",
|
|
"pbrMetallicRoughness": {
|
|
"baseColorFactor": [0.8, 0.2, 0.1, 1.0],
|
|
"metallicFactor": 0.5,
|
|
"roughnessFactor": 0.7
|
|
},
|
|
"doubleSided": true
|
|
}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
|
|
std::stringstream os;
|
|
tinygltf::TinyGLTF ctx;
|
|
REQUIRE(true == ctx.WriteGltfSceneToStream(&model, os, false, false));
|
|
|
|
tinygltf::Model model2;
|
|
std::string json2 = os.str();
|
|
REQUIRE(true ==
|
|
ctx.LoadASCIIFromString(&model2, &err, &warn, json2.c_str(),
|
|
static_cast<unsigned int>(json2.size()), ""));
|
|
REQUIRE(model2.materials.size() == 1);
|
|
REQUIRE(model2.materials[0].name == "TestMat");
|
|
REQUIRE(model2.materials[0].doubleSided == true);
|
|
REQUIRE(model2.materials[0].pbrMetallicRoughness.metallicFactor ==
|
|
Approx(0.5));
|
|
}
|
|
|
|
/* ===== Section 6: Binary (GLB) loading ==================================== */
|
|
|
|
TEST_CASE("cj-glb-invalid-magic", "[customjson][glb]") {
|
|
// GLB header: magic (4), version (4), length (4) = 12 bytes
|
|
// Wrong magic number
|
|
unsigned char data[12] = {0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
|
|
0x00, 0x00, 0x0C, 0x00, 0x00, 0x00};
|
|
tinygltf::Model model;
|
|
tinygltf::TinyGLTF ctx;
|
|
std::string err, warn;
|
|
bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, data, sizeof(data));
|
|
REQUIRE(false == ret);
|
|
}
|
|
|
|
TEST_CASE("cj-glb-truncated-header", "[customjson][glb]") {
|
|
// Less than 12 bytes
|
|
unsigned char data[8] = {0x67, 0x6C, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00};
|
|
tinygltf::Model model;
|
|
tinygltf::TinyGLTF ctx;
|
|
std::string err, warn;
|
|
bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, data, sizeof(data));
|
|
REQUIRE(false == ret);
|
|
}
|
|
|
|
TEST_CASE("cj-glb-zero-length", "[customjson][glb]") {
|
|
tinygltf::Model model;
|
|
tinygltf::TinyGLTF ctx;
|
|
std::string err, warn;
|
|
bool ret = ctx.LoadBinaryFromMemory(&model, &err, &warn, nullptr, 0);
|
|
REQUIRE(false == ret);
|
|
}
|
|
|
|
/* ===== Section 7: JSON detail helpers ===================================== */
|
|
|
|
TEST_CASE("cj-detail-GetInt", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("{\"val\": 42}");
|
|
int v = 0;
|
|
REQUIRE(tinygltf::detail::GetInt(doc["val"], v));
|
|
REQUIRE(v == 42);
|
|
}
|
|
|
|
TEST_CASE("cj-detail-GetDouble", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("{\"val\": 3.14}");
|
|
double v = 0;
|
|
REQUIRE(tinygltf::detail::GetDouble(doc["val"], v));
|
|
REQUIRE(v == Approx(3.14));
|
|
}
|
|
|
|
TEST_CASE("cj-detail-GetNumber", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("{\"val\": 99}");
|
|
double v = 0;
|
|
REQUIRE(tinygltf::detail::GetNumber(doc["val"], v));
|
|
REQUIRE(v == Approx(99.0));
|
|
}
|
|
|
|
TEST_CASE("cj-detail-GetString", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("{\"val\": \"hello\"}");
|
|
std::string v;
|
|
REQUIRE(tinygltf::detail::GetString(doc["val"], v));
|
|
REQUIRE(v == "hello");
|
|
}
|
|
|
|
TEST_CASE("cj-detail-IsArray", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("[1, 2]");
|
|
REQUIRE(tinygltf::detail::IsArray(doc));
|
|
auto doc2 = JsonConstruct("42");
|
|
REQUIRE_FALSE(tinygltf::detail::IsArray(doc2));
|
|
}
|
|
|
|
TEST_CASE("cj-detail-IsObject", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("{\"a\": 1}");
|
|
REQUIRE(tinygltf::detail::IsObject(doc));
|
|
auto doc2 = JsonConstruct("[]");
|
|
REQUIRE_FALSE(tinygltf::detail::IsObject(doc2));
|
|
}
|
|
|
|
TEST_CASE("cj-detail-IsEmpty", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("{}");
|
|
REQUIRE(tinygltf::detail::IsEmpty(doc));
|
|
auto doc2 = JsonConstruct("{\"a\": 1}");
|
|
REQUIRE_FALSE(tinygltf::detail::IsEmpty(doc2));
|
|
}
|
|
|
|
TEST_CASE("cj-detail-JsonIsNull", "[customjson][detail]") {
|
|
auto doc = JsonConstruct("null");
|
|
REQUIRE(tinygltf::detail::JsonIsNull(doc));
|
|
auto doc2 = JsonConstruct("42");
|
|
REQUIRE_FALSE(tinygltf::detail::JsonIsNull(doc2));
|
|
}
|
|
|
|
/* ===== Section 8: Serialisation helpers =================================== */
|
|
|
|
TEST_CASE("cj-detail-JsonFromString", "[customjson][detail]") {
|
|
auto j = tinygltf::detail::JsonFromString("test");
|
|
REQUIRE(j.is_string());
|
|
REQUIRE(j.get<std::string>() == "test");
|
|
}
|
|
|
|
TEST_CASE("cj-detail-JsonSetObject", "[customjson][detail]") {
|
|
tinygltf::detail::json j;
|
|
tinygltf::detail::JsonSetObject(j);
|
|
REQUIRE(j.is_object());
|
|
REQUIRE(j.size() == 0);
|
|
}
|
|
|
|
TEST_CASE("cj-detail-JsonAddMember", "[customjson][detail]") {
|
|
tinygltf::detail::json j;
|
|
tinygltf::detail::JsonSetObject(j);
|
|
auto val = tinygltf::detail::JsonFromString("bar");
|
|
tinygltf::detail::JsonAddMember(j, "foo", std::move(val));
|
|
REQUIRE(j.size() == 1);
|
|
std::string s;
|
|
REQUIRE(tinygltf::detail::GetString(j["foo"], s));
|
|
REQUIRE(s == "bar");
|
|
}
|
|
|
|
TEST_CASE("cj-detail-JsonPushBack", "[customjson][detail]") {
|
|
auto j = JsonConstruct("[]");
|
|
tinygltf::detail::json val;
|
|
val = JsonConstruct("42");
|
|
tinygltf::detail::JsonPushBack(j, std::move(val));
|
|
REQUIRE(j.size() == 1);
|
|
REQUIRE(JsonArrayAt(j, 0).get<int>() == 42);
|
|
}
|
|
|
|
/* ===== Section 9: Stress / large input ==================================== */
|
|
|
|
TEST_CASE("cj-large-array", "[customjson][stress]") {
|
|
// Array with 1000 integers
|
|
std::string json = "[";
|
|
for (int i = 0; i < 1000; i++) {
|
|
if (i > 0) json += ",";
|
|
json += std::to_string(i);
|
|
}
|
|
json += "]";
|
|
auto doc = JsonConstruct(json.c_str());
|
|
REQUIRE(doc.is_array());
|
|
REQUIRE(doc.size() == 1000);
|
|
REQUIRE(JsonArrayAt(doc, 0).get<int>() == 0);
|
|
REQUIRE(JsonArrayAt(doc, 999).get<int>() == 999);
|
|
}
|
|
|
|
TEST_CASE("cj-large-object", "[customjson][stress]") {
|
|
// Object with 500 keys
|
|
std::string json = "{";
|
|
for (int i = 0; i < 500; i++) {
|
|
if (i > 0) json += ",";
|
|
json += "\"key" + std::to_string(i) + "\":" + std::to_string(i);
|
|
}
|
|
json += "}";
|
|
auto doc = JsonConstruct(json.c_str());
|
|
REQUIRE(doc.is_object());
|
|
REQUIRE(doc.size() == 500);
|
|
int v = -1;
|
|
REQUIRE(tinygltf::detail::GetInt(doc["key0"], v));
|
|
REQUIRE(v == 0);
|
|
REQUIRE(tinygltf::detail::GetInt(doc["key499"], v));
|
|
REQUIRE(v == 499);
|
|
}
|
|
|
|
TEST_CASE("cj-long-string", "[customjson][stress]") {
|
|
// String with 10000 characters
|
|
std::string content(10000, 'x');
|
|
std::string json = "\"" + content + "\"";
|
|
auto doc = JsonConstruct(json.c_str());
|
|
REQUIRE(doc.is_string());
|
|
REQUIRE(doc.get<std::string>().size() == 10000);
|
|
}
|
|
|
|
TEST_CASE("cj-many-escapes", "[customjson][stress]") {
|
|
// String with many escape sequences
|
|
std::string json = "\"";
|
|
for (int i = 0; i < 500; i++) {
|
|
json += "\\n\\t";
|
|
}
|
|
json += "\"";
|
|
auto doc = JsonConstruct(json.c_str());
|
|
REQUIRE(doc.is_string());
|
|
REQUIRE(doc.get<std::string>().size() == 1000);
|
|
}
|
|
|
|
/* ===== Section 10: glTF complex models ==================================== */
|
|
|
|
TEST_CASE("cj-gltf-multiple-meshes-primitives", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"meshes": [
|
|
{"primitives": [{"attributes": {"POSITION": 0}, "mode": 4}]},
|
|
{"primitives": [
|
|
{"attributes": {"POSITION": 0}, "mode": 4},
|
|
{"attributes": {"POSITION": 0, "NORMAL": 1}, "mode": 4}
|
|
]}
|
|
],
|
|
"accessors": [
|
|
{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3",
|
|
"max": [1,1,1], "min": [0,0,0]},
|
|
{"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3",
|
|
"max": [1,1,1], "min": [0,0,0]}
|
|
],
|
|
"bufferViews": [{"buffer": 0, "byteLength": 36}],
|
|
"buffers": [{"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "byteLength": 36}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.meshes.size() == 2);
|
|
REQUIRE(model.meshes[0].primitives.size() == 1);
|
|
REQUIRE(model.meshes[1].primitives.size() == 2);
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-animations", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"animations": [{
|
|
"channels": [{"sampler": 0, "target": {"node": 0, "path": "translation"}}],
|
|
"samplers": [{"input": 0, "output": 1, "interpolation": "LINEAR"}]
|
|
}],
|
|
"nodes": [{"name": "AnimNode"}],
|
|
"accessors": [
|
|
{"bufferView": 0, "componentType": 5126, "count": 2, "type": "SCALAR",
|
|
"max": [1.0], "min": [0.0]},
|
|
{"bufferView": 0, "componentType": 5126, "count": 2, "type": "VEC3",
|
|
"max": [1,1,1], "min": [0,0,0]}
|
|
],
|
|
"bufferViews": [{"buffer": 0, "byteLength": 24}],
|
|
"buffers": [{"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "byteLength": 24}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.animations.size() == 1);
|
|
REQUIRE(model.animations[0].channels.size() == 1);
|
|
REQUIRE(model.animations[0].samplers.size() == 1);
|
|
REQUIRE(model.animations[0].channels[0].target_path == "translation");
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-cameras", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"cameras": [
|
|
{
|
|
"type": "perspective",
|
|
"perspective": {
|
|
"aspectRatio": 1.5,
|
|
"yfov": 0.66,
|
|
"zfar": 100.0,
|
|
"znear": 0.01
|
|
}
|
|
},
|
|
{
|
|
"type": "orthographic",
|
|
"orthographic": {
|
|
"xmag": 10.0,
|
|
"ymag": 10.0,
|
|
"zfar": 100.0,
|
|
"znear": 0.01
|
|
}
|
|
}
|
|
]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.cameras.size() == 2);
|
|
REQUIRE(model.cameras[0].type == "perspective");
|
|
REQUIRE(model.cameras[1].type == "orthographic");
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-skins", "[customjson][gltf]") {
|
|
const char *json = R"({
|
|
"asset": {"version": "2.0"},
|
|
"nodes": [
|
|
{"name": "Armature"},
|
|
{"name": "Bone1"},
|
|
{"name": "Bone2"}
|
|
],
|
|
"skins": [{
|
|
"joints": [1, 2],
|
|
"skeleton": 0
|
|
}]
|
|
})";
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
REQUIRE(true == LoadGltfFromString(json, &model, &err, &warn));
|
|
REQUIRE(model.skins.size() == 1);
|
|
REQUIRE(model.skins[0].joints.size() == 2);
|
|
REQUIRE(model.skins[0].skeleton == 0);
|
|
}
|
|
|
|
/* ===== Section 11: Truncated glTF JSON at various positions =============== */
|
|
|
|
TEST_CASE("cj-gltf-truncated-at-key", "[customjson][truncated]") {
|
|
REQUIRE(false == LoadGltfFromString("{\"asset\""));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-truncated-at-colon", "[customjson][truncated]") {
|
|
REQUIRE(false == LoadGltfFromString("{\"asset\":"));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-truncated-mid-value", "[customjson][truncated]") {
|
|
REQUIRE(false == LoadGltfFromString("{\"asset\":{\"ver"));
|
|
}
|
|
|
|
TEST_CASE("cj-gltf-truncated-after-comma", "[customjson][truncated]") {
|
|
REQUIRE(false ==
|
|
LoadGltfFromString("{\"asset\":{\"version\":\"2.0\"},"));
|
|
}
|
|
|
|
/* ===== Section 12: JSON dump / serialisation ============================== */
|
|
|
|
TEST_CASE("cj-dump-null", "[customjson][dump]") {
|
|
auto doc = JsonConstruct("null");
|
|
std::string s = doc.dump(-1);
|
|
REQUIRE(s == "null");
|
|
}
|
|
|
|
TEST_CASE("cj-dump-bool", "[customjson][dump]") {
|
|
auto doc = JsonConstruct("true");
|
|
REQUIRE(doc.dump(-1) == "true");
|
|
auto doc2 = JsonConstruct("false");
|
|
REQUIRE(doc2.dump(-1) == "false");
|
|
}
|
|
|
|
TEST_CASE("cj-dump-int", "[customjson][dump]") {
|
|
auto doc = JsonConstruct("42");
|
|
REQUIRE(doc.dump(-1) == "42");
|
|
}
|
|
|
|
TEST_CASE("cj-dump-string", "[customjson][dump]") {
|
|
auto doc = JsonConstruct("\"hello\"");
|
|
std::string s = doc.dump(-1);
|
|
REQUIRE(s == "\"hello\"");
|
|
}
|
|
|
|
TEST_CASE("cj-dump-array", "[customjson][dump]") {
|
|
auto doc = JsonConstruct("[1,2,3]");
|
|
std::string s = doc.dump(-1);
|
|
// Should contain all three values
|
|
REQUIRE(s.find("1") != std::string::npos);
|
|
REQUIRE(s.find("2") != std::string::npos);
|
|
REQUIRE(s.find("3") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("cj-dump-object", "[customjson][dump]") {
|
|
auto doc = JsonConstruct("{\"key\":\"val\"}");
|
|
std::string s = doc.dump(-1);
|
|
REQUIRE(s.find("\"key\"") != std::string::npos);
|
|
REQUIRE(s.find("\"val\"") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("cj-dump-roundtrip", "[customjson][dump]") {
|
|
const char *input = "{\"a\":[1,2,3],\"b\":{\"c\":true}}";
|
|
auto doc = JsonConstruct(input);
|
|
std::string s = doc.dump(-1);
|
|
// Re-parse the dump output
|
|
auto doc2 = JsonConstruct(s.c_str());
|
|
REQUIRE(doc2.is_object());
|
|
REQUIRE(doc2["a"].is_array());
|
|
REQUIRE(doc2["a"].size() == 3);
|
|
REQUIRE(doc2["b"]["c"].get<bool>() == true);
|
|
}
|
|
|
|
/* ===== Section 13: Fuzz-like systematic truncation ======================== */
|
|
|
|
TEST_CASE("cj-systematic-truncation", "[customjson][fuzzlike]") {
|
|
// Take a valid JSON string and verify the parser doesn't crash
|
|
// at every truncation point
|
|
const char *valid_json =
|
|
R"({"asset":{"version":"2.0"},"nodes":[{"name":"test","translation":[1.0,2.0,3.0]}]})";
|
|
size_t len = strlen(valid_json);
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
std::string truncated(valid_json, i);
|
|
tinygltf::detail::JsonDocument doc;
|
|
// Should not crash; may fail gracefully
|
|
tinygltf::detail::JsonParse(doc, truncated.c_str(), truncated.size());
|
|
// No assertion on result - just checking it doesn't crash/hang
|
|
}
|
|
}
|
|
|
|
TEST_CASE("cj-systematic-byte-flip", "[customjson][fuzzlike]") {
|
|
// Take a valid JSON string and flip each byte, verify no crash
|
|
const char *valid_json = R"({"asset":{"version":"2.0"}})";
|
|
size_t len = strlen(valid_json);
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
std::string mutated(valid_json, len);
|
|
mutated[i] = static_cast<char>(mutated[i] ^ 0xFF);
|
|
tinygltf::detail::JsonDocument doc;
|
|
tinygltf::detail::JsonParse(doc, mutated.c_str(), mutated.size());
|
|
// No assertion - just checking it doesn't crash
|
|
}
|
|
}
|
|
|
|
TEST_CASE("cj-systematic-null-insertion", "[customjson][fuzzlike]") {
|
|
// Insert null bytes at various positions
|
|
const char *valid_json = R"({"a":"b","c":1})";
|
|
size_t len = strlen(valid_json);
|
|
|
|
for (size_t i = 0; i <= len; i++) {
|
|
std::string mutated(valid_json, len);
|
|
mutated.insert(i, 1, '\0');
|
|
tinygltf::detail::JsonDocument doc;
|
|
tinygltf::detail::JsonParse(doc, mutated.c_str(), mutated.size());
|
|
// No assertion - just checking it doesn't crash
|
|
}
|
|
}
|