1use argh::{ArgsInfo, FromArgs};
6use fidl_fuchsia_net_ext as fnet_ext;
7#[cfg(not(feature = "fdomain"))]
8use fidl_fuchsia_net_interfaces_ext as finterfaces_ext;
9#[cfg(feature = "fdomain")]
10use fidl_fuchsia_net_interfaces_ext_fdomain as finterfaces_ext;
11use flex_fuchsia_net as fnet;
12use flex_fuchsia_net_interfaces as finterfaces;
13use flex_fuchsia_net_interfaces_admin as finterfaces_admin;
14use std::collections::HashMap;
15use std::convert::TryInto as _;
16use std::num::NonZeroU64;
17
18pub(crate) mod capture;
19pub(crate) mod dhcpd;
20pub(crate) mod dns;
21pub(crate) mod filter;
22
23#[derive(thiserror::Error, Clone, Debug)]
24#[error("{msg}")]
25pub struct UserFacingError {
26 pub msg: String,
27}
28
29pub fn user_facing_error(cause: impl Into<String>) -> anyhow::Error {
30 UserFacingError { msg: cause.into() }.into()
31}
32
33pub fn underlying_user_facing_error(error: &anyhow::Error) -> Option<UserFacingError> {
34 error.root_cause().downcast_ref::<UserFacingError>().cloned()
35}
36
37fn parse_ip_version_str(value: &str) -> Result<fnet::IpVersion, String> {
38 match &value.to_lowercase()[..] {
39 "ipv4" => Ok(fnet::IpVersion::V4),
40 "ipv6" => Ok(fnet::IpVersion::V6),
41 _ => Err("invalid IP version".to_string()),
42 }
43}
44
45#[derive(ArgsInfo, FromArgs, Debug)]
46pub struct Command {
48 #[argh(subcommand)]
49 pub cmd: CommandEnum,
50}
51
52#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
53#[argh(subcommand)]
54pub enum CommandEnum {
55 Capture(capture::Capture),
56 Filter(filter::Filter),
57 If(If),
58 Log(Log),
59 Neigh(Neigh),
60 Route(Route),
61 Rule(Rule),
62 Dhcp(Dhcp),
63 Dhcpd(dhcpd::Dhcpd),
64 Dns(dns::Dns),
65}
66
67#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
68#[argh(subcommand, name = "if")]
69pub struct If {
71 #[argh(subcommand)]
72 pub if_cmd: IfEnum,
73}
74
75#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
76#[argh(subcommand)]
77pub enum IfEnum {
78 Addr(IfAddr),
79 Config(IfConfig),
80 Disable(IfDisable),
81 Enable(IfEnable),
82 Get(IfGet),
83 Igmp(IfIgmp),
84 IpForward(IfIpForward),
85 List(IfList),
86 Mld(IfMld),
87 Add(IfAdd),
88 Remove(IfRemove),
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub enum InterfaceIdentifier {
93 Id(NonZeroU64),
94 Name(String),
95}
96
97impl InterfaceIdentifier {
98 pub async fn find_nicid<C>(&self, connector: &C) -> Result<NonZeroU64, anyhow::Error>
99 where
100 C: crate::ServiceConnector<finterfaces::StateMarker>,
101 {
102 match self {
103 Self::Id(id) => Ok(*id),
104 Self::Name(name) => {
105 let interfaces_state = crate::connect_with_context(connector).await?;
106 let stream = finterfaces_ext::event_stream_from_state::<
107 finterfaces_ext::AllInterest,
108 >(&interfaces_state, Default::default())?;
109 let response = finterfaces_ext::existing(
110 stream,
111 HashMap::<NonZeroU64, finterfaces_ext::PropertiesAndState<(), _>>::new(),
112 )
113 .await?;
114 response
115 .values()
116 .find_map(|properties_and_state| {
117 (&properties_and_state.properties.name == name)
118 .then_some(properties_and_state.properties.id)
119 })
120 .ok_or_else(|| user_facing_error(format!("No interface with name {}", name)))
121 }
122 }
123 }
124}
125
126impl core::str::FromStr for InterfaceIdentifier {
127 type Err = anyhow::Error;
128
129 fn from_str(s: &str) -> Result<Self, Self::Err> {
130 let nicid_parse_result = s.parse::<NonZeroU64>();
131 nicid_parse_result.map_or_else(
132 |nicid_parse_error| {
133 if !s.starts_with("name:") {
134 Err(user_facing_error(format!(
135 "Failed to parse as NICID (error: {}) or as interface name \
136 (error: interface names must be specified as `name:ifname`, where \
137 ifname is the actual interface name in this example)",
138 nicid_parse_error
139 )))
140 } else {
141 Ok(Self::Name(s["name:".len()..].to_string()))
142 }
143 },
144 |nicid| Ok(Self::Id(nicid)),
145 )
146 }
147}
148
149impl From<NonZeroU64> for InterfaceIdentifier {
150 fn from(nicid: NonZeroU64) -> InterfaceIdentifier {
151 InterfaceIdentifier::Id(nicid)
152 }
153}
154
155#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
156#[argh(subcommand, name = "addr")]
157pub struct IfAddr {
159 #[argh(subcommand)]
160 pub addr_cmd: IfAddrEnum,
161}
162
163#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
164#[argh(subcommand)]
165pub enum IfAddrEnum {
166 Add(IfAddrAdd),
167 Del(IfAddrDel),
168 Wait(IfAddrWait),
169}
170
171#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
172#[argh(subcommand, name = "add")]
173pub struct IfAddrAdd {
175 #[argh(positional, arg_name = "nicid or name:ifname")]
176 pub interface: InterfaceIdentifier,
177 #[argh(positional)]
178 pub addr: String,
179 #[argh(positional, from_str_fn(parse_netmask_or_prefix_length))]
180 pub prefix: u8,
181 #[argh(switch)]
182 pub no_subnet_route: bool,
184}
185
186#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
187#[argh(subcommand, name = "del")]
188pub struct IfAddrDel {
190 #[argh(positional, arg_name = "nicid or name:ifname")]
191 pub interface: InterfaceIdentifier,
192 #[argh(positional)]
193 pub addr: String,
194 #[argh(positional, from_str_fn(parse_netmask_or_prefix_length))]
195 pub prefix: Option<u8>,
196}
197
198#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
199#[argh(subcommand, name = "wait")]
200pub struct IfAddrWait {
205 #[argh(positional, arg_name = "nicid or name:ifname")]
206 pub interface: InterfaceIdentifier,
207 #[argh(switch)]
209 pub ipv6: bool,
210}
211
212#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
213#[argh(subcommand, name = "disable")]
214pub struct IfDisable {
216 #[argh(positional, arg_name = "nicid or name:ifname")]
217 pub interface: InterfaceIdentifier,
218}
219
220#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
221#[argh(subcommand, name = "enable")]
222pub struct IfEnable {
224 #[argh(positional, arg_name = "nicid or name:ifname")]
225 pub interface: InterfaceIdentifier,
226}
227
228#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
229#[argh(subcommand, name = "get")]
230pub struct IfGet {
232 #[argh(positional, arg_name = "nicid or name:ifname")]
233 pub interface: InterfaceIdentifier,
234}
235
236#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
238#[argh(subcommand, name = "igmp")]
239pub struct IfIgmp {
241 #[argh(subcommand)]
242 pub cmd: IfIgmpEnum,
243}
244
245#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
247#[argh(subcommand)]
248pub enum IfIgmpEnum {
249 Get(IfIgmpGet),
250 Set(IfIgmpSet),
251}
252
253#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
255#[argh(subcommand, name = "get")]
256pub struct IfIgmpGet {
258 #[argh(positional, arg_name = "nicid or name:ifname")]
259 pub interface: InterfaceIdentifier,
260}
261
262#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
264#[argh(subcommand, name = "set")]
265pub struct IfIgmpSet {
267 #[argh(positional, arg_name = "nicid or name:ifname")]
268 pub interface: InterfaceIdentifier,
269
270 #[argh(option, from_str_fn(parse_igmp_version))]
272 pub version: Option<finterfaces_admin::IgmpVersion>,
273}
274
275fn parse_igmp_version(s: &str) -> Result<finterfaces_admin::IgmpVersion, String> {
276 match s.parse::<u8>() {
277 Err(err) => Err(format!("Failed to parse IGMP version (error: {})", err)),
278 Ok(v) => match v {
279 1 => Ok(finterfaces_admin::IgmpVersion::V1),
280 2 => Ok(finterfaces_admin::IgmpVersion::V2),
281 3 => Ok(finterfaces_admin::IgmpVersion::V3),
282 v => Err(format!("Invalid IGMP version ({}). Valid values: 1, 2, 3", v)),
283 },
284 }
285}
286
287#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
289#[argh(subcommand, name = "ip-forward")]
290pub struct IfIpForward {
292 #[argh(subcommand)]
293 pub cmd: IfIpForwardEnum,
294}
295
296#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
298#[argh(subcommand)]
299pub enum IfIpForwardEnum {
300 Get(IfIpForwardGet),
301 Set(IfIpForwardSet),
302}
303
304#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
306#[argh(subcommand, name = "get")]
307pub struct IfIpForwardGet {
309 #[argh(positional, arg_name = "nicid or name:ifname")]
310 pub interface: InterfaceIdentifier,
311
312 #[argh(positional, from_str_fn(parse_ip_version_str))]
313 pub ip_version: fnet::IpVersion,
314}
315
316#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
318#[argh(subcommand, name = "set")]
319pub struct IfIpForwardSet {
321 #[argh(positional, arg_name = "nicid or name:ifname")]
322 pub interface: InterfaceIdentifier,
323
324 #[argh(positional, from_str_fn(parse_ip_version_str))]
325 pub ip_version: fnet::IpVersion,
326
327 #[argh(positional)]
328 pub enable: bool,
329}
330
331#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
332#[argh(subcommand, name = "list")]
333pub struct IfList {
335 #[argh(positional)]
336 pub name_pattern: Option<String>,
337}
338
339#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
340#[argh(subcommand, name = "add")]
341pub struct IfAdd {
343 #[argh(subcommand)]
344 pub cmd: IfAddEnum,
345}
346
347#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
348#[argh(subcommand)]
349pub enum IfAddEnum {
350 Blackhole(IfBlackholeAdd),
351}
352
353#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
354#[argh(subcommand, name = "blackhole")]
355pub struct IfBlackholeAdd {
357 #[argh(positional)]
358 pub name: String,
359}
360
361#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
362#[argh(subcommand, name = "remove")]
363pub struct IfRemove {
365 #[argh(positional, arg_name = "nicid or name:ifname")]
366 pub interface: InterfaceIdentifier,
367}
368
369#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
371#[argh(subcommand, name = "mld")]
372pub struct IfMld {
374 #[argh(subcommand)]
375 pub cmd: IfMldEnum,
376}
377
378#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
380#[argh(subcommand)]
381pub enum IfMldEnum {
382 Get(IfMldGet),
383 Set(IfMldSet),
384}
385
386#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
388#[argh(subcommand, name = "get")]
389pub struct IfMldGet {
391 #[argh(positional, arg_name = "nicid or name:ifname")]
392 pub interface: InterfaceIdentifier,
393}
394
395#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
397#[argh(subcommand, name = "set")]
398pub struct IfMldSet {
400 #[argh(positional, arg_name = "nicid or name:ifname")]
401 pub interface: InterfaceIdentifier,
402
403 #[argh(option, from_str_fn(parse_mld_version))]
405 pub version: Option<finterfaces_admin::MldVersion>,
406}
407
408fn parse_mld_version(s: &str) -> Result<finterfaces_admin::MldVersion, String> {
409 match s.parse::<u8>() {
410 Err(err) => Err(format!("Failed to parse MLD version (error: {})", err)),
411 Ok(v) => match v {
412 1 => Ok(finterfaces_admin::MldVersion::V1),
413 2 => Ok(finterfaces_admin::MldVersion::V2),
414 v => Err(format!("Invalid MLD version ({}). Valid values: 1, 2", v)),
415 },
416 }
417}
418
419#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
420#[argh(subcommand, name = "config")]
421pub struct IfConfig {
423 #[argh(positional, arg_name = "nicid or name:ifname")]
424 pub interface: InterfaceIdentifier,
425
426 #[argh(subcommand)]
427 pub cmd: IfConfigEnum,
428}
429
430#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
431#[argh(subcommand)]
432pub enum IfConfigEnum {
434 Set(IfConfigSet),
435 Get(IfConfigGet),
436}
437
438#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
439#[argh(subcommand, name = "set")]
440pub struct IfConfigSet {
460 #[argh(positional)]
462 pub options: Vec<String>,
463}
464
465#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
466#[argh(subcommand, name = "get")]
467pub struct IfConfigGet {}
469
470#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
471#[argh(subcommand, name = "log")]
472pub struct Log {
474 #[argh(subcommand)]
475 pub log_cmd: LogEnum,
476}
477
478#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
479#[argh(subcommand)]
480pub enum LogEnum {
481 SetPackets(LogSetPackets),
482}
483
484#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
485#[argh(subcommand, name = "set-packets")]
486pub struct LogSetPackets {
488 #[argh(positional)]
489 pub enabled: bool,
490}
491
492#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
493#[argh(subcommand, name = "neigh")]
494pub struct Neigh {
496 #[argh(subcommand)]
497 pub neigh_cmd: NeighEnum,
498}
499
500#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
501#[argh(subcommand)]
502pub enum NeighEnum {
503 Add(NeighAdd),
504 Clear(NeighClear),
505 Del(NeighDel),
506 List(NeighList),
507 Watch(NeighWatch),
508 Config(NeighConfig),
509}
510
511#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
512#[argh(subcommand, name = "add")]
513pub struct NeighAdd {
515 #[argh(positional, arg_name = "nicid or name:ifname")]
516 pub interface: InterfaceIdentifier,
517 #[argh(positional)]
518 pub ip: fnet_ext::IpAddress,
519 #[argh(positional)]
520 pub mac: fnet_ext::MacAddress,
521}
522
523#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
524#[argh(subcommand, name = "clear")]
525pub struct NeighClear {
527 #[argh(positional, arg_name = "nicid or name:ifname")]
528 pub interface: InterfaceIdentifier,
529
530 #[argh(positional, from_str_fn(parse_ip_version_str))]
531 pub ip_version: fnet::IpVersion,
532}
533
534#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
535#[argh(subcommand, name = "list")]
536pub struct NeighList {}
538
539#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
540#[argh(subcommand, name = "del")]
541pub struct NeighDel {
543 #[argh(positional, arg_name = "nicid or name:ifname")]
544 pub interface: InterfaceIdentifier,
545 #[argh(positional)]
546 pub ip: fnet_ext::IpAddress,
547}
548
549#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
550#[argh(subcommand, name = "watch")]
551pub struct NeighWatch {}
553
554#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
555#[argh(subcommand, name = "config")]
556pub struct NeighConfig {
558 #[argh(subcommand)]
559 pub neigh_config_cmd: NeighConfigEnum,
560}
561
562#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
563#[argh(subcommand)]
564pub enum NeighConfigEnum {
565 Get(NeighGetConfig),
566 Update(NeighUpdateConfig),
567}
568
569#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
570#[argh(subcommand, name = "get")]
571pub struct NeighGetConfig {
573 #[argh(positional, arg_name = "nicid or name:ifname")]
574 pub interface: InterfaceIdentifier,
575
576 #[argh(positional, from_str_fn(parse_ip_version_str))]
577 pub ip_version: fnet::IpVersion,
578}
579
580#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
581#[argh(subcommand, name = "update")]
582pub struct NeighUpdateConfig {
584 #[argh(positional, arg_name = "nicid or name:ifname")]
585 pub interface: InterfaceIdentifier,
586
587 #[argh(positional, from_str_fn(parse_ip_version_str))]
588 pub ip_version: fnet::IpVersion,
589
590 #[argh(option)]
593 pub base_reachable_time: Option<i64>,
594}
595
596#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
597#[argh(subcommand, name = "route")]
598pub struct Route {
600 #[argh(subcommand)]
601 pub route_cmd: RouteEnum,
602}
603
604#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
605#[argh(subcommand)]
606pub enum RouteEnum {
607 List(RouteList),
608 Add(RouteAdd),
609 Del(RouteDel),
610}
611
612fn parse_netmask_or_prefix_length(s: &str) -> Result<u8, String> {
613 let netmask_parse_result = s.parse::<std::net::IpAddr>();
614 let prefix_len_parse_result = s.parse::<u8>();
615 match (netmask_parse_result, prefix_len_parse_result) {
616 (Err(netmask_parse_error), Err(prefix_len_parse_error)) => Err(format!(
617 "Failed to parse as netmask (error: {}) or prefix length (error: {})",
618 netmask_parse_error, prefix_len_parse_error,
619 )),
620 (Ok(_), Ok(_)) => Err(format!(
621 "Input parses both as netmask and as prefix length. This should never happen."
622 )),
623 (Ok(netmask), Err(_)) => Ok(subnet_mask_to_prefix_length(netmask)),
624 (Err(_), Ok(prefix_len)) => Ok(prefix_len),
625 }
626}
627
628fn subnet_mask_to_prefix_length(addr: std::net::IpAddr) -> u8 {
629 match addr {
630 std::net::IpAddr::V4(addr) => (!u32::from_be_bytes(addr.octets())).leading_zeros(),
631 std::net::IpAddr::V6(addr) => (!u128::from_be_bytes(addr.octets())).leading_zeros(),
632 }
633 .try_into()
634 .unwrap()
635}
636
637#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
638#[argh(subcommand, name = "list")]
639pub struct RouteList {}
641
642macro_rules! route_struct {
643 ($ty_name:ident, $name:literal, $comment:expr) => {
644 #[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
645 #[argh(subcommand, name = $name)]
646 #[doc = $comment]
647 pub struct $ty_name {
648 #[argh(option)]
649 pub destination: std::net::IpAddr,
651 #[argh(
652 option,
653 arg_name = "netmask or prefix length",
654 from_str_fn(parse_netmask_or_prefix_length),
655 long = "netmask"
656 )]
657 pub prefix_len: u8,
659 #[argh(option)]
660 pub gateway: Option<std::net::IpAddr>,
662 #[argh(option, arg_name = "nicid or name:ifname", long = "nicid")]
663 pub interface: InterfaceIdentifier,
665 #[argh(option)]
666 pub metric: Option<u32>,
668 }
669 };
670}
671
672route_struct!(RouteAdd, "add", "adds a route to the route table");
674route_struct!(RouteDel, "del", "deletes a route from the route table");
675
676#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
677#[argh(subcommand, name = "rule")]
678pub struct Rule {
680 #[argh(subcommand)]
681 pub rule_cmd: RuleEnum,
682}
683
684#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
685#[argh(subcommand)]
686pub enum RuleEnum {
687 List(RuleList),
688}
689
690#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
691#[argh(subcommand, name = "list")]
692pub struct RuleList {}
694
695#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
696#[argh(subcommand, name = "dhcp")]
697pub struct Dhcp {
699 #[argh(subcommand)]
700 pub dhcp_cmd: DhcpEnum,
701}
702
703#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
704#[argh(subcommand)]
705pub enum DhcpEnum {
706 Start(DhcpStart),
707 Stop(DhcpStop),
708}
709
710#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
711#[argh(subcommand, name = "start")]
712pub struct DhcpStart {
714 #[argh(positional, arg_name = "nicid or name:ifname")]
715 pub interface: InterfaceIdentifier,
716}
717
718#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
719#[argh(subcommand, name = "stop")]
720pub struct DhcpStop {
722 #[argh(positional, arg_name = "nicid or name:ifname")]
723 pub interface: InterfaceIdentifier,
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use assert_matches::assert_matches;
730 use test_case::test_case;
731
732 #[test_case("24", 24 ; "from prefix length")]
733 #[test_case("255.255.254.0", 23 ; "from ipv4 netmask")]
734 #[test_case("ffff:fff0::", 28 ; "from ipv6 netmask")]
735 fn parse_prefix_len(to_parse: &str, want: u8) {
736 let got = parse_netmask_or_prefix_length(to_parse).unwrap();
737 assert_eq!(got, want)
738 }
739
740 proptest::proptest! {
741 #[test]
742 fn cant_parse_as_both_netmask_and_prefix_len(s: String) {
743 let netmask_parse_result = s.parse::<std::net::IpAddr>();
744 let prefix_len_parse_result = s.parse::<u8>();
745 assert_matches!((netmask_parse_result, prefix_len_parse_result), (Ok(_), Err(_)) | (Err(_), Ok(_)) | (Err(_), Err(_)));
746 }
747 }
748
749 #[test_case("1", InterfaceIdentifier::Id(NonZeroU64::new(1).unwrap()) ; "as nicid")]
750 #[test_case("name:lo", InterfaceIdentifier::Name("lo".to_string()) ; "as ifname")]
751 #[test_case("name:name:lo", InterfaceIdentifier::Name("name:lo".to_string()) ; "as ifname with 'name:' as part of name")]
752 #[test_case("name:1", InterfaceIdentifier::Name("1".to_string()) ; "as numerical ifname")]
753 fn parse_interface(to_parse: &str, want: InterfaceIdentifier) {
754 let got = to_parse.parse::<InterfaceIdentifier>().unwrap();
755 assert_eq!(got, want)
756 }
757
758 #[test]
759 fn parse_interface_without_prefix_fails() {
760 assert_matches!("lo".parse::<InterfaceIdentifier>(), Err(_))
761 }
762
763 #[test]
764 fn parse_interface_zero_fails() {
765 assert_matches!("0".parse::<InterfaceIdentifier>(), Err(_))
766 }
767
768 #[test_case("0", Err("Invalid IGMP version (0). Valid values: 1, 2, 3".to_string()); "input_0")]
769 #[test_case("1", Ok(finterfaces_admin::IgmpVersion::V1); "input_1")]
770 #[test_case("2", Ok(finterfaces_admin::IgmpVersion::V2); "input_2")]
771 #[test_case("3", Ok(finterfaces_admin::IgmpVersion::V3); "input_3")]
772 #[test_case("4", Err("Invalid IGMP version (4). Valid values: 1, 2, 3".to_string()); "input_4")]
773 #[test_case("a", Err("Failed to parse IGMP version (error: invalid digit found in string)".to_string()); "input_a")]
774 fn igmp_version(s: &str, expected: Result<finterfaces_admin::IgmpVersion, String>) {
775 assert_eq!(parse_igmp_version(s), expected)
776 }
777
778 #[test_case("0", Err("Invalid MLD version (0). Valid values: 1, 2".to_string()); "input_0")]
779 #[test_case("1", Ok(finterfaces_admin::MldVersion::V1); "input_1")]
780 #[test_case("2", Ok(finterfaces_admin::MldVersion::V2); "input_2")]
781 #[test_case("3", Err("Invalid MLD version (3). Valid values: 1, 2".to_string()); "input_3")]
782 #[test_case("a", Err("Failed to parse MLD version (error: invalid digit found in string)".to_string()); "input_a")]
783 fn mld_version(s: &str, expected: Result<finterfaces_admin::MldVersion, String>) {
784 assert_eq!(parse_mld_version(s), expected)
785 }
786}