crafter 0.3.2

Packet-level network interaction for Rust tools and agents.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
//! IEEE 802.15.4 MAC frame layer scaffolding.

use core::any::Any;

use crate::error::{CrafterError, Result};
use crate::field::Field;
use crate::packet::{IntoPacket, Layer, LayerContext, Packet};

use super::consts::{
    DOT15D4_EXTENDED_ADDR_LEN, DOT15D4_FCF_LEN, DOT15D4_FCS_LEN, DOT15D4_PAN_ID_LEN,
    DOT15D4_SEQ_LEN, DOT15D4_SHORT_ADDR_LEN,
};
use super::fcs::dot15d4_fcs;

pub use super::consts::{Dot15d4AddrMode, Dot15d4FrameType};

/// IEEE 802.15.4 MAC frame.
///
/// `Dot15d4` is the 802.15.4 analog of `BleLlAdv`: a framed-PDU layer carrying
/// the MAC Frame Control field (FCF), sequence number, optional destination and
/// source PAN identifiers and addresses, a payload, and a trailing 2-octet
/// Frame Check Sequence (FCS). Every header field uses [`Field<T>`] so a value
/// the caller sets explicitly survives `compile()` untouched (including values
/// that are wrong on purpose), while any field left unset is auto-filled.
///
/// Field semantics are grounded in `.agents/docs/dot15d4-manifest.md` and
/// `.agents/docs/dot15d4-codepoints.md` (IEEE Std 802.15.4-2020, Clause 7.2):
///
/// - The 16-bit FCF packs the frame type (bits 0-2), the security-enabled
///   (bit 3), frame-pending (bit 4), ack-request (bit 5), and PAN-ID-compression
///   (bit 6) flags, the destination addressing mode (bits 10-11), the frame
///   version (bits 12-13), and the source addressing mode (bits 14-15).
/// - The sequence number is a single octet (Clause 7.2.3).
/// - PAN identifiers and addresses are present according to the addressing
///   modes and the PAN-ID-compression flag; addresses are stored as `u64` with
///   the corresponding addressing mode deciding 16-bit (short) versus 64-bit
///   (extended) serialization (Clause 7.2.4 through 7.2.8).
/// - The FCS is a 16-bit CRC auto-filled during `compile()` (Clause 7.2.10).
///
/// Builders, `compile()`/FCS auto-fill, the `Layer` implementation, and decode
/// arrive in later steps; this step only defines the struct, a manual `Clone`,
/// and the `new()` constructor.
#[derive(Debug)]
pub struct Dot15d4 {
    /// Frame type stored in FCF bits 0..=2.
    frame_type: Field<Dot15d4FrameType>,
    /// Security Enabled flag (FCF bit 3).
    security_enabled: Field<bool>,
    /// Frame Pending flag (FCF bit 4).
    frame_pending: Field<bool>,
    /// Acknowledgment Request flag (FCF bit 5).
    ack_request: Field<bool>,
    /// PAN ID Compression flag (FCF bit 6).
    pan_id_compression: Field<bool>,
    /// Frame Version stored in FCF bits 12..=13.
    frame_version: Field<u8>,
    /// Destination addressing mode (FCF bits 10..=11).
    dest_addr_mode: Field<Dot15d4AddrMode>,
    /// Source addressing mode (FCF bits 14..=15).
    src_addr_mode: Field<Dot15d4AddrMode>,
    /// Sequence number octet.
    seq: Field<u8>,
    /// Destination PAN identifier when present.
    dest_pan: Field<u16>,
    /// Destination address; the addressing mode decides 16- vs 64-bit form.
    dest_addr: Field<u64>,
    /// Source PAN identifier when present.
    src_pan: Field<u16>,
    /// Source address; the addressing mode decides 16- vs 64-bit form.
    src_addr: Field<u64>,
    /// MAC payload octets carried after the addressing fields.
    payload: Vec<u8>,
    /// Frame Check Sequence; auto-filled during compile.
    fcs: Field<u16>,
}

impl Clone for Dot15d4 {
    fn clone(&self) -> Self {
        Self {
            frame_type: self.frame_type.clone(),
            security_enabled: self.security_enabled.clone(),
            frame_pending: self.frame_pending.clone(),
            ack_request: self.ack_request.clone(),
            pan_id_compression: self.pan_id_compression.clone(),
            frame_version: self.frame_version.clone(),
            dest_addr_mode: self.dest_addr_mode.clone(),
            src_addr_mode: self.src_addr_mode.clone(),
            seq: self.seq.clone(),
            dest_pan: self.dest_pan.clone(),
            dest_addr: self.dest_addr.clone(),
            src_pan: self.src_pan.clone(),
            src_addr: self.src_addr.clone(),
            payload: self.payload.clone(),
            fcs: self.fcs.clone(),
        }
    }
}

impl Dot15d4 {
    /// Create an empty 802.15.4 MAC frame with every header field unset.
    ///
    /// All fields start as [`Field::unset`] and the payload is empty; builders
    /// and `compile()`-time auto-fill arrive in later steps.
    pub fn new() -> Self {
        Self {
            frame_type: Field::unset(),
            security_enabled: Field::unset(),
            frame_pending: Field::unset(),
            ack_request: Field::unset(),
            pan_id_compression: Field::unset(),
            frame_version: Field::unset(),
            dest_addr_mode: Field::unset(),
            src_addr_mode: Field::unset(),
            seq: Field::unset(),
            dest_pan: Field::unset(),
            dest_addr: Field::unset(),
            src_pan: Field::unset(),
            src_addr: Field::unset(),
            payload: Vec::new(),
            fcs: Field::unset(),
        }
    }

    /// Create a Data MAC frame.
    ///
    /// Sets the frame type to [`Dot15d4FrameType::Data`]; every other field is
    /// left unset for later auto-fill.
    pub fn data() -> Self {
        Self::new().frame_type(Dot15d4FrameType::Data)
    }

    /// Create a Beacon MAC frame.
    ///
    /// Sets the frame type to [`Dot15d4FrameType::Beacon`]; every other field is
    /// left unset for later auto-fill.
    pub fn beacon() -> Self {
        Self::new().frame_type(Dot15d4FrameType::Beacon)
    }

    /// Create an Acknowledgment MAC frame.
    ///
    /// Sets the frame type to [`Dot15d4FrameType::Ack`]; every other field is
    /// left unset for later auto-fill.
    pub fn ack() -> Self {
        Self::new().frame_type(Dot15d4FrameType::Ack)
    }

    /// Create a MAC Command frame.
    ///
    /// Sets the frame type to [`Dot15d4FrameType::MacCommand`]; every other
    /// field is left unset for later auto-fill.
    pub fn command() -> Self {
        Self::new().frame_type(Dot15d4FrameType::MacCommand)
    }

    /// Set the frame type (FCF bits 0..=2).
    pub fn frame_type(mut self, frame_type: Dot15d4FrameType) -> Self {
        self.frame_type.set_user(frame_type);
        self
    }

    /// Set the sequence number octet.
    pub fn seq(mut self, seq: u8) -> Self {
        self.seq.set_user(seq);
        self
    }

    /// Set or clear the Security Enabled flag (FCF bit 3).
    pub fn security(mut self, security_enabled: bool) -> Self {
        self.security_enabled.set_user(security_enabled);
        self
    }

    /// Set or clear the Frame Pending flag (FCF bit 4).
    pub fn frame_pending(mut self, frame_pending: bool) -> Self {
        self.frame_pending.set_user(frame_pending);
        self
    }

    /// Set or clear the Acknowledgment Request flag (FCF bit 5).
    pub fn ack_request(mut self, ack_request: bool) -> Self {
        self.ack_request.set_user(ack_request);
        self
    }

    /// Set or clear the PAN ID Compression flag (FCF bit 6).
    pub fn pan_id_compression(mut self, pan_id_compression: bool) -> Self {
        self.pan_id_compression.set_user(pan_id_compression);
        self
    }

    /// Set the Frame Version (FCF bits 12..=13).
    pub fn frame_version(mut self, frame_version: u8) -> Self {
        self.frame_version.set_user(frame_version);
        self
    }

