Skip to main content

stackforge_core/layer/dot15d4/
beacon.rs

1//! IEEE 802.15.4 Beacon frame parsing and building.
2//!
3//! A beacon frame payload follows the base 802.15.4 header and contains:
4//! - Superframe Specification (2 bytes, LE)
5//! - GTS Specification (1 byte)
6//! - GTS Directions (0 or 1 byte, present if GTS descriptor count > 0)
7//! - GTS Descriptor List (variable, 3 bytes per descriptor)
8//! - Pending Address Specification (1 byte)
9//! - Pending Address List (variable)
10
11use crate::layer::field::{FieldError, read_u16_le};
12
13/// GTS descriptor: short address + starting slot/length.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct GtsDescriptor {
16    /// Short address of the device (2 bytes, LE).
17    pub short_addr: u16,
18    /// GTS starting slot (4 bits).
19    pub starting_slot: u8,
20    /// GTS length (4 bits).
21    pub length: u8,
22}
23
24/// Parsed beacon frame payload.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct BeaconPayload {
27    // Superframe Specification (2 bytes, LE)
28    /// Beacon order (4 bits).
29    pub beacon_order: u8,
30    /// Superframe order (4 bits).
31    pub superframe_order: u8,
32    /// Final CAP slot (4 bits).
33    pub final_cap_slot: u8,
34    /// Battery life extension (1 bit).
35    pub battery_life_ext: bool,
36    /// PAN coordinator (1 bit).
37    pub pan_coordinator: bool,
38    /// Association permit (1 bit).
39    pub assoc_permit: bool,
40
41    // GTS Specification (1 byte)
42    /// GTS descriptor count (3 bits).
43    pub gts_desc_count: u8,
44    /// GTS permit (1 bit).
45    pub gts_permit: bool,
46
47    // GTS Directions (0 or 1 byte)
48    /// GTS direction mask (7 bits), present if gts_desc_count > 0.
49    pub gts_dir_mask: u8,
50
51    // GTS Descriptor List
52    /// GTS descriptors (3 bytes each).
53    pub gts_descriptors: Vec<GtsDescriptor>,
54
55    // Pending Address Specification (1 byte)
56    /// Number of short addresses pending (3 bits).
57    pub pa_num_short: u8,
58    /// Number of extended (long) addresses pending (3 bits).
59    pub pa_num_long: u8,
60
61    // Pending Address List
62    /// Short addresses pending (2 bytes each).
63    pub pa_short_addresses: Vec<u16>,
64    /// Long addresses pending (8 bytes each).
65    pub pa_long_addresses: Vec<u64>,
66}
67
68impl BeaconPayload {
69    /// Parse a beacon payload from the buffer at the given offset.
70    /// Returns the parsed payload and the number of bytes consumed.
71    pub fn parse(buf: &[u8], offset: usize) -> Result<(Self, usize), FieldError> {
72        let mut pos = offset;
73
74        // Superframe Specification (2 bytes, LE)
75        if buf.len() < pos + 2 {
76            return Err(FieldError::BufferTooShort {
77                offset: pos,
78                need: 2,
79                have: buf.len().saturating_sub(pos),
80            });
81        }
82        let sf_spec = read_u16_le(buf, pos)?;
83        pos += 2;
84
85        // Scapy bit ordering in bytes: sf_spec is LE u16
86        // Bits 0-3: Beacon Order
87        // Bits 4-7: Superframe Order
88        // Bits 8-11: Final CAP Slot
89        // Bit 12: Battery Life Extension
90        // Bit 13: Reserved
91        // Bit 14: PAN Coordinator
92        // Bit 15: Association Permit
93        let beacon_order = (sf_spec & 0x0F) as u8;
94        let superframe_order = ((sf_spec >> 4) & 0x0F) as u8;
95        let final_cap_slot = ((sf_spec >> 8) & 0x0F) as u8;
96        let battery_life_ext = (sf_spec & 0x1000) != 0;
97        let pan_coordinator = (sf_spec & 0x4000) != 0;
98        let assoc_permit = (sf_spec & 0x8000) != 0;
99
100        // GTS Specification (1 byte)
101        if buf.len() < pos + 1 {
102            return Err(FieldError::BufferTooShort {
103                offset: pos,
104                need: 1,
105                have: buf.len().saturating_sub(pos),
106            });
107        }
108        let gts_spec = buf[pos];
109        pos += 1;
110
111        // Bits 0-2: GTS Descriptor Count
112        // Bits 3-6: Reserved
113        // Bit 7: GTS Permit
114        let gts_desc_count = gts_spec & 0x07;
115        let gts_permit = (gts_spec & 0x80) != 0;
116
117        // GTS Directions (1 byte, present if gts_desc_count > 0)
118        let mut gts_dir_mask: u8 = 0;
119        if gts_desc_count > 0 {
120            if buf.len() < pos + 1 {
121                return Err(FieldError::BufferTooShort {
122                    offset: pos,
123                    need: 1,
124                    have: buf.len().saturating_sub(pos),
125                });
126            }
127            let gts_dir = buf[pos];
128            pos += 1;
129            // Bits 0-6: Direction Mask, Bit 7: Reserved
130            gts_dir_mask = gts_dir & 0x7F;
131        }
132
133        // GTS Descriptor List (3 bytes per descriptor)
134        let gts_list_len = (gts_desc_count as usize) * 3;
135        if buf.len() < pos + gts_list_len {
136            return Err(FieldError::BufferTooShort {
137                offset: pos,
138                need: gts_list_len,
139                have: buf.len().saturating_sub(pos),
140            });
141        }
142        let mut gts_descriptors = Vec::with_capacity(gts_desc_count as usize);
143        for _ in 0..gts_desc_count {
144            let short_addr = read_u16_le(buf, pos)?;
145            let slot_len = buf[pos + 2];
146            let starting_slot = slot_len & 0x0F;
147            let length = (slot_len >> 4) & 0x0F;
148            gts_descriptors.push(GtsDescriptor {
149                short_addr,
150                starting_slot,
151                length,
152            });
153            pos += 3;
154        }
155
156        // Pending Address Specification (1 byte)
157        if buf.len() < pos + 1 {
158            return Err(FieldError::BufferTooShort {
159                offset: pos,
160                need: 1,
161                have: buf.len().saturating_sub(pos),
162            });
163        }
164        let pa_spec = buf[pos];
165        pos += 1;
166
167        // Bits 0-2: Number of Short Addresses Pending
168        // Bit 3: Reserved
169        // Bits 4-6: Number of Extended Addresses Pending
170        // Bit 7: Reserved
171        let pa_num_short = pa_spec & 0x07;
172        let pa_num_long = (pa_spec >> 4) & 0x07;
173
174        // Pending Short Addresses (2 bytes each)
175        let pa_short_len = (pa_num_short as usize) * 2;
176        if buf.len() < pos + pa_short_len {
177            return Err(FieldError::BufferTooShort {
178                offset: pos,
179                need: pa_short_len,
180                have: buf.len().saturating_sub(pos),
181            });
182        }
183        let mut pa_short_addresses = Vec::with_capacity(pa_num_short as usize);
184        for _ in 0..pa_num_short {
185            pa_short_addresses.push(read_u16_le(buf, pos)?);
186            pos += 2;
187        }
188
189        // Pending Long Addresses (8 bytes each)
190        let pa_long_len = (pa_num_long as usize) * 8;
191        if buf.len() < pos + pa_long_len {
192            return Err(FieldError::BufferTooShort {
193                offset: pos,
194                need: pa_long_len,
195                have: buf.len().saturating_sub(pos),
196            });
197        }
198        let mut pa_long_addresses = Vec::with_capacity(pa_num_long as usize);
199        for _ in 0..pa_num_long {
200            let mut bytes = [0u8; 8];
201            bytes.copy_from_slice(&buf[pos..pos + 8]);
202            pa_long_addresses.push(u64::from_le_bytes(bytes));
203            pos += 8;
204        }
205
206        let consumed = pos - offset;
207        Ok((
208            Self {
209                beacon_order,
210                superframe_order,
211                final_cap_slot,
212                battery_life_ext,
213                pan_coordinator,
214                assoc_permit,
215                gts_desc_count,
216                gts_permit,
217                gts_dir_mask,
218                gts_descriptors,
219                pa_num_short,
220                pa_num_long,
221                pa_short_addresses,
222                pa_long_addresses,
223            },
224            consumed,
225        ))
226    }
227
228    /// Build the beacon payload bytes.
229    pub fn build(&self) -> Vec<u8> {
230        let mut out = Vec::new();
231
232        // Superframe Specification (2 bytes, LE)
233        let mut sf_spec: u16 = 0;
234        sf_spec |= (self.beacon_order as u16) & 0x0F;
235        sf_spec |= ((self.superframe_order as u16) & 0x0F) << 4;
236        sf_spec |= ((self.final_cap_slot as u16) & 0x0F) << 8;
237        if self.battery_life_ext {
238            sf_spec |= 0x1000;
239        }
240        if self.pan_coordinator {
241            sf_spec |= 0x4000;
242        }
243        if self.assoc_permit {
244            sf_spec |= 0x8000;
245        }
246        out.extend_from_slice(&sf_spec.to_le_bytes());
247
248        // GTS Specification (1 byte)
249        let gts_count = self.gts_descriptors.len().min(7) as u8;
250        let mut gts_spec: u8 = gts_count & 0x07;
251        if self.gts_permit {
252            gts_spec |= 0x80;
253        }
254        out.push(gts_spec);
255
256        // GTS Directions (1 byte, only if descriptors present)
257        if gts_count > 0 {
258            out.push(self.gts_dir_mask & 0x7F);
259        }
260
261        // GTS Descriptor List (3 bytes each)
262        for desc in &self.gts_descriptors {
263            out.extend_from_slice(&desc.short_addr.to_le_bytes());
264            let slot_len = (desc.starting_slot & 0x0F) | ((desc.length & 0x0F) << 4);
265            out.push(slot_len);
266        }
267
268        // Pending Address Specification (1 byte)
269        let pa_num_short = self.pa_short_addresses.len().min(7) as u8;
270        let pa_num_long = self.pa_long_addresses.len().min(7) as u8;
271        let pa_spec = (pa_num_short & 0x07) | ((pa_num_long & 0x07) << 4);
272        out.push(pa_spec);
273
274        // Pending Short Addresses
275        for &addr in &self.pa_short_addresses {
276            out.extend_from_slice(&addr.to_le_bytes());
277        }
278
279        // Pending Long Addresses
280        for &addr in &self.pa_long_addresses {
281            out.extend_from_slice(&addr.to_le_bytes());
282        }
283
284        out
285    }
286
287    /// Get a human-readable summary of this beacon.
288    pub fn summary(&self) -> String {
289        format!(
290            "802.15.4 Beacon assocPermit({}) panCoord({}) beaconOrder={} sfOrder={}",
291            self.assoc_permit, self.pan_coordinator, self.beacon_order, self.superframe_order,
292        )
293    }
294}
295
296impl Default for BeaconPayload {
297    fn default() -> Self {
298        Self {
299            beacon_order: 15,
300            superframe_order: 15,
301            final_cap_slot: 15,
302            battery_life_ext: false,
303            pan_coordinator: false,
304            assoc_permit: false,
305            gts_desc_count: 0,
306            gts_permit: true,
307            gts_dir_mask: 0,
308            gts_descriptors: Vec::new(),
309            pa_num_short: 0,
310            pa_num_long: 0,
311            pa_short_addresses: Vec::new(),
312            pa_long_addresses: Vec::new(),
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_parse_minimal_beacon() {
323        // Minimal beacon: sf_spec=0xFF0F (beacon_order=15, sf_order=15, final_cap=15),
324        //   gts_spec=0x80 (permit=true, count=0),
325        //   pa_spec=0x00
326        let buf = [
327            0xFF, 0x8F, // Superframe Spec (LE): beacon=15, sf=15, final_cap=15, pan_coord=1
328            0x80, // GTS spec: permit=true, count=0
329            0x00, // Pending addr spec: 0 short, 0 long
330        ];
331        let (beacon, consumed) = BeaconPayload::parse(&buf, 0).unwrap();
332        assert_eq!(consumed, 4);
333        assert_eq!(beacon.beacon_order, 15);
334        assert_eq!(beacon.superframe_order, 15);
335        assert_eq!(beacon.final_cap_slot, 15); // bits 8-11 of 0x8FFF = 0xF
336        assert!(beacon.gts_permit);
337        assert_eq!(beacon.gts_desc_count, 0);
338        assert_eq!(beacon.pa_num_short, 0);
339        assert_eq!(beacon.pa_num_long, 0);
340    }
341
342    #[test]
343    fn test_parse_beacon_with_defaults() {
344        // Build with defaults and round-trip
345        let beacon = BeaconPayload::default();
346        let bytes = beacon.build();
347        let (parsed, consumed) = BeaconPayload::parse(&bytes, 0).unwrap();
348        assert_eq!(consumed, bytes.len());
349        assert_eq!(parsed.beacon_order, 15);
350        assert_eq!(parsed.superframe_order, 15);
351        assert_eq!(parsed.final_cap_slot, 15);
352        assert!(!parsed.battery_life_ext);
353        assert!(!parsed.pan_coordinator);
354        assert!(!parsed.assoc_permit);
355        assert!(parsed.gts_permit);
356        assert_eq!(parsed.gts_desc_count, 0);
357    }
358
359    #[test]
360    fn test_beacon_with_gts_descriptors() {
361        let beacon = BeaconPayload {
362            beacon_order: 7,
363            superframe_order: 3,
364            final_cap_slot: 10,
365            battery_life_ext: false,
366            pan_coordinator: true,
367            assoc_permit: true,
368            gts_desc_count: 2,
369            gts_permit: true,
370            gts_dir_mask: 0x03,
371            gts_descriptors: vec![
372                GtsDescriptor {
373                    short_addr: 0x1234,
374                    starting_slot: 5,
375                    length: 3,
376                },
377                GtsDescriptor {
378                    short_addr: 0x5678,
379                    starting_slot: 8,
380                    length: 2,
381                },
382            ],
383            pa_num_short: 0,
384            pa_num_long: 0,
385            pa_short_addresses: Vec::new(),
386            pa_long_addresses: Vec::new(),
387        };
388
389        let bytes = beacon.build();
390        let (parsed, consumed) = BeaconPayload::parse(&bytes, 0).unwrap();
391        assert_eq!(consumed, bytes.len());
392        assert_eq!(parsed.beacon_order, 7);
393        assert_eq!(parsed.superframe_order, 3);
394        assert_eq!(parsed.final_cap_slot, 10);
395        assert!(parsed.pan_coordinator);
396        assert!(parsed.assoc_permit);
397        assert_eq!(parsed.gts_desc_count, 2);
398        assert!(parsed.gts_permit);
399        assert_eq!(parsed.gts_dir_mask, 0x03);
400        assert_eq!(parsed.gts_descriptors.len(), 2);
401        assert_eq!(parsed.gts_descriptors[0].short_addr, 0x1234);
402        assert_eq!(parsed.gts_descriptors[0].starting_slot, 5);
403        assert_eq!(parsed.gts_descriptors[0].length, 3);
404        assert_eq!(parsed.gts_descriptors[1].short_addr, 0x5678);
405        assert_eq!(parsed.gts_descriptors[1].starting_slot, 8);
406        assert_eq!(parsed.gts_descriptors[1].length, 2);
407    }
408
409    #[test]
410    fn test_beacon_with_pending_addresses() {
411        let beacon = BeaconPayload {
412            beacon_order: 15,
413            superframe_order: 15,
414            final_cap_slot: 15,
415            battery_life_ext: false,
416            pan_coordinator: false,
417            assoc_permit: false,
418            gts_desc_count: 0,
419            gts_permit: false,
420            gts_dir_mask: 0,
421            gts_descriptors: Vec::new(),
422            pa_num_short: 2,
423            pa_num_long: 1,
424            pa_short_addresses: vec![0x1111, 0x2222],
425            pa_long_addresses: vec![0x0102030405060708],
426        };
427
428        let bytes = beacon.build();
429        let (parsed, consumed) = BeaconPayload::parse(&bytes, 0).unwrap();
430        assert_eq!(consumed, bytes.len());
431        assert_eq!(parsed.pa_num_short, 2);
432        assert_eq!(parsed.pa_num_long, 1);
433        assert_eq!(parsed.pa_short_addresses, vec![0x1111, 0x2222]);
434        assert_eq!(parsed.pa_long_addresses, vec![0x0102030405060708]);
435    }
436
437    #[test]
438    fn test_parse_with_offset() {
439        let beacon = BeaconPayload::default();
440        let bytes = beacon.build();
441        let mut buf = vec![0xAA, 0xBB, 0xCC];
442        buf.extend_from_slice(&bytes);
443
444        let (parsed, _) = BeaconPayload::parse(&buf, 3).unwrap();
445        assert_eq!(parsed.beacon_order, 15);
446    }
447
448    #[test]
449    fn test_parse_buffer_too_short() {
450        let buf = [0xFF]; // Too short for superframe spec
451        let result = BeaconPayload::parse(&buf, 0);
452        assert!(result.is_err());
453    }
454
455    #[test]
456    fn test_build_roundtrip() {
457        let beacon = BeaconPayload {
458            beacon_order: 4,
459            superframe_order: 2,
460            final_cap_slot: 9,
461            battery_life_ext: true,
462            pan_coordinator: true,
463            assoc_permit: true,
464            gts_desc_count: 1,
465            gts_permit: true,
466            gts_dir_mask: 0x01,
467            gts_descriptors: vec![GtsDescriptor {
468                short_addr: 0xABCD,
469                starting_slot: 3,
470                length: 4,
471            }],
472            pa_num_short: 1,
473            pa_num_long: 0,
474            pa_short_addresses: vec![0x9999],
475            pa_long_addresses: Vec::new(),
476        };
477
478        let bytes = beacon.build();
479        let (parsed, _) = BeaconPayload::parse(&bytes, 0).unwrap();
480        assert_eq!(parsed.beacon_order, beacon.beacon_order);
481        assert_eq!(parsed.superframe_order, beacon.superframe_order);
482        assert_eq!(parsed.final_cap_slot, beacon.final_cap_slot);
483        assert_eq!(parsed.battery_life_ext, beacon.battery_life_ext);
484        assert_eq!(parsed.pan_coordinator, beacon.pan_coordinator);
485        assert_eq!(parsed.assoc_permit, beacon.assoc_permit);
486        assert_eq!(parsed.gts_permit, beacon.gts_permit);
487        assert_eq!(parsed.gts_dir_mask, beacon.gts_dir_mask);
488        assert_eq!(parsed.gts_descriptors, beacon.gts_descriptors);
489        assert_eq!(parsed.pa_short_addresses, beacon.pa_short_addresses);
490        assert_eq!(parsed.pa_long_addresses, beacon.pa_long_addresses);
491    }
492
493    #[test]
494    fn test_summary() {
495        let beacon = BeaconPayload {
496            assoc_permit: true,
497            pan_coordinator: true,
498            beacon_order: 7,
499            superframe_order: 3,
500            ..BeaconPayload::default()
501        };
502        let summary = beacon.summary();
503        assert!(summary.contains("assocPermit(true)"));
504        assert!(summary.contains("panCoord(true)"));
505        assert!(summary.contains("beaconOrder=7"));
506    }
507}