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, Deserializer, 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 = "required_text")]
38    parent_canister_id: Option<String>,
39    #[serde(deserialize_with = "required_text")]
40    role: Option<String>,
41    #[serde(deserialize_with = "required_text")]
42    module_hash: Option<String>,
43}
44fn required_text<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
45    Option::deserialize(deserializer)
46}
47impl TryFrom<TargetFields> for InventoryTargetRecord {
48    type Error = InventoryRecordError;
49    fn try_from(fields: TargetFields) -> Result<Self, Self::Error> {
50        Self::new(&InventoryTargetRequest {
51            canister_id: fields.canister_id,
52            parent_canister_id: fields.parent_canister_id,
53            role: fields.role,
54            module_hash: fields.module_hash,
55        })
56    }
57}
58impl InventoryTargetRecord {
59    /// Normalize equivalent principals/hashes and admit bounded opaque role text.
60    ///
61    /// # Errors
62    /// Rejects malformed principals/hashes and excessive role bytes.
63    pub fn new(request: &InventoryTargetRequest) -> Result<Self, InventoryRecordError> {
64        if request
65            .role
66            .as_ref()
67            .is_some_and(|role| role.len() > MAX_INVENTORY_ROLE_BYTES)
68        {
69            return Err(InventoryRecordError::RoleTooLarge);
70        }
71        Ok(Self {
72            canister_id: crate::model::principal::canonical_text(&request.canister_id)
73                .ok_or(InventoryRecordError::InvalidPrincipal("canister_id"))?,
74            parent_canister_id: request
75                .parent_canister_id
76                .as_deref()
77                .map(|value| {
78                    crate::model::principal::canonical_text(value)
79                        .ok_or(InventoryRecordError::InvalidPrincipal("parent_canister_id"))
80                })
81                .transpose()?,
82            role: request.role.clone(),
83            module_hash: request
84                .module_hash
85                .as_deref()
86                .map(canonical_hash)
87                .transpose()?,
88        })
89    }
90    /// Read the exact canonical physical principal.
91    #[must_use]
92    pub fn canister_id(&self) -> &str {
93        &self.canister_id
94    }
95    /// Read the optional canonical declared parent.
96    #[must_use]
97    pub fn parent_canister_id(&self) -> Option<&str> {
98        self.parent_canister_id.as_deref()
99    }
100    /// Read the opaque optional role label.
101    #[must_use]
102    pub fn role(&self) -> Option<&str> {
103        self.role.as_deref()
104    }
105    /// Read the optional canonical declared module digest.
106    #[must_use]
107    pub fn module_hash(&self) -> Option<&str> {
108        self.module_hash.as_deref()
109    }
110}