Skip to main content

net_kit/
ip_stack.rs

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