1use core::{fmt, mem};
4
5use crate::ffi::b2RecPlayer;
6
7pub use crate::adapter_contract::ADAPTER_ABI_VERSION;
8pub const SNAPSHOT_VERSION: u32 = 3;
9pub const RECORDING_VERSION_MAJOR: u32 = 3;
10pub const RECORDING_VERSION_MINOR: u32 = 2;
11pub const SNAPSHOT_FACTS_VERSION: u32 = 1;
12pub const SNAPSHOT_ENTRY_VERSION: u32 = 1;
13
14pub type SnapshotStatus = u32;
15pub const SNAPSHOT_OK: SnapshotStatus = 0;
16pub const SNAPSHOT_NULL_INPUT: SnapshotStatus = 1;
17pub const SNAPSHOT_TRUNCATED: SnapshotStatus = 2;
18pub const SNAPSHOT_BAD_HEADER: SnapshotStatus = 3;
19pub const SNAPSHOT_ABI_MISMATCH: SnapshotStatus = 4;
20pub const SNAPSHOT_OVERFLOW: SnapshotStatus = 5;
21pub const SNAPSHOT_LIMIT_EXCEEDED: SnapshotStatus = 6;
22pub const SNAPSHOT_INVALID_VALUE: SnapshotStatus = 7;
23pub const SNAPSHOT_INVALID_REFERENCE: SnapshotStatus = 8;
24pub const SNAPSHOT_DUPLICATE: SnapshotStatus = 9;
25pub const SNAPSHOT_TRAILING_BYTES: SnapshotStatus = 10;
26pub const SNAPSHOT_BUFFER_TOO_SMALL: SnapshotStatus = 11;
27
28pub const SNAPSHOT_ENTRY_BODY: u32 = 1;
29pub const SNAPSHOT_ENTRY_SHAPE: u32 = 2;
30pub const SNAPSHOT_ENTRY_CHAIN: u32 = 3;
31pub const SNAPSHOT_ENTRY_CONTACT: u32 = 4;
32pub const SNAPSHOT_ENTRY_JOINT: u32 = 5;
33pub const SNAPSHOT_ENTRY_ISLAND: u32 = 6;
34pub const SNAPSHOT_ENTRY_SOLVER_SET: u32 = 7;
35
36pub const SNAPSHOT_ENTRY_LIVE: u32 = 0x0000_0001;
37pub const SNAPSHOT_ENTRY_REQUIRES_CUSTOM_FILTER: u32 = 0x0000_0002;
38pub const SNAPSHOT_ENTRY_REQUIRES_PRE_SOLVE: u32 = 0x0000_0004;
39
40#[repr(C)]
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub struct AdapterIdentity {
43 pub struct_size: u32,
44 pub abi_version: u32,
45 pub snapshot_version: u32,
46 pub recording_version_major: u32,
47 pub recording_version_minor: u32,
48 pub snapshot_layout_hash: u32,
49 pub pointer_width: u8,
50 pub little_endian: u8,
51 pub double_precision: u8,
52 pub validation_enabled: u8,
53 pub private_abi_hash: [u8; 32],
54 pub upstream_sha: [u8; 41],
55 pub target_abi: [u8; 65],
56 pub adapter_source_sha256: [u8; 65],
57 pub effective_source_sha256: [u8; 65],
58 pub recording_contract_blake3: [u8; 65],
59}
60
61impl Default for AdapterIdentity {
62 fn default() -> Self {
63 Self {
64 struct_size: 0,
65 abi_version: 0,
66 snapshot_version: 0,
67 recording_version_major: 0,
68 recording_version_minor: 0,
69 snapshot_layout_hash: 0,
70 pointer_width: 0,
71 little_endian: 0,
72 double_precision: 0,
73 validation_enabled: 0,
74 private_abi_hash: [0; 32],
75 upstream_sha: [0; 41],
76 target_abi: [0; 65],
77 adapter_source_sha256: [0; 65],
78 effective_source_sha256: [0; 65],
79 recording_contract_blake3: [0; 65],
80 }
81 }
82}
83
84#[repr(C)]
85#[derive(Clone, Copy, Debug)]
86pub struct SnapshotLimits {
87 pub struct_size: u32,
88 pub version: u32,
89 pub max_image_bytes: u64,
90 pub max_validation_work: u64,
91 pub max_entries: u32,
92 pub max_array_elements: u32,
93 pub max_tree_nodes: u32,
94 pub max_hash_capacity: u32,
95 pub max_bitset_blocks: u32,
96 pub reserved: u32,
97}
98
99impl Default for SnapshotLimits {
100 fn default() -> Self {
101 Self {
102 struct_size: size_u32::<Self>(),
103 version: SNAPSHOT_FACTS_VERSION,
104 max_image_bytes: 256 * 1024 * 1024,
105 max_validation_work: 16_000_000,
106 max_entries: 1_000_000,
107 max_array_elements: 1_000_000,
108 max_tree_nodes: 1_000_000,
109 max_hash_capacity: 1_000_000,
110 max_bitset_blocks: 1_000_000,
111 reserved: 0,
112 }
113 }
114}
115
116#[repr(C)]
117#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
118pub struct SnapshotFacts {
119 pub struct_size: u32,
120 pub version: u32,
121 pub image_bytes: u64,
122 pub consumed_bytes: u64,
123 pub required_entries: u64,
124 pub validation_work: u64,
125 pub snapshot_flags: u32,
126 pub world_flags: u32,
127 pub pool_next: [u32; 7],
128 pub pool_free: [u32; 7],
129 pub entry_counts: [u32; 7],
130 pub requires_custom_filter: u32,
131 pub requires_pre_solve: u32,
132}
133
134#[repr(C)]
135#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
136pub struct SnapshotEntry {
137 pub struct_size: u32,
138 pub version: u32,
139 pub kind: u32,
140 pub flags: u32,
141 pub index: i32,
142 pub owner_a: i32,
143 pub owner_b: i32,
144 pub set_index: i32,
145 pub local_index: i32,
146 pub color_index: i32,
147 pub free_order: i32,
148 pub generation: u32,
149 pub subtype: u32,
150 pub owner_a_prev: i32,
151 pub owner_a_next: i32,
152 pub owner_b_prev: i32,
153 pub owner_b_next: i32,
154 pub owner_b_order: i32,
155}
156
157#[derive(Debug)]
158pub struct SnapshotValidation {
159 pub facts: SnapshotFacts,
160 pub entries: Vec<SnapshotEntry>,
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168#[non_exhaustive]
169pub enum SnapshotValidationError {
170 AdapterIdentity(AdapterIdentityError),
171 Status(SnapshotStatus),
172}
173
174impl fmt::Display for SnapshotValidationError {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 Self::AdapterIdentity(error) => write!(formatter, "native adapter identity: {error}"),
178 Self::Status(status) => write!(formatter, "native snapshot validator status {status}"),
179 }
180 }
181}
182
183impl std::error::Error for SnapshotValidationError {
184 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
185 match self {
186 Self::AdapterIdentity(error) => Some(error),
187 Self::Status(_) => None,
188 }
189 }
190}
191
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
194#[non_exhaustive]
195pub enum AdapterIdentityField {
196 StructSize,
197 AbiVersion,
198 SnapshotVersion,
199 RecordingVersion,
200 PointerWidth,
201 Endianness,
202 Precision,
203 Validation,
204 UpstreamSha,
205 TargetAbi,
206 AdapterSource,
207 EffectiveSource,
208 RecordingContract,
209 SnapshotLayout,
210 PrivateAbi,
211}
212
213impl fmt::Display for AdapterIdentityField {
214 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215 let name = match self {
216 Self::StructSize => "identity struct size",
217 Self::AbiVersion => "adapter ABI version",
218 Self::SnapshotVersion => "snapshot version",
219 Self::RecordingVersion => "recording version",
220 Self::PointerWidth => "pointer width",
221 Self::Endianness => "endianness",
222 Self::Precision => "precision",
223 Self::Validation => "validation mode",
224 Self::UpstreamSha => "upstream revision",
225 Self::TargetAbi => "target ABI",
226 Self::AdapterSource => "adapter source digest",
227 Self::EffectiveSource => "effective source digest",
228 Self::RecordingContract => "recording contract digest",
229 Self::SnapshotLayout => "snapshot layout identity",
230 Self::PrivateAbi => "private ABI identity",
231 };
232 formatter.write_str(name)
233 }
234}
235
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238#[non_exhaustive]
239pub enum AdapterIdentityError {
240 Unavailable,
241 Mismatch(AdapterIdentityField),
242}
243
244impl fmt::Display for AdapterIdentityError {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 Self::Unavailable => formatter.write_str("native adapter identity is unavailable"),
248 Self::Mismatch(field) => write!(formatter, "native adapter {field} does not match"),
249 }
250 }
251}
252
253impl std::error::Error for AdapterIdentityError {}
254
255const fn size_u32<T>() -> u32 {
256 assert!(mem::size_of::<T>() <= u32::MAX as usize);
257 mem::size_of::<T>() as u32
258}
259
260#[cfg_attr(
261 all(target_arch = "wasm32", not(feature = "double-precision")),
262 link(wasm_import_module = "box2d-sys-v2-single")
263)]
264#[cfg_attr(
265 all(target_arch = "wasm32", feature = "double-precision"),
266 link(wasm_import_module = "box2d-sys-v2-double")
267)]
268unsafe extern "C" {
269 pub fn boxddAdapter_AbiVersion() -> u32;
270 pub fn boxddAdapter_GetIdentity(out: *mut AdapterIdentity, out_size: usize) -> bool;
271 pub fn boxddAdapter_GetSnapshotLayoutHash() -> u32;
272 pub static boxddEffectiveSourceSha256: [u8; 65];
273 pub fn boxddSnapshot_Validate(
274 image: *const u8,
275 size: usize,
276 limits: *const SnapshotLimits,
277 facts: *mut SnapshotFacts,
278 entries: *mut SnapshotEntry,
279 entry_capacity: usize,
280 required_entries: *mut usize,
281 ) -> SnapshotStatus;
282 pub fn boxddRecPlayer_IsHealthy(player: *const b2RecPlayer) -> bool;
283}
284
285pub fn runtime_identity() -> Option<AdapterIdentity> {
287 let mut identity = AdapterIdentity::default();
288 let ok = unsafe { boxddAdapter_GetIdentity(&mut identity, mem::size_of_val(&identity)) };
290 ok.then_some(identity)
291}
292
293pub fn verify_identity(
300 identity: &AdapterIdentity,
301 reported_abi_version: u32,
302 reported_layout_hash: u32,
303) -> Result<(), AdapterIdentityError> {
304 use AdapterIdentityField as Field;
305
306 let mismatch = |field| Err(AdapterIdentityError::Mismatch(field));
307 if identity.struct_size as usize != mem::size_of::<AdapterIdentity>() {
308 return mismatch(Field::StructSize);
309 }
310 if reported_abi_version != ADAPTER_ABI_VERSION || identity.abi_version != ADAPTER_ABI_VERSION {
311 return mismatch(Field::AbiVersion);
312 }
313 if identity.snapshot_version != SNAPSHOT_VERSION {
314 return mismatch(Field::SnapshotVersion);
315 }
316 if identity.recording_version_major != RECORDING_VERSION_MAJOR
317 || identity.recording_version_minor != RECORDING_VERSION_MINOR
318 {
319 return mismatch(Field::RecordingVersion);
320 }
321 if identity.pointer_width as usize != mem::size_of::<usize>() {
322 return mismatch(Field::PointerWidth);
323 }
324 if identity.little_endian != u8::from(cfg!(target_endian = "little")) {
325 return mismatch(Field::Endianness);
326 }
327 if identity.double_precision != u8::from(cfg!(feature = "double-precision")) {
328 return mismatch(Field::Precision);
329 }
330 if identity.validation_enabled != u8::from(cfg!(feature = "validate")) {
331 return mismatch(Field::Validation);
332 }
333 if !canonical_identity_string(&identity.upstream_sha, crate::UPSTREAM_SHA) {
334 return mismatch(Field::UpstreamSha);
335 }
336 if !canonical_identity_string(&identity.target_abi, crate::TARGET_ABI) {
337 return mismatch(Field::TargetAbi);
338 }
339 if !canonical_identity_string(
340 &identity.adapter_source_sha256,
341 crate::ADAPTER_SOURCE_SHA256,
342 ) {
343 return mismatch(Field::AdapterSource);
344 }
345 if !canonical_identity_string(
346 &identity.effective_source_sha256,
347 crate::EFFECTIVE_SOURCE_SHA256,
348 ) {
349 return mismatch(Field::EffectiveSource);
350 }
351 if !canonical_identity_string(
352 &identity.recording_contract_blake3,
353 crate::RECORDING_CONTRACT_BLAKE3,
354 ) {
355 return mismatch(Field::RecordingContract);
356 }
357 if identity.snapshot_layout_hash != crate::SNAPSHOT_LAYOUT_HASH
358 || reported_layout_hash != crate::SNAPSHOT_LAYOUT_HASH
359 {
360 return mismatch(Field::SnapshotLayout);
361 }
362 if identity.private_abi_hash != crate::PRIVATE_ABI_HASH {
363 return mismatch(Field::PrivateAbi);
364 }
365 Ok(())
366}
367
368pub fn verify_runtime_identity() -> Result<AdapterIdentity, AdapterIdentityError> {
370 let reported_abi_version = unsafe { boxddAdapter_AbiVersion() };
372 let identity = runtime_identity().ok_or(AdapterIdentityError::Unavailable)?;
373 let reported_layout_hash = unsafe { boxddAdapter_GetSnapshotLayoutHash() };
375 verify_identity(&identity, reported_abi_version, reported_layout_hash)?;
376 Ok(identity)
377}
378
379fn canonical_identity_string<const N: usize>(value: &[u8; N], expected: &str) -> bool {
380 let Some(nul) = value.iter().position(|byte| *byte == 0) else {
381 return false;
382 };
383 value[..nul] == *expected.as_bytes() && value[nul..].iter().all(|byte| *byte == 0)
384}
385
386pub fn validate_snapshot(
392 image: &[u8],
393 limits: &SnapshotLimits,
394) -> Result<SnapshotValidation, SnapshotValidationError> {
395 validate_snapshot_with(
396 image,
397 limits,
398 verify_runtime_identity,
399 validate_snapshot_native,
400 )
401}
402
403fn validate_snapshot_with(
404 image: &[u8],
405 limits: &SnapshotLimits,
406 identity: impl FnOnce() -> Result<AdapterIdentity, AdapterIdentityError>,
407 validate: impl FnOnce(&[u8], &SnapshotLimits) -> Result<SnapshotValidation, SnapshotStatus>,
408) -> Result<SnapshotValidation, SnapshotValidationError> {
409 identity().map_err(SnapshotValidationError::AdapterIdentity)?;
410 validate(image, limits).map_err(SnapshotValidationError::Status)
411}
412
413fn validate_snapshot_native(
414 image: &[u8],
415 limits: &SnapshotLimits,
416) -> Result<SnapshotValidation, SnapshotStatus> {
417 let mut facts = SnapshotFacts::default();
418 let mut required = 0usize;
419 let sizing_status = unsafe {
421 boxddSnapshot_Validate(
422 image.as_ptr(),
423 image.len(),
424 limits,
425 &mut facts,
426 core::ptr::null_mut(),
427 0,
428 &mut required,
429 )
430 };
431 if sizing_status != SNAPSHOT_BUFFER_TOO_SMALL && sizing_status != SNAPSHOT_OK {
432 return Err(sizing_status);
433 }
434 if required > limits.max_entries as usize || required != facts.required_entries as usize {
435 return Err(SNAPSHOT_LIMIT_EXCEEDED);
436 }
437
438 let mut entries = Vec::new();
439 entries
440 .try_reserve_exact(required)
441 .map_err(|_| SNAPSHOT_LIMIT_EXCEEDED)?;
442 entries.resize(required, SnapshotEntry::default());
443 let status = unsafe {
445 boxddSnapshot_Validate(
446 image.as_ptr(),
447 image.len(),
448 limits,
449 &mut facts,
450 entries.as_mut_ptr(),
451 entries.len(),
452 &mut required,
453 )
454 };
455 if status != SNAPSHOT_OK {
456 return Err(status);
457 }
458 if required != entries.len() || facts.required_entries != entries.len() as u64 {
459 return Err(SNAPSHOT_INVALID_VALUE);
460 }
461 Ok(SnapshotValidation { facts, entries })
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[derive(Clone, Copy, Debug)]
469 enum IdentityMutation {
470 StructSize,
471 ReportedAbiVersion,
472 EmbeddedAbiVersion,
473 SnapshotVersion,
474 RecordingVersionMajor,
475 RecordingVersionMinor,
476 PointerWidth,
477 Endianness,
478 Precision,
479 Validation,
480 UpstreamSha,
481 TargetAbi,
482 AdapterSource,
483 EffectiveSource,
484 RecordingContract,
485 EmbeddedSnapshotLayout,
486 ReportedSnapshotLayout,
487 PrivateAbi,
488 }
489
490 impl IdentityMutation {
491 const ALL: [Self; 18] = [
492 Self::StructSize,
493 Self::ReportedAbiVersion,
494 Self::EmbeddedAbiVersion,
495 Self::SnapshotVersion,
496 Self::RecordingVersionMajor,
497 Self::RecordingVersionMinor,
498 Self::PointerWidth,
499 Self::Endianness,
500 Self::Precision,
501 Self::Validation,
502 Self::UpstreamSha,
503 Self::TargetAbi,
504 Self::AdapterSource,
505 Self::EffectiveSource,
506 Self::RecordingContract,
507 Self::EmbeddedSnapshotLayout,
508 Self::ReportedSnapshotLayout,
509 Self::PrivateAbi,
510 ];
511
512 fn expected_field(self) -> AdapterIdentityField {
513 match self {
514 Self::StructSize => AdapterIdentityField::StructSize,
515 Self::ReportedAbiVersion | Self::EmbeddedAbiVersion => {
516 AdapterIdentityField::AbiVersion
517 }
518 Self::SnapshotVersion => AdapterIdentityField::SnapshotVersion,
519 Self::RecordingVersionMajor | Self::RecordingVersionMinor => {
520 AdapterIdentityField::RecordingVersion
521 }
522 Self::PointerWidth => AdapterIdentityField::PointerWidth,
523 Self::Endianness => AdapterIdentityField::Endianness,
524 Self::Precision => AdapterIdentityField::Precision,
525 Self::Validation => AdapterIdentityField::Validation,
526 Self::UpstreamSha => AdapterIdentityField::UpstreamSha,
527 Self::TargetAbi => AdapterIdentityField::TargetAbi,
528 Self::AdapterSource => AdapterIdentityField::AdapterSource,
529 Self::EffectiveSource => AdapterIdentityField::EffectiveSource,
530 Self::RecordingContract => AdapterIdentityField::RecordingContract,
531 Self::EmbeddedSnapshotLayout | Self::ReportedSnapshotLayout => {
532 AdapterIdentityField::SnapshotLayout
533 }
534 Self::PrivateAbi => AdapterIdentityField::PrivateAbi,
535 }
536 }
537
538 fn apply(
539 self,
540 identity: &mut AdapterIdentity,
541 reported_abi_version: &mut u32,
542 reported_layout_hash: &mut u32,
543 ) {
544 match self {
545 Self::StructSize => identity.struct_size ^= 1,
546 Self::ReportedAbiVersion => *reported_abi_version ^= 1,
547 Self::EmbeddedAbiVersion => identity.abi_version ^= 1,
548 Self::SnapshotVersion => identity.snapshot_version ^= 1,
549 Self::RecordingVersionMajor => identity.recording_version_major ^= 1,
550 Self::RecordingVersionMinor => identity.recording_version_minor ^= 1,
551 Self::PointerWidth => identity.pointer_width ^= 1,
552 Self::Endianness => identity.little_endian ^= 1,
553 Self::Precision => identity.double_precision ^= 1,
554 Self::Validation => identity.validation_enabled ^= 1,
555 Self::UpstreamSha => identity.upstream_sha[0] ^= 1,
556 Self::TargetAbi => identity.target_abi[0] ^= 1,
557 Self::AdapterSource => identity.adapter_source_sha256[0] ^= 1,
558 Self::EffectiveSource => identity.effective_source_sha256[0] ^= 1,
559 Self::RecordingContract => identity.recording_contract_blake3[0] ^= 1,
560 Self::EmbeddedSnapshotLayout => identity.snapshot_layout_hash ^= 1,
561 Self::ReportedSnapshotLayout => *reported_layout_hash ^= 1,
562 Self::PrivateAbi => identity.private_abi_hash[0] ^= 1,
563 }
564 }
565 }
566
567 #[test]
568 fn rust_layouts_match_the_versioned_header_contract() {
569 assert_eq!(mem::size_of::<SnapshotLimits>(), 48);
570 assert_eq!(mem::size_of::<SnapshotEntry>(), 72);
571 assert_eq!(mem::size_of::<SnapshotFacts>(), 144);
572 assert_eq!(mem::size_of::<AdapterIdentity>(), 364);
573 }
574
575 #[test]
576 fn runtime_adapter_identity_matches_this_crate_instance() {
577 verify_runtime_identity().expect("linked adapter must match the Rust crate build");
578 }
579
580 #[test]
581 fn forged_adapter_identity_is_rejected_field_by_field() {
582 let identity = verify_runtime_identity().expect("test adapter identity");
583
584 for mutation in IdentityMutation::ALL {
585 let mut forged = identity;
586 let mut reported_abi_version = ADAPTER_ABI_VERSION;
587 let mut reported_layout_hash = identity.snapshot_layout_hash;
588 mutation.apply(
589 &mut forged,
590 &mut reported_abi_version,
591 &mut reported_layout_hash,
592 );
593
594 assert_eq!(
595 verify_identity(&forged, reported_abi_version, reported_layout_hash),
596 Err(AdapterIdentityError::Mismatch(mutation.expected_field())),
597 "identity mutation {mutation:?} was not rejected precisely"
598 );
599 }
600 }
601
602 #[test]
603 fn canonical_identity_strings_require_exact_bytes_and_zero_padding() {
604 assert!(canonical_identity_string(b"abc\0\0", "abc"));
605 assert!(!canonical_identity_string(b"abcde", "abc"));
606 assert!(!canonical_identity_string(b"ab\0\0\0", "abc"));
607 assert!(!canonical_identity_string(b"abc\0x", "abc"));
608 }
609
610 #[test]
611 fn snapshot_identity_gate_short_circuits_the_native_validator() {
612 let identity_calls = core::cell::Cell::new(0);
613 let validator_calls = core::cell::Cell::new(0);
614 let limits = SnapshotLimits::default();
615 let error = validate_snapshot_with(
616 b"untrusted snapshot",
617 &limits,
618 || {
619 identity_calls.set(identity_calls.get() + 1);
620 Err(AdapterIdentityError::Unavailable)
621 },
622 |_, _| {
623 validator_calls.set(validator_calls.get() + 1);
624 Err(SNAPSHOT_BAD_HEADER)
625 },
626 )
627 .unwrap_err();
628
629 assert_eq!(
630 error,
631 SnapshotValidationError::AdapterIdentity(AdapterIdentityError::Unavailable)
632 );
633 assert_eq!(identity_calls.get(), 1);
634 assert_eq!(validator_calls.get(), 0);
635 }
636
637 #[test]
638 fn snapshot_identity_gate_runs_the_native_validator_after_authorization() {
639 let phase = core::cell::Cell::new(0);
640 let limits = SnapshotLimits::default();
641 let image = b"untrusted snapshot";
642 let error = validate_snapshot_with(
643 image,
644 &limits,
645 || {
646 assert_eq!(phase.replace(1), 0);
647 Ok(AdapterIdentity::default())
648 },
649 |observed_image, observed_limits| {
650 assert_eq!(phase.replace(2), 1);
651 assert_eq!(observed_image, image);
652 assert!(core::ptr::eq(observed_limits, &limits));
653 Err(SNAPSHOT_BAD_HEADER)
654 },
655 )
656 .unwrap_err();
657
658 assert_eq!(error, SnapshotValidationError::Status(SNAPSHOT_BAD_HEADER));
659 assert_eq!(phase.get(), 2);
660 }
661}