asn_process.py 7.1 KB
Newer Older
canterburym's avatar
canterburym committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import logging
import json
from pathlib import Path
from subprocess import run
from re import sub

from pycrate_asn1c.asnproc import *

def reconstrainInteger (filename):
    Path('temp.asn').write_text(Path(filename).read_text().replace("18446744073709551615", "65536"))
    return 'temp.asn'

filesWithBigInts = [
    '102232-1/LI-PS-PDU.asn',
    '102232-3/IPAccessPDU.asn',
    '102232-4/L2AccessPDU.asn'
]

def syntaxCheckASN (fileList):
    """
    Performs ASN syntax checking on a list of filenames (or pathlib Paths)

    :param fileList: List of filenames (str or Pathlib Path)
    :returns: Dict with result, return code and message for each filename

    Calls the open-source asn1c compiler with the "syntax only" option.
    As a result, asn1c must be available to run.
    """
    results = {}
    for file in fileList:
        try:
            if file.as_posix() in filesWithBigInts:
                newFile = reconstrainInteger(str(file))
                p = run(['asn1c', '-E', newFile], capture_output=True)
                Path(newFile).unlink()
            else:
                p = run(['asn1c', '-E', str(file)], capture_output=True)
            if (p.returncode != 0):
                errorMessage = p.stderr.decode().splitlines()[0]
                if errorMessage.startswith('   Value "18446744073709551615" at line'):
                    results[str(file)] = { 'ok' : True}
                    continue
                results[str(file)] = {
                    'ok'   : False,
                    'code' : p.returncode,
                    'message'  : p.stderr.decode().splitlines()[0]
                }
            else:
                results[str(file)] = {
                    'ok'   : True
                }            
        except Exception as ex:
            results[str(file)] = {
                'ok'   : False,
                'code' : -1,
                'message'  : f"{ex!r}"
            }
    return results


duplicateObjects = {
    '102232-1/LI-PS-PDU.asn' : [
        'CCPayload',
        'IRIPayload',
        'Location'
    ],
    'testing/mod1.asn' : [
        'ClashField'
    ]
}
def fixDuplicateObjects(filename):
    stringContent = filename.read_text()
    for object in duplicateObjects[filename.as_posix()]:
        stringContent = stringContent.replace(f'{object} ::=', f'Native{object} ::=')
        stringContent = stringContent.replace(f'SEQUENCE OF {object}', f'SEQUENCE OF Native{object}')
        #stringContent = sub(f"]\\w{object}", f"] Native{object}", stringContent)

    Path('temp.asn').write_text(stringContent)
    return 'temp.asn'


def compileAllTargets (compileTargets):
    """
    Attempts to compile a set of compile targets using the pycrate ASN1 tools

    :param compileTargets: list of compile targets, each of which is a list of filenames
    :returns: A dict of outcome against the first filename of each compile target. Return code and message are included for failures.

    For each compile target (list of filenames) the first filename is assumed
    to be the "primary" file. This doesn't have any relavance to the compilation,
    but will be used as the identifier when reporting any compile errors.
    The compilation is performed by the pycrate ASN compile functions; errors
    are caught as exceptions and rendered into a list. 
    
    Unfortunately, the pycrate compiler doesn't report line numbers.
    The asn1c compiler does, but doesn't properly handle identifiers with the 
    same name in different modules; as this occurs multiple times in TS 33.108,
    we can't use it.
    """
    results = {}
    for target in compileTargets:
        firstTarget = target[0]
        logging.debug(f"Compiling {firstTarget}")
        try:
            fileTexts = []
            fileNames = []
            GLOBAL.clear()
            for filename in target:
                pFile = Path(filename)
                if pFile.as_posix() in duplicateObjects:
                    tmpFile = Path(fixDuplicateObjects(pFile))
                    fileTexts.append(tmpFile.read_text())
                    #tmpFile.unlink()
                else:
                    fileTexts.append(pFile.read_text())
                fileNames.append(filename)
                logging.debug (f"  Loading {filename}")
            compile_text(fileTexts, filenames = fileNames)
            results[str(firstTarget)] = {
                'ok' : True,
            }
        except Exception as ex:
            results[str(firstTarget)] = {
                'ok'   : False,
                'code' : -1,
                'message'  : f"{ex!r}"
            }
            continue
    return results



def processResults (results, stageName):
    """
    Counts the number of errors and writes out the output per filename

    :param results: List of filenames (str or Pathlib Path)
    :param stageName: Name to decorate the output with
    :returns: The number of files which had errors
    """    
    print("")
    errorCount = sum([1 for r in results.values() if not r['ok']])
    logging.info(f"{errorCount} {stageName} errors encountered")
    
    print(f"{'-':-<60}")
    print(f"{stageName} results:")
    print(f"{'-':-<60}")
    for filename, result in results.items():
        print(f" {filename:.<55}{'..OK' if result['ok'] else 'FAIL'}")
        if not result['ok']:
            if isinstance(result['message'], list):
                for thing in result['message']:
                    print(f"    {thing['message']}")
            else:
                print(f"    {result['message']}")
    
    print(f"{'-':-<60}")
    print(f"{stageName} errors: {errorCount}")
    print(f"{'-':-<60}")
 
    return errorCount


if __name__ == '__main__':
    logging.info('Searching for ASN.1 files')
    fileList = list(Path(".").rglob("*.asn1")) + list(Path(".").rglob("*.asn"))
    logging.info(f'{len(fileList)} ASN.1 files found')
    for file in fileList:
        logging.debug(f'  {file}')
    
    ignoreList = Path('testing/asn_ignore.txt').read_text().splitlines()
    ignoredFiles = []
    for ignore in ignoreList:
        logging.debug(f'Ignoring pattern {ignore}')
        for file in fileList:
            if ignore in str(file):
                ignoredFiles.append(file)
                logging.debug(f" Ignoring {str(file)} as contains {ignore}")
    ignoredFiles = list(set(ignoredFiles))
    logging.info(f'{len(ignoredFiles)} files ignored')
    for file in ignoredFiles:
        logging.debug(f'  {file}')
    
    fileList = [file for file in fileList if file not in ignoredFiles]
    logging.info(f'{len(fileList)} files to process')
    for file in fileList:
        logging.debug(f'  {file}')

    if len(fileList) == 0:
        logging.warning ("No files specified")
        exit(0)
    
    logging.info("Parsing ASN1 files")
    parseResults = syntaxCheckASN(fileList)
    if processResults(parseResults, "Parsing") > 0:
        exit(-1)

    logging.info ("Getting compile targets")
    compileTargets = json.loads(Path('testing/asn_compile_targets.json').read_text())
    logging.info (f"{len(compileTargets)} compile targets found")

    compileResults = compileAllTargets(compileTargets)
    if processResults(compileResults, "Compiling") > 0:
        exit(-1)
    
    exit(0)