Skip to main content

stackforge_core/layer/dot11/
management.rs

1//! IEEE 802.11 management frame subtypes.
2//!
3//! Management frames are used for establishing and maintaining communications.
4//! This module provides parsers for all major management frame subtypes
5//! including Beacon, Probe Request/Response, Authentication, Association, etc.
6
7use crate::layer::field::{FieldError, MacAddress};
8
9// ============================================================================
10// Dot11Beacon
11// ============================================================================
12
13/// 802.11 Beacon frame body.
14///
15/// Fixed fields: timestamp(8B) + `beacon_interval(2B)` + capability(2B) = 12 bytes.
16/// Followed by an IE chain (parsed separately by the ie module).
17#[derive(Debug, Clone)]
18pub struct Dot11Beacon {
19    pub offset: usize,
20}
21
22/// Beacon fixed fields length.
23pub const BEACON_FIXED_LEN: usize = 12;
24
25impl Dot11Beacon {
26    #[must_use]
27    pub fn new(offset: usize) -> Self {
28        Self { offset }
29    }
30
31    /// Validate the buffer contains enough data.
32    pub fn validate(buf: &[u8], offset: usize) -> Result<(), FieldError> {
33        if buf.len() < offset + BEACON_FIXED_LEN {
34            return Err(FieldError::BufferTooShort {
35                offset,
36                need: BEACON_FIXED_LEN,
37                have: buf.len().saturating_sub(offset),
38            });
39        }
40        Ok(())
41    }
42
43    /// Timestamp (8 bytes, little-endian).
44    pub fn timestamp(&self, buf: &[u8]) -> Result<u64, FieldError> {
45        let off = self.offset;
46        if buf.len() < off + 8 {
47            return Err(FieldError::BufferTooShort {
48                offset: off,
49                need: 8,
50                have: buf.len(),
51            });
52        }
53        Ok(u64::from_le_bytes([
54            buf[off],
55            buf[off + 1],
56            buf[off + 2],
57            buf[off + 3],
58            buf[off + 4],
59            buf[off + 5],
60            buf[off + 6],
61            buf[off + 7],
62        ]))
63    }
64
65    /// Beacon interval in TUs (1 TU = 1024 microseconds), little-endian u16.
66    pub fn beacon_interval(&self, buf: &[u8]) -> Result<u16, FieldError> {
67        let off = self.offset + 8;
68        if buf.len() < off + 2 {
69            return Err(FieldError::BufferTooShort {
70                offset: off,
71                need: 2,
72                have: buf.len(),
73            });
74        }
75        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
76    }
77
78    /// Capability information (2 bytes, little-endian).
79    pub fn capability(&self, buf: &[u8]) -> Result<u16, FieldError> {
80        let off = self.offset + 10;
81        if buf.len() < off + 2 {
82            return Err(FieldError::BufferTooShort {
83                offset: off,
84                need: 2,
85                have: buf.len(),
86            });
87        }
88        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
89    }
90
91    /// Offset where the IE chain starts.
92    #[must_use]
93    pub fn ie_offset(&self) -> usize {
94        self.offset + BEACON_FIXED_LEN
95    }
96
97    /// Header length (fixed fields only).
98    #[must_use]
99    pub fn header_len(&self) -> usize {
100        BEACON_FIXED_LEN
101    }
102
103    /// Build beacon fixed fields.
104    #[must_use]
105    pub fn build(timestamp: u64, beacon_interval: u16, capability: u16) -> Vec<u8> {
106        let mut out = Vec::with_capacity(BEACON_FIXED_LEN);
107        out.extend_from_slice(&timestamp.to_le_bytes());
108        out.extend_from_slice(&beacon_interval.to_le_bytes());
109        out.extend_from_slice(&capability.to_le_bytes());
110        out
111    }
112}
113
114// ============================================================================
115// Dot11ProbeReq
116// ============================================================================
117
118/// 802.11 Probe Request frame body.
119///
120/// Has no fixed fields; just an IE chain.
121#[derive(Debug, Clone)]
122pub struct Dot11ProbeReq {
123    pub offset: usize,
124}
125
126impl Dot11ProbeReq {
127    #[must_use]
128    pub fn new(offset: usize) -> Self {
129        Self { offset }
130    }
131
132    /// Offset where the IE chain starts (immediately).
133    #[must_use]
134    pub fn ie_offset(&self) -> usize {
135        self.offset
136    }
137
138    /// Header length (no fixed fields).
139    #[must_use]
140    pub fn header_len(&self) -> usize {
141        0
142    }
143}
144
145// ============================================================================
146// Dot11ProbeResp
147// ============================================================================
148
149/// 802.11 Probe Response frame body.
150///
151/// Same fixed fields as Beacon: timestamp(8B) + `beacon_interval(2B)` + capability(2B).
152#[derive(Debug, Clone)]
153pub struct Dot11ProbeResp {
154    pub offset: usize,
155}
156
157impl Dot11ProbeResp {
158    #[must_use]
159    pub fn new(offset: usize) -> Self {
160        Self { offset }
161    }
162
163    /// Timestamp (8 bytes, little-endian).
164    pub fn timestamp(&self, buf: &[u8]) -> Result<u64, FieldError> {
165        let off = self.offset;
166        if buf.len() < off + 8 {
167            return Err(FieldError::BufferTooShort {
168                offset: off,
169                need: 8,
170                have: buf.len(),
171            });
172        }
173        Ok(u64::from_le_bytes([
174            buf[off],
175            buf[off + 1],
176            buf[off + 2],
177            buf[off + 3],
178            buf[off + 4],
179            buf[off + 5],
180            buf[off + 6],
181            buf[off + 7],
182        ]))
183    }
184
185    /// Beacon interval in TUs, little-endian.
186    pub fn beacon_interval(&self, buf: &[u8]) -> Result<u16, FieldError> {
187        let off = self.offset + 8;
188        if buf.len() < off + 2 {
189            return Err(FieldError::BufferTooShort {
190                offset: off,
191                need: 2,
192                have: buf.len(),
193            });
194        }
195        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
196    }
197
198    /// Capability information (2 bytes, little-endian).
199    pub fn capability(&self, buf: &[u8]) -> Result<u16, FieldError> {
200        let off = self.offset + 10;
201        if buf.len() < off + 2 {
202            return Err(FieldError::BufferTooShort {
203                offset: off,
204                need: 2,
205                have: buf.len(),
206            });
207        }
208        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
209    }
210
211    /// Offset where the IE chain starts.
212    #[must_use]
213    pub fn ie_offset(&self) -> usize {
214        self.offset + BEACON_FIXED_LEN
215    }
216
217    /// Header length (fixed fields only).
218    #[must_use]
219    pub fn header_len(&self) -> usize {
220        BEACON_FIXED_LEN
221    }
222
223    /// Build probe response fixed fields.
224    #[must_use]
225    pub fn build(timestamp: u64, beacon_interval: u16, capability: u16) -> Vec<u8> {
226        Dot11Beacon::build(timestamp, beacon_interval, capability)
227    }
228}
229
230// ============================================================================
231// Dot11Auth
232// ============================================================================
233
234/// 802.11 Authentication frame body.
235///
236/// Fixed fields: `auth_algo(2B)` + `auth_seq(2B)` + `status_code(2B)` = 6 bytes.
237/// May be followed by IEs (e.g., challenge text for shared key auth).
238#[derive(Debug, Clone)]
239pub struct Dot11Auth {
240    pub offset: usize,
241}
242
243pub const AUTH_FIXED_LEN: usize = 6;
244
245impl Dot11Auth {
246    #[must_use]
247    pub fn new(offset: usize) -> Self {
248        Self { offset }
249    }
250
251    /// Validate buffer.
252    pub fn validate(buf: &[u8], offset: usize) -> Result<(), FieldError> {
253        if buf.len() < offset + AUTH_FIXED_LEN {
254            return Err(FieldError::BufferTooShort {
255                offset,
256                need: AUTH_FIXED_LEN,
257                have: buf.len().saturating_sub(offset),
258            });
259        }
260        Ok(())
261    }
262
263    /// Authentication algorithm number (little-endian u16).
264    pub fn algo(&self, buf: &[u8]) -> Result<u16, FieldError> {
265        let off = self.offset;
266        if buf.len() < off + 2 {
267            return Err(FieldError::BufferTooShort {
268                offset: off,
269                need: 2,
270                have: buf.len(),
271            });
272        }
273        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
274    }
275
276    /// Authentication sequence number (little-endian u16).
277    pub fn seqnum(&self, buf: &[u8]) -> Result<u16, FieldError> {
278        let off = self.offset + 2;
279        if buf.len() < off + 2 {
280            return Err(FieldError::BufferTooShort {
281                offset: off,
282                need: 2,
283                have: buf.len(),
284            });
285        }
286        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
287    }
288
289    /// Status code (little-endian u16).
290    pub fn status_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
291        let off = self.offset + 4;
292        if buf.len() < off + 2 {
293            return Err(FieldError::BufferTooShort {
294                offset: off,
295                need: 2,
296                have: buf.len(),
297            });
298        }
299        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
300    }
301
302    /// Offset where the IE chain starts.
303    #[must_use]
304    pub fn ie_offset(&self) -> usize {
305        self.offset + AUTH_FIXED_LEN
306    }
307
308    /// Header length.
309    #[must_use]
310    pub fn header_len(&self) -> usize {
311        AUTH_FIXED_LEN
312    }
313
314    /// Check if this auth frame answers another.
315    #[must_use]
316    pub fn answers(&self, buf: &[u8], other: &Dot11Auth, other_buf: &[u8]) -> bool {
317        let self_algo = self.algo(buf).unwrap_or(0);
318        let other_algo = other.algo(other_buf).unwrap_or(0);
319        if self_algo != other_algo {
320            return false;
321        }
322        let self_seq = self.seqnum(buf).unwrap_or(0);
323        let other_seq = other.seqnum(other_buf).unwrap_or(0);
324        self_seq == other_seq + 1 || (self_algo == 3 && self_seq == other_seq)
325    }
326
327    /// Build authentication fixed fields.
328    #[must_use]
329    pub fn build(algo: u16, seqnum: u16, status_code: u16) -> Vec<u8> {
330        let mut out = Vec::with_capacity(AUTH_FIXED_LEN);
331        out.extend_from_slice(&algo.to_le_bytes());
332        out.extend_from_slice(&seqnum.to_le_bytes());
333        out.extend_from_slice(&status_code.to_le_bytes());
334        out
335    }
336}
337
338// ============================================================================
339// Dot11Deauth
340// ============================================================================
341
342/// 802.11 Deauthentication frame body.
343///
344/// Fixed fields: `reason_code(2B)`.
345#[derive(Debug, Clone)]
346pub struct Dot11Deauth {
347    pub offset: usize,
348}
349
350pub const DEAUTH_FIXED_LEN: usize = 2;
351
352impl Dot11Deauth {
353    #[must_use]
354    pub fn new(offset: usize) -> Self {
355        Self { offset }
356    }
357
358    /// Reason code (little-endian u16).
359    pub fn reason_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
360        let off = self.offset;
361        if buf.len() < off + 2 {
362            return Err(FieldError::BufferTooShort {
363                offset: off,
364                need: 2,
365                have: buf.len(),
366            });
367        }
368        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
369    }
370
371    /// Header length.
372    #[must_use]
373    pub fn header_len(&self) -> usize {
374        DEAUTH_FIXED_LEN
375    }
376
377    /// Build deauthentication body.
378    #[must_use]
379    pub fn build(reason_code: u16) -> Vec<u8> {
380        reason_code.to_le_bytes().to_vec()
381    }
382}
383
384// ============================================================================
385// Dot11Disas (Disassociation)
386// ============================================================================
387
388/// 802.11 Disassociation frame body.
389///
390/// Fixed fields: `reason_code(2B)`.
391#[derive(Debug, Clone)]
392pub struct Dot11Disas {
393    pub offset: usize,
394}
395
396pub const DISAS_FIXED_LEN: usize = 2;
397
398impl Dot11Disas {
399    #[must_use]
400    pub fn new(offset: usize) -> Self {
401        Self { offset }
402    }
403
404    /// Reason code (little-endian u16).
405    pub fn reason_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
406        let off = self.offset;
407        if buf.len() < off + 2 {
408            return Err(FieldError::BufferTooShort {
409                offset: off,
410                need: 2,
411                have: buf.len(),
412            });
413        }
414        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
415    }
416
417    /// Header length.
418    #[must_use]
419    pub fn header_len(&self) -> usize {
420        DISAS_FIXED_LEN
421    }
422
423    /// Build disassociation body.
424    #[must_use]
425    pub fn build(reason_code: u16) -> Vec<u8> {
426        reason_code.to_le_bytes().to_vec()
427    }
428}
429
430// ============================================================================
431// Dot11AssocReq (Association Request)
432// ============================================================================
433
434/// 802.11 Association Request frame body.
435///
436/// Fixed fields: capability(2B) + `listen_interval(2B)` = 4 bytes.
437/// Followed by an IE chain.
438#[derive(Debug, Clone)]
439pub struct Dot11AssocReq {
440    pub offset: usize,
441}
442
443pub const ASSOC_REQ_FIXED_LEN: usize = 4;
444
445impl Dot11AssocReq {
446    #[must_use]
447    pub fn new(offset: usize) -> Self {
448        Self { offset }
449    }
450
451    /// Capability information (little-endian u16).
452    pub fn capability(&self, buf: &[u8]) -> Result<u16, FieldError> {
453        let off = self.offset;
454        if buf.len() < off + 2 {
455            return Err(FieldError::BufferTooShort {
456                offset: off,
457                need: 2,
458                have: buf.len(),
459            });
460        }
461        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
462    }
463
464    /// Listen interval (little-endian u16).
465    pub fn listen_interval(&self, buf: &[u8]) -> Result<u16, FieldError> {
466        let off = self.offset + 2;
467        if buf.len() < off + 2 {
468            return Err(FieldError::BufferTooShort {
469                offset: off,
470                need: 2,
471                have: buf.len(),
472            });
473        }
474        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
475    }
476
477    /// Offset where the IE chain starts.
478    #[must_use]
479    pub fn ie_offset(&self) -> usize {
480        self.offset + ASSOC_REQ_FIXED_LEN
481    }
482
483    /// Header length.
484    #[must_use]
485    pub fn header_len(&self) -> usize {
486        ASSOC_REQ_FIXED_LEN
487    }
488
489    /// Build association request fixed fields.
490    #[must_use]
491    pub fn build(capability: u16, listen_interval: u16) -> Vec<u8> {
492        let mut out = Vec::with_capacity(ASSOC_REQ_FIXED_LEN);
493        out.extend_from_slice(&capability.to_le_bytes());
494        out.extend_from_slice(&listen_interval.to_le_bytes());
495        out
496    }
497}
498
499// ============================================================================
500// Dot11AssocResp (Association Response)
501// ============================================================================
502
503/// 802.11 Association Response frame body.
504///
505/// Fixed fields: capability(2B) + `status_code(2B)` + AID(2B) = 6 bytes.
506/// Followed by an IE chain.
507#[derive(Debug, Clone)]
508pub struct Dot11AssocResp {
509    pub offset: usize,
510}
511
512pub const ASSOC_RESP_FIXED_LEN: usize = 6;
513
514impl Dot11AssocResp {
515    #[must_use]
516    pub fn new(offset: usize) -> Self {
517        Self { offset }
518    }
519
520    /// Capability information (little-endian u16).
521    pub fn capability(&self, buf: &[u8]) -> Result<u16, FieldError> {
522        let off = self.offset;
523        if buf.len() < off + 2 {
524            return Err(FieldError::BufferTooShort {
525                offset: off,
526                need: 2,
527                have: buf.len(),
528            });
529        }
530        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
531    }
532
533    /// Status code (little-endian u16).
534    pub fn status_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
535        let off = self.offset + 2;
536        if buf.len() < off + 2 {
537            return Err(FieldError::BufferTooShort {
538                offset: off,
539                need: 2,
540                have: buf.len(),
541            });
542        }
543        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
544    }
545
546    /// Association ID (little-endian u16).
547    pub fn aid(&self, buf: &[u8]) -> Result<u16, FieldError> {
548        let off = self.offset + 4;
549        if buf.len() < off + 2 {
550            return Err(FieldError::BufferTooShort {
551                offset: off,
552                need: 2,
553                have: buf.len(),
554            });
555        }
556        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
557    }
558
559    /// Offset where the IE chain starts.
560    #[must_use]
561    pub fn ie_offset(&self) -> usize {
562        self.offset + ASSOC_RESP_FIXED_LEN
563    }
564
565    /// Header length.
566    #[must_use]
567    pub fn header_len(&self) -> usize {
568        ASSOC_RESP_FIXED_LEN
569    }
570
571    /// Build association response fixed fields.
572    #[must_use]
573    pub fn build(capability: u16, status_code: u16, aid: u16) -> Vec<u8> {
574        let mut out = Vec::with_capacity(ASSOC_RESP_FIXED_LEN);
575        out.extend_from_slice(&capability.to_le_bytes());
576        out.extend_from_slice(&status_code.to_le_bytes());
577        out.extend_from_slice(&aid.to_le_bytes());
578        out
579    }
580}
581
582// ============================================================================
583// Dot11ReassocReq (Reassociation Request)
584// ============================================================================
585
586/// 802.11 Reassociation Request frame body.
587///
588/// Fixed fields: capability(2B) + `listen_interval(2B)` + `current_ap(6B)` = 10 bytes.
589/// Followed by an IE chain.
590#[derive(Debug, Clone)]
591pub struct Dot11ReassocReq {
592    pub offset: usize,
593}
594
595pub const REASSOC_REQ_FIXED_LEN: usize = 10;
596
597impl Dot11ReassocReq {
598    #[must_use]
599    pub fn new(offset: usize) -> Self {
600        Self { offset }
601    }
602
603    /// Capability information (little-endian u16).
604    pub fn capability(&self, buf: &[u8]) -> Result<u16, FieldError> {
605        let off = self.offset;
606        if buf.len() < off + 2 {
607            return Err(FieldError::BufferTooShort {
608                offset: off,
609                need: 2,
610                have: buf.len(),
611            });
612        }
613        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
614    }
615
616    /// Listen interval (little-endian u16).
617    pub fn listen_interval(&self, buf: &[u8]) -> Result<u16, FieldError> {
618        let off = self.offset + 2;
619        if buf.len() < off + 2 {
620            return Err(FieldError::BufferTooShort {
621                offset: off,
622                need: 2,
623                have: buf.len(),
624            });
625        }
626        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
627    }
628
629    /// Current AP MAC address (6 bytes).
630    pub fn current_ap(&self, buf: &[u8]) -> Result<MacAddress, FieldError> {
631        let off = self.offset + 4;
632        if buf.len() < off + 6 {
633            return Err(FieldError::BufferTooShort {
634                offset: off,
635                need: 6,
636                have: buf.len(),
637            });
638        }
639        Ok(MacAddress::new([
640            buf[off],
641            buf[off + 1],
642            buf[off + 2],
643            buf[off + 3],
644            buf[off + 4],
645            buf[off + 5],
646        ]))
647    }
648
649    /// Offset where the IE chain starts.
650    #[must_use]
651    pub fn ie_offset(&self) -> usize {
652        self.offset + REASSOC_REQ_FIXED_LEN
653    }
654
655    /// Header length.
656    #[must_use]
657    pub fn header_len(&self) -> usize {
658        REASSOC_REQ_FIXED_LEN
659    }
660
661    /// Build reassociation request fixed fields.
662    #[must_use]
663    pub fn build(capability: u16, listen_interval: u16, current_ap: MacAddress) -> Vec<u8> {
664        let mut out = Vec::with_capacity(REASSOC_REQ_FIXED_LEN);
665        out.extend_from_slice(&capability.to_le_bytes());
666        out.extend_from_slice(&listen_interval.to_le_bytes());
667        out.extend_from_slice(current_ap.as_bytes());
668        out
669    }
670}
671
672// ============================================================================
673// Dot11ReassocResp (Reassociation Response)
674// ============================================================================
675
676/// 802.11 Reassociation Response frame body (same as Association Response).
677#[derive(Debug, Clone)]
678pub struct Dot11ReassocResp {
679    pub offset: usize,
680}
681
682impl Dot11ReassocResp {
683    #[must_use]
684    pub fn new(offset: usize) -> Self {
685        Self { offset }
686    }
687
688    /// Capability information (little-endian u16).
689    pub fn capability(&self, buf: &[u8]) -> Result<u16, FieldError> {
690        let off = self.offset;
691        if buf.len() < off + 2 {
692            return Err(FieldError::BufferTooShort {
693                offset: off,
694                need: 2,
695                have: buf.len(),
696            });
697        }
698        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
699    }
700
701    /// Status code (little-endian u16).
702    pub fn status_code(&self, buf: &[u8]) -> Result<u16, FieldError> {
703        let off = self.offset + 2;
704        if buf.len() < off + 2 {
705            return Err(FieldError::BufferTooShort {
706                offset: off,
707                need: 2,
708                have: buf.len(),
709            });
710        }
711        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
712    }
713
714    /// Association ID (little-endian u16).
715    pub fn aid(&self, buf: &[u8]) -> Result<u16, FieldError> {
716        let off = self.offset + 4;
717        if buf.len() < off + 2 {
718            return Err(FieldError::BufferTooShort {
719                offset: off,
720                need: 2,
721                have: buf.len(),
722            });
723        }
724        Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
725    }
726
727    /// Offset where the IE chain starts.
728    #[must_use]
729    pub fn ie_offset(&self) -> usize {
730        self.offset + ASSOC_RESP_FIXED_LEN
731    }
732
733    /// Header length.
734    #[must_use]
735    pub fn header_len(&self) -> usize {
736        ASSOC_RESP_FIXED_LEN
737    }
738
739    /// Build reassociation response fixed fields (same as assoc resp).
740    #[must_use]
741    pub fn build(capability: u16, status_code: u16, aid: u16) -> Vec<u8> {
742        Dot11AssocResp::build(capability, status_code, aid)
743    }
744}
745
746// ============================================================================
747// Dot11Action
748// ============================================================================
749
750/// 802.11 Action frame body.
751///
752/// Fixed fields: category(1B) + variable action data.
753#[derive(Debug, Clone)]
754pub struct Dot11Action {
755    pub offset: usize,
756}
757
758pub const ACTION_MIN_LEN: usize = 1;
759
760impl Dot11Action {
761    #[must_use]
762    pub fn new(offset: usize) -> Self {
763        Self { offset }
764    }
765
766    /// Action category (1 byte).
767    pub fn category(&self, buf: &[u8]) -> Result<u8, FieldError> {
768        let off = self.offset;
769        if buf.len() <= off {
770            return Err(FieldError::BufferTooShort {
771                offset: off,
772                need: 1,
773                have: buf.len(),
774            });
775        }
776        Ok(buf[off])
777    }
778
779    /// Action code (1 byte, second byte of action frame body).
780    pub fn action_code(&self, buf: &[u8]) -> Result<u8, FieldError> {
781        let off = self.offset + 1;
782        if buf.len() <= off {
783            return Err(FieldError::BufferTooShort {
784                offset: off,
785                need: 1,
786                have: buf.len(),
787            });
788        }
789        Ok(buf[off])
790    }
791
792    /// Payload offset (after category byte).
793    #[must_use]
794    pub fn payload_offset(&self) -> usize {
795        self.offset + ACTION_MIN_LEN
796    }
797
798    /// Header length (just the category byte).
799    #[must_use]
800    pub fn header_len(&self) -> usize {
801        ACTION_MIN_LEN
802    }
803
804    /// Build action frame body.
805    #[must_use]
806    pub fn build(category: u8) -> Vec<u8> {
807        vec![category]
808    }
809}
810
811// ============================================================================
812// Dot11ATIM
813// ============================================================================
814
815/// 802.11 ATIM (Announcement Traffic Indication Message) frame body.
816///
817/// Has no body fields.
818#[derive(Debug, Clone)]
819pub struct Dot11ATIM {
820    pub offset: usize,
821}
822
823impl Dot11ATIM {
824    #[must_use]
825    pub fn new(offset: usize) -> Self {
826        Self { offset }
827    }
828
829    #[must_use]
830    pub fn header_len(&self) -> usize {
831        0
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    #[test]
840    fn test_beacon_parse() {
841        let mut buf = vec![0u8; 12];
842        // timestamp = 0x0102030405060708 (LE)
843        buf[0..8].copy_from_slice(&0x0102030405060708u64.to_le_bytes());
844        // beacon_interval = 100 (0x0064)
845        buf[8..10].copy_from_slice(&100u16.to_le_bytes());
846        // capability = 0x0411 (ESS + privacy + short-slot)
847        buf[10..12].copy_from_slice(&0x0411u16.to_le_bytes());
848
849        let beacon = Dot11Beacon::new(0);
850        assert_eq!(beacon.timestamp(&buf).unwrap(), 0x0102030405060708);
851        assert_eq!(beacon.beacon_interval(&buf).unwrap(), 100);
852        assert_eq!(beacon.capability(&buf).unwrap(), 0x0411);
853        assert_eq!(beacon.ie_offset(), 12);
854    }
855
856    #[test]
857    fn test_beacon_build_roundtrip() {
858        let data = Dot11Beacon::build(0x1234567890ABCDEF, 100, 0x0411);
859        assert_eq!(data.len(), BEACON_FIXED_LEN);
860
861        let beacon = Dot11Beacon::new(0);
862        assert_eq!(beacon.timestamp(&data).unwrap(), 0x1234567890ABCDEF);
863        assert_eq!(beacon.beacon_interval(&data).unwrap(), 100);
864        assert_eq!(beacon.capability(&data).unwrap(), 0x0411);
865    }
866
867    #[test]
868    fn test_auth_parse() {
869        let mut buf = vec![0u8; 6];
870        // algo = 0 (open)
871        buf[0..2].copy_from_slice(&0u16.to_le_bytes());
872        // seqnum = 1
873        buf[2..4].copy_from_slice(&1u16.to_le_bytes());
874        // status = 0 (success)
875        buf[4..6].copy_from_slice(&0u16.to_le_bytes());
876
877        let auth = Dot11Auth::new(0);
878        assert_eq!(auth.algo(&buf).unwrap(), 0);
879        assert_eq!(auth.seqnum(&buf).unwrap(), 1);
880        assert_eq!(auth.status_code(&buf).unwrap(), 0);
881    }
882
883    #[test]
884    fn test_auth_build_roundtrip() {
885        let data = Dot11Auth::build(0, 1, 0);
886        assert_eq!(data.len(), AUTH_FIXED_LEN);
887
888        let auth = Dot11Auth::new(0);
889        assert_eq!(auth.algo(&data).unwrap(), 0);
890        assert_eq!(auth.seqnum(&data).unwrap(), 1);
891        assert_eq!(auth.status_code(&data).unwrap(), 0);
892    }
893
894    #[test]
895    fn test_auth_answers() {
896        let req = Dot11Auth::build(0, 1, 0);
897        let resp = Dot11Auth::build(0, 2, 0);
898
899        let req_auth = Dot11Auth::new(0);
900        let resp_auth = Dot11Auth::new(0);
901
902        assert!(resp_auth.answers(&resp, &req_auth, &req));
903        assert!(!req_auth.answers(&req, &resp_auth, &resp));
904    }
905
906    #[test]
907    fn test_deauth_parse() {
908        let buf = 1u16.to_le_bytes().to_vec();
909        let deauth = Dot11Deauth::new(0);
910        assert_eq!(deauth.reason_code(&buf).unwrap(), 1);
911    }
912
913    #[test]
914    fn test_deauth_build_roundtrip() {
915        let data = Dot11Deauth::build(4);
916        let deauth = Dot11Deauth::new(0);
917        assert_eq!(deauth.reason_code(&data).unwrap(), 4);
918    }
919
920    #[test]
921    fn test_disas_parse() {
922        let buf = 5u16.to_le_bytes().to_vec();
923        let disas = Dot11Disas::new(0);
924        assert_eq!(disas.reason_code(&buf).unwrap(), 5);
925    }
926
927    #[test]
928    fn test_assoc_req_parse() {
929        let mut buf = vec![0u8; 4];
930        buf[0..2].copy_from_slice(&0x0411u16.to_le_bytes()); // cap
931        buf[2..4].copy_from_slice(&200u16.to_le_bytes()); // listen interval
932
933        let assoc = Dot11AssocReq::new(0);
934        assert_eq!(assoc.capability(&buf).unwrap(), 0x0411);
935        assert_eq!(assoc.listen_interval(&buf).unwrap(), 200);
936    }
937
938    #[test]
939    fn test_assoc_req_build_roundtrip() {
940        let data = Dot11AssocReq::build(0x0411, 200);
941        let assoc = Dot11AssocReq::new(0);
942        assert_eq!(assoc.capability(&data).unwrap(), 0x0411);
943        assert_eq!(assoc.listen_interval(&data).unwrap(), 200);
944    }
945
946    #[test]
947    fn test_assoc_resp_parse() {
948        let mut buf = vec![0u8; 6];
949        buf[0..2].copy_from_slice(&0x0411u16.to_le_bytes());
950        buf[2..4].copy_from_slice(&0u16.to_le_bytes());
951        buf[4..6].copy_from_slice(&1u16.to_le_bytes());
952
953        let assoc = Dot11AssocResp::new(0);
954        assert_eq!(assoc.capability(&buf).unwrap(), 0x0411);
955        assert_eq!(assoc.status_code(&buf).unwrap(), 0);
956        assert_eq!(assoc.aid(&buf).unwrap(), 1);
957    }
958
959    #[test]
960    fn test_reassoc_req_parse() {
961        let mut buf = vec![0u8; 10];
962        buf[0..2].copy_from_slice(&0x0411u16.to_le_bytes());
963        buf[2..4].copy_from_slice(&200u16.to_le_bytes());
964        buf[4..10].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
965
966        let reassoc = Dot11ReassocReq::new(0);
967        assert_eq!(reassoc.capability(&buf).unwrap(), 0x0411);
968        assert_eq!(reassoc.listen_interval(&buf).unwrap(), 200);
969        assert_eq!(
970            reassoc.current_ap(&buf).unwrap(),
971            MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF])
972        );
973    }
974
975    #[test]
976    fn test_reassoc_req_build_roundtrip() {
977        let ap = MacAddress::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
978        let data = Dot11ReassocReq::build(0x0411, 200, ap);
979        assert_eq!(data.len(), REASSOC_REQ_FIXED_LEN);
980
981        let reassoc = Dot11ReassocReq::new(0);
982        assert_eq!(reassoc.current_ap(&data).unwrap(), ap);
983    }
984
985    #[test]
986    fn test_action_parse() {
987        let buf = vec![0x00u8, 0x04]; // category=spectrum_mgmt, action=CSA
988        let action = Dot11Action::new(0);
989        assert_eq!(action.category(&buf).unwrap(), 0x00);
990        assert_eq!(action.action_code(&buf).unwrap(), 0x04);
991    }
992
993    #[test]
994    fn test_action_build() {
995        let data = Dot11Action::build(0x0A); // WNM category
996        assert_eq!(data.len(), 1);
997        assert_eq!(data[0], 0x0A);
998    }
999
1000    #[test]
1001    fn test_probe_req() {
1002        let req = Dot11ProbeReq::new(0);
1003        assert_eq!(req.header_len(), 0);
1004        assert_eq!(req.ie_offset(), 0);
1005    }
1006
1007    #[test]
1008    fn test_probe_resp_parse() {
1009        let mut buf = vec![0u8; 12];
1010        buf[0..8].copy_from_slice(&1000u64.to_le_bytes());
1011        buf[8..10].copy_from_slice(&100u16.to_le_bytes());
1012        buf[10..12].copy_from_slice(&0x0001u16.to_le_bytes());
1013
1014        let resp = Dot11ProbeResp::new(0);
1015        assert_eq!(resp.timestamp(&buf).unwrap(), 1000);
1016        assert_eq!(resp.beacon_interval(&buf).unwrap(), 100);
1017        assert_eq!(resp.capability(&buf).unwrap(), 0x0001);
1018    }
1019
1020    #[test]
1021    fn test_reassoc_resp() {
1022        let data = Dot11ReassocResp::build(0x0411, 0, 1);
1023        let resp = Dot11ReassocResp::new(0);
1024        assert_eq!(resp.capability(&data).unwrap(), 0x0411);
1025        assert_eq!(resp.status_code(&data).unwrap(), 0);
1026        assert_eq!(resp.aid(&data).unwrap(), 1);
1027    }
1028}