| /* |
| * Copyright 2026 Rive |
| */ |
| |
| #include <rive/assets/manifest_asset.hpp> |
| #include <rive/manifest_sections.hpp> |
| #include <rive/simple_array.hpp> |
| #include <catch.hpp> |
| #include <cstdint> |
| #include <vector> |
| |
| using namespace rive; |
| |
| namespace |
| { |
| // Every value these tests write is below 128, so a varuint is a single byte. |
| // Keeping the encoding trivial keeps the expected bytes readable. |
| void writeVarUint(std::vector<uint8_t>& out, uint64_t value) |
| { |
| REQUIRE(value < 128); |
| out.push_back(static_cast<uint8_t>(value)); |
| } |
| |
| void writeSection(std::vector<uint8_t>& out, |
| ManifestSections section, |
| const std::vector<uint8_t>& payload) |
| { |
| writeVarUint(out, static_cast<uint64_t>(section)); |
| writeVarUint(out, payload.size()); |
| out.insert(out.end(), payload.begin(), payload.end()); |
| } |
| |
| // Matches the payload the Dart exporter writes in |
| // ManifestAsset._writeWatermark: version, flags, artboard index. |
| std::vector<uint8_t> watermarkPayload(uint64_t version, |
| uint64_t flags, |
| uint64_t artboardIndex) |
| { |
| std::vector<uint8_t> payload; |
| writeVarUint(payload, version); |
| writeVarUint(payload, flags); |
| writeVarUint(payload, artboardIndex); |
| return payload; |
| } |
| |
| bool decode(ManifestAsset& asset, const std::vector<uint8_t>& bytes) |
| { |
| SimpleArray<uint8_t> data(bytes.data(), bytes.size()); |
| return asset.decode(data, nullptr); |
| } |
| } // namespace |
| |
| TEST_CASE("a manifest with no sections decodes with no watermark", "[manifest]") |
| { |
| ManifestAsset asset; |
| REQUIRE(decode(asset, {})); |
| REQUIRE(!asset.hasWatermark()); |
| } |
| |
| TEST_CASE("the watermark section decodes", "[manifest]") |
| { |
| std::vector<uint8_t> bytes; |
| writeSection(bytes, ManifestSections::watermark, watermarkPayload(1, 1, 7)); |
| |
| ManifestAsset asset; |
| REQUIRE(decode(asset, bytes)); |
| REQUIRE(asset.hasWatermark()); |
| REQUIRE(asset.watermarkArtboardIndex() == 7); |
| } |
| |
| TEST_CASE("a cleared enabled flag leaves the watermark off", "[manifest]") |
| { |
| std::vector<uint8_t> bytes; |
| writeSection(bytes, ManifestSections::watermark, watermarkPayload(1, 0, 7)); |
| |
| ManifestAsset asset; |
| REQUIRE(decode(asset, bytes)); |
| REQUIRE(!asset.hasWatermark()); |
| } |
| |
| TEST_CASE("trailing bytes in the watermark section are skipped", "[manifest]") |
| { |
| // A newer exporter appending fields must not break this runtime: the |
| // section carries its own size, and decodeWatermark swallows the rest. |
| auto payload = watermarkPayload(1, 1, 3); |
| payload.push_back(0x2a); |
| payload.push_back(0x2b); |
| |
| std::vector<uint8_t> bytes; |
| writeSection(bytes, ManifestSections::watermark, payload); |
| |
| ManifestAsset asset; |
| REQUIRE(decode(asset, bytes)); |
| REQUIRE(asset.hasWatermark()); |
| REQUIRE(asset.watermarkArtboardIndex() == 3); |
| } |
| |
| TEST_CASE("an unrecognized watermark version is ignored, not fatal", |
| "[manifest]") |
| { |
| std::vector<uint8_t> bytes; |
| writeSection(bytes, |
| ManifestSections::watermark, |
| watermarkPayload(99, 1, 3)); |
| // A section after it must still parse, proving the unknown version was |
| // consumed rather than desyncing the reader. |
| std::vector<uint8_t> names; |
| writeVarUint(names, 1); // one entry |
| writeVarUint(names, 4); // id |
| writeVarUint(names, 2); // string length |
| names.push_back('h'); |
| names.push_back('i'); |
| writeSection(bytes, ManifestSections::names, names); |
| |
| ManifestAsset asset; |
| REQUIRE(decode(asset, bytes)); |
| REQUIRE(!asset.hasWatermark()); |
| REQUIRE(asset.resolveName(4) == "hi"); |
| } |
| |
| TEST_CASE("an unknown section is skipped and later sections still decode", |
| "[manifest]") |
| { |
| std::vector<uint8_t> bytes; |
| writeSection(bytes, static_cast<ManifestSections>(99), {0x01, 0x02, 0x03}); |
| writeSection(bytes, ManifestSections::watermark, watermarkPayload(1, 1, 5)); |
| |
| ManifestAsset asset; |
| REQUIRE(decode(asset, bytes)); |
| REQUIRE(asset.hasWatermark()); |
| REQUIRE(asset.watermarkArtboardIndex() == 5); |
| } |
| |
| // A watermark index that does not fit in 32 bits is a malformed manifest. It |
| // used to be truncated, so an encoded 2^32 became index 0 and would attach |
| // whatever artboard sits there as the watermark; now the section is rejected. |
| TEST_CASE("an out-of-range watermark artboard index is rejected", "[manifest]") |
| { |
| // manifest_detail::appendVarUint writes real multi-byte LEB128, which the |
| // single-byte helper above deliberately cannot. |
| std::vector<uint8_t> payload; |
| manifest_detail::appendVarUint(payload, watermarkSectionVersion); |
| manifest_detail::appendVarUint(payload, watermarkFlagEnabled); |
| manifest_detail::appendVarUint(payload, uint64_t(1) << 32); |
| |
| std::vector<uint8_t> bytes; |
| writeSection(bytes, ManifestSections::watermark, payload); |
| |
| ManifestAsset asset; |
| CHECK(!decode(asset, bytes)); |
| CHECK(!asset.hasWatermark()); |
| |
| // The largest index that does fit still decodes. |
| std::vector<uint8_t> okPayload; |
| manifest_detail::appendVarUint(okPayload, watermarkSectionVersion); |
| manifest_detail::appendVarUint(okPayload, watermarkFlagEnabled); |
| manifest_detail::appendVarUint(okPayload, 0xFFFFFFFFu); |
| |
| std::vector<uint8_t> okBytes; |
| writeSection(okBytes, ManifestSections::watermark, okPayload); |
| |
| ManifestAsset ok; |
| REQUIRE(decode(ok, okBytes)); |
| CHECK(ok.hasWatermark()); |
| CHECK(ok.watermarkArtboardIndex() == 0xFFFFFFFFu); |
| } |
| |
| // ManifestSections has an unsigned char base, so casting a section id down to |
| // it aliases every id mod 256 onto a known section. 258 landed on watermark, |
| // so a section the runtime should have skipped attached one instead -- and the |
| // skip-unknown branch that lets a newer exporter add sections never ran. |
| TEST_CASE("a section id that aliases a known one is skipped", "[manifest]") |
| { |
| // A watermark payload carried under ids the decoder must not recognize. |
| std::vector<uint8_t> payload = |
| watermarkPayload(watermarkSectionVersion, watermarkFlagEnabled, 7); |
| |
| for (uint64_t aliased : {uint64_t(256), uint64_t(257), uint64_t(258)}) |
| { |
| std::vector<uint8_t> bytes; |
| manifest_detail::appendVarUint(bytes, aliased); |
| manifest_detail::appendVarUint(bytes, payload.size()); |
| bytes.insert(bytes.end(), payload.begin(), payload.end()); |
| |
| ManifestAsset asset; |
| INFO("section id " << aliased); |
| // Unknown sections are skipped, so the manifest still decodes... |
| REQUIRE(decode(asset, bytes)); |
| // ...and nothing was read out of them. |
| CHECK(!asset.hasWatermark()); |
| } |
| |
| // The real id still works, so this is not just rejecting everything. |
| std::vector<uint8_t> real; |
| writeSection(real, ManifestSections::watermark, payload); |
| ManifestAsset asset; |
| REQUIRE(decode(asset, real)); |
| CHECK(asset.hasWatermark()); |
| CHECK(asset.watermarkArtboardIndex() == 7); |
| } |