use std::collections::BTreeMap;
use tensogram::*;
fn make_simple_float32_pair(shape: Vec<u64>) -> (GlobalMetadata, DataObjectDescriptor) {
let strides: Vec<u64> = if shape.is_empty() {
vec![]
} else {
let mut s = vec![1u64; shape.len()];
for i in (0..shape.len() - 1).rev() {
s[i] = s[i + 1] * shape[i + 1];
}
s
};
let global = GlobalMetadata {
extra: BTreeMap::new(),
..Default::default()
};
let desc = DataObjectDescriptor {
obj_type: "ntensor".to_string(),
ndim: shape.len() as u64,
shape,
strides,
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
masks: None,
};
(global, desc)
}
fn make_shuffle_pair(shape: Vec<u64>, element_size: u64) -> (GlobalMetadata, DataObjectDescriptor) {
let strides: Vec<u64> = if shape.is_empty() {
vec![]
} else {
let mut s = vec![1u64; shape.len()];
for i in (0..shape.len() - 1).rev() {
s[i] = s[i + 1] * shape[i + 1];
}
s
};
let mut params = BTreeMap::new();
params.insert(
"shuffle_element_size".to_string(),
ciborium::Value::Integer(element_size.into()),
);
let global = GlobalMetadata {
extra: BTreeMap::new(),
..Default::default()
};
let desc = DataObjectDescriptor {
obj_type: "ntensor".to_string(),
ndim: shape.len() as u64,
shape,
strides,
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "shuffle".to_string(),
compression: "none".to_string(),
params,
masks: None,
};
(global, desc)
}
#[test]
fn test_adversarial_truncated_message_rejected() {
let (global, desc) = make_simple_float32_pair(vec![4]);
let data = vec![0u8; 4 * 4];
let encoded = encode(&global, &[(&desc, &data)], &EncodeOptions::default()).unwrap();
let truncated = &encoded[..encoded.len() / 2];
let result = decode(truncated, &DecodeOptions::default());
assert!(
result.is_err(),
"expected Err for truncated message but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_wrong_magic_rejected() {
let (global, desc) = make_simple_float32_pair(vec![4]);
let data = vec![0u8; 4 * 4];
let mut encoded = encode(&global, &[(&desc, &data)], &EncodeOptions::default()).unwrap();
encoded[0..8].copy_from_slice(b"BADMAGIC");
let result = decode(&encoded, &DecodeOptions::default());
assert!(
result.is_err(),
"expected Err for wrong magic but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_corrupted_end_magic_rejected() {
let (global, desc) = make_simple_float32_pair(vec![4]);
let data = vec![0u8; 4 * 4];
let mut encoded = encode(&global, &[(&desc, &data)], &EncodeOptions::default()).unwrap();
let len = encoded.len();
encoded[len - 8..].copy_from_slice(b"BADMAGIC");
let result = decode(&encoded, &DecodeOptions::default());
assert!(
result.is_err(),
"expected Err for corrupted end magic but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_empty_buffer_rejected() {
let result = decode(&[], &DecodeOptions::default());
assert!(
result.is_err(),
"expected Err for empty buffer but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_negative_cbor_int_wraps() {
let below_i32_min: i64 = i32::MIN as i64 - 1;
let mut params = BTreeMap::new();
params.insert(
"sp_reference_value".to_string(),
ciborium::Value::Float(0.0),
);
params.insert(
"sp_binary_scale_factor".to_string(),
ciborium::Value::Integer(below_i32_min.into()),
);
params.insert(
"sp_decimal_scale_factor".to_string(),
ciborium::Value::Integer(0.into()),
);
params.insert(
"sp_bits_per_value".to_string(),
ciborium::Value::Integer(16.into()),
);
let global = GlobalMetadata {
extra: BTreeMap::new(),
..Default::default()
};
let desc = DataObjectDescriptor {
obj_type: "ntensor".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![1],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "simple_packing".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params,
masks: None,
};
let data = vec![0u8; 4 * 8];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for out-of-range binary_scale_factor but got Ok: {:?}",
result
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("binary_scale_factor"),
"expected 'binary_scale_factor' in error message, got: {msg}"
);
}
#[test]
fn test_adversarial_non_f64_simple_packing() {
let mut params = BTreeMap::new();
params.insert(
"sp_reference_value".to_string(),
ciborium::Value::Float(0.0),
);
params.insert(
"sp_binary_scale_factor".to_string(),
ciborium::Value::Integer(0.into()),
);
params.insert(
"sp_decimal_scale_factor".to_string(),
ciborium::Value::Integer(0.into()),
);
params.insert(
"sp_bits_per_value".to_string(),
ciborium::Value::Integer(16.into()),
);
let global = GlobalMetadata {
extra: BTreeMap::new(),
..Default::default()
};
let desc = DataObjectDescriptor {
obj_type: "ntensor".to_string(),
ndim: 1,
shape: vec![10],
strides: vec![1],
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "simple_packing".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params,
masks: None,
};
let data = vec![0u8; 10 * 4];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for simple_packing with Float32 but got Ok: {:?}",
result
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("simple_packing") || msg.contains("float64") || msg.contains("f64"),
"expected 'simple_packing' or 'float64' in error message, got: {msg}"
);
}
#[test]
fn test_adversarial_shuffle_element_size_zero() {
let (global, desc) = make_shuffle_pair(vec![10], 0);
let data = vec![0u8; 10 * 4];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for shuffle_element_size=0 but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_shuffle_misaligned() {
let float32_byte_width: usize = 4;
let num_elements: usize = 10;
let element_size_that_doesnt_divide_40: u64 = 3;
let (global, desc) = make_shuffle_pair(
vec![num_elements as u64],
element_size_that_doesnt_divide_40,
);
let data = vec![0u8; num_elements * float32_byte_width];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for misaligned shuffle (element_size=3 on 40-byte Float32 data) but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_decode_range_with_shuffle() {
let float32_element_size: u64 = 4;
let (global, desc) = make_shuffle_pair(vec![10], float32_element_size);
let data: Vec<u8> = (0u8..40).collect();
let encoded = encode(&global, &[(&desc, &data)], &EncodeOptions::default())
.expect("encode with shuffle must succeed for this test");
let result = decode_range(&encoded, 0, &[(0, 5)], &DecodeOptions::default());
assert!(
result.is_err(),
"expected Err for decode_range on shuffled payload but got Ok: {:?}",
result
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("shuffle") || msg.contains("filter"),
"expected 'shuffle' or 'filter' in error message, got: {msg}"
);
}
#[test]
fn test_adversarial_shape_product_overflow() {
let global = GlobalMetadata {
extra: BTreeMap::new(),
..Default::default()
};
let desc = DataObjectDescriptor {
obj_type: "ntensor".to_string(),
ndim: 2,
shape: vec![u64::MAX, 2],
strides: vec![2, 1],
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
masks: None,
};
let data = vec![0u8; 64];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for shape product overflow but got Ok: {:?}",
result
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("overflow"),
"expected 'overflow' in error message, got: {msg}"
);
}
#[test]
fn test_adversarial_empty_obj_type() {
let (global, mut desc) = make_simple_float32_pair(vec![4]);
desc.obj_type = String::new();
let data = vec![0u8; 4 * 4];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for empty obj_type but got Ok: {:?}",
result
);
}
#[test]
fn test_adversarial_ndim_mismatch() {
let global = GlobalMetadata {
extra: BTreeMap::new(),
..Default::default()
};
let desc = DataObjectDescriptor {
obj_type: "ntensor".to_string(),
ndim: 5,
shape: vec![4, 5],
strides: vec![5, 1],
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
masks: None,
};
let data = vec![0u8; 4 * 5 * 4];
let result = encode(&global, &[(&desc, &data)], &EncodeOptions::default());
assert!(
result.is_err(),
"expected Err for ndim/shape mismatch but got Ok: {:?}",
result
);
}
#[test]
fn postamble_total_length_mismatch_fails() {
let (global, desc) = make_simple_float32_pair(vec![2, 3]);
let payload = vec![0u8; 4 * 2 * 3];
let mut msg = encode(&global, &[(&desc, &payload)], &EncodeOptions::default()).unwrap();
let msg_len = msg.len() as u64;
let fake = msg_len - 1;
let slot_start = msg.len() - 16;
msg[slot_start..slot_start + 8].copy_from_slice(&fake.to_be_bytes());
let result = framing::decode_message(&msg);
assert!(
result.is_err(),
"expected postamble/preamble total_length mismatch to fail decode"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("postamble total_length") && err.contains("preamble total_length"),
"expected mismatch error, got: {err}"
);
}
#[test]
fn postamble_zero_total_length_accepted() {
let (global, desc) = make_simple_float32_pair(vec![2]);
let payload = vec![0u8; 4 * 2];
let mut msg = encode(&global, &[(&desc, &payload)], &EncodeOptions::default()).unwrap();
let slot_start = msg.len() - 16;
msg[slot_start..slot_start + 8].copy_from_slice(&0u64.to_be_bytes());
let decoded = framing::decode_message(&msg).unwrap();
assert_eq!(decoded.objects.len(), 1);
}
#[test]
fn postamble_is_24_bytes() {
let (global, desc) = make_simple_float32_pair(vec![1]);
let payload = vec![0u8; 4];
let msg = encode(&global, &[(&desc, &payload)], &EncodeOptions::default()).unwrap();
assert_eq!(&msg[msg.len() - 8..], b"39277777");
let pa_total = u64::from_be_bytes(msg[msg.len() - 16..msg.len() - 8].try_into().unwrap());
assert_eq!(pa_total, msg.len() as u64);
}
#[test]
fn frame_type_4_is_rejected() {
use tensogram::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let body = vec![0u8; 32];
let total_length = (FRAME_HEADER_SIZE + body.len() + FRAME_END.len()) as u64;
let mut frame = Vec::new();
frame.extend_from_slice(FRAME_MAGIC);
frame.extend_from_slice(&4u16.to_be_bytes()); frame.extend_from_slice(&1u16.to_be_bytes()); frame.extend_from_slice(&0u16.to_be_bytes()); frame.extend_from_slice(&total_length.to_be_bytes());
frame.extend_from_slice(&body);
frame.extend_from_slice(FRAME_END);
let err = tensogram::wire::FrameHeader::read_from(&frame).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("reserved frame type 4"),
"expected reserved-type-4 error, got: {msg}"
);
assert!(
msg.contains("obsolete v2"),
"expected 'obsolete v2' in the error, got: {msg}"
);
}
#[test]
fn sec001_undersized_total_length_is_rejected_not_panic() {
let crash_input: &[u8] = &[
84, 69, 78, 83, 79, 71, 82, 77, 0, 3, 0, 0, 82, 77, 0, 3, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 10,
];
let err = decode(crash_input, &DecodeOptions::default())
.expect_err("undersized total_length must be rejected");
assert!(
matches!(err, TensogramError::Framing(_)),
"expected a Framing error, got: {err:?}"
);
use tensogram::wire::{MAGIC, POSTAMBLE_SIZE, PREAMBLE_SIZE, WIRE_VERSION};
for bad_total in 1..(PREAMBLE_SIZE + POSTAMBLE_SIZE) as u64 {
let mut buf = Vec::with_capacity(PREAMBLE_SIZE + POSTAMBLE_SIZE);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&WIRE_VERSION.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&bad_total.to_be_bytes()); buf.resize(PREAMBLE_SIZE + POSTAMBLE_SIZE, 0);
let err = decode(&buf, &DecodeOptions::default())
.expect_err("total_length below minimum must be rejected");
assert!(
matches!(err, TensogramError::Framing(_)),
"total_length={bad_total} expected Framing error, got: {err:?}"
);
}
}
#[test]
fn sec002_scan_huge_total_length_does_not_overflow() {
use tensogram::wire::{MAGIC, WIRE_VERSION};
let mut buf = Vec::new();
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&WIRE_VERSION.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&u64::MAX.to_be_bytes()); buf.resize(64, 0); let found = scan(&buf);
assert!(
found.is_empty(),
"a preamble with total_length=u64::MAX must not yield a message"
);
for total in [
u64::MAX,
u64::MAX - 1,
u64::MAX - 7,
(usize::MAX as u64) - 3,
] {
let mut b = Vec::new();
b.extend_from_slice(MAGIC);
b.extend_from_slice(&WIRE_VERSION.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes());
b.extend_from_slice(&total.to_be_bytes());
b.resize(64, 0);
let _ = scan(&b); }
}
#[test]
fn sec004_inline_hash_walk_huge_frame_total_does_not_overflow() {
use tensogram::wire::{
FRAME_HEADER_SIZE, FRAME_MAGIC, MAGIC, POSTAMBLE_SIZE, PREAMBLE_SIZE, WIRE_VERSION,
};
let msg_total = (PREAMBLE_SIZE + FRAME_HEADER_SIZE + POSTAMBLE_SIZE) as u64;
let mut buf = Vec::new();
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&WIRE_VERSION.to_be_bytes());
buf.extend_from_slice(&((1u16) << 7).to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&msg_total.to_be_bytes());
buf.extend_from_slice(FRAME_MAGIC);
buf.extend_from_slice(&9u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&u64::MAX.to_be_bytes()); buf.resize(msg_total as usize, 0);
let _ = tensogram::data_object_inline_hashes(&buf);
}
#[test]
fn sec005_backward_scan_tiny_total_no_oob() {
let crash_input: &[u8] = &[
164, 164, 164, 164, 195, 195, 195, 195, 195, 195, 195, 195, 195, 0, 0, 0, 0, 84, 69, 78,
83, 79, 71, 82, 77, 0, 3, 0, 0, 237, 197, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42,
0, 0, 0, 0, 0, 71, 51, 57, 50, 55, 55, 55, 55, 55, 197, 0, 0, 0, 0, 0, 0, 0, 69, 78, 83,
79, 71, 197, 0, 3, 237, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2, 37, 0, 0, 0, 0, 0, 0, 0, 4, 51, 57, 50,
];
let _ = scan(crash_input);
use tensogram::wire::{END_MAGIC, POSTAMBLE_SIZE};
for tiny_total in [1u64, 2, 7, 8, 23, (POSTAMBLE_SIZE as u64) - 1] {
let mut b = vec![0u8; 128];
let n = b.len();
b[n - 8..].copy_from_slice(END_MAGIC);
b[n - 16..n - 8].copy_from_slice(&tiny_total.to_be_bytes());
let _ = scan(&b); }
}
#[test]
fn sec006_decode_metadata_huge_skip_frame_no_overflow() {
let crash_input: &[u8] = &[
84, 69, 78, 83, 79, 71, 82, 77, 0, 3, 82, 77, 0, 82, 77, 3, 71, 70, 70, 70, 139, 139, 139,
139, 139, 139, 139, 139, 70, 82, 0, 3, 70, 70, 70, 246, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255,
];
let _ = decode_metadata(crash_input); }
#[test]
fn sec007_scan_file_huge_and_tiny_total_no_overflow() {
use std::io::Cursor;
use tensogram::wire::{END_MAGIC, MAGIC, WIRE_VERSION};
let mut buf = Vec::new();
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&WIRE_VERSION.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&u64::MAX.to_be_bytes());
buf.resize(128, 0);
buf[120..].copy_from_slice(END_MAGIC); let mut cur = Cursor::new(buf);
let _ = tensogram::scan_file(&mut cur);
let mut buf2 = vec![0u8; 96];
let n = buf2.len();
buf2[..MAGIC.len()].copy_from_slice(MAGIC);
buf2[n - 8..].copy_from_slice(END_MAGIC);
buf2[n - 16..n - 8].copy_from_slice(&3u64.to_be_bytes()); let mut cur2 = Cursor::new(buf2);
let _ = tensogram::scan_file(&mut cur2); }
#[test]
fn sec008_cbor_recursion_bomb_is_rejected_not_stack_overflow() {
let depth = 10_000usize;
let mut bomb = vec![0x81u8; depth];
bomb.push(0x00); let err = tensogram::metadata::cbor_to_global_metadata(&bomb)
.expect_err("a 10k-deep CBOR nesting must be rejected, not overflow the stack");
assert!(
matches!(err, TensogramError::Metadata(_)),
"expected Metadata error, got: {err:?}"
);
}
#[test]
fn sec001_minimum_size_message_is_accepted() {
let (global, desc) = make_simple_float32_pair(vec![]);
let data = vec![0u8; 4];
let encoded = encode(&global, &[(&desc, &data)], &EncodeOptions::default()).unwrap();
let (_md, objs) = decode(&encoded, &DecodeOptions::default())
.expect("a legitimately-encoded minimal message must decode, not hit the min-size floor");
assert_eq!(objs.len(), 1, "expected exactly one decoded object");
use tensogram::wire::{MAGIC, POSTAMBLE_SIZE, PREAMBLE_SIZE, WIRE_VERSION};
assert!(
encoded.len() >= PREAMBLE_SIZE + POSTAMBLE_SIZE,
"encoded length {} must be at least the minimum message size {}",
encoded.len(),
PREAMBLE_SIZE + POSTAMBLE_SIZE
);
let min = (PREAMBLE_SIZE + POSTAMBLE_SIZE) as u64;
let mut buf = Vec::with_capacity(min as usize);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&WIRE_VERSION.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&min.to_be_bytes()); buf.resize(min as usize, 0); let res = decode(&buf, &DecodeOptions::default());
if let Err(TensogramError::Framing(msg)) = &res {
assert!(
!msg.contains("smaller than the minimum message size"),
"total_length exactly at the minimum must pass the size floor; \
got floor rejection: {msg}"
);
}
}
#[test]
fn sec006_decode_metadata_returns_valid_metadata() {
use ciborium::Value;
let (mut global, desc) = make_simple_float32_pair(vec![4]);
global.extra.insert(
"producer".to_string(),
Value::Text("sec006-pin".to_string()),
);
let data = vec![1u8; 4 * 4];
let encoded = encode(
&global,
&[(&desc, &data), (&desc, &data)],
&EncodeOptions::default(),
)
.unwrap();
let md = decode_metadata(&encoded)
.expect("decode_metadata must return the metadata of a valid multi-object message");
assert_eq!(
md.extra.get("producer"),
Some(&Value::Text("sec006-pin".to_string())),
"decode_metadata must recover the exact global metadata after \
correctly skipping the (header-sized, 8-byte-aligned) data frames"
);
}
#[test]
fn sec004_inline_hashes_match_on_valid_hashed_message() {
let (global, desc) = make_simple_float32_pair(vec![8]);
let data: Vec<u8> = (0..8 * 4).map(|i| i as u8).collect();
let encoded = encode(&global, &[(&desc, &data)], &EncodeOptions::default()).unwrap();
let hashes = tensogram::data_object_inline_hashes(&encoded)
.expect("inline-hash walk must succeed on a valid message");
assert_eq!(
hashes.len(),
1,
"expected exactly one data-object hash slot"
);
let recovered = hashes[0].expect("a normally-encoded object carries a non-zero inline hash");
let verify = DecodeOptions {
verify_hash: true,
..DecodeOptions::default()
};
decode(&encoded, &verify)
.expect("verify_hash decode must succeed, confirming the inline hash slot is well-located");
let other_f32: Vec<f32> = (0..8).map(|i| i as f32 * 1.5).collect();
let mut other = Vec::with_capacity(8 * 4);
for v in &other_f32 {
other.extend_from_slice(&v.to_be_bytes());
}
let encoded2 = encode(&global, &[(&desc, &other)], &EncodeOptions::default()).unwrap();
let recovered2 = tensogram::data_object_inline_hashes(&encoded2)
.expect("inline-hash walk must succeed")[0]
.expect("non-zero inline hash");
assert_ne!(
recovered, recovered2,
"distinct payloads must yield distinct inline hashes — proves the \
slot offset reads real frame bytes, not a fixed location"
);
}