Skip to main content

ic_backup/policy/selection/
mod.rs

1//! Explicit physical selection and bounded parent-edge expansion; no live discovery.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    inventory::{
6        InventoryRecord, InventoryRecordError, InventoryTargetRecord, MAX_INVENTORY_TARGETS,
7    },
8};
9use std::collections::{BTreeMap, BTreeSet, VecDeque};
10use thiserror::Error;
11
12/// Exact expansion selected by the caller; no implicit role or root special case.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum SelectionExpansion {
15    /// Include exactly the explicitly named physical targets.
16    Exact,
17    /// Include named targets and their direct children only.
18    DirectChildren,
19    /// Include named targets and every declared descendant.
20    Descendants,
21}
22
23/// Passive bounded selection request, without a non-neutral default.
24#[derive(Clone, Debug)]
25pub struct SelectionRequest {
26    /// Nonempty exact principal list; equivalent duplicates reject.
27    pub canister_ids: Vec<String>,
28    /// Explicit requested expansion over declared parent edges.
29    pub expansion: SelectionExpansion,
30}
31
32/// Read-only projection bound to the entire declared inventory, not fresh membership.
33#[derive(Clone, Debug)]
34pub struct SelectionView<'a> {
35    /// Digest of the original full declared inventory, including unselected targets.
36    pub inventory: ArtifactChecksumRecord,
37    /// Unique targets in canonical principal order, not dispatch order.
38    ///
39    /// Parent links retain original meaning even if a parent is outside this selection.
40    pub targets: Vec<&'a InventoryTargetRecord>,
41}
42
43/// Resolve exact identities and expand only the requested validated declared edges.
44///
45/// # Errors
46/// Rejects empty/excessive requests, malformed/absent identities and duplicate selectors.
47pub fn select<'a>(
48    inventory: &'a InventoryRecord,
49    request: &SelectionRequest,
50) -> Result<SelectionView<'a>, SelectionError> {
51    if request.canister_ids.is_empty() {
52        return Err(SelectionError::EmptySelection);
53    }
54    if request.canister_ids.len() > MAX_INVENTORY_TARGETS {
55        return Err(SelectionError::TooManySelectors);
56    }
57    let mut selected = BTreeSet::new();
58    let mut queue = VecDeque::new();
59    for id in &request.canister_ids {
60        let target = inventory.target(id)?;
61        if !selected.insert(target.canister_id()) {
62            return Err(SelectionError::DuplicateSelector(
63                target.canister_id().into(),
64            ));
65        }
66        queue.push_back(target.canister_id());
67    }
68    if request.expansion != SelectionExpansion::Exact {
69        let mut children: BTreeMap<_, Vec<_>> = BTreeMap::new();
70        for target in inventory.targets() {
71            if let Some(parent) = target.parent_canister_id() {
72                children
73                    .entry(parent)
74                    .or_default()
75                    .push(target.canister_id());
76            }
77        }
78        while let Some(parent) = queue.pop_front() {
79            for child in children.get(parent).into_iter().flatten() {
80                if selected.insert(*child) && request.expansion == SelectionExpansion::Descendants {
81                    queue.push_back(*child);
82                }
83            }
84        }
85    }
86    Ok(SelectionView {
87        inventory: inventory.digest(),
88        targets: inventory
89            .targets()
90            .iter()
91            .filter(|target| selected.contains(target.canister_id()))
92            .collect(),
93    })
94}
95
96/// Typed declared selection rejection, before any effects.
97#[derive(Debug, Error)]
98pub enum SelectionError {
99    /// At least one exact physical selector is required.
100    #[error("selection contains no targets")]
101    EmptySelection,
102    /// Selector count exceeds the inventory bound.
103    #[error("selection exceeds {MAX_INVENTORY_TARGETS} selectors")]
104    TooManySelectors,
105    /// Equivalent principal was explicitly selected more than once.
106    #[error("duplicate physical selector {0}")]
107    DuplicateSelector(String),
108    /// Owning inventory boundary rejected an identity.
109    #[error(transparent)]
110    Inventory(#[from] InventoryRecordError),
111}
112
113#[cfg(test)]
114mod tests;