peat_mesh/security/
callsign.rs1use rand_core::{OsRng, RngCore};
9use std::collections::HashSet;
10
11pub const NATO_ALPHABET: [&str; 26] = [
13 "ALPHA", "BRAVO", "CHARLIE", "DELTA", "ECHO", "FOXTROT", "GOLF", "HOTEL", "INDIA", "JULIET",
14 "KILO", "LIMA", "MIKE", "NOVEMBER", "OSCAR", "PAPA", "QUEBEC", "ROMEO", "SIERRA", "TANGO",
15 "UNIFORM", "VICTOR", "WHISKEY", "XRAY", "YANKEE", "ZULU",
16];
17
18pub const MAX_CALLSIGN_LENGTH: usize = 11;
20
21pub const TOTAL_CALLSIGNS: usize = 2600;
23
24#[derive(Debug, Clone, Default)]
28pub struct CallsignGenerator {
29 used: HashSet<String>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum CallsignError {
35 InvalidFormat(String),
37 AlreadyInUse(String),
39 Exhausted,
41}
42
43impl std::fmt::Display for CallsignError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 CallsignError::InvalidFormat(s) => write!(f, "invalid callsign format: {}", s),
47 CallsignError::AlreadyInUse(s) => write!(f, "callsign already in use: {}", s),
48 CallsignError::Exhausted => write!(f, "all 2,600 callsigns exhausted"),
49 }
50 }
51}
52
53impl std::error::Error for CallsignError {}
54
55impl CallsignGenerator {
56 pub fn new() -> Self {
58 Self {
59 used: HashSet::new(),
60 }
61 }
62
63 pub fn generate(&mut self) -> Result<String, CallsignError> {
67 if self.used.len() >= TOTAL_CALLSIGNS {
68 return Err(CallsignError::Exhausted);
69 }
70
71 loop {
72 let callsign = Self::random_callsign();
73 if !self.used.contains(&callsign) {
74 self.used.insert(callsign.clone());
75 return Ok(callsign);
76 }
77 }
80 }
81
82 pub fn reserve(&mut self, callsign: &str) -> Result<(), CallsignError> {
87 let normalized = Self::normalize(callsign);
88
89 if !Self::is_valid_format(&normalized) {
90 return Err(CallsignError::InvalidFormat(callsign.to_string()));
91 }
92
93 if self.used.contains(&normalized) {
94 return Err(CallsignError::AlreadyInUse(callsign.to_string()));
95 }
96
97 self.used.insert(normalized);
98 Ok(())
99 }
100
101 pub fn release(&mut self, callsign: &str) -> bool {
105 let normalized = Self::normalize(callsign);
106 self.used.remove(&normalized)
107 }
108
109 pub fn is_available(&self, callsign: &str) -> bool {
111 let normalized = Self::normalize(callsign);
112 Self::is_valid_format(&normalized) && !self.used.contains(&normalized)
113 }
114
115 pub fn is_valid_format(callsign: &str) -> bool {
119 Self::parse(callsign).is_some()
120 }
121
122 pub fn parse(callsign: &str) -> Option<(usize, u8)> {
126 let normalized = Self::normalize(callsign);
127 let parts: Vec<&str> = normalized.split('-').collect();
128
129 if parts.len() != 2 {
130 return None;
131 }
132
133 let word = parts[0];
134 let num_str = parts[1];
135
136 let letter_idx = NATO_ALPHABET.iter().position(|&w| w == word)?;
138
139 if num_str.len() != 2 {
141 return None;
142 }
143 let number: u8 = num_str.parse().ok()?;
144 if number > 99 {
145 return None;
146 }
147
148 Some((letter_idx, number))
149 }
150
151 pub fn used_count(&self) -> usize {
153 self.used.len()
154 }
155
156 pub fn available_count(&self) -> usize {
158 TOTAL_CALLSIGNS.saturating_sub(self.used.len())
159 }
160
161 fn random_callsign() -> String {
163 let mut bytes = [0u8; 2];
164 OsRng.fill_bytes(&mut bytes);
165
166 let letter_idx = (bytes[0] as usize) % 26;
167 let number = bytes[1] % 100;
168
169 format!("{}-{:02}", NATO_ALPHABET[letter_idx], number)
170 }
171
172 fn normalize(callsign: &str) -> String {
174 callsign.trim().to_uppercase()
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_generate_callsign() {
184 let mut gen = CallsignGenerator::new();
185 let callsign = gen.generate().unwrap();
186
187 assert!(CallsignGenerator::is_valid_format(&callsign));
189 assert!(callsign.contains('-'));
190
191 let parts: Vec<&str> = callsign.split('-').collect();
192 assert_eq!(parts.len(), 2);
193 assert!(NATO_ALPHABET.contains(&parts[0]));
194
195 let num: u8 = parts[1].parse().unwrap();
196 assert!(num <= 99);
197 }
198
199 #[test]
200 fn test_generate_unique() {
201 let mut gen = CallsignGenerator::new();
202 let mut callsigns = HashSet::new();
203
204 for _ in 0..100 {
205 let cs = gen.generate().unwrap();
206 assert!(callsigns.insert(cs), "duplicate callsign generated");
207 }
208 }
209
210 #[test]
211 fn test_reserve() {
212 let mut gen = CallsignGenerator::new();
213
214 assert!(gen.reserve("ALPHA-01").is_ok());
215 assert!(gen.reserve("BRAVO-42").is_ok());
216
217 assert!(matches!(
219 gen.reserve("ALPHA-01"),
220 Err(CallsignError::AlreadyInUse(_))
221 ));
222
223 assert!(matches!(
225 gen.reserve("alpha-01"),
226 Err(CallsignError::AlreadyInUse(_))
227 ));
228 }
229
230 #[test]
231 fn test_reserve_invalid() {
232 let mut gen = CallsignGenerator::new();
233
234 assert!(matches!(
236 gen.reserve("INVALID"),
237 Err(CallsignError::InvalidFormat(_))
238 ));
239 assert!(matches!(
240 gen.reserve("ALPHA-100"),
241 Err(CallsignError::InvalidFormat(_))
242 ));
243 assert!(matches!(
244 gen.reserve("ALPHA-1"),
245 Err(CallsignError::InvalidFormat(_))
246 ));
247 assert!(matches!(
248 gen.reserve("BADWORD-01"),
249 Err(CallsignError::InvalidFormat(_))
250 ));
251 }
252
253 #[test]
254 fn test_release() {
255 let mut gen = CallsignGenerator::new();
256
257 gen.reserve("ZULU-99").unwrap();
258 assert!(!gen.is_available("ZULU-99"));
259
260 assert!(gen.release("ZULU-99"));
261 assert!(gen.is_available("ZULU-99"));
262
263 assert!(!gen.release("ZULU-99"));
265 }
266
267 #[test]
268 fn test_is_available() {
269 let mut gen = CallsignGenerator::new();
270
271 assert!(gen.is_available("CHARLIE-05"));
272 gen.reserve("CHARLIE-05").unwrap();
273 assert!(!gen.is_available("CHARLIE-05"));
274
275 assert!(!gen.is_available("INVALID-FORMAT"));
277 }
278
279 #[test]
280 fn test_parse() {
281 assert_eq!(CallsignGenerator::parse("ALPHA-00"), Some((0, 0)));
282 assert_eq!(CallsignGenerator::parse("BRAVO-01"), Some((1, 1)));
283 assert_eq!(CallsignGenerator::parse("ZULU-99"), Some((25, 99)));
284 assert_eq!(CallsignGenerator::parse("november-42"), Some((13, 42)));
285
286 assert_eq!(CallsignGenerator::parse("INVALID"), None);
287 assert_eq!(CallsignGenerator::parse("ALPHA-100"), None);
288 assert_eq!(CallsignGenerator::parse("ALPHA-1"), None);
289 }
290
291 #[test]
292 fn test_counts() {
293 let mut gen = CallsignGenerator::new();
294
295 assert_eq!(gen.used_count(), 0);
296 assert_eq!(gen.available_count(), TOTAL_CALLSIGNS);
297
298 gen.reserve("ALPHA-01").unwrap();
299 gen.reserve("BRAVO-02").unwrap();
300
301 assert_eq!(gen.used_count(), 2);
302 assert_eq!(gen.available_count(), TOTAL_CALLSIGNS - 2);
303 }
304
305 #[test]
306 fn test_nato_alphabet() {
307 assert_eq!(NATO_ALPHABET.len(), 26);
308 assert_eq!(NATO_ALPHABET[0], "ALPHA");
309 assert_eq!(NATO_ALPHABET[25], "ZULU");
310 }
311
312 #[test]
313 fn test_max_length() {
314 assert_eq!("NOVEMBER-99".len(), MAX_CALLSIGN_LENGTH);
316 }
317}