Skip to main content

strat9_abi/
ip.rs

1//! IPv4/IPv6 literal parsing helpers.
2//!
3//! These functions parse IP address literals from strings into byte arrays
4//! suitable for network operations. They handle both dotted-decimal (IPv4)
5//! and colon-hex (IPv6) formats.
6//!
7//! # Examples
8//!
9//! ```ignore
10//! use strat9_abi::ip::{parse_ipv4_literal, parse_ipv6_literal};
11//!
12//! // IPv4
13//! assert_eq!(parse_ipv4_literal("192.168.1.10"), Some([192, 168, 1, 10]));
14//! assert_eq!(parse_ipv4_literal("10.0.0.1"), Some([10, 0, 0, 1]));
15//! assert_eq!(parse_ipv4_literal("256.1.1.1"), None); // octet > 255
16//!
17//! // IPv6
18//! assert_eq!(
19//!     parse_ipv6_literal("::1"),
20//!     Some([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
21//! );
22//! assert_eq!(
23//!     parse_ipv6_literal("fe80::1"),
24//!     Some([0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
25//! );
26//! ```
27
28/// Returns true when the input looks like a dotted IPv4 literal candidate.
29///
30/// This is a fast pre-filter: it checks that the string contains only
31/// digits and dots, and has at least one dot. It does NOT validate the
32/// actual address format — use [`parse_ipv4_literal`] for that.
33///
34/// # Example
35///
36/// ```ignore
37/// assert!(is_ipv4_literal_candidate("192.168.1.1"));
38/// assert!(!is_ipv4_literal_candidate("hello"));
39/// assert!(is_ipv4_literal_candidate("999.999.999.999")); // passes filter, fails parse
40/// ```
41pub fn is_ipv4_literal_candidate(s: &str) -> bool {
42    let bytes = s.as_bytes();
43    !bytes.is_empty()
44        && bytes.iter().all(|b| b.is_ascii_digit() || *b == b'.')
45        && bytes.contains(&b'.')
46}
47
48/// Returns true when the input looks like an IPv6 literal candidate.
49///
50/// This is a fast pre-filter: it checks that the string contains only
51/// hex digits, colons, and dots, and has at least one colon.
52///
53/// # Limitations
54///
55/// - IPv4-mapped/embedded forms (e.g. `::ffff:192.168.1.1`) are **not** recognised
56///   as candidates: the embedded decimal octets are not valid hex digits.
57/// - Zone IDs (e.g. `fe80::1%eth0`) are **not** recognised: `%` is not in the
58///   allowed character set.
59pub fn is_ipv6_literal_candidate(s: &str) -> bool {
60    let bytes = s.as_bytes();
61    !bytes.is_empty()
62        && bytes
63            .iter()
64            .all(|b| b.is_ascii_hexdigit() || *b == b':' || *b == b'.')
65        && bytes.contains(&b':')
66}
67
68/// Parses an IPv4 literal into network-order octets.
69///
70/// Input must be in standard dotted-decimal format: `A.B.C.D` where
71/// each octet is 0-255.
72///
73/// Returns `None` if the format is invalid.
74///
75/// # Example
76///
77/// ```ignore
78/// assert_eq!(parse_ipv4_literal("192.168.1.10"), Some([192, 168, 1, 10]));
79/// assert_eq!(parse_ipv4_literal("10.0.0.1"), Some([10, 0, 0, 1]));
80/// assert_eq!(parse_ipv4_literal("0.0.0.0"), Some([0, 0, 0, 0]));
81/// assert_eq!(parse_ipv4_literal("255.255.255.255"), Some([255, 255, 255, 255]));
82///
83/// // Invalid formats
84/// assert_eq!(parse_ipv4_literal(""), None);
85/// assert_eq!(parse_ipv4_literal("192.168.1"), None);      // too few octets
86/// assert_eq!(parse_ipv4_literal("192.168.1.1.1"), None);  // too many octets
87/// assert_eq!(parse_ipv4_literal("256.1.1.1"), None);      // octet > 255
88/// assert_eq!(parse_ipv4_literal("abc.def.ghi.jkl"), None); // non-numeric
89/// ```
90pub fn parse_ipv4_literal(s: &str) -> Option<[u8; 4]> {
91    let mut octets = [0u8; 4];
92    let mut idx = 0usize;
93    let mut val: u16 = 0;
94    let mut has_digit = false;
95
96    for &b in s.as_bytes() {
97        if b == b'.' {
98            if !has_digit || idx >= 3 || val > 255 {
99                return None;
100            }
101            octets[idx] = val as u8;
102            idx += 1;
103            val = 0;
104            has_digit = false;
105            continue;
106        }
107
108        if !b.is_ascii_digit() {
109            return None;
110        }
111
112        val = val.checked_mul(10)?.checked_add((b - b'0') as u16)?;
113        has_digit = true;
114    }
115
116    if !has_digit || idx != 3 || val > 255 {
117        return None;
118    }
119
120    octets[3] = val as u8;
121    Some(octets)
122}
123
124/// Parses a hex string of 1-4 digits into a `u16`.
125fn parse_hex_u16(s: &str) -> Option<u16> {
126    if s.is_empty() || s.len() > 4 {
127        return None;
128    }
129
130    let mut value = 0u16;
131    for &b in s.as_bytes() {
132        let digit = match b {
133            b'0'..=b'9' => (b - b'0') as u16,
134            b'a'..=b'f' => 10 + (b - b'a') as u16,
135            b'A'..=b'F' => 10 + (b - b'A') as u16,
136            _ => return None,
137        };
138        value = value.checked_mul(16)?.checked_add(digit)?;
139    }
140
141    Some(value)
142}
143
144/// Parses an IPv6 literal into network-order octets.
145///
146/// Supports standard colon-hex notation (RFC 4291) with `::` compression.
147///
148/// # Limitations
149///
150/// - IPv4-mapped/embedded forms (e.g. `::ffff:192.168.1.1`) are **not** supported;
151///   only pure colon-hex notation is accepted.
152/// - Zone IDs (e.g. `fe80::1%eth0`) are **not** supported; the zone identifier
153///   must be stripped before calling this function.
154///
155/// # Example
156///
157/// ```ignore
158/// assert_eq!(
159///     parse_ipv6_literal("2001:0db8:0000:0000:0000:ff00:0042:8329"),
160///     Some([
161///         0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00,
162///         0x00, 0x00, 0xff, 0x00, 0x00, 0x42, 0x83, 0x29,
163///     ])
164/// );
165///
166/// // Compressed forms
167/// assert_eq!(
168///     parse_ipv6_literal("::1"),
169///     Some([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
170/// );
171/// assert_eq!(
172///     parse_ipv6_literal("fe80::5054:ff:fe12:3456"),
173///     Some([
174///         0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
175///         0x50, 0x54, 0x00, 0xff, 0xfe, 0x12, 0x34, 0x56,
176///     ])
177/// );
178///
179/// // Loopback
180/// assert_eq!(
181///     parse_ipv6_literal("::1"),
182///     Some([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
183/// );
184/// ```
185pub fn parse_ipv6_literal(s: &str) -> Option<[u8; 16]> {
186    let mut groups = [0u16; 8];
187
188    if let Some(dc) = s.find("::") {
189        let left = &s[..dc];
190        let right = &s[dc + 2..];
191        let mut left_count = 0usize;
192        if !left.is_empty() {
193            for part in left.split(':') {
194                if left_count >= 8 {
195                    return None;
196                }
197                groups[left_count] = parse_hex_u16(part)?;
198                left_count += 1;
199            }
200        }
201
202        let mut right_groups = [0u16; 8];
203        let mut right_count = 0usize;
204        if !right.is_empty() {
205            for part in right.split(':') {
206                if right_count >= 8 {
207                    return None;
208                }
209                right_groups[right_count] = parse_hex_u16(part)?;
210                right_count += 1;
211            }
212        }
213
214        if left_count + right_count > 8 {
215            return None;
216        }
217
218        let start = 8 - right_count;
219        for i in 0..right_count {
220            groups[start + i] = right_groups[i];
221        }
222    } else {
223        let mut count = 0usize;
224        for part in s.split(':') {
225            if count >= 8 {
226                return None;
227            }
228            groups[count] = parse_hex_u16(part)?;
229            count += 1;
230        }
231        if count != 8 {
232            return None;
233        }
234    }
235
236    let mut octets = [0u8; 16];
237    for (idx, &group) in groups.iter().enumerate() {
238        octets[2 * idx] = (group >> 8) as u8;
239        octets[2 * idx + 1] = (group & 0xFF) as u8;
240    }
241
242    Some(octets)
243}
244
245#[cfg(test)]
246mod tests {
247    use super::{parse_ipv4_literal, parse_ipv6_literal};
248
249    #[test]
250    fn parses_ipv4_literal() {
251        assert_eq!(parse_ipv4_literal("192.168.1.10"), Some([192, 168, 1, 10]));
252    }
253
254    #[test]
255    fn parses_full_ipv6_literal() {
256        assert_eq!(
257            parse_ipv6_literal("2001:0db8:0000:0000:0000:ff00:0042:8329"),
258            Some([
259                0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00,
260                0x42, 0x83, 0x29,
261            ])
262        );
263    }
264
265    #[test]
266    fn parses_compressed_ipv6_literal() {
267        assert_eq!(
268            parse_ipv6_literal("fe80::5054:ff:fe12:3456"),
269            Some([
270                0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x54, 0x00, 0xff, 0xfe,
271                0x12, 0x34, 0x56,
272            ])
273        );
274    }
275
276    #[test]
277    fn rejects_multiple_double_colons() {
278        assert_eq!(parse_ipv6_literal("fe80::5054::ff::fe12::3456"), None);
279    }
280}