use crate::action::ContentHash;
use crate::error::{Result, SomaError};
use crate::value::Value;
use std::sync::Arc;
pub const MAGIC: &[u8; 6] = b"SOMA1\0";
const TAG_EMPTY: u8 = 0;
const TAG_TENSOR: u8 = 1;
const TAG_JSON: u8 = 2;
const TAG_BYTES: u8 = 3;
const TAG_OBJECT: u8 = 4;
const TAG_TEXT: u8 = 5;
const COMPRESSION_RAW: u8 = 0;
pub fn encode_value(value: &Value) -> Result<Vec<u8>> {
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(MAGIC);
match value {
Value::Empty => {
buf.push(TAG_EMPTY);
buf.push(COMPRESSION_RAW);
}
Value::Tensor { values, shape } => {
buf.push(TAG_TENSOR);
buf.push(COMPRESSION_RAW);
buf.reserve(4 + shape.len() * 8 + 8 + values.len() * 8);
buf.extend_from_slice(&(shape.len() as u32).to_le_bytes());
for dim in shape {
buf.extend_from_slice(&(*dim as u64).to_le_bytes());
}
buf.extend_from_slice(&(values.len() as u64).to_le_bytes());
for v in values.iter() {
buf.extend_from_slice(&v.to_le_bytes());
}
}
Value::Text(s) => {
buf.push(TAG_TEXT);
buf.push(COMPRESSION_RAW);
buf.extend_from_slice(s.as_bytes());
}
Value::Json(v) => {
buf.push(TAG_JSON);
buf.push(COMPRESSION_RAW);
let bytes = serde_json::to_vec(v.as_ref())
.map_err(|e| SomaError::Cache(format!("codec: json encode: {e}")))?;
buf.extend_from_slice(&bytes);
}
Value::Bytes(b) => {
buf.push(TAG_BYTES);
buf.push(COMPRESSION_RAW);
buf.extend_from_slice(b);
}
Value::Object(b) => {
buf.push(TAG_OBJECT);
buf.push(COMPRESSION_RAW);
buf.extend_from_slice(b);
}
}
Ok(buf)
}
pub fn encode_and_hash(value: &Value) -> Result<(Vec<u8>, ContentHash)> {
let bytes = encode_value(value)?;
let hash = ContentHash::blake3(&bytes);
Ok((bytes, hash))
}
pub fn decode_value(bytes: &[u8]) -> Result<Value> {
if bytes.len() < 8 || &bytes[..6] != MAGIC {
return Err(SomaError::Cache("codec: not a SOMA1 frame".into()));
}
let tag = bytes[6];
if bytes[7] != COMPRESSION_RAW {
return Err(SomaError::Cache(format!(
"codec: unknown compression byte {}",
bytes[7]
)));
}
let payload = &bytes[8..];
match tag {
TAG_EMPTY => Ok(Value::Empty),
TAG_TENSOR => decode_tensor(payload),
TAG_JSON => {
let v: serde_json::Value = serde_json::from_slice(payload)
.map_err(|e| SomaError::Cache(format!("codec: json decode: {e}")))?;
Ok(Value::json(v))
}
TAG_TEXT => {
let s = std::str::from_utf8(payload)
.map_err(|e| SomaError::Cache(format!("codec: text decode: {e}")))?;
Ok(Value::text(s))
}
TAG_BYTES => Ok(Value::bytes(payload.to_vec())),
TAG_OBJECT => Ok(Value::object(payload.to_vec())),
other => Err(SomaError::Cache(format!("codec: unknown tag {other}"))),
}
}
fn decode_tensor(payload: &[u8]) -> Result<Value> {
let err = || SomaError::Cache("codec: truncated tensor frame".into());
let mut at = 0usize;
let take = |at: &mut usize, n: usize| -> Result<&[u8]> {
let slice = payload.get(*at..*at + n).ok_or_else(err)?;
*at += n;
Ok(slice)
};
let ndim = u32::from_le_bytes(take(&mut at, 4)?.try_into().unwrap()) as usize;
if ndim > 64 {
return Err(SomaError::Cache(format!("codec: implausible ndim {ndim}")));
}
let mut shape = Vec::with_capacity(ndim);
for _ in 0..ndim {
shape.push(u64::from_le_bytes(take(&mut at, 8)?.try_into().unwrap()) as usize);
}
let count = u64::from_le_bytes(take(&mut at, 8)?.try_into().unwrap()) as usize;
let data = take(&mut at, count.checked_mul(8).ok_or_else(err)?)?;
let mut values = Vec::with_capacity(count);
for chunk in data.chunks_exact(8) {
values.push(f64::from_le_bytes(chunk.try_into().unwrap()));
}
Ok(Value::Tensor {
values: Arc::new(values),
shape,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn roundtrip_all_variants() {
let values = vec![
Value::Empty,
Value::tensor(vec![1.0, -2.5, f64::MAX, 0.0], vec![2, 2]),
Value::tensor(vec![], vec![0]),
Value::json(json!({"a": [1, 2.5, "x"], "b": null})),
Value::bytes(vec![0, 255, 128]),
Value::object(vec![0x80, 0x04]),
Value::text(""),
Value::text("Summarize the following in one sentence."),
Value::text("resumen: ¿qué pasó? — 数字 🧬"),
];
for v in values {
let (bytes, hash) = encode_and_hash(&v).unwrap();
assert!(hash.verify(&bytes));
assert_eq!(decode_value(&bytes).unwrap(), v, "roundtrip failed for {v}");
}
}
#[test]
fn text_and_json_string_are_distinct() {
let (text_bytes, text_hash) = encode_and_hash(&Value::text("hello")).unwrap();
let (json_bytes, json_hash) = encode_and_hash(&Value::json(json!("hello"))).unwrap();
assert_ne!(text_bytes, json_bytes);
assert_ne!(text_hash, json_hash);
assert_eq!(decode_value(&text_bytes).unwrap(), Value::text("hello"));
assert_eq!(
decode_value(&json_bytes).unwrap(),
Value::json(json!("hello"))
);
}
#[test]
fn text_frame_rejects_invalid_utf8() {
let mut frame = Vec::from(MAGIC);
frame.push(TAG_TEXT);
frame.push(COMPRESSION_RAW);
frame.extend_from_slice(&[0xff, 0xfe]);
assert!(decode_value(&frame).is_err());
}
#[test]
fn tensor_size_is_near_raw() {
let n = 10_000;
let v = Value::tensor((0..n).map(|i| (i as f64).sin()).collect(), vec![n]);
let encoded = encode_value(&v).unwrap();
let raw = n * 8;
assert!(
encoded.len() <= raw + raw / 10 + 64,
"encoded {} bytes vs raw {} — must stay within ~1.1×",
encoded.len(),
raw
);
let json_len = serde_json::to_vec(&v).unwrap().len();
assert!(
encoded.len() * 2 < json_len,
"binary ({}) should be well under half of JSON ({})",
encoded.len(),
json_len
);
}
#[test]
fn identical_values_hash_identically() {
let a = Value::tensor(vec![1.0, 2.0], vec![2]);
let b = Value::tensor(vec![1.0, 2.0], vec![2]);
assert_eq!(
encode_and_hash(&a).unwrap().1,
encode_and_hash(&b).unwrap().1
);
let c = Value::tensor(vec![1.0, 2.0], vec![2, 1]);
assert_ne!(
encode_and_hash(&a).unwrap().1,
encode_and_hash(&c).unwrap().1
);
}
#[test]
fn garbage_rejected() {
assert!(decode_value(b"").is_err());
assert!(decode_value(b"NOTSOMA1xxxx").is_err());
let mut frame = encode_value(&Value::Empty).unwrap();
frame[7] = 9; assert!(decode_value(&frame).is_err());
}
}