Helper routines for generating source code
This includes "signing" source to detect modifications, and maintaining user-modified sections. Hooray!
This commit is contained in:
parent
0243b0bf77
commit
4941cd049c
2 changed files with 217 additions and 0 deletions
107
parser/generated_source.py
Normal file
107
parser/generated_source.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
|
|
||||||
|
_SIGNING_SLUG = "!*RVCugYltjOsekrgCXTlKuqIrfy4-ScohO22mEDCr2ts"
|
||||||
|
_SIGNING_PREFIX = "generated source"
|
||||||
|
|
||||||
|
_BEGIN_PATTERN = re.compile("BEGIN MANUAL SECTION ([^ ]+)")
|
||||||
|
_END_PATTERN = re.compile("END MANUAL SECTION")
|
||||||
|
_SIGNATURE_PATTERN = re.compile(_SIGNING_PREFIX + " Signed<<([0-9a-f]+)>>")
|
||||||
|
|
||||||
|
|
||||||
|
def signature_token() -> str:
|
||||||
|
return _SIGNING_PREFIX + " " + _SIGNING_SLUG
|
||||||
|
|
||||||
|
|
||||||
|
def begin_manual_section(name: str) -> str:
|
||||||
|
return f"BEGIN MANUAL SECTION {name}"
|
||||||
|
|
||||||
|
|
||||||
|
def end_manual_section() -> str:
|
||||||
|
return f"END MANUAL SECTION"
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_digest(source: str) -> str:
|
||||||
|
m = hashlib.sha256()
|
||||||
|
for section, lines in _iterate_sections(source):
|
||||||
|
if section is None:
|
||||||
|
for line in lines:
|
||||||
|
m.update(line.encode("utf-8"))
|
||||||
|
return m.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def sign_generated_source(source: str) -> str:
|
||||||
|
# Only compute the hash over the automatically generated sections of the
|
||||||
|
# source file.
|
||||||
|
digest = _compute_digest(source)
|
||||||
|
signed = source.replace(_SIGNING_SLUG, f"Signed<<{digest}>>")
|
||||||
|
if signed == source:
|
||||||
|
raise ValueError("Source did not contain a signature token to replace")
|
||||||
|
return signed
|
||||||
|
|
||||||
|
|
||||||
|
def is_signed(source: str) -> bool:
|
||||||
|
return _SIGNATURE_PATTERN.search(source) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_signature(source: str) -> bool:
|
||||||
|
signatures = [m.group(1) for m in _SIGNATURE_PATTERN.finditer(source)]
|
||||||
|
if len(signatures) > 1:
|
||||||
|
raise ValueError("Multiple signatures found in source")
|
||||||
|
if len(signatures) == 0:
|
||||||
|
raise ValueError("Source does not appear to be signed")
|
||||||
|
signature: str = signatures[0]
|
||||||
|
|
||||||
|
unsigned = source.replace(f"Signed<<{signature}>>", _SIGNING_SLUG)
|
||||||
|
actual = _compute_digest(unsigned)
|
||||||
|
|
||||||
|
return signature == actual
|
||||||
|
|
||||||
|
|
||||||
|
def merge_existing(existing: str, generated: str) -> str:
|
||||||
|
manual_sections = _extract_manual_sections(existing)
|
||||||
|
|
||||||
|
result_lines = []
|
||||||
|
for section, lines in _iterate_sections(generated):
|
||||||
|
if section is not None:
|
||||||
|
lines = manual_sections.get(section, lines)
|
||||||
|
result_lines.extend(lines)
|
||||||
|
|
||||||
|
return "".join(result_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_manual_sections(code: str) -> dict[str, list[str]]:
|
||||||
|
result = {}
|
||||||
|
for section, lines in _iterate_sections(code):
|
||||||
|
if section is not None:
|
||||||
|
existing = result.get(section)
|
||||||
|
if existing is not None:
|
||||||
|
existing.extend(lines)
|
||||||
|
else:
|
||||||
|
result[section] = lines
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _iterate_sections(code: str) -> typing.Generator[tuple[str | None, list[str]], None, None]:
|
||||||
|
current_section: str | None = None
|
||||||
|
current_lines = []
|
||||||
|
for line in code.splitlines(keepends=True):
|
||||||
|
if current_section is None:
|
||||||
|
current_lines.append(line)
|
||||||
|
match = _BEGIN_PATTERN.search(line)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield (None, current_lines)
|
||||||
|
current_lines = []
|
||||||
|
current_section = match.group(1)
|
||||||
|
else:
|
||||||
|
if _END_PATTERN.search(line):
|
||||||
|
yield (current_section, current_lines)
|
||||||
|
current_lines = []
|
||||||
|
current_section = None
|
||||||
|
|
||||||
|
current_lines.append(line)
|
||||||
|
|
||||||
|
yield (current_section, current_lines)
|
||||||
110
tests/test_generated_source.py
Normal file
110
tests/test_generated_source.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import parser.generated_source as generated_source
|
||||||
|
|
||||||
|
|
||||||
|
def test_signature():
|
||||||
|
input_source = f"""
|
||||||
|
This is a random thing.
|
||||||
|
|
||||||
|
Put your slug here: {generated_source.signature_token()}
|
||||||
|
|
||||||
|
Here are some more things:
|
||||||
|
|
||||||
|
- Machine Generated
|
||||||
|
- More Machine Gnerated
|
||||||
|
{generated_source.begin_manual_section('foo')}
|
||||||
|
- You can edit here!
|
||||||
|
{generated_source.end_manual_section()}
|
||||||
|
- But not here.
|
||||||
|
{generated_source.begin_manual_section('bar')}
|
||||||
|
- You can edit here too!
|
||||||
|
{generated_source.end_manual_section()}
|
||||||
|
- Also not here.
|
||||||
|
"""
|
||||||
|
signed = generated_source.sign_generated_source(input_source)
|
||||||
|
assert signed != input_source
|
||||||
|
assert generated_source.is_signed(signed)
|
||||||
|
assert generated_source.validate_signature(signed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_changes():
|
||||||
|
input_source = f"""
|
||||||
|
This is a random thing.
|
||||||
|
|
||||||
|
Put your slug here: {generated_source.signature_token()}
|
||||||
|
|
||||||
|
Here are some more things:
|
||||||
|
|
||||||
|
- Machine Generated
|
||||||
|
- More Machine Gnerated
|
||||||
|
{generated_source.begin_manual_section('foo')}
|
||||||
|
- XXXXX
|
||||||
|
{generated_source.end_manual_section()}
|
||||||
|
- But not here.
|
||||||
|
"""
|
||||||
|
signed = generated_source.sign_generated_source(input_source)
|
||||||
|
modified = signed.replace("XXXXX", "YYYYY")
|
||||||
|
assert modified != signed
|
||||||
|
|
||||||
|
assert generated_source.is_signed(modified)
|
||||||
|
assert generated_source.validate_signature(modified)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_changes():
|
||||||
|
input_source = f"""
|
||||||
|
This is a random thing.
|
||||||
|
|
||||||
|
Put your slug here: {generated_source.signature_token()}
|
||||||
|
|
||||||
|
Here are some more things:
|
||||||
|
|
||||||
|
- Machine Generated
|
||||||
|
- More Machine Gnerated
|
||||||
|
{generated_source.begin_manual_section('foo')}
|
||||||
|
- XXXXX
|
||||||
|
{generated_source.end_manual_section()}
|
||||||
|
- ZZZZZ
|
||||||
|
"""
|
||||||
|
signed = generated_source.sign_generated_source(input_source)
|
||||||
|
modified = signed.replace("ZZZZZ", "YYYYY")
|
||||||
|
assert modified != signed
|
||||||
|
|
||||||
|
assert generated_source.is_signed(modified)
|
||||||
|
assert not generated_source.validate_signature(modified)
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_changes():
|
||||||
|
original_source = f"""
|
||||||
|
A
|
||||||
|
// {generated_source.begin_manual_section('foo')}
|
||||||
|
B
|
||||||
|
// {generated_source.end_manual_section()}
|
||||||
|
C
|
||||||
|
// {generated_source.begin_manual_section('bar')}
|
||||||
|
D
|
||||||
|
// {generated_source.end_manual_section()}
|
||||||
|
"""
|
||||||
|
new_source = f"""
|
||||||
|
E
|
||||||
|
// {generated_source.begin_manual_section('bar')}
|
||||||
|
F
|
||||||
|
// {generated_source.end_manual_section()}
|
||||||
|
// {generated_source.begin_manual_section('foo')}
|
||||||
|
G
|
||||||
|
// {generated_source.end_manual_section()}
|
||||||
|
H
|
||||||
|
"""
|
||||||
|
|
||||||
|
merged = generated_source.merge_existing(original_source, new_source)
|
||||||
|
assert (
|
||||||
|
merged
|
||||||
|
== f"""
|
||||||
|
E
|
||||||
|
// {generated_source.begin_manual_section('bar')}
|
||||||
|
D
|
||||||
|
// {generated_source.end_manual_section()}
|
||||||
|
// {generated_source.begin_manual_section('foo')}
|
||||||
|
B
|
||||||
|
// {generated_source.end_manual_section()}
|
||||||
|
H
|
||||||
|
"""
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue