canic_subnet_catalog/resolver/
mod.rs1use 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#[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#[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#[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 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 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 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 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}