mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 07:32:27 +00:00
152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Ship the adapted Meshtext MIT notice only for actually linked code."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
from firmware_elf import FirmwareElf
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
NOTICE_PATH = ROOT / "licenses/ESPP-Meshtext-MIT.txt"
|
|
SIDECAR_SUFFIX = ".meshtext-license.txt"
|
|
CAPABILITY = "companion.meshtext.channel_text"
|
|
COMPONENT = "espp-meshtext"
|
|
|
|
|
|
def notice_bytes():
|
|
data = NOTICE_PATH.read_bytes()
|
|
if not valid_notice(data):
|
|
raise ValueError("Meshtext MIT source notice is incomplete")
|
|
return data
|
|
|
|
|
|
def valid_notice(data):
|
|
return (0 < len(data) <= 262144 and all(text in data for text in (
|
|
b"Copyright (c) 2022 esp-cpp", b"Permission is hereby granted, free of charge",
|
|
b"included in all", b"copies or substantial portions of the Software",
|
|
b'THE SOFTWARE IS PROVIDED "AS IS"', b"OTHER DEALINGS IN THE", b"SOFTWARE.")))
|
|
|
|
|
|
def linked_symbols(path):
|
|
"""Only allocated executable definitions qualify; debug strings do not."""
|
|
if Path(path).read_bytes()[:4] != b"\x7fELF":
|
|
return [] # Existing marker-only test fixtures are not linked ELFs.
|
|
elf = FirmwareElf(path)
|
|
result = []
|
|
for name, (_address, size, index) in elf.symbols.items():
|
|
if size <= 0 or index >= len(elf.sections):
|
|
continue
|
|
section = elf.sections[index]
|
|
if section[1] != 8 and section[2] & 6 == 6:
|
|
result.append(name)
|
|
return sorted(result)
|
|
|
|
|
|
def meshtext_evidence(path):
|
|
symbols = linked_symbols(path)
|
|
# Match the Itanium namespace prefix, not arbitrary names containing MT.
|
|
codec = [name for name in symbols if name.startswith("_ZN4mesh2mt")]
|
|
required = ("_ZN4mesh2mt10encodeTextE", "_ZN4mesh2mt10decodeTextE",
|
|
"_ZN6MyMesh16sendMeshtextTextE", "_ZN6MyMesh19onForeignRadioFrameE",
|
|
"_ZN4mesh10Dispatcher22queueForeignRadioFrameE")
|
|
selected = []
|
|
for operation in required:
|
|
matches = [name for name in symbols if name.startswith(operation)]
|
|
if not matches:
|
|
return codec, []
|
|
selected.append(matches[0])
|
|
return codec, selected if codec else []
|
|
|
|
|
|
def notice_record(stem, data):
|
|
return {"component": COMPONENT, "license": "MIT",
|
|
"file": Path(stem).name + SIDECAR_SUFFIX,
|
|
"sha256": hashlib.sha256(data).hexdigest()}
|
|
|
|
|
|
def sidecar(stem):
|
|
stem = Path(stem)
|
|
return stem.parent / (stem.name + SIDECAR_SUFFIX)
|
|
|
|
|
|
def manifest_record(stem):
|
|
stem = Path(stem)
|
|
manifest = json.loads((stem.parent / (stem.name + ".capabilities.json")).read_text())
|
|
records = [record for record in manifest.get("third_party_notices", [])
|
|
if record.get("component") == COMPONENT]
|
|
if len(records) > 1:
|
|
raise ValueError(f"{stem}: duplicate Meshtext license declarations")
|
|
if not records:
|
|
if CAPABILITY in manifest.get("capabilities", []):
|
|
raise ValueError(f"{stem}: Meshtext capability lacks its MIT notice")
|
|
return None
|
|
record = records[0]
|
|
if (record.get("file") != sidecar(stem).name or record.get("license") != "MIT"
|
|
or not re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", "")))):
|
|
raise ValueError(f"{stem}: invalid Meshtext license declaration")
|
|
return record
|
|
|
|
|
|
def validate_artifact_notice(stem):
|
|
record = manifest_record(stem)
|
|
output = sidecar(stem)
|
|
if record is None:
|
|
if output.exists():
|
|
raise ValueError(f"{output}: stale notice for an unaffected firmware")
|
|
return None
|
|
if not output.is_file():
|
|
raise ValueError(f"{output}: missing Meshtext MIT notice")
|
|
data = output.read_bytes()
|
|
if not valid_notice(data) or hashlib.sha256(data).hexdigest() != record["sha256"]:
|
|
raise ValueError(f"{output}: incomplete or mismatched Meshtext MIT notice")
|
|
return output
|
|
|
|
|
|
def collect_build_notice(directory, stem):
|
|
codec, _chat = meshtext_evidence(Path(directory) / "firmware.elf")
|
|
record = manifest_record(stem)
|
|
if bool(codec) != bool(record):
|
|
raise ValueError("Meshtext license declaration does not match the linked ELF")
|
|
if not codec:
|
|
return validate_artifact_notice(stem)
|
|
data = notice_bytes()
|
|
if record != notice_record(stem, data):
|
|
raise ValueError("Meshtext license declaration does not match this build's source notice")
|
|
output = sidecar(stem)
|
|
if output.exists():
|
|
return validate_artifact_notice(stem)
|
|
temporary = output.with_name(f".{output.name}.{os.getpid()}.tmp")
|
|
try:
|
|
temporary.write_bytes(data)
|
|
temporary.replace(output)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
return validate_artifact_notice(stem)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("action", choices=("collect", "validate"))
|
|
parser.add_argument("path", type=Path)
|
|
parser.add_argument("--stem", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.action == "collect":
|
|
if args.stem is None:
|
|
parser.error("collect requires --stem")
|
|
collect_build_notice(args.path, args.stem)
|
|
else:
|
|
validate_artifact_notice(args.path)
|
|
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
parser.exit(1, f"Meshtext license qualification failed: {error}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|