#!/usr/bin/env python3
"""mkpatch.py -- build a patched firmware image from a manifest of byte edits.

Every edit must be length-identical (in-place patch; no layout shift). Verifies
each site's expected bytes before writing; refuses to produce anything else.

Usage: mkpatch.py <stock-img> <manifest.tsv> <out-img>
Manifest: lines "OFFSET  OLDHEX  NEWHEX  # comment" (hex spaces optional).
"""
import hashlib
import sys


def main():
    if len(sys.argv) != 4:
        print(__doc__)
        sys.exit(2)
    stock, manifest, out = sys.argv[1:4]
    data = bytearray(open(stock, 'rb').read())
    n = 0
    for line in open(manifest):
        line = line.split('#', 1)[0].strip()
        if not line:
            continue
        off_s, old_s, new_s = line.split()[:3]
        off = int(off_s, 16)
        old = bytes.fromhex(old_s.replace(' ', ''))
        new = bytes.fromhex(new_s.replace(' ', ''))
        assert len(old) == len(new), f'length change at {off_s}: {len(old)}->{len(new)}'
        got = bytes(data[off:off + len(old)])
        assert got == old, f'site {off_s}: expected {old.hex(" ")}, found {got.hex(" ")}'
        data[off:off + len(new)] = new
        print(f'patched @{off_s}: {old.hex(" ")} -> {new.hex(" ")}')
        n += 1
    assert len(data) == len(open(stock, 'rb').read())
    # byte-diff summary vs stock
    stock_b = open(stock, 'rb').read()
    diff = [i for i in range(len(data)) if data[i] != stock_b[i]]
    print(f'{n} site(s); bytes differing vs stock: {len(diff)} '
          f'({", ".join(hex(i) for i in diff[:8])})')
    sha = hashlib.sha256(data).hexdigest()
    open(out, 'wb').write(data)
    print(f'wrote {out}  ({len(data)} B)  sha256 {sha}')


if __name__ == '__main__':
    main()
