rvm: switch binary serialization to postcard (#582)

Move RVM binary encoding from bincode to postcard and bump the format version. Update test helpers, docs, changelog, and refresh lockfiles after the swap.

Closes #575

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-03-03 15:09:45 -06:00
committed by GitHub
parent b6f11c5602
commit 006e819d52
13 changed files with 261 additions and 268 deletions

View File

@@ -85,7 +85,7 @@ pub struct Program {
impl Program {
/// Current serialization format version
pub const SERIALIZATION_VERSION: u32 = 3;
pub const SERIALIZATION_VERSION: u32 = 4;
/// Magic bytes to identify Regorus program files
pub const MAGIC: [u8; 4] = *b"REGO";
/// Maximum instructions supported (matches u16 jump targets)

View File

@@ -4,8 +4,7 @@
use alloc::format;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use bincode::config::standard;
use bincode::serde::{decode_from_slice, encode_to_vec};
use postcard::{from_bytes, to_allocvec};
use super::{DeserializationResult, Program};
use crate::value::Value;
@@ -70,7 +69,7 @@ impl Program {
}
/// Serialize program to binary format.
/// Uses pure bincode for all sections now that `Value` supports serde.
/// Uses postcard for all sections now that `Value` supports serde.
pub fn serialize_binary(&self) -> Result<Vec<u8>, String> {
self.validate_limits()?;
@@ -79,19 +78,19 @@ impl Program {
buffer.extend_from_slice(&Self::MAGIC);
buffer.extend_from_slice(&Self::SERIALIZATION_VERSION.to_le_bytes());
let entry_points_bin = encode_to_vec(&self.entry_points, standard())
.map_err(|e| format!("Entry points bincode serialization failed: {}", e))?;
let entry_points_bin = to_allocvec(&self.entry_points)
.map_err(|e| format!("Entry points postcard serialization failed: {}", e))?;
let sources_bin = encode_to_vec(&self.sources, standard())
.map_err(|e| format!("Sources bincode serialization failed: {}", e))?;
let sources_bin = to_allocvec(&self.sources)
.map_err(|e| format!("Sources postcard serialization failed: {}", e))?;
let literals_bin = encode_to_vec(BinaryValueSlice(self.literals.as_slice()), standard())
.map_err(|e| format!("Literals bincode serialization failed: {}", e))?;
let literals_bin = to_allocvec(&BinaryValueSlice(self.literals.as_slice()))
.map_err(|e| format!("Literals postcard serialization failed: {}", e))?;
let rule_tree_bin = encode_to_vec(BinaryValueRef(&self.rule_tree), standard())
.map_err(|e| format!("Rule tree bincode serialization failed: {}", e))?;
let rule_tree_bin = to_allocvec(&BinaryValueRef(&self.rule_tree))
.map_err(|e| format!("Rule tree postcard serialization failed: {}", e))?;
let binary_data = encode_to_vec(self, standard())
let binary_data = to_allocvec(self)
.map_err(|e| format!("Program structure binary serialization failed: {}", e))?;
buffer.extend_from_slice(
@@ -152,128 +151,13 @@ impl Program {
}
match version {
1 => {
if data.len() < 25 {
return Err("Data too short for header".to_string());
}
let entry_points_len = Self::read_u32_as_usize(data, 8)?;
let sources_len = Self::read_u32_as_usize(data, 12)?;
let rego_v0 = Self::read_byte(data, 16)? != 0;
let mut offset = OffsetTracker::new(17);
let entry_points_start = offset.advance(entry_points_len)?;
let sources_start = offset.advance(sources_len)?;
let binary_len_start = offset.current();
if data.len() < binary_len_start.checked_add(4).ok_or("Offset overflow")? {
return Err("Data too short for binary length".to_string());
}
let binary_len = Self::read_u32_as_usize(data, binary_len_start)?;
let mut binary_offset = OffsetTracker::new(binary_len_start);
binary_offset.advance(4)?; // Skip the binary_len u32
let binary_start = binary_offset.advance(binary_len)?;
let json_start = binary_offset.current();
if data.len() < json_start.checked_add(4).ok_or("Offset overflow")? {
return Err("Data too short for JSON length".to_string());
}
let json_len = Self::read_u32_as_usize(data, json_start)?;
let total_expected = json_start
.checked_add(4)
.and_then(|v| v.checked_add(json_len))
.ok_or("Offset overflow")?;
if data.len() < total_expected {
return Err("Data truncated".to_string());
}
let json_end = json_start
.checked_add(4)
.and_then(|v| v.checked_add(json_len))
.ok_or("Offset overflow")?;
let entry_points = decode_from_slice(
Self::get_slice(data, entry_points_start, sources_start)?,
standard(),
)
.map(|(value, _)| value)
.map_err(|e| format!("Entry points deserialization failed: {}", e))?;
let sources = decode_from_slice(
Self::get_slice(data, sources_start, binary_len_start)?,
standard(),
)
.map(|(value, _)| value)
.map_err(|e| format!("Sources deserialization failed: {}", e))?;
let mut needs_recompilation = false;
let mut program = match decode_from_slice::<Program, _>(
Self::get_slice(data, binary_start, json_start)?,
standard(),
) {
Ok((prog, _)) => prog,
Err(_e) => {
needs_recompilation = true;
Program::new()
}
};
let (literals, rule_tree) =
match serde_json::from_slice::<serde_json::Value>(Self::get_slice(
data,
json_start.checked_add(4).ok_or("Offset overflow")?,
json_end,
)?) {
Ok(combined) => {
let literals = combined
.get("literals")
.and_then(|v| serde_json::from_value::<Vec<Value>>(v.clone()).ok())
.unwrap_or_else(|| {
needs_recompilation = true;
Vec::new()
});
let rule_tree = combined
.get("rule_tree")
.and_then(|v| serde_json::from_value::<Value>(v.clone()).ok())
.unwrap_or_else(|| {
needs_recompilation = true;
Value::new_object()
});
(literals, rule_tree)
}
Err(_e) => {
needs_recompilation = true;
(Vec::new(), Value::new_object())
}
};
program.entry_points = entry_points;
program.sources = sources;
program.literals = literals;
program.rule_tree = rule_tree;
program.rego_v0 = rego_v0;
program.needs_recompilation = needs_recompilation;
if !program.builtin_info_table.is_empty() {
if let Err(_e) = program.initialize_resolved_builtins() {
program.needs_recompilation = true;
}
}
if program.needs_recompilation {
Ok(DeserializationResult::Partial(program))
} else {
Ok(DeserializationResult::Complete(program))
}
1..=3 => {
let mut program = Program::new();
program.needs_recompilation = true;
program.rego_v0 = Self::legacy_rego_v0(data, version).unwrap_or(false);
Ok(DeserializationResult::Partial(program))
}
2 | 3 => {
4 => {
if data.len() < 29 {
return Err("Data too short for header".to_string());
}
@@ -306,27 +190,21 @@ impl Program {
return Err("Data truncated".to_string());
}
let entry_points = decode_from_slice(
Self::get_slice(data, entry_points_start, sources_start)?,
standard(),
)
.map(|(value, _)| value)
.map_err(|e| format!("Entry points deserialization failed: {}", e))?;
let entry_points =
from_bytes(Self::get_slice(data, entry_points_start, sources_start)?)
.map_err(|e| format!("Entry points deserialization failed: {}", e))?;
let sources = decode_from_slice(
Self::get_slice(data, sources_start, literals_start)?,
standard(),
)
.map(|(value, _)| value)
.map_err(|e| format!("Sources deserialization failed: {}", e))?;
let sources = from_bytes(Self::get_slice(data, sources_start, literals_start)?)
.map_err(|e| format!("Sources deserialization failed: {}", e))?;
let mut needs_recompilation = false;
let literals = match decode_from_slice::<Vec<BinaryValue>, _>(
Self::get_slice(data, literals_start, rule_tree_start)?,
standard(),
) {
Ok((binary_literals, _)) => match binaries_to_values(binary_literals) {
let literals = match from_bytes::<Vec<BinaryValue>>(Self::get_slice(
data,
literals_start,
rule_tree_start,
)?) {
Ok(binary_literals) => match binaries_to_values(binary_literals) {
Ok(values) => values,
Err(_e) => {
needs_recompilation = true;
@@ -339,11 +217,12 @@ impl Program {
}
};
let rule_tree = match decode_from_slice::<BinaryValue, _>(
Self::get_slice(data, rule_tree_start, binary_len_start)?,
standard(),
) {
Ok((binary_tree, _)) => match binary_to_value(binary_tree) {
let rule_tree = match from_bytes::<BinaryValue>(Self::get_slice(
data,
rule_tree_start,
binary_len_start,
)?) {
Ok(binary_tree) => match binary_to_value(binary_tree) {
Ok(value) => value,
Err(_e) => {
needs_recompilation = true;
@@ -356,16 +235,14 @@ impl Program {
}
};
let mut program = match decode_from_slice::<Program, _>(
Self::get_slice(data, binary_start, binary_end)?,
standard(),
) {
Ok((prog, _)) => prog,
Err(_e) => {
needs_recompilation = true;
Program::new()
}
};
let mut program =
match from_bytes::<Program>(Self::get_slice(data, binary_start, binary_end)?) {
Ok(prog) => prog,
Err(_e) => {
needs_recompilation = true;
Program::new()
}
};
program.entry_points = entry_points;
program.sources = sources;
@@ -404,8 +281,16 @@ impl Program {
let version = Self::read_u32(data, 4).ok();
match version {
Some(1..=3) => Ok(true),
Some(1..=4) => Ok(true),
_ => Ok(false),
}
}
fn legacy_rego_v0(data: &[u8], version: u32) -> Option<bool> {
match version {
1 => data.get(16).map(|value| *value != 0),
2 | 3 => data.get(24).map(|value| *value != 0),
_ => None,
}
}
}

View File

@@ -13,8 +13,7 @@ use crate::rvm::program::{binaries_to_values, BinaryValue, Program};
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use bincode::config::standard;
use bincode::serde::decode_from_slice;
use postcard::from_bytes;
/// Test utility function for round-trip serialization
/// Serializes program, deserializes it, and serializes again to check for consistency
@@ -31,7 +30,7 @@ pub fn test_round_trip_serialization(program: &Program) -> Result<(), String> {
serialized1[7],
]);
if version == 2 && serialized1.len() >= 25 {
if version == Program::SERIALIZATION_VERSION && serialized1.len() >= 25 {
let entry_points_len = u32::from_le_bytes([
serialized1[8],
serialized1[9],
@@ -56,11 +55,9 @@ pub fn test_round_trip_serialization(program: &Program) -> Result<(), String> {
let rule_tree_start = literals_start + literals_len;
if literals_len > 0 && serialized1.len() >= rule_tree_start {
match decode_from_slice::<Vec<BinaryValue>, _>(
&serialized1[literals_start..rule_tree_start],
standard(),
) {
Ok((decoded_literals, _)) => {
match from_bytes::<Vec<BinaryValue>>(&serialized1[literals_start..rule_tree_start])
{
Ok(decoded_literals) => {
if binaries_to_values(decoded_literals).is_err() {
return Err(
"Failed to convert literal table from binary representation".into(),
@@ -69,7 +66,7 @@ pub fn test_round_trip_serialization(program: &Program) -> Result<(), String> {
}
Err(err) => {
return Err(format!(
"Failed to decode literal table with bincode: {}",
"Failed to decode literal table with postcard: {}",
err
));
}