You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
15 KiB

4 years ago
# vim: fileencoding=utf-8
#
# ToDo: добавить проверку того, полностью ли парсился документ. Если отпарсился
# не весь файл -- выдаем ошибку.
#
from .base_format import Format
4 years ago
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):
4 years ago
processing_methods = []
super().__init__(processing_methods)
self._ignore_comments = ignore_comments
self._comments_processing = True
self._join_before = join_before
4 years ago
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()
4 years ago
if document_text == '':
self._document_dictionary = OrderedDict()
else:
self._parse_text(document_text)
def _initialize_parser(self):
'''Метод для инициализации парсеров. Для данного формата парсеры
инициализируется при создании экземпляра формата, поскольку настройка
парсеров зависит от того, включен ли флаг ignore_comments.'''
4 years ago
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=';{}'))
4 years ago
# Будущий парсер блока.
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)
4 years ago
# Для парсинга комментариев.
python_style_comment = originalTextFor(Literal('#') + restOfLine)
comments = (cppStyleComment |
python_style_comment).setParseAction(
self._create_comment_list)
4 years ago
# Для парсинга директивы include.
include_line = (Optional(action_symbols, default='')('action')
+ Keyword('include')
+ Word(printables, excludeChars=';{}')
+ Optional(semicolon.suppress())
).setParseAction(self._add_include_line)
4 years ago
# Для парсинга простых директив состоящих из одного
# или двух параметров.
plain_line = (Group(Optional(action_symbols, default='')('action')
+ statement)('name')
4 years ago
+ Optional(parameter_value)
+ Optional(semicolon.suppress())).setParseAction(
self._add_plain_line)
4 years ago
# Метод для парсинга 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')
4 years ago
+ block('content')
+ Optional(semicolon.suppress())).setParseAction(
self._add_param_block)
4 years ago
# Виды блочных директив.
block_types = (inet_spec | param_block)
# Парсер параметров с комментариями.
# Note: Применение parser.ignore(comments) является причиной странного
# поведения парсера, при котором невозможно многократное повторное
# применение формата после установки флага ignore_comments.
if self._ignore_comments:
param_line_with_comments = (ZeroOrMore(comments).suppress()(
'comments')
4 years ago
+ param_line('value')
).setParseAction(
self._add_comments_to_paramline)
4 years ago
else:
param_line_with_comments = (ZeroOrMore(comments)('comments')
+ param_line('value')
).setParseAction(
self._add_comments_to_paramline)
4 years ago
# Парсер блока с комментариями.
if self._ignore_comments:
param_block_with_comments = (ZeroOrMore(comments).suppress()(
'comments')
4 years ago
+ block_types('value')
).setParseAction(
self._add_comments_to_block)
4 years ago
else:
param_block_with_comments = (ZeroOrMore(comments)('comments')
+ block_types('value')
).setParseAction(
self._add_comments_to_block)
4 years ago
# Парсер содержимого блоков.
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):
'''Метод для запуска разбора документа.'''
4 years ago
parsing_result = self._document_parser.parseString(text, parseAll=True)
list_of_elements = parsing_result.asList()
# На выходе парсера получаем список словарей секций, который затем
# преобразуем в словарь документа.
4 years ago
for part in list_of_elements:
self._join_dictionary(self._document_dictionary,
part)
def _join_dictionary(self, out_dictionary, dictionary_to_add):
'''Метод для преобразования результата парсинга в итоговый словарь.
Работает рекурсивно. Умеет объединять секции с одинаковыми названиями.
'''
4 years ago
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):
'''Метод используемый в парсере простых строк, состоящих только из
имени параметра и опционально из его значения.'''
4 years ago
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.'''
4 years ago
name = current_parse.asList()
return OrderedDict({tuple(name): ['']})
def _add_ipline(self, current_parse):
'''Метод используемый в парсере ip адресов.'''
4 years ago
ip_value = current_parse.IP
return OrderedDict({tuple(ip_value): ['']})
def _add_inet_specline(self, current_parse):
'''Метод используемый в парсере директивы inet_spec.'''
4 years ago
# Удаляем пробельные символы из второго параметра директивы.
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 для построения словаря блока
параметров. Учитывает возможность наличия вложенных блоков.'''
4 years ago
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):
'''Метод используемый в парсере директив вместе с относящимися к ним
комментариями. Закрепляет комментарии за данным параметром в итоговом
словаре.'''
4 years ago
[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):
'''Метод используемый в парсере блоков директив вместе с относящимися
к ним комментариями. Закрепляет комментарии за данным параметром в
итоговом словаре.'''
4 years ago
[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):
'''Метод используемый в парсере комментариев. Формирует из обнаруженных
комментариев список, который затем закрепляется за блоком или
параметром.'''
4 years ago
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