use crate::layout::{FieldKind, ListElementKind, OffsetTable, SchemaLayout};
use crate::schema_canonical::{Field, Schema, TypeRepr};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Region {
pub start: usize,
pub end: usize,
}
impl Region {
pub fn new(start: usize, end: usize) -> Result<Self, VerifyError> {
if start > end {
return Err(VerifyError::DegenerateRegion { start, end });
}
Ok(Self { start, end })
}
fn contains_span(&self, off: usize, len: usize) -> bool {
match off.checked_add(len) {
Some(end) => off >= self.start && end <= self.end,
None => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegionTag {
Const,
In,
Out,
Scratch,
}
impl RegionTag {
pub fn label(self) -> &'static str {
match self {
RegionTag::Const => "const",
RegionTag::In => "in",
RegionTag::Out => "out",
RegionTag::Scratch => "scratch",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MultiRegion {
regions: [(RegionTag, Region); 4],
}
impl MultiRegion {
pub fn new(
const_data: (usize, usize),
in_buf: (usize, usize),
out_buf: (usize, usize),
scratch: (usize, usize),
) -> Result<Self, VerifyError> {
Ok(Self {
regions: [
(RegionTag::Const, Region::new(const_data.0, const_data.1)?),
(RegionTag::In, Region::new(in_buf.0, in_buf.1)?),
(RegionTag::Out, Region::new(out_buf.0, out_buf.1)?),
(RegionTag::Scratch, Region::new(scratch.0, scratch.1)?),
],
})
}
fn max_end(&self) -> usize {
self.regions.iter().map(|(_, r)| r.end).max().unwrap_or(0)
}
fn classify_span(&self, off: usize, len: usize) -> Option<RegionTag> {
for (tag, region) in &self.regions {
if region.contains_span(off, len) {
return Some(*tag);
}
}
None
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum VerifyError {
#[error("degenerate region: start {start} > end {end}")]
DegenerateRegion {
start: usize,
end: usize,
},
#[error("buffer of {have} bytes is shorter than region end {end}")]
BufferShorterThanRegion {
have: usize,
end: usize,
},
#[error("fixed-area slot for `{field}` at {offset}..+{size} escapes region [{start}, {end})")]
FixedSlotOutOfRegion {
field: String,
offset: usize,
size: usize,
start: usize,
end: usize,
},
#[error("offset {offset}..+{len} for `{field}` ({what}) escapes region [{start}, {end})")]
OutOfRegion {
field: String,
what: &'static str,
offset: usize,
len: usize,
start: usize,
end: usize,
},
#[error("arithmetic overflow computing span for `{field}` ({what})")]
SpanOverflow {
field: String,
what: &'static str,
},
#[error("verifier does not model type `{ty}` in field `{field}`")]
UnsupportedType {
field: String,
ty: &'static str,
},
#[error(
"offset {offset}..+{len} for `{field}` ({what}) fits no arena region \
(const, in, out, scratch are all disjoint windows)"
)]
NoRegion {
field: String,
what: &'static str,
offset: usize,
len: usize,
},
#[error("verifier recursion depth exceeded ({depth}) at field `{field}`")]
DepthExceeded {
field: String,
depth: usize,
},
}
#[derive(Debug, Clone, Copy)]
enum Bounds {
Single(Region),
Multi(MultiRegion),
}
impl Bounds {
fn required_len(&self) -> usize {
match self {
Bounds::Single(r) => r.end,
Bounds::Multi(m) => m.max_end(),
}
}
fn require_span(
&self,
field: &str,
what: &'static str,
off: usize,
len: usize,
) -> Result<(), VerifyError> {
if off.checked_add(len).is_none() {
return Err(VerifyError::SpanOverflow {
field: field.to_string(),
what,
});
}
match self {
Bounds::Single(region) => {
if region.contains_span(off, len) {
Ok(())
} else {
Err(VerifyError::OutOfRegion {
field: field.to_string(),
what,
offset: off,
len,
start: region.start,
end: region.end,
})
}
}
Bounds::Multi(multi) => {
if multi.classify_span(off, len).is_some() {
Ok(())
} else {
Err(VerifyError::NoRegion {
field: field.to_string(),
what,
offset: off,
len,
})
}
}
}
}
fn require_fixed_slot(&self, field: &str, off: usize, size: usize) -> Result<(), VerifyError> {
match self {
Bounds::Single(region) => {
if region.contains_span(off, size) {
Ok(())
} else {
Err(VerifyError::FixedSlotOutOfRegion {
field: field.to_string(),
offset: off,
size,
start: region.start,
end: region.end,
})
}
}
Bounds::Multi(multi) => {
if multi.classify_span(off, size).is_some() {
Ok(())
} else {
Err(VerifyError::NoRegion {
field: field.to_string(),
what: "record fixed slot",
offset: off,
len: size,
})
}
}
}
}
}
const MAX_DEPTH: usize = 64;
pub fn verify_record(
bytes: &[u8],
layout: &OffsetTable,
fields: &[Field],
record_base: usize,
region: Region,
) -> Result<(), VerifyError> {
verify_record_with_bounds(bytes, layout, fields, record_base, Bounds::Single(region))
}
pub fn verify_record_multi(
bytes: &[u8],
layout: &OffsetTable,
fields: &[Field],
record_base: usize,
multi: MultiRegion,
) -> Result<(), VerifyError> {
verify_record_with_bounds(bytes, layout, fields, record_base, Bounds::Multi(multi))
}
fn verify_record_with_bounds(
bytes: &[u8],
layout: &OffsetTable,
fields: &[Field],
record_base: usize,
bounds: Bounds,
) -> Result<(), VerifyError> {
let required = bounds.required_len();
if bytes.len() < required {
return Err(VerifyError::BufferShorterThanRegion {
have: bytes.len(),
end: required,
});
}
verify_record_inner(bytes, layout, fields, record_base, bounds, 0)
}
pub fn verify_value_at(
bytes: &[u8],
ty: &TypeRepr,
list_element: Option<ListElementKind>,
root: usize,
region: Region,
) -> Result<(), VerifyError> {
verify_value_at_with_bounds(bytes, ty, list_element, root, Bounds::Single(region))
}
pub fn verify_value_at_multi(
bytes: &[u8],
ty: &TypeRepr,
list_element: Option<ListElementKind>,
root: usize,
multi: MultiRegion,
) -> Result<(), VerifyError> {
verify_value_at_with_bounds(bytes, ty, list_element, root, Bounds::Multi(multi))
}
fn verify_value_at_with_bounds(
bytes: &[u8],
ty: &TypeRepr,
list_element: Option<ListElementKind>,
root: usize,
bounds: Bounds,
) -> Result<(), VerifyError> {
let required = bounds.required_len();
if bytes.len() < required {
return Err(VerifyError::BufferShorterThanRegion {
have: bytes.len(),
end: required,
});
}
verify_pointer_target(bytes, "<in-place root>", ty, list_element, root, bounds, 0)
}
fn verify_record_inner(
bytes: &[u8],
layout: &OffsetTable,
fields: &[Field],
record_base: usize,
bounds: Bounds,
depth: usize,
) -> Result<(), VerifyError> {
if depth >= MAX_DEPTH {
return Err(VerifyError::DepthExceeded {
field: "<record>".to_string(),
depth: MAX_DEPTH,
});
}
bounds.require_fixed_slot("<root>", record_base, layout.root_size)?;
for fo in &layout.fields {
let slot_abs =
record_base
.checked_add(fo.offset)
.ok_or_else(|| VerifyError::SpanOverflow {
field: fo.name.clone(),
what: "fixed slot offset",
})?;
bounds.require_fixed_slot(&fo.name, slot_abs, fo.size)?;
let FieldKind::PointerIndirect { .. } = fo.kind else {
continue;
};
let declared = fields.iter().find(|f| f.name == fo.name);
let ty = match declared {
Some(f) => &f.ty,
None => {
return Err(VerifyError::UnsupportedType {
field: fo.name.clone(),
ty: "<missing schema field>",
});
}
};
let ptr = read_u32(bytes, slot_abs, &fo.name, "pointer slot", bounds)?;
verify_pointer_target(bytes, &fo.name, ty, fo.list_element, ptr, bounds, depth)?;
}
Ok(())
}
fn verify_pointer_target(
bytes: &[u8],
field: &str,
ty: &TypeRepr,
list_element: Option<ListElementKind>,
ptr: usize,
bounds: Bounds,
depth: usize,
) -> Result<(), VerifyError> {
match ty {
TypeRepr::String => {
let len = read_u32(bytes, ptr, field, "length prefix", bounds)?;
let payload = ptr
.checked_add(4)
.ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "string payload start",
})?;
bounds.require_span(field, "string payload", payload, len)
}
TypeRepr::Schema { schema } => {
let sub_layout =
SchemaLayout::offsets_for(schema).map_err(|_| VerifyError::UnsupportedType {
field: field.to_string(),
ty: "Schema (unlayoutable)",
})?;
verify_record_inner(bytes, &sub_layout, &schema.fields, ptr, bounds, depth + 1)
}
TypeRepr::List { element } => {
verify_list_target(bytes, field, element, list_element, ptr, bounds, depth)
}
TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
verify_variant_target(bytes, field, ty, ptr, bounds, depth)
}
other => Err(VerifyError::UnsupportedType {
field: field.to_string(),
ty: type_label(other),
}),
}
}
fn verify_variant_target(
bytes: &[u8],
field: &str,
ty: &TypeRepr,
ptr: usize,
bounds: Bounds,
depth: usize,
) -> Result<(), VerifyError> {
if depth >= MAX_DEPTH {
return Err(VerifyError::DepthExceeded {
field: field.to_string(),
depth: MAX_DEPTH,
});
}
bounds.require_span(field, "variant tag", ptr, 1)?;
let tag = bytes[ptr];
let Some(payload_ty) =
variant_payload_type(ty, tag).ok_or_else(|| VerifyError::UnsupportedType {
field: field.to_string(),
ty: type_label(ty),
})?
else {
return Ok(());
};
let (slot_size, slot_align) = variant_payload_slot_layout(&payload_ty);
let slot = align_up(
ptr.checked_add(1)
.ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "variant payload slot start",
})?,
slot_align,
)
.ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "variant payload slot alignment",
})?;
bounds.require_span(field, "variant payload slot", slot, slot_size)?;
match &payload_ty {
TypeRepr::Unit | TypeRepr::Bool | TypeRepr::Int | TypeRepr::Float => Ok(()),
TypeRepr::String
| TypeRepr::Schema { .. }
| TypeRepr::List { .. }
| TypeRepr::Option { .. }
| TypeRepr::Result { .. }
| TypeRepr::Enum { .. } => {
let target = read_u32(bytes, slot, field, "variant payload pointer", bounds)?;
verify_pointer_target(
bytes,
field,
&payload_ty,
list_kind_for_list_type(&payload_ty),
target,
bounds,
depth + 1,
)
}
TypeRepr::Closure { .. } => Err(VerifyError::UnsupportedType {
field: field.to_string(),
ty: "Closure",
}),
}
}
fn variant_payload_type(ty: &TypeRepr, tag: u8) -> Option<Option<TypeRepr>> {
match ty {
TypeRepr::Option { inner } => match tag {
0 => Some(None),
1 => Some(Some(inner.as_ref().clone())),
_ => None,
},
TypeRepr::Result { ok, err } => match tag {
0 => Some(Some(ok.as_ref().clone())),
1 => Some(Some(err.as_ref().clone())),
_ => None,
},
TypeRepr::Enum { name, variants } => variants
.iter()
.find(|variant| variant.tag == tag)
.map(|variant| {
variant.payload_schema(name).map(|schema| TypeRepr::Schema {
schema: Box::new(schema),
})
}),
_ => None,
}
}
fn variant_payload_slot_layout(ty: &TypeRepr) -> (usize, usize) {
match ty {
TypeRepr::Unit | TypeRepr::Bool => (1, 1),
TypeRepr::Int | TypeRepr::Float => (8, 8),
_ => (4, 4),
}
}
fn verify_list_target(
bytes: &[u8],
field: &str,
element: &TypeRepr,
list_element: Option<ListElementKind>,
ptr: usize,
bounds: Bounds,
depth: usize,
) -> Result<(), VerifyError> {
let count = read_u32(bytes, ptr, field, "list length prefix", bounds)?;
let entries_start = ptr
.checked_add(4)
.ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "list entries start",
})?;
match list_element {
Some(ListElementKind::InlineFixed {
elem_size,
elem_align,
}) => {
let payload_start =
align_up(entries_start, elem_align).ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "inline list payload start",
})?;
let byte_len =
count
.checked_mul(elem_size)
.ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "inline list byte length",
})?;
bounds.require_span(field, "inline list payload", payload_start, byte_len)
}
Some(ListElementKind::PointerArray { .. }) => {
for i in 0..count {
let entry_off = entries_start
.checked_add(i.checked_mul(4).ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "list entry index",
})?)
.ok_or_else(|| VerifyError::SpanOverflow {
field: field.to_string(),
what: "list entry offset",
})?;
let entry_ptr = read_u32(bytes, entry_off, field, "list entry pointer", bounds)?;
verify_pointer_target(
bytes,
field,
element,
element_list_kind(element),
entry_ptr,
bounds,
depth + 1,
)?;
}
Ok(())
}
None => {
Err(VerifyError::UnsupportedType {
field: field.to_string(),
ty: "List (missing element layout)",
})
}
}
}
fn element_list_kind(element: &TypeRepr) -> Option<ListElementKind> {
let TypeRepr::List { .. } = element else {
return None;
};
list_kind_for_list_type(element)
}
fn list_kind_for_list_type(ty: &TypeRepr) -> Option<ListElementKind> {
let TypeRepr::List { .. } = ty else {
return None;
};
let probe = Schema {
name: "<probe>".to_string(),
generics: vec![],
is_tuple: false,
fields: vec![Field {
name: "f".to_string(),
ty: ty.clone(),
default: None,
}],
};
SchemaLayout::offsets_for(&probe)
.ok()
.and_then(|t| t.fields.into_iter().next())
.and_then(|fo| fo.list_element)
}
fn read_u32(
bytes: &[u8],
off: usize,
field: &str,
what: &'static str,
bounds: Bounds,
) -> Result<usize, VerifyError> {
bounds.require_span(field, what, off, 4)?;
let mut buf = [0u8; 4];
buf.copy_from_slice(&bytes[off..off + 4]);
Ok(u32::from_le_bytes(buf) as usize)
}
fn align_up(off: usize, align: usize) -> Option<usize> {
if align <= 1 {
return Some(off);
}
off.checked_next_multiple_of(align)
}
fn type_label(ty: &TypeRepr) -> &'static str {
match ty {
TypeRepr::Unit => "Unit",
TypeRepr::Bool => "Bool",
TypeRepr::Int => "Int",
TypeRepr::Float => "Float",
TypeRepr::String => "String",
TypeRepr::List { .. } => "List",
TypeRepr::Option { .. } => "Option",
TypeRepr::Result { .. } => "Result",
TypeRepr::Enum { .. } => "Enum",
TypeRepr::Schema { .. } => "Schema",
TypeRepr::Closure { .. } => "Closure",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::BufferBuilder;
use crate::layout::SchemaLayout;
use crate::schema_canonical::{Field, Schema};
fn field(name: &str, ty: TypeRepr) -> Field {
Field {
name: name.into(),
ty,
default: None,
}
}
fn list(inner: TypeRepr) -> TypeRepr {
TypeRepr::List {
element: Box::new(inner),
}
}
fn user_schema() -> Schema {
Schema {
name: "User".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("name", TypeRepr::String), field("age", TypeRepr::Int)],
}
}
fn full_region(bytes: &[u8]) -> Region {
Region::new(0, bytes.len()).expect("region")
}
#[test]
fn legal_string_int_record_verifies() {
let schema = user_schema();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_string("name", "ada").unwrap();
b.write_int("age", 36).unwrap();
let bytes = b.finish();
verify_record(&bytes, &layout, &schema.fields, 0, full_region(&bytes))
.expect("legal buffer must verify");
}
#[test]
fn legal_list_string_record_verifies() {
let schema = Schema {
name: "Tags".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("tags", list(TypeRepr::String))],
};
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_list_string("tags", &["a", "bb", "", "䏿–‡"])
.unwrap();
let bytes = b.finish();
verify_record(&bytes, &layout, &schema.fields, 0, full_region(&bytes))
.expect("legal list-string buffer must verify");
}
#[test]
fn legal_list_int_record_verifies() {
let schema = Schema {
name: "Nums".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("nums", list(TypeRepr::Int))],
};
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_list_int("nums", &[1, -2, 3, i64::MIN, i64::MAX])
.unwrap();
let bytes = b.finish();
verify_record(&bytes, &layout, &schema.fields, 0, full_region(&bytes))
.expect("legal list-int buffer must verify");
}
#[test]
fn legal_nested_schema_record_verifies() {
let addr = Schema {
name: "Addr".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("city", TypeRepr::String), field("zip", TypeRepr::Int)],
};
let usr = Schema {
name: "Usr".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field(
"addr",
TypeRepr::Schema {
schema: Box::new(addr.clone()),
},
),
field("name", TypeRepr::String),
],
};
let usr_layout = SchemaLayout::offsets_for(&usr).expect("usr layout");
let addr_layout = SchemaLayout::offsets_for(&addr).expect("addr layout");
let mut b = BufferBuilder::new(&usr_layout, &usr.fields);
let mut sub = b.sub_record("addr", &addr_layout, &addr.fields).unwrap();
sub.write_string("city", "BJ").unwrap();
sub.write_int("zip", 100000).unwrap();
b.finish_sub_record("addr", sub).unwrap();
b.write_string("name", "Bob").unwrap();
let bytes = b.finish();
verify_record(&bytes, &usr_layout, &usr.fields, 0, full_region(&bytes))
.expect("legal nested-schema buffer must verify");
}
#[test]
fn out_of_range_string_pointer_rejected() {
let schema = user_schema();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_string("name", "ada").unwrap();
b.write_int("age", 36).unwrap();
let mut bytes = b.finish();
let bogus = (bytes.len() as u32 + 9999).to_le_bytes();
bytes[0..4].copy_from_slice(&bogus);
let region = full_region(&bytes);
let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
.expect_err("bogus string pointer must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion {
what: "length prefix",
..
}
),
"got {err:?}"
);
}
#[test]
fn overlong_string_len_prefix_rejected() {
let schema = user_schema();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_string("name", "ada").unwrap();
b.write_int("age", 36).unwrap();
let mut bytes = b.finish();
let ptr = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
bytes[ptr..ptr + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
let region = full_region(&bytes);
let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
.expect_err("overlong string len must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion {
what: "string payload",
..
}
),
"got {err:?}"
);
}
#[test]
fn cross_region_pointer_rejected() {
let schema = user_schema();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_string("name", "ada").unwrap();
b.write_int("age", 36).unwrap();
let bytes = b.finish();
let ptr = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
let region = Region::new(0, ptr + 4).expect("region");
let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
.expect_err("payload outside region must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion {
what: "string payload",
..
}
),
"got {err:?}"
);
}
#[test]
fn root_record_past_region_rejected() {
let schema = user_schema();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_string("name", "ada").unwrap();
b.write_int("age", 36).unwrap();
let bytes = b.finish();
let region = Region::new(0, 4).expect("region"); let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
.expect_err("undersized region must reject the root record");
assert!(
matches!(err, VerifyError::FixedSlotOutOfRegion { ref field, .. } if field == "<root>"),
"got {err:?}"
);
}
#[test]
fn list_entry_pointer_out_of_range_rejected() {
let schema = Schema {
name: "Tags".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("tags", list(TypeRepr::String))],
};
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_list_string("tags", &["a", "bb"]).unwrap();
let mut bytes = b.finish();
let header = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
let off0_pos = header + 4;
let bogus = (bytes.len() as u32 + 5000).to_le_bytes();
bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
let region = full_region(&bytes);
let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
.expect_err("bogus list entry pointer must reject");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn buffer_shorter_than_region_rejected() {
let schema = user_schema();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let bytes = vec![0u8; 4];
let region = Region::new(0, 64).expect("region");
let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
.expect_err("region beyond slice must reject");
assert!(matches!(
err,
VerifyError::BufferShorterThanRegion { have: 4, end: 64 }
));
}
#[test]
fn degenerate_region_rejected() {
let err = Region::new(10, 4).expect_err("inverted region");
assert!(matches!(
err,
VerifyError::DegenerateRegion { start: 10, end: 4 }
));
}
fn list_list_int_buffer() -> (Vec<u8>, Option<ListElementKind>, usize) {
let schema = Schema {
name: "Ret".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("value", list(list(TypeRepr::Int)))],
};
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
let rows: Vec<crate::value::Value> = vec![
crate::value::Value::List(std::sync::Arc::new(vec![
crate::value::Value::Int(1),
crate::value::Value::Int(2),
])),
crate::value::Value::List(std::sync::Arc::new(vec![crate::value::Value::Int(3)])),
crate::value::Value::List(std::sync::Arc::new(vec![])),
];
crate::buffer::write_nested_scalar_list(&mut b, "value", &TypeRepr::Int, &rows)
.expect("write nested list");
let bytes = b.finish();
let fo = &layout.fields[0];
let mut slot = [0u8; 4];
slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
let header_off = u32::from_le_bytes(slot) as usize;
(bytes, fo.list_element, header_off)
}
#[test]
fn inplace_list_list_verifies_clean() {
let (bytes, list_element, root) = list_list_int_buffer();
let region = Region::new(0, bytes.len()).expect("region");
verify_value_at(
&bytes,
&list(list(TypeRepr::Int)),
list_element,
root,
region,
)
.expect("a legal in-place List<List<Int>> root must verify");
}
#[test]
fn inplace_corrupt_inner_pointer_rejected() {
let (mut bytes, list_element, root) = list_list_int_buffer();
let off0_pos = root + 4;
let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(
&bytes,
&list(list(TypeRepr::Int)),
list_element,
root,
region,
)
.expect_err("a corrupt inner pointer must be rejected loudly");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_root_outside_region_rejected() {
let (bytes, list_element, root) = list_list_int_buffer();
let region = Region::new(0, root).expect("region"); let err = verify_value_at(
&bytes,
&list(list(TypeRepr::Int)),
list_element,
root,
region,
)
.expect_err("a root outside the region must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
fn list_string_buffer(items: &[&str]) -> (Vec<u8>, Option<ListElementKind>, usize) {
let schema = Schema {
name: "Ret".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("value", list(TypeRepr::String))],
};
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
b.write_list_string("value", items).expect("write list str");
let bytes = b.finish();
let fo = &layout.fields[0];
let mut slot = [0u8; 4];
slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
let header_off = u32::from_le_bytes(slot) as usize;
(bytes, fo.list_element, header_off)
}
#[test]
fn inplace_list_string_verifies_clean() {
let multibyte: String = [0x4E2Du32, 0x6587]
.iter()
.map(|c| char::from_u32(*c).unwrap())
.collect();
let items = ["", "x", "abc", multibyte.as_str()];
let (bytes, list_element, root) = list_string_buffer(&items);
let region = Region::new(0, bytes.len()).expect("region");
verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
.expect("a legal in-place List<String> root must verify");
}
#[test]
fn inplace_list_string_corrupt_entry_pointer_rejected() {
let (mut bytes, list_element, root) = list_string_buffer(&["a", "bb"]);
let off0_pos = root + 4;
let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
.expect_err("a corrupt entry pointer must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_list_string_overlong_str_len_rejected() {
let (mut bytes, list_element, root) = list_string_buffer(&["ada", "bob"]);
let mut o0 = [0u8; 4];
o0.copy_from_slice(&bytes[root + 4..root + 8]);
let rec0 = u32::from_le_bytes(o0) as usize;
bytes[rec0..rec0 + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
.expect_err("an overlong String len must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion {
what: "string payload",
..
}
),
"got {err:?}"
);
}
#[test]
fn inplace_list_string_outer_len_lies_rejected() {
let (mut bytes, list_element, root) = list_string_buffer(&["a"]);
bytes[root..root + 4].copy_from_slice(&9999u32.to_le_bytes());
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
.expect_err("a lying outer len must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_list_string_root_outside_region_rejected() {
let (bytes, list_element, root) = list_string_buffer(&["a", "b"]);
let region = Region::new(0, root).expect("region"); let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
.expect_err("a root outside the region must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
fn cfg_schema() -> Schema {
Schema {
name: "Cfg".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field("flag", TypeRepr::Bool),
field("name", TypeRepr::String),
field("port", TypeRepr::Int),
field("tags", list(TypeRepr::String)),
field("nums", list(TypeRepr::Int)),
],
}
}
fn list_cfg_buffer(n: usize) -> (Vec<u8>, Option<ListElementKind>, usize) {
let cfg = cfg_schema();
let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
let ret = Schema {
name: "Ret".into(),
generics: vec![],
is_tuple: false,
fields: vec![field(
"value",
list(TypeRepr::Schema {
schema: Box::new(cfg.clone()),
}),
)],
};
let ret_layout = SchemaLayout::offsets_for(&ret).expect("ret layout");
let mut b = BufferBuilder::new(&ret_layout, &ret.fields);
let mut writer = b
.list_record_writer("value", &cfg_layout, &cfg)
.expect("list_record_writer");
for i in 0..n {
let mut child = writer.start_entry();
child.write_bool("flag", i % 2 == 0).unwrap();
child.write_string("name", &format!("cfg-{i}")).unwrap();
child.write_int("port", (1000 + i) as i64).unwrap();
child.write_list_string("tags", &["a", "", "bb"]).unwrap();
child.write_list_int("nums", &[i as i64, -1, 7]).unwrap();
writer.finish_entry(&mut b, child).expect("finish entry");
}
b.finish_list_record(writer).expect("finish list");
let bytes = b.finish();
let fo = &ret_layout.fields[0];
let mut slot = [0u8; 4];
slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
let header_off = u32::from_le_bytes(slot) as usize;
(bytes, fo.list_element, header_off)
}
fn list_cfg_ty() -> TypeRepr {
list(TypeRepr::Schema {
schema: Box::new(cfg_schema()),
})
}
#[test]
fn inplace_list_schema_verifies_clean() {
let (bytes, list_element, root) = list_cfg_buffer(3);
let region = Region::new(0, bytes.len()).expect("region");
verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect("a legal in-place List<Schema> root must verify to the field-pointer layer");
}
#[test]
fn inplace_list_schema_empty_verifies_clean() {
let (bytes, list_element, root) = list_cfg_buffer(0);
let region = Region::new(0, bytes.len()).expect("region");
verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect("an empty in-place List<Schema> must verify");
}
#[test]
fn inplace_list_schema_corrupt_entry_pointer_rejected() {
let (mut bytes, list_element, root) = list_cfg_buffer(2);
let off0_pos = root + 4;
let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect_err("a corrupt sub-record pointer must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion { .. } | VerifyError::FixedSlotOutOfRegion { .. }
),
"got {err:?}"
);
}
#[test]
fn inplace_list_schema_corrupt_field_string_pointer_rejected() {
let (mut bytes, list_element, root) = list_cfg_buffer(1);
let mut o0 = [0u8; 4];
o0.copy_from_slice(&bytes[root + 4..root + 8]);
let sub_base = u32::from_le_bytes(o0) as usize;
let cfg = cfg_schema();
let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
let name_fo = cfg_layout
.fields
.iter()
.find(|fo| fo.name == "name")
.expect("name field");
let name_slot = sub_base + name_fo.offset;
let bogus = (bytes.len() as u32 + 8192).to_le_bytes();
bytes[name_slot..name_slot + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect_err("a corrupt sub-record String field pointer must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_list_schema_corrupt_field_list_pointer_rejected() {
let (mut bytes, list_element, root) = list_cfg_buffer(1);
let mut o0 = [0u8; 4];
o0.copy_from_slice(&bytes[root + 4..root + 8]);
let sub_base = u32::from_le_bytes(o0) as usize;
let cfg = cfg_schema();
let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
let tags_fo = cfg_layout
.fields
.iter()
.find(|fo| fo.name == "tags")
.expect("tags field");
let tags_slot = sub_base + tags_fo.offset;
let bogus = (bytes.len() as u32 + 8192).to_le_bytes();
bytes[tags_slot..tags_slot + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect_err("a corrupt sub-record List field pointer must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_list_schema_outer_len_lies_rejected() {
let (mut bytes, list_element, root) = list_cfg_buffer(1);
bytes[root..root + 4].copy_from_slice(&9999u32.to_le_bytes());
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect_err("a lying outer len must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion { .. } | VerifyError::FixedSlotOutOfRegion { .. }
),
"got {err:?}"
);
}
#[test]
fn inplace_list_schema_root_outside_region_rejected() {
let (bytes, list_element, root) = list_cfg_buffer(2);
let region = Region::new(0, root).expect("region"); let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
.expect_err("a root outside the region must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
fn list_str(items: &[&str]) -> crate::value::Value {
crate::value::Value::List(std::sync::Arc::new(
items
.iter()
.map(|s| crate::value::Value::String((*s).into()))
.collect(),
))
}
fn list_list_string_buffer() -> (Vec<u8>, Option<ListElementKind>, usize) {
let schema = Schema {
name: "Ret".into(),
generics: vec![],
is_tuple: false,
fields: vec![field("value", list(list(TypeRepr::String)))],
};
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let mut b = BufferBuilder::new(&layout, &schema.fields);
let rows = vec![list_str(&["a", "", "bb"]), list_str(&[]), list_str(&["zz"])];
crate::buffer::write_nested_pointer_array_list(&mut b, "value", &TypeRepr::String, &rows)
.expect("write nested pointer-array list");
let bytes = b.finish();
let fo = &layout.fields[0];
let mut slot = [0u8; 4];
slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
let header_off = u32::from_le_bytes(slot) as usize;
(bytes, fo.list_element, header_off)
}
fn list_list_string_ty() -> TypeRepr {
list(list(TypeRepr::String))
}
#[test]
fn inplace_list_list_string_verifies_clean() {
let (bytes, le, root) = list_list_string_buffer();
let region = Region::new(0, bytes.len()).expect("region");
verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
.expect("a legal List<List<String>> must verify to the innermost String");
}
#[test]
fn inplace_list_list_string_corrupt_outer_entry_rejected() {
let (mut bytes, le, root) = list_list_string_buffer();
let off0 = root + 4;
let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
bytes[off0..off0 + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
.expect_err("a corrupt outer entry must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_list_list_string_corrupt_inner_entry_rejected() {
let (mut bytes, le, root) = list_list_string_buffer();
let mut o0 = [0u8; 4];
o0.copy_from_slice(&bytes[root + 4..root + 8]);
let inner_header = u32::from_le_bytes(o0) as usize;
let inner_off0 = inner_header + 4;
let bogus = (bytes.len() as u32 + 8192).to_le_bytes();
bytes[inner_off0..inner_off0 + 4].copy_from_slice(&bogus);
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
.expect_err("a corrupt inner entry must be rejected");
assert!(
matches!(err, VerifyError::OutOfRegion { .. }),
"got {err:?}"
);
}
#[test]
fn inplace_list_list_string_overlong_innermost_str_rejected() {
let (mut bytes, le, root) = list_list_string_buffer();
let mut o0 = [0u8; 4];
o0.copy_from_slice(&bytes[root + 4..root + 8]);
let inner_header = u32::from_le_bytes(o0) as usize;
let mut i0 = [0u8; 4];
i0.copy_from_slice(&bytes[inner_header + 4..inner_header + 8]);
let str_rec = u32::from_le_bytes(i0) as usize;
bytes[str_rec..str_rec + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
let region = Region::new(0, bytes.len()).expect("region");
let err = verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
.expect_err("an overlong innermost String must be rejected");
assert!(
matches!(
err,
VerifyError::OutOfRegion {
what: "string payload",
..
}
),
"got {err:?}"
);
}
fn arena_with_regions(
in_bytes: &[u8],
out_bytes: &[u8],
) -> (Vec<u8>, MultiRegion, usize, usize) {
let const_len = 16usize;
let in_start = const_len;
let in_len = in_bytes.len().max(4);
let in_end = in_start + in_len;
let out_start = in_end + 8;
let out_len = out_bytes.len().max(4);
let out_end = out_start + out_len;
let scratch_start = out_end + 8;
let scratch_len = 16usize;
let arena_size = scratch_start + scratch_len;
let mut arena = vec![0u8; arena_size];
arena[in_start..in_start + in_bytes.len()].copy_from_slice(in_bytes);
arena[out_start..out_start + out_bytes.len()].copy_from_slice(out_bytes);
let multi = MultiRegion::new(
(0, const_len),
(in_start, in_end),
(out_start, out_end),
(scratch_start, arena_size),
)
.expect("multi region");
(arena, multi, in_start, out_start)
}
fn string_record(s: &str) -> Vec<u8> {
let mut v = Vec::with_capacity(4 + s.len());
v.extend_from_slice(&(s.len() as u32).to_le_bytes());
v.extend_from_slice(s.as_bytes());
v
}
fn cross_region_cfg() -> Schema {
Schema {
name: "Cfg".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field("name", TypeRepr::String),
field("port", TypeRepr::Int),
],
}
}
#[allow(clippy::type_complexity)]
fn cross_region_cfg_arena() -> (Vec<u8>, MultiRegion, OffsetTable, Schema, usize, usize) {
let schema = cross_region_cfg();
let layout = SchemaLayout::offsets_for(&schema).expect("cfg layout");
let in_bytes = string_record("ada");
let out_bytes = vec![0u8; layout.root_size];
let (mut arena, multi, in_start, out_start) = arena_with_regions(&in_bytes, &out_bytes);
let name_fo = layout
.fields
.iter()
.find(|fo| fo.name == "name")
.expect("name fo");
let port_fo = layout
.fields
.iter()
.find(|fo| fo.name == "port")
.expect("port fo");
let name_string_abs = in_start;
let name_slot = out_start + name_fo.offset;
arena[name_slot..name_slot + 4].copy_from_slice(&(name_string_abs as u32).to_le_bytes());
let port_slot = out_start + port_fo.offset;
arena[port_slot..port_slot + 8].copy_from_slice(&8080i64.to_le_bytes());
(arena, multi, layout, schema, out_start, name_string_abs)
}
#[test]
fn multi_region_cross_region_record_verifies_clean() {
let (arena, multi, layout, schema, base, _) = cross_region_cfg_arena();
verify_record_multi(&arena, &layout, &schema.fields, base, multi)
.expect("a legal cross-region record must verify under the multi-region map");
}
#[test]
fn multi_region_pointer_to_no_region_rejected() {
let (mut arena, multi, layout, schema, base, _) = cross_region_cfg_arena();
let name_fo = layout.fields.iter().find(|fo| fo.name == "name").unwrap();
let name_slot = base + name_fo.offset;
let in_start = 16usize;
let in_record_len = string_record("ada").len();
let in_len = in_record_len.max(4);
let gap_off = in_start + in_len + 1; arena[name_slot..name_slot + 4].copy_from_slice(&(gap_off as u32).to_le_bytes());
let err = verify_record_multi(&arena, &layout, &schema.fields, base, multi)
.expect_err("a pointer into the inter-region gap must be rejected");
assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
}
#[test]
fn multi_region_pointer_payload_runs_off_region_rejected() {
let (mut arena, multi, layout, schema, base, name_abs) = cross_region_cfg_arena();
arena[name_abs..name_abs + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
let err = verify_record_multi(&arena, &layout, &schema.fields, base, multi)
.expect_err("an overlong String payload escaping its region must be rejected");
assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
}
#[test]
fn multi_region_record_head_in_no_region_rejected() {
let (arena, multi, layout, schema, _, _) = cross_region_cfg_arena();
let in_start = 16usize;
let in_len = string_record("ada").len().max(4);
let gap_base = in_start + in_len + 1;
let err = verify_record_multi(&arena, &layout, &schema.fields, gap_base, multi)
.expect_err("a record head fitting no region must be rejected");
assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
}
#[allow(clippy::type_complexity)]
fn cross_region_dict_of_list_cfg() -> (Vec<u8>, MultiRegion, OffsetTable, Schema, usize) {
let cfg = cross_region_cfg();
let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
let outer = Schema {
name: "Out".into(),
generics: vec![],
is_tuple: false,
fields: vec![
field(
"servers",
list(TypeRepr::Schema {
schema: Box::new(cfg.clone()),
}),
),
field("n", TypeRepr::Int),
],
};
let outer_layout = SchemaLayout::offsets_for(&outer).expect("outer layout");
let two = 2u32;
let header_rel = 0usize;
let entries_rel = header_rel + 4; let cfg0_rel = entries_rel + 8;
let cfg1_rel = cfg0_rel + cfg_layout.root_size;
let name0_rec = string_record("alpha");
let name1_rec = string_record("beta");
let name0_rel = cfg1_rel + cfg_layout.root_size;
let name1_rel = name0_rel + name0_rec.len();
let in_len = name1_rel + name1_rec.len();
let name_fo = cfg_layout.fields.iter().find(|f| f.name == "name").unwrap();
let port_fo = cfg_layout.fields.iter().find(|f| f.name == "port").unwrap();
let in_placeholder = vec![0u8; in_len];
let out_placeholder = vec![0u8; outer_layout.root_size];
let (mut arena, multi, in_start, out_start) =
arena_with_regions(&in_placeholder, &out_placeholder);
let put_u32 = |arena: &mut [u8], abs: usize, v: u32| {
arena[abs..abs + 4].copy_from_slice(&v.to_le_bytes());
};
let put_i64 = |arena: &mut [u8], abs: usize, v: i64| {
arena[abs..abs + 8].copy_from_slice(&v.to_le_bytes());
};
put_u32(&mut arena, in_start + header_rel, two);
put_u32(
&mut arena,
in_start + entries_rel,
(in_start + cfg0_rel) as u32,
);
put_u32(
&mut arena,
in_start + entries_rel + 4,
(in_start + cfg1_rel) as u32,
);
put_u32(
&mut arena,
in_start + cfg0_rel + name_fo.offset,
(in_start + name0_rel) as u32,
);
put_i64(&mut arena, in_start + cfg0_rel + port_fo.offset, 1);
put_u32(
&mut arena,
in_start + cfg1_rel + name_fo.offset,
(in_start + name1_rel) as u32,
);
put_i64(&mut arena, in_start + cfg1_rel + port_fo.offset, 2);
arena[in_start + name0_rel..in_start + name0_rel + name0_rec.len()]
.copy_from_slice(&name0_rec);
arena[in_start + name1_rel..in_start + name1_rel + name1_rec.len()]
.copy_from_slice(&name1_rec);
let servers_fo = outer_layout
.fields
.iter()
.find(|f| f.name == "servers")
.unwrap();
let n_fo = outer_layout.fields.iter().find(|f| f.name == "n").unwrap();
put_u32(
&mut arena,
out_start + servers_fo.offset,
(in_start + header_rel) as u32,
);
put_i64(&mut arena, out_start + n_fo.offset, 42);
(arena, multi, outer_layout, outer, out_start)
}
#[test]
fn multi_region_dict_of_list_cfg_verifies_clean() {
let (arena, multi, layout, schema, base) = cross_region_dict_of_list_cfg();
verify_record_multi(&arena, &layout, &schema.fields, base, multi).expect(
"a legal cross-region Dict { servers: List<Cfg>, n: Int } must verify multi-region",
);
}
#[test]
fn multi_region_dict_of_list_cfg_corrupt_subrecord_field_rejected() {
let (mut arena, multi, layout, schema, base) = cross_region_dict_of_list_cfg();
let cfg = cross_region_cfg();
let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
let name_fo = cfg_layout.fields.iter().find(|f| f.name == "name").unwrap();
let in_start = 16usize;
let entries_rel = 4usize;
let cfg0_rel = entries_rel + 8;
let name_slot = in_start + cfg0_rel + name_fo.offset;
let bogus = (arena.len() as u32) + 4096;
arena[name_slot..name_slot + 4].copy_from_slice(&bogus.to_le_bytes());
let err = verify_record_multi(&arena, &layout, &schema.fields, base, multi)
.expect_err("a corrupt cross-region sub-record field pointer must be rejected");
assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
}
#[test]
fn multi_region_buffer_shorter_than_regions_rejected() {
let multi = MultiRegion::new((0, 8), (8, 16), (16, 64), (64, 80)).expect("multi");
let schema = cross_region_cfg();
let layout = SchemaLayout::offsets_for(&schema).expect("layout");
let short = vec![0u8; 16];
let err = verify_record_multi(&short, &layout, &schema.fields, 16, multi)
.expect_err("a slice shorter than the region span must be rejected");
assert!(
matches!(
err,
VerifyError::BufferShorterThanRegion { have: 16, end: 80 }
),
"got {err:?}"
);
}
}