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::{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        while let Some(parent) = queue.pop_front() {
70            for child in inventory
71                .targets()
72                .iter()
73                .filter(|target| target.parent_canister_id() == Some(parent))
74            {
75                if selected.insert(child.canister_id())
76                    && request.expansion == SelectionExpansion::Descendants
77                {
78                    queue.push_back(child.canister_id());
79                }
80            }
81        }
82    }
83    Ok(SelectionView {
84        inventory: inventory.digest(),
85        targets: inventory
86            .targets()
87            .iter()
88            .filter(|target| selected.contains(target.canister_id()))
89            .collect(),
90    })
91}
92
93/// Typed declared selection rejection, before any effects.
94#[derive(Debug, Error)]
95pub enum SelectionError {
96    /// At least one exact physical selector is required.
97    #[error("selection contains no targets")]
98    EmptySelection,
99    /// Selector count exceeds the inventory bound.
100    #[error("selection exceeds {MAX_INVENTORY_TARGETS} selectors")]
101    TooManySelectors,
102    /// Equivalent principal was explicitly selected more than once.
103    #[error("duplicate physical selector {0}")]
104    DuplicateSelector(String),
105    /// Owning inventory boundary rejected an identity.
106    #[error(transparent)]
107    Inventory(#[from] InventoryRecordError),
108}
109
110#[cfg(test)]
111mod tests;