Import a suggested guide for regulatory db.txt.
[platform/upstream/crda.git] / dbparse.py
1 #!/usr/bin/env python
2
3 import sys, math
4
5 flag_definitions = {
6     'NO-CCK':           1<<0,
7     'NO-OFDM':          1<<1,
8     'NO-INDOOR':        1<<2,
9     'NO-OUTDOOR':       1<<3,
10     'DFS':              1<<4,
11     'PTP-ONLY':         1<<5,
12     'PTMP-ONLY':        1<<6,
13     'PASSIVE-SCAN':     1<<7,
14     'NO-IBSS':          1<<8,
15     # hole at bit 9
16     'NO-HT40':          1<<10,
17     'EDGE-POWER-1':     1<<11,
18     'EDGE-POWER-2':     2<<11,
19     'EDGE-POWER-3':     3<<11,
20 }
21
22 class FreqBand(object):
23     def __init__(self, start, end, bw, comments=None):
24         self.start = start
25         self.end = end
26         self.maxbw = bw
27         self.comments = comments or []
28
29     def __cmp__(self, other):
30         s = self
31         o = other
32         if not isinstance(o, FreqBand):
33             return False
34         return cmp((s.start, s.end, s.maxbw), (o.start, o.end, o.maxbw))
35
36     def __hash__(self):
37         s = self
38         return hash((s.start, s.end, s.maxbw))
39
40     def __str__(self):
41         return '<FreqBand %.3f - %.3f @ %.3f>' % (
42                   self.start, self.end, self.maxbw)
43
44 class PowerRestriction(object):
45     def __init__(self, max_ant_gain, max_eirp, comments = None):
46         self.max_ant_gain = max_ant_gain
47         self.max_eirp = max_eirp
48         self.comments = comments or []
49
50     def __cmp__(self, other):
51         s = self
52         o = other
53         if not isinstance(o, PowerRestriction):
54             return False
55         return cmp((s.max_ant_gain, s.max_eirp),
56                    (o.max_ant_gain, o.max_eirp))
57
58     def __str__(self):
59         return '<PowerRestriction ...>'
60
61     def __hash__(self):
62         s = self
63         return hash((s.max_ant_gain, s.max_eirp))
64
65 class FlagError(Exception):
66     def __init__(self, flag):
67         self.flag = flag
68
69 class DuplicateEdgePowerError(Exception):
70     pass
71
72 class Permission(object):
73     def __init__(self, freqband, power, flags):
74         assert isinstance(freqband, FreqBand)
75         assert isinstance(power, PowerRestriction)
76         self.freqband = freqband
77         self.power = power
78         self.flags = 0
79         f = [e for e in flags if e.startswith('EDGE-POWER-')]
80         if len(f) > 1:
81             raise DuplicateEdgePowerError()
82         for flag in flags:
83             if not flag in flag_definitions:
84                 raise FlagError(flag)
85             self.flags |= flag_definitions[flag]
86         self.textflags = flags
87
88     def _as_tuple(self):
89         return (self.freqband, self.power, self.flags)
90
91     def __cmp__(self, other):
92         if not isinstance(other, Permission):
93             return False
94         return cmp(self._as_tuple(), other._as_tuple())
95
96     def __hash__(self):
97         return hash(self._as_tuple())
98
99 class Country(object):
100     def __init__(self, permissions=None, comments=None):
101         self._permissions = permissions or []
102         self.comments = comments or []
103
104     def add(self, perm):
105         assert isinstance(perm, Permission)
106         self._permissions.append(perm)
107         self._permissions.sort()
108
109     def __contains__(self, perm):
110         assert isinstance(perm, Permission)
111         return perm in self._permissions
112
113     def __str__(self):
114         r = ['(%s, %s)' % (str(b), str(p)) for b, p in self._permissions]
115         return '<Country (%s)>' % (', '.join(r))
116
117     def _get_permissions_tuple(self):
118         return tuple(self._permissions)
119     permissions = property(_get_permissions_tuple)
120
121 class SyntaxError(Exception):
122     pass
123
124 class DBParser(object):
125     def __init__(self, warn=None):
126         self._warn_callout = warn or sys.stderr.write
127
128     def _syntax_error(self, txt=None):
129         txt = txt and ' (%s)' % txt or ''
130         raise SyntaxError("Syntax error in line %d%s" % (self._lineno, txt))
131
132     def _warn(self, txt):
133         self._warn_callout("Warning (line %d): %s\n" % (self._lineno, txt))
134
135     def _parse_band_def(self, bname, banddef, dupwarn=True):
136         try:
137             freqs, bw = banddef.split('@')
138             bw = float(bw)
139         except ValueError:
140             bw = 20.0
141
142         try:
143             start, end = freqs.split('-')
144             start = float(start)
145             end = float(end)
146         except ValueError:
147             self._syntax_error("band must have frequency range")
148
149         b = FreqBand(start, end, bw, comments=self._comments)
150         self._comments = []
151         self._banddup[bname] = bname
152         if b in self._bandrev:
153             if dupwarn:
154                 self._warn('Duplicate band definition ("%s" and "%s")' % (
155                               bname, self._bandrev[b]))
156             self._banddup[bname] = self._bandrev[b]
157         self._bands[bname] = b
158         self._bandrev[b] = bname
159         self._bandline[bname] = self._lineno
160
161     def _parse_band(self, line):
162         try:
163             bname, line = line.split(':', 1)
164             if not bname:
165                 self._syntax_error("'band' keyword must be followed by name")
166         except ValueError:
167             self._syntax_error("band name must be followed by colon")
168
169         if bname in flag_definitions:
170             self._syntax_error("Invalid band name")
171
172         self._parse_band_def(bname, line)
173
174     def _parse_power(self, line):
175         try:
176             pname, line = line.split(':', 1)
177             if not pname:
178                 self._syntax_error("'power' keyword must be followed by name")
179         except ValueError:
180             self._syntax_error("power name must be followed by colon")
181
182         if pname in flag_definitions:
183             self._syntax_error("Invalid power name")
184
185         self._parse_power_def(pname, line)
186
187     def _parse_power_def(self, pname, line, dupwarn=True):
188         try:
189             (max_ant_gain,
190              max_eirp) = line.split(',')
191             if max_ant_gain == 'N/A':
192                 max_ant_gain = '0'
193             if max_eirp == 'N/A':
194                 max_eirp = '0'
195             max_ant_gain = float(max_ant_gain)
196             def conv_pwr(pwr):
197                 if pwr.endswith('mW'):
198                     pwr = float(pwr[:-2])
199                     return 10.0 * math.log10(pwr)
200                 else:
201                     return float(pwr)
202             max_eirp = conv_pwr(max_eirp)
203         except ValueError:
204             self._syntax_error("invalid power data")
205
206         p = PowerRestriction(max_ant_gain, max_eirp,
207                              comments=self._comments)
208         self._comments = []
209         self._powerdup[pname] = pname
210         if p in self._powerrev:
211             if dupwarn:
212                 self._warn('Duplicate power definition ("%s" and "%s")' % (
213                               pname, self._powerrev[p]))
214             self._powerdup[pname] = self._powerrev[p]
215         self._power[pname] = p
216         self._powerrev[p] = pname
217         self._powerline[pname] = self._lineno
218
219     def _parse_country(self, line):
220         try:
221             cname, line = line.split(':', 1)
222             if not cname:
223                 self._syntax_error("'country' keyword must be followed by name")
224             if line:
225                 self._syntax_error("extra data at end of country line")
226         except ValueError:
227             self._syntax_error("country name must be followed by colon")
228
229         cnames = cname.split(',')
230
231         self._current_countries = {}
232         for cname in cnames:
233             if len(cname) != 2:
234                 self._warn("country '%s' not alpha2" % cname)
235             if not cname in self._countries:
236                 self._countries[cname] = Country(comments=self._comments)
237             self._current_countries[cname] = self._countries[cname]
238         self._comments = []
239
240     def _parse_country_item(self, line):
241         if line[0] == '(':
242             try:
243                 band, line = line[1:].split('),', 1)
244                 bname = 'UNNAMED %d' % self._lineno
245                 self._parse_band_def(bname, band, dupwarn=False)
246             except:
247                 self._syntax_error("Badly parenthesised band definition")
248         else:
249             try:
250                 bname, line = line.split(',', 1)
251                 if not bname:
252                     self._syntax_error("country definition must have band")
253                 if not line:
254                     self._syntax_error("country definition must have power")
255             except ValueError:
256                 self._syntax_error("country definition must have band and power")
257
258         if line[0] == '(':
259             items = line.split('),', 1)
260             if len(items) == 1:
261                 pname = items[0]
262                 line = ''
263                 if not pname[-1] == ')':
264                     self._syntax_error("Badly parenthesised power definition")
265                 pname = pname[:-1]
266                 flags = []
267             else:
268                 pname = items[0]
269                 flags = items[1].split(',')
270             power = pname[1:]
271             pname = 'UNNAMED %d' % self._lineno
272             self._parse_power_def(pname, power, dupwarn=False)
273         else:
274             line = line.split(',')
275             pname = line[0]
276             flags = line[1:]
277
278         if not bname in self._bands:
279             self._syntax_error("band does not exist")
280         if not pname in self._power:
281             self._syntax_error("power does not exist")
282         self._bands_used[bname] = True
283         self._power_used[pname] = True
284         # de-duplicate so binary database is more compact
285         bname = self._banddup[bname]
286         pname = self._powerdup[pname]
287         b = self._bands[bname]
288         p = self._power[pname]
289         try:
290             perm = Permission(b, p, flags)
291         except FlagError, e:
292             self._syntax_error("Invalid flag '%s'" % e.flag)
293         except DuplicateEdgePowerError:
294             self._syntax_error("More than one edge power value given!")
295         for cname, c in self._current_countries.iteritems():
296             if perm in c:
297                 self._warn('Rule "%s, %s" added to "%s" twice' % (
298                               bname, pname, cname))
299             else:
300                 c.add(perm)
301
302     def parse(self, f):
303         self._current_countries = None
304         self._bands = {}
305         self._power = {}
306         self._countries = {}
307         self._bands_used = {}
308         self._power_used = {}
309         self._bandrev = {}
310         self._powerrev = {}
311         self._banddup = {}
312         self._powerdup = {}
313         self._bandline = {}
314         self._powerline = {}
315
316         self._comments = []
317
318         self._lineno = 0
319         for line in f:
320             self._lineno += 1
321             line = line.strip()
322             if line[0:1] == '#':
323                 self._comments.append(line[1:].strip())
324             line = line.replace(' ', '').replace('\t', '')
325             if not line:
326                 self._comments = []
327             line = line.split('#')[0]
328             if not line:
329                 continue
330             if line[0:4] == 'band':
331                 self._parse_band(line[4:])
332                 self._current_countries = None
333                 self._comments = []
334             elif line[0:5] == 'power':
335                 self._parse_power(line[5:])
336                 self._current_countries = None
337                 self._comments = []
338             elif line[0:7] == 'country':
339                 self._parse_country(line[7:])
340                 self._comments = []
341             elif self._current_countries is not None:
342                 self._parse_country_item(line)
343                 self._comments = []
344             else:
345                 self._syntax_error("Expected band, power or country definition")
346
347         countries = self._countries
348         bands = {}
349         for k, v in self._bands.iteritems():
350             if k in self._bands_used:
351                 bands[self._banddup[k]] = v
352                 continue
353             # we de-duplicated, but don't warn again about the dupes
354             if self._banddup[k] == k:
355                 self._lineno = self._bandline[k]
356                 self._warn('Unused band definition "%s"' % k)
357         power = {}
358         for k, v in self._power.iteritems():
359             if k in self._power_used:
360                 power[self._powerdup[k]] = v
361                 continue
362             # we de-duplicated, but don't warn again about the dupes
363             if self._powerdup[k] == k:
364                 self._lineno = self._powerline[k]
365                 self._warn('Unused power definition "%s"' % k)
366         return countries