1use std::fmt;
9use std::str::FromStr;
10
11use umsh::core::{ChannelKey, MicSize, PublicKey, RegionCode, RouterHint};
12use umsh::crypto::{ChannelNameError, MAX_CHANNEL_NAME_LEN};
13use umsh::node::Channel;
14use umsh::ulcp_wire::items::Filter;
15
16pub fn parse_key32(text: &str) -> Result<[u8; 32], String> {
17 text.parse::<PublicKey>()
18 .map(|key| key.0)
19 .map_err(|error| format!("expected 44-char base58 or 64-char hex key: {error}"))
20}
21
22pub fn parse_hex<const N: usize>(text: &str) -> Result<[u8; N], String> {
23 let text = text.trim();
24 if text.len() != 2 * N || !text.chars().all(|c| c.is_ascii_hexdigit()) {
25 return Err(format!("expected {} hex characters, got {text:?}", 2 * N));
26 }
27 let mut out = [0u8; N];
28 for (index, byte) in out.iter_mut().enumerate() {
29 *byte = u8::from_str_radix(&text[2 * index..2 * index + 2], 16)
30 .map_err(|error| error.to_string())?;
31 }
32 Ok(out)
33}
34
35pub fn parse_u32(text: &str) -> Result<u32, String> {
37 match text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
38 Some(hex) => u32::from_str_radix(hex, 16),
39 None => text.parse(),
40 }
41 .map_err(|_| format!("invalid number: {text}"))
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct HintPrefixArg(pub Vec<u8>);
50
51impl FromStr for HintPrefixArg {
52 type Err = String;
53
54 fn from_str(text: &str) -> Result<Self, Self::Err> {
55 let bytes = BytesArg::from_str(text)?.0;
56 if bytes.is_empty() || bytes.len() > 3 {
57 return Err(format!(
58 "a hint prefix is one to three hex octets, got {} in {text:?}",
59 bytes.len()
60 ));
61 }
62 Ok(Self(bytes))
63 }
64}
65
66#[derive(Clone, Debug, Default, PartialEq, Eq)]
69pub struct BytesArg(pub Vec<u8>);
70
71impl FromStr for BytesArg {
72 type Err = String;
73
74 fn from_str(text: &str) -> Result<Self, Self::Err> {
75 let text = text.trim();
76 if !text.len().is_multiple_of(2) || !text.chars().all(|c| c.is_ascii_hexdigit()) {
77 return Err(format!(
78 "expected an even number of hex digits, got {text:?}"
79 ));
80 }
81 text.as_bytes()
82 .chunks_exact(2)
83 .map(|pair| {
84 u8::from_str_radix(core::str::from_utf8(pair).unwrap_or_default(), 16)
85 .map_err(|error| error.to_string())
86 })
87 .collect::<Result<Vec<u8>, String>>()
88 .map(Self)
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct AssignArg(pub u32, pub Vec<u8>);
95
96impl FromStr for AssignArg {
97 type Err = String;
98
99 fn from_str(text: &str) -> Result<Self, Self::Err> {
100 let Some((key, value)) = text.split_once('=') else {
101 return Err(format!("expected PROP=VALUE, got {text:?}"));
102 };
103 let key = key.trim().parse::<super::props::PropArg>()?;
107 let value = super::props::encode_value(key.0, value).map_err(|error| error.to_string())?;
108 Ok(Self(key.0, value))
109 }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct KeyArg(pub [u8; 32]);
115
116impl FromStr for KeyArg {
117 type Err = String;
118
119 fn from_str(text: &str) -> Result<Self, Self::Err> {
120 parse_key32(text).map(Self)
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub struct FilterArg(pub Filter);
128
129impl FromStr for FilterArg {
130 type Err = String;
131
132 fn from_str(text: &str) -> Result<Self, Self::Err> {
133 let fields: Vec<&str> = text
134 .split(|c: char| c == ':' || c.is_whitespace())
135 .filter(|field| !field.is_empty())
136 .collect();
137 let [kind, value] = fields[..] else {
138 return Err(format!("expected filter as TYPE:VALUE, got {text:?}"));
139 };
140 let filter = match kind {
141 "dest-hint" => Filter::DestHint(parse_hex::<3>(value)?),
142 "channel-id" => Filter::ChannelId(parse_hex::<2>(value)?),
143 "pkt-type" => Filter::PktType(
144 match value.strip_prefix("0x") {
145 Some(hex) => u8::from_str_radix(hex, 16),
146 None => value.parse(),
147 }
148 .map_err(|error| format!("pkt-type: {error}"))?,
149 ),
150 other => {
151 return Err(format!(
152 "unknown filter type {other:?}; expected dest-hint, channel-id, or pkt-type"
153 ));
154 }
155 };
156 Ok(Self(filter))
157 }
158}
159
160impl fmt::Display for FilterArg {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self.0 {
163 Filter::DestHint(hint) => write!(f, "dest-hint:{}", crate::output::hex(&hint)),
164 Filter::ChannelId(id) => write!(f, "channel-id:{}", crate::output::hex(&id)),
165 Filter::PktType(pkt_type) => write!(f, "pkt-type:{pkt_type}"),
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub struct PinArg(pub Option<u32>);
173
174impl FromStr for PinArg {
175 type Err = String;
176
177 fn from_str(text: &str) -> Result<Self, Self::Err> {
178 if text == "clear" {
179 return Ok(Self(None));
180 }
181 if text.len() == 6 && text.chars().all(|c| c.is_ascii_digit()) {
182 return text
183 .parse::<u32>()
184 .map(|pin| Self(Some(pin)))
185 .map_err(|error| error.to_string());
186 }
187 Err(format!("expected a 6-digit PIN or `clear`, got {text:?}"))
188 }
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub struct DutyLimitArg(pub u16);
194
195impl FromStr for DutyLimitArg {
196 type Err = String;
197
198 fn from_str(text: &str) -> Result<Self, Self::Err> {
199 if text == "off" {
200 return Ok(Self(u16::MAX));
201 }
202 text.parse::<u16>()
203 .map(Self)
204 .map_err(|_| format!("expected 0-65535 or `off`, got {text:?}"))
205 }
206}
207
208fn parse_region(text: &str) -> Result<String, String> {
213 text.parse::<RegionCode>()
214 .map_err(|error| format!("region {text:?}: {error}"))?;
215 Ok(text.to_owned())
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
221pub struct RegionListArg(pub Vec<String>);
222
223impl FromStr for RegionListArg {
224 type Err = String;
225
226 fn from_str(text: &str) -> Result<Self, Self::Err> {
227 if text.eq_ignore_ascii_case("none") {
228 return Ok(Self(Vec::new()));
229 }
230 text.split(',')
231 .map(parse_region)
232 .collect::<Result<Vec<_>, _>>()
233 .map(Self)
234 }
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct RegionArg(pub String);
240
241impl FromStr for RegionArg {
242 type Err = String;
243
244 fn from_str(text: &str) -> Result<Self, Self::Err> {
245 parse_region(text).map(Self)
246 }
247}
248
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub struct OptRegionArg(pub Option<RegionCode>);
252
253impl FromStr for OptRegionArg {
254 type Err = String;
255
256 fn from_str(text: &str) -> Result<Self, Self::Err> {
257 if text.eq_ignore_ascii_case("none") {
258 return Ok(Self(None));
259 }
260 text.parse::<RegionCode>()
262 .map(|code| Self(Some(code)))
263 .map_err(|error| format!("region {text:?}: {error}"))
264 }
265}
266
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
269pub struct MinRssiArg(pub Option<i16>);
270
271impl FromStr for MinRssiArg {
272 type Err = String;
273
274 fn from_str(text: &str) -> Result<Self, Self::Err> {
275 if text.eq_ignore_ascii_case("none") {
276 return Ok(Self(None));
277 }
278 text.parse::<i16>()
279 .map(|dbm| Self(Some(dbm)))
280 .map_err(|_| format!("expected dBm or `none`, got {text:?}"))
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub struct MinSnrArg(pub Option<i8>);
287
288impl FromStr for MinSnrArg {
289 type Err = String;
290
291 fn from_str(text: &str) -> Result<Self, Self::Err> {
292 if text.eq_ignore_ascii_case("none") {
293 return Ok(Self(None));
294 }
295 text.parse::<i8>()
296 .map(|db| Self(Some(db)))
297 .map_err(|_| format!("expected dB or `none`, got {text:?}"))
298 }
299}
300
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
303pub struct HexU16Arg(pub u16);
304
305impl FromStr for HexU16Arg {
306 type Err = String;
307
308 fn from_str(text: &str) -> Result<Self, Self::Err> {
309 parse_u32(text)?
310 .try_into()
311 .map(Self)
312 .map_err(|_| format!("value out of 16-bit range: {text}"))
313 }
314}
315
316#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub struct MicArg(pub MicSize);
319
320impl FromStr for MicArg {
321 type Err = String;
322
323 fn from_str(text: &str) -> Result<Self, Self::Err> {
324 match text {
325 "4" => Ok(Self(MicSize::Mic4)),
326 "8" => Ok(Self(MicSize::Mic8)),
327 "12" => Ok(Self(MicSize::Mic12)),
328 "16" => Ok(Self(MicSize::Mic16)),
329 other => Err(format!("expected 4, 8, 12, or 16, got {other:?}")),
330 }
331 }
332}
333
334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336pub struct RegionCodeArg(pub RegionCode);
337
338impl FromStr for RegionCodeArg {
339 type Err = String;
340
341 fn from_str(text: &str) -> Result<Self, Self::Err> {
342 text.parse::<RegionCode>()
343 .map(Self)
344 .map_err(|error| format!("region {text:?}: {error}"))
345 }
346}
347
348#[derive(Clone, Debug, PartialEq, Eq)]
355pub struct RouteArg(pub Vec<RouterHint>);
356
357impl FromStr for RouteArg {
358 type Err = String;
359
360 fn from_str(text: &str) -> Result<Self, Self::Err> {
361 let hops = text
362 .split(|c: char| c == ',' || c.is_whitespace())
363 .filter(|hop| !hop.is_empty())
364 .map(|hop| {
365 if hop.len() == 4 {
366 parse_hex::<2>(hop).map(RouterHint)
367 } else {
368 parse_key32(hop)
369 .map(|key| RouterHint::from_public_key(&PublicKey(key)))
370 .map_err(|error| {
371 format!("hop {hop:?}: 4 hex digits or a full key: {error}")
372 })
373 }
374 })
375 .collect::<Result<Vec<_>, String>>()?;
376 if hops.is_empty() {
377 return Err(String::from("a source route needs at least one hop"));
378 }
379 if hops.len() > MAX_ROUTE_HOPS {
382 return Err(format!(
383 "a source route carries at most {MAX_ROUTE_HOPS} hops, got {}",
384 hops.len()
385 ));
386 }
387 Ok(Self(hops))
388 }
389}
390
391const MAX_ROUTE_HOPS: usize = 15;
393
394#[derive(Clone, Debug, PartialEq, Eq)]
401pub struct ChannelArg(pub Channel);
402
403impl FromStr for ChannelArg {
404 type Err = String;
405
406 fn from_str(text: &str) -> Result<Self, Self::Err> {
407 if let Ok(key) = parse_key32(text) {
408 let channel = Channel::private(ChannelKey(key), "");
409 let named = Channel::private(
410 ChannelKey(key),
411 &crate::output::hex(&channel.channel_id().0),
412 );
413 return Ok(Self(named));
414 }
415 Channel::named(text).map(Self).map_err(|error| match error {
416 ChannelNameError::NotAscii => {
417 format!("channel {text:?}: names must be ASCII")
418 }
419 ChannelNameError::TooLong => {
420 format!("channel {text:?}: names are at most {MAX_CHANNEL_NAME_LEN} characters")
421 }
422 })
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 const KEY_HEX: &str = "c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4";
431
432 #[test]
433 fn parses_filters() {
434 assert_eq!(
435 "dest-hint:a1b2c3".parse::<FilterArg>().unwrap().0,
436 Filter::DestHint([0xA1, 0xB2, 0xC3])
437 );
438 assert_eq!(
439 "channel-id 9b68".parse::<FilterArg>().unwrap().0,
440 Filter::ChannelId([0x9B, 0x68])
441 );
442 assert_eq!(
443 "pkt-type:0x0a".parse::<FilterArg>().unwrap().0,
444 Filter::PktType(10)
445 );
446 assert!("src-hint:aabbcc".parse::<FilterArg>().is_err());
447 assert!("dest-hint:zzzzzz".parse::<FilterArg>().is_err());
448 }
449
450 #[test]
451 fn pin_requires_six_digits_or_clear() {
452 assert_eq!("042319".parse::<PinArg>().unwrap(), PinArg(Some(42_319)));
453 assert_eq!("clear".parse::<PinArg>().unwrap(), PinArg(None));
454 assert!("12345".parse::<PinArg>().is_err());
455 assert!("1234567".parse::<PinArg>().is_err());
456 assert!("abcdef".parse::<PinArg>().is_err());
457 }
458
459 #[test]
460 fn duty_limit_accepts_the_raw_scale_and_off() {
461 assert_eq!("655".parse::<DutyLimitArg>().unwrap().0, 655);
462 assert_eq!("off".parse::<DutyLimitArg>().unwrap().0, u16::MAX);
463 assert!("70000".parse::<DutyLimitArg>().is_err());
464 }
465
466 #[test]
467 fn region_lists_keep_every_string_form_as_written() {
468 assert_eq!(
469 "SJC,0x7853,Rogue Valley"
470 .parse::<RegionListArg>()
471 .unwrap()
472 .0,
473 vec!["SJC", "0x7853", "Rogue Valley"]
474 );
475 assert_eq!(
478 "none".parse::<RegionListArg>().unwrap().0,
479 Vec::<String>::new()
480 );
481 assert!("SJC,".parse::<RegionListArg>().is_err());
482 assert!("A".repeat(25).parse::<RegionArg>().is_err());
484 assert_eq!(
485 "Rogue Valley".parse::<RegionArg>().unwrap().0,
486 "Rogue Valley"
487 );
488 }
489
490 #[test]
491 fn gates_accept_their_clear_forms() {
492 assert_eq!("-110".parse::<MinRssiArg>().unwrap().0, Some(-110));
493 assert_eq!("none".parse::<MinRssiArg>().unwrap().0, None);
494 assert_eq!("-7".parse::<MinSnrArg>().unwrap().0, Some(-7));
495 assert_eq!("none".parse::<MinSnrArg>().unwrap().0, None);
496 assert!("loud".parse::<MinRssiArg>().is_err());
497 assert!("-40000".parse::<MinRssiArg>().is_err());
498 assert!("-200".parse::<MinSnrArg>().is_err());
499 }
500
501 #[test]
502 fn keys_accept_base58_and_hex() {
503 let hex = KEY_HEX.parse::<KeyArg>().unwrap();
504 assert_eq!(hex.0, [0xC4; 32]);
505 let base58 = PublicKey(hex.0).to_string();
506 assert_eq!(base58.parse::<KeyArg>().unwrap().0, [0xC4; 32]);
507 assert!("nonsense".parse::<KeyArg>().is_err());
508 }
509
510 #[test]
511 fn mic_sizes_are_named_by_their_byte_length() {
512 assert_eq!("4".parse::<MicArg>().unwrap().0, MicSize::Mic4);
513 assert_eq!("16".parse::<MicArg>().unwrap().0, MicSize::Mic16);
514 assert!("10".parse::<MicArg>().is_err());
515 assert!("mic8".parse::<MicArg>().is_err());
516 }
517
518 #[test]
519 fn routes_take_hints_or_whole_keys() {
520 let route = format!("a1b2,{KEY_HEX}").parse::<RouteArg>().unwrap();
521 assert_eq!(
522 route.0,
523 vec![RouterHint([0xA1, 0xB2]), RouterHint([0xC4, 0xC4])]
524 );
525 assert!("".parse::<RouteArg>().is_err());
528 assert!("a1b".parse::<RouteArg>().is_err());
529 assert!(vec!["a1b2"; 16].join(",").parse::<RouteArg>().is_err());
530 }
531
532 #[test]
533 fn channels_come_from_a_name_or_a_raw_key() {
534 let named = "public".parse::<ChannelArg>().unwrap();
535 assert_eq!(named.0, Channel::named("public").unwrap());
536 assert_eq!(named.0.name(), "public");
537
538 let private = KEY_HEX.parse::<ChannelArg>().unwrap();
539 assert_eq!(private.0.key().0, [0xC4; 32]);
540 assert!(!private.0.name().contains("c4c4"));
542 assert_eq!(
543 private.0.name(),
544 crate::output::hex(&private.0.channel_id().0)
545 );
546 }
547
548 #[test]
549 fn numbers_accept_hex_and_decimal() {
550 assert_eq!(parse_u32("915000").unwrap(), 915_000);
551 assert_eq!("0x1234".parse::<HexU16Arg>().unwrap().0, 0x1234);
552 assert!("0x1ffff".parse::<HexU16Arg>().is_err());
553 }
554}