    /// Set the MAC payload octets carried after the addressing fields.
    pub fn payload(mut self, payload: &[u8]) -> Self {
        self.payload = payload.to_vec();
        self
    }

    /// Set a 16-bit (short) destination PAN identifier and address.
    ///
    /// Sets `dest_pan`/`dest_addr` and, unless the caller already set
    /// `dest_addr_mode` explicitly, marks the destination addressing mode as
    /// [`Dot15d4AddrMode::Short`] (IEEE Std 802.15.4-2020, Clause 7.2.2.8).
    pub fn dest_short(mut self, pan: u16, addr: u16) -> Self {
        self.dest_pan.set_user(pan);
        self.dest_addr.set_user(u64::from(addr));
        if !self.dest_addr_mode.is_user_set() {
            self.dest_addr_mode.set_user(Dot15d4AddrMode::Short);
        }
        self
    }

    /// Set a 64-bit (extended) destination PAN identifier and address.
    ///
    /// Sets `dest_pan`/`dest_addr` and, unless the caller already set
    /// `dest_addr_mode` explicitly, marks the destination addressing mode as
    /// [`Dot15d4AddrMode::Extended`] (IEEE Std 802.15.4-2020, Clause 7.2.2.8).
    pub fn dest_extended(mut self, pan: u16, addr: u64) -> Self {
        self.dest_pan.set_user(pan);
        self.dest_addr.set_user(addr);
        if !self.dest_addr_mode.is_user_set() {
            self.dest_addr_mode.set_user(Dot15d4AddrMode::Extended);
        }
        self
    }

    /// Set a 16-bit (short) source PAN identifier and address.
    ///
    /// Sets `src_pan`/`src_addr` and, unless the caller already set
    /// `src_addr_mode` explicitly, marks the source addressing mode as
    /// [`Dot15d4AddrMode::Short`] (IEEE Std 802.15.4-2020, Clause 7.2.2.10).
    pub fn src_short(mut self, pan: u16, addr: u16) -> Self {
        self.src_pan.set_user(pan);
        self.src_addr.set_user(u64::from(addr));
        if !self.src_addr_mode.is_user_set() {
            self.src_addr_mode.set_user(Dot15d4AddrMode::Short);
        }
        self
    }

    /// Set a 64-bit (extended) source PAN identifier and address.
    ///
    /// Sets `src_pan`/`src_addr` and, unless the caller already set
    /// `src_addr_mode` explicitly, marks the source addressing mode as
    /// [`Dot15d4AddrMode::Extended`] (IEEE Std 802.15.4-2020, Clause 7.2.2.10).
    pub fn src_extended(mut self, pan: u16, addr: u64) -> Self {
        self.src_pan.set_user(pan);
        self.src_addr.set_user(addr);
        if !self.src_addr_mode.is_user_set() {
            self.src_addr_mode.set_user(Dot15d4AddrMode::Extended);
        }
        self
    }

    /// Resolve the effective destination addressing mode (FCF bits 10..=11).
    ///
    /// Honors a user-set `dest_addr_mode`; otherwise infers it from the
    /// presence of a destination address: a set address defaults to
    /// [`Dot15d4AddrMode::Short`] (a fuller short/extended distinction is made
    /// by the typed `dest_short`/`dest_extended` builders), and an unset
    /// address resolves to [`Dot15d4AddrMode::None`]
    /// (IEEE Std 802.15.4-2020, Clause 7.2.2.8).
    pub(crate) fn effective_dest_addr_mode(&self) -> Dot15d4AddrMode {
        match self.dest_addr_mode.value() {
            Some(mode) => *mode,
            None => {
                if self.dest_addr.value().is_some() {
                    Dot15d4AddrMode::Short
                } else {
                    Dot15d4AddrMode::None
                }
            }
        }
    }

    /// Resolve the effective source addressing mode (FCF bits 14..=15).
    ///
    /// Honors a user-set `src_addr_mode`; otherwise infers it from the
    /// presence of a source address, mirroring
    /// [`Dot15d4::effective_dest_addr_mode`]
    /// (IEEE Std 802.15.4-2020, Clause 7.2.2.10).
    pub(crate) fn effective_src_addr_mode(&self) -> Dot15d4AddrMode {
        match self.src_addr_mode.value() {
            Some(mode) => *mode,
            None => {
                if self.src_addr.value().is_some() {
                    Dot15d4AddrMode::Short
                } else {
                    Dot15d4AddrMode::None
                }
            }
        }
    }

    /// Resolve the effective addressing mode for the requested direction.
    ///
    /// `effective_addr_mode(true)` returns the destination mode and
    /// `effective_addr_mode(false)` the source mode, sharing the same
    /// honored-override-then-infer rule used by the per-direction resolvers.
    #[cfg_attr(not(test), allow(dead_code))]
    pub(crate) fn effective_addr_mode(&self, destination: bool) -> Dot15d4AddrMode {
        if destination {
            self.effective_dest_addr_mode()
        } else {
            self.effective_src_addr_mode()
        }
    }

    /// Resolve the effective PAN-ID-compression bit (FCF bit 6).
    ///
    /// Honors a user-set `pan_id_compression`; otherwise applies the standard
    /// default rule: compression is set when both the destination and source
    /// addresses are present and share the same PAN identifier, so the source
    /// PAN ID is omitted on the wire and the single shared PAN ID is serialized
    /// once (IEEE Std 802.15.4-2020, Clause 7.2.2.6, PAN ID Compression field).
    pub(crate) fn effective_pan_id_compression(&self) -> bool {
        if let Some(value) = self.pan_id_compression.value() {
            return *value;
        }

        let dest_present = self.effective_dest_addr_mode() != Dot15d4AddrMode::None;
        let src_present = self.effective_src_addr_mode() != Dot15d4AddrMode::None;
        if !(dest_present && src_present) {
            return false;
        }

        match (self.dest_pan.value(), self.src_pan.value()) {
            (Some(dest_pan), Some(src_pan)) => dest_pan == src_pan,
            // With both addresses present and only one PAN ID supplied, treat
            // the single PAN as shared and compress.
            (Some(_), None) | (None, Some(_)) => true,
            (None, None) => false,
        }
    }

    /// Resolve whether the destination PAN identifier is present on the wire.
    ///
    /// The destination PAN ID is present whenever the destination addressing
    /// mode is not [`Dot15d4AddrMode::None`]
    /// (IEEE Std 802.15.4-2020, Clause 7.2.2, Table 7-2).
    pub(crate) fn effective_dest_pan_present(&self) -> bool {
        self.effective_dest_addr_mode() != Dot15d4AddrMode::None
    }

    /// Resolve whether the source PAN identifier is present on the wire.
    ///
    /// The source PAN ID is present when the source addressing mode is not
    /// [`Dot15d4AddrMode::None`] and PAN-ID compression is not in effect; when
    /// compression is set the source PAN ID is omitted and the destination PAN
    /// ID serves both addresses
    /// (IEEE Std 802.15.4-2020, Clause 7.2.2.6 / Table 7-2).
    pub(crate) fn effective_src_pan_present(&self) -> bool {
        if self.effective_src_addr_mode() == Dot15d4AddrMode::None {
            return false;
        }
        !self.effective_pan_id_compression()
    }

    /// Resolve the effective frame type (FCF bits 0..=2).
    ///
    /// Honors a user-set `frame_type`; otherwise defaults to
    /// [`Dot15d4FrameType::Data`] (the type the bare `Dot15d4::new()` frame
    /// carries on the wire).
    fn effective_frame_type(&self) -> Dot15d4FrameType {
        self.frame_type
            .value()
            .copied()
            .unwrap_or(Dot15d4FrameType::Data)
    }

    /// Resolve the effective sequence number (auto-fill 0 when unset).
    fn effective_seq(&self) -> u8 {
        self.seq.value().copied().unwrap_or(0)
    }

    /// Resolve the effective Frame Version (FCF bits 12..=13, default 0).
    fn effective_frame_version(&self) -> u8 {
        self.frame_version.value().copied().unwrap_or(0)
    }

