Skip to main content

ic_backup/policy/snapshot_inventory_delta/
mod.rs

1//! Pure snapshot inventory comparison, without capture attribution or settlement.
2
3use crate::model::{
4    ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
5    ic_snapshot_reply::{IcSnapshotInfo, IcSnapshotReply},
6};
7use thiserror::Error;
8
9/// Read-only new-snapshot candidates under exact capture and inventory declarations.
10///
11/// Even one candidate is not proof that the capture created it: another controller
12/// may have acted. No result authorizes a receipt, retry, load, deletion or dispatch.
13/// Integrations own authenticated association, baseline custody, observation timing
14/// and exclusive attribution. An empty delta does not prove the capture failed.
15#[derive(Debug)]
16pub struct SnapshotInventoryDeltaView<'a> {
17    capture: &'a IcManagementRequestRecord,
18    baseline: &'a IcSnapshotReply<'a>,
19    observed: &'a IcSnapshotReply<'a>,
20    candidates: Vec<&'a IcSnapshotInfo>,
21}
22
23impl<'a> SnapshotInventoryDeltaView<'a> {
24    /// Read the exact capture declaration, including its existing wire digest.
25    #[must_use]
26    pub const fn capture(&self) -> &'a IcManagementRequestRecord {
27        self.capture
28    }
29
30    /// Read the original inventory declaration and exact raw-payload evidence.
31    #[must_use]
32    pub const fn baseline(&self) -> &'a IcSnapshotReply<'a> {
33        self.baseline
34    }
35
36    /// Read the subsequent inventory declaration and exact raw-payload evidence.
37    #[must_use]
38    pub const fn observed(&self) -> &'a IcSnapshotReply<'a> {
39        self.observed
40    }
41
42    /// Read zero, one or multiple new descriptors in exact raw-ID order.
43    ///
44    /// Cardinality is descriptive. A singleton grants no capture attribution;
45    /// multiple candidates remain explicit rather than selecting an arbitrary ID.
46    #[must_use]
47    pub fn candidates(&self) -> &[&'a IcSnapshotInfo] {
48        &self.candidates
49    }
50}
51
52/// Compare admitted inventories for the exact declared new-snapshot capture target.
53///
54/// Both replies must be inventory replies for the same exact canonical target as
55/// the capture. Every baseline ID must remain present with unchanged timestamp and
56/// size. New IDs are projected in the reply owner's canonical order, retaining its
57/// existing 1,024-entry and 256-ID-byte bounds without re-encoding or remote IO.
58///
59/// There is no clock, network/caller validation, effect receipt or spending change.
60/// The integration must retain the original baseline before the attempted capture;
61/// this function cannot establish chronology or authenticate supplied reply bytes.
62///
63/// # Errors
64/// Rejects wrong request methods, mismatched targets, lost baseline entries and
65/// changed metadata for a retained exact ID. No input or journal is mutated.
66pub fn compare<'a>(
67    capture: &'a IcManagementRequestRecord,
68    baseline: &'a IcSnapshotReply<'a>,
69    observed: &'a IcSnapshotReply<'a>,
70) -> Result<SnapshotInventoryDeltaView<'a>, SnapshotInventoryDeltaError> {
71    if capture.method() != IcManagementMethodRecord::TakeCanisterSnapshot {
72        return Err(SnapshotInventoryDeltaError::WrongCaptureMethod);
73    }
74    let candidates = compare_inventories(capture.target(), baseline, observed)?;
75    Ok(SnapshotInventoryDeltaView {
76        capture,
77        baseline,
78        observed,
79        candidates,
80    })
81}
82
83/// Shared closed-baseline admission; candidates remain descriptive only.
84pub(crate) fn compare_inventories<'a>(
85    target: &str,
86    baseline: &IcSnapshotReply<'_>,
87    observed: &'a IcSnapshotReply<'_>,
88) -> Result<Vec<&'a IcSnapshotInfo>, SnapshotInventoryDeltaError> {
89    for reply in [baseline, observed] {
90        if reply.request().method() != IcManagementMethodRecord::ListCanisterSnapshots {
91            return Err(SnapshotInventoryDeltaError::WrongInventoryMethod);
92        }
93        if reply.request().target() != target {
94            return Err(SnapshotInventoryDeltaError::TargetMismatch);
95        }
96    }
97    let mut candidates = Vec::new();
98    let mut previous = baseline.snapshots().iter().peekable();
99    for snapshot in observed.snapshots() {
100        match previous.peek() {
101            Some(old) if old.id() < snapshot.id() => {
102                return Err(SnapshotInventoryDeltaError::LostBaseline);
103            }
104            Some(old) if old.id() == snapshot.id() => {
105                if *old != snapshot {
106                    return Err(SnapshotInventoryDeltaError::ChangedBaselineMetadata);
107                }
108                previous.next();
109            }
110            _ => candidates.push(snapshot),
111        }
112    }
113    if previous.next().is_some() {
114        return Err(SnapshotInventoryDeltaError::LostBaseline);
115    }
116    Ok(candidates)
117}
118
119/// Typed comparison rejection, with no raw identifiers or payload diagnostics.
120#[derive(Debug, Error, Eq, PartialEq)]
121pub enum SnapshotInventoryDeltaError {
122    /// The supplied intent is not the closed non-replacing capture method.
123    #[error("inventory comparison requires a new-snapshot capture request")]
124    WrongCaptureMethod,
125    /// A decoded singleton capture reply cannot substitute for an inventory.
126    #[error("inventory comparison requires two snapshot-list replies")]
127    WrongInventoryMethod,
128    /// One inventory is declared under another canonical target.
129    #[error("snapshot inventory target differs from capture target")]
130    TargetMismatch,
131    /// At least one retained baseline ID disappeared from the subsequent inventory.
132    #[error("snapshot inventory lost a baseline identifier")]
133    LostBaseline,
134    /// A retained exact ID changed its declared timestamp or total size.
135    #[error("snapshot inventory changed baseline metadata")]
136    ChangedBaselineMetadata,
137}
138
139#[cfg(test)]
140mod tests;