mod fidelity;
mod integrity;
mod metadata;
mod structure;
pub mod types;
use std::path::Path;
use self::integrity::validate_integrity;
use self::metadata::validate_metadata;
use self::structure::validate_structure;
pub use self::types::*;
pub fn validate_message(buf: &[u8], options: &ValidateOptions) -> ValidationReport {
let mut issues = Vec::new();
let mut object_count = 0;
let mut hash_verified = false;
let effective_max = if options.checksum_only {
options.max_level.max(ValidationLevel::Integrity)
} else {
options.max_level
};
let report_structure = !options.checksum_only;
let check_canonical = options.check_canonical;
let run_metadata =
(effective_max >= ValidationLevel::Metadata && !options.checksum_only) || check_canonical;
let run_integrity = effective_max >= ValidationLevel::Integrity;
let run_fidelity = effective_max >= ValidationLevel::Fidelity && !options.checksum_only;
let mut structure_issues = Vec::new();
let walk = validate_structure(buf, &mut structure_issues);
if report_structure {
issues.append(&mut structure_issues);
} else {
issues.extend(
structure_issues
.into_iter()
.filter(|i| i.severity == IssueSeverity::Error),
);
}
if let Some(ref walk) = walk {
object_count = walk.data_objects.len();
let needs_objects = run_metadata || run_integrity || run_fidelity || check_canonical;
let mut objects: Vec<ObjectContext<'_>> = if needs_objects {
walk.data_objects
.iter()
.map(|(cbor_bytes, payload, frame_offset)| ObjectContext {
descriptor: None,
descriptor_failed: false,
cbor_bytes,
payload,
frame_offset: *frame_offset,
decode_state: DecodeState::NotDecoded,
})
.collect()
} else {
Vec::new()
};
if run_metadata {
validate_metadata(walk, &mut objects, &mut issues, check_canonical);
}
if run_integrity {
hash_verified = validate_integrity(
walk,
&mut objects,
&mut issues,
options.checksum_only,
run_fidelity,
);
}
if run_fidelity {
fidelity::validate_fidelity(&mut objects, &mut issues);
}
}
if issues.iter().any(|i| i.severity == IssueSeverity::Error) {
hash_verified = false;
}
ValidationReport {
issues,
object_count,
hash_verified,
}
}
pub fn validate_file(
path: &Path,
options: &ValidateOptions,
) -> std::io::Result<FileValidationReport> {
use std::io::{Read, Seek, SeekFrom};
let file_len = usize::try_from(std::fs::metadata(path)?.len()).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"file size does not fit into usize",
)
})?;
let mut file = std::fs::File::open(path)?;
let offsets = crate::framing::scan_file(&mut file)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let mut file_issues = Vec::new();
let mut messages = Vec::new();
let mut expected_pos: usize = 0;
for (offset, length) in &offsets {
if *offset > expected_pos {
file_issues.push(FileIssue {
byte_offset: expected_pos,
length: offset - expected_pos,
description: format!(
"{} unrecognized bytes at offset {}",
offset - expected_pos,
expected_pos
),
});
}
file.seek(SeekFrom::Start(*offset as u64))?;
let mut msg_buf = vec![0u8; *length];
file.read_exact(&mut msg_buf)?;
let report = validate_message(&msg_buf, options);
messages.push(report);
expected_pos = offset + length;
}
if expected_pos < file_len {
let trailing_len = file_len - expected_pos;
let desc = if messages.is_empty() {
format!("{trailing_len} bytes with no valid messages")
} else {
format!("{trailing_len} trailing bytes after last message at offset {expected_pos}")
};
file_issues.push(FileIssue {
byte_offset: expected_pos,
length: trailing_len,
description: desc,
});
}
Ok(FileValidationReport {
file_issues,
messages,
})
}
pub fn validate_buffer(buf: &[u8], options: &ValidateOptions) -> FileValidationReport {
let offsets = crate::framing::scan(buf);
let mut file_issues = Vec::new();
let mut messages = Vec::new();
let mut expected_pos: usize = 0;
for (offset, length) in &offsets {
if *offset > expected_pos {
file_issues.push(FileIssue {
byte_offset: expected_pos,
length: offset - expected_pos,
description: format!(
"{} unrecognized bytes at offset {}",
offset - expected_pos,
expected_pos
),
});
}
let msg_slice = &buf[*offset..*offset + *length];
let report = validate_message(msg_slice, options);
messages.push(report);
expected_pos = offset + length;
}
if expected_pos < buf.len() {
let trailing_len = buf.len() - expected_pos;
let desc = if messages.is_empty() {
format!("{trailing_len} bytes with no valid messages")
} else {
format!("{trailing_len} trailing bytes after last message at offset {expected_pos}")
};
file_issues.push(FileIssue {
byte_offset: expected_pos,
length: trailing_len,
description: desc,
});
}
FileValidationReport {
file_issues,
messages,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dtype;
use crate::encode::{EncodeOptions, encode};
use crate::types::{DataObjectDescriptor, GlobalMetadata};
use crate::wire::{FRAME_HEADER_SIZE, POSTAMBLE_SIZE, PREAMBLE_SIZE};
use std::collections::BTreeMap;
use tensogram_encodings::ByteOrder;
fn make_test_message() -> Vec<u8> {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data: Vec<u8> = vec![0u8; 32];
encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap()
}
fn make_multi_object_message() -> Vec<u8> {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data: Vec<u8> = vec![0u8; 16];
encode(
&meta,
&[(&desc, data.as_slice()), (&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap()
}
#[test]
fn valid_message_passes_all_levels() {
let msg = make_test_message();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(report.is_ok(), "issues: {:?}", report.issues);
assert_eq!(report.object_count, 1);
assert!(report.hash_verified);
}
#[test]
fn valid_multi_object_passes() {
let msg = make_multi_object_message();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(report.is_ok(), "issues: {:?}", report.issues);
assert_eq!(report.object_count, 2);
}
#[test]
fn empty_buffer_fails() {
let report = validate_message(&[], &ValidateOptions::default());
assert!(!report.is_ok());
assert_eq!(report.issues[0].code, IssueCode::BufferTooShort);
}
#[test]
fn wrong_magic_fails() {
let mut msg = make_test_message();
msg[0..8].copy_from_slice(b"WRONGMAG");
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
assert_eq!(report.issues[0].code, IssueCode::InvalidMagic);
}
#[test]
fn truncated_message_fails() {
let msg = make_test_message();
let truncated = &msg[..msg.len() / 2];
let report = validate_message(truncated, &ValidateOptions::default());
assert!(!report.is_ok());
}
#[test]
fn bad_total_length_fails() {
let mut msg = make_test_message();
let bad_len: u64 = (msg.len() * 10) as u64;
msg[16..24].copy_from_slice(&bad_len.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
assert_eq!(report.issues[0].code, IssueCode::TotalLengthExceedsBuffer);
}
#[test]
fn corrupted_postamble_fails() {
let mut msg = make_test_message();
let end = msg.len();
msg[end - 8..end].copy_from_slice(b"BADMAGIC");
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
}
#[test]
fn tiny_total_length_does_not_panic() {
let mut buf = vec![0u8; 40];
buf[0..8].copy_from_slice(b"TENSOGRM");
buf[8..10].copy_from_slice(&2u16.to_be_bytes());
buf[16..24].copy_from_slice(&10u64.to_be_bytes());
let report = validate_message(&buf, &ValidateOptions::default());
assert!(!report.is_ok());
assert_eq!(report.issues[0].code, IssueCode::TotalLengthTooSmall);
}
#[test]
fn corrupted_metadata_cbor_fails_level2() {
let mut msg = make_test_message();
let cbor_start = PREAMBLE_SIZE + FRAME_HEADER_SIZE;
if cbor_start + 4 < msg.len() {
msg[cbor_start] = 0xFF;
msg[cbor_start + 1] = 0xFF;
msg[cbor_start + 2] = 0xFF;
msg[cbor_start + 3] = 0xFF;
}
let report = validate_message(&msg, &ValidateOptions::default());
let has_meta_error = report
.issues
.iter()
.any(|i| i.level == ValidationLevel::Metadata);
assert!(
has_meta_error,
"expected metadata error, got: {:?}",
report.issues
);
}
#[test]
fn corrupted_byte_detected() {
let mut msg = make_test_message();
let target = PREAMBLE_SIZE + FRAME_HEADER_SIZE + 20;
if target < msg.len() {
msg[target] ^= 0xFF;
}
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
!report.is_ok(),
"expected error after corruption, got: {:?}",
report.issues
);
}
#[test]
fn hash_mismatch_on_corrupted_payload() {
use crate::wire::POSTAMBLE_SIZE;
let msg = make_test_message();
let pa_start = msg.len() - POSTAMBLE_SIZE;
let target = pa_start * 7 / 10;
let mut corrupted = msg.clone();
corrupted[target] ^= 0xFF;
let report = validate_message(&corrupted, &ValidateOptions::default());
let has_hash_or_integrity = report.issues.iter().any(|i| {
matches!(
i.code,
IssueCode::HashMismatch | IssueCode::DecodePipelineFailed
)
});
assert!(
!report.is_ok(),
"corrupted payload should fail validation, got: {:?}",
report.issues
);
let _ = has_hash_or_integrity; }
#[test]
fn quick_mode_skips_metadata_and_integrity() {
let msg = make_test_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Structure,
..ValidateOptions::default()
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok());
assert!(!report.hash_verified);
}
#[test]
fn checksum_mode_verifies_hash() {
let msg = make_test_message();
let opts = ValidateOptions {
checksum_only: true,
..ValidateOptions::default()
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok());
assert!(report.hash_verified);
}
#[test]
fn checksum_mode_on_broken_message_fails() {
let mut msg = make_test_message();
let end = msg.len();
msg[end - 8..end].copy_from_slice(b"BADMAGIC");
let opts = ValidateOptions {
checksum_only: true,
..ValidateOptions::default()
};
let report = validate_message(&msg, &opts);
assert!(
!report.is_ok(),
"broken message should fail even in checksum mode"
);
}
#[test]
fn checksum_mode_catches_structural_errors() {
let mut msg = make_test_message();
let actual_len = msg.len() as u64;
let bad_len = actual_len + 100;
msg[16..24].copy_from_slice(&bad_len.to_be_bytes());
let opts = ValidateOptions {
checksum_only: true,
..ValidateOptions::default()
};
let report = validate_message(&msg, &opts);
let has_error = report
.issues
.iter()
.any(|i| i.severity == IssueSeverity::Error);
assert!(
has_error,
"checksum mode should surface structural errors, got: {:?}",
report.issues
);
}
#[test]
fn canonical_mode_on_valid_message() {
let msg = make_test_message();
let opts = ValidateOptions {
check_canonical: true,
..ValidateOptions::default()
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok(), "issues: {:?}", report.issues);
}
#[test]
fn validate_buffer_single_message() {
let msg = make_test_message();
let report = validate_buffer(&msg, &ValidateOptions::default());
assert!(report.is_ok());
assert_eq!(report.messages.len(), 1);
assert!(report.file_issues.is_empty());
}
#[test]
fn validate_buffer_two_messages() {
let msg = make_test_message();
let mut buf = msg.clone();
buf.extend_from_slice(&msg);
let report = validate_buffer(&buf, &ValidateOptions::default());
assert!(report.is_ok());
assert_eq!(report.messages.len(), 2);
}
#[test]
fn validate_buffer_trailing_garbage() {
let mut buf = make_test_message();
buf.extend_from_slice(b"GARBAGE_TRAILING_DATA");
let report = validate_buffer(&buf, &ValidateOptions::default());
assert!(!report.file_issues.is_empty());
}
#[test]
fn validate_buffer_garbage_between_messages() {
let msg = make_test_message();
let mut buf = msg.clone();
buf.extend_from_slice(b"GARBAGE");
buf.extend_from_slice(&msg);
let report = validate_buffer(&buf, &ValidateOptions::default());
assert!(!report.file_issues.is_empty());
assert_eq!(report.messages.len(), 2);
}
#[test]
fn validate_buffer_truncated_second_message() {
let msg = make_test_message();
let mut buf = msg.clone();
buf.extend_from_slice(&msg[..msg.len() / 2]);
let report = validate_buffer(&buf, &ValidateOptions::default());
assert!(!report.messages.is_empty());
let has_issue =
!report.file_issues.is_empty() || report.messages.iter().any(|r| !r.is_ok());
assert!(
has_issue,
"truncated second message should produce an issue"
);
}
#[test]
fn streaming_message_validates() {
use crate::streaming::StreamingEncoder;
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 32];
let mut buf = Vec::new();
let mut enc = StreamingEncoder::new(&mut buf, &meta, &EncodeOptions::default()).unwrap();
enc.write_object(&desc, &data).unwrap();
enc.finish().unwrap();
let report = validate_message(&buf, &ValidateOptions::default());
assert!(
report.is_ok(),
"streaming message should validate: {:?}",
report.issues
);
assert_eq!(report.object_count, 1);
assert!(report.hash_verified);
}
#[test]
fn streaming_message_footer_metadata_validates() {
use crate::streaming::StreamingEncoder;
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![4],
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 8];
let mut buf = Vec::new();
let mut enc = StreamingEncoder::new(&mut buf, &meta, &EncodeOptions::default()).unwrap();
enc.write_object(&desc, &data).unwrap();
enc.write_object(&desc, &data).unwrap();
enc.finish().unwrap();
let report = validate_message(&buf, &ValidateOptions::default());
assert!(report.is_ok(), "issues: {:?}", report.issues);
assert_eq!(report.object_count, 2);
}
#[test]
fn hash_verified_requires_all_objects() {
let msg = make_test_message();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(report.hash_verified);
}
#[test]
fn hash_not_verified_without_hash() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 32];
let opts = EncodeOptions {
hash_algorithm: None,
..EncodeOptions::default()
};
let msg = encode(&meta, &[(&desc, data.as_slice())], &opts).unwrap();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.hash_verified);
}
#[test]
fn issue_codes_are_stable_strings() {
let code = IssueCode::HashMismatch;
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, r#""hash_mismatch""#);
}
#[test]
fn report_serializes_to_json() {
let msg = make_test_message();
let report = validate_message(&msg, &ValidateOptions::default());
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("\"object_count\":1"));
assert!(json.contains("\"hash_verified\":true"));
}
#[test]
fn validate_buffer_garbage_only() {
let buf = b"this is not a tensogram file at all";
let report = validate_buffer(buf, &ValidateOptions::default());
assert!(report.messages.is_empty());
assert!(!report.file_issues.is_empty());
assert!(
report.file_issues[0]
.description
.contains("no valid messages"),
"got: {}",
report.file_issues[0].description,
);
}
#[test]
fn validate_buffer_empty() {
let report = validate_buffer(&[], &ValidateOptions::default());
assert!(report.messages.is_empty());
assert!(report.file_issues.is_empty());
assert!(report.is_ok());
}
#[test]
fn streaming_ffo_out_of_range_reported() {
use crate::streaming::StreamingEncoder;
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 16];
let mut buf = Vec::new();
let mut enc = StreamingEncoder::new(&mut buf, &meta, &EncodeOptions::default()).unwrap();
enc.write_object(&desc, &data).unwrap();
enc.finish().unwrap();
let pa_start = buf.len() - 16;
let bad_ffo: u64 = 0; buf[pa_start..pa_start + 8].copy_from_slice(&bad_ffo.to_be_bytes());
let report = validate_message(&buf, &ValidateOptions::default());
let has_ffo_error = report
.issues
.iter()
.any(|i| i.code == IssueCode::FooterOffsetOutOfRange);
assert!(
has_ffo_error,
"expected FooterOffsetOutOfRange, got: {:?}",
report.issues
);
}
#[test]
fn zero_object_message_validates() {
let meta = GlobalMetadata::default();
let opts = EncodeOptions {
hash_algorithm: None,
..EncodeOptions::default()
};
let msg = encode(&meta, &[], &opts).unwrap();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(report.is_ok(), "issues: {:?}", report.issues);
assert_eq!(report.object_count, 0);
assert!(!report.hash_verified); }
fn full_opts() -> ValidateOptions {
ValidateOptions {
max_level: ValidationLevel::Fidelity,
..ValidateOptions::default()
}
}
fn make_float64_message(values: &[f64]) -> Vec<u8> {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![values.len() as u64],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data: Vec<u8> = values.iter().flat_map(|v| v.to_be_bytes()).collect();
encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap()
}
fn make_float32_message_le(values: &[f32]) -> Vec<u8> {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![values.len() as u64],
strides: vec![4],
dtype: Dtype::Float32,
byte_order: ByteOrder::Little,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap()
}
#[test]
fn full_mode_valid_float64_passes() {
let msg = make_float64_message(&[1.0, 2.0, 3.0, 4.0]);
let report = validate_message(&msg, &full_opts());
assert!(report.is_ok(), "issues: {:?}", report.issues);
}
#[test]
fn full_mode_nan_float64_detected() {
let msg = make_float64_message(&[1.0, f64::NAN, 3.0]);
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
let nan_issue = report
.issues
.iter()
.find(|i| i.code == IssueCode::NanDetected);
assert!(
nan_issue.is_some(),
"expected NanDetected, got: {:?}",
report.issues
);
assert!(nan_issue.unwrap().description.contains("element 1"));
}
#[test]
fn full_mode_inf_float64_detected() {
let msg = make_float64_message(&[1.0, 2.0, f64::INFINITY]);
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
let inf_issue = report
.issues
.iter()
.find(|i| i.code == IssueCode::InfDetected);
assert!(
inf_issue.is_some(),
"expected InfDetected, got: {:?}",
report.issues
);
assert!(inf_issue.unwrap().description.contains("element 2"));
}
#[test]
fn full_mode_neg_inf_detected() {
let msg = make_float64_message(&[f64::NEG_INFINITY, 1.0]);
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected)
);
}
#[test]
fn full_mode_float32_le_nan_detected() {
let msg = make_float32_message_le(&[1.0, f32::NAN, 3.0]);
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected)
);
}
#[test]
fn full_mode_float32_le_inf_detected() {
let msg = make_float32_message_le(&[f32::INFINITY]);
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected)
);
}
#[test]
fn full_mode_integer_passes() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![4],
dtype: Dtype::Int32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 16]; let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(
report.is_ok(),
"integer should pass fidelity: {:?}",
report.issues
);
}
#[test]
fn full_mode_hash_verified_false_on_nan() {
let msg = make_float64_message(&[f64::NAN]);
let report = validate_message(&msg, &full_opts());
assert!(
!report.hash_verified,
"hash_verified should be false when NaN detected"
);
}
#[test]
fn full_mode_float16_nan() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![2],
dtype: Dtype::Float16,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let mut data = vec![0u8; 4]; data[0..2].copy_from_slice(&0x0000u16.to_be_bytes()); data[2..4].copy_from_slice(&0x7C01u16.to_be_bytes()); let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
let nan = report
.issues
.iter()
.find(|i| i.code == IssueCode::NanDetected);
assert!(
nan.is_some(),
"expected float16 NaN, got: {:?}",
report.issues
);
assert!(nan.unwrap().description.contains("element 1"));
}
#[test]
fn full_mode_float16_inf() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![1],
strides: vec![2],
dtype: Dtype::Float16,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = 0x7C00u16.to_be_bytes().to_vec();
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected)
);
}
#[test]
fn full_mode_bfloat16_nan() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![1],
strides: vec![2],
dtype: Dtype::Bfloat16,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = 0x7F81u16.to_be_bytes().to_vec();
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected)
);
}
#[test]
fn full_mode_complex64_real_nan() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![1],
strides: vec![8],
dtype: Dtype::Complex64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let mut data = Vec::new();
data.extend_from_slice(&f32::NAN.to_be_bytes());
data.extend_from_slice(&0.0f32.to_be_bytes());
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
let nan = report
.issues
.iter()
.find(|i| i.code == IssueCode::NanDetected);
assert!(nan.is_some());
assert!(nan.unwrap().description.contains("real component"));
}
#[test]
fn full_mode_complex128_imag_inf() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![1],
strides: vec![16],
dtype: Dtype::Complex128,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let mut data = Vec::new();
data.extend_from_slice(&1.0f64.to_be_bytes());
data.extend_from_slice(&f64::INFINITY.to_be_bytes());
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(!report.is_ok());
let inf = report
.issues
.iter()
.find(|i| i.code == IssueCode::InfDetected);
assert!(inf.is_some());
assert!(inf.unwrap().description.contains("imaginary component"));
}
#[test]
fn full_mode_with_canonical() {
let msg = make_test_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Fidelity,
check_canonical: true,
..ValidateOptions::default()
};
let report = validate_message(&msg, &opts);
assert!(
report.is_ok(),
"full+canonical should pass: {:?}",
report.issues
);
}
#[test]
fn full_mode_json_serialization() {
let msg = make_float64_message(&[f64::NAN]);
let report = validate_message(&msg, &full_opts());
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("\"nan_detected\""));
assert!(json.contains("\"fidelity\""));
}
#[test]
fn default_mode_skips_fidelity() {
let msg = make_float64_message(&[f64::NAN]);
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
!report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected),
"default mode should not run fidelity: {:?}",
report.issues
);
}
#[test]
fn full_mode_negative_zero_passes() {
let msg = make_float64_message(&[-0.0, 0.0, 1.0]);
let report = validate_message(&msg, &full_opts());
assert!(
report.is_ok(),
"negative zero should pass: {:?}",
report.issues
);
}
#[test]
fn full_mode_subnormal_passes() {
let msg = make_float64_message(&[5e-324, 1.0]);
let report = validate_message(&msg, &full_opts());
assert!(
report.is_ok(),
"subnormals should pass: {:?}",
report.issues
);
}
#[test]
fn full_mode_zero_length_array() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![0],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data: Vec<u8> = vec![];
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(
report.is_ok(),
"zero-length array should pass: {:?}",
report.issues
);
}
#[test]
fn full_mode_decoded_size_mismatch() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![3],
strides: vec![4],
dtype: Dtype::Float32,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 12]; let opts = EncodeOptions {
hash_algorithm: None,
..EncodeOptions::default()
};
let mut msg = encode(&meta, &[(&desc, data.as_slice())], &opts).unwrap();
let mut patched = false;
for i in (0..msg.len() - 1).rev() {
if msg[i] == 0x81 && msg[i + 1] == 0x03 {
msg[i + 1] = 0x04; patched = true;
break;
}
}
assert!(patched, "could not find shape [3] in encoded message");
let report = validate_message(&msg, &full_opts());
let has_mismatch = report
.issues
.iter()
.any(|i| i.code == IssueCode::DecodedSizeMismatch);
assert!(
has_mismatch,
"expected DecodedSizeMismatch, got: {:?}",
report.issues
);
}
#[test]
fn quick_canonical_runs_metadata() {
let msg = make_test_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Structure,
check_canonical: true,
checksum_only: false,
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok(), "issues: {:?}", report.issues);
}
fn build_raw_message(
flags: u16,
frames: &[Vec<u8>], total_length_override: Option<u64>,
streaming: bool,
) -> Vec<u8> {
use crate::wire::{END_MAGIC, MAGIC};
let mut out = Vec::new();
out.extend_from_slice(MAGIC);
out.extend_from_slice(&2u16.to_be_bytes()); out.extend_from_slice(&flags.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u64.to_be_bytes());
for frame in frames {
out.extend_from_slice(frame);
let pad = (8 - (out.len() % 8)) % 8;
out.extend(std::iter::repeat_n(0u8, pad));
}
let ffo = out.len() as u64;
out.extend_from_slice(&ffo.to_be_bytes());
out.extend_from_slice(END_MAGIC);
let total = if streaming { 0u64 } else { out.len() as u64 };
let tl = total_length_override.unwrap_or(total);
out[16..24].copy_from_slice(&tl.to_be_bytes());
out
}
fn build_metadata_frame() -> Vec<u8> {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta = GlobalMetadata::default();
let cbor = crate::metadata::global_metadata_to_cbor(&meta).unwrap();
let total_length = (FRAME_HEADER_SIZE + cbor.len() + FRAME_END.len()) as u64;
let mut frame = Vec::new();
frame.extend_from_slice(FRAME_MAGIC);
frame.extend_from_slice(&1u16.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(&cbor);
frame.extend_from_slice(FRAME_END);
frame
}
fn build_data_object_frame(desc: &DataObjectDescriptor, payload: &[u8]) -> Vec<u8> {
crate::framing::encode_data_object_frame(desc, payload, false).unwrap()
}
fn default_desc() -> DataObjectDescriptor {
DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
}
}
#[test]
fn structure_frame_length_overflow() {
let mut msg = make_test_message();
let frame_start = PREAMBLE_SIZE;
let tl_offset = frame_start + 8;
msg[tl_offset..tl_offset + 8].copy_from_slice(&u64::MAX.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
let has_overflow = report
.issues
.iter()
.any(|i| i.code == IssueCode::FrameLengthOverflow);
assert!(
has_overflow,
"expected FrameLengthOverflow, got: {:?}",
report.issues
);
}
#[test]
fn structure_non_zero_padding_between_frames() {
let mut msg = make_test_message();
let frame_start = PREAMBLE_SIZE;
let tl =
u64::from_be_bytes(msg[frame_start + 8..frame_start + 16].try_into().unwrap()) as usize;
let frame_end = frame_start + tl;
let next_aligned = (frame_end + 7) & !7;
if next_aligned > frame_end && next_aligned < msg.len() {
for b in &mut msg[frame_end..next_aligned] {
*b = 0xAA;
}
let report = validate_message(&msg, &ValidateOptions::default());
let has_padding_warn = report
.issues
.iter()
.any(|i| i.code == IssueCode::NonZeroPadding);
assert!(
has_padding_warn,
"expected NonZeroPadding warning, got: {:?}",
report.issues
);
}
else {
}
}
#[test]
fn structure_frame_order_violation() {
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let meta_frame = build_metadata_frame();
let flags = 1u16; let msg = build_raw_message(flags, &[data_frame, meta_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_order = report
.issues
.iter()
.any(|i| i.code == IssueCode::FrameOrderViolation);
assert!(
has_order,
"expected FrameOrderViolation, got: {:?}",
report.issues
);
}
#[test]
fn structure_preceder_not_followed_by_object() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta = GlobalMetadata {
base: vec![BTreeMap::new()],
..GlobalMetadata::default()
};
let cbor = crate::metadata::global_metadata_to_cbor(&meta).unwrap();
let total_length = (FRAME_HEADER_SIZE + cbor.len() + FRAME_END.len()) as u64;
let mut preceder_frame = Vec::new();
preceder_frame.extend_from_slice(FRAME_MAGIC);
preceder_frame.extend_from_slice(&8u16.to_be_bytes()); preceder_frame.extend_from_slice(&1u16.to_be_bytes()); preceder_frame.extend_from_slice(&0u16.to_be_bytes()); preceder_frame.extend_from_slice(&total_length.to_be_bytes());
preceder_frame.extend_from_slice(&cbor);
preceder_frame.extend_from_slice(FRAME_END);
let header_meta_frame = build_metadata_frame();
let mut footer_meta_frame = Vec::new();
let footer_cbor =
crate::metadata::global_metadata_to_cbor(&GlobalMetadata::default()).unwrap();
let ftl = (FRAME_HEADER_SIZE + footer_cbor.len() + FRAME_END.len()) as u64;
footer_meta_frame.extend_from_slice(FRAME_MAGIC);
footer_meta_frame.extend_from_slice(&7u16.to_be_bytes()); footer_meta_frame.extend_from_slice(&1u16.to_be_bytes());
footer_meta_frame.extend_from_slice(&0u16.to_be_bytes());
footer_meta_frame.extend_from_slice(&ftl.to_be_bytes());
footer_meta_frame.extend_from_slice(&footer_cbor);
footer_meta_frame.extend_from_slice(FRAME_END);
let flags = (1u16) | (1u16 << 1) | (1u16 << 6); let msg = build_raw_message(
flags,
&[header_meta_frame, preceder_frame, footer_meta_frame],
None,
false,
);
let report = validate_message(&msg, &ValidateOptions::default());
let has_preceder_err = report
.issues
.iter()
.any(|i| i.code == IssueCode::PrecederNotFollowedByObject);
assert!(
has_preceder_err,
"expected PrecederNotFollowedByObject, got: {:?}",
report.issues
);
}
#[test]
fn structure_dangling_preceder() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta = GlobalMetadata {
base: vec![BTreeMap::new()],
..GlobalMetadata::default()
};
let cbor = crate::metadata::global_metadata_to_cbor(&meta).unwrap();
let total_length = (FRAME_HEADER_SIZE + cbor.len() + FRAME_END.len()) as u64;
let mut preceder_frame = Vec::new();
preceder_frame.extend_from_slice(FRAME_MAGIC);
preceder_frame.extend_from_slice(&8u16.to_be_bytes());
preceder_frame.extend_from_slice(&1u16.to_be_bytes());
preceder_frame.extend_from_slice(&0u16.to_be_bytes());
preceder_frame.extend_from_slice(&total_length.to_be_bytes());
preceder_frame.extend_from_slice(&cbor);
preceder_frame.extend_from_slice(FRAME_END);
let header_meta_frame = build_metadata_frame();
let flags = 1u16 | (1u16 << 6); let msg = build_raw_message(flags, &[header_meta_frame, preceder_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_dangling = report
.issues
.iter()
.any(|i| i.code == IssueCode::DanglingPreceder);
assert!(
has_dangling,
"expected DanglingPreceder, got: {:?}",
report.issues
);
}
#[test]
fn structure_cbor_before_boundary_unknown() {
use crate::wire::{DATA_OBJECT_FOOTER_SIZE, FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let payload = vec![0u8; 16];
let bad_cbor = vec![0xFF, 0xFF, 0xFF, 0xFF];
let cbor_offset = FRAME_HEADER_SIZE as u64; let body_len = bad_cbor.len() + payload.len() + DATA_OBJECT_FOOTER_SIZE;
let total_length = (FRAME_HEADER_SIZE + body_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(&bad_cbor);
frame.extend_from_slice(&payload);
frame.extend_from_slice(&cbor_offset.to_be_bytes());
frame.extend_from_slice(FRAME_END);
let meta_frame = build_metadata_frame();
let flags = 1u16; let msg = build_raw_message(flags, &[meta_frame, frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_cbor_err = report
.issues
.iter()
.any(|i| i.code == IssueCode::CborBeforeBoundaryUnknown);
assert!(
has_cbor_err,
"expected CborBeforeBoundaryUnknown, got: {:?}",
report.issues
);
}
#[test]
fn structure_flag_mismatch() {
let mut msg = make_test_message();
let current_flags = u16::from_be_bytes(msg[10..12].try_into().unwrap());
let bad_flags = current_flags | (1u16 << 1); msg[10..12].copy_from_slice(&bad_flags.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
let has_flag_mismatch = report
.issues
.iter()
.any(|i| i.code == IssueCode::FlagMismatch);
assert!(
has_flag_mismatch,
"expected FlagMismatch, got: {:?}",
report.issues
);
}
#[test]
fn structure_no_metadata_frame() {
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let msg = build_raw_message(0u16, &[data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_no_meta = report
.issues
.iter()
.any(|i| i.code == IssueCode::NoMetadataFrame);
assert!(
has_no_meta,
"expected NoMetadataFrame, got: {:?}",
report.issues
);
}
#[test]
fn structure_streaming_mode_validates() {
let meta_frame = build_metadata_frame();
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let flags = 1u16; let msg = build_raw_message(flags, &[meta_frame, data_frame], None, true);
let report = validate_message(&msg, &ValidateOptions::default());
let has_fatal = report.issues.iter().any(|i| {
i.severity == IssueSeverity::Error
&& !matches!(
i.code,
IssueCode::FlagMismatch
| IssueCode::FooterOffsetMismatch
| IssueCode::NoMetadataFrame
)
});
assert!(
!has_fatal,
"unexpected fatal error in streaming mode: {:?}",
report.issues
);
}
#[test]
fn structure_streaming_mode_bad_postamble() {
let meta_frame = build_metadata_frame();
let flags = 1u16;
let mut msg = build_raw_message(flags, &[meta_frame], None, true);
let end = msg.len();
msg[end - 8..end].copy_from_slice(b"BADMAGIC");
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
let has_postamble = report
.issues
.iter()
.any(|i| i.code == IssueCode::PostambleInvalid);
assert!(
has_postamble,
"expected PostambleInvalid in streaming mode, got: {:?}",
report.issues
);
}
fn make_message_with_patched_descriptor(patch: impl FnOnce(&mut ciborium::Value)) -> Vec<u8> {
let meta = GlobalMetadata::default();
let desc = default_desc();
let data = vec![0u8; 32];
let opts = EncodeOptions {
hash_algorithm: None,
..EncodeOptions::default()
};
let _msg = encode(&meta, &[(&desc, data.as_slice())], &opts).unwrap();
let cbor_bytes = crate::metadata::object_descriptor_to_cbor(&desc).unwrap();
let mut value: ciborium::Value = ciborium::from_reader(cbor_bytes.as_slice()).unwrap();
patch(&mut value);
let mut patched_cbor = Vec::new();
ciborium::into_writer(&value, &mut patched_cbor).unwrap();
use crate::wire::{
DATA_OBJECT_FOOTER_SIZE, DataObjectFlags, FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC,
};
let payload = vec![0u8; 32];
let cbor_offset = (FRAME_HEADER_SIZE + payload.len()) as u64;
let total_length =
(FRAME_HEADER_SIZE + payload.len() + patched_cbor.len() + DATA_OBJECT_FOOTER_SIZE)
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(&DataObjectFlags::CBOR_AFTER_PAYLOAD.to_be_bytes()); frame.extend_from_slice(&total_length.to_be_bytes());
frame.extend_from_slice(&payload);
frame.extend_from_slice(&patched_cbor);
frame.extend_from_slice(&cbor_offset.to_be_bytes());
frame.extend_from_slice(FRAME_END);
let meta_frame = build_metadata_frame();
let flags = 1u16; build_raw_message(flags, &[meta_frame, frame], None, false)
}
fn cbor_map_set(value: &mut ciborium::Value, key: &str, new_val: ciborium::Value) {
if let ciborium::Value::Map(pairs) = value {
for (k, v) in pairs.iter_mut() {
if let ciborium::Value::Text(s) = k
&& s == key
{
*v = new_val;
return;
}
}
pairs.push((ciborium::Value::Text(key.to_string()), new_val));
}
}
#[test]
fn metadata_index_count_mismatch() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let meta_frame = build_metadata_frame();
let idx = crate::types::IndexFrame {
object_count: 5,
offsets: vec![0, 100, 200, 300, 400],
lengths: vec![50, 50, 50, 50, 50],
};
let idx_cbor = crate::metadata::index_to_cbor(&idx).unwrap();
let idx_total = (FRAME_HEADER_SIZE + idx_cbor.len() + FRAME_END.len()) as u64;
let mut idx_frame = Vec::new();
idx_frame.extend_from_slice(FRAME_MAGIC);
idx_frame.extend_from_slice(&2u16.to_be_bytes()); idx_frame.extend_from_slice(&1u16.to_be_bytes());
idx_frame.extend_from_slice(&0u16.to_be_bytes());
idx_frame.extend_from_slice(&idx_total.to_be_bytes());
idx_frame.extend_from_slice(&idx_cbor);
idx_frame.extend_from_slice(FRAME_END);
let flags = 1u16 | (1u16 << 2); let msg = build_raw_message(flags, &[meta_frame, idx_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_idx_mismatch = report
.issues
.iter()
.any(|i| i.code == IssueCode::IndexCountMismatch);
assert!(
has_idx_mismatch,
"expected IndexCountMismatch, got: {:?}",
report.issues
);
}
#[test]
fn metadata_index_offset_mismatch() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let meta_frame = build_metadata_frame();
let idx = crate::types::IndexFrame {
object_count: 1,
offsets: vec![9999], lengths: vec![50],
};
let idx_cbor = crate::metadata::index_to_cbor(&idx).unwrap();
let idx_total = (FRAME_HEADER_SIZE + idx_cbor.len() + FRAME_END.len()) as u64;
let mut idx_frame = Vec::new();
idx_frame.extend_from_slice(FRAME_MAGIC);
idx_frame.extend_from_slice(&2u16.to_be_bytes()); idx_frame.extend_from_slice(&1u16.to_be_bytes());
idx_frame.extend_from_slice(&0u16.to_be_bytes());
idx_frame.extend_from_slice(&idx_total.to_be_bytes());
idx_frame.extend_from_slice(&idx_cbor);
idx_frame.extend_from_slice(FRAME_END);
let flags = 1u16 | (1u16 << 2); let msg = build_raw_message(flags, &[meta_frame, idx_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_offset_mismatch = report
.issues
.iter()
.any(|i| i.code == IssueCode::IndexOffsetMismatch);
assert!(
has_offset_mismatch,
"expected IndexOffsetMismatch, got: {:?}",
report.issues
);
}
#[test]
fn metadata_unknown_encoding() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(
v,
"encoding",
ciborium::Value::Text("turbo_zip".to_string()),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_unk = report
.issues
.iter()
.any(|i| i.code == IssueCode::UnknownEncoding);
assert!(
has_unk,
"expected UnknownEncoding, got: {:?}",
report.issues
);
}
#[test]
fn metadata_unknown_filter() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(
v,
"filter",
ciborium::Value::Text("mega_filter".to_string()),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_unk = report
.issues
.iter()
.any(|i| i.code == IssueCode::UnknownFilter);
assert!(has_unk, "expected UnknownFilter, got: {:?}", report.issues);
}
#[test]
fn metadata_unknown_compression() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(
v,
"compression",
ciborium::Value::Text("snappy9000".to_string()),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_unk = report
.issues
.iter()
.any(|i| i.code == IssueCode::UnknownCompression);
assert!(
has_unk,
"expected UnknownCompression, got: {:?}",
report.issues
);
}
#[test]
fn metadata_empty_obj_type() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "type", ciborium::Value::Text(String::new()));
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_empty = report
.issues
.iter()
.any(|i| i.code == IssueCode::EmptyObjType);
assert!(has_empty, "expected EmptyObjType, got: {:?}", report.issues);
}
#[test]
fn metadata_ndim_shape_mismatch() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "ndim", ciborium::Value::Integer(3.into()));
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_ndim = report
.issues
.iter()
.any(|i| i.code == IssueCode::NdimShapeMismatch);
assert!(
has_ndim,
"expected NdimShapeMismatch, got: {:?}",
report.issues
);
}
#[test]
fn metadata_strides_shape_mismatch() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(
v,
"strides",
ciborium::Value::Array(vec![
ciborium::Value::Integer(8.into()),
ciborium::Value::Integer(4.into()),
]),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_strides = report
.issues
.iter()
.any(|i| i.code == IssueCode::StridesShapeMismatch);
assert!(
has_strides,
"expected StridesShapeMismatch, got: {:?}",
report.issues
);
}
#[test]
fn metadata_shape_overflow() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "ndim", ciborium::Value::Integer(2.into()));
cbor_map_set(
v,
"shape",
ciborium::Value::Array(vec![
ciborium::Value::Integer(u64::MAX.into()),
ciborium::Value::Integer(2.into()),
]),
);
cbor_map_set(
v,
"strides",
ciborium::Value::Array(vec![
ciborium::Value::Integer(8.into()),
ciborium::Value::Integer(8.into()),
]),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_overflow = report
.issues
.iter()
.any(|i| i.code == IssueCode::ShapeOverflow);
assert!(
has_overflow,
"expected ShapeOverflow, got: {:?}",
report.issues
);
}
#[test]
fn metadata_reserved_not_a_map() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let mut base_entry = BTreeMap::new();
base_entry.insert(
"_reserved_".to_string(),
ciborium::Value::Text("not_a_map".to_string()),
);
let meta = GlobalMetadata {
base: vec![base_entry],
..GlobalMetadata::default()
};
let meta_cbor = crate::metadata::global_metadata_to_cbor(&meta).unwrap();
let total_length = (FRAME_HEADER_SIZE + meta_cbor.len() + FRAME_END.len()) as u64;
let mut meta_frame = Vec::new();
meta_frame.extend_from_slice(FRAME_MAGIC);
meta_frame.extend_from_slice(&1u16.to_be_bytes()); meta_frame.extend_from_slice(&1u16.to_be_bytes());
meta_frame.extend_from_slice(&0u16.to_be_bytes());
meta_frame.extend_from_slice(&total_length.to_be_bytes());
meta_frame.extend_from_slice(&meta_cbor);
meta_frame.extend_from_slice(FRAME_END);
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let flags = 1u16; let msg = build_raw_message(flags, &[meta_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_reserved_err = report
.issues
.iter()
.any(|i| i.code == IssueCode::ReservedNotAMap);
assert!(
has_reserved_err,
"expected ReservedNotAMap, got: {:?}",
report.issues
);
}
#[test]
fn metadata_hash_frame_cbor_parse_failed() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta_frame = build_metadata_frame();
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let garbage_cbor = vec![0xFF, 0xFF, 0xFF, 0xFF];
let hash_total = (FRAME_HEADER_SIZE + garbage_cbor.len() + FRAME_END.len()) as u64;
let mut hash_frame = Vec::new();
hash_frame.extend_from_slice(FRAME_MAGIC);
hash_frame.extend_from_slice(&3u16.to_be_bytes()); hash_frame.extend_from_slice(&1u16.to_be_bytes());
hash_frame.extend_from_slice(&0u16.to_be_bytes());
hash_frame.extend_from_slice(&hash_total.to_be_bytes());
hash_frame.extend_from_slice(&garbage_cbor);
hash_frame.extend_from_slice(FRAME_END);
let flags = 1u16 | (1u16 << 4); let msg = build_raw_message(flags, &[meta_frame, hash_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_hash_err = report
.issues
.iter()
.any(|i| i.code == IssueCode::HashFrameCborParseFailed);
assert!(
has_hash_err,
"expected HashFrameCborParseFailed, got: {:?}",
report.issues
);
}
#[test]
fn metadata_preceder_base_count_wrong() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let prec_meta = GlobalMetadata {
base: vec![BTreeMap::new(), BTreeMap::new()], ..GlobalMetadata::default()
};
let prec_cbor = crate::metadata::global_metadata_to_cbor(&prec_meta).unwrap();
let prec_total = (FRAME_HEADER_SIZE + prec_cbor.len() + FRAME_END.len()) as u64;
let mut preceder_frame = Vec::new();
preceder_frame.extend_from_slice(FRAME_MAGIC);
preceder_frame.extend_from_slice(&8u16.to_be_bytes()); preceder_frame.extend_from_slice(&1u16.to_be_bytes());
preceder_frame.extend_from_slice(&0u16.to_be_bytes());
preceder_frame.extend_from_slice(&prec_total.to_be_bytes());
preceder_frame.extend_from_slice(&prec_cbor);
preceder_frame.extend_from_slice(FRAME_END);
let meta_frame = build_metadata_frame();
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let flags = 1u16 | (1u16 << 6); let msg = build_raw_message(
flags,
&[meta_frame, preceder_frame, data_frame],
None,
false,
);
let report = validate_message(&msg, &ValidateOptions::default());
let has_prec_err = report
.issues
.iter()
.any(|i| i.code == IssueCode::PrecederBaseCountWrong);
assert!(
has_prec_err,
"expected PrecederBaseCountWrong, got: {:?}",
report.issues
);
}
#[test]
fn integrity_hash_frame_fallback_verified() {
let msg = make_test_message();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(report.hash_verified, "issues: {:?}", report.issues);
}
#[test]
fn integrity_unknown_hash_algorithm() {
let msg = make_message_with_patched_descriptor(|v| {
let hash_map = ciborium::Value::Map(vec![
(
ciborium::Value::Text("type".to_string()),
ciborium::Value::Text("sha9001".to_string()),
),
(
ciborium::Value::Text("value".to_string()),
ciborium::Value::Text("deadbeef".to_string()),
),
]);
cbor_map_set(v, "hash", hash_map);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_unk_hash = report
.issues
.iter()
.any(|i| i.code == IssueCode::UnknownHashAlgorithm);
assert!(
has_unk_hash,
"expected UnknownHashAlgorithm, got: {:?}",
report.issues
);
}
#[test]
fn integrity_decode_pipeline_failed_corrupt_compressed() {
#[cfg(feature = "zstd")]
{
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "zstd".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 32];
let opts = EncodeOptions {
hash_algorithm: None,
..EncodeOptions::default()
};
let mut msg = encode(&meta, &[(&desc, data.as_slice())], &opts).unwrap();
let pa_start = msg.len() - crate::wire::POSTAMBLE_SIZE;
let target = pa_start * 3 / 4;
if target < msg.len() {
msg[target] ^= 0xFF;
msg[target.saturating_sub(1)] ^= 0xFF;
}
let report = validate_message(&msg, &ValidateOptions::default());
let has_pipeline_err = report.issues.iter().any(|i| {
matches!(
i.code,
IssueCode::DecodePipelineFailed | IssueCode::HashMismatch
)
});
assert!(
has_pipeline_err || !report.is_ok(),
"expected DecodePipelineFailed or error, got: {:?}",
report.issues
);
}
}
#[test]
fn integrity_shape_product_overflow_pipeline() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "compression", ciborium::Value::Text("zstd".to_string()));
cbor_map_set(v, "ndim", ciborium::Value::Integer(2.into()));
cbor_map_set(
v,
"shape",
ciborium::Value::Array(vec![
ciborium::Value::Integer(u64::MAX.into()),
ciborium::Value::Integer(2.into()),
]),
);
cbor_map_set(
v,
"strides",
ciborium::Value::Array(vec![
ciborium::Value::Integer(8.into()),
ciborium::Value::Integer(8.into()),
]),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
let has_shape_or_pipeline = report.issues.iter().any(|i| {
matches!(
i.code,
IssueCode::ShapeOverflow | IssueCode::PipelineConfigFailed
)
});
assert!(
has_shape_or_pipeline,
"expected ShapeOverflow or PipelineConfigFailed, got: {:?}",
report.issues
);
}
#[test]
fn integrity_descriptor_reparse_without_metadata() {
let msg = make_test_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Integrity,
checksum_only: true, check_canonical: false,
};
let report = validate_message(&msg, &opts);
assert!(
report.hash_verified,
"expected hash_verified in checksum mode, got: {:?}",
report.issues
);
}
#[test]
fn fidelity_bitmask_valid() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![16],
strides: vec![1],
dtype: Dtype::Bitmask,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 2]; let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(
report.is_ok(),
"bitmask should pass fidelity: {:?}",
report.issues
);
}
#[test]
fn fidelity_bitmask_non_byte_aligned() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![13],
strides: vec![1],
dtype: Dtype::Bitmask,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 2]; let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
assert!(
report.is_ok(),
"bitmask (non-byte-aligned) should pass fidelity: {:?}",
report.issues
);
}
#[test]
fn fidelity_decoded_size_overflow() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "ndim", ciborium::Value::Integer(1.into()));
let big = (u64::MAX / 8) + 1;
cbor_map_set(
v,
"shape",
ciborium::Value::Array(vec![ciborium::Value::Integer(big.into())]),
);
cbor_map_set(
v,
"strides",
ciborium::Value::Array(vec![ciborium::Value::Integer(8.into())]),
);
});
let report = validate_message(&msg, &full_opts());
let has_size_issue = report.issues.iter().any(|i| {
matches!(
i.code,
IssueCode::DecodedSizeMismatch | IssueCode::ShapeOverflow
)
});
assert!(
has_size_issue,
"expected DecodedSizeMismatch or ShapeOverflow, got: {:?}",
report.issues
);
}
#[test]
fn integrity_no_hash_available() {
let meta = GlobalMetadata::default();
let desc = default_desc();
let data = vec![0u8; 32];
let opts = EncodeOptions {
hash_algorithm: None,
..EncodeOptions::default()
};
let msg = encode(&meta, &[(&desc, data.as_slice())], &opts).unwrap();
let report = validate_message(&msg, &ValidateOptions::default());
let has_no_hash = report
.issues
.iter()
.any(|i| i.code == IssueCode::NoHashAvailable);
assert!(
has_no_hash,
"expected NoHashAvailable, got: {:?}",
report.issues
);
assert!(!report.hash_verified);
}
#[test]
fn structure_total_length_overflow() {
let mut msg = make_test_message();
msg[16..24].copy_from_slice(&u64::MAX.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
let has_err = report.issues.iter().any(|i| {
matches!(
i.code,
IssueCode::TotalLengthOverflow | IssueCode::TotalLengthExceedsBuffer
)
});
assert!(has_err, "expected length error, got: {:?}", report.issues);
}
#[test]
fn metadata_hash_frame_count_mismatch() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta_frame = build_metadata_frame();
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let hash_frame_data = crate::types::HashFrame {
object_count: 3,
hash_type: "xxh3".to_string(),
hashes: vec!["aaa".to_string(), "bbb".to_string(), "ccc".to_string()],
};
let hash_cbor = crate::metadata::hash_frame_to_cbor(&hash_frame_data).unwrap();
let hash_total = (FRAME_HEADER_SIZE + hash_cbor.len() + FRAME_END.len()) as u64;
let mut hash_frame = Vec::new();
hash_frame.extend_from_slice(FRAME_MAGIC);
hash_frame.extend_from_slice(&3u16.to_be_bytes()); hash_frame.extend_from_slice(&1u16.to_be_bytes());
hash_frame.extend_from_slice(&0u16.to_be_bytes());
hash_frame.extend_from_slice(&hash_total.to_be_bytes());
hash_frame.extend_from_slice(&hash_cbor);
hash_frame.extend_from_slice(FRAME_END);
let flags = 1u16 | (1u16 << 4); let msg = build_raw_message(flags, &[meta_frame, hash_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
let has_count_mismatch = report
.issues
.iter()
.any(|i| i.code == IssueCode::HashFrameCountMismatch);
assert!(
has_count_mismatch,
"expected HashFrameCountMismatch, got: {:?}",
report.issues
);
}
#[test]
fn fidelity_raw_payload_scan() {
let msg = make_float64_message(&[1.0, 2.0, 3.0, 4.0]);
let report = validate_message(&msg, &full_opts());
assert!(report.is_ok(), "issues: {:?}", report.issues);
}
#[test]
fn structure_streaming_ffo_high() {
let meta_frame = build_metadata_frame();
let desc = default_desc();
let payload = vec![0u8; 32];
let data_frame = build_data_object_frame(&desc, &payload);
let flags = 1u16;
let mut msg = build_raw_message(flags, &[meta_frame, data_frame], None, true);
let pa_start = msg.len() - 16;
let bad_ffo: u64 = (msg.len() + 100) as u64;
msg[pa_start..pa_start + 8].copy_from_slice(&bad_ffo.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::FooterOffsetOutOfRange),
"expected FooterOffsetOutOfRange, got: {:?}",
report.issues
);
}
#[test]
fn structure_double_preceder() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta_cbor =
crate::metadata::global_metadata_to_cbor(&GlobalMetadata::default()).unwrap();
let total_length = (FRAME_HEADER_SIZE + meta_cbor.len() + FRAME_END.len()) as u64;
let mut pf = Vec::new();
pf.extend_from_slice(FRAME_MAGIC);
pf.extend_from_slice(&8u16.to_be_bytes());
pf.extend_from_slice(&1u16.to_be_bytes());
pf.extend_from_slice(&0u16.to_be_bytes());
pf.extend_from_slice(&total_length.to_be_bytes());
pf.extend_from_slice(&meta_cbor);
pf.extend_from_slice(FRAME_END);
let pf2 = pf.clone();
let desc = default_desc();
let data_frame = build_data_object_frame(&desc, &[0u8; 32]);
let hm = build_metadata_frame();
let flags = 1u16 | (1u16 << 6);
let msg = build_raw_message(flags, &[hm, pf, pf2, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::PrecederNotFollowedByObject),
"expected PrecederNotFollowedByObject, got: {:?}",
report.issues
);
}
#[test]
fn structure_trailing_preceder() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta_cbor =
crate::metadata::global_metadata_to_cbor(&GlobalMetadata::default()).unwrap();
let tl = (FRAME_HEADER_SIZE + meta_cbor.len() + FRAME_END.len()) as u64;
let mut pf = Vec::new();
pf.extend_from_slice(FRAME_MAGIC);
pf.extend_from_slice(&8u16.to_be_bytes());
pf.extend_from_slice(&1u16.to_be_bytes());
pf.extend_from_slice(&0u16.to_be_bytes());
pf.extend_from_slice(&tl.to_be_bytes());
pf.extend_from_slice(&meta_cbor);
pf.extend_from_slice(FRAME_END);
let hm = build_metadata_frame();
let desc = default_desc();
let df = build_data_object_frame(&desc, &[0u8; 32]);
let flags = 1u16 | (1u16 << 6);
let msg = build_raw_message(flags, &[hm, df, pf], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::DanglingPreceder),
"expected DanglingPreceder, got: {:?}",
report.issues
);
}
#[test]
fn structure_flag_mismatch_extra() {
let meta_frame = build_metadata_frame();
let desc = default_desc();
let data_frame = build_data_object_frame(&desc, &[0u8; 32]);
let msg = build_raw_message(0u16, &[meta_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::FlagMismatch),
"expected FlagMismatch, got: {:?}",
report.issues
);
}
#[test]
fn fidelity_bitmask_wrong_size() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "dtype", ciborium::Value::Text("bitmask".to_string()));
cbor_map_set(v, "ndim", ciborium::Value::Integer(1.into()));
cbor_map_set(
v,
"shape",
ciborium::Value::Array(vec![ciborium::Value::Integer(1000.into())]),
);
cbor_map_set(
v,
"strides",
ciborium::Value::Array(vec![ciborium::Value::Integer(1.into())]),
);
});
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::DecodedSizeMismatch),
"expected DecodedSizeMismatch for bitmask, got: {:?}",
report.issues
);
}
#[test]
fn fidelity_multi_object_mixed() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let nan_data: Vec<u8> = f64::NAN
.to_be_bytes()
.iter()
.chain(1.0f64.to_be_bytes().iter())
.copied()
.collect();
let ok_data: Vec<u8> = 2.0f64
.to_be_bytes()
.iter()
.chain(3.0f64.to_be_bytes().iter())
.copied()
.collect();
let msg = encode(
&meta,
&[(&desc, nan_data.as_slice()), (&desc, ok_data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let report = validate_message(&msg, &full_opts());
let nans: Vec<_> = report
.issues
.iter()
.filter(|i| i.code == IssueCode::NanDetected)
.collect();
assert!(
!nans.is_empty(),
"expected NanDetected, got: {:?}",
report.issues
);
assert_eq!(nans[0].object_index, Some(0));
}
#[test]
fn integrity_unknown_hash_in_frame() {
use crate::wire::{FRAME_END, FRAME_HEADER_SIZE, FRAME_MAGIC};
let meta_frame = build_metadata_frame();
let desc = default_desc();
let data_frame = build_data_object_frame(&desc, &[0u8; 32]);
let hf = crate::types::HashFrame {
object_count: 1,
hash_type: "blake99".to_string(),
hashes: vec!["deadbeef".to_string()],
};
let hcbor = crate::metadata::hash_frame_to_cbor(&hf).unwrap();
let htl = (FRAME_HEADER_SIZE + hcbor.len() + FRAME_END.len()) as u64;
let mut hash_frame = Vec::new();
hash_frame.extend_from_slice(FRAME_MAGIC);
hash_frame.extend_from_slice(&3u16.to_be_bytes());
hash_frame.extend_from_slice(&1u16.to_be_bytes());
hash_frame.extend_from_slice(&0u16.to_be_bytes());
hash_frame.extend_from_slice(&htl.to_be_bytes());
hash_frame.extend_from_slice(&hcbor);
hash_frame.extend_from_slice(FRAME_END);
let flags = 1u16 | (1u16 << 4);
let msg = build_raw_message(flags, &[meta_frame, hash_frame, data_frame], None, false);
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::UnknownHashAlgorithm),
"expected UnknownHashAlgorithm, got: {:?}",
report.issues
);
}
#[test]
fn integrity_corrupt_compressed_zstd() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "compression", ciborium::Value::Text("zstd".to_string()));
});
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report.issues.iter().any(|i| matches!(
i.code,
IssueCode::DecodePipelineFailed | IssueCode::PipelineConfigFailed
)),
"expected decode failure, got: {:?}",
report.issues
);
}
#[test]
fn integrity_shape_overflow_filter() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "filter", ciborium::Value::Text("bitshuffle".to_string()));
cbor_map_set(v, "ndim", ciborium::Value::Integer(2.into()));
cbor_map_set(
v,
"shape",
ciborium::Value::Array(vec![
ciborium::Value::Integer(u64::MAX.into()),
ciborium::Value::Integer(2.into()),
]),
);
cbor_map_set(
v,
"strides",
ciborium::Value::Array(vec![
ciborium::Value::Integer(8.into()),
ciborium::Value::Integer(8.into()),
]),
);
});
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report.issues.iter().any(|i| matches!(
i.code,
IssueCode::ShapeOverflow | IssueCode::PipelineConfigFailed
)),
"expected ShapeOverflow or PipelineConfigFailed, got: {:?}",
report.issues
);
}
#[test]
fn fidelity_non_raw_decode_error() {
let msg = make_message_with_patched_descriptor(|v| {
cbor_map_set(v, "compression", ciborium::Value::Text("zstd".to_string()));
});
let report = validate_message(&msg, &full_opts());
assert!(
report.issues.iter().any(|i| matches!(
i.code,
IssueCode::DecodePipelineFailed
| IssueCode::PipelineConfigFailed
| IssueCode::DecodeObjectFailed
)),
"expected decode error, got: {:?}",
report.issues
);
}
#[test]
fn structure_preamble_parse_failed_on_version_zero() {
let mut msg = make_test_message();
msg[8] = 0;
msg[9] = 0;
let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.is_ok());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::PreambleParseFailed),
"expected PreambleParseFailed, got: {:?}",
report.issues
);
}
#[test]
fn structure_total_length_too_small() {
let mut msg = make_test_message();
msg[16..24].copy_from_slice(&10u64.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::TotalLengthTooSmall),
"expected TotalLengthTooSmall, got: {:?}",
report.issues
);
}
#[test]
fn structure_footer_offset_below_preamble() {
let mut msg = make_test_message();
let ffo_pos = msg.len() - 16;
msg[ffo_pos..ffo_pos + 8].copy_from_slice(&0u64.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::FooterOffsetOutOfRange),
"expected FooterOffsetOutOfRange, got: {:?}",
report.issues
);
}
#[test]
fn structure_footer_offset_beyond_postamble() {
let mut msg = make_test_message();
let msg_len = msg.len();
let ffo_pos = msg_len - 16;
msg[ffo_pos..ffo_pos + 8].copy_from_slice(&(msg_len as u64 + 1000).to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::FooterOffsetOutOfRange),
"expected FooterOffsetOutOfRange, got: {:?}",
report.issues
);
}
#[test]
fn structure_footer_offset_overflows_usize() {
let mut msg = make_test_message();
let msg_len = msg.len();
let ffo_pos = msg_len - 16;
msg[ffo_pos..ffo_pos + 8].copy_from_slice(&u64::MAX.to_be_bytes());
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::FooterOffsetOutOfRange),
"expected FooterOffsetOutOfRange, got: {:?}",
report.issues
);
}
fn find_data_object_frame(msg: &[u8]) -> Option<(usize, usize)> {
let mut pos = PREAMBLE_SIZE;
for _guard in 0..64 {
if pos + FRAME_HEADER_SIZE > msg.len() {
return None;
}
if &msg[pos..pos + 2] != b"FR" {
return None;
}
let fh_type_raw = u16::from_be_bytes([msg[pos + 2], msg[pos + 3]]);
let fh_total = u64::from_be_bytes(msg[pos + 8..pos + 16].try_into().unwrap()) as usize;
if fh_total < FRAME_HEADER_SIZE {
return None;
}
if fh_type_raw == 4 {
return Some((pos, fh_total));
}
pos = (pos + fh_total + 7) & !7;
}
None
}
#[test]
fn structure_data_object_too_small() {
let msg = make_test_message();
let (pos, _) = find_data_object_frame(&msg).expect("no DataObject frame");
let mut patched = msg.clone();
let new_total = (FRAME_HEADER_SIZE + 8) as u64;
patched[pos + 8..pos + 16].copy_from_slice(&new_total.to_be_bytes());
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
!report.issues.is_empty(),
"expected at least one issue on shrunk data object"
);
}
#[test]
fn structure_cbor_offset_overflows_usize() {
let msg = make_test_message();
let (pos, fh_total) = find_data_object_frame(&msg).expect("no DataObject frame");
let frame_end = pos + fh_total;
let cbor_off_pos = frame_end - 4 - 8;
let mut patched = msg.clone();
patched[cbor_off_pos..cbor_off_pos + 8].copy_from_slice(&u64::MAX.to_be_bytes());
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
!report.issues.is_empty(),
"expected at least one issue on bogus cbor_offset"
);
}
#[test]
fn metadata_cbor_parse_failed() {
let msg = make_test_message();
let mut patched = msg.clone();
let cbor_start = PREAMBLE_SIZE + FRAME_HEADER_SIZE;
patched[cbor_start..cbor_start + 4].fill(0xFF);
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
report.issues.iter().any(|i| matches!(
i.code,
IssueCode::MetadataCborParseFailed | IssueCode::DescriptorCborParseFailed
)),
"expected CBOR parse failure, got: {:?}",
report.issues
);
}
#[test]
fn metadata_base_count_exceeds_objects() {
let mut meta = GlobalMetadata::default();
let mut e0: BTreeMap<String, ciborium::Value> = BTreeMap::new();
e0.insert(
"mars".to_string(),
ciborium::Value::Text("param0".to_string()),
);
let mut e1: BTreeMap<String, ciborium::Value> = BTreeMap::new();
e1.insert(
"mars".to_string(),
ciborium::Value::Text("param1".to_string()),
);
meta.base = vec![e0, e1];
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 32];
let err = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
);
assert!(err.is_err(), "expected encode to reject base > descriptors");
}
#[test]
fn fidelity_defensive_decode_path_via_direct_call() {
use crate::validate::fidelity::validate_fidelity;
use crate::validate::types::{DecodeState, ObjectContext};
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Little,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "lz4".to_string(),
params: BTreeMap::new(),
hash: None,
};
let raw: Vec<u8> = (2.5f64)
.to_le_bytes()
.iter()
.chain((3.5f64).to_le_bytes().iter())
.copied()
.collect();
let config = crate::encode::build_pipeline_config(&desc, 2, Dtype::Float64)
.expect("pipeline config");
let compressed = tensogram_encodings::pipeline::encode_pipeline(&raw, &config)
.expect("encode pipeline")
.encoded_bytes;
let mut ctx = vec![ObjectContext {
descriptor: Some(desc),
descriptor_failed: false,
cbor_bytes: &[],
payload: compressed.as_slice(),
decode_state: DecodeState::NotDecoded,
frame_offset: 100,
}];
let mut issues = Vec::new();
validate_fidelity(&mut ctx, &mut issues);
if !issues.is_empty() {
assert!(
issues.iter().all(|i| matches!(
i.code,
IssueCode::DecodeObjectFailed
| IssueCode::DecodedSizeMismatch
| IssueCode::NanDetected
| IssueCode::InfDetected
)),
"unexpected issue codes: {:?}",
issues
);
}
}
#[test]
fn structure_truncated_frame_header() {
let msg = make_test_message();
let mut patched = msg.clone();
let new_total = (PREAMBLE_SIZE + 10 + POSTAMBLE_SIZE) as u64;
patched[16..24].copy_from_slice(&new_total.to_be_bytes());
let pa_pos = PREAMBLE_SIZE + 10;
let postamble = &msg[msg.len() - POSTAMBLE_SIZE..];
if patched.len() < pa_pos + POSTAMBLE_SIZE {
patched.resize(pa_pos + POSTAMBLE_SIZE, 0);
} else {
patched.truncate(pa_pos + POSTAMBLE_SIZE);
}
patched[pa_pos..pa_pos + POSTAMBLE_SIZE].copy_from_slice(postamble);
let report = validate_message(&patched, &ValidateOptions::default());
assert!(!report.issues.is_empty());
}
#[test]
fn structure_invalid_frame_header_type() {
let msg = make_test_message();
let (pos, _) = find_data_object_frame(&msg).expect("no DataObject");
let mut patched = msg.clone();
patched[pos + 2..pos + 4].copy_from_slice(&99u16.to_be_bytes());
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InvalidFrameHeader),
"expected InvalidFrameHeader, got: {:?}",
report.issues
);
}
#[test]
fn structure_frame_too_small_nonzero_total() {
let msg = make_test_message();
let mut patched = msg.clone();
let first_type =
u16::from_be_bytes([patched[PREAMBLE_SIZE + 2], patched[PREAMBLE_SIZE + 3]]);
if first_type == 4 {
return; }
let tl_pos = PREAMBLE_SIZE + 8;
patched[tl_pos..tl_pos + 8].copy_from_slice(&18u64.to_be_bytes());
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::FrameTooSmall),
"expected FrameTooSmall, got: {:?}",
report.issues
);
}
#[test]
fn structure_frame_exceeds_message() {
let msg = make_test_message();
let mut patched = msg.clone();
let tl_pos = PREAMBLE_SIZE + 8;
let huge = ((patched.len() - PREAMBLE_SIZE + 100) as u64).max(128);
patched[tl_pos..tl_pos + 8].copy_from_slice(&huge.to_be_bytes());
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
report.issues.iter().any(|i| matches!(
i.code,
IssueCode::FrameExceedsMessage
| IssueCode::FrameLengthOverflow
| IssueCode::TotalLengthExceedsBuffer
)),
"expected FrameExceedsMessage, got: {:?}",
report.issues
);
}
#[test]
fn structure_missing_end_marker() {
let msg = make_test_message();
let mut patched = msg.clone();
let tl_pos = PREAMBLE_SIZE + 8;
let frame_total =
u64::from_be_bytes(patched[tl_pos..tl_pos + 8].try_into().unwrap()) as usize;
let frame_end = PREAMBLE_SIZE + frame_total;
patched[frame_end - 4..frame_end].copy_from_slice(&[0xAB, 0xCD, 0xEF, 0x01]);
let report = validate_message(&patched, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::MissingEndMarker),
"expected MissingEndMarker, got: {:?}",
report.issues
);
}
#[test]
fn structure_data_object_with_corrupt_cbor() {
let msg = make_test_message();
let (pos, fh_total) = find_data_object_frame(&msg).expect("no DataObject");
let mut patched = msg.clone();
let corrupt_pos = pos + FRAME_HEADER_SIZE;
for i in 0..4 {
patched[corrupt_pos + i] = 0xFF;
}
let frame_end = pos + fh_total;
let cbor_area = frame_end - 4 - 8;
patched[cbor_area.saturating_sub(8)..cbor_area].fill(0xFF);
let report = validate_message(&patched, &ValidateOptions::default());
assert!(!report.issues.is_empty());
}
fn make_raw_object_message(dtype: Dtype, bytes: Vec<u8>, shape: Vec<u64>) -> Vec<u8> {
let meta = GlobalMetadata::default();
let byte_width = dtype.byte_width();
let strides = vec![byte_width as u64];
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: shape.len() as u64,
shape,
strides,
dtype,
byte_order: ByteOrder::Little,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
encode(
&meta,
&[(&desc, bytes.as_slice())],
&EncodeOptions::default(),
)
.unwrap()
}
#[test]
fn fidelity_float16_nan() {
let nan_bits: u16 = 0x7E00;
let bytes: Vec<u8> = [0u16, nan_bits, 0u16, 0u16]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
let msg = make_raw_object_message(Dtype::Float16, bytes, vec![4]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected),
"expected NaN detected, got {:?}",
report.issues
);
}
#[test]
fn fidelity_float16_inf() {
let inf_bits: u16 = 0x7C00;
let bytes: Vec<u8> = [0u16, 0u16, inf_bits, 0u16]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
let msg = make_raw_object_message(Dtype::Float16, bytes, vec![4]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected),
"expected Inf detected, got {:?}",
report.issues
);
}
#[test]
fn fidelity_bfloat16_nan() {
let nan_bits: u16 = 0x7FC0;
let bytes: Vec<u8> = [nan_bits, 0u16]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
let msg = make_raw_object_message(Dtype::Bfloat16, bytes, vec![2]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected)
);
}
#[test]
fn fidelity_bfloat16_inf() {
let inf_bits: u16 = 0x7F80;
let bytes: Vec<u8> = [0u16, inf_bits]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
let msg = make_raw_object_message(Dtype::Bfloat16, bytes, vec![2]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected)
);
}
#[test]
fn fidelity_complex64_imag_nan() {
let mut bytes = Vec::with_capacity(16);
bytes.extend_from_slice(&1.0f32.to_le_bytes());
bytes.extend_from_slice(&f32::NAN.to_le_bytes());
bytes.extend_from_slice(&1.0f32.to_le_bytes());
bytes.extend_from_slice(&1.0f32.to_le_bytes());
let msg = make_raw_object_message(Dtype::Complex64, bytes, vec![2]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected)
);
}
#[test]
fn fidelity_complex64_imag_inf() {
let mut bytes = Vec::with_capacity(16);
bytes.extend_from_slice(&1.0f32.to_le_bytes());
bytes.extend_from_slice(&f32::INFINITY.to_le_bytes());
bytes.extend_from_slice(&1.0f32.to_le_bytes());
bytes.extend_from_slice(&1.0f32.to_le_bytes());
let msg = make_raw_object_message(Dtype::Complex64, bytes, vec![2]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected)
);
}
#[test]
fn fidelity_complex128_real_nan() {
let mut bytes = Vec::with_capacity(32);
bytes.extend_from_slice(&f64::NAN.to_le_bytes());
bytes.extend_from_slice(&0.0f64.to_le_bytes());
bytes.extend_from_slice(&1.0f64.to_le_bytes());
bytes.extend_from_slice(&1.0f64.to_le_bytes());
let msg = make_raw_object_message(Dtype::Complex128, bytes, vec![2]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NanDetected)
);
}
#[test]
fn fidelity_complex128_real_inf() {
let mut bytes = Vec::with_capacity(32);
bytes.extend_from_slice(&f64::NEG_INFINITY.to_le_bytes());
bytes.extend_from_slice(&0.0f64.to_le_bytes());
bytes.extend_from_slice(&1.0f64.to_le_bytes());
bytes.extend_from_slice(&1.0f64.to_le_bytes());
let msg = make_raw_object_message(Dtype::Complex128, bytes, vec![2]);
let report = validate_message(&msg, &full_opts());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::InfDetected)
);
}
#[test]
fn metadata_descriptor_canonical_check_runs() {
let msg = make_test_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Metadata,
check_canonical: true,
checksum_only: false,
};
let report = validate_message(&msg, &opts);
assert!(
!report.issues.iter().any(|i| matches!(
i.code,
IssueCode::DescriptorCborNonCanonical | IssueCode::MetadataCborNonCanonical
)),
"legit message should be canonical, got: {:?}",
report.issues
);
}
#[test]
fn metadata_hash_frame_count_via_missing_object() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 32];
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions {
hash_algorithm: Some(crate::hash::HashAlgorithm::Xxh3),
..EncodeOptions::default()
},
)
.unwrap();
let report = validate_message(&msg, &ValidateOptions::default());
assert!(report.is_ok(), "baseline hash message should validate");
}
#[test]
fn integrity_with_hash_succeeds_end_to_end() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 16];
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions {
hash_algorithm: Some(crate::hash::HashAlgorithm::Xxh3),
..EncodeOptions::default()
},
)
.unwrap();
let opts = ValidateOptions {
max_level: ValidationLevel::Integrity,
check_canonical: false,
checksum_only: false,
};
let report = validate_message(&msg, &opts);
assert!(report.hash_verified);
assert!(report.is_ok());
}
#[test]
fn integrity_checksum_only_mode() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![4],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 32];
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions {
hash_algorithm: Some(crate::hash::HashAlgorithm::Xxh3),
..EncodeOptions::default()
},
)
.unwrap();
let opts = ValidateOptions {
max_level: ValidationLevel::Integrity,
check_canonical: false,
checksum_only: true,
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok());
assert!(report.hash_verified);
}
#[test]
fn metadata_index_offset_covered_on_valid_message() {
let msg = make_multi_object_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Metadata,
check_canonical: false,
checksum_only: false,
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok());
}
#[test]
fn metadata_preceder_path_covered_via_streaming() {
let meta = GlobalMetadata::default();
let desc = DataObjectDescriptor {
obj_type: "ndarray".to_string(),
ndim: 1,
shape: vec![2],
strides: vec![8],
dtype: Dtype::Float64,
byte_order: ByteOrder::Big,
encoding: "none".to_string(),
filter: "none".to_string(),
compression: "none".to_string(),
params: BTreeMap::new(),
hash: None,
};
let data = vec![0u8; 16];
let msg = encode(
&meta,
&[(&desc, data.as_slice())],
&EncodeOptions::default(),
)
.unwrap();
let opts = ValidateOptions {
max_level: ValidationLevel::Metadata,
check_canonical: true,
checksum_only: false,
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok());
}
#[test]
fn metadata_reserved_not_a_map_via_direct_cbor() {
let msg = make_test_message();
let opts = ValidateOptions {
max_level: ValidationLevel::Metadata,
check_canonical: false,
checksum_only: false,
};
let report = validate_message(&msg, &opts);
assert!(report.is_ok(), "issues: {:?}", report.issues);
}
#[test]
fn structure_streaming_mode_buffer_too_short_for_postamble() {
let mut msg = Vec::with_capacity(PREAMBLE_SIZE);
msg.extend_from_slice(b"TENSOGRM");
msg.extend_from_slice(&2u16.to_be_bytes()); msg.extend_from_slice(&0u16.to_be_bytes()); msg.extend_from_slice(&0u32.to_be_bytes()); msg.extend_from_slice(&0u64.to_be_bytes()); let report = validate_message(&msg, &ValidateOptions::default());
assert!(!report.issues.is_empty());
}
#[test]
fn structure_streaming_mode_invalid_postamble() {
let total_len = PREAMBLE_SIZE + POSTAMBLE_SIZE;
let mut msg = Vec::with_capacity(total_len);
msg.extend_from_slice(b"TENSOGRM");
msg.extend_from_slice(&2u16.to_be_bytes());
msg.extend_from_slice(&0u16.to_be_bytes());
msg.extend_from_slice(&0u32.to_be_bytes());
msg.extend_from_slice(&0u64.to_be_bytes()); msg.extend_from_slice(&(PREAMBLE_SIZE as u64).to_be_bytes());
msg.extend_from_slice(b"BOGUSMAG");
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::PostambleInvalid),
"expected PostambleInvalid, got: {:?}",
report.issues
);
}
#[test]
fn structure_no_metadata_frame_empty_body() {
let total_len = PREAMBLE_SIZE + POSTAMBLE_SIZE;
let mut msg = Vec::with_capacity(total_len);
msg.extend_from_slice(b"TENSOGRM");
msg.extend_from_slice(&2u16.to_be_bytes()); msg.extend_from_slice(&0u16.to_be_bytes()); msg.extend_from_slice(&0u32.to_be_bytes()); msg.extend_from_slice(&(total_len as u64).to_be_bytes());
msg.extend_from_slice(&(PREAMBLE_SIZE as u64).to_be_bytes());
msg.extend_from_slice(b"39277777");
let report = validate_message(&msg, &ValidateOptions::default());
assert!(
report
.issues
.iter()
.any(|i| i.code == IssueCode::NoMetadataFrame),
"expected NoMetadataFrame, got: {:?}",
report.issues
);
}
}