Skip to main content

boxdd_sys/
adapter.rs

1//! Repository-owned safety adapter for snapshot and replay internals.
2
3use 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/// Why a snapshot could not be safely validated by the linked adapter.
164///
165/// The identity variant is returned before any validator call receives Rust-owned output
166/// pointers. A native status is returned only after the linked adapter has been authorized.
167#[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/// One field of the linked native adapter that does not match this Rust crate instance.
193#[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/// Failure to authorize the linked native adapter before using any Box2D API.
237#[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
285/// Returns the runtime identity compiled into the linked adapter.
286pub fn runtime_identity() -> Option<AdapterIdentity> {
287    let mut identity = AdapterIdentity::default();
288    // SAFETY: identity is a writable value of the exact versioned C ABI type.
289    let ok = unsafe { boxddAdapter_GetIdentity(&mut identity, mem::size_of_val(&identity)) };
290    ok.then_some(identity)
291}
292
293/// Verifies a captured identity against this exact Rust crate build.
294///
295/// `reported_abi_version` and `reported_layout_hash` are separate adapter exports. Comparing them
296/// prevents a stale or partially implemented adapter from satisfying the contract with a single
297/// fabricated struct. The private C layout cannot be recomputed in Rust, so its non-zero identity
298/// is authorized by the independently matched repository adapter-source digest.
299pub 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
368/// Reads and authorizes the linked adapter before any non-adapter Box2D FFI call.
369pub fn verify_runtime_identity() -> Result<AdapterIdentity, AdapterIdentityError> {
370    // SAFETY: these functions take no caller pointers and are the adapter's identity handshake.
371    let reported_abi_version = unsafe { boxddAdapter_AbiVersion() };
372    let identity = runtime_identity().ok_or(AdapterIdentityError::Unavailable)?;
373    // SAFETY: this function takes no caller pointers and returns a fixed-width identity value.
374    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
386/// Fully validates a snapshot and returns its canonical slot facts.
387///
388/// Before passing Rust-owned output storage to the native validator, this function verifies that
389/// the linked adapter matches this exact crate build. Callers therefore receive identity failures
390/// separately from untrusted snapshot-content failures.
391pub 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    // SAFETY: the adapter accepts arbitrary byte slices and performs no unchecked input access.
420    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    // SAFETY: entries has exactly `required` initialized, writable elements and image remains alive.
444    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}