use crate::grain::bytecode::code::{self, tag};
use crate::grain::bytecode::{Chain, Chunk, Op, Receiver, Root, Step, Switch, Tail};
use crate::grain::format::Caps;
use crate::grain::program::Function;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
#[derive(Debug, Clone)]
pub struct Pools<'a> {
pub consts: usize,
pub names: usize,
pub tokens: usize,
pub assign_ops: usize,
pub residuals: usize,
pub chains: &'a [Chain],
pub switches: &'a [Switch],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyError {
MissingCaps {
artifact: String,
missing: String,
at: usize,
},
Undecodable {
at: usize,
},
TrailingBytes {
at: usize,
len: usize,
},
ChunkOutOfRange {
entry: u32,
end: u32,
len: usize,
},
JumpOutOfRange {
at: usize,
target: u32,
},
JumpIntoAnInstruction {
at: usize,
target: u32,
},
DepthConflict {
at: usize,
expected: usize,
found: usize,
},
Underflow {
at: usize,
need: usize,
have: usize,
},
FallsOffTheEnd,
StackExceedsDeclared {
needed: usize,
declared: u16,
},
IteratorUnderflow {
at: usize,
},
HandlerUnderflow {
at: usize,
},
BadIndex {
at: usize,
what: &'static str,
index: u32,
},
}
pub fn verify(
caps: Caps,
code: &[u8],
functions: &[Function],
chunks: &[Chunk],
pools: &Pools,
) -> Result<Vec<u16>, VerifyError> {
let mut starts = vec![false; code.len() + 1];
let mut at = 0usize;
while at < code.len() {
starts[at] = true;
let width = code::width(code, at).ok_or(VerifyError::Undecodable { at })?;
check_indices(at, code, pools)?;
at += width;
}
if at != code.len() {
return Err(VerifyError::TrailingBytes {
at,
len: code.len(),
});
}
if !functions.is_empty() && !caps.contains(Caps::FUNCTION) {
return Err(VerifyError::MissingCaps {
at: 0,
artifact: caps.to_string(),
missing: Caps::FUNCTION.to_string(),
});
}
chunks
.iter()
.map(|chunk| verify_chunk(caps, code, chunk, &starts, pools))
.collect()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct State {
operands: usize,
iters: usize,
handlers: usize,
}
fn verify_chunk(
caps: Caps,
code: &[u8],
chunk: &Chunk,
starts: &[bool],
pools: &Pools,
) -> Result<u16, VerifyError> {
let (entry, end) = (chunk.entry() as usize, chunk.end() as usize);
if end > code.len() || entry > end {
return Err(VerifyError::ChunkOutOfRange {
entry: chunk.entry(),
end: chunk.end(),
len: code.len(),
});
}
let mut depth_at: Vec<Option<State>> = vec![None; code.len()];
let mut work_list = vec![(entry, State::default())];
let mut high_water = 0usize;
while let Some((at, state)) = work_list.pop() {
if at >= end {
return Err(VerifyError::FallsOffTheEnd);
}
match depth_at[at] {
Some(seen) if seen == state => continue,
Some(seen) => {
return Err(VerifyError::DepthConflict {
at,
expected: seen.operands,
found: state.operands,
})
}
None => depth_at[at] = Some(state),
}
let depth = state.operands;
high_water = high_water.max(depth);
let op = code::decode(code, at).ok_or(VerifyError::Undecodable { at })?;
let required_caps = required_caps(&op, pools);
if !caps.contains(required_caps) {
return Err(VerifyError::MissingCaps {
at,
artifact: caps.to_string(),
missing: (required_caps - caps).to_string(),
});
}
let (requires, pops, pushes) = effect(&op, pools);
if pops > requires {
return Err(VerifyError::Undecodable { at });
}
if depth < requires {
return Err(VerifyError::Underflow {
at,
need: requires,
have: depth,
});
}
let next_state = State {
operands: depth - pops + pushes,
iters: match op {
Op::IterInit => state.iters + 1,
Op::IterDrop => state
.iters
.checked_sub(1)
.ok_or(VerifyError::IteratorUnderflow { at })?,
_ => state.iters,
},
handlers: match op {
Op::PushHandler { .. } => state.handlers + 1,
Op::PopHandler => state
.handlers
.checked_sub(1)
.ok_or(VerifyError::HandlerUnderflow { at })?,
_ => state.handlers,
},
};
let next_depth = next_state.operands;
high_water = high_water.max(next_depth);
let width = code::width(code, at).expect("decoded, so it has a width");
let next = at + width;
let mut go = |target: u32, state: State| -> Result<(), VerifyError> {
let target = target as usize;
if target < entry || target >= end {
return Err(VerifyError::JumpOutOfRange {
at,
target: target as u32,
});
}
if !starts[target] {
return Err(VerifyError::JumpIntoAnInstruction {
at,
target: target as u32,
});
}
work_list.push((target, state));
Ok(())
};
match op {
Op::Return | Op::Throw => {}
Op::Jump(target) => go(target, next_state)?,
Op::PushHandler { target, .. } => {
go(
target,
State {
operands: depth,
iters: state.iters,
handlers: next_state.handlers,
},
)?;
work_list.push((next, next_state));
}
Op::JumpIfFalse { target }
| Op::JumpIfTrue { target }
| Op::SkipIfNotUnit { target } => {
go(target, next_state)?;
work_list.push((next, next_state));
}
Op::IterNext { exit, indexed } => {
go(
exit,
State {
operands: depth,
iters: state
.iters
.checked_sub(1)
.ok_or(VerifyError::IteratorUnderflow { at })?,
handlers: state.handlers,
},
)?;
work_list.push((
next,
State {
operands: depth + 1 + usize::from(indexed),
iters: state.iters,
handlers: state.handlers,
},
));
}
Op::Switch(index) => {
if let Some(table) = pools.switches.get(index as usize) {
for target in table
.cases
.iter()
.map(|case| case.target)
.chain(table.ranges.iter().map(|range| range.target))
.chain(core::iter::once(table.default))
{
go(target, next_state)?;
}
}
}
_ => {
if next >= end {
return Err(VerifyError::FallsOffTheEnd);
}
work_list.push((next, next_state));
}
}
}
let Ok(high_water) = u16::try_from(high_water) else {
return Err(VerifyError::StackExceedsDeclared {
needed: high_water,
declared: chunk.max_stack(),
});
};
if high_water > chunk.max_stack() {
return Err(VerifyError::StackExceedsDeclared {
needed: high_water as usize,
declared: chunk.max_stack(),
});
}
Ok(high_water)
}
fn required_caps(op: &Op, pools: &Pools) -> Caps {
match op {
Op::Chain(index) => match pools.chains.get(*index as usize) {
Some(chain) => {
let mut caps = Caps::empty();
match chain.root {
Root::Local { .. } | Root::Named { .. } | Root::Temporary => {}
Root::This { .. } => caps.insert(Caps::THIS),
}
chain.steps.iter().for_each(|step| match step {
Step::Index { .. } => caps.insert(Caps::INDEXING),
Step::Property { .. } => caps.insert(Caps::PROPERTY),
Step::Method { .. } => caps.insert(Caps::METHOD),
});
caps
}
None => Caps::empty(),
},
Op::Const(..)
| Op::Unit
| Op::Bool(..)
| Op::LoadLocal(..)
| Op::LoadNamed(..)
| Op::StoreLocal { .. }
| Op::DeclareLocal { .. }
| Op::Pop
| Op::AssignLocal { .. }
| Op::AssignNamed { .. }
| Op::JumpIfFalse { .. }
| Op::JumpIfTrue { .. }
| Op::Switch(..)
| Op::Jump(..)
| Op::UnwindTo(..)
| Op::Tick
| Op::Checkpoint
| Op::PushHandler { .. }
| Op::PopHandler
| Op::SkipIfNotUnit { .. }
| Op::Call { .. }
| Op::Rotate(..)
| Op::CheckSize { .. }
| Op::InterpolateStart
| Op::InterpolateAppend
| Op::InterpolateEnd
| Op::Throw
| Op::IterInit
| Op::IterNext { .. }
| Op::IterDrop
| Op::Return
| Op::LoadShared(..)
| Op::LoadSharedNamed(..)
| Op::StoreShared(..)
| Op::Statement { .. } => Caps::empty(),
Op::MakeFnPtr | Op::MakeClosure(..) | Op::CallFnPtr { .. } => Caps::FN_PTR,
Op::Curry(..) => Caps::FN_PTR | Caps::CURRYING,
Op::EvalAst { .. } => Caps::empty(),
Op::Share(..) | Op::ShareNamed(..) => Caps::SHARING,
Op::RequireThis | Op::LoadThis | Op::LoadThisShared | Op::AssignThis { .. } => Caps::THIS,
Op::CallRef { receiver, .. } => match receiver {
Receiver::Local(..) | Receiver::Named(..) => Caps::empty(),
Receiver::This => Caps::THIS,
},
Op::MakeArray(..) => Caps::ARRAY,
Op::MakeMap(..) => Caps::MAP,
Op::IsShared => Caps::SHARING,
}
}
fn effect(op: &Op, pools: &Pools) -> (usize, usize, usize) {
match op {
Op::Chain(index) => match pools.chains.get(*index as usize) {
Some(chain) => {
let consumes = chain.consumes();
(consumes, consumes, 1)
}
None => (0, 0, 1),
},
Op::Const(..)
| Op::Unit
| Op::Bool(..)
| Op::LoadLocal(..)
| Op::LoadNamed(..)
| Op::LoadShared(..)
| Op::LoadSharedNamed(..)
| Op::MakeClosure(..)
| Op::LoadThis
| Op::LoadThisShared
| Op::EvalAst { .. } => (0, 0, 1),
Op::RequireThis => (0, 0, 0),
Op::Share(..) | Op::ShareNamed(..) => (0, 0, 0),
Op::StoreLocal { .. } | Op::DeclareLocal { .. } | Op::Pop => (1, 1, 0),
Op::AssignLocal { .. } | Op::AssignNamed { .. } | Op::AssignThis { .. } => (1, 1, 0),
Op::JumpIfFalse { .. } | Op::JumpIfTrue { .. } | Op::Switch(..) => (1, 1, 0),
Op::Jump(..)
| Op::UnwindTo(..)
| Op::Tick
| Op::Checkpoint
| Op::Statement { .. }
| Op::PushHandler { .. }
| Op::PopHandler => (0, 0, 0),
Op::SkipIfNotUnit { .. } => (1, 0, 0),
Op::Call { argc, .. } => (*argc as usize, *argc as usize, 1),
Op::CallRef { argc, receiver, .. } => match receiver {
Receiver::Local(..) => {
let len = (*argc as usize).saturating_sub(1);
(len, len, 1)
}
Receiver::Named(..) | Receiver::This => (*argc as usize, *argc as usize, 1),
},
Op::Rotate(under) => (
*under as usize + 1,
*under as usize + 1,
*under as usize + 1,
),
Op::MakeArray(len) => (*len as usize, *len as usize, 1),
Op::MakeMap(len) => {
let len = 2 * *len as usize + 1;
(len, len, 1)
}
Op::CheckSize { .. } => (1, 1, 1),
Op::MakeFnPtr | Op::IsShared => (1, 1, 1),
Op::Curry(argc) => (*argc as usize + 1, *argc as usize + 1, 1),
Op::CallFnPtr { argc, .. } => (*argc as usize + 1, *argc as usize + 1, 1),
Op::InterpolateStart => (0, 0, 1),
Op::InterpolateAppend => (1, 1, 0),
Op::InterpolateEnd => (1, 1, 1),
Op::Throw | Op::StoreShared(..) => (1, 1, 0),
Op::IterInit => (1, 1, 0),
Op::IterNext { .. } | Op::IterDrop => (0, 0, 0),
Op::Return => (0, 0, 0),
}
}
fn check_indices(at: usize, code: &[u8], pools: &Pools) -> Result<(), VerifyError> {
let index = |offset: usize| code::u16_at(code, at + offset).map_or(0, u32::from);
let bounded = |index: u32, what: &'static str, len: usize| {
if index as usize >= len {
Err(VerifyError::BadIndex { at, what, index })
} else {
Ok(())
}
};
match code[at] {
tag::CONST => bounded(index(1), "constant", pools.consts),
tag::DECLARE_LOCAL | tag::DECLARE_CONST => bounded(index(1), "name", pools.names),
tag::CALL
| tag::CALL_CAPTURE
| tag::CALL_LOCAL_REF
| tag::CALL_LOCAL_REF_CAPTURE
| tag::CALL_THIS_REF
| tag::CALL_THIS_REF_CAPTURE => bounded(index(1), "name", pools.names),
tag::CALL_NAMED_REF | tag::CALL_NAMED_REF_CAPTURE => {
bounded(index(1), "name", pools.names)?;
bounded(index(4), "name", pools.names)
}
tag::CALL_OP => {
bounded(index(1), "name", pools.names)?;
bounded(index(4), "operator", pools.tokens)
}
tag::ASSIGN_LOCAL => bounded(index(3), "name", pools.names),
tag::LOAD_NAMED
| tag::LOAD_SHARED_NAMED
| tag::ASSIGN_NAMED
| tag::SHARE_NAMED
| tag::MAKE_CLOSURE => bounded(index(1), "name", pools.names),
tag::ASSIGN_NAMED_OP => {
bounded(index(1), "name", pools.names)?;
bounded(index(3), "op-assignment", pools.assign_ops)
}
tag::ASSIGN_LOCAL_OP => {
bounded(index(3), "name", pools.names)?;
bounded(index(5), "op-assignment", pools.assign_ops)
}
tag::ASSIGN_THIS_OP => bounded(index(1), "op-assignment", pools.assign_ops),
tag::CALL_FN_PTR_ON_NAMED => bounded(index(2), "name", pools.names),
tag::EVAL_AST | tag::EVAL_AST_KEEP => bounded(index(1), "fragment", pools.residuals),
tag::CHAIN => {
bounded(index(1), "chain", pools.chains.len())?;
check_chain_indices(at, &pools.chains[index(1) as usize], pools)
}
tag::SWITCH => bounded(index(1), "switch", pools.switches.len()),
_ => Ok(()),
}
}
fn check_chain_indices(at: usize, chain: &Chain, pools: &Pools) -> Result<(), VerifyError> {
let bounded = |index: u32, what: &'static str, len: usize| {
if index as usize >= len {
Err(VerifyError::BadIndex { at, what, index })
} else {
Ok(())
}
};
match chain.root {
Root::Local { name, .. } | Root::Named { name, .. } => {
bounded(name, "name", pools.names)?;
}
Root::This { .. } | Root::Temporary => {}
}
for step in &chain.steps {
match step {
Step::Index { .. } => {}
Step::Property {
name,
getter,
setter,
..
} => {
bounded(*name, "name", pools.names)?;
bounded(*getter, "name", pools.names)?;
bounded(*setter, "name", pools.names)?;
}
Step::Method { name, .. } => bounded(*name, "name", pools.names)?,
}
}
match chain.tail {
Tail::Assign { op: Some(op) } => bounded(op, "op-assignment", pools.assign_ops),
Tail::Assign { op: None } | Tail::Read => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grain::bytecode::assemble;
use crate::grain::format::Abi;
fn pools() -> Pools<'static> {
Pools {
consts: 0,
names: 0,
tokens: 0,
assign_ops: 0,
residuals: 0,
chains: &[],
switches: &[],
}
}
fn check(ops: Vec<Op>) -> Result<Vec<u16>, VerifyError> {
let (code, _) = assemble(&ops).expect("the test ops must assemble");
let chunk = Chunk::new(0, code.len() as u32, 8);
verify(Abi::host().caps, &code, &[], &[chunk], &pools())
}
fn check_bytes(code: Vec<u8>, max_stack: u16) -> Result<Vec<u16>, VerifyError> {
let chunk = Chunk::new(0, code.len() as u32, max_stack);
verify(Abi::host().caps, &code, &[], &[chunk], &pools())
}
#[test]
fn accepts_a_well_formed_chunk() {
assert_eq!(check(vec![Op::Unit, Op::Return]), Ok(vec![1]));
}
#[test]
#[cfg(not(feature = "no_function"))]
fn the_this_register_is_reached_without_touching_the_scope() {
assert_eq!(check(vec![Op::LoadThis, Op::Return]), Ok(vec![1]));
assert_eq!(check(vec![Op::LoadThisShared, Op::Return]), Ok(vec![1]));
assert_eq!(
check(vec![Op::RequireThis, Op::Unit, Op::Return]),
Ok(vec![1])
);
assert_eq!(
check(vec![
Op::RequireThis,
Op::Unit,
Op::AssignThis { op: None },
Op::Unit,
Op::Return
]),
Ok(vec![1])
);
}
#[test]
fn rejects_an_op_assignment_to_this_that_the_pool_does_not_have() {
let ops = vec![
Op::Unit,
Op::AssignThis { op: Some(0) },
Op::Unit,
Op::Return,
];
assert_eq!(
check(ops),
Err(VerifyError::BadIndex {
at: 1,
what: "op-assignment",
index: 0,
})
);
}
#[test]
fn rejects_a_chain_that_names_something_the_pools_do_not_have() {
let chain = |root, steps, tail| Chain {
root,
steps,
tail,
operands: 0,
};
let property = |name| Step::Property {
name,
getter: 0,
setter: 0,
flags: Default::default(),
pos: rhai::Position::NONE,
};
let past_the_end = [
chain(
Root::Named {
name: 3,
pos: rhai::Position::NONE,
},
vec![],
Tail::Read,
),
chain(Root::Local { slot: 0, name: 3 }, vec![], Tail::Read),
chain(Root::Temporary, vec![property(3)], Tail::Read),
chain(
Root::Temporary,
vec![Step::Method {
name: 3,
argc: 0,
operand: 0,
flags: Default::default(),
pos: rhai::Position::NONE,
}],
Tail::Read,
),
];
for chain in past_the_end {
let temporary = chain.roots_on_stack();
let mut ops = vec![Op::Chain(0), Op::Return];
if temporary {
ops.insert(0, Op::Unit);
}
let (code, _) = assemble(&ops).expect("must assemble");
let chunk = Chunk::new(0, code.len() as u32, 8);
let pools = Pools {
names: 1,
chains: core::slice::from_ref(&chain),
..pools()
};
assert!(
matches!(
verify(Abi::host().caps, &code, &[], &[chunk], &pools),
Err(VerifyError::BadIndex {
what: "name",
index: 3,
..
}),
),
"{chain:?} names name 3 of 1 and must be refused",
);
}
let assigning = chain(
Root::Local { slot: 0, name: 0 },
vec![],
Tail::Assign { op: Some(2) },
);
let (code, _) = assemble(&[Op::Unit, Op::Chain(0), Op::Return]).expect("must assemble");
let chunk = Chunk::new(0, code.len() as u32, 8);
assert!(matches!(
verify(
Abi::host().caps,
&code,
&[],
&[chunk],
&Pools {
names: 1,
chains: core::slice::from_ref(&assigning),
..pools()
},
),
Err(VerifyError::BadIndex {
what: "op-assignment",
index: 2,
..
}),
));
}
#[test]
fn rejects_a_jump_from_one_chunk_into_another() {
let (mut code, _) = assemble(&[Op::Unit, Op::Return]).unwrap();
let boundary = code.len() as u32;
code.push(tag::JUMP);
code.extend_from_slice(&0u32.to_le_bytes()); code.push(tag::RETURN);
let chunks = [
Chunk::new(0, boundary, 8),
Chunk::new(boundary, code.len() as u32, 8),
];
assert!(matches!(
verify(Abi::host().caps, &code, &[], &chunks, &pools()),
Err(VerifyError::JumpOutOfRange { .. }),
));
}
#[test]
fn the_high_water_is_what_the_chunk_uses_not_what_it_declares() {
assert_eq!(
check(vec![
Op::Unit,
Op::Unit,
Op::Pop,
Op::Pop,
Op::Unit,
Op::Return
]),
Ok(vec![2]),
);
}
#[test]
fn rejects_branches_that_disagree_on_depth() {
let ops = vec![
Op::Bool(true),
Op::JumpIfFalse { target: 3 },
Op::Unit, Op::Return,
];
assert!(
matches!(check(ops.clone()), Err(VerifyError::DepthConflict { .. })),
"a branch imbalance must be rejected, got {:?}",
check(ops),
);
}
#[test]
fn rejects_a_jump_off_the_end() {
let mut code = vec![tag::JUMP];
code.extend_from_slice(&99u32.to_le_bytes());
code.push(tag::RETURN);
assert!(matches!(
check_bytes(code, 8),
Err(VerifyError::JumpOutOfRange { .. }),
));
}
#[test]
fn rejects_a_jump_into_the_middle_of_an_instruction() {
let mut code = vec![tag::JUMP];
code.extend_from_slice(&3u32.to_le_bytes()); code.push(tag::RETURN);
assert_eq!(
check_bytes(code, 8),
Err(VerifyError::JumpIntoAnInstruction { at: 0, target: 3 }),
);
}
#[test]
fn every_arm_of_a_switch_is_checked() {
let ops = vec![
Op::Unit,
Op::Switch(0),
Op::Unit, Op::Return,
Op::Unit, Op::Return,
];
let (code, offsets) = assemble(&ops).expect("must assemble");
let chunk = Chunk::new(0, code.len() as u32, 8);
let table = |case: u32, default: u32| Switch {
cases: vec![crate::grain::bytecode::SwitchCase {
hash: 7,
target: case,
}],
ranges: Vec::new(),
default,
};
let good = [table(offsets[2], offsets[4])];
assert_eq!(
verify(
Abi::host().caps,
&code,
&[],
&[chunk],
&Pools {
switches: &good,
..pools()
}
),
Ok(vec![1]),
);
let mid = [table(offsets[1] + 1, offsets[4])];
assert!(
matches!(
verify(
Abi::host().caps,
&code,
&[],
&[chunk],
&Pools {
switches: &mid,
..pools()
}
),
Err(VerifyError::JumpIntoAnInstruction { .. }),
),
"a case arm landing mid-instruction must be refused",
);
let outside = [table(offsets[2], 9999)];
assert!(
matches!(
verify(
Abi::host().caps,
&code,
&[],
&[chunk],
&Pools {
switches: &outside,
..pools()
}
),
Err(VerifyError::JumpOutOfRange { .. }),
),
"a default outside the chunk must be refused",
);
}
#[test]
fn rejects_popping_an_empty_stack() {
assert!(matches!(
check(vec![Op::Pop, Op::Return]),
Err(VerifyError::Underflow { .. }),
));
}
#[test]
fn rejects_a_rotate_that_reaches_below_the_frame() {
assert!(matches!(
check(vec![Op::Unit, Op::Unit, Op::Rotate(2), Op::Return]),
Err(VerifyError::Underflow {
need: 3,
have: 2,
..
}),
));
assert_eq!(
check(vec![
Op::Unit,
Op::Unit,
Op::Unit,
Op::Rotate(2),
Op::Return
]),
Ok(vec![3]),
"with the third operand there it is in range, and nothing moves",
);
}
#[test]
fn rejects_running_past_the_last_instruction() {
assert_eq!(check(vec![Op::Unit]), Err(VerifyError::FallsOffTheEnd));
}
#[test]
fn rejects_an_index_with_nothing_behind_it() {
let (code, _) = assemble(&[Op::Const(7), Op::Return]).unwrap();
let chunk = Chunk::new(0, code.len() as u32, 8);
assert!(matches!(
verify(
Abi::host().caps,
&code,
&[],
&[chunk],
&Pools {
consts: 1,
..pools()
}
),
Err(VerifyError::BadIndex {
what: "constant",
..
}),
));
}
#[test]
fn rejects_a_chunk_that_outgrows_its_declared_stack() {
let (code, _) = assemble(&[Op::Unit, Op::Unit, Op::Unit, Op::Return]).unwrap();
assert!(matches!(
check_bytes(code, 2),
Err(VerifyError::StackExceedsDeclared { .. }),
));
}
#[test]
fn rejects_a_tag_it_does_not_know() {
assert_eq!(
check_bytes(vec![0xff], 8),
Err(VerifyError::Undecodable { at: 0 }),
);
}
#[test]
fn rejects_an_instruction_whose_operands_are_cut_off() {
assert_eq!(
check_bytes(vec![tag::CONST, 0], 8),
Err(VerifyError::Undecodable { at: 0 }),
);
}
#[test]
fn rejects_a_chunk_that_names_code_it_does_not_have() {
let (code, _) = assemble(&[Op::Unit, Op::Return]).unwrap();
assert!(matches!(
verify(
Abi::host().caps,
&code,
&[],
&[Chunk::new(0, 9999, 8)],
&pools()
),
Err(VerifyError::ChunkOutOfRange { .. }),
));
}
}