    /// Assemble the 16-bit Frame Control field from the effective fields.
    ///
    /// Packs the frame type (bits 0..=2), the security-enabled (bit 3),
    /// frame-pending (bit 4), ack-request (bit 5), and PAN-ID-compression
    /// (bit 6) flags, the destination addressing mode (bits 10..=11), the frame
    /// version (bits 12..=13), and the source addressing mode (bits 14..=15),
    /// per IEEE Std 802.15.4-2020, Clause 7.2.2 (see
    /// `.agents/docs/dot15d4-manifest.md`). User-set flags are honored exactly;
    /// the FCF is never "corrected" to be consistent with the addresses
    /// present.
    fn frame_control(&self) -> u16 {
        let frame_type = u16::from(self.effective_frame_type().as_u3() & 0b111);
        let security = u16::from(self.security_enabled.value().copied().unwrap_or(false));
        let frame_pending = u16::from(self.frame_pending.value().copied().unwrap_or(false));
        let ack_request = u16::from(self.ack_request.value().copied().unwrap_or(false));
        let pan_id_compression = u16::from(self.effective_pan_id_compression());
        let dest_mode = u16::from(self.effective_dest_addr_mode().as_u2() & 0b11);
        let frame_version = u16::from(self.effective_frame_version() & 0b11);
        let src_mode = u16::from(self.effective_src_addr_mode().as_u2() & 0b11);

        frame_type
            | (security << 3)
            | (frame_pending << 4)
            | (ack_request << 5)
            | (pan_id_compression << 6)
            | (dest_mode << 10)
            | (frame_version << 12)
            | (src_mode << 14)
    }

    /// Number of address octets implied by an addressing mode.
    ///
    /// `None` carries no address, `Short` two octets, `Extended` eight, per
    /// IEEE Std 802.15.4-2020, Clause 7.2.4 through 7.2.8.
    fn addr_octets(mode: Dot15d4AddrMode) -> usize {
        match mode {
            Dot15d4AddrMode::None => 0,
            Dot15d4AddrMode::Short => DOT15D4_SHORT_ADDR_LEN,
            Dot15d4AddrMode::Extended => DOT15D4_EXTENDED_ADDR_LEN,
        }
    }

    /// Append an address sized by its addressing mode, little-endian.
    fn encode_addr(out: &mut Vec<u8>, mode: Dot15d4AddrMode, addr: u64) {
        match mode {
            Dot15d4AddrMode::None => {}
            Dot15d4AddrMode::Short => out.extend_from_slice(&(addr as u16).to_le_bytes()),
            Dot15d4AddrMode::Extended => out.extend_from_slice(&addr.to_le_bytes()),
        }
    }

    /// Encoded length, in octets, of the full MAC frame including the FCS.
    ///
    /// Mirrors [`Dot15d4::encode`]: FCF + sequence number + the addressing
    /// fields implied by the effective addressing modes and PAN-ID compression
    /// + payload + the 2-octet FCS.
    pub(crate) fn encoded_len(&self) -> usize {
        let dest_mode = self.effective_dest_addr_mode();
        let src_mode = self.effective_src_addr_mode();

        let mut len = DOT15D4_FCF_LEN + DOT15D4_SEQ_LEN;
        if self.effective_dest_pan_present() {
            len += DOT15D4_PAN_ID_LEN;
        }
        len += Self::addr_octets(dest_mode);
        if self.effective_src_pan_present() {
            len += DOT15D4_PAN_ID_LEN;
        }
        len += Self::addr_octets(src_mode);
        len += self.payload.len();
        len += DOT15D4_FCS_LEN;
        len
    }

    /// Append the MAC header (FCF + sequence + addressing) and this layer's own
    /// `payload`, without the trailing FCS.
    ///
    /// Emits the Frame Control field (little-endian), the sequence number, the
    /// addressing fields in spec order (destination PAN, destination address,
    /// source PAN — omitted under PAN-ID compression —, source address, each
    /// little-endian and sized by its addressing mode), and the MAC payload.
    /// Every user-set field is honored verbatim; no value is clamped or
    /// "corrected" (IEEE Std 802.15.4-2020, Clause 7.2; see
    /// `.agents/docs/dot15d4-manifest.md`). The caller appends the FCS over the
    /// full MAC frame (this header/payload plus any following layers).
    fn encode_header_and_payload(&self, out: &mut Vec<u8>) {
        out.extend_from_slice(&self.frame_control().to_le_bytes());
        out.push(self.effective_seq());

        let dest_mode = self.effective_dest_addr_mode();
        let src_mode = self.effective_src_addr_mode();

        if self.effective_dest_pan_present() {
            out.extend_from_slice(&self.dest_pan.value().copied().unwrap_or(0).to_le_bytes());
        }
        Self::encode_addr(out, dest_mode, self.dest_addr.value().copied().unwrap_or(0));

        if self.effective_src_pan_present() {
            out.extend_from_slice(&self.src_pan.value().copied().unwrap_or(0).to_le_bytes());
        }
        Self::encode_addr(out, src_mode, self.src_addr.value().copied().unwrap_or(0));

        out.extend_from_slice(&self.payload);
    }

    /// Append the trailing 2-octet Frame Check Sequence over the MAC frame.
    ///
    /// `frame_start` is the offset in `out` where this MAC frame began; the FCS
    /// is computed over `out[frame_start..]` (the MAC header, this layer's
    /// payload, and any following layers already appended) via [`dot15d4_fcs`]
    /// and pushed little-endian (low byte first). A user-set `fcs` is appended
    /// exactly as supplied (malformed-on-purpose is supported), little-endian,
    /// without recomputing it.
    fn append_fcs(&self, frame_start: usize, out: &mut Vec<u8>) {
        let fcs = match self.fcs.value() {
            Some(value) => *value,
            None => dot15d4_fcs(&out[frame_start..]),
        };
        out.extend_from_slice(&fcs.to_le_bytes());
    }

    /// Serialize the standalone 802.15.4 MAC frame to bytes, auto-filling the FCS.
    ///
    /// Emits the MAC header and this layer's `payload`, then a trailing 2-octet
    /// Frame Check Sequence computed over the emitted header and payload via
    /// [`dot15d4_fcs`] (or the user-set `fcs` verbatim). This is the terminal
    /// form used when the MAC frame carries no following layers; the layered
    /// `compile` path instead computes the FCS over the header, payload, and the
    /// following layers' bytes so the FCS is the last octets of the whole frame
    /// (IEEE Std 802.15.4-2020, Clause 7.2; see `.agents/docs/dot15d4-manifest.md`).
    pub(crate) fn encode(&self, out: &mut Vec<u8>) {
        let start = out.len();
        self.encode_header_and_payload(out);
        self.append_fcs(start, out);
    }
}

impl Default for Dot15d4 {
    fn default() -> Self {
        Self::new()
    }
}

/// Stable label for an 802.15.4 MAC frame type used in summaries.
fn dot15d4_frame_type_label(frame_type: Dot15d4FrameType) -> &'static str {
    match frame_type {
        Dot15d4FrameType::Beacon => "Beacon",
        Dot15d4FrameType::Data => "Data",
        Dot15d4FrameType::Ack => "Ack",
        Dot15d4FrameType::MacCommand => "MacCommand",
    }
}

/// Display an address sized by its addressing mode, or `None` when absent.
///
/// Short addresses render as four hex digits and extended addresses as sixteen,
/// each prefixed `0x`, matching the on-wire address width.
fn dot15d4_addr_label(mode: Dot15d4AddrMode, addr: Option<u64>) -> Option<String> {
    match (mode, addr) {
        (Dot15d4AddrMode::Short, Some(addr)) => Some(format!("0x{:04X}", addr as u16)),
        (Dot15d4AddrMode::Extended, Some(addr)) => Some(format!("0x{addr:016X}")),
        // `None` mode or a missing address: no address to display.
        _ => None,
    }
}

