Skip to main content

net_kit/
ip_stack.rs

1use std::fmt;
2
3/// The IP-stack capability currently available to the host.
4///
5/// This reflects which IP protocol versions the host has usable addresses /
6/// routes for, as reported by the underlying [`netwatch`] interface state
7/// (`have_v4` / `have_v6`). The semantics are identical across platforms: it
8/// describes local stack availability, not per-protocol Internet reachability.
9///
10/// [`netwatch`]: https://crates.io/crates/netwatch
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum IpStack {
13    /// Neither IPv4 nor IPv6 is available.
14    #[default]
15    None,
16    /// Only IPv4 is available.
17    V4Only,
18    /// Only IPv6 is available.
19    V6Only,
20    /// Both IPv4 and IPv6 are available.
21    DualStack,
22}
23
24impl IpStack {
25    /// Fold the two protocol-availability flags into a single [`IpStack`].
26    pub(crate) const fn from_flags(have_v4: bool, have_v6: bool) -> Self {
27        match (have_v4, have_v6) {
28            (true, true) => IpStack::DualStack,
29            (true, false) => IpStack::V4Only,
30            (false, true) => IpStack::V6Only,
31            (false, false) => IpStack::None,
32        }
33    }
34
35    /// The variant name as a static string (e.g. `"DualStack"`).
36    ///
37    /// Used both by the [`fmt::Display`] implementation and as a stable,
38    /// human-readable label when forwarding the value to logging.
39    pub const fn name(&self) -> &'static str {
40        match self {
41            IpStack::None => "None",
42            IpStack::V4Only => "V4Only",
43            IpStack::V6Only => "V6Only",
44            IpStack::DualStack => "DualStack",
45        }
46    }
47
48    /// Whether IPv4 is available (true for [`IpStack::V4Only`] and
49    /// [`IpStack::DualStack`]).
50    pub const fn has_ipv4(&self) -> bool {
51        matches!(self, IpStack::V4Only | IpStack::DualStack)
52    }
53
54    /// Whether IPv6 is available (true for [`IpStack::V6Only`] and
55    /// [`IpStack::DualStack`]).
56    pub const fn has_ipv6(&self) -> bool {
57        matches!(self, IpStack::V6Only | IpStack::DualStack)
58    }
59
60    /// Whether both IPv4 and IPv6 are available.
61    pub const fn is_dual_stack(&self) -> bool {
62        matches!(self, IpStack::DualStack)
63    }
64}
65
66impl fmt::Display for IpStack {
67    /// Renders the variant name, so `to_string()` yields e.g. `"DualStack"`
68    /// rather than a structural form.
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str(self.name())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::IpStack;
77
78    #[test]
79    fn from_flags_covers_all_combinations() {
80        assert_eq!(IpStack::from_flags(false, false), IpStack::None);
81        assert_eq!(IpStack::from_flags(true, false), IpStack::V4Only);
82        assert_eq!(IpStack::from_flags(false, true), IpStack::V6Only);
83        assert_eq!(IpStack::from_flags(true, true), IpStack::DualStack);
84    }
85
86    #[test]
87    fn default_is_none() {
88        assert_eq!(IpStack::default(), IpStack::None);
89    }
90
91    #[test]
92    fn has_ipv4_is_true_only_for_v4_and_dual() {
93        assert!(!IpStack::None.has_ipv4());
94        assert!(IpStack::V4Only.has_ipv4());
95        assert!(!IpStack::V6Only.has_ipv4());
96        assert!(IpStack::DualStack.has_ipv4());
97    }
98
99    #[test]
100    fn has_ipv6_is_true_only_for_v6_and_dual() {
101        assert!(!IpStack::None.has_ipv6());
102        assert!(!IpStack::V4Only.has_ipv6());
103        assert!(IpStack::V6Only.has_ipv6());
104        assert!(IpStack::DualStack.has_ipv6());
105    }
106
107    #[test]
108    fn is_dual_stack_is_true_only_for_dual() {
109        assert!(!IpStack::None.is_dual_stack());
110        assert!(!IpStack::V4Only.is_dual_stack());
111        assert!(!IpStack::V6Only.is_dual_stack());
112        assert!(IpStack::DualStack.is_dual_stack());
113    }
114
115    #[test]
116    fn name_and_display_agree() {
117        for variant in [
118            IpStack::None,
119            IpStack::V4Only,
120            IpStack::V6Only,
121            IpStack::DualStack,
122        ] {
123            assert_eq!(variant.name(), variant.to_string());
124        }
125        assert_eq!(IpStack::DualStack.name(), "DualStack");
126    }
127
128    #[test]
129    fn serde_round_trips() {
130        for variant in [
131            IpStack::None,
132            IpStack::V4Only,
133            IpStack::V6Only,
134            IpStack::DualStack,
135        ] {
136        }
137    }
138}