Skip to main content

ic_backup/model/inventory/target/
mod.rs

1//! Passive physical target admission; role and module claims are integration declarations.
2
3use super::InventoryRecordError;
4use crate::model::artifacts::canonical_hash;
5use serde::{Deserialize, Serialize};
6
7/// Maximum optional role size in UTF-8 bytes, without role-based authority semantics.
8pub const MAX_INVENTORY_ROLE_BYTES: usize = 256;
9
10/// Passive physical inventory row supplied by an integration or explicit operator selection.
11#[derive(Clone, Debug)]
12pub struct InventoryTargetRequest {
13    /// Exact physical principal text.
14    pub canister_id: String,
15    /// Exact parent inside the declared forest, or no declared parent.
16    pub parent_canister_id: Option<String>,
17    /// Opaque optional application label; never a selector or authority substitute.
18    pub role: Option<String>,
19    /// Optional declared exact module SHA-256 digest, not a fresh observation.
20    pub module_hash: Option<String>,
21}
22
23/// Canonical immutable declared physical target with required nullable fields.
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[serde(try_from = "TargetFields")]
26pub struct InventoryTargetRecord {
27    canister_id: String,
28    parent_canister_id: Option<String>,
29    role: Option<String>,
30    module_hash: Option<String>,
31}
32
33#[derive(Deserialize)]
34#[serde(deny_unknown_fields)]
35struct TargetFields {
36    canister_id: String,
37    #[serde(deserialize_with = "crate::model::required_option::deserialize")]
38    parent_canister_id: Option<String>,
39    #[serde(deserialize_with = "crate::model::required_option::deserialize")]
40    role: Option<String>,
41    #[serde(deserialize_with = "crate::model::required_option::deserialize")]
42    module_hash: Option<String>,
43}
44
45impl TryFrom<TargetFields> for InventoryTargetRecord {
46    type Error = InventoryRecordError;
47    fn try_from(fields: TargetFields) -> Result<Self, Self::Error> {
48        Self::new(&InventoryTargetRequest {
49            canister_id: fields.canister_id,
50            parent_canister_id: fields.parent_canister_id,
51            role: fields.role,
52            module_hash: fields.module_hash,
53        })
54    }
55}
56impl InventoryTargetRecord {
57    /// Normalize equivalent principals/hashes and admit bounded opaque role text.
58    ///
59    /// # Errors
60    /// Rejects malformed principals/hashes and excessive role bytes.
61    pub fn new(request: &InventoryTargetRequest) -> Result<Self, InventoryRecordError> {
62        if request
63            .role
64            .as_ref()
65            .is_some_and(|role| role.len() > MAX_INVENTORY_ROLE_BYTES)
66        {
67            return Err(InventoryRecordError::RoleTooLarge);
68        }
69        Ok(Self {
70            canister_id: crate::model::principal::canonical_text(&request.canister_id)
71                .ok_or(InventoryRecordError::InvalidPrincipal("canister_id"))?,
72            parent_canister_id: request
73                .parent_canister_id
74                .as_deref()
75                .map(|value| {
76                    crate::model::principal::canonical_text(value)
77                        .ok_or(InventoryRecordError::InvalidPrincipal("parent_canister_id"))
78                })
79                .transpose()?,
80            role: request.role.clone(),
81            module_hash: request
82                .module_hash
83                .as_deref()
84                .map(canonical_hash)
85                .transpose()?,
86        })
87    }
88    /// Read the exact canonical physical principal.
89    #[must_use]
90    pub fn canister_id(&self) -> &str {
91        &self.canister_id
92    }
93    /// Read the optional canonical declared parent.
94    #[must_use]
95    pub fn parent_canister_id(&self) -> Option<&str> {
96        self.parent_canister_id.as_deref()
97    }
98    /// Read the opaque optional role label.
99    #[must_use]
100    pub fn role(&self) -> Option<&str> {
101        self.role.as_deref()
102    }
103    /// Read the optional canonical declared module digest.
104    #[must_use]
105    pub fn module_hash(&self) -> Option<&str> {
106        self.module_hash.as_deref()
107    }
108}