impl Layer for Dot15d4 {
    fn name(&self) -> &'static str {
        "Dot15d4"
    }

    fn summary(&self) -> String {
        let mut fields = vec![dot15d4_frame_type_label(self.effective_frame_type()).to_string()];

        fields.push(format!("seq={}", self.effective_seq()));

        if let Some(dst) = dot15d4_addr_label(
            self.effective_dest_addr_mode(),
            self.dest_addr.value().copied(),
        ) {
            fields.push(format!("dst={dst}"));
        }
        if let Some(src) = dot15d4_addr_label(
            self.effective_src_addr_mode(),
            self.src_addr.value().copied(),
        ) {
            fields.push(format!("src={src}"));
        }

        format!("Dot15d4({})", fields.join(", "))
    }

    fn inspection_fields(&self) -> Vec<(&'static str, String)> {
        let mut fields = vec![
            (
                "frame_type",
                dot15d4_frame_type_label(self.effective_frame_type()).to_string(),
            ),
            ("seq", self.effective_seq().to_string()),
            (
                "security_enabled",
                self.security_enabled
                    .value()
                    .copied()
                    .unwrap_or(false)
                    .to_string(),
            ),
            (
                "frame_pending",
                self.frame_pending
                    .value()
                    .copied()
                    .unwrap_or(false)
                    .to_string(),
            ),
            (
                "ack_request",
                self.ack_request
                    .value()
                    .copied()
                    .unwrap_or(false)
                    .to_string(),
            ),
            (
                "pan_id_compression",
                self.effective_pan_id_compression().to_string(),
            ),
        ];

        if let Some(dst) = dot15d4_addr_label(
            self.effective_dest_addr_mode(),
            self.dest_addr.value().copied(),
        ) {
            fields.push(("dest_addr", dst));
        }
        if let Some(src) = dot15d4_addr_label(
            self.effective_src_addr_mode(),
            self.src_addr.value().copied(),
        ) {
            fields.push(("src_addr", src));
        }

        fields
    }

    fn encoded_len(&self) -> usize {
        Dot15d4::encoded_len(self)
    }

    fn encoded_len_with_context(&self, ctx: &LayerContext<'_>) -> usize {
        // The standalone `encoded_len` already counts the MAC header, this
        // layer's own payload, and the trailing 2-octet FCS. When following
        // layers ride inside the MAC payload, add their encoded length so the
        // capacity hint stays accurate (matching the IGMP/ICMP precedent).
        Dot15d4::encoded_len(self) + ctx.packet().encoded_len_after(ctx.index())
    }

    fn compile(&self, ctx: &LayerContext<'_>, out: &mut Vec<u8>) -> Result<()> {
        // The FCS is the LAST two octets of the whole MAC frame, computed over
        // the MAC header, this layer's payload, and every following layer's
        // bytes (IEEE Std 802.15.4-2020, Clause 7.2). Emit the header+payload,
        // then the following layers, then append the FCS over all of it. A
        // user-set `fcs` is honored verbatim (malformed-on-purpose is supported).
        let start = out.len();
        self.encode_header_and_payload(out);
        if let Err(err) = ctx.packet().compile_layers_after_into(ctx.index(), out) {
            out.truncate(start);
            return Err(err);
        }
        self.append_fcs(start, out);
        Ok(())
    }

    fn consumes_following(&self) -> bool {
        // This layer owns the trailing FCS over the following layers and emits
        // them itself in `compile`, so the packet must not re-emit them after
        // this layer (mirrors ICMPv4/IGMP checksum-over-following layers).
        true
    }

    fn clone_layer(&self) -> Box<dyn Layer> {
        Box::new(self.clone())
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }

    fn into_any(self: Box<Self>) -> Box<dyn Any> {
        self
    }
}

impl<R: IntoPacket> core::ops::Div<R> for Dot15d4 {
    type Output = Packet;

    fn div(self, rhs: R) -> Self::Output {
        Packet::from_layer(self).concat(rhs)
    }
}

/// Read an address sized by its addressing mode from `bytes` at `*offset`.
///
/// Advances `*offset` past the consumed octets and returns the address as a
/// `u64` (short addresses occupy the low 16 bits). A `None` mode reads nothing.
/// Truncation surfaces a structured [`CrafterError`] with the supplied context.
fn read_dot15d4_addr(
    bytes: &[u8],
    offset: &mut usize,
    mode: Dot15d4AddrMode,
    context: &'static str,
) -> Result<u64> {
    let width = Dot15d4::addr_octets(mode);
    if width == 0 {
        return Ok(0);
    }

    let required = *offset + width;
    if bytes.len() < required {
        return Err(CrafterError::buffer_too_short(
            context,
            required,
            bytes.len(),
        ));
    }

    let addr = match mode {
        Dot15d4AddrMode::None => 0,
        Dot15d4AddrMode::Short => {
            u64::from(u16::from_le_bytes([bytes[*offset], bytes[*offset + 1]]))
        }
        Dot15d4AddrMode::Extended => u64::from_le_bytes([
            bytes[*offset],
            bytes[*offset + 1],
            bytes[*offset + 2],
            bytes[*offset + 3],
            bytes[*offset + 4],
            bytes[*offset + 5],
            bytes[*offset + 6],
            bytes[*offset + 7],
        ]),
    };
    *offset = required;
    Ok(addr)
}

/// Read a 2-octet little-endian PAN identifier from `bytes` at `*offset`.
///
/// Advances `*offset` past the consumed octets. Truncation surfaces a structured
/// [`CrafterError`] with the supplied context.
fn read_dot15d4_pan(bytes: &[u8], offset: &mut usize, context: &'static str) -> Result<u16> {
    let required = *offset + DOT15D4_PAN_ID_LEN;
    if bytes.len() < required {
        return Err(CrafterError::buffer_too_short(
            context,
            required,
            bytes.len(),
        ));
    }
    let pan = u16::from_le_bytes([bytes[*offset], bytes[*offset + 1]]);
    *offset = required;
    Ok(pan)
}

