Skip to main content

canic_core/dto/
topology.rs

1use crate::{
2    dto::{canister::CanisterInfo, prelude::*},
3    ids::FleetBinding,
4};
5use serde::{
6    Deserializer,
7    de::{self, SeqAccess, Visitor},
8};
9use std::fmt;
10
11/// Maximum encoded bytes accepted by the state-cascade endpoint.
12pub const DIRECTORY_CASCADE_MAX_BYTES: usize = 16_384;
13/// Maximum entries accepted in one Directory cascade or page.
14pub const DIRECTORY_ENTRY_MAX_COUNT: usize = 1_000;
15
16//
17// SubnetRegistryResponse
18//
19// External subnet registry view.
20//
21
22#[derive(CandidType, Deserialize)]
23pub struct SubnetRegistryResponse(pub Vec<SubnetRegistryEntry>);
24
25//
26// SubnetRegistryEntry
27//
28// Subnet registry entry.
29//
30
31#[derive(CandidType, Deserialize)]
32pub struct SubnetRegistryEntry {
33    pub pid: Principal,
34    pub role: CanisterRole,
35    pub record: CanisterInfo,
36}
37
38//
39// FleetDirectoryInput
40//
41
42#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
43pub struct FleetDirectoryInput {
44    pub provenance: DirectoryProvenance,
45    #[serde(deserialize_with = "deserialize_directory_entries")]
46    pub entries: Vec<DirectoryEntryInput>,
47}
48
49//
50// SubnetDirectoryInput
51//
52
53#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
54pub struct SubnetDirectoryInput {
55    pub provenance: DirectoryProvenance,
56    #[serde(deserialize_with = "deserialize_directory_entries")]
57    pub entries: Vec<DirectoryEntryInput>,
58}
59
60//
61// DirectoryProvenance
62//
63
64#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
65pub struct DirectoryProvenance {
66    pub fleet: FleetBinding,
67    pub source_root: Principal,
68}
69
70//
71// DirectoryEntryInput
72//
73
74#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
75pub struct DirectoryEntryInput {
76    pub role: CanisterRole,
77    pub pid: Principal,
78}
79
80//
81// DirectoryEntryResponse
82//
83
84#[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
85pub struct DirectoryEntryResponse {
86    pub role: CanisterRole,
87    pub pid: Principal,
88}
89
90//
91// FleetDirectoryPageResponse
92//
93
94#[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
95pub struct FleetDirectoryPageResponse {
96    pub provenance: DirectoryProvenance,
97    #[serde(deserialize_with = "deserialize_directory_responses")]
98    pub entries: Vec<DirectoryEntryResponse>,
99    pub total: u64,
100}
101
102//
103// SubnetDirectoryPageResponse
104//
105
106#[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
107pub struct SubnetDirectoryPageResponse {
108    pub provenance: DirectoryProvenance,
109    #[serde(deserialize_with = "deserialize_directory_responses")]
110    pub entries: Vec<DirectoryEntryResponse>,
111    pub total: u64,
112}
113
114fn deserialize_directory_entries<'de, D>(
115    deserializer: D,
116) -> Result<Vec<DirectoryEntryInput>, D::Error>
117where
118    D: Deserializer<'de>,
119{
120    deserializer.deserialize_seq(BoundedDirectoryVisitor::<DirectoryEntryInput>::new())
121}
122
123fn deserialize_directory_responses<'de, D>(
124    deserializer: D,
125) -> Result<Vec<DirectoryEntryResponse>, D::Error>
126where
127    D: Deserializer<'de>,
128{
129    deserializer.deserialize_seq(BoundedDirectoryVisitor::<DirectoryEntryResponse>::new())
130}
131
132struct BoundedDirectoryVisitor<T> {
133    marker: std::marker::PhantomData<T>,
134}
135
136impl<T> BoundedDirectoryVisitor<T> {
137    const fn new() -> Self {
138        Self {
139            marker: std::marker::PhantomData,
140        }
141    }
142}
143
144impl<'de, T> Visitor<'de> for BoundedDirectoryVisitor<T>
145where
146    T: Deserialize<'de>,
147{
148    type Value = Vec<T>;
149
150    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151        write!(
152            formatter,
153            "at most {DIRECTORY_ENTRY_MAX_COUNT} Directory entries"
154        )
155    }
156
157    fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
158    where
159        A: SeqAccess<'de>,
160    {
161        let expected = sequence.size_hint().unwrap_or_default();
162        if expected > DIRECTORY_ENTRY_MAX_COUNT {
163            return Err(de::Error::invalid_length(expected, &self));
164        }
165
166        let mut entries = Vec::with_capacity(expected);
167        while let Some(entry) = sequence.next_element()? {
168            if entries.len() == DIRECTORY_ENTRY_MAX_COUNT {
169                return Err(de::Error::invalid_length(entries.len() + 1, &self));
170            }
171            entries.push(entry);
172        }
173        Ok(entries)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::ids::{AppId, CanonicalNetworkId, FleetId, FleetKey};
181
182    fn provenance() -> DirectoryProvenance {
183        DirectoryProvenance {
184            fleet: FleetBinding {
185                fleet: FleetKey {
186                    network: CanonicalNetworkId::public_ic(),
187                    fleet_id: FleetId::from_generated_bytes([1; 32]),
188                },
189                app: AppId::from("app"),
190            },
191            source_root: Principal::from_slice(&[2; 29]),
192        }
193    }
194
195    fn entry(index: usize) -> DirectoryEntryInput {
196        let principal_byte = u8::try_from(index % 255).expect("modulo result must fit in u8");
197        DirectoryEntryInput {
198            role: CanisterRole::from(format!("role_{index}")),
199            pid: Principal::from_slice(&[principal_byte; 29]),
200        }
201    }
202
203    #[test]
204    fn directory_input_accepts_the_exact_entry_limit() {
205        let input = FleetDirectoryInput {
206            provenance: provenance(),
207            entries: (0..DIRECTORY_ENTRY_MAX_COUNT).map(entry).collect(),
208        };
209        let bytes = candid::encode_one(&input).expect("encode bounded Directory");
210        let decoded: FleetDirectoryInput =
211            candid::decode_one(&bytes).expect("decode bounded Directory");
212
213        assert_eq!(decoded, input);
214    }
215
216    #[test]
217    fn directory_input_rejects_before_admitting_entry_overflow() {
218        let input = FleetDirectoryInput {
219            provenance: provenance(),
220            entries: (0..=DIRECTORY_ENTRY_MAX_COUNT).map(entry).collect(),
221        };
222        let bytes = candid::encode_one(&input).expect("encode oversized Directory");
223
224        assert!(candid::decode_one::<FleetDirectoryInput>(&bytes).is_err());
225    }
226}