mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 21:21:17 +01:00
feature: bytecode header format (#11293) 68f1096d75
* feature: bytecode header format * Reexport scripting test rivs * Reexport one more riv Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com> Co-authored-by: Phil Chung <philterdesign@gmail.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
316
tests/unit_tests/runtime/bytecode_header_test.cpp
Normal file
316
tests/unit_tests/runtime/bytecode_header_test.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#include <catch.hpp>
|
||||
#include <rive/bytecode_header.hpp>
|
||||
#include <rive/span.hpp>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
// Tests for BytecodeHeader class (no scripting dependency)
|
||||
TEST_CASE("BytecodeHeader - empty data is invalid", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("BytecodeHeader - unsigned header", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data = {0x00, 0x01, 0x02, 0x03};
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == true);
|
||||
REQUIRE(header.isSigned() == false);
|
||||
REQUIRE(header.version() == 0);
|
||||
REQUIRE(header.bytecodeOffset() == 1);
|
||||
|
||||
auto bytecode = header.bytecode();
|
||||
REQUIRE(bytecode.size() == 3);
|
||||
REQUIRE(bytecode[0] == 0x01);
|
||||
REQUIRE(bytecode[1] == 0x02);
|
||||
REQUIRE(bytecode[2] == 0x03);
|
||||
|
||||
auto signature = header.signature();
|
||||
REQUIRE(signature.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("BytecodeHeader - signed header", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data(1 + rive::kSignatureSize + 3);
|
||||
data[0] = 0x80; // signed flag
|
||||
// Fill signature with pattern
|
||||
for (size_t i = 0; i < rive::kSignatureSize; i++)
|
||||
{
|
||||
data[1 + i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
// Bytecode
|
||||
data[1 + rive::kSignatureSize] = 0xAA;
|
||||
data[1 + rive::kSignatureSize + 1] = 0xBB;
|
||||
data[1 + rive::kSignatureSize + 2] = 0xCC;
|
||||
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == true);
|
||||
REQUIRE(header.isSigned() == true);
|
||||
REQUIRE(header.version() == 0);
|
||||
REQUIRE(header.bytecodeOffset() == 65);
|
||||
|
||||
auto signature = header.signature();
|
||||
REQUIRE(signature.size() == rive::kSignatureSize);
|
||||
REQUIRE(signature[0] == 0);
|
||||
REQUIRE(signature[63] == 63);
|
||||
|
||||
auto bytecode = header.bytecode();
|
||||
REQUIRE(bytecode.size() == 3);
|
||||
REQUIRE(bytecode[0] == 0xAA);
|
||||
REQUIRE(bytecode[1] == 0xBB);
|
||||
REQUIRE(bytecode[2] == 0xCC);
|
||||
}
|
||||
|
||||
TEST_CASE("BytecodeHeader - version extraction", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data = {0x2A, 0x01}; // version 42, not signed
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == true);
|
||||
REQUIRE(header.isSigned() == false);
|
||||
REQUIRE(header.version() == 42);
|
||||
}
|
||||
|
||||
TEST_CASE("BytecodeHeader - truncated signed data is invalid", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data = {0x80,
|
||||
0x01,
|
||||
0x02}; // claims signed but only 3 bytes
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == false);
|
||||
REQUIRE(header.isSigned() == true); // flag is set
|
||||
}
|
||||
|
||||
TEST_CASE("BytecodeHeader - minimum unsigned (flags only)", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data = {0x00};
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == true);
|
||||
REQUIRE(header.bytecode().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("BytecodeHeader - minimum signed (no bytecode)", "[bytecode]")
|
||||
{
|
||||
std::vector<uint8_t> data(1 + rive::kSignatureSize);
|
||||
data[0] = 0x80;
|
||||
rive::BytecodeHeader header{rive::Span<const uint8_t>(data)};
|
||||
|
||||
REQUIRE(header.isValid() == true);
|
||||
REQUIRE(header.isSigned() == true);
|
||||
REQUIRE(header.bytecode().empty());
|
||||
REQUIRE(header.signature().size() == rive::kSignatureSize);
|
||||
}
|
||||
|
||||
#ifdef WITH_RIVE_SCRIPTING
|
||||
#include <rive/assets/script_asset.hpp>
|
||||
|
||||
// Access the public key for verification
|
||||
namespace rive
|
||||
{
|
||||
extern const uint8_t g_scriptVerificationPublicKey[32];
|
||||
}
|
||||
|
||||
// Note: hydro_sign_BYTES = 64
|
||||
constexpr size_t SIGNATURE_SIZE = 64;
|
||||
|
||||
TEST_CASE("bytecode header parsing - empty data fails", "[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
std::vector<uint8_t> emptyData;
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(emptyData));
|
||||
|
||||
REQUIRE(result == false);
|
||||
REQUIRE(asset.verified() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - unsigned bytecode succeeds",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// Create unsigned bytecode: [flags:1] [bytecode:N]
|
||||
// Flags = 0x00 (version 0, not signed)
|
||||
std::vector<uint8_t> data = {
|
||||
0x00, // flags: version 0, not signed
|
||||
0x01,
|
||||
0x02,
|
||||
0x03, // dummy bytecode
|
||||
};
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(asset.verified() == false); // Unsigned = unverified
|
||||
|
||||
// Verify the bytecode was extracted correctly (without header)
|
||||
auto bytecode = asset.moduleBytecode();
|
||||
REQUIRE(bytecode.size() == 3);
|
||||
REQUIRE(bytecode[0] == 0x01);
|
||||
REQUIRE(bytecode[1] == 0x02);
|
||||
REQUIRE(bytecode[2] == 0x03);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - signed flag is detected",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// Create data that claims to be signed but has invalid signature
|
||||
// [flags:1] [signature:64] [bytecode:N]
|
||||
// Flags = 0x80 (version 0, signed)
|
||||
std::vector<uint8_t> data(1 + SIGNATURE_SIZE + 3);
|
||||
data[0] = 0x80; // flags: version 0, signed
|
||||
|
||||
// Fill signature with zeros (invalid)
|
||||
for (size_t i = 1; i <= SIGNATURE_SIZE; i++)
|
||||
{
|
||||
data[i] = 0x00;
|
||||
}
|
||||
|
||||
// Dummy bytecode
|
||||
data[1 + SIGNATURE_SIZE] = 0x01;
|
||||
data[1 + SIGNATURE_SIZE + 1] = 0x02;
|
||||
data[1 + SIGNATURE_SIZE + 2] = 0x03;
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
// Should fail because signature doesn't verify
|
||||
REQUIRE(result == false);
|
||||
REQUIRE(asset.verified() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - truncated signed data fails",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// Create data that claims to be signed but doesn't have enough bytes
|
||||
// for the signature
|
||||
std::vector<uint8_t> data = {
|
||||
0x80, // flags: signed
|
||||
0x01,
|
||||
0x02, // only 2 bytes of "signature" (need 64)
|
||||
};
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
REQUIRE(result == false);
|
||||
REQUIRE(asset.verified() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - version is preserved in flags",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// Version is in bits 0-6, so we can test that non-zero versions
|
||||
// are handled (even if we only use version 0 currently)
|
||||
// Flags = 0x01 (version 1, not signed)
|
||||
std::vector<uint8_t> data = {
|
||||
0x01, // flags: version 1, not signed
|
||||
0x01,
|
||||
0x02,
|
||||
0x03, // dummy bytecode
|
||||
};
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
// Should succeed - version is currently ignored
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(asset.verified() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - signed bytecode offset is correct",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// For signed data, bytecode starts at offset 65 (1 flag + 64 signature)
|
||||
std::vector<uint8_t> data(1 + SIGNATURE_SIZE + 4);
|
||||
data[0] = 0x80; // flags: signed
|
||||
|
||||
// Fill with pattern to verify offset
|
||||
for (size_t i = 0; i < data.size(); i++)
|
||||
{
|
||||
data[i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
data[0] = 0x80; // Restore flags
|
||||
|
||||
// Even though verification will fail, we can check the offset calculation
|
||||
// by inspecting what gets stored (if verification passed)
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
// Will fail due to invalid signature, but tests the parsing path
|
||||
REQUIRE(result == false);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - unsigned bytecode offset is correct",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// For unsigned data, bytecode starts at offset 1 (just the flag byte)
|
||||
std::vector<uint8_t> data = {
|
||||
0x00, // flags: not signed
|
||||
0xAA,
|
||||
0xBB,
|
||||
0xCC,
|
||||
0xDD,
|
||||
0xEE, // bytecode
|
||||
};
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
REQUIRE(result == true);
|
||||
|
||||
auto bytecode = asset.moduleBytecode();
|
||||
REQUIRE(bytecode.size() == 5);
|
||||
REQUIRE(bytecode[0] == 0xAA);
|
||||
REQUIRE(bytecode[1] == 0xBB);
|
||||
REQUIRE(bytecode[2] == 0xCC);
|
||||
REQUIRE(bytecode[3] == 0xDD);
|
||||
REQUIRE(bytecode[4] == 0xEE);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - minimum valid unsigned data",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// Minimum valid: just the flags byte with empty bytecode
|
||||
std::vector<uint8_t> data = {0x00}; // flags only, no bytecode
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
REQUIRE(result == true);
|
||||
REQUIRE(asset.verified() == false);
|
||||
|
||||
auto bytecode = asset.moduleBytecode();
|
||||
REQUIRE(bytecode.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("bytecode header parsing - minimum valid signed data structure",
|
||||
"[scripting][bytecode]")
|
||||
{
|
||||
rive::ScriptAsset asset;
|
||||
|
||||
// Minimum signed: flags + 64 byte signature + empty bytecode
|
||||
std::vector<uint8_t> data(1 + SIGNATURE_SIZE);
|
||||
data[0] = 0x80; // flags: signed
|
||||
|
||||
bool result = asset.bytecode(rive::Span<uint8_t>(data));
|
||||
|
||||
// Will fail signature verification but shouldn't crash
|
||||
REQUIRE(result == false);
|
||||
}
|
||||
|
||||
#endif // WITH_RIVE_SCRIPTING
|
||||
Reference in New Issue
Block a user