258 lines
8.5 KiB
Python
258 lines
8.5 KiB
Python
"""Generate CFFI cdef and Python modules from libaegis C sources."""
|
|
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
from typing import Dict, Tuple
|
|
|
|
|
|
def preprocess_content(content: str) -> str:
|
|
content = re.sub(r"/\*.*?\*/", " ", content, flags=re.DOTALL)
|
|
content = re.sub(r"//.*$", "", content, flags=re.MULTILINE)
|
|
content = re.sub(r"^\s*#.*$", "", content, flags=re.MULTILINE)
|
|
content = re.sub(r'extern\s+"C"\s*\{', "", content)
|
|
content = re.sub(r"(?:^|\n)\s*\}\s*(?:\n|$)", "\n", content, flags=re.MULTILINE)
|
|
return content
|
|
|
|
|
|
def clean_declaration(text: str) -> str:
|
|
while "__attribute__" in text:
|
|
old = text
|
|
text = re.sub(r"__attribute__\s*\(\([^()]*\)\)", "", text)
|
|
if text == old:
|
|
break
|
|
|
|
if "CRYPTO_ALIGN" in text and "typedef struct" in text:
|
|
text = re.sub(
|
|
r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)\s+uint8_t\s+opaque\[\d+\];", "...;", text
|
|
)
|
|
else:
|
|
text = re.sub(r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)", "", text)
|
|
|
|
lines = [
|
|
re.sub(r"\s+", " ", line).strip() for line in text.split("\n") if line.strip()
|
|
]
|
|
return " ".join(lines)
|
|
|
|
|
|
def extract_declarations(header_path: pathlib.Path) -> list[str]:
|
|
content = preprocess_content(header_path.read_text(encoding="utf-8"))
|
|
declarations = []
|
|
|
|
typedef_pattern = r"typedef\s+struct\s+\w+\s*\{[^}]+\}\s*\w+\s*;"
|
|
for match in re.finditer(typedef_pattern, content, re.DOTALL):
|
|
if decl := clean_declaration(match.group(0)):
|
|
declarations.append(decl)
|
|
|
|
func_pattern = r"((?:const\s+)?(?:int|void|size_t)\s+\w+\s*\([^;]+?\)\s*;)"
|
|
for match in re.finditer(func_pattern, content, re.DOTALL):
|
|
if (decl := clean_declaration(match.group(0))) and "aegis" in decl.lower():
|
|
declarations.append(decl)
|
|
|
|
return declarations
|
|
|
|
|
|
def format_declaration(decl: str, max_width: int = 100) -> str:
|
|
if len(decl) <= max_width:
|
|
return decl
|
|
|
|
if "(" in decl and ")" in decl:
|
|
if match := re.match(r"(.*?\s+\w+\s*)\((.*)\)(.*)", decl):
|
|
prefix, params, suffix = match.groups()
|
|
if len(prefix) + len(params) + 2 > max_width:
|
|
param_list = [p.strip() for p in params.split(",")]
|
|
if len(param_list) > 1:
|
|
formatted_params = (",\n" + " " * (len(prefix) + 1)).join(
|
|
param_list
|
|
)
|
|
return f"{prefix}({formatted_params}){suffix}"
|
|
|
|
return decl
|
|
|
|
|
|
def generate_cdef(include_dir: pathlib.Path) -> str:
|
|
lines = [
|
|
"/* This file is generated with tools/generate.py. Do not edit. */",
|
|
"",
|
|
"typedef unsigned char uint8_t;",
|
|
"typedef unsigned long size_t;",
|
|
"",
|
|
]
|
|
|
|
headers = [
|
|
"aegis.h",
|
|
"aegis128l.h",
|
|
"aegis128x2.h",
|
|
"aegis128x4.h",
|
|
"aegis256.h",
|
|
"aegis256x2.h",
|
|
"aegis256x4.h",
|
|
]
|
|
|
|
for header_name in headers:
|
|
header_path = include_dir / header_name
|
|
if not header_path.exists():
|
|
print(f"Warning: {header_name} not found", file=sys.stderr)
|
|
continue
|
|
|
|
lines.append(f"/* {header_name} */")
|
|
for decl in extract_declarations(header_path):
|
|
lines.append(format_declaration(decl))
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def extract_constants(common_h_path: pathlib.Path) -> Tuple[int, int]:
|
|
content = common_h_path.read_text(encoding="utf-8")
|
|
|
|
align_match = re.search(r"^\s*#define\s+ALIGNMENT\s+(\d+)", content, re.MULTILINE)
|
|
rate_match = re.search(r"^\s*#define\s+RATE\s+(\d+)", content, re.MULTILINE)
|
|
|
|
if not align_match or not rate_match:
|
|
raise ValueError(
|
|
f"Could not extract ALIGNMENT and/or RATE from {common_h_path}"
|
|
)
|
|
|
|
return int(align_match.group(1)), int(rate_match.group(1))
|
|
|
|
|
|
def extract_all_constants(libaegis_src_dir: pathlib.Path) -> Dict[str, Tuple[int, int]]:
|
|
variants = [
|
|
"aegis128l",
|
|
"aegis128x2",
|
|
"aegis128x4",
|
|
"aegis256",
|
|
"aegis256x2",
|
|
"aegis256x4",
|
|
]
|
|
constants = {}
|
|
|
|
for variant in variants:
|
|
common_h = libaegis_src_dir / variant / f"{variant}_common.h"
|
|
if not common_h.exists():
|
|
print(f"Warning: {common_h} not found, skipping {variant}", file=sys.stderr)
|
|
continue
|
|
|
|
try:
|
|
alignment, rate = extract_constants(common_h)
|
|
constants[variant] = (alignment, rate)
|
|
except Exception as e:
|
|
print(f"Error extracting constants from {variant}: {e}", file=sys.stderr)
|
|
|
|
return constants
|
|
|
|
|
|
ALIGNMENT_RE = re.compile(r"^(ALIGNMENT\s*=\s*)(\d+)(\s*)$", re.MULTILINE)
|
|
RATE_RE = re.compile(r"^(RATE\s*=\s*)(\d+)(\s*)$", re.MULTILINE)
|
|
|
|
|
|
def replace_constant(pattern: re.Pattern, text: str, value: int) -> str:
|
|
return pattern.sub(lambda m: f"{m.group(1)}{value}{m.group(3)}", text)
|
|
|
|
|
|
def algo_label(name: str) -> str:
|
|
return "AEGIS-" + name[5:].upper()
|
|
|
|
|
|
def generate_variant(template_src: str, variant: str, alignment: int, rate: int) -> str:
|
|
s = template_src.replace("aegis256x4", variant).replace(
|
|
"AEGIS-256X4", algo_label(variant)
|
|
)
|
|
# Fix the comment to reference the template, not the variant itself
|
|
s = re.sub(
|
|
r"# All modules are generated from \w+\.py by tools/generate\.py!",
|
|
"# All modules are generated from aegis256x4.py by tools/generate.py!",
|
|
s,
|
|
)
|
|
s = replace_constant(ALIGNMENT_RE, s, alignment)
|
|
s = replace_constant(RATE_RE, s, rate)
|
|
return s
|
|
|
|
|
|
def generate_python_modules(
|
|
template_path: pathlib.Path,
|
|
output_dir: pathlib.Path,
|
|
constants: Dict[str, Tuple[int, int]],
|
|
) -> Tuple[list[pathlib.Path], list[pathlib.Path]]:
|
|
if not template_path.exists():
|
|
raise FileNotFoundError(f"Template not found: {template_path}")
|
|
|
|
template_src = template_path.read_text(encoding="utf-8")
|
|
if "aegis256x4" not in template_src or "AEGIS-256X4" not in template_src:
|
|
raise ValueError("Template file does not contain expected identifiers")
|
|
|
|
updated = []
|
|
unchanged = []
|
|
for variant, (alignment, rate) in constants.items():
|
|
dst = output_dir / f"{variant}.py"
|
|
if variant == "aegis256x4":
|
|
new_content = replace_constant(ALIGNMENT_RE, template_src, alignment)
|
|
new_content = replace_constant(RATE_RE, new_content, rate)
|
|
else:
|
|
new_content = generate_variant(template_src, variant, alignment, rate)
|
|
|
|
if dst.exists() and dst.read_text(encoding="utf-8") == new_content:
|
|
unchanged.append(dst)
|
|
else:
|
|
dst.write_text(new_content, encoding="utf-8")
|
|
updated.append(dst)
|
|
|
|
return updated, unchanged
|
|
|
|
|
|
def main() -> int:
|
|
root = pathlib.Path(__file__).parent.parent
|
|
libaegis_src_dir = root / "libaegis" / "src"
|
|
include_dir = libaegis_src_dir / "include"
|
|
pyaegis_dir = root / "pyaegis"
|
|
|
|
if not include_dir.exists():
|
|
print(f"Include directory not found: {include_dir}", file=sys.stderr)
|
|
return 1
|
|
|
|
if not libaegis_src_dir.exists():
|
|
print(f"Source directory not found: {libaegis_src_dir}", file=sys.stderr)
|
|
return 1
|
|
|
|
print("Step 1: Extracting constants from C sources...", file=sys.stderr)
|
|
constants = extract_all_constants(libaegis_src_dir)
|
|
if not constants:
|
|
print("Error: No constants extracted", file=sys.stderr)
|
|
return 1
|
|
|
|
print("Step 2: Generating CFFI cdef header...", file=sys.stderr)
|
|
pyaegis_dir.mkdir(exist_ok=True)
|
|
cdef_path = pyaegis_dir / "aegis_cdef.h"
|
|
cdef_content = generate_cdef(include_dir)
|
|
|
|
if cdef_path.exists() and cdef_path.read_text(encoding="utf-8") == cdef_content:
|
|
print(f" - No changes to {cdef_path}", file=sys.stderr)
|
|
else:
|
|
cdef_path.write_text(cdef_content, encoding="utf-8")
|
|
print(f" - Updated {cdef_path}", file=sys.stderr)
|
|
|
|
print("Step 3: Generating Python modules...", file=sys.stderr)
|
|
try:
|
|
updated, unchanged = generate_python_modules(
|
|
pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants
|
|
)
|
|
if updated:
|
|
for p in updated:
|
|
print(f" - {p.relative_to(root)}", file=sys.stderr)
|
|
if unchanged:
|
|
print(
|
|
" - No changes to",
|
|
f"{len(unchanged)} modules" if len(unchanged) > 1 else unchanged[0],
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as e:
|
|
print(f"Error generating Python modules: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|