1use std::fmt;
9use std::str::FromStr;
10
11use umsh::core::{PublicKey, RegionCode};
12use umsh::ulcp_wire::items::{Filter, PeerKeyEntry};
13
14pub fn parse_key32(text: &str) -> Result<[u8; 32], String> {
15 text.parse::<PublicKey>()
16 .map(|key| key.0)
17 .map_err(|error| format!("expected 44-char base58 or 64-char hex key: {error}"))
18}
19
20pub fn parse_hex<const N: usize>(text: &str) -> Result<[u8; N], String> {
21 let text = text.trim();
22 if text.len() != 2 * N || !text.chars().all(|c| c.is_ascii_hexdigit()) {
23 return Err(format!("expected {} hex characters, got {text:?}", 2 * N));
24 }
25 let mut out = [0u8; N];
26 for (index, byte) in out.iter_mut().enumerate() {
27 *byte = u8::from_str_radix(&text[2 * index..2 * index + 2], 16)
28 .map_err(|error| error.to_string())?;
29 }
30 Ok(out)
31}
32
33pub fn parse_bool(text: &str) -> Result<bool, String> {
34 match text {
35 "on" | "true" | "1" => Ok(true),
36 "off" | "false" | "0" => Ok(false),
37 other => Err(format!("expected on or off, got {other:?}")),
38 }
39}
40
41pub fn parse_u32(text: &str) -> Result<u32, String> {
43 match text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
44 Some(hex) => u32::from_str_radix(hex, 16),
45 None => text.parse(),
46 }
47 .map_err(|_| format!("invalid number: {text}"))
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct KeyArg(pub [u8; 32]);
53
54impl FromStr for KeyArg {
55 type Err = String;
56
57 fn from_str(text: &str) -> Result<Self, Self::Err> {
58 parse_key32(text).map(Self)
59 }
60}
61
62#[derive(Clone, Copy, PartialEq, Eq)]
65pub struct PeerArg(pub PeerKeyEntry);
66
67impl FromStr for PeerArg {
68 type Err = String;
69
70 fn from_str(text: &str) -> Result<Self, Self::Err> {
71 let fields: Vec<&str> = text
72 .split(|c: char| c == ',' || c.is_whitespace())
73 .filter(|field| !field.is_empty())
74 .collect();
75 let [public_key, k_enc, k_mic] = fields[..] else {
76 return Err(format!(
77 "expected peer as PUB,KENC,KMIC (got {} fields)",
78 fields.len()
79 ));
80 };
81 Ok(Self(PeerKeyEntry {
82 public_key: parse_key32(public_key)?,
83 k_enc: parse_hex::<16>(k_enc)?,
84 k_mic: parse_hex::<16>(k_mic)?,
85 }))
86 }
87}
88
89impl fmt::Debug for PeerArg {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(f, "PeerArg({}, <secrets>)", PublicKey(self.0.public_key))
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct FilterArg(pub Filter);
101
102impl FromStr for FilterArg {
103 type Err = String;
104
105 fn from_str(text: &str) -> Result<Self, Self::Err> {
106 let fields: Vec<&str> = text
107 .split(|c: char| c == ':' || c.is_whitespace())
108 .filter(|field| !field.is_empty())
109 .collect();
110 let [kind, value] = fields[..] else {
111 return Err(format!("expected filter as TYPE:VALUE, got {text:?}"));
112 };
113 let filter = match kind {
114 "dest-hint" => Filter::DestHint(parse_hex::<3>(value)?),
115 "channel-id" => Filter::ChannelId(parse_hex::<2>(value)?),
116 "pkt-type" => Filter::PktType(
117 match value.strip_prefix("0x") {
118 Some(hex) => u8::from_str_radix(hex, 16),
119 None => value.parse(),
120 }
121 .map_err(|error| format!("pkt-type: {error}"))?,
122 ),
123 other => {
124 return Err(format!(
125 "unknown filter type {other:?}; expected dest-hint, channel-id, or pkt-type"
126 ));
127 }
128 };
129 Ok(Self(filter))
130 }
131}
132
133impl fmt::Display for FilterArg {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self.0 {
136 Filter::DestHint(hint) => write!(f, "dest-hint:{}", crate::output::hex(&hint)),
137 Filter::ChannelId(id) => write!(f, "channel-id:{}", crate::output::hex(&id)),
138 Filter::PktType(pkt_type) => write!(f, "pkt-type:{pkt_type}"),
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct PinArg(pub Option<u32>);
146
147impl FromStr for PinArg {
148 type Err = String;
149
150 fn from_str(text: &str) -> Result<Self, Self::Err> {
151 if text == "clear" {
152 return Ok(Self(None));
153 }
154 if text.len() == 6 && text.chars().all(|c| c.is_ascii_digit()) {
155 return text
156 .parse::<u32>()
157 .map(|pin| Self(Some(pin)))
158 .map_err(|error| error.to_string());
159 }
160 Err(format!("expected a 6-digit PIN or `clear`, got {text:?}"))
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub struct DutyLimitArg(pub u16);
167
168impl FromStr for DutyLimitArg {
169 type Err = String;
170
171 fn from_str(text: &str) -> Result<Self, Self::Err> {
172 if text == "off" {
173 return Ok(Self(u16::MAX));
174 }
175 text.parse::<u16>()
176 .map(Self)
177 .map_err(|_| format!("expected 0-65535 or `off`, got {text:?}"))
178 }
179}
180
181fn parse_region(text: &str) -> Result<RegionCode, String> {
185 text.parse::<RegionCode>()
186 .map_err(|error| format!("region {text:?}: {error}"))
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct RegionListArg(pub Vec<RegionCode>);
193
194impl FromStr for RegionListArg {
195 type Err = String;
196
197 fn from_str(text: &str) -> Result<Self, Self::Err> {
198 if text.eq_ignore_ascii_case("none") {
199 return Ok(Self(Vec::new()));
200 }
201 text.split(',')
202 .map(parse_region)
203 .collect::<Result<Vec<_>, _>>()
204 .map(Self)
205 }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub struct OptRegionArg(pub Option<RegionCode>);
211
212impl FromStr for OptRegionArg {
213 type Err = String;
214
215 fn from_str(text: &str) -> Result<Self, Self::Err> {
216 if text.eq_ignore_ascii_case("none") {
217 return Ok(Self(None));
218 }
219 parse_region(text).map(|code| Self(Some(code)))
220 }
221}
222
223#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub struct MinRssiArg(pub Option<i16>);
226
227impl FromStr for MinRssiArg {
228 type Err = String;
229
230 fn from_str(text: &str) -> Result<Self, Self::Err> {
231 if text.eq_ignore_ascii_case("none") {
232 return Ok(Self(None));
233 }
234 text.parse::<i16>()
235 .map(|dbm| Self(Some(dbm)))
236 .map_err(|_| format!("expected dBm or `none`, got {text:?}"))
237 }
238}
239
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub struct MinSnrArg(pub Option<i8>);
243
244impl FromStr for MinSnrArg {
245 type Err = String;
246
247 fn from_str(text: &str) -> Result<Self, Self::Err> {
248 if text.eq_ignore_ascii_case("none") {
249 return Ok(Self(None));
250 }
251 text.parse::<i8>()
252 .map(|db| Self(Some(db)))
253 .map_err(|_| format!("expected dB or `none`, got {text:?}"))
254 }
255}
256
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260pub struct OnOffArg(pub bool);
261
262impl FromStr for OnOffArg {
263 type Err = String;
264
265 fn from_str(text: &str) -> Result<Self, Self::Err> {
266 parse_bool(text).map(Self)
267 }
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct HexU16Arg(pub u16);
273
274impl FromStr for HexU16Arg {
275 type Err = String;
276
277 fn from_str(text: &str) -> Result<Self, Self::Err> {
278 parse_u32(text)?
279 .try_into()
280 .map(Self)
281 .map_err(|_| format!("value out of 16-bit range: {text}"))
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 const KEY_HEX: &str = "c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4";
290
291 #[test]
292 fn parses_filters() {
293 assert_eq!(
294 "dest-hint:a1b2c3".parse::<FilterArg>().unwrap().0,
295 Filter::DestHint([0xA1, 0xB2, 0xC3])
296 );
297 assert_eq!(
298 "channel-id 9b68".parse::<FilterArg>().unwrap().0,
299 Filter::ChannelId([0x9B, 0x68])
300 );
301 assert_eq!(
302 "pkt-type:0x0a".parse::<FilterArg>().unwrap().0,
303 Filter::PktType(10)
304 );
305 assert!("src-hint:aabbcc".parse::<FilterArg>().is_err());
306 assert!("dest-hint:zzzzzz".parse::<FilterArg>().is_err());
307 }
308
309 #[test]
310 fn pin_requires_six_digits_or_clear() {
311 assert_eq!("042319".parse::<PinArg>().unwrap(), PinArg(Some(42_319)));
312 assert_eq!("clear".parse::<PinArg>().unwrap(), PinArg(None));
313 assert!("12345".parse::<PinArg>().is_err());
314 assert!("1234567".parse::<PinArg>().is_err());
315 assert!("abcdef".parse::<PinArg>().is_err());
316 }
317
318 #[test]
319 fn duty_limit_accepts_the_raw_scale_and_off() {
320 assert_eq!("655".parse::<DutyLimitArg>().unwrap().0, 655);
321 assert_eq!("off".parse::<DutyLimitArg>().unwrap().0, u16::MAX);
322 assert!("70000".parse::<DutyLimitArg>().is_err());
323 }
324
325 #[test]
326 fn region_lists_parse_every_code_form() {
327 assert_eq!(
328 "SJC,0x7853,Rogue Valley"
329 .parse::<RegionListArg>()
330 .unwrap()
331 .0,
332 vec![
333 RegionCode::from_iata("SJC").unwrap(),
334 RegionCode::from_u16(0x7853),
335 RegionCode::from_name("Rogue Valley"),
336 ]
337 );
338 assert_eq!("none".parse::<RegionListArg>().unwrap().0, Vec::new());
341 assert!("SJC,".parse::<RegionListArg>().is_err());
342 }
343
344 #[test]
345 fn gates_accept_their_clear_forms() {
346 assert_eq!("-110".parse::<MinRssiArg>().unwrap().0, Some(-110));
347 assert_eq!("none".parse::<MinRssiArg>().unwrap().0, None);
348 assert_eq!("-7".parse::<MinSnrArg>().unwrap().0, Some(-7));
349 assert_eq!("none".parse::<MinSnrArg>().unwrap().0, None);
350 assert!("loud".parse::<MinRssiArg>().is_err());
351 assert!("-40000".parse::<MinRssiArg>().is_err());
352 assert!("-200".parse::<MinSnrArg>().is_err());
353 }
354
355 #[test]
356 fn keys_accept_base58_and_hex() {
357 let hex = KEY_HEX.parse::<KeyArg>().unwrap();
358 assert_eq!(hex.0, [0xC4; 32]);
359 let base58 = PublicKey(hex.0).to_string();
360 assert_eq!(base58.parse::<KeyArg>().unwrap().0, [0xC4; 32]);
361 assert!("nonsense".parse::<KeyArg>().is_err());
362 }
363
364 #[test]
365 fn peer_entries_carry_both_pairwise_secrets() {
366 let peer: PeerArg = format!("{KEY_HEX},{},{}", "e0".repeat(16), "50".repeat(16))
367 .parse()
368 .unwrap();
369 assert_eq!(peer.0.public_key, [0xC4; 32]);
370 assert_eq!(peer.0.k_enc, [0xE0; 16]);
371 assert_eq!(peer.0.k_mic, [0x50; 16]);
372 assert!(!format!("{peer:?}").contains("e0e0"));
374 }
375
376 #[test]
377 fn numbers_accept_hex_and_decimal() {
378 assert_eq!(parse_u32("915000").unwrap(), 915_000);
379 assert_eq!("0x1234".parse::<HexU16Arg>().unwrap().0, 0x1234);
380 assert!("0x1ffff".parse::<HexU16Arg>().is_err());
381 }
382}