py-xbc/src/xbc/__init__.py

380 lines
11 KiB
Python

# Copyright (c) 2024 Síle Ekaterin Liszka
#
# 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 BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# 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.
'''
This module implements support for the eXtra Boot Configuration file
format specified by the Linux kernel. For more information, please see
https://docs.kernel.org/admin-guide/bootconfig.html for more
information.
This is not a strictly-conforming implementation. In particular, this
module does not adhere to the kernel's 32,767-byte restriction and does
not enforce the maximum depth of 16 namespaces.
'''
import re
from collections.abc import Mapping, Sequence
from pyparsing import (
alphas,
CharsNotIn,
DelimitedList,
Forward,
Group,
nums,
Literal,
OneOrMore,
Optional,
Regex,
restOfLine,
QuotedString,
Word,
ZeroOrMore
)
from .utils import normalise
from .version import version as __version__
class XBCNode:
# pylint: disable=too-few-public-methods
'An XBC XBCNode.'
def __init__(self, *args, kind=None):
if isinstance(args[0], str):
self.args = args[0]
elif isinstance(args[0][0], str):
self.args = args[0][0]
else:
self.args = args[0][0][0]
self.type = kind
@property
def key(self):
'The key associated with the node.'
return self.args[0]
class XBCKey(XBCNode):
# pylint: disable=too-few-public-methods
'An XBC key.'
def __init__(self, *args):
# pylint: disable=super-init-not-called
self.args = args[0]
self.type = 'key'
class XBCKeyValue(XBCNode):
'An XBC key/value operation.'
def __init__(self, *args):
super().__init__(args, kind='kv')
@property
def op(self):
'The operator being performed.'
return self.args[1]
@property
def value(self):
'The data associated with the operation.'
return self.args[2]
class XBCBlock(XBCNode):
'An XBC block.'
def __init__(self, *args):
super().__init__(args, kind='block')
@property
def contents(self):
'The contents of the block.'
return self.args[1]
XBCParser = None
class ParseError(Exception):
'Exception for parsing errors.'
def lex(data):
# pylint: disable=too-many-locals,global-statement,unnecessary-lambda
'Run the lexer over the provided data.'
global XBCParser
if XBCParser is None:
key_fragment = Word(alphas + nums + '_-')
key = DelimitedList(key_fragment, delim='.', combine=True)
bareval = CharsNotIn('{}#=+:;,\n\'"')
strvals = QuotedString("'", multiline=True, unquote_results=False)
strvald = QuotedString('"', multiline=True, unquote_results=False)
value = bareval | strvald | strvals
assign = Literal('=')
update = Literal(':=')
append = Literal('+=')
op = assign | update | append
lbrace = Literal('{').suppress()
rbrace = Literal('}').suppress()
terminal = Word(';\n').suppress()
NL = Literal('\n').suppress() # pylint: disable=invalid-name
WS = Word(' \t').suppress() # pylint: disable=invalid-name
WS_NL = Word(' \t\n').suppress() # pylint: disable=invalid-name
comment = Literal('#') + restOfLine
values = Group(
value + ZeroOrMore(
Literal(',').suppress() +
Optional(WS_NL) + value
), aslist=True
)
keyvalue = Group(key + Optional(WS) + op + Optional(WS) + values, aslist=True)
keyvalue.set_parse_action(lambda x: XBCKeyValue(x))
key_stmt = key + Optional(WS) + Optional(assign).suppress() + Optional(WS)
key_stmt.set_parse_action(lambda x: XBCKey(x))
block = Forward()
statement = keyvalue | key_stmt
statements = DelimitedList(statement, delim=terminal)
segment = Group(OneOrMore(block | statements), aslist=True)
# pylint: disable=expression-not-assigned
block << Group(key + Optional(WS) + lbrace + segment + rbrace + Optional(NL))
block.set_parse_action(lambda x: XBCBlock(x))
XBCParser = OneOrMore(segment)
XBCParser.ignore(comment)
# pylint: disable=too-many-function-args
return XBCParser.parseString(data).asList()
def unquote(val):
'Remove quotes and trailing whitespace from values.'
if val[0] in '\'"' and val[0] == val[-1]:
return val[1:-1]
return val.strip()
def key_walk(d, key):
'Walk the key to guard against post-block key assignments.'
split = key.split('.')
for i in range(len(split) - 1, 0, -1):
x = '.'.join(split[:i])
if x not in d:
d[x] = False
def parse_block(key, seq):
# pylint: disable=too-many-branches,too-many-statements
'Parse the AST in to a real data structure.'
if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list):
seq = seq[0]
ret = {}
for item in seq:
if key is not None:
k = f'{key}.{item.key}'
else:
k = item.key
if isinstance(item, XBCKey):
if k not in ret:
ret[k] = True
key_walk(ret, k)
else:
raise ParseError(f'key {k} already defined')
elif isinstance(item, XBCKeyValue):
value = item.value
op = item.op
if op == '=':
if k in ret:
raise ParseError(f'key {k} already defined')
assign = value
else:
if k not in ret:
raise ParseError(f'key {k} not defined')
if op == '+=':
if isinstance(ret[k], str):
assign = [ret[k]]
elif ret[k] is True:
assign = []
else:
assign = ret[k]
if isinstance(value, str):
assign.append(value)
else:
assign.extend(value)
if isinstance(assign, list) and len(assign) == 1:
assign = assign[0]
else:
assign = value
if isinstance(assign, list):
for i, item in enumerate(assign):
assign[i] = unquote(item)
else:
assign = unquote(assign)
if isinstance(assign, list) and len(assign) == 1:
assign = assign[0]
ret[k] = assign
if '.' in k:
key_walk(ret, k)
elif isinstance(item, XBCBlock):
value = item.contents
if k not in ret:
ret[k] = False
if not isinstance(value, list):
value = [value]
d = parse_block(k, value)
for k, v in d.items():
if k in ret:
continue
ret[k] = v
return ret
def parse(data):
'Call the lexer and then the parser.'
tree = lex(data)
d = parse_block(None, tree)
return d
def loads_xbc(data):
'Load XBC data provided in a string.'
return parse(data)
def load_xbc(fp):
'Open a file and parse its contents.'
with open(fp, mode='r', encoding='UTF-8') as f:
return loads_xbc(f.read())
def longest_key(seq):
'Find the deepest-nested key in the sequence provided.'
lens = [len(x) for x in seq]
shortest = min(lens)
if shortest < 1:
return None
ret = []
for i in range(shortest):
count = {}
for item in seq:
j = item[i]
if j not in count:
count[j] = 0
count[j] += 1
if len(count.keys()) == 1:
ret.append(seq[0][i])
else:
return '.'.join(ret)
return None
def longest_keys(keys):
'Find the longest keys in the sequence provided.'
keys = [k.split('.') for k in keys]
ret = set()
for a in keys:
for b in keys[1:]:
longest = longest_key([a, b])
if longest is not None:
ret.add(longest)
ret.discard('')
return ret
def make_block(data):
# pylint: disable=too-many-locals,too-many-branches
'Create XBC blocks.'
ret = []
leafs = []
blocks = set()
for key in data.keys():
if '.' not in key:
leafs.append(key)
else:
k, _ = key.split('.', maxsplit=1)
blocks.add(k)
keys = [k for k in data.keys() if '.' in k]
temp = longest_keys(keys)
if temp:
mindots = 99
for i in temp:
if 0 < i.count('.') < mindots:
mindots = i.count('.')
temp = [i for i in temp if i.count('.') == mindots]
blocks = set(temp)
for key in leafs:
if data[key] is True:
ret.append(f'{key}')
elif data[key] is False:
continue
else:
value = normalise(data[key])
ret.append(f'{key} = {value}')
for key in blocks:
block = {}
klen = len(key) + 1
for k, v in data.items():
if not k.startswith(f'{key}.'):
continue
block[k[klen:]] = v
chunk = make_block(block)
ret.append(key + ' {')
for line in chunk:
ret.append(f'\t{line}')
ret.append('}')
return ret
def saves_xbc(data):
'Export the provided dictionary to an XBC-formatted string.'
ret = make_block(data)
return '\n'.join(ret)
def save_xbc(data, filename):
'Export the provided dictionary to an XBC-formatted string and save it.'
with open(filename, mode='w', encoding='UTF-8') as f:
f.write(saves_xbc(data))
__all__ = ['loads_xbc', 'load_xbc', 'saves_xbc', 'save_xbc', 'ParseError']