Nothing to see here, move along meow
1use lancer_core::dns::{
2 DNS_TYPE_A, DNS_TYPE_PTR, MDNS_TTL, build_mdns_a_response, build_mdns_ptr_response,
3 dns_encode_name, dns_name_eq, dns_skip_name, put_u16_be, put_u32_be,
4};
5use proptest::prelude::*;
6
7fn encode_labels(labels: &[&[u8]]) -> ([u8; 256], usize) {
8 let mut buf = [0u8; 256];
9 let len = dns_encode_name(&mut buf, labels).unwrap();
10 (buf, len)
11}
12
13#[test]
14fn encode_single_label() {
15 let (buf, len) = encode_labels(&[b"lancer"]);
16 assert_eq!(len, 8);
17 assert_eq!(buf[0], 6);
18 assert_eq!(&buf[1..7], b"lancer");
19 assert_eq!(buf[7], 0);
20}
21
22#[test]
23fn encode_multiple_labels() {
24 let (buf, len) = encode_labels(&[b"lancer", b"local"]);
25 assert_eq!(len, 14);
26 assert_eq!(buf[0], 6);
27 assert_eq!(&buf[1..7], b"lancer");
28 assert_eq!(buf[7], 5);
29 assert_eq!(&buf[8..13], b"local");
30 assert_eq!(buf[13], 0);
31}
32
33#[test]
34fn encode_empty_labels() {
35 let (buf, len) = encode_labels(&[]);
36 assert_eq!(len, 1);
37 assert_eq!(buf[0], 0);
38}
39
40#[test]
41fn encode_rejects_buffer_too_small() {
42 let mut buf = [0u8; 3];
43 assert!(dns_encode_name(&mut buf, &[b"lancer"]).is_none());
44}
45
46#[test]
47fn encode_rejects_label_over_63_bytes() {
48 let long_label = [b'a'; 64];
49 let mut buf = [0u8; 256];
50 assert!(dns_encode_name(&mut buf, &[&long_label]).is_none());
51}
52
53#[test]
54fn encode_accepts_label_exactly_63_bytes() {
55 let label = [b'z'; 63];
56 let mut buf = [0u8; 256];
57 assert!(dns_encode_name(&mut buf, &[&label]).is_some());
58}
59
60#[test]
61fn name_eq_matches_exact() {
62 let (buf, len) = encode_labels(&[b"lancer", b"local"]);
63 let result = dns_name_eq(&buf[..len], 0, &[b"lancer", b"local"]);
64 assert_eq!(result, Some(len));
65}
66
67#[test]
68fn name_eq_case_insensitive() {
69 let (buf, len) = encode_labels(&[b"LANCER", b"LOCAL"]);
70 let result = dns_name_eq(&buf[..len], 0, &[b"lancer", b"local"]);
71 assert_eq!(result, Some(len));
72}
73
74#[test]
75fn name_eq_rejects_wrong_labels() {
76 let (buf, len) = encode_labels(&[b"lancer", b"local"]);
77 assert_eq!(dns_name_eq(&buf[..len], 0, &[b"other", b"local"]), None);
78}
79
80#[test]
81fn name_eq_rejects_prefix_match() {
82 let (buf, len) = encode_labels(&[b"lancer", b"local"]);
83 assert_eq!(dns_name_eq(&buf[..len], 0, &[b"lancer"]), None);
84}
85
86#[test]
87fn name_eq_rejects_too_few_wire_labels() {
88 let (buf, len) = encode_labels(&[b"lancer"]);
89 assert_eq!(dns_name_eq(&buf[..len], 0, &[b"lancer", b"local"]), None);
90}
91
92#[test]
93fn name_eq_rejects_truncated_wire() {
94 let (buf, _len) = encode_labels(&[b"lancer", b"local"]);
95 assert_eq!(dns_name_eq(&buf[..5], 0, &[b"lancer", b"local"]), None);
96}
97
98#[test]
99fn name_eq_empty_target() {
100 let (buf, len) = encode_labels(&[]);
101 let result = dns_name_eq(&buf[..len], 0, &[]);
102 assert!(result.is_some());
103}
104
105#[test]
106fn name_eq_with_offset() {
107 let mut buf = [0u8; 256];
108 buf[0..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
109 let written = dns_encode_name(&mut buf[4..], &[b"lancer", b"local"]).unwrap();
110 let result = dns_name_eq(&buf[..4 + written], 4, &[b"lancer", b"local"]);
111 assert_eq!(result, Some(4 + written));
112}
113
114#[test]
115fn name_eq_follows_compression_pointer() {
116 let mut buf = [0u8; 64];
117 let name_len = dns_encode_name(&mut buf, &[b"lancer", b"local"]).unwrap();
118 buf[name_len] = 0xC0;
119 buf[name_len + 1] = 0x00;
120 let wire_len = name_len + 2;
121 let result = dns_name_eq(&buf[..wire_len], name_len, &[b"lancer", b"local"]);
122 assert_eq!(result, Some(wire_len));
123}
124
125#[test]
126fn name_eq_partial_compression() {
127 let mut buf = [0u8; 64];
128 let local_offset = 0usize;
129 let local_len = dns_encode_name(&mut buf, &[b"local"]).unwrap();
130 let name_start = local_len;
131 buf[name_start] = 6;
132 buf[name_start + 1..name_start + 7].copy_from_slice(b"lancer");
133 buf[name_start + 7] = 0xC0;
134 buf[name_start + 8] = local_offset as u8;
135 let wire_len = name_start + 9;
136 let result = dns_name_eq(&buf[..wire_len], name_start, &[b"lancer", b"local"]);
137 assert_eq!(result, Some(wire_len));
138}
139
140#[test]
141fn name_eq_rejects_infinite_pointer_loop() {
142 let buf = [0xC0, 0x00];
143 assert_eq!(dns_name_eq(&buf, 0, &[b"lancer"]), None);
144}
145
146#[test]
147fn skip_name_regular() {
148 let (buf, len) = encode_labels(&[b"lancer", b"local"]);
149 assert_eq!(dns_skip_name(&buf[..len], 0), Some(len));
150}
151
152#[test]
153fn skip_name_compression_pointer() {
154 let mut buf = [0u8; 32];
155 let _ = dns_encode_name(&mut buf, &[b"lancer", b"local"]).unwrap();
156 buf[20] = 0xC0;
157 buf[21] = 0x00;
158 assert_eq!(dns_skip_name(&buf[..22], 20), Some(22));
159}
160
161#[test]
162fn skip_name_empty() {
163 let buf = [0u8; 1];
164 assert_eq!(dns_skip_name(&buf, 0), Some(1));
165}
166
167#[test]
168fn skip_name_truncated() {
169 let buf = [6u8, b'a'];
170 assert_eq!(dns_skip_name(&buf, 0), None);
171}
172
173#[test]
174fn put_u16_be_correct() {
175 let mut buf = [0u8; 4];
176 assert!(put_u16_be(&mut buf, 1, 0xABCD).is_some());
177 assert_eq!(buf[1], 0xAB);
178 assert_eq!(buf[2], 0xCD);
179}
180
181#[test]
182fn put_u16_be_rejects_overflow() {
183 let mut buf = [0u8; 2];
184 assert!(put_u16_be(&mut buf, 1, 0xABCD).is_none());
185}
186
187#[test]
188fn put_u32_be_correct() {
189 let mut buf = [0u8; 6];
190 assert!(put_u32_be(&mut buf, 1, 0x12345678).is_some());
191 assert_eq!(&buf[1..5], &[0x12, 0x34, 0x56, 0x78]);
192}
193
194#[test]
195fn put_u32_be_rejects_overflow() {
196 let mut buf = [0u8; 3];
197 assert!(put_u32_be(&mut buf, 1, 0x12345678).is_none());
198}
199
200#[test]
201fn build_mdns_a_response_structure() {
202 let ip = [10, 0, 0, 42];
203 let mut buf = [0u8; 512];
204 let len = build_mdns_a_response(&mut buf, ip).unwrap();
205
206 assert!(len > 12);
207
208 let flags = ((buf[2] as u16) << 8) | buf[3] as u16;
209 assert_eq!(flags, 0x8400);
210
211 let ancount = ((buf[6] as u16) << 8) | buf[7] as u16;
212 assert_eq!(ancount, 1);
213
214 assert_eq!(&buf[len - 4..len], &ip);
215
216 let ttl_offset = len - 4 - 2 - 4;
217 let ttl = ((buf[ttl_offset] as u32) << 24)
218 | ((buf[ttl_offset + 1] as u32) << 16)
219 | ((buf[ttl_offset + 2] as u32) << 8)
220 | buf[ttl_offset + 3] as u32;
221 assert_eq!(ttl, MDNS_TTL);
222}
223
224#[test]
225fn build_mdns_a_response_type_is_a() {
226 let mut buf = [0u8; 512];
227 let len = build_mdns_a_response(&mut buf, [192, 168, 1, 1]).unwrap();
228
229 let name_end = dns_name_eq(&buf[12..len], 0, &[b"lancer", b"local"])
230 .map(|off| 12 + off)
231 .unwrap();
232 let qtype = ((buf[name_end] as u16) << 8) | buf[name_end + 1] as u16;
233 assert_eq!(qtype, DNS_TYPE_A);
234}
235
236#[test]
237fn build_mdns_a_response_rejects_tiny_buffer() {
238 let mut buf = [0u8; 10];
239 assert!(build_mdns_a_response(&mut buf, [10, 0, 0, 1]).is_none());
240}
241
242#[test]
243fn build_mdns_ptr_response_structure() {
244 let ip = [10, 0, 0, 99];
245 let mut buf = [0u8; 512];
246 let len = build_mdns_ptr_response(&mut buf, ip, 22).unwrap();
247
248 let flags = ((buf[2] as u16) << 8) | buf[3] as u16;
249 assert_eq!(flags, 0x8400);
250
251 let ancount = ((buf[6] as u16) << 8) | buf[7] as u16;
252 assert_eq!(ancount, 1);
253
254 let arcount = ((buf[10] as u16) << 8) | buf[11] as u16;
255 assert_eq!(arcount, 2);
256
257 assert_eq!(&buf[len - 4..len], &ip);
258}
259
260#[test]
261fn build_mdns_ptr_response_contains_srv_port() {
262 let mut buf = [0u8; 512];
263 let len = build_mdns_ptr_response(&mut buf, [10, 0, 0, 1], 22).unwrap();
264
265 let has_port_22 = (12..len.saturating_sub(1)).any(|i| buf[i] == 0x00 && buf[i + 1] == 22);
266 assert!(has_port_22, "SRV record should contain port 22");
267}
268
269#[test]
270fn build_mdns_ptr_response_custom_port() {
271 let mut buf = [0u8; 512];
272 let len = build_mdns_ptr_response(&mut buf, [10, 0, 0, 1], 8080).unwrap();
273
274 let has_port = (12..len.saturating_sub(1)).any(|i| buf[i] == 0x1F && buf[i + 1] == 0x90);
275 assert!(has_port, "SRV record should contain port 8080");
276}
277
278#[test]
279fn build_mdns_ptr_response_has_ptr_type() {
280 let mut buf = [0u8; 512];
281 let len = build_mdns_ptr_response(&mut buf, [10, 0, 0, 1], 22).unwrap();
282
283 let svc_end = dns_name_eq(&buf[12..len], 0, &[b"_lancer", b"_tcp", b"local"])
284 .map(|off| 12 + off)
285 .unwrap();
286 let qtype = ((buf[svc_end] as u16) << 8) | buf[svc_end + 1] as u16;
287 assert_eq!(qtype, DNS_TYPE_PTR);
288}
289
290fn label_strategy() -> impl Strategy<Value = Vec<u8>> {
291 prop::collection::vec(b'a'..=b'z', 1..=10)
292}
293
294fn labels_strategy() -> impl Strategy<Value = Vec<Vec<u8>>> {
295 prop::collection::vec(label_strategy(), 1..=4)
296}
297
298proptest! {
299 #[test]
300 fn encode_decode_roundtrip(labels in labels_strategy()) {
301 let label_slices: Vec<&[u8]> = labels.iter().map(|l| l.as_slice()).collect();
302 let mut buf = [0u8; 256];
303 let len = dns_encode_name(&mut buf, &label_slices).unwrap();
304 let result = dns_name_eq(&buf[..len], 0, &label_slices);
305 prop_assert!(result.is_some(), "round-trip failed for {:?}", labels);
306 prop_assert_eq!(result.unwrap(), len);
307 }
308
309 #[test]
310 fn encode_format_is_length_prefixed(labels in labels_strategy()) {
311 let label_slices: Vec<&[u8]> = labels.iter().map(|l| l.as_slice()).collect();
312 let mut buf = [0u8; 256];
313 let len = dns_encode_name(&mut buf, &label_slices).unwrap();
314
315 prop_assert!(len > 0);
316 prop_assert_eq!(buf[len - 1], 0, "must end with null terminator");
317
318 let mut pos = 0usize;
319 label_slices.iter().for_each(|label| {
320 assert_eq!(buf[pos] as usize, label.len());
321 assert_eq!(&buf[pos + 1..pos + 1 + label.len()], *label);
322 pos += 1 + label.len();
323 });
324 prop_assert_eq!(buf[pos], 0);
325 }
326
327 #[test]
328 fn name_eq_case_insensitive_proptest(labels in labels_strategy()) {
329 let upper: Vec<Vec<u8>> = labels.iter().map(|l| {
330 l.iter().map(|b| b.to_ascii_uppercase()).collect()
331 }).collect();
332 let upper_slices: Vec<&[u8]> = upper.iter().map(|l| l.as_slice()).collect();
333 let lower_slices: Vec<&[u8]> = labels.iter().map(|l| l.as_slice()).collect();
334
335 let mut buf = [0u8; 256];
336 let len = dns_encode_name(&mut buf, &upper_slices).unwrap();
337 let result = dns_name_eq(&buf[..len], 0, &lower_slices);
338 prop_assert!(result.is_some(), "case-insensitive match failed");
339 }
340
341 #[test]
342 fn name_eq_rejects_extra_wire_labels(labels in labels_strategy()) {
343 let label_slices: Vec<&[u8]> = labels.iter().map(|l| l.as_slice()).collect();
344 let mut extended = labels.clone();
345 extended.push(b"extra".to_vec());
346 let ext_slices: Vec<&[u8]> = extended.iter().map(|l| l.as_slice()).collect();
347 let mut buf = [0u8; 256];
348 let len = dns_encode_name(&mut buf, &ext_slices).unwrap();
349 let result = dns_name_eq(&buf[..len], 0, &label_slices);
350 prop_assert!(result.is_none(), "prefix match should fail for {:?}", labels);
351 }
352
353 #[test]
354 fn skip_name_agrees_with_encode(labels in labels_strategy()) {
355 let label_slices: Vec<&[u8]> = labels.iter().map(|l| l.as_slice()).collect();
356 let mut buf = [0u8; 256];
357 let len = dns_encode_name(&mut buf, &label_slices).unwrap();
358 let skipped = dns_skip_name(&buf[..len], 0);
359 prop_assert_eq!(skipped, Some(len));
360 }
361
362 #[test]
363 fn a_response_always_has_valid_header(
364 a in 0u8..=255,
365 b in 0u8..=255,
366 c in 0u8..=255,
367 d in 0u8..=255,
368 ) {
369 let mut buf = [0u8; 512];
370 let len = build_mdns_a_response(&mut buf, [a, b, c, d]).unwrap();
371 prop_assert!(len > 12);
372 let flags = ((buf[2] as u16) << 8) | buf[3] as u16;
373 prop_assert_eq!(flags, 0x8400);
374 let ancount = ((buf[6] as u16) << 8) | buf[7] as u16;
375 prop_assert_eq!(ancount, 1);
376 prop_assert_eq!(&buf[len - 4..len], &[a, b, c, d]);
377 }
378}