From d31efd3352875a95cb9cd432d6a73b43b34d0f1b Mon Sep 17 00:00:00 2001 From: James Coleman Date: Thu, 27 Aug 2026 16:32:59 -0500 Subject: [PATCH] Make efs2.py a CLI with ls/pull/push/backup Replace the library-style main() with argparse subcommands: ls, stat, pull, push (writes then verifies read-back), and backup (recursive directory download). Drops the hardcoded scratchpad paths. Update the SKILL.md notes to the new commands. Claude-Session: https://claude.ai/code/session_01Ctisr9uXe4H8XsscZ2exGE --- xperia-firmware-upgrade/5g-policyman/efs2.py | 156 +++++++++++++------ xperia-firmware-upgrade/SKILL.md | 11 +- 2 files changed, 116 insertions(+), 51 deletions(-) diff --git a/xperia-firmware-upgrade/5g-policyman/efs2.py b/xperia-firmware-upgrade/5g-policyman/efs2.py index c43c4a2..cec4146 100644 --- a/xperia-firmware-upgrade/5g-policyman/efs2.py +++ b/xperia-firmware-upgrade/5g-policyman/efs2.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 """Minimal Qualcomm DIAG EFS2 client over a serial diag port. -Packet layouts and opcodes replicate JohnBel/EfsTools exactly. Used to -back up and replace policyman RF-band config on a rooted Sony Xperia 1 VI. +Command-line tool to list, pull, push and back up files in a rooted phone's +modem EFS. Packet layouts and opcodes replicate JohnBel/EfsTools exactly. +Originally written to back up and replace the policyman RF-band config on a +Sony Xperia 1 VI. Run with -h for usage. """ -import sys, os, time, struct, glob, serial +import sys, os, time, struct, glob, argparse, serial DIAG_SUBSYS = 0x4B SUBSYS_EFS = 19 @@ -244,53 +246,113 @@ class Diag: return off -TARGET_DIR = "/policyman" -TARGETS = ["band_set_01.xml", "plmn_mcc_supported_01.xml", "policies.xml"] -SRC = "/tmp/claude-1000/-home-grmrgecko-Downloads-XperiFirm-5-8-1--by-Igor-Eisberg-/aa2c5f73-89ed-4884-8a6f-7f146ae4f002/scratchpad/xda_xml" -BACKUP = "/tmp/claude-1000/-home-grmrgecko-Downloads-XperiFirm-5-8-1--by-Igor-Eisberg-/aa2c5f73-89ed-4884-8a6f-7f146ae4f002/scratchpad/policyman_backup" +TYPE_NAMES = {0: "file", 1: "dir", 2: "link", 3: "immovable", 15: "item"} + + +def cmd_ls(d, args): + entries = d.listdir(args.path) + for name, etype, size in entries: + print("%-42s %-9s %8d" % (name, TYPE_NAMES.get(etype, str(etype)), size)) + print("%d entries in %s" % (len(entries), args.path)) + + +def cmd_stat(d, args): + err, mode, size = d.stat(args.path) + if err: + raise SystemExit("stat %s: efs error %d" % (args.path, err)) + print("%s mode=%o size=%d" % (args.path, mode & 0xffff, size)) + + +def cmd_pull(d, args): + local = args.local or os.path.basename(args.path.rstrip("/")) + data = d.read_file(args.path) + with open(local, "wb") as f: + f.write(data) + print("pulled %s -> %s (%d bytes)" % (args.path, local, len(data))) + + +def cmd_push(d, args): + with open(args.local, "rb") as f: + data = f.read() + n = d.write_file(args.path, data) + verified = d.read_file(args.path) == data + print("pushed %s -> %s (%d bytes), read-back %s" + % (args.local, args.path, n, "matches" if verified else "MISMATCH")) + if not verified: + raise SystemExit(2) + + +def cmd_backup(d, args): + written = [0] + + def walk(efs_dir, local_dir): + os.makedirs(local_dir, exist_ok=True) + for name, etype, size in d.listdir(efs_dir): + efs_path = efs_dir.rstrip("/") + "/" + name + local_path = os.path.join(local_dir, name) + if etype == 1: # directory + walk(efs_path, local_path) + elif etype in (0, 15): # regular file or item file + try: + data = d.read_file(efs_path) + except Exception as e: + sys.stderr.write(" skip %s: %s\n" % (efs_path, e)) + continue + with open(local_path, "wb") as f: + f.write(data) + written[0] += 1 + print(" %s (%d bytes)" % (efs_path, len(data))) + + walk(args.path, args.dest) + print("backed up %d files from %s to %s" % (written[0], args.path, args.dest)) + + +def build_parser(): + p = argparse.ArgumentParser( + prog="efs2.py", + description="Minimal Qualcomm DIAG EFS2 client: list, pull, push and " + "back up files in a rooted phone's modem EFS.", + epilog="Put the phone in diag mode first (adb shell su -c 'setprop " + "sys.usb.config diag,serial_cdev,rmnet,adb') and bind the option " + "driver. The diag port is auto-detected; override with --port or " + "the DIAGPORT env var.") + p.add_argument("-p", "--port", help="diag serial port (default: auto-detect)") + sub = p.add_subparsers(dest="command", required=True) + + s = sub.add_parser("ls", help="list an EFS directory") + s.add_argument("path") + s.set_defaults(func=cmd_ls) + + s = sub.add_parser("stat", help="stat an EFS file") + s.add_argument("path") + s.set_defaults(func=cmd_stat) + + s = sub.add_parser("pull", help="download one EFS file") + s.add_argument("path", help="EFS path, e.g. /policyman/band_set_01.xml") + s.add_argument("local", nargs="?", help="local path (default: the basename)") + s.set_defaults(func=cmd_pull) + + s = sub.add_parser("push", help="upload/overwrite one EFS file, then verify") + s.add_argument("local", help="local file to upload") + s.add_argument("path", help="destination EFS path") + s.set_defaults(func=cmd_push) + + s = sub.add_parser("backup", help="recursively download an EFS directory") + s.add_argument("path", help="EFS directory, e.g. /policyman") + s.add_argument("dest", help="local directory to write into") + s.set_defaults(func=cmd_backup) + return p + def main(): - cmd = sys.argv[1] if len(sys.argv) > 1 else "probe" - d = Diag() - print("using diag port %s" % d.port) - ver = d.hello() - print("EFS hello ok, version=%d" % ver) + args = build_parser().parse_args() + d = Diag(args.port) + sys.stderr.write("using diag port %s (efs v%d)\n" % (d.port, d.hello())) + try: + args.func(d, args) + finally: + d.s.close() - if cmd == "probe": - entries = d.listdir(TARGET_DIR) - print("%s: %d entries" % (TARGET_DIR, len(entries))) - for name, etype, size in entries: - mark = " <== TARGET" if name in TARGETS else "" - print(" %-40s type=%d size=%d%s" % (name, etype, size, mark)) - for t in TARGETS: - err, mode, size = d.stat("%s/%s" % (TARGET_DIR, t)) - print("stat %s -> err=%d mode=%o size=%d" % (t, err, mode & 0xffff, size)) - - elif cmd == "backup": - os.makedirs(BACKUP, exist_ok=True) - for t in TARGETS: - data = d.read_file("%s/%s" % (TARGET_DIR, t)) - open("%s/%s" % (BACKUP, t), "wb").write(data) - print("backed up %s (%d bytes)" % (t, len(data))) - - elif cmd == "write": - for t in TARGETS: - data = open("%s/%s" % (SRC, t), "rb").read() - n = d.write_file("%s/%s" % (TARGET_DIR, t), data) - print("wrote %s (%d bytes)" % (t, n)) - - elif cmd == "verify": - ok = True - for t in TARGETS: - want = open("%s/%s" % (SRC, t), "rb").read() - got = d.read_file("%s/%s" % (TARGET_DIR, t)) - same = want == got - ok = ok and same - print("verify %s: %s (device %d bytes, source %d bytes)" % - (t, "MATCH" if same else "DIFFER", len(got), len(want))) - print("ALL VERIFIED" if ok else "MISMATCH!") - - d.s.close() if __name__ == "__main__": main() diff --git a/xperia-firmware-upgrade/SKILL.md b/xperia-firmware-upgrade/SKILL.md index 99b2eb5..4b1364f 100644 --- a/xperia-firmware-upgrade/SKILL.md +++ b/xperia-firmware-upgrade/SKILL.md @@ -122,10 +122,13 @@ be redone. 3. Talk EFS2 over DIAG. EfsTools (JohnBel) is the usual tool but its libnserial hits EIO on the option-driver port; the reliable path was a small pyserial EFS2 client (HDLC + FCS-16, subsys 0x4B/EFS 19). Script is - `5g-policyman/efs2.py` in this skill. It does: - hello → list `/policyman` → **back up** the 3 files → write new ones - (unlink then open `O_WRONLY|O_CREAT` perm 0777, 1 KB write chunks) → - read back and byte-compare to verify. + `5g-policyman/efs2.py` in this skill, a CLI with `ls`, `stat`, `pull`, + `push` (writes then reads back to verify) and `backup` (recursive): + - `python3 efs2.py backup /policyman ./policyman-backup` (always first) + - `python3 efs2.py push band_set_01.xml /policyman/band_set_01.xml` + (and the same for plmn_mcc_supported_01.xml and policies.xml) + `push` deletes then re-creates (`O_WRONLY|O_CREAT` perm 0777, 1 KB + chunks) and confirms the read-back matches. 4. `adb reboot`. Reboot clears diag mode back to normal adb. 5. Verify: `dumpsys telephony.registry | grep isNrAvailable` → `true` (stock SEA firmware shows false); `settings get global