py-xbc/xbc/__init__.py

331 lines
7.9 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 provides utilities for reading and writing files in the
Linux kernel's bootconfig format. This is not a strictly-conformant
implementation. In particular, this implementation does not restrict
key/block depths and makes no attempt whatsoever to ensure output is
under 32,767 bytes as mandated by the Linux kernel implementation.
XBC has three types of configuration:
- Keys are sequences of one or more characters in the range
`[a-zA-Z0-9_-]`, namespaced with dots. They can be specified without a
value, allowing their presence to be a boolean.
- Key/value pairs have a key component, an operator, and one or more
values. Values have three flavours:
- Bare values are sequences of one or more characters that are not in
the range `{}#=+:;,\\n'" `.
- Quoted strings are bounded by either single or double quotes, and
cannot contain the quote being bounded. Escaped quotes (`\\'` and
`\\"`) are not supported.
- Arrays are bare values or strings delimited by commas.
- Blocks have a key component and a sequence of keys, key/value pairs,
or blocks. Keys within blocks are not mapped separately; `a.a` is
identical to `a { a }`, for example.
XBC supports single-line comments using the pound sign (`#`).
"""
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
class Node:
def __init__(self, *args, type=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 = type
@property
def key(self):
return self.args[0]
class Key(Node):
def __init__(self, *args):
self.args = args[0]
self.type = 'key'
class KeyValue(Node):
def __init__(self, *args):
super().__init__(args, type='kv')
@property
def op(self):
return self.args[1]
@property
def value(self):
return self.args[2]
class Block(Node):
def __init__(self, *args):
super().__init__(args, type='block')
@property
def contents(self):
return self.args[1]
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
semi = Literal(';').suppress()
lbrace = Literal('{').suppress()
rbrace = Literal('}').suppress()
NL = Literal('\n').suppress()
WS = Word(' \t').suppress()
WS_NL = Word(' \t\n').suppress()
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: KeyValue(x))
key_stmt = Optional(WS) + key + Optional(assign).suppress() + Optional(WS)
key_stmt.set_parse_action(lambda x: Key(x))
kv_stmt = Optional(WS) + keyvalue + Optional(WS)
block = Forward()
statement = kv_stmt | key_stmt
term_stmt = kv_stmt | key_stmt + Optional(semi) + Optional(WS)
line_statements = Group(term_stmt + ZeroOrMore(NL + term_stmt))
term_stmt_list = Group(statement + ZeroOrMore(semi + statement))
segment = OneOrMore(block | term_stmt_list | line_statements)
block << Group(key + Optional(WS) + lbrace + Group(segment, aslist=True) + rbrace + Optional(NL))
block.set_parse_action(lambda x: Block(x))
data = OneOrMore(block | (term_stmt + Optional(NL)))
XBCParser = data
XBCParser.ignore(comment)
class ParseError(Exception):
pass
def lex(data):
tree = XBCParser.parseString(data).asList()
return tree
def parse_block(key, seq):
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, Key):
if k not in ret:
ret[k] = True
else:
raise ParseError(f'key {k} already defined')
elif isinstance(item, KeyValue):
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]]
else:
assign = ret[k]
if isinstance(value, str):
assign.append(value)
else:
assign.extend(value)
else:
assign = value
if isinstance(assign, list) and len(assign) == 1:
assign = assign[0]
ret[k] = assign
elif isinstance(item, Block):
value = item.contents
if k not in ret:
ret[k] = False
ret.update(parse_block(k, value))
return ret
def parse(data):
tree = lex(data)
d = parse_block(None, tree)
return d
def loads_xbc(data):
return parse(data)
def load_xbc(fp):
with open(fp, mode='r') as f:
return loads_xbc(f.read())
def longest_key(L):
lens = [len(x) for x in L]
shortest = min(lens)
if shortest < 1:
return None
ret = []
for i in range(shortest):
count = {}
for item in L:
j = item[i]
if j not in count:
count[j] = 0
count[j] += 1
if len(count.keys()) == 1:
ret.append(L[0][i])
else:
return '.'.join(ret)
return None
def longest_keys(keys):
keys = [k.split('.') for k in keys]
ret = set()
for i in range(len(keys)):
for j in range(1, len(keys)):
longest = longest_key([keys[i], keys[j]])
if longest is not None:
ret.add(longest)
ret.discard('')
return ret
def make_block(data):
ret = []
leafs = []
blocks = set()
block_keys = []
for key in data.keys():
if '.' not in key:
leafs.append(key)
else:
k, rest = key.split('.', maxsplit=1)
blocks.add(k)
keys = [k for k in data.keys() if '.' in k]
temp = longest_keys(keys)
if len(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):
ret = make_block(data)
return '\n'.join(ret)
def save_xbc(data, filename):
with open(filename, mode='w') as f:
f.write(saves_xbc(data))
__all__ = ['loads_xbc', 'load_xbc', 'saves_xbc', 'save_xbc', 'ParseError']