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
"""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)
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!")
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 __name__ == "__main__":
main()

View file

@ -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