#!/usr/bin/env python
# -*- coding: utf-8 -*-
# asn1-doxygen-filter is an extension to the Doxygen utility for creating
# documents from ASN.1 files
#
# Copyright 2017 OnBoard Security, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUTNOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE ANDNONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import argparse # parse arguments
import os.path # getting extension from file
import sys # output and stuff
import re # for regular expressions
## extract doxygen-tag namespace
RE_MODULE = re.compile(
r'^(\w[\w-]*)[\s{](?:.|\n)*DEFINITIONS(?:.|\n)*::=\s*BEGIN'
, re.IGNORECASE | re.VERBOSE | re.DOTALL | re.MULTILINE)
RE_EXPORTS = re.compile(
r'^\s*EXPORTS .*?;'
, re.VERBOSE | re.DOTALL | re.MULTILINE)
RE_IMPORTS = re.compile(
r'^\s*IMPORTS .*?;'
, re.VERBOSE | re.DOTALL | re.MULTILINE)
RE_END = re.compile(
r'^\s*END\s*$'
, re.VERBOSE | re.MULTILINE)
RE_DOXY_COMMENTS = re.compile(
r'^\s*--[-#!]\s'
, re.VERBOSE | re.MULTILINE)
RE_DOXY_C_COMMENTS = re.compile(
r'^\s*/\*\*\s(.*?)\*/'
, re.VERBOSE | re.MULTILINE | re.DOTALL)
RE_COMMENT = re.compile(
r'--.*?(?:--|\n)'
, re.VERBOSE | re.MULTILINE)
RE_TYPE = re.compile(r'(([A-Z][\w-]*)\s*::=[\w \t]+(?:{(.*?)})?.*?)\n\s*\n'
, re.VERBOSE | re.MULTILINE | re.DOTALL)
extTypes = {}
def indentString(s, indent):
lines = s.splitlines()
s = ''
for c in lines:
s += ''.ljust(indent, ' ') + c + '\n'
return s
def parseText(content, indent=None):
# we need to keep only documenting text under the /** */ and --!
c = re.sub(r'^\s*--[-!#]\s(.*)', '/** \g<1>*/', content, 0, re.MULTILINE)
if c is not None:
content = c
ret = ''
for m in RE_DOXY_C_COMMENTS.finditer(content):
lines = m[1].splitlines()
for l in lines:
l = re.sub(r'\s*\*', '', l, 1).rstrip()
ret += l + '\n'
def repl_ref(m):
return '[**{0}**]({1}#{0})'.format(m[1], extTypes.get(m[1],''))
c = re.sub(r'@ref\s+([\w-]+)', repl_ref, ret, 0, re.VERBOSE | re.MULTILINE)
if c is not None:
ret = c
c = re.sub(r'@class\s+([\w-]+)', '', ret, 0, re.VERBOSE | re.MULTILINE)
if c is not None:
ret = c
c = re.sub(r'@brief\s+', '', ret, 0, re.VERBOSE | re.MULTILINE)
if c is not None:
ret = c
return ret
cpos=0
def parseModule(mname, content):
global cpos
cpos = 0
ret = ''
m = re.search(r'^\s*IMPORTS\s*(.*?);', content, re.VERBOSE | re.DOTALL | re.MULTILINE)
if m is not None:
pos = 0
if m[1] is not None:
ret += '## Imports:\n'
s = m[1]
for fm in re.finditer(r'^([,\s\w-]*?)FROM\s*([\w-]+)\s*({[^}]*}(?:\s+WITH\s+SUCCESSORS)?)?', s, re.VERBOSE | re.MULTILINE):
imName = fm[2]
for im in re.finditer(r'[^,\s]+', fm[1], 0):
extTypes[im[0]] = imName+'.md'
ret += ' * **{}** *{}*
\n'.format(imName, re.sub(r'\s+', ' ', fm[3] or '', 0, 0))
ret += parseText(s[pos:fm.start()], 3)+'\n'
pos = fm.end()
ret += parseText(s[pos:])
cpos = m.end()
m = re.search(r'^\s*EXPORTS.*?;', content, re.VERBOSE | re.DOTALL | re.MULTILINE)
if m is not None:
if cpos < m.end():
cpos = m.end()
# parse types
def repl_type (m, doc):
ret = '## {0}\n\n'.format(m[2])
if doc is not None:
ret += parseText(doc) + '\n\n'
# parse fields and get out fields descriptions
if m[3] is not None:
ret += 'Fields:\n'
pos = 0
for fm in re.finditer('^\s*([\w-]+?)\s+(OCTET\s+STRING|BIT\s+STRING|[A-Z][.\w-]+)?(.*?)(?:,|$)', m[3], re.VERBOSE | re.MULTILINE | re.DOTALL):
if fm[2] is not None:
f = fm[1].strip()
t = fm[2].strip()
if re.match('^OCTET\s+STRING|BIT\s+STRING|BOOLEAN|INTEGER|FLOAT', t, re.VERBOSE|re.MULTILINE) is not None:
ret += '* {0} **{1}** {2}
\n'.format(f, t, fm[3] or '')
else:
ret += '* {0} [**{1}**]({2}#{1}) {3}
\n'.format(f, t, extTypes.get(t,''), fm[3] or '')
ret += parseText(fm.string[pos:fm.start()], 3)
pos = fm.end()
return ret + '```asn1\n' + re.sub(r'^\s*--.*\n', '', m[1].strip(), 0, re.MULTILINE) +'\n```\n\n'
pos = 0
for m in RE_TYPE.finditer(content[cpos:]):
ss = repl_type (m, m.string[pos:m.start()])
ret += ss
pos = m.end()
return ret
def parseAsn(outDir, content) :
# iterate modules in the file
pos= 0
RE_MODULE=re.compile(r'^\s*([A-Z][\w-]*)\s*({.*?})?\s*DEFINITIONS.*?::=\s*?BEGIN(.*)END', re.VERBOSE | re.MULTILINE | re.DOTALL)
cnt = 0
for m in RE_MODULE.finditer(content):
ret = '# ASN.1 module {}\n OID: _{}_\n'.format(m[1], re.sub(r'\s+', ' ', m[2]))
ret += parseText(content[pos:m.start()]) + '\n'
if m[3] is not None:
ret += parseModule(m[1], m[3])
ret += '\n\n'
open(outDir + '/' + m[1] + '.md', "w").write(ret)
pos = m.end()
return cnt
def main():
argc = len(sys.argv)
if argc < 3:
sys.stderr.write("Usage: {} [ASN.1 files]\n".format(sys.argv[0]))
exit(1)
outDir = sys.argv[1]
cnt = 0
for a in sys.argv[2:]:
try:
content = open(a).read()
cnt += parseAsn(outDir, content)
except IOError as e:
sys.stderr.write(e[1]+"\n")
print("{} modules porcessed\n".format(cnt))
if __name__ == '__main__':
main()