import logging from asn1tools import parse_files, compile_dict, ParseError, CompileError from glob import glob from pathlib import Path import string from pprint import pprint import functools import lintingexceptions moduleLevelTests = [] typeLevelTests = [] fileLevelTests = [] def lintingTest (testName, testKind, testDescription): def decorate (func): @functools.wraps(func) def wrapper(*args, **kwargs): logging.debug (f" Running test {testName}") errors = func(*args, **kwargs) for error in errors: error['testName'] = testName error['testKind'] = testKind error['testDescription'] = testDescription return errors if (testKind == "type"): typeLevelTests.append(wrapper) if (testKind == "module"): moduleLevelTests.append(wrapper) if (testKind == "file"): fileLevelTests.append(wrapper) return wrapper return decorate def formatFailure(f): return f"{f['testName'] if f.get('testName') else 'Failure'}: {f['message']}" def appendFailure(failures, context, newFailure): combinedFailure = {**context, **newFailure} logging.info (f"Test Failure: {combinedFailure}") failures.append(combinedFailure) #-------------------------------------------------------------------- # File level tests #-------------------------------------------------------------------- @lintingTest(testName = "D.4.9", testKind = "file", testDescription = "Fields, tags, types and flags are space aligned") def D41 (fileLines, context): errors = [] for lineNumber, line in enumerate(fileLines): if '\t' in line: appendFailure(errors, context, { "line" : lineNumber, "message" : f"Line {lineNumber} contains tab characters"}) return errors @lintingTest(testName = "D.4.11", testKind = "file", testDescription = "Braces are given their own line") def D41 (fileLines, context): errors = [] for lineNumber, line in enumerate(fileLines): if ('{' in line and line.strip().replace(",","") != '{') or ('}' in line and line.strip().replace(",","") != '}'): if "itu-t(0)" in line: continue if "OBJECT IDENTIFIER" in line: continue if "RELATIVE-OID" in line: continue appendFailure(errors, context, { "line" : lineNumber + 1, "message" : f"Line {lineNumber + 1} contains a brace but also other characters ('{line}')"}) return errors #-------------------------------------------------------------------- # Module level tests #-------------------------------------------------------------------- @lintingTest(testName = "D.4.1", testKind = "module", testDescription = "EXTENSIBILITY IMPLIED directive set") def D41 (module, context): errors = [] if (not ('extensibility-implied' in module.keys()) or (module['extensibility-implied'] == False)): appendFailure(errors, context, {"message" : "EXTENSIBILITY IMPLIED directive not set"}) return errors @lintingTest(testName = "D.4.2", testKind = "module", testDescription = "AUTOMATIC TAGS not used") def D42(module, context): errors = [] if (module['tags'] == 'AUTOMATIC'): appendFailure(errors, context, {"message" : "AUTOMATIC TAGS directive used"}) return errors #-------------------------------------------------------------------- # Type level tests #-------------------------------------------------------------------- @lintingTest(testName = "D.3.4", testKind = "type", testDescription = "Field names only contain characters A-Z, a-z, 0-9") def D34(t, context): if not 'members' in t.keys(): logging.debug (f" D34 ignoring {context['module']} '{context['type']}' as it has no members") return [] errors = [] for m in t['members']: logging.debug (f" D34 checking member {m}") badLetters = list(set([letter for letter in m['name'] if not ((letter in string.ascii_letters) or (letter in string.digits)) ])) if len(badLetters) > 0: appendFailure (errors, context, { "field" : m['name'], "message" : f"Field '{m['name']}' contains disallowed characters {badLetters!r}"}) return errors @lintingTest(testName = "D.4.3", testKind = "type", testDescription = "Tag numbers start at zero") def D43 (t, context): errors = [] if (t['type'] == 'SEQUENCE') or (t['type'] == 'CHOICE'): if t['members'][0]['tag']['number'] != 1: appendFailure (errors, context, {"message" : f"Tag numbers for {context['type']} start at {t['members'][0]['tag']['number']}, not 1"}) return errors @lintingTest(testName = "D.4.4", testKind = "type", testDescription = "Enumerations start at zero") def D44 (t, context): errors = [] if t['type'] == 'ENUMERATED': if t['values'][0][1] != 1: appendFailure(errors, context, { "message" : f"Enumerations for {context['type']} start at {t['values'][0][1]}, not 1"}) return errors @lintingTest(testName = "D.4.5", testKind = "type", testDescription = "No anonymous types") def checkD45 (t, context): if not 'members' in t: logging.debug (f" D45: No members in type {context['type']}, ignoring") return [] errors = [] for m in t['members']: if m['type'] in ['ENUMERATED','SEQUENCE','CHOICE', 'SET']: appendFailure(errors, context, { "field" : m['name'], "message" : f"Field '{m['name']}' in {context['type']} is an anonymous {m['type']}"}) return errors def lintASN1File (asnFile): errors = [] context = {'file' : asnFile} try: logging.info ("Checking file {0}...".format(asnFile)) with open(asnFile) as f: s = f.read().splitlines() for test in fileLevelTests: errors += test(s, context) d = parse_files(asnFile) for moduleName, module in d.items(): logging.info (" Checking module {0}".format(moduleName)) for test in moduleLevelTests: context['module'] = moduleName errors += test(module, context) for typeName, typeDef in module['types'].items(): context['type'] = typeName context['module'] = moduleName for test in typeLevelTests: errors += test(typeDef, context) except ParseError as ex: appendFailure(errors, context, { "message" : "ParseError: {0}".format(ex)}) logging.error("ParseError: {0}".format(ex)) return errors def lintASN1Files (fileList): if len(fileList) == 0: logging.warning ("No files specified") return [] errorMap = {} logging.info("Checking files...") for f in fileList: errorMap[f] = lintASN1File(f) return errorMap def lintAllASN1FilesInPath (path): globPattern = str(Path(path)) + '/*.asn1' logging.info("Searching: " + globPattern) schemaGlob = glob(globPattern, recursive=True) return lintASN1Files(schemaGlob) if __name__ == '__main__': result = lintAllASN1FilesInPath("./") totalErrors = 0 totalSuppressed = 0 print ("Drafting rule checks:") print ("-----------------------------") for filename, results in result.items(): errors = [r for r in results if not (formatFailure(r) in lintingexceptions.exceptedStrings)] suppressedErrors = [r for r in results if formatFailure(r) in lintingexceptions.exceptedStrings] print (f"{filename}: {'OK' if len(errors) == 0 else f'{len(errors)} errors detected'}") for error in errors: print(" " + formatFailure(error)) for error in suppressedErrors: print(" (" + formatFailure(error) + " - suppressed)") totalErrors += len(errors) totalSuppressed += len(suppressedErrors) print ("-----------------------------") print (f"{totalErrors} non-compliances detected, {totalSuppressed} errors suppressed") exit(totalErrors)