# 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']