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
This commit is contained in:
James Coleman 2026-08-27 16:32:59 -05:00
parent b42dc4c3f1
commit d31efd3352
2 changed files with 116 additions and 51 deletions

View file

@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Minimal Qualcomm DIAG EFS2 client over a serial diag port. """Minimal Qualcomm DIAG EFS2 client over a serial diag port.
Packet layouts and opcodes replicate JohnBel/EfsTools exactly. Used to Command-line tool to list, pull, push and back up files in a rooted phone's
back up and replace policyman RF-band config on a rooted Sony Xperia 1 VI. 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 DIAG_SUBSYS = 0x4B
SUBSYS_EFS = 19 SUBSYS_EFS = 19
@ -244,53 +246,113 @@ class Diag:
return off return off
TARGET_DIR = "/policyman" TYPE_NAMES = {0: "file", 1: "dir", 2: "link", 3: "immovable", 15: "item"}
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" 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(): def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "probe" args = build_parser().parse_args()
d = Diag() d = Diag(args.port)
print("using diag port %s" % d.port) sys.stderr.write("using diag port %s (efs v%d)\n" % (d.port, d.hello()))
ver = d.hello() try:
print("EFS hello ok, version=%d" % ver) 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__": if __name__ == "__main__":
main() main()

View file

@ -122,10 +122,13 @@ be redone.
3. Talk EFS2 over DIAG. EfsTools (JohnBel) is the usual tool but its 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 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 small pyserial EFS2 client (HDLC + FCS-16, subsys 0x4B/EFS 19). Script is
`5g-policyman/efs2.py` in this skill. It does: `5g-policyman/efs2.py` in this skill, a CLI with `ls`, `stat`, `pull`,
hello → list `/policyman`**back up** the 3 files → write new ones `push` (writes then reads back to verify) and `backup` (recursive):
(unlink then open `O_WRONLY|O_CREAT` perm 0777, 1 KB write chunks) → - `python3 efs2.py backup /policyman ./policyman-backup` (always first)
read back and byte-compare to verify. - `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. 4. `adb reboot`. Reboot clears diag mode back to normal adb.
5. Verify: `dumpsys telephony.registry | grep isNrAvailable``true` 5. Verify: `dumpsys telephony.registry | grep isNrAvailable``true`
(stock SEA firmware shows false); `settings get global (stock SEA firmware shows false); `settings get global