Skip to main content

canic_subnet_catalog/resolver/
mod.rs

1use crate::{
2    CatalogError, SubnetCatalog, SubnetInfo, canonical_principal_text, parse_principal,
3    principal_bytes,
4};
5use serde::{Deserialize, Serialize};
6use std::{collections::BTreeSet, str::FromStr};
7
8///
9/// ResolveAs
10///
11#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ResolveAs {
14    Subnet,
15    Canister,
16}
17
18impl ResolveAs {
19    #[must_use]
20    pub const fn as_str(self) -> &'static str {
21        match self {
22            Self::Subnet => "subnet",
23            Self::Canister => "canister",
24        }
25    }
26}
27
28impl FromStr for ResolveAs {
29    type Err = String;
30
31    fn from_str(value: &str) -> Result<Self, Self::Err> {
32        match value {
33            "subnet" => Ok(Self::Subnet),
34            "canister" => Ok(Self::Canister),
35            other => Err(format!("invalid value {other}; use subnet or canister")),
36        }
37    }
38}
39
40///
41/// ResolvedSubnetSubject
42///
43#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub enum ResolvedSubnetSubject {
46    Subnet,
47    Canister,
48}
49
50impl ResolvedSubnetSubject {
51    #[must_use]
52    pub const fn as_str(self) -> &'static str {
53        match self {
54            Self::Subnet => "subnet",
55            Self::Canister => "canister",
56        }
57    }
58}
59
60///
61/// ResolvedSubnet
62///
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct ResolvedSubnet {
65    pub input_principal: String,
66    pub resolved_as: ResolvedSubnetSubject,
67    pub resolved_from: String,
68    pub subnet: SubnetInfo,
69    pub matched_canister_principal: Option<String>,
70    pub matched_routing_range: Option<crate::RoutingRange>,
71}
72
73impl SubnetCatalog {
74    /// Resolve a principal as a known subnet or as a canister covered by a cached range.
75    pub fn resolve_principal(
76        &self,
77        input: &str,
78        forced: Option<ResolveAs>,
79    ) -> Result<ResolvedSubnet, CatalogError> {
80        let input_principal = canonical_principal_text(input)?;
81        match forced {
82            Some(ResolveAs::Subnet) => self.resolve_known_subnet(&input_principal),
83            None if self.subnet_by_principal(&input_principal).is_some() => {
84                self.resolve_known_subnet(&input_principal)
85            }
86            Some(ResolveAs::Canister) | None => self.resolve_canister(&input_principal),
87        }
88    }
89
90    /// Resolve an exact principal or a unique cached subnet principal prefix.
91    pub fn resolve_principal_or_prefix(
92        &self,
93        input: &str,
94        forced: Option<ResolveAs>,
95    ) -> Result<ResolvedSubnet, CatalogError> {
96        if canonical_principal_text(input).is_ok() {
97            return self.resolve_principal(input, forced);
98        }
99        self.resolve_principal_prefix(input, forced)
100    }
101
102    /// Resolve a unique prefix of a cached subnet principal.
103    pub fn resolve_principal_prefix(
104        &self,
105        input: &str,
106        forced: Option<ResolveAs>,
107    ) -> Result<ResolvedSubnet, CatalogError> {
108        let prefix = input.trim().to_ascii_lowercase();
109        if prefix.is_empty() {
110            return Err(CatalogError::PrincipalPrefixNotFound { prefix });
111        }
112
113        let matches = self.subnet_principal_prefix_matches(&prefix, forced);
114        let mut iter = matches.iter();
115        let Some(first) = iter.next() else {
116            return Err(CatalogError::PrincipalPrefixNotFound { prefix });
117        };
118        if iter.next().is_some() {
119            return Err(CatalogError::AmbiguousPrincipalPrefix {
120                prefix,
121                matches: matches
122                    .iter()
123                    .map(|subnet| format!("subnet:{subnet}"))
124                    .collect::<Vec<_>>(),
125            });
126        }
127
128        let mut resolved = self.resolve_known_subnet(first)?;
129        resolved.input_principal = input.to_string();
130        resolved.resolved_from = "subnet_principal_prefix".to_string();
131        Ok(resolved)
132    }
133
134    fn subnet_principal_prefix_matches(
135        &self,
136        prefix: &str,
137        forced: Option<ResolveAs>,
138    ) -> BTreeSet<String> {
139        let mut matches = BTreeSet::new();
140        if forced != Some(ResolveAs::Canister) {
141            for subnet in &self.subnets {
142                if subnet.subnet_principal.starts_with(prefix) {
143                    matches.insert(subnet.subnet_principal.clone());
144                }
145            }
146        }
147        matches
148    }
149
150    fn resolve_known_subnet(&self, input_principal: &str) -> Result<ResolvedSubnet, CatalogError> {
151        let subnet = self
152            .subnet_by_principal(input_principal)
153            .cloned()
154            .ok_or_else(|| CatalogError::UnknownSubnet {
155                subnet_principal: input_principal.to_string(),
156            })?;
157        Ok(ResolvedSubnet {
158            input_principal: input_principal.to_string(),
159            resolved_as: ResolvedSubnetSubject::Subnet,
160            resolved_from: "subnet_principal".to_string(),
161            subnet,
162            matched_canister_principal: None,
163            matched_routing_range: None,
164        })
165    }
166
167    /// Resolve a canister principal through cached routing ranges.
168    pub fn resolve_canister(&self, input_principal: &str) -> Result<ResolvedSubnet, CatalogError> {
169        let canonical_canister = parse_principal(input_principal, "canister_principal")?.to_text();
170        let canister_bytes = principal_bytes(&canonical_canister, "canister_principal")?;
171        let range = self
172            .routing_ranges
173            .iter()
174            .find(|range| range_contains_principal(range, &canister_bytes).unwrap_or(false))
175            .ok_or_else(|| CatalogError::RouteNotFound {
176                canister_principal: canonical_canister.clone(),
177                registry_version: self.registry_version,
178                catalog_schema_version: self.catalog_schema_version,
179            })?;
180        let subnet = self
181            .subnet_by_principal(&range.subnet_principal)
182            .expect("catalog validation ensures routing subnet exists")
183            .clone();
184        Ok(ResolvedSubnet {
185            input_principal: canonical_canister.clone(),
186            resolved_as: ResolvedSubnetSubject::Canister,
187            resolved_from: "routing_range".to_string(),
188            subnet,
189            matched_canister_principal: Some(canonical_canister),
190            matched_routing_range: Some(range.clone()),
191        })
192    }
193}
194
195pub(crate) fn routing_range_sorts_after(start: &[u8], end: &[u8]) -> bool {
196    start > end
197}
198
199fn range_contains_principal(
200    range: &crate::RoutingRange,
201    principal: &[u8],
202) -> Result<bool, CatalogError> {
203    let start = principal_bytes(&range.start_canister_id, "start_canister_id")?;
204    let end = principal_bytes(&range.end_canister_id, "end_canister_id")?;
205    Ok(start.as_slice() <= principal && principal <= end.as_slice())
206}