Skip to main content

openipc_core/
radiotap.rs

1/// 802.11 data frame type used for ordinary injected payloads.
2pub const FRAME_TYPE_DATA: u8 = 0x08;
3/// 802.11 RTS frame type used by OpenIPC adaptive-link uplink packets.
4pub const FRAME_TYPE_RTS: u8 = 0xb4;
5
6const IEEE80211_RADIOTAP_MCS_HAVE_BW: u8 = 0x01;
7const IEEE80211_RADIOTAP_MCS_HAVE_MCS: u8 = 0x02;
8const IEEE80211_RADIOTAP_MCS_HAVE_GI: u8 = 0x04;
9const IEEE80211_RADIOTAP_MCS_HAVE_FEC: u8 = 0x10;
10const IEEE80211_RADIOTAP_MCS_HAVE_STBC: u8 = 0x20;
11const IEEE80211_RADIOTAP_MCS_SGI: u8 = 0x04;
12const IEEE80211_RADIOTAP_MCS_FEC_LDPC: u8 = 0x10;
13const IEEE80211_RADIOTAP_MCS_STBC_SHIFT: u8 = 5;
14
15const IEEE80211_RADIOTAP_VHT_FLAG_STBC: u8 = 0x01;
16const IEEE80211_RADIOTAP_VHT_FLAG_SGI: u8 = 0x04;
17const IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0: u8 = 0x01;
18
19const RADIOTAP_PRESENT_RATE: u32 = 1 << 2;
20const RADIOTAP_PRESENT_CHANNEL: u32 = 1 << 3;
21const RADIOTAP_PRESENT_TX_FLAGS: u32 = 1 << 15;
22const RADIOTAP_PRESENT_MCS: u32 = 1 << 19;
23const RADIOTAP_PRESENT_VHT: u32 = 1 << 21;
24const RADIOTAP_TX_FLAGS_NO_ACK: u16 = 0x0008;
25
26/// Length of the legacy-rate radiotap header built by this crate.
27pub const RADIOTAP_LEGACY_LEN: usize = 13;
28/// Length of the HT/MCS radiotap header built by this crate.
29pub const RADIOTAP_HT_LEN: usize = 13;
30/// Length of the VHT radiotap header built by this crate.
31pub const RADIOTAP_VHT_LEN: usize = 22;
32
33/// Channel bandwidth requested for an injected frame.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ChannelBandwidth {
36    /// 20 MHz channel.
37    Mhz20,
38    /// 40 MHz channel.
39    Mhz40,
40    /// 80 MHz channel.
41    Mhz80,
42    /// 160 MHz channel.
43    Mhz160,
44}
45
46impl ChannelBandwidth {
47    /// Convert this bandwidth to the compact bit pattern used in Realtek TX descriptors.
48    pub const fn realtek_desc_bits(self) -> u8 {
49        match self {
50            Self::Mhz20 => 0,
51            Self::Mhz40 => 1,
52            Self::Mhz80 | Self::Mhz160 => 2,
53        }
54    }
55
56    const fn ht_mcs_bits(self) -> u8 {
57        match self {
58            Self::Mhz20 => 0,
59            Self::Mhz40 | Self::Mhz80 | Self::Mhz160 => 1,
60        }
61    }
62
63    const fn vht_bits(self) -> u8 {
64        match self {
65            Self::Mhz20 => 0x00,
66            Self::Mhz40 => 0x01,
67            Self::Mhz80 => 0x04,
68            Self::Mhz160 => 0x0b,
69        }
70    }
71}
72
73/// Physical-layer rate family for a transmit packet.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TxModeKind {
76    /// Legacy 802.11a/b/g rate.
77    Legacy,
78    /// 802.11n HT MCS rate.
79    Ht,
80    /// 802.11ac VHT MCS rate.
81    Vht,
82}
83
84/// Parsed or requested TX mode for radiotap and Realtek descriptor generation.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct TxMode {
87    /// Rate family.
88    pub kind: TxModeKind,
89    /// Legacy rate in 500 kbps units, matching the radiotap rate field.
90    pub legacy_rate_500kbps: u8,
91    /// HT MCS index for HT frames.
92    pub ht_mcs: u8,
93    /// VHT MCS index for VHT frames.
94    pub vht_mcs: u8,
95    /// VHT spatial stream count.
96    pub vht_nss: u8,
97    /// Requested channel bandwidth.
98    pub bandwidth: ChannelBandwidth,
99    /// Use short guard interval when supported.
100    pub short_gi: bool,
101    /// Use LDPC coding when supported.
102    pub ldpc: bool,
103    /// Use STBC when supported.
104    pub stbc: bool,
105}
106
107impl TxMode {
108    /// Build a legacy TX mode from a radiotap 500 kbps rate value.
109    pub const fn legacy(rate_500kbps: u8) -> Self {
110        Self {
111            kind: TxModeKind::Legacy,
112            legacy_rate_500kbps: rate_500kbps,
113            ht_mcs: 0,
114            vht_mcs: 0,
115            vht_nss: 1,
116            bandwidth: ChannelBandwidth::Mhz20,
117            short_gi: false,
118            ldpc: false,
119            stbc: false,
120        }
121    }
122
123    /// Build the default OpenIPC legacy 6 Mbps TX mode.
124    pub const fn legacy_6m() -> Self {
125        Self::legacy(12)
126    }
127
128    /// Build a legacy 1 Mbps TX mode.
129    pub const fn legacy_1m() -> Self {
130        Self::legacy(2)
131    }
132
133    /// Build an HT TX mode with the supplied MCS index.
134    pub const fn ht(mcs: u8) -> Self {
135        Self {
136            kind: TxModeKind::Ht,
137            legacy_rate_500kbps: 12,
138            ht_mcs: mcs,
139            vht_mcs: 0,
140            vht_nss: 1,
141            bandwidth: ChannelBandwidth::Mhz20,
142            short_gi: false,
143            ldpc: false,
144            stbc: false,
145        }
146    }
147
148    /// Build a VHT TX mode with the supplied stream count and MCS index.
149    pub const fn vht(nss: u8, mcs: u8) -> Self {
150        Self {
151            kind: TxModeKind::Vht,
152            legacy_rate_500kbps: 12,
153            ht_mcs: 0,
154            vht_mcs: mcs,
155            vht_nss: nss,
156            bandwidth: ChannelBandwidth::Mhz20,
157            short_gi: false,
158            ldpc: false,
159            stbc: false,
160        }
161    }
162}
163
164impl Default for TxMode {
165    fn default() -> Self {
166        Self::legacy_6m()
167    }
168}
169
170/// Compact radio parameters used when building OpenIPC uplink radiotap headers.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct TxRadioParams {
173    /// MCS index for HT/VHT rates.
174    pub mcs_index: u8,
175    /// Number of spatial streams for VHT rates.
176    pub nss: u8,
177    /// Requested channel bandwidth.
178    pub bandwidth: ChannelBandwidth,
179    /// Use short guard interval when supported.
180    pub short_gi: bool,
181    /// STBC stream count encoded into radiotap.
182    pub stbc: u8,
183    /// Use LDPC coding when supported.
184    pub ldpc: bool,
185    /// Emit a VHT header instead of an HT header.
186    pub vht: bool,
187    /// 802.11 frame type to use when wrapping WFB packets.
188    pub frame_type: u8,
189}
190
191impl TxRadioParams {
192    /// Return the default OpenIPC adaptive-link uplink radio parameters.
193    pub const fn openipc_uplink_default() -> Self {
194        Self {
195            mcs_index: 0,
196            nss: 1,
197            bandwidth: ChannelBandwidth::Mhz20,
198            short_gi: false,
199            stbc: 1,
200            ldpc: true,
201            vht: false,
202            frame_type: FRAME_TYPE_RTS,
203        }
204    }
205}
206
207impl Default for TxRadioParams {
208    fn default() -> Self {
209        Self::openipc_uplink_default()
210    }
211}
212
213/// Decoded radiotap TX information in the older OpenIPC adaptive-link shape.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct RadiotapTxInfo {
216    /// Whether this packet used VHT rather than HT/legacy radiotap fields.
217    pub vht: bool,
218    /// MCS index when present.
219    pub mcs_index: u8,
220    /// Number of spatial streams.
221    pub nss: u8,
222    /// Channel bandwidth.
223    pub bandwidth: ChannelBandwidth,
224    /// Whether short guard interval was requested.
225    pub short_gi: bool,
226    /// STBC stream count.
227    pub stbc: u8,
228    /// Whether LDPC coding was requested.
229    pub ldpc: bool,
230}
231
232/// TX metadata decoded from the supported radiotap fields.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct RadiotapTxMetadata {
235    /// Per-packet TX mode, when a RATE, MCS, or VHT field was present.
236    pub mode: Option<TxMode>,
237    /// Requested center frequency from the radiotap CHANNEL field.
238    pub frequency_mhz: Option<u16>,
239}
240
241/// Error returned while parsing a radiotap TX header.
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum RadiotapError {
244    /// Packet is too short to contain the radiotap fixed header.
245    TooShort,
246    /// Radiotap length field is zero, out of range, or inconsistent.
247    InvalidLength,
248    /// The header uses fields this minimal parser does not support.
249    UnsupportedHeader,
250}
251
252impl std::fmt::Display for RadiotapError {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        match self {
255            Self::TooShort => write!(f, "radiotap packet is too short"),
256            Self::InvalidLength => write!(f, "radiotap length is invalid"),
257            Self::UnsupportedHeader => write!(f, "unsupported radiotap TX header"),
258        }
259    }
260}
261
262impl std::error::Error for RadiotapError {}
263
264/// Build an HT or VHT radiotap header from compact OpenIPC TX parameters.
265pub fn build_radiotap_header(params: TxRadioParams) -> Vec<u8> {
266    if params.vht {
267        build_vht_radiotap_header(params)
268    } else {
269        build_ht_radiotap_header(params)
270    }
271}
272
273/// Build a radiotap header for a parsed or requested stream TX mode.
274pub fn build_stream_radiotap(mode: TxMode) -> Vec<u8> {
275    match mode.kind {
276        TxModeKind::Legacy => build_legacy_radiotap_header(mode),
277        TxModeKind::Ht => build_ht_radiotap_header(TxRadioParams {
278            mcs_index: mode.ht_mcs,
279            bandwidth: mode.bandwidth,
280            short_gi: mode.short_gi,
281            stbc: u8::from(mode.stbc),
282            ldpc: mode.ldpc,
283            vht: false,
284            ..TxRadioParams::default()
285        }),
286        TxModeKind::Vht => build_vht_radiotap_header(TxRadioParams {
287            mcs_index: mode.vht_mcs,
288            nss: mode.vht_nss,
289            bandwidth: mode.bandwidth,
290            short_gi: mode.short_gi,
291            stbc: u8::from(mode.stbc),
292            ldpc: mode.ldpc,
293            vht: true,
294            ..TxRadioParams::default()
295        }),
296    }
297}
298
299/// Build a stream radiotap header with a per-packet WiFi channel request.
300///
301/// Realtek drivers that support fast retuning treat the CHANNEL frequency as
302/// authoritative and retune before constructing the USB TX descriptor.
303pub fn build_stream_radiotap_on_channel(mode: TxMode, channel: u8) -> Option<Vec<u8>> {
304    let frequency = crate::wifi::channel_to_frequency(channel)?;
305    let (present, mut fields) = match mode.kind {
306        TxModeKind::Legacy => {
307            let mut fields = vec![mode.legacy_rate_500kbps, 0];
308            fields.extend_from_slice(&frequency.to_le_bytes());
309            fields.extend_from_slice(&0u16.to_le_bytes());
310            fields.extend_from_slice(&RADIOTAP_TX_FLAGS_NO_ACK.to_le_bytes());
311            (
312                RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_CHANNEL | RADIOTAP_PRESENT_TX_FLAGS,
313                fields,
314            )
315        }
316        TxModeKind::Ht => {
317            let params = TxRadioParams {
318                mcs_index: mode.ht_mcs,
319                bandwidth: mode.bandwidth,
320                short_gi: mode.short_gi,
321                stbc: u8::from(mode.stbc),
322                ldpc: mode.ldpc,
323                vht: false,
324                ..TxRadioParams::default()
325            };
326            let base = build_ht_radiotap_header(params);
327            let mut fields = Vec::with_capacity(9);
328            fields.extend_from_slice(&frequency.to_le_bytes());
329            fields.extend_from_slice(&0u16.to_le_bytes());
330            fields.extend_from_slice(&RADIOTAP_TX_FLAGS_NO_ACK.to_le_bytes());
331            fields.extend_from_slice(&base[10..13]);
332            (
333                RADIOTAP_PRESENT_CHANNEL | RADIOTAP_PRESENT_TX_FLAGS | RADIOTAP_PRESENT_MCS,
334                fields,
335            )
336        }
337        TxModeKind::Vht => {
338            let params = TxRadioParams {
339                mcs_index: mode.vht_mcs,
340                nss: mode.vht_nss,
341                bandwidth: mode.bandwidth,
342                short_gi: mode.short_gi,
343                stbc: u8::from(mode.stbc),
344                ldpc: mode.ldpc,
345                vht: true,
346                ..TxRadioParams::default()
347            };
348            let base = build_vht_radiotap_header(params);
349            let mut fields = Vec::with_capacity(18);
350            fields.extend_from_slice(&frequency.to_le_bytes());
351            fields.extend_from_slice(&0u16.to_le_bytes());
352            fields.extend_from_slice(&RADIOTAP_TX_FLAGS_NO_ACK.to_le_bytes());
353            fields.extend_from_slice(&base[10..22]);
354            (
355                RADIOTAP_PRESENT_CHANNEL | RADIOTAP_PRESENT_TX_FLAGS | RADIOTAP_PRESENT_VHT,
356                fields,
357            )
358        }
359    };
360    let length = 8usize.checked_add(fields.len())?;
361    let length = u16::try_from(length).ok()?;
362    let mut header = Vec::with_capacity(length as usize);
363    header.extend_from_slice(&[0, 0]);
364    header.extend_from_slice(&length.to_le_bytes());
365    header.extend_from_slice(&present.to_le_bytes());
366    header.append(&mut fields);
367    Some(header)
368}
369
370/// Build a legacy-rate radiotap header.
371pub fn build_legacy_radiotap_header(mode: TxMode) -> Vec<u8> {
372    vec![
373        0x00,
374        0x00,
375        RADIOTAP_LEGACY_LEN as u8,
376        0x00,
377        (RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) as u8,
378        ((RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) >> 8) as u8,
379        0x00,
380        0x00,
381        mode.legacy_rate_500kbps,
382        0x00,
383        (RADIOTAP_TX_FLAGS_NO_ACK & 0xff) as u8,
384        (RADIOTAP_TX_FLAGS_NO_ACK >> 8) as u8,
385        0x00,
386    ]
387}
388
389/// Build an HT/MCS radiotap header.
390pub fn build_ht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
391    let known = IEEE80211_RADIOTAP_MCS_HAVE_MCS
392        | IEEE80211_RADIOTAP_MCS_HAVE_BW
393        | IEEE80211_RADIOTAP_MCS_HAVE_GI
394        | IEEE80211_RADIOTAP_MCS_HAVE_STBC
395        | IEEE80211_RADIOTAP_MCS_HAVE_FEC;
396    let mut flags = params.bandwidth.ht_mcs_bits();
397    if params.short_gi {
398        flags |= IEEE80211_RADIOTAP_MCS_SGI;
399    }
400    if params.ldpc {
401        flags |= IEEE80211_RADIOTAP_MCS_FEC_LDPC;
402    }
403    flags |= (params.stbc & 0x03) << IEEE80211_RADIOTAP_MCS_STBC_SHIFT;
404
405    vec![
406        0x00,
407        0x00,
408        RADIOTAP_HT_LEN as u8,
409        0x00,
410        0x00,
411        0x80,
412        0x08,
413        0x00,
414        0x08,
415        0x00,
416        known,
417        flags,
418        params.mcs_index.min(31),
419    ]
420}
421
422/// Build a VHT radiotap header.
423pub fn build_vht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
424    let mut flags = 0u8;
425    if params.stbc != 0 {
426        flags |= IEEE80211_RADIOTAP_VHT_FLAG_STBC;
427    }
428    if params.short_gi {
429        flags |= IEEE80211_RADIOTAP_VHT_FLAG_SGI;
430    }
431    let nss = params.nss.clamp(1, 4);
432    let mcs = params.mcs_index.min(9);
433    let mcs_nss0 = (mcs << 4) | nss;
434    let coding = if params.ldpc {
435        IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0
436    } else {
437        0
438    };
439
440    vec![
441        0x00,
442        0x00,
443        RADIOTAP_VHT_LEN as u8,
444        0x00,
445        0x00,
446        0x80,
447        0x20,
448        0x00,
449        0x08,
450        0x00,
451        0x45,
452        0x00,
453        flags,
454        params.bandwidth.vht_bits(),
455        mcs_nss0,
456        0x00,
457        0x00,
458        0x00,
459        coding,
460        0x00,
461        0x00,
462        0x00,
463    ]
464}
465
466/// Return the radiotap header length from a radiotap packet.
467pub fn radiotap_len(packet: &[u8]) -> Result<usize, RadiotapError> {
468    if packet.len() < 4 {
469        return Err(RadiotapError::TooShort);
470    }
471    let len = u16::from_le_bytes([packet[2], packet[3]]) as usize;
472    if len == 0 || len >= packet.len() {
473        return Err(RadiotapError::InvalidLength);
474    }
475    Ok(len)
476}
477
478/// Parse a human-readable TX mode such as `6M`, `MCS0/SGI`, or `VHT1SS_MCS3/80`.
479pub fn parse_tx_mode_str(spec: &str) -> TxMode {
480    parse_tx_mode_str_impl(spec, false).unwrap_or_default()
481}
482
483/// Strictly parse a human-readable TX mode.
484///
485/// Unlike [`parse_tx_mode_str`], this returns `None` for an empty specification,
486/// an unknown rate, or an unsupported modifier. It is intended for user-facing
487/// configuration where silently falling back to 1 Mbps would hide a typo.
488pub fn try_parse_tx_mode_str(spec: &str) -> Option<TxMode> {
489    parse_tx_mode_str_impl(spec, true)
490}
491
492fn parse_tx_mode_str_impl(spec: &str, reject_unknown_modifier: bool) -> Option<TxMode> {
493    let trimmed = spec
494        .chars()
495        .filter(|ch| !ch.is_whitespace())
496        .flat_map(char::to_uppercase)
497        .collect::<String>();
498    if trimmed.is_empty() {
499        return None;
500    }
501
502    let mut tokens = trimmed.split('/');
503    let rate_token = tokens.next()?;
504    let mut mode = parse_tx_rate_token(rate_token)?;
505
506    for token in tokens {
507        match token {
508            "SGI" => mode.short_gi = true,
509            "LDPC" => mode.ldpc = true,
510            "STBC" => mode.stbc = true,
511            "20" => mode.bandwidth = ChannelBandwidth::Mhz20,
512            "40" => mode.bandwidth = ChannelBandwidth::Mhz40,
513            "80" => mode.bandwidth = ChannelBandwidth::Mhz80,
514            "160" => mode.bandwidth = ChannelBandwidth::Mhz160,
515            _ if reject_unknown_modifier => return None,
516            _ => {}
517        }
518    }
519    Some(mode)
520}
521
522/// Parse supported radiotap TX fields into a `TxMode`.
523pub fn parse_radiotap_tx_mode(packet: &[u8]) -> Result<Option<TxMode>, RadiotapError> {
524    Ok(parse_radiotap_tx_metadata(packet)?.mode)
525}
526
527/// Parse the radiotap CHANNEL field into a supported WiFi channel number.
528pub fn parse_radiotap_tx_channel(packet: &[u8]) -> Result<Option<u8>, RadiotapError> {
529    Ok(parse_radiotap_tx_metadata(packet)?
530        .frequency_mhz
531        .and_then(crate::wifi::frequency_to_channel))
532}
533
534/// Parse the supported RATE, CHANNEL, TX_FLAGS, MCS, and VHT fields.
535pub fn parse_radiotap_tx_metadata(packet: &[u8]) -> Result<RadiotapTxMetadata, RadiotapError> {
536    let len = radiotap_len(packet)?;
537    if len < 8 || packet.len() < len {
538        return Err(RadiotapError::InvalidLength);
539    }
540    let present = u32::from_le_bytes(packet[4..8].try_into().expect("present word is in range"));
541    if present & (1 << 31) != 0 {
542        return Err(RadiotapError::UnsupportedHeader);
543    }
544    let supported = RADIOTAP_PRESENT_RATE
545        | RADIOTAP_PRESENT_CHANNEL
546        | RADIOTAP_PRESENT_TX_FLAGS
547        | RADIOTAP_PRESENT_MCS
548        | RADIOTAP_PRESENT_VHT;
549    if present & !supported != 0 {
550        return Err(RadiotapError::UnsupportedHeader);
551    }
552
553    let mut offset = 8usize;
554    let mut mode = None;
555    let mut frequency_mhz = None;
556
557    if present & RADIOTAP_PRESENT_RATE != 0 {
558        require_field(packet, len, offset, 1)?;
559        mode = Some(TxMode::legacy(packet[offset]));
560        offset += 1;
561    }
562
563    if present & RADIOTAP_PRESENT_CHANNEL != 0 {
564        offset = align(offset, 2);
565        require_field(packet, len, offset, 4)?;
566        frequency_mhz = Some(u16::from_le_bytes([packet[offset], packet[offset + 1]]));
567        offset += 4;
568    }
569
570    if present & RADIOTAP_PRESENT_TX_FLAGS != 0 {
571        offset = align(offset, 2);
572        require_field(packet, len, offset, 2)?;
573        offset += 2;
574    }
575
576    if present & RADIOTAP_PRESENT_MCS != 0 {
577        require_field(packet, len, offset, 3)?;
578        let known = packet[offset];
579        let flags = packet[offset + 1];
580        let mcs = packet[offset + 2];
581        let mut ht = TxMode::ht(if known & IEEE80211_RADIOTAP_MCS_HAVE_MCS != 0 {
582            mcs.min(31)
583        } else {
584            0
585        });
586        ht.bandwidth = if known & IEEE80211_RADIOTAP_MCS_HAVE_BW != 0 && flags & 0x03 == 1 {
587            ChannelBandwidth::Mhz40
588        } else {
589            ChannelBandwidth::Mhz20
590        };
591        ht.short_gi =
592            known & IEEE80211_RADIOTAP_MCS_HAVE_GI != 0 && flags & IEEE80211_RADIOTAP_MCS_SGI != 0;
593        ht.ldpc = known & IEEE80211_RADIOTAP_MCS_HAVE_FEC != 0
594            && flags & IEEE80211_RADIOTAP_MCS_FEC_LDPC != 0;
595        ht.stbc = known & IEEE80211_RADIOTAP_MCS_HAVE_STBC != 0
596            && ((flags >> IEEE80211_RADIOTAP_MCS_STBC_SHIFT) & 0x03) != 0;
597        mode = Some(ht);
598        offset += 3;
599    }
600
601    if present & RADIOTAP_PRESENT_VHT != 0 {
602        offset = align(offset, 2);
603        require_field(packet, len, offset, 12)?;
604        let known = u16::from_le_bytes([packet[offset], packet[offset + 1]]);
605        let flags = packet[offset + 2];
606        let bandwidth = match packet[offset + 3] & 0x1f {
607            1..=3 => ChannelBandwidth::Mhz40,
608            4..=10 => ChannelBandwidth::Mhz80,
609            11..=31 => ChannelBandwidth::Mhz160,
610            _ => ChannelBandwidth::Mhz20,
611        };
612        let mcs_nss = packet[offset + 4];
613        let mut vht = TxMode::vht((mcs_nss & 0x0f).clamp(1, 4), ((mcs_nss >> 4) & 0x0f).min(9));
614        if known & (1 << 6) != 0 {
615            vht.bandwidth = bandwidth;
616        }
617        vht.short_gi = known & (1 << 2) != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_SGI != 0;
618        vht.stbc = known & 1 != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_STBC != 0;
619        vht.ldpc = packet[offset + 8] & IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0 != 0;
620        mode = Some(vht);
621    }
622
623    Ok(RadiotapTxMetadata {
624        mode,
625        frequency_mhz,
626    })
627}
628
629/// Parse supported radiotap TX fields into compact adaptive-link TX info.
630pub fn parse_radiotap_tx_info(packet: &[u8]) -> Result<RadiotapTxInfo, RadiotapError> {
631    match parse_radiotap_tx_mode(packet)? {
632        Some(mode) => Ok(RadiotapTxInfo {
633            vht: mode.kind == TxModeKind::Vht,
634            mcs_index: match mode.kind {
635                TxModeKind::Legacy | TxModeKind::Ht => mode.ht_mcs,
636                TxModeKind::Vht => mode.vht_mcs,
637            },
638            nss: mode.vht_nss,
639            bandwidth: mode.bandwidth,
640            short_gi: mode.short_gi,
641            stbc: u8::from(mode.stbc),
642            ldpc: mode.ldpc,
643        }),
644        None => Err(RadiotapError::UnsupportedHeader),
645    }
646}
647
648fn parse_tx_rate_token(token: &str) -> Option<TxMode> {
649    match token {
650        "1M" => Some(TxMode::legacy(2)),
651        "2M" => Some(TxMode::legacy(4)),
652        "5.5M" | "5_5M" | "5M" => Some(TxMode::legacy(11)),
653        "6M" => Some(TxMode::legacy(12)),
654        "9M" => Some(TxMode::legacy(18)),
655        "11M" => Some(TxMode::legacy(22)),
656        "12M" => Some(TxMode::legacy(24)),
657        "18M" => Some(TxMode::legacy(36)),
658        "24M" => Some(TxMode::legacy(48)),
659        "36M" => Some(TxMode::legacy(72)),
660        "48M" => Some(TxMode::legacy(96)),
661        "54M" => Some(TxMode::legacy(108)),
662        _ => {
663            if let Some(raw) = token.strip_prefix("MCS") {
664                return raw
665                    .parse::<u8>()
666                    .ok()
667                    .filter(|mcs| *mcs <= 31)
668                    .map(TxMode::ht);
669            }
670            if let Some(raw) = token.strip_prefix("VHT") {
671                let (nss_raw, mcs_raw) = raw.split_once("SS_MCS")?;
672                let nss = nss_raw.parse::<u8>().ok()?;
673                let mcs = mcs_raw.parse::<u8>().ok()?;
674                if (1..=4).contains(&nss) && mcs <= 9 {
675                    return Some(TxMode::vht(nss, mcs));
676                }
677            }
678            None
679        }
680    }
681}
682
683const fn align(offset: usize, alignment: usize) -> usize {
684    (offset + alignment - 1) & !(alignment - 1)
685}
686
687fn require_field(
688    packet: &[u8],
689    radiotap_len: usize,
690    offset: usize,
691    len: usize,
692) -> Result<(), RadiotapError> {
693    if offset + len <= radiotap_len && offset + len <= packet.len() {
694        Ok(())
695    } else {
696        Err(RadiotapError::InvalidLength)
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn ht_header_roundtrips_tx_info() {
706        let params = TxRadioParams {
707            mcs_index: 3,
708            bandwidth: ChannelBandwidth::Mhz40,
709            short_gi: true,
710            stbc: 1,
711            ldpc: true,
712            ..TxRadioParams::default()
713        };
714        let mut packet = build_radiotap_header(params);
715        packet.extend_from_slice(&[0u8; 24]);
716        let parsed = parse_radiotap_tx_info(&packet).unwrap();
717        assert_eq!(parsed.mcs_index, 3);
718        assert_eq!(parsed.bandwidth, ChannelBandwidth::Mhz40);
719        assert!(parsed.short_gi);
720        assert!(parsed.ldpc);
721        assert_eq!(parsed.stbc, 1);
722    }
723
724    #[test]
725    fn parses_devourer_tx_mode_strings() {
726        let mode = parse_tx_mode_str("vht2ss_mcs5/80/sgi/ldpc/stbc");
727        assert_eq!(mode.kind, TxModeKind::Vht);
728        assert_eq!(mode.vht_nss, 2);
729        assert_eq!(mode.vht_mcs, 5);
730        assert_eq!(mode.bandwidth, ChannelBandwidth::Mhz80);
731        assert!(mode.short_gi);
732        assert!(mode.ldpc);
733        assert!(mode.stbc);
734    }
735
736    #[test]
737    fn strict_tx_mode_parser_rejects_typos() {
738        assert!(try_parse_tx_mode_str("MCS4/SGI").is_some());
739        assert!(try_parse_tx_mode_str("MCS4/FAST").is_none());
740        assert!(try_parse_tx_mode_str("MCS99").is_none());
741        assert!(try_parse_tx_mode_str("").is_none());
742        assert_eq!(parse_tx_mode_str("not-a-rate"), TxMode::default());
743    }
744
745    #[test]
746    fn parses_devourer_cck_rate_names() {
747        for (spec, rate) in [
748            ("1M", 2),
749            ("2M", 4),
750            ("5.5M", 11),
751            ("5_5M", 11),
752            ("11M", 22),
753        ] {
754            let mode = parse_tx_mode_str(spec);
755            assert_eq!(mode.kind, TxModeKind::Legacy);
756            assert_eq!(mode.legacy_rate_500kbps, rate);
757        }
758    }
759
760    #[test]
761    fn legacy_stream_radiotap_carries_rate_and_tx_flags() {
762        let mode = TxMode::legacy(12);
763        let mut packet = build_stream_radiotap(mode);
764        packet.extend_from_slice(&[0u8; 24]);
765        let parsed = parse_radiotap_tx_mode(&packet).unwrap().unwrap();
766        assert_eq!(parsed.kind, TxModeKind::Legacy);
767        assert_eq!(parsed.legacy_rate_500kbps, 12);
768    }
769
770    #[test]
771    fn channel_headers_roundtrip_for_every_rate_family() {
772        for mode in [TxMode::legacy_6m(), TxMode::ht(4), TxMode::vht(1, 7)] {
773            let mut packet = build_stream_radiotap_on_channel(mode, 36).unwrap();
774            packet.extend_from_slice(&[0u8; 24]);
775            let metadata = parse_radiotap_tx_metadata(&packet).unwrap();
776            assert_eq!(metadata.mode, Some(mode));
777            assert_eq!(metadata.frequency_mhz, Some(5180));
778            assert_eq!(parse_radiotap_tx_channel(&packet).unwrap(), Some(36));
779        }
780    }
781
782    #[test]
783    fn ignores_out_of_band_channel_frequency_without_losing_rate() {
784        let mut packet = build_stream_radiotap_on_channel(TxMode::ht(2), 36).unwrap();
785        packet[8..10].copy_from_slice(&4_900u16.to_le_bytes());
786        packet.extend_from_slice(&[0u8; 24]);
787        assert_eq!(parse_radiotap_tx_channel(&packet).unwrap(), None);
788        assert_eq!(
789            parse_radiotap_tx_mode(&packet).unwrap(),
790            Some(TxMode::ht(2))
791        );
792    }
793}