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
11pub const DIRECTORY_CASCADE_MAX_BYTES: usize = 16_384;
13pub const DIRECTORY_ENTRY_MAX_COUNT: usize = 1_000;
15
16#[derive(CandidType, Deserialize)]
23pub struct SubnetRegistryResponse(pub Vec<SubnetRegistryEntry>);
24
25#[derive(CandidType, Deserialize)]
32pub struct SubnetRegistryEntry {
33 pub pid: Principal,
34 pub role: CanisterRole,
35 pub record: CanisterInfo,
36}
37
38#[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#[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#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
65pub struct DirectoryProvenance {
66 pub fleet: FleetBinding,
67 pub source_root: Principal,
68}
69
70#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
75pub struct DirectoryEntryInput {
76 pub role: CanisterRole,
77 pub pid: Principal,
78}
79
80#[derive(CandidType, Debug, Deserialize, Eq, PartialEq)]
85pub struct DirectoryEntryResponse {
86 pub role: CanisterRole,
87 pub pid: Principal,
88}
89
90#[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#[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}