/// Decode an IEEE 802.15.4 MAC frame header, addressing, payload, and FCS.
///
/// Parses the 16-bit Frame Control field (little-endian), derives the
/// addressing-field layout from the FCF (honoring PAN-ID compression), consumes
/// the destination/source PAN identifiers and addresses, splits the trailing
/// 2-octet Frame Check Sequence (stored verbatim; an FCS mismatch is **not**
/// rejected — validity is recorded through the radio descriptor), and returns
/// the decoded layer plus the inner MAC payload as the tail for the next layer.
///
/// Every parsed field is stored as [`Field::user`] so a round-trip through
/// `encode` reproduces the same bytes. Truncation mid-FCF, mid-address, or
/// missing-FCS surfaces a structured [`CrafterError`] with stable context
/// strings (`"dot15d4.mac.fcf"`, `"dot15d4.mac.addressing"`, `"dot15d4.mac.fcs"`)
/// rather than panicking, and a reserved frame type surfaces as
/// [`CrafterError::invalid_field_value`] for `"dot15d4.mac.frame_type"`
/// (IEEE Std 802.15.4-2020, Clause 7.2; see `.agents/docs/dot15d4-manifest.md`).
pub(crate) fn decode_dot15d4(bytes: &[u8]) -> Result<(Dot15d4, &[u8])> {
    if bytes.len() < DOT15D4_FCF_LEN {
        return Err(CrafterError::buffer_too_short(
            "dot15d4.mac.fcf",
            DOT15D4_FCF_LEN,
            bytes.len(),
        ));
    }

    let fcf = u16::from_le_bytes([bytes[0], bytes[1]]);
    let frame_type = Dot15d4FrameType::from_u3((fcf & 0b111) as u8).ok_or_else(|| {
        CrafterError::invalid_field_value("dot15d4.mac.frame_type", "reserved frame type")
    })?;
    let security_enabled = (fcf & (1 << 3)) != 0;
    let frame_pending = (fcf & (1 << 4)) != 0;
    let ack_request = (fcf & (1 << 5)) != 0;
    let pan_id_compression = (fcf & (1 << 6)) != 0;
    let dest_addr_mode = Dot15d4AddrMode::from_u2(((fcf >> 10) & 0b11) as u8).ok_or_else(|| {
        CrafterError::invalid_field_value(
            "dot15d4.mac.addressing",
            "reserved destination addressing mode",
        )
    })?;
    let frame_version = ((fcf >> 12) & 0b11) as u8;
    let src_addr_mode = Dot15d4AddrMode::from_u2(((fcf >> 14) & 0b11) as u8).ok_or_else(|| {
        CrafterError::invalid_field_value(
            "dot15d4.mac.addressing",
            "reserved source addressing mode",
        )
    })?;

    // The MHR is at least the FCF plus the 1-octet sequence number.
    let seq_offset = DOT15D4_FCF_LEN;
    if bytes.len() < seq_offset + DOT15D4_SEQ_LEN {
        return Err(CrafterError::buffer_too_short(
            "dot15d4.mac.seq",
            seq_offset + DOT15D4_SEQ_LEN,
            bytes.len(),
        ));
    }
    let seq = bytes[seq_offset];

    // Addressing-field presence follows the FCF and the PAN-ID-compression bit
    // (IEEE Std 802.15.4-2020, Clause 7.2.2, Table 7-2). The destination PAN ID
    // is present whenever a destination address is present; the source PAN ID is
    // present when a source address is present and compression is not in effect.
    let dest_present = dest_addr_mode != Dot15d4AddrMode::None;
    let src_present = src_addr_mode != Dot15d4AddrMode::None;
    let dest_pan_present = dest_present;
    let src_pan_present = src_present && !pan_id_compression;

    let mut offset = seq_offset + DOT15D4_SEQ_LEN;

    let dest_pan = if dest_pan_present {
        Field::user(read_dot15d4_pan(
            bytes,
            &mut offset,
            "dot15d4.mac.addressing",
        )?)
    } else {
        Field::unset()
    };
    let dest_addr = if dest_present {
        Field::user(read_dot15d4_addr(
            bytes,
            &mut offset,
            dest_addr_mode,
            "dot15d4.mac.addressing",
        )?)
    } else {
        Field::unset()
    };

    let src_pan = if src_pan_present {
        Field::user(read_dot15d4_pan(
            bytes,
            &mut offset,
            "dot15d4.mac.addressing",
        )?)
    } else {
        Field::unset()
    };
    let src_addr = if src_present {
        Field::user(read_dot15d4_addr(
            bytes,
            &mut offset,
            src_addr_mode,
            "dot15d4.mac.addressing",
        )?)
    } else {
        Field::unset()
    };

    // The frame must carry at least the trailing 2-octet FCS after the MHR.
    if bytes.len() < offset + DOT15D4_FCS_LEN {
        return Err(CrafterError::buffer_too_short(
            "dot15d4.mac.fcs",
            offset + DOT15D4_FCS_LEN,
            bytes.len(),
        ));
    }

    let payload_end = bytes.len() - DOT15D4_FCS_LEN;
    let payload = &bytes[offset..payload_end];
    let fcs = u16::from_le_bytes([bytes[payload_end], bytes[payload_end + 1]]);

    let frame = Dot15d4 {
        frame_type: Field::user(frame_type),
        security_enabled: Field::user(security_enabled),
        frame_pending: Field::user(frame_pending),
        ack_request: Field::user(ack_request),
        pan_id_compression: Field::user(pan_id_compression),
        frame_version: Field::user(frame_version),
        dest_addr_mode: Field::user(dest_addr_mode),
        src_addr_mode: Field::user(src_addr_mode),
        seq: Field::user(seq),
        dest_pan,
        dest_addr,
        src_pan,
        src_addr,
        // The inner MAC payload is returned as the tail so the next layer
        // (Zigbee NWK/APS or `Raw`) decodes it; the decoded MAC header carries
        // no payload of its own.
        payload: Vec::new(),
        fcs: Field::user(fcs),
    };

    Ok((frame, payload))
}

#[cfg(test)]
mod dot15d4_mac_builder {
    use super::{Dot15d4, Dot15d4FrameType};

    #[test]
    fn frame_type_constructors_mark_frame_type_user_set() {
        for (frame, expected) in [
            (Dot15d4::data(), Dot15d4FrameType::Data),
            (Dot15d4::beacon(), Dot15d4FrameType::Beacon),
            (Dot15d4::ack(), Dot15d4FrameType::Ack),
            (Dot15d4::command(), Dot15d4FrameType::MacCommand),
        ] {
            assert!(frame.frame_type.is_user_set());
            assert_eq!(frame.frame_type.value(), Some(&expected));
        }
    }

    #[test]
    fn setters_mark_their_fields_user_set() {
        let frame = Dot15d4::new()
            .frame_type(Dot15d4FrameType::Data)
            .seq(7)
            .security(true)
            .frame_pending(true)
            .ack_request(true)
            .pan_id_compression(true)
            .frame_version(2);

        assert!(frame.frame_type.is_user_set());
        assert!(frame.seq.is_user_set());
        assert!(frame.security_enabled.is_user_set());
        assert!(frame.frame_pending.is_user_set());
        assert!(frame.ack_request.is_user_set());
        assert!(frame.pan_id_compression.is_user_set());
        assert!(frame.frame_version.is_user_set());

        assert_eq!(frame.security_enabled.value(), Some(&true));
        assert_eq!(frame.frame_pending.value(), Some(&true));
        assert_eq!(frame.ack_request.value(), Some(&true));
        assert_eq!(frame.pan_id_compression.value(), Some(&true));
        assert_eq!(frame.frame_version.value(), Some(&2));
    }

    #[test]
    fn data_seq_payload_holds_expected_values() {
        let frame = Dot15d4::data().seq(7).payload(&[1, 2, 3]);

        assert_eq!(frame.frame_type.value(), Some(&Dot15d4FrameType::Data));
        assert_eq!(frame.seq.value(), Some(&7));
        assert_eq!(frame.payload, vec![1, 2, 3]);
    }

    #[test]
    fn untouched_fields_stay_unset() {
        let frame = Dot15d4::data().seq(7).payload(&[1, 2, 3]);

        assert!(frame.security_enabled.is_unset());
        assert!(frame.frame_pending.is_unset());
        assert!(frame.ack_request.is_unset());
        assert!(frame.pan_id_compression.is_unset());
        assert!(frame.frame_version.is_unset());
        assert!(frame.fcs.is_unset());
    }
}

#[cfg(test)]
mod dot15d4_mac_address {
    use super::{Dot15d4, Dot15d4AddrMode};

    #[test]
    fn short_dest_and_src_share_pan_under_compression() {
        let frame = Dot15d4::data()
            .dest_short(0xABCD, 0x1234)
            .src_short(0xABCD, 0x5678);

        // Typed builders set the addressing modes to Short.
        assert_eq!(frame.dest_addr_mode.value(), Some(&Dot15d4AddrMode::Short));
        assert_eq!(frame.src_addr_mode.value(), Some(&Dot15d4AddrMode::Short));
        assert_eq!(frame.dest_addr.value(), Some(&0x1234));
        assert_eq!(frame.src_addr.value(), Some(&0x5678));

        // Resolvers report Short modes.
        assert_eq!(frame.effective_dest_addr_mode(), Dot15d4AddrMode::Short);
        assert_eq!(frame.effective_src_addr_mode(), Dot15d4AddrMode::Short);
        assert_eq!(frame.effective_addr_mode(true), Dot15d4AddrMode::Short);
        assert_eq!(frame.effective_addr_mode(false), Dot15d4AddrMode::Short);

        // Both addresses present and share a PAN: compression defaults on, so
        // the source PAN ID is omitted and the single shared PAN is serialized
        // once via the destination PAN ID.
        assert!(frame.effective_pan_id_compression());
        assert!(frame.effective_dest_pan_present());
        assert!(!frame.effective_src_pan_present());
    }

