# vim: fileencoding=utf-8 # # ToDo: добавить проверку того, полностью ли парсился документ. Если отпарсился # не весь файл -- выдаем ошибку. # from .base_format import Format from collections import OrderedDict from pyparsing import originalTextFor, OneOrMore, Word, alphanums, Literal,\ ZeroOrMore, Forward, Optional, Group, restOfLine,\ cppStyleComment, Keyword, printables, nums, SkipTo class BINDFormat(Format): '''Класс формата BIND. В отличие от большинства других форматов обрабатывает документ не построчно, а напрямую применяя парсер ко всему тексту документа.''' FORMAT = 'bind' EXECUTABLE = False comment_symbol = '#' def __init__(self, document_text: str, template_path, ignore_comments=False, join_before=False, add_header=False, already_changed=False, **kwargs): processing_methods = [] super().__init__(processing_methods) self._ignore_comments = ignore_comments self._comments_processing = True self._join_before = join_before self._last_comments_list = [] self._initialize_parser() if add_header and not ignore_comments: self.header, document_text = self._get_header_and_document_text( document_text, template_path, already_changed=already_changed) else: self.header = '' document_text = document_text.strip() if document_text == '': self._document_dictionary = OrderedDict() else: self._parse_text(document_text) def _initialize_parser(self): '''Метод для инициализации парсеров. Для данного формата парсеры инициализируется при создании экземпляра формата, поскольку настройка парсеров зависит от того, включен ли флаг ignore_comments.''' left_brace = Literal('{') right_brace = Literal('}') semicolon = Literal(';') action_symbols = (Literal('!') | Literal('-')) plain_allow = Keyword('allow') drop_allow = Keyword('!allow') replace_allow = Keyword('-allow') allow = (plain_allow | drop_allow | replace_allow) keys = Keyword('keys') inet = Keyword('inet') statement = originalTextFor(Word(alphanums+'_-', excludeChars='{};')) statement_name = originalTextFor(Word(printables, excludeChars='{};')) statement_class = originalTextFor(Word(printables, excludeChars='{};')) parameter_value = originalTextFor(Word(printables, excludeChars='{};') )('parameter') ip_value = originalTextFor(Word(nums+':./', excludeChars=';{}')) # Будущий парсер блока. block = Forward() # Для парсинга директивы inet_spec. allow_item = (ip_value | statement_name) + semicolon.suppress() key_item = statement_name + semicolon.suppress() allow_group = Group(Group(Optional(action_symbols, default='')('action') + allow) + Group(left_brace.suppress() + ZeroOrMore(allow_item) + right_brace.suppress())) keys_group = Group(Group(Optional(action_symbols, default='')('action') + keys) + Group(left_brace.suppress() + ZeroOrMore(key_item) + right_brace.suppress())) inet_spec = (Group(Optional(action_symbols, default='')('action') + inet + originalTextFor(SkipTo(allow, include=False) )('parameter') )('name') + Group(allow_group + Optional(keys_group) + semicolon.suppress())('content') ).setParseAction(self._add_inet_specline) # Для парсинга комментариев. python_style_comment = originalTextFor(Literal('#') + restOfLine) comments = (cppStyleComment | python_style_comment).setParseAction( self._create_comment_list) # Для парсинга директивы include. include_line = (Optional(action_symbols, default='')('action') + Keyword('include') + Word(printables, excludeChars=';{}') + Optional(semicolon.suppress()) ).setParseAction(self._add_include_line) # Для парсинга простых директив состоящих из одного # или двух параметров. plain_line = (Group(Optional(action_symbols, default='')('action') + statement)('name') + Optional(parameter_value) + Optional(semicolon.suppress())).setParseAction( self._add_plain_line) # Метод для парсинга IP адресов. ip_line = (Group(Optional(action_symbols, default='')('action') + ip_value + Optional(semicolon.suppress()))('IP') ).setParseAction(self._add_ipline) # Парсеры параметров. param_line = (include_line | ip_line | plain_line) # Парсер блока параметров. param_block = (Group(Optional(action_symbols, default='')('action') + statement + Optional(statement_name) + Optional(statement_class))('name') + block('content') + Optional(semicolon.suppress())).setParseAction( self._add_param_block) # Виды блочных директив. block_types = (inet_spec | param_block) # Парсер параметров с комментариями. # Note: Применение parser.ignore(comments) является причиной странного # поведения парсера, при котором невозможно многократное повторное # применение формата после установки флага ignore_comments. if self._ignore_comments: param_line_with_comments = (ZeroOrMore(comments).suppress()( 'comments') + param_line('value') ).setParseAction( self._add_comments_to_paramline) else: param_line_with_comments = (ZeroOrMore(comments)('comments') + param_line('value') ).setParseAction( self._add_comments_to_paramline) # Парсер блока с комментариями. if self._ignore_comments: param_block_with_comments = (ZeroOrMore(comments).suppress()( 'comments') + block_types('value') ).setParseAction( self._add_comments_to_block) else: param_block_with_comments = (ZeroOrMore(comments)('comments') + block_types('value') ).setParseAction( self._add_comments_to_block) # Парсер содержимого блоков. block_item = (param_block_with_comments | param_line_with_comments) # Для парсинга всего блока с любым содержимым. block << Group(left_brace.suppress() + ZeroOrMore(block_item) + right_brace.suppress()) # Парсер всего документа. self._document_parser = OneOrMore(block_item) def _parse_text(self, text): '''Метод для запуска разбора документа.''' parsing_result = self._document_parser.parseString(text, parseAll=True) list_of_elements = parsing_result.asList() # На выходе парсера получаем список словарей секций, который затем # преобразуем в словарь документа. for part in list_of_elements: self._join_dictionary(self._document_dictionary, part) def _join_dictionary(self, out_dictionary, dictionary_to_add): '''Метод для преобразования результата парсинга в итоговый словарь. Работает рекурсивно. Умеет объединять секции с одинаковыми названиями. ''' for key in dictionary_to_add: if dictionary_to_add == OrderedDict(): return if key in out_dictionary and\ isinstance(dictionary_to_add[key], OrderedDict) and\ isinstance(out_dictionary[key], OrderedDict): self._join_dictionary(out_dictionary[key], dictionary_to_add[key]) else: out_dictionary[key] = dictionary_to_add[key] def _add_plain_line(self, current_parse): '''Метод используемый в парсере простых строк, состоящих только из имени параметра и опционально из его значения.''' name = tuple(current_parse.name.asList()) if not current_parse.parameter == '': value = current_parse.parameter else: if current_parse.name.action == '-': return OrderedDict() value = '' return OrderedDict({name: [value]}) def _add_include_line(self, current_parse): '''Метод используемый в парсере директивы include.''' name = current_parse.asList() return OrderedDict({tuple(name): ['']}) def _add_ipline(self, current_parse): '''Метод используемый в парсере ip адресов.''' ip_value = current_parse.IP return OrderedDict({tuple(ip_value): ['']}) def _add_inet_specline(self, current_parse): '''Метод используемый в парсере директивы inet_spec.''' # Удаляем пробельные символы из второго параметра директивы. current_parse.name.parameter = current_parse.name.parameter.strip() block_name = tuple(current_parse.name.asList()) block_content = current_parse.content.asList() content = OrderedDict({'#': []}) for item in block_content: current_keyword, values = item current_keyword = tuple(current_keyword) content[current_keyword] = values return OrderedDict({block_name: content}) def _add_param_block(self, current_parse): '''Метод используемый в парсере блоков параметров. Использует рекурсивный метод self._join_dictionary для построения словаря блока параметров. Учитывает возможность наличия вложенных блоков.''' block_name = tuple(current_parse.name.asList()) block_content = current_parse.content.asList() content = OrderedDict({'#': []}) for item in block_content: self._join_dictionary(content, item) return OrderedDict({block_name: content}) def _add_comments_to_paramline(self, current_parse): '''Метод используемый в парсере директив вместе с относящимися к ним комментариями. Закрепляет комментарии за данным параметром в итоговом словаре.''' [parameter] = current_parse.value.asList() comments = current_parse.comments if parameter == OrderedDict(): if not comments == '': self._last_comments_list.extend(comments.asList()) return OrderedDict() name = next(iter(parameter)) value = parameter[name] if not comments == '': comments_list = comments.asList() parameter[name] = (self._last_comments_list + comments_list + value) self._last_comments_list = [] return parameter def _add_comments_to_block(self, current_parse): '''Метод используемый в парсере блоков директив вместе с относящимися к ним комментариями. Закрепляет комментарии за данным параметром в итоговом словаре.''' [value] = current_parse.value [block_name] = value block = value[block_name] if not current_parse.comments == '': block['#'] = (self._last_comments_list + current_parse.comments.asList()) self._last_comments_list = [] else: block.pop('#') return value def _create_comment_list(self, current_parse): '''Метод используемый в парсере комментариев. Формирует из обнаруженных комментариев список, который затем закрепляется за блоком или параметром.''' comments_list = [] comments = current_parse.asList() for comment in comments: lines = comment.splitlines() for line in lines: comments_list.append(line.strip()) return comments_list