comparison contrib/tools/csv_to_fd @ 1479:c0aa1e66c12e

csv_to_fd: improve validation * Validate flags: - Ensure M and V can't occur more than once. - Ensure absence of V for vendor 0. - Ensure presence of V for vendor not 0. * Ensure AVP name is unique per vendor. * Add AVP name to most errors. * Use csv.DictReader.line_num for more accurate line numbers. TODO: enforce M and V must be present once and only once. Some AVPs currently fail that rule, including: - RFC 4005 / RFC 7155 QoS-Filter-Rule - 3GPP TS 29.212 PDN-Connection-ID - 3GPP TS 29.212 PS-to-CS-Session-Continuity
author Luke Mewburn <luke@mewburn.net>
date Thu, 26 Mar 2020 10:28:16 +1100
parents 48fa8d70e6ad
children 5c2d061a8c8e
comparison
equal deleted inserted replaced
1478:47ce98681bd7 1479:c0aa1e66c12e
21 - RFC 7155 section 4.1 21 - RFC 7155 section 4.1
22 - Must, May, ShouldNot, MustNot: 22 - Must, May, ShouldNot, MustNot:
23 Flags, possibly comma or space separated: M, V 23 Flags, possibly comma or space separated: M, V
24 24
25 - Comment row. First cell: 25 - Comment row. First cell:
26 # Comment text Comment text 26 # Comment text 'Comment text'
27 #= Header row of ==== 27 #= '/*========*/'
28 # Blank line 28 # Blank line
29 29
30 - Parameter row: 30 - Parameter row:
31 @Parameter,Value [, ...] 31 @Parameter,Value [, ...]
32 Supported Parameter terms: 32 Supported Parameter terms:
90 90
91 # Regex to validate flags: M, P, V, comma, space 91 # Regex to validate flags: M, P, V, comma, space
92 _flags_re = re.compile(r'^[MPV, ]*$') 92 _flags_re = re.compile(r'^[MPV, ]*$')
93 93
94 __slots__ = CSV_COLUMN_NAMES + [ 94 __slots__ = CSV_COLUMN_NAMES + [
95 'filename', 'linenum', 'standard', 'vendor', ] 95 'filename', 'line_num', 'standard', 'vendor', ]
96 96
97 def __init__(self, name, code, section, datatype, 97 def __init__(self, name, code, section, datatype,
98 must, may, shouldnot, mustnot, extra_cells, 98 must, may, shouldnot, mustnot, extra_cells=[],
99 filename='', linenum=0, standard='', vendor=0): 99 filename='', line_num=0, standard='', vendor=0):
100 # Members from CSV row 100 # Members from CSV row
101 self.name = name 101 self.name = name
102 self.code = int(code) 102 self.code = int(code)
103 self.section = section 103 self.section = section
104 self.datatype = datatype 104 self.datatype = datatype
106 self.may = may 106 self.may = may
107 self.shouldnot = shouldnot 107 self.shouldnot = shouldnot
108 self.mustnot = mustnot 108 self.mustnot = mustnot
109 # Members from file state 109 # Members from file state
110 self.filename = filename 110 self.filename = filename
111 self.linenum = linenum 111 self.line_num = line_num
112 self.standard = standard 112 self.standard = standard
113 self.vendor = vendor 113 self.vendor = vendor
114 # Validate CSV fields 114 # Validate CSV fields
115 if not self._name_re.match(self.name): 115 if not self._name_re.match(self.name):
116 raise ValueError('Invalid AVP name "{}"'.format(self.name)) 116 raise ValueError('Invalid AVP name "{}"'.format(self.name))
117 if (self.code < 0 or self.code > 4294967295): 117 if (self.code < 0 or self.code > 4294967295):
118 raise ValueError('Invalid AVP code {}'.format(self.code)) 118 raise ValueError('AVP "{}" invalid code {}'.format(
119 self.name, self.code))
119 if (self.datatype not in ( 120 if (self.datatype not in (
120 'OctetString', 'Integer32', 'Integer64', 'Unsigned32', 121 'OctetString', 'Integer32', 'Integer64', 'Unsigned32',
121 'Unsigned64', 'Float32', 'Float64', 'Grouped') 122 'Unsigned64', 'Float32', 'Float64', 'Grouped')
122 and self.datatype not in DERIVED_TO_BASE): 123 and self.datatype not in DERIVED_TO_BASE):
123 raise ValueError('Invalid AVP data type "{}"'.format( 124 raise ValueError('AVP "{}" invalid data type "{}"'.format(
124 self.datatype)) 125 self.name, self.datatype))
126 # Validate flags
127 flags = collections.Counter()
125 for val, desc in [ 128 for val, desc in [
126 (self.must, 'Must'), 129 (self.must, 'Must'),
127 (self.may, 'May'), 130 (self.may, 'May'),
128 (self.shouldnot, 'Should Not'), 131 (self.shouldnot, 'Should Not'),
129 (self.mustnot, 'Must Not'), 132 (self.mustnot, 'Must Not'),
130 ]: 133 ]:
131 if not self._flags_re.match(val): 134 if not self._flags_re.match(val):
132 raise ValueError('Invalid AVP Flags {} "{}"'.format(desc, val)) 135 raise ValueError('AVP "{}" invalid {} Flags "{}"'.format(
136 self.name, desc, val))
137 flags.update(val)
138 # Check occurrence of M,V in Must,May,ShouldNot,MustNot
139 for flag in 'MV':
140 # TODO: can AVP flags not appear at all?
141 # if flags[flag] == 0:
142 # raise ValueError('AVP "{}" Flag "{}" not set'.format(
143 # self.name, flag))
144 if flags[flag] > 1:
145 raise ValueError('AVP "{}" Flag "{}" set {} times'.format(
146 self.name, flag, flags[flag]))
147 # Compare V presence against vendor
148 if 'V' in self.must:
149 if self.vendor == 0:
150 raise ValueError('AVP "{}" Flag "V" set for vendor 0'.format(
151 self.name))
152 else:
153 if self.vendor != 0:
154 raise ValueError(
155 'AVP "{}" Flag "V" not set for vendor {}'.format(
156 self.name, self.vendor))
133 157
134 @property 158 @property
135 def __dict__(self): 159 def __dict__(self):
136 return {s: getattr(self, s) for s in self.__slots__} 160 return {s: getattr(self, s) for s in self.__slots__}
137 161
166 def avp(self, avp): 190 def avp(self, avp):
167 """Process a validated Avp.""" 191 """Process a validated Avp."""
168 pass 192 pass
169 193
170 @abc.abstractmethod 194 @abc.abstractmethod
171 def comment(self, comment, filename, linenum): 195 def comment(self, comment, filename, line_num):
172 """Process a comment row: 196 """Process a comment row:
173 #comment, 197 #comment,
174 """ 198 """
175 pass 199 pass
176 200
188 212
189 def avp(self, avp): 213 def avp(self, avp):
190 avpdict = vars(avp) 214 avpdict = vars(avp)
191 print('AVP: {name}, {code}, {datatype}'.format(**avpdict)) 215 print('AVP: {name}, {code}, {datatype}'.format(**avpdict))
192 216
193 def comment(self, comment, filename, linenum): 217 def comment(self, comment, filename, line_num):
194 print('Comment: {}'.format(comment)) 218 print('Comment: {}'.format(comment))
195 219
196 def generate(self): 220 def generate(self):
197 print('Generate') 221 print('Generate')
198 222
204 pass 228 pass
205 229
206 def avp(self, avp): 230 def avp(self, avp):
207 pass 231 pass
208 232
209 def comment(self, comment, filename, linenum): 233 def comment(self, comment, filename, line_num):
210 pass 234 pass
211 235
212 def generate(self): 236 def generate(self):
213 pass 237 pass
214 238
269 avp_type)) 293 avp_type))
270 # TODO: remove ; on scope brace 294 # TODO: remove ; on scope brace
271 self.add('\t};') 295 self.add('\t};')
272 self.add('') 296 self.add('')
273 297
274 def comment(self, comment, filename, linenum): 298 def comment(self, comment, filename, line_num):
275 if comment == '': 299 if comment == '':
276 self.add('') 300 self.add('')
277 elif comment == '=': 301 elif comment == '=':
278 self.add_header() 302 self.add_header()
279 elif comment.startswith(' '): 303 elif comment.startswith(' '):
350 ('Type', avp.datatype), 374 ('Type', avp.datatype),
351 ('Vendor', avp.vendor), 375 ('Vendor', avp.vendor),
352 ]) 376 ])
353 self.avps.append(row) 377 self.avps.append(row)
354 378
355 def comment(self, comment, filename, linenum): 379 def comment(self, comment, filename, line_num):
356 pass 380 pass
357 381
358 def generate(self): 382 def generate(self):
359 doc = {"AVPs": self.avps} 383 doc = {"AVPs": self.avps}
360 print(json.dumps(doc, indent=2)) 384 print(json.dumps(doc, indent=2))
416 parser.error('Unknown processor "{}"'.format(opts.processor)) 440 parser.error('Unknown processor "{}"'.format(opts.processor))
417 441
418 # dict of [vendor][code] : Avp 442 # dict of [vendor][code] : Avp
419 avp_codes = collections.defaultdict(dict) 443 avp_codes = collections.defaultdict(dict)
420 444
445 # dict of [vendor][name] : Avp
446 avp_names = collections.defaultdict(dict)
447
421 # Process files 448 # Process files
422 for filename in args: 449 for filename in args:
423 avpproc.next_file(filename) 450 avpproc.next_file(filename)
424 with open(filename, 'r') as csvfile: 451 with open(filename, 'r') as csvfile:
425 csvdata = csv.DictReader(csvfile, CSV_COLUMN_NAMES, 452 csvdata = csv.DictReader(csvfile, CSV_COLUMN_NAMES,
426 restkey='extra_cells') 453 restkey='extra_cells')
427 linenum = 0
428 standard = '' 454 standard = ''
429 vendor = 0 455 vendor = 0
430 for row in csvdata: 456 for row in csvdata:
431 linenum += 1
432 try: 457 try:
433 if row['name'] in (None, '', 'Attribute Name'): 458 if row['name'] in (None, '', 'Attribute Name'):
434 continue 459 continue
435 elif row['name'].startswith('#'): 460 elif row['name'].startswith('#'):
436 comment = row['name'][1:] 461 comment = row['name'][1:]
437 avpproc.comment(comment, filename, linenum) 462 avpproc.comment(comment, filename, csvdata.line_num)
438 elif row['name'].startswith('@'): 463 elif row['name'].startswith('@'):
439 parameter = row['name'][1:] 464 parameter = row['name'][1:]
440 value = row['code'] 465 value = row['code']
441 if False: 466 if False:
442 pass 467 pass
446 vendor = int(value) 471 vendor = int(value)
447 else: 472 else:
448 raise ValueError('Unknown parameter "{}"'.format( 473 raise ValueError('Unknown parameter "{}"'.format(
449 parameter)) 474 parameter))
450 else: 475 else:
451 avp = Avp(filename=filename, linenum=linenum, 476 avp = Avp(filename=filename, line_num=csvdata.line_num,
452 standard=standard, vendor=vendor, 477 standard=standard, vendor=vendor,
453 **row) 478 **row)
454 # Ensure AVP vendor/code not already defined 479 # Ensure AVP vendor/code not already defined
455 if avp.code in avp_codes[avp.vendor]: 480 if avp.code in avp_codes[avp.vendor]:
456 conflict = avp_codes[avp.vendor][avp.code] 481 conflict = avp_codes[avp.vendor][avp.code]
457 raise ValueError( 482 raise ValueError(
458 'AVP vendor {} code {} already present' 483 'AVP vendor {} code {} already present'
459 ' in file "{}" line {}'.format( 484 ' in file "{}" line {}'.format(
460 avp.vendor, avp.code, 485 avp.vendor, avp.code,
461 conflict.filename, conflict.linenum)) 486 conflict.filename, conflict.line_num))
462 avp_codes[avp.vendor][avp.code] = avp 487 avp_codes[avp.vendor][avp.code] = avp
488 # Ensure AVP vendor/name not already defined
489 if avp.name in avp_names[avp.vendor]:
490 conflict = avp_names[avp.vendor][avp.name]
491 raise ValueError(
492 'AVP vendor {} name "{}" already present'
493 ' in file "{}" line {}'.format(
494 avp.vendor, avp.name,
495 conflict.filename, conflict.line_num))
496 avp_names[avp.vendor][avp.name] = avp
463 # Process AVP 497 # Process AVP
464 avpproc.avp(avp) 498 avpproc.avp(avp)
465 except ValueError as e: 499 except ValueError as e:
466 sys.stderr.write('CSV file "{}" line {}: {}: {}\n'.format( 500 sys.stderr.write('CSV file "{}" line {}: {}\n'.format(
467 filename, linenum, e.__class__.__name__, e)) 501 filename, csvdata.line_num, e))
468 sys.exit(1) 502 sys.exit(1)
469 503
470 # Generate result 504 # Generate result
471 avpproc.generate() 505 avpproc.generate()
472 506
"Welcome to our mercurial repository"