    #[test]
    fn extended_dest_and_src_addresses() {
        let frame = Dot15d4::data()
            .dest_extended(0x0001, 0x0011_2233_4455_6677)
            .src_extended(0x0002, 0x8899_AABB_CCDD_EEFF);

        assert_eq!(
            frame.dest_addr_mode.value(),
            Some(&Dot15d4AddrMode::Extended)
        );
        assert_eq!(
            frame.src_addr_mode.value(),
            Some(&Dot15d4AddrMode::Extended)
        );
        assert_eq!(frame.dest_addr.value(), Some(&0x0011_2233_4455_6677));
        assert_eq!(frame.src_addr.value(), Some(&0x8899_AABB_CCDD_EEFF));

        assert_eq!(frame.effective_dest_addr_mode(), Dot15d4AddrMode::Extended);
        assert_eq!(frame.effective_src_addr_mode(), Dot15d4AddrMode::Extended);

        // Distinct PAN IDs: compression stays off and both PAN IDs are present.
        assert!(!frame.effective_pan_id_compression());
        assert!(frame.effective_dest_pan_present());
        assert!(frame.effective_src_pan_present());
    }

    #[test]
    fn destination_only_frame_has_no_source_addressing() {
        let frame = Dot15d4::data().dest_short(0xABCD, 0x1234);

        assert_eq!(frame.effective_dest_addr_mode(), Dot15d4AddrMode::Short);
        assert_eq!(frame.effective_src_addr_mode(), Dot15d4AddrMode::None);

        // With no source address, compression does not apply; the destination
        // PAN ID is present and there is no source PAN ID.
        assert!(!frame.effective_pan_id_compression());
        assert!(frame.effective_dest_pan_present());
        assert!(!frame.effective_src_pan_present());
    }

    #[test]
    fn typed_builder_does_not_override_explicit_addr_mode() {
        // Caller sets an extended mode but then supplies a short address on
        // purpose; the explicit mode must survive (malformed-on-purpose).
        let frame = Dot15d4::data();
        let mut frame = frame;
        frame.dest_addr_mode.set_user(Dot15d4AddrMode::Extended);
        let frame = frame.dest_short(0xABCD, 0x1234);

        assert_eq!(
            frame.dest_addr_mode.value(),
            Some(&Dot15d4AddrMode::Extended)
        );
        assert_eq!(frame.effective_dest_addr_mode(), Dot15d4AddrMode::Extended);
    }

    #[test]
    fn user_set_compression_is_honored() {
        // Destination-only frame would default compression off, but an explicit
        // request to compress must be honored.
        let frame = Dot15d4::data()
            .dest_short(0xABCD, 0x1234)
            .pan_id_compression(true);

        assert!(frame.effective_pan_id_compression());

        // And an explicit request to disable compression on a frame that would
        // otherwise compress must also be honored.
        let frame = Dot15d4::data()
            .dest_short(0xABCD, 0x1234)
            .src_short(0xABCD, 0x5678)
            .pan_id_compression(false);

        assert!(!frame.effective_pan_id_compression());
        assert!(frame.effective_src_pan_present());
    }
}

#[cfg(test)]
mod dot15d4_mac_encode {
    use super::Dot15d4;

    fn encode(frame: &Dot15d4) -> Vec<u8> {
        let mut out = Vec::new();
        frame.encode(&mut out);
        out
    }

    #[test]
    fn short_dest_src_data_frame_matches_reference() {
        // Data frame, short dest+src sharing PAN 0xABCD (PAN-ID compression
        // defaults on, so the source PAN ID is omitted), sequence number 7,
        // payload [0xCA, 0xFE].
        //
        // FCF = 0x8841 (Data=001, PAN-ID compression bit 6 set, dest mode Short
        // = 0b10 in bits 10..=11, src mode Short = 0b10 in bits 14..=15), LE
        // bytes 41 88. The MHR+payload is 41 88 07 CD AB 34 12 78 56 CA FE; the
        // reflected CRC-16/CCITT FCS over those octets is 0x8B43, serialized
        // little-endian as 43 8B. Cross-checked against the `dot15d4_fcs`
        // reference algorithm (the reference backend's `makeFCS`).
        let frame = Dot15d4::data()
            .seq(7)
            .dest_short(0xABCD, 0x1234)
            .src_short(0xABCD, 0x5678)
            .payload(&[0xCA, 0xFE]);

        let bytes = encode(&frame);

        assert_eq!(
            bytes,
            vec![
                0x41, 0x88, // FCF (little-endian)
                0x07, // sequence number
                0xCD, 0xAB, // dest PAN 0xABCD (little-endian)
                0x34, 0x12, // dest short address 0x1234 (little-endian)
                // source PAN omitted under PAN-ID compression
                0x78, 0x56, // src short address 0x5678 (little-endian)
                0xCA, 0xFE, // payload
                0x43, 0x8B, // FCS 0x8B43 (little-endian)
            ]
        );

        // encoded_len() matches the serialized length.
        assert_eq!(frame.encoded_len(), bytes.len());
    }

    #[test]
    fn user_set_wrong_fcs_is_emitted_verbatim() {
        // The same frame, but the caller forces a deliberately wrong FCS; the
        // emitted trailing two octets must be exactly that value
        // (little-endian), not the recomputed correct 0x8B43.
        let frame = Dot15d4::data()
            .seq(7)
            .dest_short(0xABCD, 0x1234)
            .src_short(0xABCD, 0x5678)
            .payload(&[0xCA, 0xFE]);
        let mut frame = frame;
        frame.fcs.set_user(0xDEAD);
        let bytes = encode(&frame);

        // Header + payload unchanged from the reference frame.
        assert_eq!(
            &bytes[..bytes.len() - 2],
            &[0x41, 0x88, 0x07, 0xCD, 0xAB, 0x34, 0x12, 0x78, 0x56, 0xCA, 0xFE]
        );
        // Trailing FCS is the wrong user value, little-endian (AD DE), not the
        // recomputed 0x8B43 (43 8B).
        assert_eq!(&bytes[bytes.len() - 2..], &[0xAD, 0xDE]);
    }
}

#[cfg(test)]
mod dot15d4_mac_layer {
    use super::{decode_dot15d4, Dot15d4, Dot15d4AddrMode, Dot15d4FrameType};
    use crate::error::CrafterError;
    use crate::packet::{Layer, Packet, Raw};

    /// The reference short-dest/short-src data frame from step 16.
    fn reference_frame() -> Dot15d4 {
        Dot15d4::data()
            .seq(7)
            .dest_short(0xABCD, 0x1234)
            .src_short(0xABCD, 0x5678)
            .payload(&[0xCA, 0xFE])
    }

    #[test]
    fn layer_compile_equals_encode() {
        // Compiling the MAC frame through the packet stack must emit exactly the
        // same bytes the standalone encoder produces.
        let frame = reference_frame();

        let mut encoded = Vec::new();
        frame.encode(&mut encoded);

        let compiled = Packet::from_layer(frame.clone())
            .compile()
            .expect("compile Dot15d4 MAC frame");

        assert_eq!(compiled.as_bytes(), encoded.as_slice());
        assert_eq!(compiled.len(), Layer::encoded_len(&frame));
    }

    #[test]
    fn layer_name_and_summary() {
        let frame = reference_frame();
        assert_eq!(frame.name(), "Dot15d4");
        assert_eq!(
            frame.summary(),
            "Dot15d4(Data, seq=7, dst=0x1234, src=0x5678)"
        );

        // Inspection fields surface the frame type, sequence number, flags, and
        // addresses.
        let fields = frame.inspection_fields();
        assert!(fields.contains(&("frame_type", "Data".to_string())));
        assert!(fields.contains(&("seq", "7".to_string())));
        assert!(fields.contains(&("dest_addr", "0x1234".to_string())));
        assert!(fields.contains(&("src_addr", "0x5678".to_string())));
    }

