initial commit
commit
13b9e1e7dc
|
@ -0,0 +1,4 @@
|
||||||
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
build/
|
||||||
|
dist/
|
|
@ -0,0 +1,20 @@
|
||||||
|
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.
|
|
@ -0,0 +1,72 @@
|
||||||
|
# py-xbc
|
||||||
|
|
||||||
|
`py-xbc` is a pure-Python library for reading and writing files in the
|
||||||
|
eXtra BootConfig (XBC) file format specified by the Linux kernel. This
|
||||||
|
is not a strictly-conforming implementation: in particular, this
|
||||||
|
implementation does not enforce the 32,767-byte ceiling on XBC files,
|
||||||
|
nor does it enforce the 16-level cap on keys and blocks.
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
`py-xbc` currently requires `pyparsing` and Python 3.3+.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
`py-xbc` exports four functions:
|
||||||
|
|
||||||
|
- `loads_xbc` parses a string.
|
||||||
|
- `load_xbc` opens a file and then parses a string.
|
||||||
|
- `saves_xbc` renders to a string.
|
||||||
|
- `save_xbc` renders to a string and writes the string to a file.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
XBC files consist of a series of statements, of which there are three
|
||||||
|
kinds:
|
||||||
|
|
||||||
|
- A key is a sequence of one or more bytes in the range `a-zA-Z0-9_-`.
|
||||||
|
They are namespaced with periods (`.`) and may be followed by an
|
||||||
|
equals sign (`=`). Key statements are terminated by a semicolon (`;`),
|
||||||
|
a linefeed, or a semicolon followed by a linefeed.
|
||||||
|
|
||||||
|
- A key/value statement is a key followed by an operator, followed in
|
||||||
|
turn by one or more values. There are three operators:
|
||||||
|
|
||||||
|
- Assignment (`=`) specifies an initial value.
|
||||||
|
- Updates (`:=`) overwrites whatever value was previously there.
|
||||||
|
- Appends (`+=`) appends one or more values.
|
||||||
|
|
||||||
|
There are two kinds of values: strings and arrays. Strings can be
|
||||||
|
either 'bare' or quoted.
|
||||||
|
|
||||||
|
- Bare strings are a sequence of one or more bytes that are not in the
|
||||||
|
range `{}#=+:;,\n'" `.
|
||||||
|
- Quoted strings are a sequence of bytes that begins with a single
|
||||||
|
quote (`'`) or a double quote (`"`) and ends only with the same
|
||||||
|
quote. Quotes cannot be escaped.
|
||||||
|
- Arrays are a sequence of one or more values delimited by a comma
|
||||||
|
(`,`).
|
||||||
|
|
||||||
|
- A block is a key followed by a pair of curly braces, inside which is
|
||||||
|
one or more key or key/value statements.
|
||||||
|
|
||||||
|
Keys are composable. The following examples are equivalent:
|
||||||
|
|
||||||
|
```xbc
|
||||||
|
foo {
|
||||||
|
bar {
|
||||||
|
fluff = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# is equivalent to
|
||||||
|
foo.bar.fluff = 1
|
||||||
|
# is equivalent to
|
||||||
|
foo.bar { fluff = 1 }
|
||||||
|
# is equivalent to
|
||||||
|
foo { bar.fluff = 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
# Licence
|
||||||
|
|
||||||
|
`py-xbc` is published under the MIT license. See `LICENSE` for more
|
||||||
|
information.
|
|
@ -0,0 +1,36 @@
|
||||||
|
[project]
|
||||||
|
name = 'py-xbc'
|
||||||
|
version = '0.1.0'
|
||||||
|
authors = [
|
||||||
|
{ name = 'Síle Ekaterin Liszka', email = 'sheila@vulpine.house' }
|
||||||
|
]
|
||||||
|
description = 'A library for manipulating eXtra BootConfig (XBC) files'
|
||||||
|
readme = 'README.md'
|
||||||
|
keywords = ['bootconfig', 'xbc', 'configuration']
|
||||||
|
dependencies = [
|
||||||
|
'pyparsing',
|
||||||
|
'pytest'
|
||||||
|
]
|
||||||
|
requires-python = '>=3.7'
|
||||||
|
classifiers = [
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
|
'Programming Language :: Python :: 3.11',
|
||||||
|
'Programming Language :: Python :: 3.12',
|
||||||
|
'Topic :: Utilities'
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = 'https://gitea.treehouse.systems/VulpineAmethyst/py-xbc'
|
||||||
|
Issues = 'https://gitea.treehouse.systems/VulpineAmethyst/py-xbc/issues'
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
|
@ -0,0 +1,2 @@
|
||||||
|
feature.option.foo = 1
|
||||||
|
feature.option.bar = 2
|
|
@ -0,0 +1,4 @@
|
||||||
|
feature.option {
|
||||||
|
foo = 1
|
||||||
|
bar = 2
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
feature.options = "foo", "bar"
|
|
@ -0,0 +1 @@
|
||||||
|
feature.option{foo=1;bar=2}
|
|
@ -0,0 +1,21 @@
|
||||||
|
ftrace.event {
|
||||||
|
task.task_newtask {
|
||||||
|
filter = "pid < 128"
|
||||||
|
enable
|
||||||
|
}
|
||||||
|
kprobes.vfs_read {
|
||||||
|
probes = "vfs_read $arg1 $arg2"
|
||||||
|
filter = "common_pid < 200"
|
||||||
|
enable
|
||||||
|
}
|
||||||
|
synthetic.initcall_latency {
|
||||||
|
fields = "unsigned long func", "u64 lat"
|
||||||
|
actions = "hist:keys=func.sym,lat:vals=lat:sort=lat"
|
||||||
|
}
|
||||||
|
initcall.initcall_start {
|
||||||
|
actions = "hist:keys=func:ts0=common_timestamp.usecs"
|
||||||
|
}
|
||||||
|
initcall.initcall_finish {
|
||||||
|
actions = "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
ftrace.event.synthetic.initcall_latency {
|
||||||
|
fields = "unsigned long func", "u64 lat"
|
||||||
|
hist {
|
||||||
|
from {
|
||||||
|
event = initcall.initcall_start
|
||||||
|
key = func
|
||||||
|
assigns = "ts0=common_timestamp.usecs"
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
event = initcall.initcall_finish
|
||||||
|
key = func
|
||||||
|
assigns = "lat=common_timestamp.usecs-$ts0"
|
||||||
|
onmatch = func, $lat
|
||||||
|
}
|
||||||
|
keys = func.sym, lat
|
||||||
|
vals = lat
|
||||||
|
sort = lat
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
foo {
|
||||||
|
bar = 1
|
||||||
|
foo = 2
|
||||||
|
}
|
||||||
|
foo = 4
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from xbc import loads_xbc, ParseError
|
||||||
|
|
||||||
|
def test_key():
|
||||||
|
assert loads_xbc('a') == {'a': True}
|
||||||
|
|
||||||
|
def test_keyvalue():
|
||||||
|
assert loads_xbc('a = 1') == {'a': '1'}
|
||||||
|
|
||||||
|
def test_keys():
|
||||||
|
assert loads_xbc('a;b') == {'a': True, 'b': True}
|
||||||
|
|
||||||
|
def test_string():
|
||||||
|
assert loads_xbc('a = "b"') == {'a': '"b"'}
|
||||||
|
|
||||||
|
def test_array():
|
||||||
|
assert loads_xbc('a = 1, 2') == {'a': ['1', '2']}
|
||||||
|
|
||||||
|
def test_block():
|
||||||
|
assert loads_xbc('a { a = 1 }') == {'a': False, 'a.a': '1'}
|
||||||
|
|
||||||
|
def test_block2():
|
||||||
|
assert loads_xbc('a = 1\na { a = 1 }') == {'a': '1', 'a.a': '1'}
|
||||||
|
|
||||||
|
def test_reassignment():
|
||||||
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a = 1\na = 2')
|
||||||
|
|
||||||
|
def test_ovewrite_nonexistent():
|
||||||
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a := 1')
|
||||||
|
|
||||||
|
def test_assign_after_block():
|
||||||
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a { a = 1 }\na = 1')
|
|
@ -0,0 +1,330 @@
|
||||||
|
# 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']
|
|
@ -0,0 +1,119 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from collections.abc import Sequence, Mapping
|
||||||
|
|
||||||
|
KEY_RE = re.compile(r'^[a-zA-Z0-9_-]+(?:\.(?:[a-zA-Z0-9_-]+))*$')
|
||||||
|
NVAL_RE = re.compile(r'^[^{}=+:;,\n\'"]+$')
|
||||||
|
quotes = '\'"'
|
||||||
|
other = {'"': "'", "'": '"'}
|
||||||
|
escapes = {
|
||||||
|
'backslash': {'"': '\\x22', "'": '\\x27'},
|
||||||
|
'html': {'"': '"', "'": '''},
|
||||||
|
'url': {'"': '%22', "'": '%27'}
|
||||||
|
}
|
||||||
|
|
||||||
|
def quote(data, escape='backslash'):
|
||||||
|
esc = None
|
||||||
|
|
||||||
|
# how shall we escape embedded quotes?
|
||||||
|
if isinstance(esc, Mapping):
|
||||||
|
if '"' in escape and "'" in escape:
|
||||||
|
esc = escape
|
||||||
|
elif escape in escapes:
|
||||||
|
esc = escapes[escape]
|
||||||
|
|
||||||
|
if esc is None:
|
||||||
|
raise ValueError('unrecognised escape format')
|
||||||
|
|
||||||
|
f = data[0]
|
||||||
|
|
||||||
|
# is this a quoted string?
|
||||||
|
if f in quotes and data[-1] == f:
|
||||||
|
# return it if we don't need to do anything
|
||||||
|
if f not in data[1:-1]:
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
# escape embedded quotes
|
||||||
|
x = data[1:-1].replace(f, esc[f])
|
||||||
|
return f'{f}{x}{f}'
|
||||||
|
else:
|
||||||
|
# if the other quote isn't used, wrap in it
|
||||||
|
if f in quotes and other[f] not in data[1:]:
|
||||||
|
q = other[f]
|
||||||
|
return f'{q}{data}{q}'
|
||||||
|
# not a quoted string, but has only one kind of quote
|
||||||
|
elif "'" in data and '"' not in data:
|
||||||
|
return f'"{data}"'
|
||||||
|
elif '"' in data and "'" not in data:
|
||||||
|
return f"'{data}'"
|
||||||
|
# not a quoted string and has both types; we escape one
|
||||||
|
else:
|
||||||
|
data = data.replace("'", esc["'"])
|
||||||
|
return f"'{data}"
|
||||||
|
|
||||||
|
def normalise_string(string):
|
||||||
|
if not isinstance(string, str):
|
||||||
|
string = str(string)
|
||||||
|
|
||||||
|
if NVAL_RE.match(string) is None:
|
||||||
|
string = quote(string)
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def normalise(data):
|
||||||
|
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
|
||||||
|
return normalise_string(data)
|
||||||
|
elif isinstance(data, Sequence):
|
||||||
|
L = []
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)):
|
||||||
|
L.append(normalise_string(item))
|
||||||
|
# we can unwind nested sequences
|
||||||
|
elif isinstance(item, Sequence):
|
||||||
|
L.extend(normalise(item))
|
||||||
|
# ...but we can't do that with mappings, the format doesn't
|
||||||
|
# support it.
|
||||||
|
elif isinstance(item, Mapping):
|
||||||
|
raise ValueError('nested mapping')
|
||||||
|
else:
|
||||||
|
raise ValueError(type(value))
|
||||||
|
L = ', '.join(L)
|
||||||
|
return L
|
||||||
|
elif isinstance(data, Mapping):
|
||||||
|
d = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
if Key.pattern.match(k) is None:
|
||||||
|
raise KeyError(k)
|
||||||
|
else:
|
||||||
|
k = Key(k)
|
||||||
|
|
||||||
|
v = normalise(v)
|
||||||
|
|
||||||
|
d[k] = v
|
||||||
|
return d
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
__all__ = ['quote', 'normalise', 'normalise_string']
|
Loading…
Reference in New Issue