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:
@@ -1 +1 @@
|
||||
ba953a142ba3703152a81b89da93ea77ecb30d57
|
||||
68f1096d75f18308bb74a07f756812a72430278f
|
||||
|
||||
@@ -145,8 +145,11 @@ public:
|
||||
|
||||
bool initScriptedObject(ScriptedObject* object);
|
||||
|
||||
/// Sets the bytecode if the signature verifies.
|
||||
bool bytecode(Span<uint8_t> bytecode, Span<uint8_t> signature);
|
||||
/// Sets the bytecode from data with header format:
|
||||
/// [flags:1] [signature:64 if signed] [luau_bytecode:N]
|
||||
/// Flags byte: bits 0-6 = version, bit 7 = isSigned
|
||||
/// Returns true if bytecode was set (verification is separate from return).
|
||||
bool bytecode(Span<uint8_t> data);
|
||||
|
||||
/// Bytecode provided via decode should only happen with in-band bytecode.
|
||||
/// The signature will later be verified once file loading completes, so it
|
||||
|
||||
80
include/rive/bytecode_header.hpp
Normal file
80
include/rive/bytecode_header.hpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#ifndef _RIVE_BYTECODE_HEADER_HPP_
|
||||
#define _RIVE_BYTECODE_HEADER_HPP_
|
||||
|
||||
#include "rive/span.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
namespace rive
|
||||
{
|
||||
|
||||
/// Size of the signature in bytes (hydro_sign_BYTES from libhydrogen).
|
||||
static constexpr size_t kSignatureSize = 64;
|
||||
|
||||
/// Lightweight view into bytecode data with header.
|
||||
/// Does not copy the underlying data - just provides accessors.
|
||||
///
|
||||
/// Header format:
|
||||
/// [flags:1] [signature:64 if signed] [luau_bytecode:N]
|
||||
///
|
||||
/// Flags byte layout:
|
||||
/// Bits 0-6: Version number (0-127)
|
||||
/// Bit 7: isSigned flag
|
||||
class BytecodeHeader
|
||||
{
|
||||
public:
|
||||
/// Constructs a BytecodeHeader from raw data (header + bytecode).
|
||||
/// The data must outlive this object.
|
||||
explicit BytecodeHeader(Span<const uint8_t> data) : m_data(data)
|
||||
{
|
||||
if (!data.empty())
|
||||
{
|
||||
m_flags = data[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns true if the header indicates the bytecode is signed
|
||||
bool isSigned() const { return (m_flags & 0x80) != 0; }
|
||||
|
||||
/// @returns the version number from the header (0-127)
|
||||
uint8_t version() const { return m_flags & 0x7F; }
|
||||
|
||||
/// @returns true if the data is valid (has at least the header byte and
|
||||
/// enough bytes for signature if signed)
|
||||
bool isValid() const { return m_data.size() >= bytecodeOffset(); }
|
||||
|
||||
/// @returns the offset where bytecode starts (1 if unsigned, 65 if signed)
|
||||
size_t bytecodeOffset() const
|
||||
{
|
||||
return isSigned() ? (1 + kSignatureSize) : 1;
|
||||
}
|
||||
|
||||
/// @returns a span of the signature bytes, or empty span if not signed
|
||||
Span<const uint8_t> signature() const
|
||||
{
|
||||
if (!isSigned() || m_data.size() < 1 + kSignatureSize)
|
||||
{
|
||||
return Span<const uint8_t>();
|
||||
}
|
||||
return Span<const uint8_t>(m_data.data() + 1, kSignatureSize);
|
||||
}
|
||||
|
||||
/// @returns a span of the actual bytecode (without header)
|
||||
Span<const uint8_t> bytecode() const
|
||||
{
|
||||
size_t offset = bytecodeOffset();
|
||||
if (m_data.size() < offset)
|
||||
{
|
||||
return Span<const uint8_t>();
|
||||
}
|
||||
return Span<const uint8_t>(m_data.data() + offset,
|
||||
m_data.size() - offset);
|
||||
}
|
||||
|
||||
private:
|
||||
Span<const uint8_t> m_data;
|
||||
uint8_t m_flags = 0;
|
||||
};
|
||||
|
||||
} // namespace rive
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "rive/importers/script_asset_importer.hpp"
|
||||
#endif
|
||||
#include "rive/assets/script_asset.hpp"
|
||||
#include "rive/bytecode_header.hpp"
|
||||
#include "rive/file.hpp"
|
||||
#include "rive/script_input_artboard.hpp"
|
||||
#include "rive/script_input_boolean.hpp"
|
||||
@@ -228,24 +229,47 @@ bool ScriptAsset::decode(SimpleArray<uint8_t>& data, Factory* factory)
|
||||
{
|
||||
#ifdef WITH_RIVE_SCRIPTING
|
||||
m_verified = false;
|
||||
// Don't move here as the script asset importer needs to keep the bytecode
|
||||
// around for verification.
|
||||
m_bytecode = SimpleArray<uint8_t>(data);
|
||||
|
||||
// For in-band bytecode, isSigned should always be false (signature is
|
||||
// stored separately for aggregate verification).
|
||||
BytecodeHeader header(Span<const uint8_t>(data.data(), data.size()));
|
||||
if (!header.isValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store just the bytecode (without header) for later verification and use.
|
||||
auto bytecode = header.bytecode();
|
||||
m_bytecode = SimpleArray<uint8_t>(bytecode.data(), bytecode.size());
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScriptAsset::bytecode(Span<uint8_t> bytecode, Span<uint8_t> signature)
|
||||
bool ScriptAsset::bytecode(Span<uint8_t> data)
|
||||
{
|
||||
#ifdef WITH_RIVE_SCRIPTING
|
||||
if (signature.size() != hydro_sign_BYTES)
|
||||
BytecodeHeader header(Span<const uint8_t>(data.data(), data.size()));
|
||||
if (!header.isValid())
|
||||
{
|
||||
m_verified = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto bytecode = header.bytecode();
|
||||
|
||||
if (!header.isSigned())
|
||||
{
|
||||
// Unsigned bytecode - mark as unverified
|
||||
m_verified = false;
|
||||
m_bytecode = SimpleArray<uint8_t>(bytecode.data(), bytecode.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto signature = header.signature();
|
||||
if (hydro_sign_verify(signature.data(),
|
||||
bytecode.data(),
|
||||
bytecode.size(),
|
||||
"rive",
|
||||
"RiveCode",
|
||||
g_scriptVerificationPublicKey) != 0)
|
||||
{
|
||||
// Forged.
|
||||
@@ -253,7 +277,7 @@ bool ScriptAsset::bytecode(Span<uint8_t> bytecode, Span<uint8_t> signature)
|
||||
return false;
|
||||
}
|
||||
m_verified = true;
|
||||
m_bytecode = SimpleArray<uint8_t>(bytecode);
|
||||
m_bytecode = SimpleArray<uint8_t>(bytecode.data(), bytecode.size());
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "rive/importers/file_asset_importer.hpp"
|
||||
#include "rive/assets/file_asset_contents.hpp"
|
||||
#include "rive/assets/script_asset.hpp"
|
||||
#include "rive/bytecode_header.hpp"
|
||||
#include "rive/file_asset_loader.hpp"
|
||||
#include "rive/span.hpp"
|
||||
#include <cstdint>
|
||||
@@ -37,9 +38,16 @@ void ScriptAssetImporter::onFileAssetContents(
|
||||
std::unique_ptr<FileAssetContents> contents)
|
||||
{
|
||||
// When contents are found in band, this script is part of the verification
|
||||
// set.
|
||||
m_scriptVerificationSet->emplace_back(
|
||||
InBandByteCode(scriptAsset(), contents->bytes()));
|
||||
// set. Strip the header to get raw bytecode for aggregate verification.
|
||||
auto& bytes = contents->bytes();
|
||||
BytecodeHeader header(Span<const uint8_t>(bytes.data(), bytes.size()));
|
||||
if (header.isValid())
|
||||
{
|
||||
auto bytecode = header.bytecode();
|
||||
SimpleArray<uint8_t> rawBytecode(bytecode.data(), bytecode.size());
|
||||
m_scriptVerificationSet->emplace_back(
|
||||
InBandByteCode(scriptAsset(), rawBytecode));
|
||||
}
|
||||
FileAssetImporter::onFileAssetContents(std::move(contents));
|
||||
}
|
||||
|
||||
|
||||
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