    #[test]
    fn decode_round_trips_reference_frame() {
        // Encode the reference frame, then decode the bytes and confirm the MAC
        // header fields round-trip and the inner payload is returned as the tail.
        let frame = reference_frame();
        let mut bytes = Vec::new();
        frame.encode(&mut bytes);

        let (decoded, tail) = decode_dot15d4(&bytes).expect("decode reference MAC frame");

        // The inner MAC payload is returned as the tail for the next layer.
        assert_eq!(tail, &[0xCA, 0xFE]);

        assert_eq!(decoded.frame_type.value(), Some(&Dot15d4FrameType::Data));
        assert_eq!(decoded.seq.value(), Some(&7));
        assert_eq!(
            decoded.dest_addr_mode.value(),
            Some(&Dot15d4AddrMode::Short)
        );
        assert_eq!(decoded.src_addr_mode.value(), Some(&Dot15d4AddrMode::Short));
        assert_eq!(decoded.dest_pan.value(), Some(&0xABCD));
        assert_eq!(decoded.dest_addr.value(), Some(&0x1234));
        // Source PAN omitted on the wire under PAN-ID compression.
        assert_eq!(decoded.pan_id_compression.value(), Some(&true));
        assert!(decoded.src_pan.is_unset());
        assert_eq!(decoded.src_addr.value(), Some(&0x5678));
        // The trailing FCS is stored verbatim (the auto-filled 0x8B43).
        assert_eq!(decoded.fcs.value(), Some(&0x8B43));

        // Re-attaching the payload reproduces the original frame bytes exactly.
        let mut reencoded = Vec::new();
        decoded.payload(&[0xCA, 0xFE]).encode(&mut reencoded);
        assert_eq!(reencoded, bytes);
    }

    #[test]
    fn decode_too_short_fcf_is_structured_error() {
        // A single octet cannot hold the 2-octet FCF.
        let err = decode_dot15d4(&[0x41]).expect_err("must reject a truncated FCF");

        assert_eq!(err, CrafterError::buffer_too_short("dot15d4.mac.fcf", 2, 1));
    }

    #[test]
    fn decode_addressing_claiming_more_bytes_than_present_is_structured_error() {
        // FCF 0x8841: Data, PAN-ID compression set, short dest + short src
        // addressing. Supply the FCF, sequence number, and a full destination
        // PAN + address but truncate the source address so the declared
        // addressing claims more bytes than are present.
        let bytes = [
            0x41, 0x88, // FCF (little-endian): short dest + short src, compressed
            0x07, // sequence number
            0xCD, 0xAB, // dest PAN
            0x34, 0x12, // dest short address
            0x78, // only one octet of the 2-octet src short address
        ];

        let err = decode_dot15d4(&bytes)
            .expect_err("must reject addressing that claims more bytes than present");

        assert_eq!(
            err,
            CrafterError::buffer_too_short("dot15d4.mac.addressing", 9, bytes.len())
        );
    }

    #[test]
    fn decode_reserved_frame_type_is_structured_error() {
        // FCF low three bits = 0b100 (frame type 4) is reserved and is reported
        // structurally rather than modeled.
        let bytes = [0x04, 0x00, 0x00, 0x00, 0x00];
        let err = decode_dot15d4(&bytes).expect_err("must reject a reserved frame type");

        assert_eq!(
            err,
            CrafterError::invalid_field_value("dot15d4.mac.frame_type", "reserved frame type")
        );
    }

    #[test]
    fn div_builds_two_layer_packet() {
        let packet = Dot15d4::data().seq(1) / Raw::from_bytes([0xAA, 0xBB]);

        assert_eq!(packet.len(), 2);
        assert!(packet.layer::<Dot15d4>().is_some());
        assert!(packet.layer::<Raw>().is_some());
    }

    #[test]
    fn dot15d4_mac_roundtrip() {
        // Case 1: short dest + short src addressing.
        //
        // Build a Data frame addressed short-to-short on the lab PAN 0xABCD
        // (documentation-style identifiers), compile it through the packet
        // stack, and confirm the bytes match the spec-derived reference, then
        // decode them and confirm every MAC header field round-trips.
        //
        // FCF = 0x8841 (Data=001, PAN-ID compression bit 6 set because both
        // addresses share PAN 0xABCD, dest mode Short = 0b10 in bits 10..=11,
        // src mode Short = 0b10 in bits 14..=15), little-endian 41 88. The
        // MHR+payload 41 88 07 CD AB 34 12 78 56 CA FE carries a reflected
        // CRC-16/CCITT FCS of 0x8B43, serialized little-endian as 43 8B.
        let frame = Dot15d4::data()
            .seq(7)
            .dest_short(0xABCD, 0x1234)
            .src_short(0xABCD, 0x5678)
            .payload(&[0xCA, 0xFE]);

        let compiled = Packet::from_layer(frame)
            .compile()
            .expect("compile short-addressed Dot15d4 MAC frame");
        let bytes = compiled.as_bytes();

        // The FCF is the first two octets, little-endian.
        assert_eq!(&bytes[..2], &[0x41, 0x88]);
        // The trailing two octets are the auto-filled FCS, little-endian.
        assert_eq!(&bytes[bytes.len() - 2..], &[0x43, 0x8B]);
        assert_eq!(
            bytes,
            &[
                0x41, 0x88, // FCF (little-endian)
                0x07, // sequence number
                0xCD, 0xAB, // dest PAN 0xABCD (little-endian)
                0x34, 0x12, // dest short address 0x1234 (little-endian)
                // source PAN omitted under PAN-ID compression
                0x78, 0x56, // src short address 0x5678 (little-endian)
                0xCA, 0xFE, // payload
                0x43, 0x8B, // FCS 0x8B43 (little-endian)
            ]
        );

        let (decoded, tail) = decode_dot15d4(bytes).expect("decode short-addressed MAC frame");

        // The inner MAC payload is returned as the tail for the next layer.
        assert_eq!(tail, &[0xCA, 0xFE]);
        // Frame type and sequence number round-trip.
        assert_eq!(decoded.frame_type.value(), Some(&Dot15d4FrameType::Data));
        assert_eq!(decoded.seq.value(), Some(&7));
        // Destination PAN/address pair round-trips.
        assert_eq!(
            decoded.dest_addr_mode.value(),
            Some(&Dot15d4AddrMode::Short)
        );
        assert_eq!(decoded.dest_pan.value(), Some(&0xABCD));
        assert_eq!(decoded.dest_addr.value(), Some(&0x1234));
        // Source address round-trips; the source PAN is omitted on the wire
        // under PAN-ID compression but is implied by the shared destination PAN.
        assert_eq!(decoded.src_addr_mode.value(), Some(&Dot15d4AddrMode::Short));
        assert_eq!(decoded.pan_id_compression.value(), Some(&true));
        assert!(decoded.src_pan.is_unset());
        assert_eq!(decoded.src_addr.value(), Some(&0x5678));
        // The trailing FCS is stored verbatim (the auto-filled 0x8B43).
        assert_eq!(decoded.fcs.value(), Some(&0x8B43));

        // Case 2: extended (64-bit) dest + src addressing with PAN-ID
        // compression. Both addresses share lab PAN 0x1AAA, so compression
        // defaults on and the source PAN ID is omitted on the wire; the frame
        // must round-trip back to the same bytes.
        let ext = Dot15d4::data()
            .seq(42)
            .dest_extended(0x1AAA, 0x0011_2233_4455_6677)
            .src_extended(0x1AAA, 0x8899_AABB_CCDD_EEFF)
            .payload(&[0xDE, 0xAD, 0xBE, 0xEF]);

        // Compression is on (shared PAN), so the source PAN ID is not serialized.
        assert!(ext.effective_pan_id_compression());
        assert!(ext.effective_dest_pan_present());
        assert!(!ext.effective_src_pan_present());

        let ext_bytes = Packet::from_layer(ext.clone())
            .compile()
            .expect("compile extended-addressed Dot15d4 MAC frame");
        let ext_bytes = ext_bytes.as_bytes();

        let (ext_decoded, ext_tail) =
            decode_dot15d4(ext_bytes).expect("decode extended-addressed MAC frame");

        assert_eq!(ext_tail, &[0xDE, 0xAD, 0xBE, 0xEF]);
        assert_eq!(
            ext_decoded.frame_type.value(),
            Some(&Dot15d4FrameType::Data)
        );
        assert_eq!(ext_decoded.seq.value(), Some(&42));
        assert_eq!(
            ext_decoded.dest_addr_mode.value(),
            Some(&Dot15d4AddrMode::Extended)
        );
        assert_eq!(
            ext_decoded.src_addr_mode.value(),
            Some(&Dot15d4AddrMode::Extended)
        );
        assert_eq!(ext_decoded.dest_pan.value(), Some(&0x1AAA));
        assert_eq!(ext_decoded.dest_addr.value(), Some(&0x0011_2233_4455_6677));
        // Source PAN omitted under PAN-ID compression; the shared PAN is implied.
        assert_eq!(ext_decoded.pan_id_compression.value(), Some(&true));
        assert!(ext_decoded.src_pan.is_unset());
        assert_eq!(ext_decoded.src_addr.value(), Some(&0x8899_AABB_CCDD_EEFF));

        // Re-attaching the payload reproduces the original extended frame bytes.
        let reencoded = Packet::from_layer(ext_decoded.payload(&[0xDE, 0xAD, 0xBE, 0xEF]))
            .compile()
            .expect("recompile extended-addressed MAC frame");
        assert_eq!(reencoded.as_bytes(), ext_bytes);

        // A frame truncated mid-FCF returns a structured error, never a panic.
        let err = decode_dot15d4(&[0x41]).expect_err("must reject a truncated FCF");
        assert_eq!(err, CrafterError::buffer_too_short("dot15d4.mac.fcf", 2, 1));
    }
}

