use std::collections::HashSet;
use std::collections::BTreeMap;
use thiserror::Error;
use variable_core::ast::{Value, VarFile};
pub const MAGIC: [u8; 4] = *b"VARB";
pub const VERSION_MAJOR: u8 = 1;
pub const VERSION_MINOR: u8 = 0;
pub const SECTION_METADATA: u16 = 0x0001;
pub const SECTION_FEATURE_OVERRIDES: u16 = 0x0002;
pub const DEFAULT_MAX_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
pub const DEFAULT_MAX_STRING_BYTES: usize = 1024 * 1024;
pub const DEFAULT_MAX_SOURCE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotMetadata {
pub schema_revision: u64,
pub manifest_revision: u64,
pub generated_at_unix_ms: u64,
pub source: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Snapshot {
pub metadata: SnapshotMetadata,
pub features: Vec<FeatureSnapshot>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FeatureSnapshot {
pub feature_id: u32,
pub variables: Vec<VariableSnapshot>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VariableSnapshot {
pub variable_id: u32,
pub value: Value,
}
#[derive(Debug, Clone, Copy)]
pub struct EncodeOptions {
pub max_payload_bytes: usize,
pub max_string_bytes: usize,
pub max_source_bytes: usize,
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
max_payload_bytes: DEFAULT_MAX_PAYLOAD_BYTES,
max_string_bytes: DEFAULT_MAX_STRING_BYTES,
max_source_bytes: DEFAULT_MAX_SOURCE_BYTES,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DecodeOptions {
pub max_payload_bytes: usize,
pub max_string_bytes: usize,
pub max_source_bytes: usize,
}
impl Default for DecodeOptions {
fn default() -> Self {
Self {
max_payload_bytes: DEFAULT_MAX_PAYLOAD_BYTES,
max_string_bytes: DEFAULT_MAX_STRING_BYTES,
max_source_bytes: DEFAULT_MAX_SOURCE_BYTES,
}
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum EncodeError {
#[error("source metadata exceeds max size: {len} > {max}")]
SourceTooLarge { len: usize, max: usize },
#[error(
"string value exceeds max size for feature {feature_id} variable {variable_id}: {len} > {max}"
)]
StringTooLarge {
feature_id: u32,
variable_id: u32,
len: usize,
max: usize,
},
#[error("payload exceeds max size: {len} > {max}")]
PayloadTooLarge { len: usize, max: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticKind {
UnsupportedVersion,
MalformedEnvelope,
TruncatedSection,
LimitExceeded,
UnknownSectionType,
DuplicateFeatureId,
DuplicateVariableId,
UnknownValueType,
InvalidBooleanEncoding,
InvalidNumberEncoding,
InvalidUtf8String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeDiagnostic {
pub kind: DiagnosticKind,
pub severity: DiagnosticSeverity,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodeReport {
pub snapshot: Option<Snapshot>,
pub diagnostics: Vec<DecodeDiagnostic>,
}
pub fn snapshot_from_var_file(var_file: &VarFile, metadata: SnapshotMetadata) -> Snapshot {
let struct_defs: std::collections::HashMap<&str, &variable_core::ast::StructDef> = var_file
.structs
.iter()
.map(|s| (s.name.as_str(), s))
.collect();
let mut features: Vec<FeatureSnapshot> = var_file
.features
.iter()
.map(|feature| FeatureSnapshot {
feature_id: feature.id,
variables: feature
.variables
.iter()
.map(|variable| {
let value = resolve_default_value(&variable.default, &struct_defs);
VariableSnapshot {
variable_id: variable.id,
value,
}
})
.collect(),
})
.collect();
for feature in &mut features {
feature
.variables
.sort_by_key(|variable| variable.variable_id);
}
features.sort_by_key(|feature| feature.feature_id);
Snapshot { metadata, features }
}
fn resolve_default_value(
value: &Value,
struct_defs: &std::collections::HashMap<&str, &variable_core::ast::StructDef>,
) -> Value {
match value {
Value::Struct {
struct_name,
fields,
} => {
if let Some(struct_def) = struct_defs.get(struct_name.as_str()) {
let mut resolved_fields = BTreeMap::new();
for field in &struct_def.fields {
let field_value = fields
.get(&field.name)
.cloned()
.unwrap_or_else(|| field.default.clone());
resolved_fields.insert(field.name.clone(), field_value);
}
Value::Struct {
struct_name: struct_name.clone(),
fields: resolved_fields,
}
} else {
value.clone()
}
}
other => other.clone(),
}
}
pub fn encode_var_file_defaults(
var_file: &VarFile,
metadata: SnapshotMetadata,
) -> Result<Vec<u8>, EncodeError> {
let snapshot = snapshot_from_var_file(var_file, metadata);
encode_snapshot(&snapshot)
}
pub fn encode_snapshot(snapshot: &Snapshot) -> Result<Vec<u8>, EncodeError> {
encode_snapshot_with_options(snapshot, EncodeOptions::default())
}
pub fn encode_snapshot_with_options(
snapshot: &Snapshot,
options: EncodeOptions,
) -> Result<Vec<u8>, EncodeError> {
let metadata_payload = encode_metadata_payload(&snapshot.metadata, &options)?;
let mut features = snapshot.features.clone();
features.sort_by_key(|feature| feature.feature_id);
for feature in &mut features {
feature
.variables
.sort_by_key(|variable| variable.variable_id);
}
let overrides_payload = encode_overrides_payload(&features, &options)?;
let mut out = Vec::with_capacity(12 + 8 + metadata_payload.len() + 8 + overrides_payload.len());
out.extend_from_slice(&MAGIC);
out.push(VERSION_MAJOR);
out.push(VERSION_MINOR);
push_u16(&mut out, 0);
push_u32(&mut out, 2);
push_section(&mut out, SECTION_METADATA, &metadata_payload)?;
push_section(&mut out, SECTION_FEATURE_OVERRIDES, &overrides_payload)?;
if out.len() > options.max_payload_bytes {
return Err(EncodeError::PayloadTooLarge {
len: out.len(),
max: options.max_payload_bytes,
});
}
Ok(out)
}
pub fn decode_snapshot(input: &[u8]) -> DecodeReport {
decode_snapshot_with_options(input, DecodeOptions::default())
}
pub fn decode_snapshot_with_options(input: &[u8], options: DecodeOptions) -> DecodeReport {
let mut diagnostics = Vec::new();
if input.len() > options.max_payload_bytes {
push_diag(
&mut diagnostics,
DiagnosticKind::LimitExceeded,
DiagnosticSeverity::Error,
format!(
"payload exceeds max size: {} > {}",
input.len(),
options.max_payload_bytes
),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
}
let mut cursor = Cursor::new(input);
let Some(magic) = cursor.read_exact(4) else {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"payload is too short to contain header".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
if magic != MAGIC {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"invalid magic bytes".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
}
let Some(version_major) = cursor.read_u8() else {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"missing version_major".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
let _version_minor = match cursor.read_u8() {
Some(value) => value,
None => {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"missing version_minor".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
}
};
if version_major != VERSION_MAJOR {
push_diag(
&mut diagnostics,
DiagnosticKind::UnsupportedVersion,
DiagnosticSeverity::Error,
format!(
"unsupported major version: {} (expected {})",
version_major, VERSION_MAJOR
),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
}
let Some(_flags) = cursor.read_u16() else {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"missing flags".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
let Some(section_count) = cursor.read_u32() else {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"missing section_count".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
let mut metadata: Option<SnapshotMetadata> = None;
let mut features: Option<Vec<FeatureSnapshot>> = None;
for _ in 0..section_count {
let Some(section_type) = cursor.read_u16() else {
push_diag(
&mut diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Error,
"truncated section header (type)".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
let Some(reserved) = cursor.read_u16() else {
push_diag(
&mut diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Error,
"truncated section header (reserved)".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
if reserved != 0 {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
format!("section {} has non-zero reserved field", section_type),
);
}
let Some(section_len) = cursor.read_u32() else {
push_diag(
&mut diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Error,
"truncated section header (length)".to_string(),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
let section_len = section_len as usize;
let Some(payload) = cursor.read_exact(section_len) else {
push_diag(
&mut diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Error,
format!("section {} truncated", section_type),
);
return DecodeReport {
snapshot: None,
diagnostics,
};
};
match section_type {
SECTION_METADATA => {
if metadata.is_some() {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
"duplicate metadata section; ignoring later section".to_string(),
);
continue;
}
metadata = decode_metadata(payload, &options, &mut diagnostics);
}
SECTION_FEATURE_OVERRIDES => {
if features.is_some() {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
"duplicate overrides section; ignoring later section".to_string(),
);
continue;
}
features = decode_overrides(payload, &options, &mut diagnostics);
}
unknown => {
push_diag(
&mut diagnostics,
DiagnosticKind::UnknownSectionType,
DiagnosticSeverity::Info,
format!("unknown section type {} skipped", unknown),
);
}
}
}
if cursor.remaining() > 0 {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
"trailing bytes after section list".to_string(),
);
}
let snapshot = match (metadata, features) {
(Some(metadata), Some(features)) => Some(Snapshot { metadata, features }),
_ => {
push_diag(
&mut diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Error,
"missing required section(s)".to_string(),
);
None
}
};
DecodeReport {
snapshot,
diagnostics,
}
}
fn encode_metadata_payload(
metadata: &SnapshotMetadata,
options: &EncodeOptions,
) -> Result<Vec<u8>, EncodeError> {
let source_bytes = metadata.source.as_deref().unwrap_or("").as_bytes();
if source_bytes.len() > options.max_source_bytes {
return Err(EncodeError::SourceTooLarge {
len: source_bytes.len(),
max: options.max_source_bytes,
});
}
let mut payload = Vec::with_capacity(8 + 8 + 8 + 4 + source_bytes.len());
push_u64(&mut payload, metadata.schema_revision);
push_u64(&mut payload, metadata.manifest_revision);
push_u64(&mut payload, metadata.generated_at_unix_ms);
push_u32(
&mut payload,
checked_u32_len(source_bytes.len(), options.max_payload_bytes)?,
);
payload.extend_from_slice(source_bytes);
Ok(payload)
}
fn encode_overrides_payload(
features: &[FeatureSnapshot],
options: &EncodeOptions,
) -> Result<Vec<u8>, EncodeError> {
let mut payload = Vec::new();
push_u32(
&mut payload,
checked_u32_len(features.len(), options.max_payload_bytes)?,
);
for feature in features {
push_u32(&mut payload, feature.feature_id);
push_u32(
&mut payload,
checked_u32_len(feature.variables.len(), options.max_payload_bytes)?,
);
for variable in &feature.variables {
push_u32(&mut payload, variable.variable_id);
match &variable.value {
Value::Boolean(value) => {
payload.push(1);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(&mut payload, 1);
payload.push(u8::from(*value));
}
Value::Float(value) => {
payload.push(2);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(&mut payload, 8);
payload.extend_from_slice(&value.to_le_bytes());
}
Value::String(value) => {
let bytes = value.as_bytes();
if bytes.len() > options.max_string_bytes {
return Err(EncodeError::StringTooLarge {
feature_id: feature.feature_id,
variable_id: variable.variable_id,
len: bytes.len(),
max: options.max_string_bytes,
});
}
payload.push(3);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(
&mut payload,
checked_u32_len(bytes.len(), options.max_payload_bytes)?,
);
payload.extend_from_slice(bytes);
}
Value::Integer(value) => {
payload.push(4);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(&mut payload, 8);
payload.extend_from_slice(&value.to_le_bytes());
}
Value::Struct { fields, .. } => {
payload.push(5);
payload.extend_from_slice(&[0, 0, 0]);
let struct_payload = encode_struct_value_payload(
feature.feature_id,
variable.variable_id,
fields,
options,
)?;
push_u32(
&mut payload,
checked_u32_len(struct_payload.len(), options.max_payload_bytes)?,
);
payload.extend_from_slice(&struct_payload);
}
}
}
}
Ok(payload)
}
fn encode_struct_value_payload(
feature_id: u32,
variable_id: u32,
fields: &BTreeMap<String, Value>,
options: &EncodeOptions,
) -> Result<Vec<u8>, EncodeError> {
let mut payload = Vec::new();
push_u32(
&mut payload,
checked_u32_len(fields.len(), options.max_payload_bytes)?,
);
for (name, value) in fields {
let name_bytes = name.as_bytes();
push_u32(
&mut payload,
checked_u32_len(name_bytes.len(), options.max_payload_bytes)?,
);
payload.extend_from_slice(name_bytes);
match value {
Value::Boolean(v) => {
payload.push(1);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(&mut payload, 1);
payload.push(u8::from(*v));
}
Value::Float(v) => {
payload.push(2);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(&mut payload, 8);
payload.extend_from_slice(&v.to_le_bytes());
}
Value::String(v) => {
let bytes = v.as_bytes();
if bytes.len() > options.max_string_bytes {
return Err(EncodeError::StringTooLarge {
feature_id,
variable_id,
len: bytes.len(),
max: options.max_string_bytes,
});
}
payload.push(3);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(
&mut payload,
checked_u32_len(bytes.len(), options.max_payload_bytes)?,
);
payload.extend_from_slice(bytes);
}
Value::Integer(v) => {
payload.push(4);
payload.extend_from_slice(&[0, 0, 0]);
push_u32(&mut payload, 8);
payload.extend_from_slice(&v.to_le_bytes());
}
Value::Struct { .. } => {
return Err(EncodeError::PayloadTooLarge { len: 0, max: 0 });
}
}
}
Ok(payload)
}
fn push_section(out: &mut Vec<u8>, section_type: u16, payload: &[u8]) -> Result<(), EncodeError> {
if payload.len() > u32::MAX as usize {
return Err(EncodeError::PayloadTooLarge {
len: payload.len(),
max: u32::MAX as usize,
});
}
push_u16(out, section_type);
push_u16(out, 0);
push_u32(out, payload.len() as u32);
out.extend_from_slice(payload);
Ok(())
}
fn checked_u32_len(len: usize, max_payload_bytes: usize) -> Result<u32, EncodeError> {
if len > u32::MAX as usize {
return Err(EncodeError::PayloadTooLarge {
len,
max: max_payload_bytes,
});
}
Ok(len as u32)
}
fn decode_metadata(
payload: &[u8],
options: &DecodeOptions,
diagnostics: &mut Vec<DecodeDiagnostic>,
) -> Option<SnapshotMetadata> {
let mut cursor = Cursor::new(payload);
macro_rules! read_or_diag {
($expr:expr, $message:expr) => {
match $expr {
Some(value) => value,
None => {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Error,
$message.to_string(),
);
return None;
}
}
};
}
let schema_revision = read_or_diag!(
cursor.read_u64(),
"metadata section missing schema_revision"
);
let manifest_revision = read_or_diag!(
cursor.read_u64(),
"metadata section missing manifest_revision"
);
let generated_at_unix_ms = read_or_diag!(
cursor.read_u64(),
"metadata section missing generated_at_unix_ms"
);
let source_len =
read_or_diag!(cursor.read_u32(), "metadata section missing source_len") as usize;
let source_bytes = read_or_diag!(
cursor.read_exact(source_len),
"metadata section has truncated source bytes"
);
let source = if source_len == 0 {
None
} else if source_len > options.max_source_bytes {
push_diag(
diagnostics,
DiagnosticKind::LimitExceeded,
DiagnosticSeverity::Warning,
format!(
"metadata source exceeds max size: {} > {}",
source_len, options.max_source_bytes
),
);
None
} else {
match String::from_utf8(source_bytes.to_vec()) {
Ok(value) => Some(value),
Err(_) => {
push_diag(
diagnostics,
DiagnosticKind::InvalidUtf8String,
DiagnosticSeverity::Warning,
"metadata source is not valid UTF-8".to_string(),
);
None
}
}
};
if cursor.remaining() > 0 {
push_diag(
diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
"metadata section has trailing bytes".to_string(),
);
}
Some(SnapshotMetadata {
schema_revision,
manifest_revision,
generated_at_unix_ms,
source,
})
}
fn decode_overrides(
payload: &[u8],
options: &DecodeOptions,
diagnostics: &mut Vec<DecodeDiagnostic>,
) -> Option<Vec<FeatureSnapshot>> {
let mut cursor = Cursor::new(payload);
macro_rules! read_or_diag {
($expr:expr, $message:expr) => {
match $expr {
Some(value) => value,
None => {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Error,
$message.to_string(),
);
return None;
}
}
};
}
let feature_count = read_or_diag!(cursor.read_u32(), "overrides section missing feature_count");
let mut features = Vec::new();
let mut seen_feature_ids = HashSet::new();
for _ in 0..feature_count {
let feature_id = read_or_diag!(cursor.read_u32(), "overrides section missing feature_id");
let variable_count = read_or_diag!(
cursor.read_u32(),
"overrides section missing variable_count"
);
let duplicate_feature = !seen_feature_ids.insert(feature_id);
if duplicate_feature {
push_diag(
diagnostics,
DiagnosticKind::DuplicateFeatureId,
DiagnosticSeverity::Warning,
format!("duplicate feature id {} in payload", feature_id),
);
}
let mut variables = Vec::new();
let mut seen_variable_ids = HashSet::new();
for _ in 0..variable_count {
let variable_id =
read_or_diag!(cursor.read_u32(), "overrides section missing variable_id");
let value_type =
read_or_diag!(cursor.read_u8(), "overrides section missing value_type");
let reserved = read_or_diag!(
cursor.read_exact(3),
"overrides section missing variable reserved bytes"
);
if reserved != [0, 0, 0] {
push_diag(
diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
format!(
"variable {} in feature {} has non-zero reserved bytes",
variable_id, feature_id
),
);
}
let value_len =
read_or_diag!(cursor.read_u32(), "overrides section missing value_len") as usize;
let value_bytes = read_or_diag!(
cursor.read_exact(value_len),
"overrides section has truncated value bytes"
);
if !seen_variable_ids.insert(variable_id) {
push_diag(
diagnostics,
DiagnosticKind::DuplicateVariableId,
DiagnosticSeverity::Warning,
format!(
"duplicate variable id {} in feature {}",
variable_id, feature_id
),
);
continue;
}
let Some(value) = decode_value(
feature_id,
variable_id,
value_type,
value_len,
value_bytes,
options,
diagnostics,
) else {
continue;
};
variables.push(VariableSnapshot { variable_id, value });
}
if !duplicate_feature {
variables.sort_by_key(|variable| variable.variable_id);
features.push(FeatureSnapshot {
feature_id,
variables,
});
}
}
if cursor.remaining() > 0 {
push_diag(
diagnostics,
DiagnosticKind::MalformedEnvelope,
DiagnosticSeverity::Warning,
"overrides section has trailing bytes".to_string(),
);
}
features.sort_by_key(|feature| feature.feature_id);
Some(features)
}
fn decode_value(
feature_id: u32,
variable_id: u32,
value_type: u8,
value_len: usize,
value_bytes: &[u8],
options: &DecodeOptions,
diagnostics: &mut Vec<DecodeDiagnostic>,
) -> Option<Value> {
match value_type {
1 => {
if value_len != 1 {
push_diag(
diagnostics,
DiagnosticKind::InvalidBooleanEncoding,
DiagnosticSeverity::Warning,
format!(
"invalid boolean length {} for feature {} variable {}",
value_len, feature_id, variable_id
),
);
return None;
}
match value_bytes[0] {
0 => Some(Value::Boolean(false)),
1 => Some(Value::Boolean(true)),
other => {
push_diag(
diagnostics,
DiagnosticKind::InvalidBooleanEncoding,
DiagnosticSeverity::Warning,
format!(
"invalid boolean byte {} for feature {} variable {}",
other, feature_id, variable_id
),
);
None
}
}
}
2 => {
if value_len != 8 {
push_diag(
diagnostics,
DiagnosticKind::InvalidNumberEncoding,
DiagnosticSeverity::Warning,
format!(
"invalid float length {} for feature {} variable {}",
value_len, feature_id, variable_id
),
);
return None;
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(value_bytes);
Some(Value::Float(f64::from_le_bytes(bytes)))
}
3 => {
if value_len > options.max_string_bytes {
push_diag(
diagnostics,
DiagnosticKind::LimitExceeded,
DiagnosticSeverity::Warning,
format!(
"string value exceeds max size for feature {} variable {}: {} > {}",
feature_id, variable_id, value_len, options.max_string_bytes
),
);
return None;
}
match String::from_utf8(value_bytes.to_vec()) {
Ok(value) => Some(Value::String(value)),
Err(_) => {
push_diag(
diagnostics,
DiagnosticKind::InvalidUtf8String,
DiagnosticSeverity::Warning,
format!(
"string value is not valid UTF-8 for feature {} variable {}",
feature_id, variable_id
),
);
None
}
}
}
4 => {
if value_len != 8 {
push_diag(
diagnostics,
DiagnosticKind::InvalidNumberEncoding,
DiagnosticSeverity::Warning,
format!(
"invalid integer length {} for feature {} variable {}",
value_len, feature_id, variable_id
),
);
return None;
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(value_bytes);
Some(Value::Integer(i64::from_le_bytes(bytes)))
}
5 => {
let mut cursor = Cursor::new(value_bytes);
let Some(field_count) = cursor.read_u32() else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct value missing field_count for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let mut fields = BTreeMap::new();
for _ in 0..field_count {
let Some(name_len) = cursor.read_u32() else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct field name_len truncated for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let Some(name_bytes) = cursor.read_exact(name_len as usize) else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct field name truncated for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let field_name = match String::from_utf8(name_bytes.to_vec()) {
Ok(s) => s,
Err(_) => {
push_diag(
diagnostics,
DiagnosticKind::InvalidUtf8String,
DiagnosticSeverity::Warning,
format!(
"struct field name is not valid UTF-8 for feature {} variable {}",
feature_id, variable_id
),
);
return None;
}
};
let Some(field_value_type) = cursor.read_u8() else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct field value_type truncated for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let Some(_reserved) = cursor.read_exact(3) else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct field reserved truncated for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let Some(field_value_len) = cursor.read_u32() else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct field value_len truncated for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let Some(field_value_bytes) = cursor.read_exact(field_value_len as usize) else {
push_diag(
diagnostics,
DiagnosticKind::TruncatedSection,
DiagnosticSeverity::Warning,
format!(
"struct field value bytes truncated for feature {} variable {}",
feature_id, variable_id
),
);
return None;
};
let Some(field_value) = decode_value(
feature_id,
variable_id,
field_value_type,
field_value_len as usize,
field_value_bytes,
options,
diagnostics,
) else {
continue;
};
fields.insert(field_name, field_value);
}
Some(Value::Struct {
struct_name: String::new(), fields,
})
}
other => {
push_diag(
diagnostics,
DiagnosticKind::UnknownValueType,
DiagnosticSeverity::Warning,
format!(
"unknown value type {} for feature {} variable {}",
other, feature_id, variable_id
),
);
None
}
}
}
fn push_diag(
diagnostics: &mut Vec<DecodeDiagnostic>,
kind: DiagnosticKind,
severity: DiagnosticSeverity,
message: String,
) {
diagnostics.push(DecodeDiagnostic {
kind,
severity,
message,
});
}
fn push_u16(out: &mut Vec<u8>, value: u16) {
out.extend_from_slice(&value.to_le_bytes());
}
fn push_u32(out: &mut Vec<u8>, value: u32) {
out.extend_from_slice(&value.to_le_bytes());
}
fn push_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
struct Cursor<'a> {
input: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(input: &'a [u8]) -> Self {
Self { input, pos: 0 }
}
fn remaining(&self) -> usize {
self.input.len().saturating_sub(self.pos)
}
fn read_exact(&mut self, len: usize) -> Option<&'a [u8]> {
if self.remaining() < len {
return None;
}
let end = self.pos + len;
let value = &self.input[self.pos..end];
self.pos = end;
Some(value)
}
fn read_u8(&mut self) -> Option<u8> {
self.read_exact(1).map(|bytes| bytes[0])
}
fn read_u16(&mut self) -> Option<u16> {
let bytes = self.read_exact(2)?;
let mut array = [0u8; 2];
array.copy_from_slice(bytes);
Some(u16::from_le_bytes(array))
}
fn read_u32(&mut self) -> Option<u32> {
let bytes = self.read_exact(4)?;
let mut array = [0u8; 4];
array.copy_from_slice(bytes);
Some(u32::from_le_bytes(array))
}
fn read_u64(&mut self) -> Option<u64> {
let bytes = self.read_exact(8)?;
let mut array = [0u8; 8];
array.copy_from_slice(bytes);
Some(u64::from_le_bytes(array))
}
}
#[cfg(test)]
mod tests {
use super::*;
use variable_core::parse_and_validate;
#[test]
fn encode_decode_roundtrip_snapshot() {
let snapshot = Snapshot {
metadata: SnapshotMetadata {
schema_revision: 7,
manifest_revision: 11,
generated_at_unix_ms: 123,
source: Some("test".to_string()),
},
features: vec![
FeatureSnapshot {
feature_id: 2,
variables: vec![
VariableSnapshot {
variable_id: 2,
value: Value::String("hello".to_string()),
},
VariableSnapshot {
variable_id: 1,
value: Value::Boolean(true),
},
],
},
FeatureSnapshot {
feature_id: 1,
variables: vec![VariableSnapshot {
variable_id: 1,
value: Value::Integer(42),
}],
},
],
};
let encoded = encode_snapshot(&snapshot).unwrap();
let report = decode_snapshot(&encoded);
assert!(report.diagnostics.is_empty());
let decoded = report.snapshot.unwrap();
assert_eq!(decoded.features[0].feature_id, 1);
assert_eq!(decoded.features[1].feature_id, 2);
assert_eq!(decoded.features[1].variables[0].variable_id, 1);
assert_eq!(decoded.features[1].variables[1].variable_id, 2);
}
#[test]
fn encode_var_file_defaults_roundtrip() {
let source = r#"1: Feature Checkout = {
1: enabled Boolean = true
2: max_items Integer = 50
3: header_text String = "Complete your purchase"
4: discount_rate Float = 0.15
}
2: Feature Search = {
1: enabled Boolean = false
2: max_results Integer = 10
3: placeholder String = "Search..."
4: boost_factor Float = 1.5
}"#;
let var_file = parse_and_validate(source).unwrap();
let metadata = SnapshotMetadata {
schema_revision: 1,
manifest_revision: 2,
generated_at_unix_ms: 3,
source: Some("fixture".to_string()),
};
let encoded = encode_var_file_defaults(&var_file, metadata).unwrap();
let report = decode_snapshot(&encoded);
assert!(report.diagnostics.is_empty());
let decoded = report.snapshot.unwrap();
assert_eq!(decoded.features.len(), 2);
assert_eq!(decoded.features[0].feature_id, 1);
assert_eq!(decoded.features[0].variables.len(), 4);
assert_eq!(decoded.features[1].feature_id, 2);
assert_eq!(
decoded.features[1].variables[0].value,
Value::Boolean(false)
);
assert_eq!(decoded.features[0].variables[3].value, Value::Float(0.15));
assert_eq!(decoded.features[1].variables[3].value, Value::Float(1.5));
}
#[test]
fn decode_skips_unknown_section() {
let snapshot = Snapshot {
metadata: SnapshotMetadata {
schema_revision: 1,
manifest_revision: 1,
generated_at_unix_ms: 1,
source: None,
},
features: vec![FeatureSnapshot {
feature_id: 1,
variables: vec![VariableSnapshot {
variable_id: 1,
value: Value::Boolean(true),
}],
}],
};
let mut encoded = encode_snapshot(&snapshot).unwrap();
encoded[8..12].copy_from_slice(&3u32.to_le_bytes());
encoded.extend_from_slice(&0x9000u16.to_le_bytes());
encoded.extend_from_slice(&0u16.to_le_bytes());
encoded.extend_from_slice(&4u32.to_le_bytes());
encoded.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let report = decode_snapshot(&encoded);
assert!(report.snapshot.is_some());
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == DiagnosticKind::UnknownSectionType)
);
}
#[test]
fn decode_rejects_invalid_magic() {
let report = decode_snapshot(b"NOPE");
assert!(report.snapshot.is_none());
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == DiagnosticKind::MalformedEnvelope)
);
}
#[test]
fn decode_reports_invalid_boolean_value() {
let snapshot = Snapshot {
metadata: SnapshotMetadata {
schema_revision: 1,
manifest_revision: 1,
generated_at_unix_ms: 1,
source: None,
},
features: vec![FeatureSnapshot {
feature_id: 1,
variables: vec![VariableSnapshot {
variable_id: 1,
value: Value::Boolean(true),
}],
}],
};
let mut encoded = encode_snapshot(&snapshot).unwrap();
let last = encoded.len() - 1;
encoded[last] = 2;
let report = decode_snapshot(&encoded);
let decoded = report.snapshot.unwrap();
assert_eq!(decoded.features[0].variables.len(), 0);
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == DiagnosticKind::InvalidBooleanEncoding)
);
}
#[test]
fn encode_enforces_source_size_limit() {
let snapshot = Snapshot {
metadata: SnapshotMetadata {
schema_revision: 1,
manifest_revision: 1,
generated_at_unix_ms: 1,
source: Some("abc".to_string()),
},
features: Vec::new(),
};
let err = encode_snapshot_with_options(
&snapshot,
EncodeOptions {
max_source_bytes: 2,
..EncodeOptions::default()
},
)
.unwrap_err();
assert_eq!(err, EncodeError::SourceTooLarge { len: 3, max: 2 });
}
#[test]
fn decode_respects_string_size_limit() {
let snapshot = Snapshot {
metadata: SnapshotMetadata {
schema_revision: 1,
manifest_revision: 1,
generated_at_unix_ms: 1,
source: None,
},
features: vec![FeatureSnapshot {
feature_id: 1,
variables: vec![VariableSnapshot {
variable_id: 1,
value: Value::String("abc".to_string()),
}],
}],
};
let encoded = encode_snapshot(&snapshot).unwrap();
let report = decode_snapshot_with_options(
&encoded,
DecodeOptions {
max_string_bytes: 2,
..DecodeOptions::default()
},
);
let decoded = report.snapshot.unwrap();
assert!(decoded.features[0].variables.is_empty());
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == DiagnosticKind::LimitExceeded)
);
}
}