| /* | |
| * Copyright 2026 Rive | |
| */ | |
| #include "rive/assets/shader_asset.hpp" | |
| #include "rive/simple_array.hpp" | |
| #include <array> | |
| #include <catch.hpp> | |
| #include <cstring> | |
| #include <vector> | |
| namespace rive | |
| { | |
| // Helper to build a minimal RSTB v4 binary. | |
| // variants: vector of (target, blobData) pairs. | |
| // sections: vector of (tag, sectionData) pairs written verbatim after the | |
| // variant descriptors with a u16 length prefix. | |
| static std::vector<uint8_t> makeRSTB( | |
| uint16_t version, | |
| const std::vector<std::pair<uint8_t, std::vector<uint8_t>>>& variants, | |
| const std::vector<std::pair<uint8_t, std::vector<uint8_t>>>& sections = {}, | |
| uint32_t magic = 0x52535442u) | |
| { | |
| std::vector<uint8_t> out; | |
| auto pushU32 = [&](uint32_t v) { | |
| out.push_back(v & 0xFF); | |
| out.push_back((v >> 8) & 0xFF); | |
| out.push_back((v >> 16) & 0xFF); | |
| out.push_back((v >> 24) & 0xFF); | |
| }; | |
| auto pushU16 = [&](uint16_t v) { | |
| out.push_back(v & 0xFF); | |
| out.push_back((v >> 8) & 0xFF); | |
| }; | |
| // Header: magic + version + variant_count + section_count. | |
| pushU32(magic); | |
| pushU16(version); | |
| out.push_back(static_cast<uint8_t>(variants.size())); | |
| out.push_back(static_cast<uint8_t>(sections.size())); | |
| // Variant descriptors with offsets relative to blob section. | |
| std::vector<uint8_t> blobSection; | |
| for (auto& [target, blob] : variants) | |
| { | |
| out.push_back(target); | |
| pushU32(static_cast<uint32_t>(blobSection.size())); | |
| pushU32(static_cast<uint32_t>(blob.size())); | |
| blobSection.insert(blobSection.end(), blob.begin(), blob.end()); | |
| } | |
| // Tagged sections: tag(u8) + length(u16) + data[length]. | |
| for (auto& [tag, data] : sections) | |
| { | |
| out.push_back(tag); | |
| pushU16(static_cast<uint16_t>(data.size())); | |
| out.insert(out.end(), data.begin(), data.end()); | |
| } | |
| out.insert(out.end(), blobSection.begin(), blobSection.end()); | |
| return out; | |
| } | |
| TEST_CASE("ShaderAsset-isTypeOf", "[ShaderAsset]") | |
| { | |
| ShaderAsset asset; | |
| CHECK(asset.isTypeOf(970) == true); // ShaderAssetBase::typeKey | |
| CHECK(asset.isTypeOf(103) == true); // FileAssetBase::typeKey | |
| CHECK(asset.isTypeOf(99) == true); // AssetBase::typeKey | |
| CHECK(asset.isTypeOf(0) == false); | |
| CHECK(asset.isTypeOf(999) == false); | |
| } | |
| TEST_CASE("ShaderAsset-coreType", "[ShaderAsset]") | |
| { | |
| ShaderAsset asset; | |
| CHECK(asset.coreType() == 970); | |
| } | |
| TEST_CASE("ShaderAsset-fileExtension", "[ShaderAsset]") | |
| { | |
| ShaderAsset asset; | |
| CHECK(asset.fileExtension() == "rstb"); | |
| } | |
| // Wrap raw RSTB bytes in an unsigned SignedContentHeader envelope (flags=0) | |
| // to match ShaderAsset::decode's expected input. | |
| static SimpleArray<uint8_t> envelope(const std::vector<uint8_t>& rstb) | |
| { | |
| std::vector<uint8_t> out; | |
| out.push_back(0x00); | |
| out.insert(out.end(), rstb.begin(), rstb.end()); | |
| return SimpleArray<uint8_t>(out.data(), out.size()); | |
| } | |
| TEST_CASE("ShaderAsset-decode-valid", "[ShaderAsset]") | |
| { | |
| std::vector<uint8_t> blob = {0xDE, 0xAD, 0xBE, 0xEF, 0x42}; | |
| auto data = envelope(makeRSTB(4, {{2, blob}})); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == true); | |
| auto result = asset.findShader(2); | |
| REQUIRE(result.size() == 5); | |
| CHECK(result[0] == 0xDE); | |
| CHECK(result[1] == 0xAD); | |
| CHECK(result[2] == 0xBE); | |
| CHECK(result[3] == 0xEF); | |
| CHECK(result[4] == 0x42); | |
| } | |
| TEST_CASE("ShaderAsset-decode-bad-magic", "[ShaderAsset]") | |
| { | |
| auto data = envelope(makeRSTB(4, {}, {}, 0xBADBAD00u)); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| } | |
| TEST_CASE("ShaderAsset-decode-bad-version", "[ShaderAsset]") | |
| { | |
| // v3 (and earlier) is no longer accepted after the v4 entry-container bump. | |
| auto data = envelope(makeRSTB(3, {})); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| } | |
| TEST_CASE("ShaderAsset-decode-truncated", "[ShaderAsset]") | |
| { | |
| // Envelope byte + less than 8 bytes of RSTB = too short for header. | |
| uint8_t tiny[] = {0x00, 0x52, 0x53, 0x54, 0x42, 0x01, 0x00}; | |
| SimpleArray<uint8_t> data(tiny, sizeof(tiny)); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| } | |
| TEST_CASE("ShaderAsset-findShader-miss", "[ShaderAsset]") | |
| { | |
| auto data = envelope(makeRSTB(4, {{2, {0xAA}}})); | |
| ShaderAsset asset; | |
| REQUIRE(asset.decode(data, nullptr) == true); | |
| CHECK(asset.findShader(0).empty()); | |
| CHECK(asset.findShader(99).empty()); | |
| REQUIRE(asset.findShader(2).size() == 1); | |
| } | |
| TEST_CASE("ShaderAsset-multiple-targets", "[ShaderAsset]") | |
| { | |
| std::vector<uint8_t> blob0 = {0x11, 0x22}; | |
| std::vector<uint8_t> blob1 = {0x33, 0x44, 0x55}; | |
| std::vector<uint8_t> blob2 = {0x66}; | |
| std::vector<uint8_t> blob3 = {0x77, 0x88, 0x99, 0xAA}; | |
| auto data = | |
| envelope(makeRSTB(4, {{0, blob0}, {1, blob1}, {2, blob2}, {3, blob3}})); | |
| ShaderAsset asset; | |
| REQUIRE(asset.decode(data, nullptr) == true); | |
| auto r0 = asset.findShader(0); | |
| REQUIRE(r0.size() == 2); | |
| CHECK(r0[0] == 0x11); | |
| auto r1 = asset.findShader(1); | |
| REQUIRE(r1.size() == 3); | |
| CHECK(r1[0] == 0x33); | |
| auto r2 = asset.findShader(2); | |
| REQUIRE(r2.size() == 1); | |
| CHECK(r2[0] == 0x66); | |
| auto r3 = asset.findShader(3); | |
| REQUIRE(r3.size() == 4); | |
| CHECK(r3[0] == 0x77); | |
| } | |
| // Tag 1 section data: pair_count(u8) + N * (texGroup, texBinding, | |
| // sampGroup, sampBinding). | |
| static std::vector<uint8_t> makeTexSamplerPairsTag( | |
| const std::vector<std::array<uint8_t, 4>>& pairs) | |
| { | |
| std::vector<uint8_t> out; | |
| out.push_back(static_cast<uint8_t>(pairs.size())); | |
| for (auto& p : pairs) | |
| { | |
| out.push_back(p[0]); | |
| out.push_back(p[1]); | |
| out.push_back(p[2]); | |
| out.push_back(p[3]); | |
| } | |
| return out; | |
| } | |
| TEST_CASE("ShaderAsset-decode-texture-sampler-pairs", "[ShaderAsset]") | |
| { | |
| auto pairs = | |
| makeTexSamplerPairsTag({{0, 1, 0, 2}, {1, 3, 1, 4}, {2, 5, 2, 6}}); | |
| auto data = envelope(makeRSTB(4, {{2, {0xAA}}}, {{1, pairs}})); | |
| ShaderAsset asset; | |
| REQUIRE(asset.decode(data, nullptr) == true); | |
| auto out = asset.textureSamplerPairs(); | |
| REQUIRE(out.size() == 3); | |
| CHECK(out[0].texGroup == 0); | |
| CHECK(out[0].texBinding == 1); | |
| CHECK(out[0].sampGroup == 0); | |
| CHECK(out[0].sampBinding == 2); | |
| CHECK(out[1].texBinding == 3); | |
| CHECK(out[2].sampBinding == 6); | |
| // The variant blob still resolves correctly after the tagged section. | |
| auto blob = asset.findShader(2); | |
| REQUIRE(blob.size() == 1); | |
| CHECK(blob[0] == 0xAA); | |
| } | |
| TEST_CASE("ShaderAsset-decode-no-sections-empty-pairs", "[ShaderAsset]") | |
| { | |
| auto data = envelope(makeRSTB(4, {{2, {0x42}}})); | |
| ShaderAsset asset; | |
| REQUIRE(asset.decode(data, nullptr) == true); | |
| CHECK(asset.textureSamplerPairs().size() == 0); | |
| } | |
| TEST_CASE("ShaderAsset-decode-unknown-tag-skipped", "[ShaderAsset]") | |
| { | |
| // Tag 99 (unknown). Reader must skip the section without failing, | |
| // and pairs must remain empty. | |
| std::vector<uint8_t> bogusSection = {0xDE, 0xAD, 0xBE, 0xEF}; | |
| auto data = envelope(makeRSTB(4, {{2, {0x42}}}, {{99, bogusSection}})); | |
| ShaderAsset asset; | |
| REQUIRE(asset.decode(data, nullptr) == true); | |
| CHECK(asset.textureSamplerPairs().size() == 0); | |
| auto blob = asset.findShader(2); | |
| REQUIRE(blob.size() == 1); | |
| CHECK(blob[0] == 0x42); | |
| } | |
| TEST_CASE("ShaderAsset-decode-truncated-section-header", "[ShaderAsset]") | |
| { | |
| // Build a valid RSTB then chop bytes so the section header (3 bytes) | |
| // doesn't fit. Header(8) + Variant(9) = 17 bytes before the section, | |
| // and we claim section_count=1 so the reader will look for a section. | |
| auto rstb = makeRSTB(4, {{2, {0x42}}}, {{1, {0}}}); | |
| // Truncate to leave only 1 byte where the 3-byte section header | |
| // should be. Section header starts at offset 17. | |
| rstb.resize(17 + 1); | |
| auto data = envelope(rstb); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| } | |
| TEST_CASE("ShaderAsset-decode-truncated-section-data", "[ShaderAsset]") | |
| { | |
| // Section claims length=10 but only 2 data bytes follow. | |
| auto rstb = makeRSTB(4, {{2, {0x42}}}); | |
| // Bump section_count from 0 to 1. | |
| rstb[7] = 1; | |
| // Append a section header claiming 10 data bytes but supply only 2. | |
| rstb.push_back(1); // tag | |
| rstb.push_back(10); // length lo | |
| rstb.push_back(0); // length hi | |
| rstb.push_back(0xAA); // data byte 1 | |
| rstb.push_back(0xBB); // data byte 2 | |
| auto data = envelope(rstb); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| } | |
| TEST_CASE("ShaderAsset-decode-rejects-out-of-range-variant", "[ShaderAsset]") | |
| { | |
| // Build a valid 1-variant RSTB then patch the variant's blob_size to | |
| // a value that runs past m_bytes. Decode must fail rather than store | |
| // an entry that findShader would later return as a Span past the buf. | |
| auto rstb = makeRSTB(4, {{2, {0xAA}}}); | |
| // Variant descriptor starts at byte 8: target(u8) + blob_offset(u32) | |
| // + blob_size(u32). blob_size lives at bytes 13..16. | |
| rstb[13] = 0xFF; | |
| rstb[14] = 0xFF; | |
| rstb[15] = 0xFF; | |
| rstb[16] = 0x7F; | |
| auto data = envelope(rstb); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| CHECK(asset.findShader(2).empty()); | |
| } | |
| TEST_CASE("ShaderAsset-decode-rejects-overflowing-variant", "[ShaderAsset]") | |
| { | |
| // blob_offset = 0xFFFFFFF0, blob_size = 0x20. Naive uint32_t addition | |
| // wraps to 0x10 and a non-overflow-aware bounds check would pass. | |
| auto rstb = makeRSTB(4, {{2, {0xAA}}}); | |
| rstb[9] = 0xF0; | |
| rstb[10] = 0xFF; | |
| rstb[11] = 0xFF; | |
| rstb[12] = 0xFF; | |
| rstb[13] = 0x20; | |
| rstb[14] = 0x00; | |
| rstb[15] = 0x00; | |
| rstb[16] = 0x00; | |
| auto data = envelope(rstb); | |
| ShaderAsset asset; | |
| CHECK(asset.decode(data, nullptr) == false); | |
| } | |
| TEST_CASE("ShaderAsset-decode-pair-count-mismatch-ignored", "[ShaderAsset]") | |
| { | |
| // Section claims pair_count=5 but only carries enough bytes for 2. | |
| // Reader should skip the pair payload (length insufficient) but not | |
| // fail the overall decode; subsequent variants still resolve. | |
| std::vector<uint8_t> badPairs = {5, 0, 1, 0, 2, 1, 3, 1, 4}; | |
| auto data = envelope(makeRSTB(4, {{2, {0xAA}}}, {{1, badPairs}})); | |
| ShaderAsset asset; | |
| REQUIRE(asset.decode(data, nullptr) == true); | |
| CHECK(asset.textureSamplerPairs().size() == 0); | |
| auto blob = asset.findShader(2); | |
| REQUIRE(blob.size() == 1); | |
| CHECK(blob[0] == 0xAA); | |
| } | |
| } // namespace rive |