wireless_regdb/
lib.rs

1//!
2//! Read a wireless regdb, and convert it to a binary firmware file.
3//!
4//! Inspired by the python code from
5//! [kernel.googlesource.com/pub/scm/linux/kernel/git/sforshee/wireless-regdb](https://kernel.googlesource.com/pub/scm/linux/kernel/git/sforshee/wireless-regdb/+/refs/heads/master/)
6//!
7//! # Example
8//! ```
9//! let lexer = wireless_regdb::lexer::TokType::parse("db.txt").unwrap();
10//! let db = wireless_regdb::RegDB::from_lexer(lexer).unwrap();
11//! let bin_db = wireless_regdb::Binary::from_regdb(&db).unwrap();
12//! bin_db.write_file("regulatory.db").unwrap();
13//! ```
14//!
15
16use std::collections::HashMap;
17
18use std::iter::Peekable;
19use std::slice::Iter;
20
21pub(crate) mod binary;
22pub mod lexer;
23
24pub use crate::binary::Binary;
25pub use crate::lexer::TokType;
26
27use anyhow::{anyhow, bail, Result};
28use std::convert::TryFrom;
29
30/// The regulatory database with wmmrules and countries
31#[derive(Debug, Default, PartialEq, Eq)]
32pub struct RegDB {
33    /// Regulatory rules for regions.
34    ///
35    /// *ETSI* for example
36    pub wmm_rules: HashMap<String, WmmRule>,
37    /// Contries in the database. The key usese the 2 digit alpha representation
38    pub countries: HashMap<String, Country>,
39}
40
41impl RegDB {
42    /// Create a regulatory database from a TokenStream(`Vec<lexer::TokType>`)
43    ///
44    /// # Arguments
45    ///
46    /// * `lexer` - Vector of tokens representing the database txt format
47    pub fn from_lexer(lexer: Vec<TokType>) -> Result<Self> {
48        let mut ret = Self::default();
49
50        let mut it = lexer.iter().peekable();
51
52        while let Some(&v) = it.peek() {
53            match &v {
54                TokType::String(v) if v == "wmmrule" => {
55                    it.next();
56                    let name = it
57                        .next()
58                        .map(|x| x.get_string())
59                        .ok_or_else(|| anyhow!("wmmrule does not contain a name"))??;
60                    match it.next() {
61                        Some(TokType::Colon) => (),
62                        v => bail!("not a vailid wmmrule {}: {:?}", name, v),
63                    }
64                    ret.wmm_rules.insert(name, WmmRule::from_lexer(&mut it)?);
65                }
66                TokType::String(v) if v == "country" => {
67                    it.next();
68                    // special case for country 00
69                    let name = match it.next() {
70                        Some(TokType::String(v)) => v.to_string(),
71                        Some(TokType::Int(v)) if *v == 0 => String::from("00"),
72                        _ => bail!("country does not contain a name"),
73                    };
74                    match it.next() {
75                        Some(TokType::Colon) => (),
76                        v => bail!("not a vailid country {}: {:?}", name, v),
77                    }
78
79                    let dfs = match it.peek() {
80                        Some(TokType::String(v)) => {
81                            it.next();
82                            Some(v.to_string())
83                        }
84                        _ => None,
85                    };
86
87                    ret.countries
88                        .insert(name.clone(), Country::from_lexer(&name, &mut it, dfs)?);
89                }
90                v => bail!("not expected for a regulatory db: {:?}", v),
91            }
92        }
93
94        Ok(ret)
95    }
96}
97
98const WMMRULE_ITEMS: [&str; 8] = [
99    "vo_c", "vi_c", "be_c", "bk_c", "vo_ap", "vi_ap", "be_ap", "bk_ap",
100];
101
102/// Regulatory rules for a region.
103#[derive(Debug, Default, PartialEq, Eq)]
104#[allow(non_snake_case)]
105pub struct WmmRule {
106    pub vo_c: WmmRuleItem,
107    pub vi_c: WmmRuleItem,
108    pub be_c: WmmRuleItem,
109    pub bk_c: WmmRuleItem,
110    pub vo_ap: WmmRuleItem,
111    pub vi_ap: WmmRuleItem,
112    pub be_ap: WmmRuleItem,
113    pub bk_ap: WmmRuleItem,
114}
115
116impl WmmRule {
117    fn from_lexer(it: &mut Peekable<Iter<TokType>>) -> Result<Self> {
118        let mut result = Self::default();
119        let mut set = Vec::new();
120
121        while let Some(&t) = it.peek() {
122            match &t {
123                TokType::String(x) if x == "country" || x == "wmmrule" => break,
124                TokType::String(_) => {
125                    let name = it.next().unwrap().get_string().unwrap(); // checked with peek
126                    set.push(name.clone());
127                    match it.next() {
128                        Some(TokType::Colon) => (),
129                        v => bail!("not a vailid wmmrule item {}: {:?}", name, v),
130                    }
131                    let ret = match name.as_str() {
132                        "vo_c" => &mut result.vo_c,
133                        "vi_c" => &mut result.vi_c,
134                        "be_c" => &mut result.be_c,
135                        "bk_c" => &mut result.bk_c,
136                        "vo_ap" => &mut result.vo_ap,
137                        "vi_ap" => &mut result.vi_ap,
138                        "be_ap" => &mut result.be_ap,
139                        "bk_ap" => &mut result.bk_ap,
140                        v => bail!("not a valid wwmrule item {}: {:?}", name, v),
141                    };
142
143                    let mut set_item = Vec::new();
144                    while let Some(&t) = it.peek() {
145                        match &t {
146                            TokType::String(_) => {
147                                let name = it.next().unwrap().get_string().unwrap(); // checked with peek
148                                set_item.push(name.clone());
149                                match it.next() {
150                                    Some(TokType::Equals) => (),
151                                    v => bail!("not a vailid wmmrule item {}: {:?}", name, v),
152                                }
153                                let ret = match name.as_str() {
154                                    "cw_min" => &mut ret.cw_min,
155                                    "cw_max" => &mut ret.cw_max,
156                                    "aifsn" => &mut ret.aifsn,
157                                    "cot" => &mut ret.cot,
158                                    v => bail!("not a valid wwmrule item {}: {:?}", name, v),
159                                };
160
161                                let value = it
162                                    .next()
163                                    .map(|x| x.get_int())
164                                    .ok_or_else(|| anyhow!("not a vailid wmmrule item"))??;
165                                *ret = value;
166                                match it.peek() {
167                                    Some(TokType::Comma) => {
168                                        it.next();
169                                    }
170                                    _ => break,
171                                }
172                            }
173                            v => bail!("not expected for a wmmrule item: {:?}", v),
174                        }
175                    }
176
177                    for x in &WMMRULEITEM_ITEMS {
178                        if !set_item.contains(&x.to_string()) {
179                            bail!("wmm rule item {} does not conain {}", name, x);
180                        }
181                    }
182                }
183                v => bail!("not expected for a wmmrule: {:?}", v),
184            }
185        }
186
187        for x in &WMMRULE_ITEMS {
188            if !set.contains(&x.to_string()) {
189                bail!("wmm rule does not conain {}", x);
190            }
191        }
192
193        Ok(result)
194    }
195}
196
197/// Item of a [`WmmRule`]
198///
199/// [`WmmRule`]: ./struct.WmmRule.html
200#[allow(non_snake_case)]
201#[derive(Debug, Default, PartialEq, Eq)]
202pub struct WmmRuleItem {
203    pub cw_min: usize,
204    pub cw_max: usize,
205    pub aifsn: usize,
206    pub cot: usize,
207}
208
209const WMMRULEITEM_ITEMS: [&str; 4] = ["cw_min", "cw_max", "aifsn", "cot"];
210
211/// Contry definiton in the Regulatory Database
212#[derive(Debug, PartialEq, Eq, Default)]
213pub struct Country {
214    pub frequencies: HashMap<(String, String), FrequencyBand>,
215    pub dfs: DfsRegion,
216}
217
218impl Country {
219    fn from_lexer(
220        name: &str,
221        it: &mut Peekable<Iter<TokType>>,
222        dfs: Option<String>,
223    ) -> Result<Self> {
224        let mut result = Self::default();
225        result.dfs = dfs
226            .map(|v| DfsRegion::try_from(v.as_str()).ok())
227            .flatten()
228            .unwrap_or(DfsRegion::None);
229
230        let conv_pwr = |p: f64, mw: bool| -> f64 {
231            if mw {
232                10.0f64 * (p.log10())
233            } else {
234                p
235            }
236        };
237
238        while let Some(&t) = it.peek() {
239            match t {
240                TokType::LParen => {
241                    it.next();
242                    let from = Self::get_freq(it.next())?;
243                    match it.next() {
244                        Some(TokType::Minus) => (),
245                        v => bail!("not a vailid country {}: {:?}", name, v),
246                    }
247                    let to = Self::get_freq(it.next())?;
248                    match it.next() {
249                        Some(TokType::At) => (),
250                        v => bail!("not a vailid country {}: {:?}", name, v),
251                    }
252                    // sanity check
253                    let freqs = Self::check_band(&from, &to)?;
254
255                    let size = Self::get_freq(it.next())?.parse()?;
256                    match it.next() {
257                        Some(TokType::RParen) => (),
258                        v => bail!("not a vailid country {}: {:?}", name, v),
259                    }
260                    match it.next() {
261                        Some(TokType::Comma) => (),
262                        v => bail!("not a vailid country {}: {:?}", name, v),
263                    }
264                    match it.next() {
265                        Some(TokType::LParen) => (),
266                        v => bail!("not a vailid country {}: {:?}", name, v),
267                    }
268
269                    let power = Self::get_freq(it.next())?.parse()?;
270                    let power_unit = match it.peek() {
271                        Some(TokType::String(x)) => {
272                            it.next();
273                            Some(x.to_string())
274                        }
275                        _ => None,
276                    };
277                    let power = conv_pwr(power, power_unit.is_some());
278
279                    match it.next() {
280                        Some(TokType::RParen) => (),
281                        v => bail!("not a vailid country {}: {:?}", name, v),
282                    }
283
284                    match it.peek() {
285                        Some(TokType::Comma) => (),
286                        _ => {
287                            result.frequencies.insert(
288                                (from, to),
289                                FrequencyBand::new(freqs, size, power, power_unit),
290                            );
291                            continue;
292                        }
293                    }
294
295                    let mut flags = Vec::new();
296                    let mut wmmrule = None;
297
298                    while let Some(TokType::Comma) = it.peek() {
299                        it.next();
300
301                        let name = it
302                            .next()
303                            .map(|x| x.get_string())
304                            .ok_or_else(|| anyhow!("wmmrule does not contain a name"))??;
305                        if let Some(TokType::Equals) = it.peek() {
306                            it.next();
307                            let rule = it
308                                .next()
309                                .map(|x| x.get_string())
310                                .ok_or_else(|| anyhow!("wmmrule does not contain a name"))??;
311                            assert_eq!(name, "wmmrule"); // TODO
312                            assert!(wmmrule.is_none()); // TODO
313                            wmmrule = Some(rule);
314                        //wmmrules.push(name);
315                        } else {
316                            flags.push(name);
317                        }
318                    }
319
320                    //let freqs = (from.parse()?, to.parse()?);
321                    result.frequencies.insert(
322                        (from, to),
323                        FrequencyBand {
324                            freqs,
325                            size,
326                            power,
327                            power_unit,
328                            flags: Flags(flags),
329                            wmmrule,
330                        },
331                    );
332                }
333                _ => break,
334            }
335        }
336
337        Ok(result)
338    }
339
340    fn get_freq(tok: Option<&TokType>) -> Result<String> {
341        match tok {
342            Some(TokType::Int(x)) => Ok(x.to_string()),
343            Some(TokType::String(x)) => Ok(x.to_string()),
344            _ => bail!("invalid country"),
345        }
346    }
347
348    fn check_band(from: &str, to: &str) -> Result<(f64, f64)> {
349        let from = from.parse::<f64>()?;
350        let to = to.parse::<f64>()?;
351        if to <= from {
352            bail!("freqency in wrong order {} < {}", to, from);
353        }
354
355        Ok((from, to))
356    }
357}
358
359/// Dfs region Definiton
360#[repr(u8)]
361// It can not be orderd, but Collection needs it
362#[derive(Debug, PartialEq, Eq, Copy, Clone, Ord, PartialOrd)]
363pub enum DfsRegion {
364    None = 0,
365    FCC = 1,
366    ETSI = 2,
367    JP = 3,
368}
369
370impl TryFrom<&str> for DfsRegion {
371    type Error = anyhow::Error;
372
373    fn try_from(value: &str) -> Result<Self, Self::Error> {
374        match value {
375            "DFS-FCC" => Ok(DfsRegion::FCC),
376            "DFS-ETSI" => Ok(DfsRegion::ETSI),
377            "DFS-JP" => Ok(DfsRegion::JP),
378            v => bail!("{} is not a dfs region", v),
379        }
380    }
381}
382
383impl Default for DfsRegion {
384    fn default() -> Self {
385        DfsRegion::None
386    }
387}
388
389/// Freqency entry
390#[derive(Debug)]
391pub struct FrequencyBand {
392    pub freqs: (f64, f64),
393    pub size: f64,
394    pub power: f64,
395    pub power_unit: Option<String>,
396    pub flags: Flags,            // TODO: rename?
397    pub wmmrule: Option<String>, // can only have one wwmrule?
398}
399
400impl FrequencyBand {
401    pub fn new(freqs: (f64, f64), size: f64, power: f64, power_unit: Option<String>) -> Self {
402        assert!(!freqs.0.is_nan());
403        assert!(!freqs.1.is_nan());
404        Self {
405            freqs,
406            size,
407            power,
408            power_unit,
409            flags: Flags(Vec::new()),
410            wmmrule: None,
411        }
412    }
413}
414
415impl std::hash::Hash for FrequencyBand {
416    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
417        ((self.freqs.0 * 1000f64) as usize).hash(state);
418        ((self.freqs.1 * 1000f64) as usize).hash(state);
419        ((self.power * 100f64) as usize).hash(state);
420        ((self.size * 1000f64) as usize).hash(state);
421        self.flags.to_u8().hash(state);
422    }
423}
424
425impl Ord for FrequencyBand {
426    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
427        match self.freqs.0.partial_cmp(&other.freqs.0) {
428            Some(std::cmp::Ordering::Equal) => match self.freqs.1.partial_cmp(&other.freqs.1) {
429                Some(v) if v == std::cmp::Ordering::Equal => {
430                    match self.size.partial_cmp(&other.size) {
431                        Some(std::cmp::Ordering::Equal) => {
432                            match self.power.partial_cmp(&other.power) {
433                                Some(std::cmp::Ordering::Equal) => {
434                                    match self.flags.to_u8().cmp(&other.flags.to_u8()) {
435                                        std::cmp::Ordering::Equal => {
436                                            self.wmmrule.cmp(&other.wmmrule)
437                                        }
438                                        v => v,
439                                    }
440                                }
441                                Some(v) => v,
442                                None => unreachable!(), // It never should be NAN, so panic if it would be
443                            }
444                        }
445                        Some(v) => v,
446                        None => unreachable!(),
447                    }
448                }
449                Some(v) => v,
450                None => unreachable!(),
451            },
452            Some(v) => v,
453            None => unreachable!(),
454        }
455    }
456}
457
458impl PartialOrd for FrequencyBand {
459    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
460        Some(self.cmp(other))
461    }
462}
463
464impl PartialEq for FrequencyBand {
465    fn eq(&self, other: &Self) -> bool {
466        self.freqs.0 == other.freqs.0
467            && self.freqs.1 == other.freqs.1
468            && self.size == other.size
469            && self.power == other.power
470            && self.flags.to_u8() == other.flags.to_u8()
471            && self.wmmrule == other.wmmrule
472    }
473}
474
475/// Flags for a Freqency to regulato special behavior
476#[derive(Debug, PartialEq, Eq, Default)]
477pub struct Flags(Vec<String>);
478
479impl Flags {
480    pub fn to_u8(&self) -> u8 {
481        let mut flags = 0;
482        for v in &self.0 {
483            match v.as_str() {
484                "NO-OFDM" => flags |= 1, //<< 0,
485                "NO-OUTDOOR" => flags |= 1 << 1,
486                "DFS" => flags |= 1 << 2,
487                "NO-IR" => flags |= 1 << 3,
488                "AUTO-BW" => flags |= 1 << 4,
489                _ => (),
490            }
491        }
492        flags
493    }
494}
495
496impl Ord for Flags {
497    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
498        self.to_u8().cmp(&other.to_u8())
499    }
500}
501
502impl PartialOrd for Flags {
503    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
504        Some(self.cmp(other))
505    }
506}
507
508impl Eq for FrequencyBand {}
509
510impl std::ops::Deref for Flags {
511    type Target = Vec<String>;
512
513    fn deref(&self) -> &Self::Target {
514        &self.0
515    }
516}
517
518#[cfg(test)]
519mod test {
520    use super::{DfsRegion, HashMap, RegDB, TokType};
521    #[test]
522    fn wmmrule() {
523        #[rustfmt::skip]
524		let wmmrule = r#"
525wmmrule ETSI:
526  vo_c: cw_min=3, cw_max=7, aifsn=2, cot=2
527  vi_c: cw_min=7, cw_max=15, aifsn=2, cot=4
528  be_c: cw_min=15, cw_max=1023, aifsn=3, cot=6
529  bk_c: cw_min=15, cw_max=1023, aifsn=7, cot=6
530  vo_ap: cw_min=3, cw_max=7, aifsn=1, cot=2
531  vi_ap: cw_min=7, cw_max=15, aifsn=1, cot=4
532  be_ap: cw_min=15, cw_max=63, aifsn=3, cot=6
533  bk_ap: cw_min=15, cw_max=1023, aifsn=7, cot=6
534        "#;
535
536        let wmmrule = TokType::parse_str(wmmrule).unwrap();
537
538        let db = RegDB::from_lexer(wmmrule).unwrap();
539
540        let mut should = super::WmmRule::default();
541        should.vo_c = super::WmmRuleItem {
542            cw_min: 3,
543            cw_max: 7,
544            aifsn: 2,
545            cot: 2,
546        };
547        should.vi_c = super::WmmRuleItem {
548            cw_min: 7,
549            cw_max: 15,
550            aifsn: 2,
551            cot: 4,
552        };
553        should.be_c = super::WmmRuleItem {
554            cw_min: 15,
555            cw_max: 1023,
556            aifsn: 3,
557            cot: 6,
558        };
559        should.bk_c = super::WmmRuleItem {
560            cw_min: 15,
561            cw_max: 1023,
562            aifsn: 7,
563            cot: 6,
564        };
565        should.vo_ap = super::WmmRuleItem {
566            cw_min: 3,
567            cw_max: 7,
568            aifsn: 1,
569            cot: 2,
570        };
571        should.vi_ap = super::WmmRuleItem {
572            cw_min: 7,
573            cw_max: 15,
574            aifsn: 1,
575            cot: 4,
576        };
577        should.be_ap = super::WmmRuleItem {
578            cw_min: 15,
579            cw_max: 63,
580            aifsn: 3,
581            cot: 6,
582        };
583        should.bk_ap = super::WmmRuleItem {
584            cw_min: 15,
585            cw_max: 1023,
586            aifsn: 7,
587            cot: 6,
588        };
589
590        let mut should_hm = HashMap::new();
591        should_hm.insert("ETSI".to_string(), should);
592        let should = RegDB {
593            wmm_rules: should_hm,
594            countries: HashMap::new(),
595        };
596
597        assert_eq!(db, should);
598    }
599
600    #[test]
601    fn invalid_wmmrule() {
602        #[rustfmt::skip]
603		let wmmrule = r#"
604wmmrule ETSI:
605  vo_c: cw_min=3, cw_max=7, aifsn=2, cot=2
606  vi_c: cw_min=7, cw_max=15, aifsn=2, cot=4
607  be_c: cw_min=15, cw_max=1023, aifsn=3, cot=6
608  bk_c: cw_min=15, cw_max=1023, aifsn=7, cot=6
609  vo_ap: cw_min=3, cw_max=7, aifsn=1, cot=2
610  vi_ap: cw_min=7, cw_max=15, aifsn=1, cot=4
611  be_ap: cw_min=15, cw_max=63, aifsn=3, cot=6
612        "#;
613
614        let wmmrule = TokType::parse_str(wmmrule).unwrap();
615
616        let db = RegDB::from_lexer(wmmrule);
617
618        assert!(db.is_err());
619    }
620
621    #[test]
622    fn invalid_wmmrule_item() {
623        #[rustfmt::skip]
624		let wmmrule = r#"
625wmmrule ETSI:
626  vo_c: cw_min=3, cw_max=7, aifsn=2, cot=2
627  vi_c: cw_min=7, cw_max=15, aifsn=2, cot=4
628  be_c: cw_min=15, cw_max=1023, aifsn=3, cot=6
629  bk_c: cw_min=15, cw_max=1023, aifsn=7, cot=6
630  vo_ap: cw_min=3, cw_max=7, aifsn=1, cot=2
631  vi_ap: cw_min=7, cw_max=15, aifsn=1, cot=4
632  be_ap: cw_min=15, cw_max=63, aifsn=3
633  bk_ap: cw_min=15, cw_max=1023, aifsn=7, cot=6
634        "#;
635
636        let wmmrule = TokType::parse_str(wmmrule).unwrap();
637
638        let db = RegDB::from_lexer(wmmrule);
639
640        assert!(db.is_err());
641    }
642
643    #[test]
644    fn parse_db() {
645        let db = include_str!("./tests/db_2.txt");
646
647        let lexer = TokType::parse_str(db).unwrap();
648        let db = RegDB::from_lexer(lexer).unwrap();
649
650        assert_eq!(db.countries.len(), 2);
651        assert_eq!(db.wmm_rules.len(), 1);
652
653        assert!(db.countries.get("AD").is_some());
654
655        assert_eq!(db.countries.get("AD").unwrap().dfs, DfsRegion::ETSI);
656        assert_eq!(db.countries.get("00").unwrap().dfs, DfsRegion::None);
657
658        let ad = db.countries.get("AD").unwrap();
659        let freq = ad
660            .frequencies
661            .get(&("5150".to_string(), "5250".to_string()))
662            .unwrap();
663        assert_eq!(freq.flags.len(), 2);
664        assert!(freq.wmmrule.is_some());
665        assert_eq!(freq.wmmrule.as_ref().unwrap(), "ETSI");
666    }
667
668    #[test]
669    fn write_db() {
670        let db = include_str!("./../db.txt");
671
672        let lexer = TokType::parse_str(db).unwrap();
673        let db = RegDB::from_lexer(lexer).unwrap();
674
675        let db = super::binary::Binary::from_regdb(&db).unwrap();
676
677        db.write_file("/dev/null").unwrap(); // TODO: /dev/null?
678    }
679}