Skip to main content

wireforge_app/dns/
name.rs

1//! DNS QNAME compression parser (RFC 1035 Section 4.1.4).
2
3use wireforge_core::util::read_u16be;
4
5/// Maximum compression pointer jumps before we stop (prevents loops).
6const MAX_JUMPS: u8 = 10;
7
8/// Parse a DNS domain name at `start` in `buf`.
9///
10/// Returns the decompressed name and the byte position after the parsed
11/// name (either the zero-length terminator or the compression pointer).
12/// Labels > 63 bytes return `None`. Compression loops return `None`.
13pub fn parse_name(buf: &[u8], start: usize) -> Option<(String, usize)> {
14    let mut name = String::new();
15    let mut pos = start;
16    let mut jumped = false;
17    let mut jump_count = 0u8;
18    let mut end_pos = start;
19
20    loop {
21        if jump_count > MAX_JUMPS {
22            return None;
23        }
24        if pos >= buf.len() {
25            return None;
26        }
27        let label_len = buf[pos] as usize;
28        if label_len & 0xC0 == 0xC0 {
29            // Compression pointer: top 2 bits = 11
30            if pos + 2 > buf.len() {
31                return None;
32            }
33            let offset = read_u16be(&buf[pos..pos + 2]) as usize & 0x3FFF;
34            if !jumped {
35                end_pos = pos + 2;
36            }
37            pos = offset;
38            jumped = true;
39            jump_count += 1;
40        } else if label_len == 0 {
41            if !jumped {
42                end_pos = pos + 1;
43            }
44            break;
45        } else {
46            if label_len > 63 {
47                return None;
48            }
49            if pos + 1 + label_len > buf.len() {
50                return None;
51            }
52            if !name.is_empty() {
53                name.push('.');
54            }
55            let label_bytes = &buf[pos + 1..pos + 1 + label_len];
56            // Domain labels are ASCII case-insensitive; store lowercased
57            name.push_str(core::str::from_utf8(label_bytes).ok()?);
58            pos += 1 + label_len;
59            if !jumped {
60                end_pos = pos;
61            }
62        }
63    }
64    Some((name, end_pos))
65}
66
67/// Encode a domain name into wire format (labels prefixed by length, no compression).
68pub fn encode_name(name: &str) -> Vec<u8> {
69    let mut out = Vec::new();
70    for label in name.split('.') {
71        let label_bytes = label.as_bytes();
72        out.push(label_bytes.len() as u8);
73        out.extend_from_slice(label_bytes);
74    }
75    out.push(0); // terminator
76    out
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn parse_simple_name() {
85        // "www.example.com" encoded: 3www7example3com0
86        let data: &[u8] = &[3, b'w', b'w', b'w', 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0];
87        let (name, pos) = parse_name(data, 0).unwrap();
88        assert_eq!(name, "www.example.com");
89        assert_eq!(pos, data.len());
90    }
91
92    #[test]
93    fn parse_compressed_name() {
94        // "example.com" at offset 12, then "www.example.com" with pointer
95        let mut data = vec![0u8; 12];
96        data.extend_from_slice(&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0]);
97        // "www" + pointer to offset 12
98        data.extend_from_slice(&[3, b'w', b'w', b'w', 0xC0, 12]);
99
100        let (name, _) = parse_name(&data, 25).unwrap(); // start at "www..."
101        assert_eq!(name, "www.example.com");
102    }
103
104    #[test]
105    fn parse_multi_jump_compression() {
106        // Chain: label -> ptr1 -> ptr2 -> "example.com"
107        let name_data: &[u8] = &[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0];
108        let ptr1: &[u8] = &[3, b'w', b'w', b'w', 0xC0, 0]; // points to name_data
109        let ptr2: &[u8] = &[0xC0, 13]; // points to ptr1 (offset 13 = after name_data)
110
111        let mut data = Vec::new();
112        data.extend_from_slice(name_data); // offset 0
113        data.extend_from_slice(ptr1);      // offset 13
114        data.extend_from_slice(ptr2);      // offset 18
115
116        let (name, _) = parse_name(&data, 19).unwrap();
117        assert_eq!(name, "www.example.com");
118    }
119
120    #[test]
121    fn parse_compression_loop_returns_none() {
122        // Two pointers that point to each other
123        let data: &[u8] = &[0xC0, 2, 0xC0, 0];
124        assert!(parse_name(data, 0).is_none());
125    }
126
127    #[test]
128    fn parse_label_too_long() {
129        let mut data = vec![64u8]; // label > 63
130        data.extend(std::iter::repeat_n(b'x', 64));
131        assert!(parse_name(&data, 0).is_none());
132    }
133
134    #[test]
135    fn encode_name_roundtrip() {
136        let encoded = encode_name("www.example.com");
137        let (decoded, _) = parse_name(&encoded, 0).unwrap();
138        assert_eq!(decoded, "www.example.com");
139    }
140}