use crate::jit::trace_types::{ExitTag, FrameMaterializeInfo, TagResKind};
pub const AOT_META_MAGIC: u32 = 0xAA77_0001;
pub const AOT_META_VERSION: u32 = 3;
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct AotTraceMetaHeader {
pub magic: u32,
pub version: u32,
pub head_pc: u32,
pub n_ops: u32,
pub window_size: u32,
pub dispatchable: u8,
pub tag_res_kind: u8,
pub entry_tags_len: u16,
pub exit_tags_len: u32,
}
impl AotTraceMetaHeader {
pub const SIZE: usize = 28;
}
pub fn pack_exit_tag(t: ExitTag) -> u8 {
match t {
ExitTag::Untouched => 0,
ExitTag::Int => 1,
ExitTag::Float => 2,
ExitTag::Table => 3,
ExitTag::Closure => 4,
ExitTag::Nil => 5,
ExitTag::Str => 6,
}
}
pub fn unpack_exit_tag(b: u8) -> Option<ExitTag> {
match b {
0 => Some(ExitTag::Untouched),
1 => Some(ExitTag::Int),
2 => Some(ExitTag::Float),
3 => Some(ExitTag::Table),
4 => Some(ExitTag::Closure),
5 => Some(ExitTag::Nil),
6 => Some(ExitTag::Str),
_ => None,
}
}
pub fn pack_tag_res_kind(k: TagResKind) -> u8 {
match k {
TagResKind::AllUntouched => 0,
TagResKind::AllInt => 1,
TagResKind::Mixed => 2,
}
}
pub fn unpack_tag_res_kind(b: u8) -> Option<TagResKind> {
match b {
0 => Some(TagResKind::AllUntouched),
1 => Some(TagResKind::AllInt),
2 => Some(TagResKind::Mixed),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct PerExitTagsEntry {
pub cont_pc: u32,
pub tags_packed: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct PerExitInlineEntry {
pub cont_pc: u32,
pub head_resume_pc: u32,
pub tags_packed: Vec<u8>,
pub chain_bytes: Vec<u8>,
}
impl PerExitInlineEntry {
pub const FRAME_MATERIALIZE_INFO_SIZE: usize = 12;
pub fn from_inline_side_exit(src: &crate::jit::trace_types::InlineSideExit) -> Self {
let tags_packed: Vec<u8> = src.exit_tags.iter().copied().map(pack_exit_tag).collect();
let n = src.chain.len();
let mut chain_bytes = Vec::with_capacity(n * Self::FRAME_MATERIALIZE_INFO_SIZE);
for fm in src.chain.iter() {
chain_bytes.extend_from_slice(&fm.base_offset.to_le_bytes());
chain_bytes.extend_from_slice(&fm.pc.to_le_bytes());
chain_bytes.extend_from_slice(&fm.nresults.to_le_bytes());
}
PerExitInlineEntry {
cont_pc: src.cont_pc,
head_resume_pc: src.head_resume_pc,
tags_packed,
chain_bytes,
}
}
pub fn rebuild_chain(&self) -> Option<Vec<FrameMaterializeInfo>> {
let unit = Self::FRAME_MATERIALIZE_INFO_SIZE;
if !self.chain_bytes.len().is_multiple_of(unit) {
return None;
}
let n = self.chain_bytes.len() / unit;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let off = i * unit;
let base_offset =
u32::from_le_bytes(self.chain_bytes[off..off + 4].try_into().unwrap());
let pc = u32::from_le_bytes(self.chain_bytes[off + 4..off + 8].try_into().unwrap());
let nresults =
i32::from_le_bytes(self.chain_bytes[off + 8..off + 12].try_into().unwrap());
out.push(FrameMaterializeInfo {
base_offset,
pc,
nresults,
});
}
Some(out)
}
}
pub const FRAME_MATERIALIZE_INFO_WIRE_SIZE_CHECK: () = assert!(
core::mem::size_of::<FrameMaterializeInfo>() == PerExitInlineEntry::FRAME_MATERIALIZE_INFO_SIZE,
"FrameMaterializeInfo wire size drifted — update PerExitInlineEntry::FRAME_MATERIALIZE_INFO_SIZE \
and any deploy-side rebuilders together"
);
pub fn encode_meta_blob(
header: &AotTraceMetaHeader,
entry_tags: &[u8],
exit_tags_packed: &[u8],
per_exit_tags: &[PerExitTagsEntry],
per_exit_inline: &[PerExitInlineEntry],
) -> Vec<u8> {
assert_eq!(entry_tags.len(), header.entry_tags_len as usize);
assert_eq!(exit_tags_packed.len(), header.exit_tags_len as usize);
assert_eq!(header.version, AOT_META_VERSION);
let v2_tail_bytes: usize = 4 + per_exit_tags
.iter()
.map(|e| 4 + 4 + e.tags_packed.len())
.sum::<usize>();
let v3_tail_bytes: usize = 4 + per_exit_inline
.iter()
.map(|e| 4 + 4 + 4 + e.tags_packed.len() + 4 + e.chain_bytes.len())
.sum::<usize>();
let mut out = Vec::with_capacity(
AotTraceMetaHeader::SIZE
+ entry_tags.len()
+ exit_tags_packed.len()
+ v2_tail_bytes
+ v3_tail_bytes,
);
out.extend_from_slice(&header.magic.to_le_bytes());
out.extend_from_slice(&header.version.to_le_bytes());
out.extend_from_slice(&header.head_pc.to_le_bytes());
out.extend_from_slice(&header.n_ops.to_le_bytes());
out.extend_from_slice(&header.window_size.to_le_bytes());
out.push(header.dispatchable);
out.push(header.tag_res_kind);
out.extend_from_slice(&header.entry_tags_len.to_le_bytes());
out.extend_from_slice(&header.exit_tags_len.to_le_bytes());
out.extend_from_slice(entry_tags);
out.extend_from_slice(exit_tags_packed);
out.extend_from_slice(&(per_exit_tags.len() as u32).to_le_bytes());
for ent in per_exit_tags {
out.extend_from_slice(&ent.cont_pc.to_le_bytes());
out.extend_from_slice(&(ent.tags_packed.len() as u32).to_le_bytes());
out.extend_from_slice(&ent.tags_packed);
}
out.extend_from_slice(&(per_exit_inline.len() as u32).to_le_bytes());
for ent in per_exit_inline {
out.extend_from_slice(&ent.cont_pc.to_le_bytes());
out.extend_from_slice(&ent.head_resume_pc.to_le_bytes());
out.extend_from_slice(&(ent.tags_packed.len() as u32).to_le_bytes());
out.extend_from_slice(&ent.tags_packed);
out.extend_from_slice(&(ent.chain_bytes.len() as u32).to_le_bytes());
out.extend_from_slice(&ent.chain_bytes);
}
out
}
#[derive(Debug)]
pub struct DecodedMeta {
pub header: AotTraceMetaHeader,
pub entry_tags: Vec<u8>,
pub exit_tags: Vec<u8>,
pub per_exit_tags: Vec<PerExitTagsEntry>,
pub per_exit_inline: Vec<PerExitInlineEntry>,
}
pub fn decode_meta_blob(bytes: &[u8]) -> Result<DecodedMeta, &'static str> {
if bytes.len() < AotTraceMetaHeader::SIZE {
return Err("blob shorter than header");
}
let magic = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
if magic != AOT_META_MAGIC {
return Err("AOT_META_MAGIC mismatch");
}
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
if version != AOT_META_VERSION {
return Err("AOT_META_VERSION mismatch");
}
let head_pc = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
let n_ops = u32::from_le_bytes(bytes[12..16].try_into().unwrap());
let window_size = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
let dispatchable = bytes[20];
let tag_res_kind = bytes[21];
let entry_tags_len = u16::from_le_bytes(bytes[22..24].try_into().unwrap());
let exit_tags_len = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
let header = AotTraceMetaHeader {
magic,
version,
head_pc,
n_ops,
window_size,
dispatchable,
tag_res_kind,
entry_tags_len,
exit_tags_len,
};
let total_payload = entry_tags_len as usize + exit_tags_len as usize;
if bytes.len() < AotTraceMetaHeader::SIZE + total_payload {
return Err("blob shorter than declared payload");
}
let entry_start = AotTraceMetaHeader::SIZE;
let entry_end = entry_start + entry_tags_len as usize;
let exit_end = entry_end + exit_tags_len as usize;
let entry_tags = bytes[entry_start..entry_end].to_vec();
let exit_tags = bytes[entry_end..exit_end].to_vec();
let mut per_exit_tags: Vec<PerExitTagsEntry> = Vec::new();
let mut cur = exit_end;
if bytes.len() > cur {
if bytes.len() < cur + 4 {
return Err("v2 tail truncated at count");
}
let count = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
per_exit_tags.reserve(count);
for _ in 0..count {
if bytes.len() < cur + 8 {
return Err("v2 tail truncated at entry header");
}
let cont_pc = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap());
cur += 4;
let tags_len = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
if bytes.len() < cur + tags_len {
return Err("v2 tail truncated at entry tags");
}
let tags_packed = bytes[cur..cur + tags_len].to_vec();
cur += tags_len;
per_exit_tags.push(PerExitTagsEntry {
cont_pc,
tags_packed,
});
}
}
let mut per_exit_inline: Vec<PerExitInlineEntry> = Vec::new();
if bytes.len() > cur {
if bytes.len() < cur + 4 {
return Err("v3 tail truncated at count");
}
let count = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
per_exit_inline.reserve(count);
for _ in 0..count {
if bytes.len() < cur + 12 {
return Err("v3 tail truncated at entry header");
}
let cont_pc = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap());
cur += 4;
let head_resume_pc = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap());
cur += 4;
let tags_len = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
if bytes.len() < cur + tags_len {
return Err("v3 tail truncated at entry tags");
}
let tags_packed = bytes[cur..cur + tags_len].to_vec();
cur += tags_len;
if bytes.len() < cur + 4 {
return Err("v3 tail truncated at chain header");
}
let chain_bytes_len =
u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
if bytes.len() < cur + chain_bytes_len {
return Err("v3 tail truncated at chain bytes");
}
if !chain_bytes_len.is_multiple_of(PerExitInlineEntry::FRAME_MATERIALIZE_INFO_SIZE) {
return Err("v3 tail chain_bytes_len not a multiple of FrameMaterializeInfo size");
}
let chain_bytes = bytes[cur..cur + chain_bytes_len].to_vec();
cur += chain_bytes_len;
per_exit_inline.push(PerExitInlineEntry {
cont_pc,
head_resume_pc,
tags_packed,
chain_bytes,
});
}
}
Ok(DecodedMeta {
header,
entry_tags,
exit_tags,
per_exit_tags,
per_exit_inline,
})
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct AotTraceIndexEntry {
pub proto_hash: [u8; 16],
pub head_pc: u32,
pub _pad: u32,
pub fn_ptr: u64,
pub meta_ptr: u64,
pub meta_len: u32,
pub _pad2: u32,
}
impl AotTraceIndexEntry {
pub const SIZE: usize = 48;
}
pub const AOT_TRACE_INDEX_ENTRY_SIZE_CHECK: () = assert!(
core::mem::size_of::<AotTraceIndexEntry>() == AotTraceIndexEntry::SIZE,
"AotTraceIndexEntry must be 48 bytes — alignment / padding regressed"
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_round_trip() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 42,
n_ops: 7,
window_size: 4,
dispatchable: 1,
tag_res_kind: pack_tag_res_kind(TagResKind::AllInt),
entry_tags_len: 2,
exit_tags_len: 3,
};
let entry_tags = vec![1u8, 2u8];
let exit_tags = vec![
pack_exit_tag(ExitTag::Int),
pack_exit_tag(ExitTag::Untouched),
pack_exit_tag(ExitTag::Float),
];
let blob = encode_meta_blob(&header, &entry_tags, &exit_tags, &[], &[]);
assert_eq!(blob.len(), AotTraceMetaHeader::SIZE + 2 + 3 + 4 + 4);
let decoded = decode_meta_blob(&blob).expect("decode");
assert!(decoded.per_exit_tags.is_empty());
assert!(decoded.per_exit_inline.is_empty());
assert_eq!(decoded.header.head_pc, 42);
assert_eq!(decoded.header.window_size, 4);
assert_eq!(decoded.header.dispatchable, 1);
assert_eq!(decoded.entry_tags, entry_tags);
assert_eq!(decoded.exit_tags, exit_tags);
assert_eq!(
unpack_tag_res_kind(decoded.header.tag_res_kind),
Some(TagResKind::AllInt)
);
for (raw, expected) in
decoded
.exit_tags
.iter()
.zip([ExitTag::Int, ExitTag::Untouched, ExitTag::Float])
{
assert_eq!(unpack_exit_tag(*raw), Some(expected));
}
}
#[test]
fn decode_rejects_magic_mismatch() {
let mut blob = vec![0u8; AotTraceMetaHeader::SIZE];
let err = decode_meta_blob(&blob).unwrap_err();
assert!(err.contains("MAGIC"));
blob[..4].copy_from_slice(&AOT_META_MAGIC.to_le_bytes());
let err = decode_meta_blob(&blob).unwrap_err();
assert!(err.contains("VERSION"));
}
#[test]
fn v2_per_exit_tags_round_trip() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 7,
n_ops: 12,
window_size: 5,
dispatchable: 1,
tag_res_kind: pack_tag_res_kind(TagResKind::Mixed),
entry_tags_len: 0,
exit_tags_len: 0,
};
let entries = vec![
PerExitTagsEntry {
cont_pc: 3,
tags_packed: vec![
pack_exit_tag(ExitTag::Int),
pack_exit_tag(ExitTag::Untouched),
],
},
PerExitTagsEntry {
cont_pc: 11,
tags_packed: vec![
pack_exit_tag(ExitTag::Closure),
pack_exit_tag(ExitTag::Table),
pack_exit_tag(ExitTag::Float),
],
},
];
let blob = encode_meta_blob(&header, &[], &[], &entries, &[]);
let decoded = decode_meta_blob(&blob).expect("decode v2");
assert_eq!(decoded.per_exit_tags.len(), 2);
assert_eq!(decoded.per_exit_tags[0].cont_pc, 3);
assert_eq!(decoded.per_exit_tags[0].tags_packed.len(), 2);
assert_eq!(decoded.per_exit_tags[1].cont_pc, 11);
assert_eq!(decoded.per_exit_tags[1].tags_packed.len(), 3);
assert!(decoded.per_exit_inline.is_empty());
}
#[test]
fn v3_per_exit_inline_round_trip() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 0,
n_ops: 0,
window_size: 8,
dispatchable: 1,
tag_res_kind: pack_tag_res_kind(TagResKind::Mixed),
entry_tags_len: 0,
exit_tags_len: 0,
};
let inline = vec![
PerExitInlineEntry {
cont_pc: 5,
head_resume_pc: 9,
tags_packed: vec![pack_exit_tag(ExitTag::Int), pack_exit_tag(ExitTag::Int)],
chain_bytes: {
let mut v = Vec::new();
v.extend_from_slice(&3u32.to_le_bytes());
v.extend_from_slice(&4u32.to_le_bytes());
v.extend_from_slice(&1i32.to_le_bytes());
v
},
},
PerExitInlineEntry {
cont_pc: 17,
head_resume_pc: 21,
tags_packed: vec![
pack_exit_tag(ExitTag::Closure),
pack_exit_tag(ExitTag::Untouched),
pack_exit_tag(ExitTag::Float),
],
chain_bytes: {
let mut v = Vec::new();
for (off, pc, nr) in [(2u32, 7u32, 1i32), (5u32, 11u32, 2i32)] {
v.extend_from_slice(&off.to_le_bytes());
v.extend_from_slice(&pc.to_le_bytes());
v.extend_from_slice(&nr.to_le_bytes());
}
v
},
},
];
let blob = encode_meta_blob(&header, &[], &[], &[], &inline);
let decoded = decode_meta_blob(&blob).expect("decode v3");
assert_eq!(decoded.per_exit_inline.len(), 2);
assert_eq!(decoded.per_exit_inline[0].cont_pc, 5);
assert_eq!(decoded.per_exit_inline[0].head_resume_pc, 9);
assert_eq!(decoded.per_exit_inline[0].tags_packed.len(), 2);
let chain0 = decoded.per_exit_inline[0]
.rebuild_chain()
.expect("rebuild chain[0]");
assert_eq!(chain0.len(), 1);
assert_eq!(chain0[0].base_offset, 3);
assert_eq!(chain0[0].pc, 4);
assert_eq!(chain0[0].nresults, 1);
let chain1 = decoded.per_exit_inline[1]
.rebuild_chain()
.expect("rebuild chain[1]");
assert_eq!(chain1.len(), 2);
assert_eq!(chain1[0].base_offset, 2);
assert_eq!(chain1[1].pc, 11);
assert_eq!(chain1[1].nresults, 2);
}
#[test]
fn v3_per_exit_inline_round_trip_from_live() {
use crate::jit::trace_types::{ExitTag, FrameMaterializeInfo, InlineSideExit};
let chain = vec![FrameMaterializeInfo {
base_offset: 1,
pc: 2,
nresults: 3,
}];
let live = InlineSideExit {
cont_pc: 42,
head_resume_pc: 50,
exit_tags: std::rc::Rc::from(
vec![ExitTag::Int, ExitTag::Float, ExitTag::Untouched].into_boxed_slice(),
),
chain: std::rc::Rc::from(chain.into_boxed_slice()),
side_trace_ptr: Box::new(std::cell::Cell::new(std::ptr::null())),
};
let entry = PerExitInlineEntry::from_inline_side_exit(&live);
assert_eq!(entry.cont_pc, 42);
assert_eq!(entry.head_resume_pc, 50);
assert_eq!(entry.tags_packed.len(), 3);
assert_eq!(
entry.chain_bytes.len(),
PerExitInlineEntry::FRAME_MATERIALIZE_INFO_SIZE
);
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 0,
n_ops: 0,
window_size: 4,
dispatchable: 1,
tag_res_kind: pack_tag_res_kind(TagResKind::Mixed),
entry_tags_len: 0,
exit_tags_len: 0,
};
let blob = encode_meta_blob(&header, &[], &[], &[], &[entry]);
let decoded = decode_meta_blob(&blob).expect("decode v3 from live");
assert_eq!(decoded.per_exit_inline.len(), 1);
let rebuilt = decoded.per_exit_inline[0].rebuild_chain().expect("rebuild");
assert_eq!(rebuilt.len(), 1);
assert_eq!(rebuilt[0].base_offset, 1);
assert_eq!(rebuilt[0].pc, 2);
assert_eq!(rebuilt[0].nresults, 3);
}
#[test]
fn decode_rejects_v3_chain_bytes_misaligned() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 0,
n_ops: 0,
window_size: 0,
dispatchable: 0,
tag_res_kind: 0,
entry_tags_len: 0,
exit_tags_len: 0,
};
let mut blob = Vec::new();
blob.extend_from_slice(&header.magic.to_le_bytes());
blob.extend_from_slice(&header.version.to_le_bytes());
blob.extend_from_slice(&header.head_pc.to_le_bytes());
blob.extend_from_slice(&header.n_ops.to_le_bytes());
blob.extend_from_slice(&header.window_size.to_le_bytes());
blob.push(header.dispatchable);
blob.push(header.tag_res_kind);
blob.extend_from_slice(&header.entry_tags_len.to_le_bytes());
blob.extend_from_slice(&header.exit_tags_len.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.extend_from_slice(&7u32.to_le_bytes());
blob.extend(std::iter::repeat_n(0u8, 7));
let err = decode_meta_blob(&blob).unwrap_err();
assert!(
err.contains("FrameMaterializeInfo"),
"expected misalignment err, got {err:?}"
);
}
#[test]
fn decode_tolerates_v1_blob_shape() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 0,
n_ops: 0,
window_size: 0,
dispatchable: 0,
tag_res_kind: 0,
entry_tags_len: 1,
exit_tags_len: 0,
};
let mut blob = Vec::new();
blob.extend_from_slice(&header.magic.to_le_bytes());
blob.extend_from_slice(&header.version.to_le_bytes());
blob.extend_from_slice(&header.head_pc.to_le_bytes());
blob.extend_from_slice(&header.n_ops.to_le_bytes());
blob.extend_from_slice(&header.window_size.to_le_bytes());
blob.push(header.dispatchable);
blob.push(header.tag_res_kind);
blob.extend_from_slice(&header.entry_tags_len.to_le_bytes());
blob.extend_from_slice(&header.exit_tags_len.to_le_bytes());
blob.push(0); let decoded = decode_meta_blob(&blob).expect("decode v1-shaped");
assert!(decoded.per_exit_tags.is_empty());
assert!(decoded.per_exit_inline.is_empty());
}
#[test]
fn decode_tolerates_v2_blob_shape() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 0,
n_ops: 0,
window_size: 0,
dispatchable: 0,
tag_res_kind: 0,
entry_tags_len: 0,
exit_tags_len: 0,
};
let mut blob = Vec::new();
blob.extend_from_slice(&header.magic.to_le_bytes());
blob.extend_from_slice(&header.version.to_le_bytes());
blob.extend_from_slice(&header.head_pc.to_le_bytes());
blob.extend_from_slice(&header.n_ops.to_le_bytes());
blob.extend_from_slice(&header.window_size.to_le_bytes());
blob.push(header.dispatchable);
blob.push(header.tag_res_kind);
blob.extend_from_slice(&header.entry_tags_len.to_le_bytes());
blob.extend_from_slice(&header.exit_tags_len.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.extend_from_slice(&3u32.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.push(pack_exit_tag(ExitTag::Int));
let decoded = decode_meta_blob(&blob).expect("decode v2-shaped");
assert_eq!(decoded.per_exit_tags.len(), 1);
assert!(decoded.per_exit_inline.is_empty());
}
#[test]
fn decode_rejects_truncated() {
let header = AotTraceMetaHeader {
magic: AOT_META_MAGIC,
version: AOT_META_VERSION,
head_pc: 0,
n_ops: 0,
window_size: 0,
dispatchable: 0,
tag_res_kind: 0,
entry_tags_len: 0,
exit_tags_len: 10,
};
let blob = {
let mut b = Vec::new();
b.extend_from_slice(&header.magic.to_le_bytes());
b.extend_from_slice(&header.version.to_le_bytes());
b.extend_from_slice(&header.head_pc.to_le_bytes());
b.extend_from_slice(&header.n_ops.to_le_bytes());
b.extend_from_slice(&header.window_size.to_le_bytes());
b.push(header.dispatchable);
b.push(header.tag_res_kind);
b.extend_from_slice(&header.entry_tags_len.to_le_bytes());
b.extend_from_slice(&header.exit_tags_len.to_le_bytes());
b
};
let err = decode_meta_blob(&blob).unwrap_err();
assert!(err.contains("payload"));
}
}