#[cfg(test)]
mod dot15d4_stack {
    use super::super::aps::{decode_zigbee_aps, ZigbeeAps};
    use super::super::nwk::{decode_zigbee_nwk, ZigbeeNwk};
    use super::{decode_dot15d4, dot15d4_fcs, Dot15d4, Dot15d4AddrMode, Dot15d4FrameType};
    use crate::packet::Packet;

    #[test]
    fn dot15d4_stack_roundtrip() {
        // A full Dot15d4 / ZigbeeNwk / ZigbeeAps stack composed with `/`, using
        // lab-safe addresses/PANs (documentation-style identifiers). The MAC FCS
        // must be the LAST two octets of the whole frame, computed over the MAC
        // header plus every following layer's bytes (IEEE Std 802.15.4-2020,
        // Clause 7.2), not over the MAC header alone.
        let packet = Dot15d4::data()
            .dest_short(0x1234, 0x0000)
            .src_short(0x1234, 0xABCD)
            .seq(9)
            / ZigbeeNwk::data().dest(0x0000).src(0xABCD).radius(30).seq(5)
            / ZigbeeAps::data()
                .cluster(0x0006)
                .profile(0x0104)
                .dest_endpoint(1)
                .src_endpoint(1)
                .counter(7)
                .payload(&[0x01, 0x02]);

        let compiled = packet
            .compile()
            .expect("compile Dot15d4/ZigbeeNwk/ZigbeeAps stack");
        let bytes = compiled.as_bytes();

        // Reference MAC header (FCF + seq + addressing), NWK header, and APS
        // header + payload, each derived from the per-layer spec layout.
        //
        // MAC FCF = 0x8841 (Data=0b001, PAN-ID compression bit 6 set because
        // both addresses share PAN 0x1234, dest mode Short = 0b10 in bits
        // 10..=11, src mode Short = 0b10 in bits 14..=15), little-endian 41 88;
        // seq 9; dest PAN 0x1234 (34 12); dest short address 0x0000 (00 00);
        // source PAN omitted under PAN-ID compression; src short address 0xABCD
        // (CD AB).
        let mac_header = [0x41u8, 0x88, 0x09, 0x34, 0x12, 0x00, 0x00, 0xCD, 0xAB];
        // NWK FC = 0x0008 (Data + default protocol version 0x02), little-endian
        // 08 00; dest 0x0000 (00 00); src 0xABCD (CD AB); radius 30 (1E); seq 5.
        let nwk_header = [0x08u8, 0x00, 0x00, 0x00, 0xCD, 0xAB, 0x1E, 0x05];
        // APS FC = 0x00 (Data + unicast delivery); dest endpoint 1; cluster
        // 0x0006 (06 00); profile 0x0104 (04 01); src endpoint 1; counter 7;
        // payload 01 02.
        let aps = [0x00u8, 0x01, 0x06, 0x00, 0x04, 0x01, 0x01, 0x07, 0x01, 0x02];

        let mut frame_no_fcs = Vec::new();
        frame_no_fcs.extend_from_slice(&mac_header);
        frame_no_fcs.extend_from_slice(&nwk_header);
        frame_no_fcs.extend_from_slice(&aps);

        // The compiled frame is exactly the spec-derived MAC+NWK+APS bytes plus
        // the 2-octet FCS, so the FCS is the LAST two octets, not in the middle.
        assert_eq!(bytes.len(), frame_no_fcs.len() + 2);
        assert_eq!(&bytes[..frame_no_fcs.len()], frame_no_fcs.as_slice());

        // The trailing two octets are the FCS computed over ALL preceding octets
        // (the full MAC header + NWK + APS), serialized little-endian. Computing
        // the FCS over the MAC header alone would land it in the middle of the
        // frame; here it is the suffix over everything.
        let expected_fcs = dot15d4_fcs(&frame_no_fcs);
        assert_eq!(&bytes[frame_no_fcs.len()..], &expected_fcs.to_le_bytes());

        // Decode the frame back by chaining the per-layer decoders over the
        // successive tails: MAC payload -> NWK -> APS.
        let (mac, mac_tail) = decode_dot15d4(bytes).expect("decode MAC frame");
        // The MAC payload tail is the NWK + APS bytes (the FCS is split off).
        assert_eq!(
            mac_tail,
            frame_no_fcs[mac_header.len()..].to_vec().as_slice()
        );
        assert_eq!(mac.frame_type.value(), Some(&Dot15d4FrameType::Data));
        assert_eq!(mac.seq.value(), Some(&9));
        assert_eq!(mac.dest_addr_mode.value(), Some(&Dot15d4AddrMode::Short));
        assert_eq!(mac.src_addr_mode.value(), Some(&Dot15d4AddrMode::Short));
        assert_eq!(mac.dest_pan.value(), Some(&0x1234));
        assert_eq!(mac.dest_addr.value(), Some(&0x0000));
        // Source PAN omitted on the wire under PAN-ID compression.
        assert_eq!(mac.pan_id_compression.value(), Some(&true));
        assert!(mac.src_pan.is_unset());
        assert_eq!(mac.src_addr.value(), Some(&0xABCD));
        // The trailing FCS is stored verbatim (the auto-filled value over the
        // whole frame).
        assert_eq!(mac.fcs.value(), Some(&expected_fcs));

        let (nwk, nwk_tail) = decode_zigbee_nwk(mac_tail).expect("decode NWK frame");
        // The NWK payload tail is the APS bytes.
        assert_eq!(nwk_tail, aps.as_slice());
        // The decoded NWK re-encodes to its header + the APS bytes it carries.
        assert_eq!(nwk.encode(), frame_no_fcs[mac_header.len()..].to_vec());

        let (aps_decoded, aps_tail) = decode_zigbee_aps(nwk_tail).expect("decode APS frame");
        // The final APS payload is the application bytes.
        assert_eq!(aps_tail, &[0x01, 0x02]);
        // The decoded APS re-encodes to its full header + payload verbatim.
        assert_eq!(aps_decoded.encode(), aps.to_vec());

        // Byte-stable: recompiling the decoded layers reproduces the compiled
        // frame exactly. The decoded MAC carries an empty payload and the FCS
        // user-set to the original value; re-attaching the decoded NWK bytes
        // (which embed the APS bytes as the NWK payload) as the MAC payload and
        // recompiling reproduces the original frame, FCS included.
        let recompiled = Packet::from_layer(mac.payload(&nwk.encode()))
            .compile()
            .expect("recompile decoded layers");
        assert_eq!(recompiled.as_bytes(), bytes);
    }
}