testsAndMisc/phone_focus_mode/lib/backup_capture.sh
Krzysztof kuhy Rudnicki 61be666329
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
phone_backup: split 333 -> 229 + 114, with the library at 100%
The capture half -- dump_packages, pull_apks, pull_sdcard, dump_settings
and dump_focus_state -- moves into lib/backup_capture.sh. SDCARD_DIRS goes
with pull_sdcard, its only reader: left behind it would be a global the
entry script never reads (SC2034), and the hook runs shellcheck without -x
so each file has to stand alone.

This code decides what a phone backup actually contains and had no test at
all. It has one now, against a fake device mocking adb_cmd -- the single
boundary all five functions cross. 22 assertions, 52/52 lines, and 13
mutations all killed. One of those mutations reintroduces the stdin-
draining bug pull_apks' own comment documents (it pulled 1 of 54 packages
before it was fixed); the suite catches it.

Nine of the ten functions moved verbatim per verify_shell_split.sh.
dump_focus_state is the exception and the tool correctly says so: its
`{ ... } >file` group hid a line kcov instruments but bash never reports,
and rewriting it as individual redirects drew SC2129 instead. One printf
satisfies both. Its rendered focus-state.md was diffed against HEAD's
output on the same inputs -- byte-identical.

Same for three multi-line literals (SDCARD_DIRS and two mapfile process
substitutions), which were stuck at 1/N covered for the same reason.

phone_backup.sh is PC-side and in neither deploy list, so no list edits.
--help exits 0; --verify on an empty dir exits 1 naming the four missing
files, matching HEAD exactly. Over-cap count 22 -> 21.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 16:02:25 +02:00

120 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# lib/backup_capture.sh — the capture half of phone_backup.sh: package lists,
# third-party APKs, shared-storage directories, settings dumps and the
# focus-mode state. Each function is handed the output directory to fill.
#
# Sourced by phone_backup.sh, which owns adb_common.sh (adb_cmd, adb_shell,
# _info, _warn). SDCARD_DIRS lives here rather than in the entry script
# because pull_sdcard is its only reader, and a file must not assign a global
# it never reads (SC2034) — shellcheck runs without -x, so each file stands
# alone.
# Directories pulled wholesale from shared storage. Anything not
# listed here is not backed up, so keep the list honest.
# Written on one line rather than one entry per line: kcov instruments a
# multi-line literal's continuation lines but bash only ever reports the
# statement at its first line, so a list spanning N lines is permanently
# stuck at 1/N covered however thoroughly it is tested.
readonly SDCARD_DIRS=(DCIM Pictures Download Documents Movies Music Signal RunnerUp)
dump_packages() {
local dir="$1"
adb_cmd shell pm list packages | sed 's/^package://' | tr -d '\r' |
sort >"${dir}/packages-all.txt"
adb_cmd shell pm list packages -3 | sed 's/^package://' | tr -d '\r' |
sort >"${dir}/packages-third-party.txt"
# Packages removed for user 0 read as uninstalled here; recorded
# separately so a restore knows what was deliberately purged.
adb_cmd shell pm list packages -u | sed 's/^package://' | tr -d '\r' |
sort >"${dir}/packages-including-removed.txt"
comm -13 "${dir}/packages-all.txt" "${dir}/packages-including-removed.txt" \
>"${dir}/packages-removed-for-user0.txt"
_info "Packages: $(wc -l <"${dir}/packages-third-party.txt") third-party, $(wc -l <"${dir}/packages-removed-for-user0.txt") removed for user 0"
}
pull_apks() {
local dir="$1" pkg="" path="" n=0 base=""
local -a pkgs=() paths=()
mkdir -p "${dir}/apks"
# Read the list into an array FIRST. Iterating the file with
# `while read < file` breaks after one package, because `adb`
# inside the loop inherits and drains that same stdin -- measured:
# it pulled 1 of 54 before this was fixed.
mapfile -t pkgs <"${dir}/packages-third-party.txt"
for pkg in "${pkgs[@]}"; do
[[ -n "${pkg}" ]] || continue
# A split app reports several paths (base + split_config.*).
# All of them are needed for a working offline reinstall, so
# each package gets its own directory rather than one file.
mapfile -t paths < <(adb_cmd shell pm path "${pkg}" 2>/dev/null | sed 's/^package://' | tr -d '\r')
[[ "${#paths[@]}" -gt 0 ]] || {
_warn "no APK path for ${pkg}"
continue
}
mkdir -p "${dir}/apks/${pkg}"
for path in "${paths[@]}"; do
[[ -n "${path}" ]] || continue
base="$(basename "${path}")"
adb_cmd pull "${path}" "${dir}/apks/${pkg}/${base}" >/dev/null 2>&1 ||
_warn "could not pull ${base} for ${pkg}"
done
n=$((n + 1))
done
_info "APKs pulled: ${n} packages, $(find "${dir}/apks" -name '*.apk' | wc -l) files"
}
pull_sdcard() {
local dir="$1" d=""
mkdir -p "${dir}/sdcard"
for d in "${SDCARD_DIRS[@]}"; do
if adb_cmd shell "test -d /sdcard/${d}" 2>/dev/null; then
_info "Pulling /sdcard/${d}"
adb_cmd pull "/sdcard/${d}" "${dir}/sdcard/" >/dev/null 2>&1 ||
_warn "partial or failed pull: ${d}"
fi
done
# Same stdin caveat as pull_apks: collect first, pull second.
local -a kdbx_paths=()
mapfile -t kdbx_paths < <(adb_cmd shell 'find /sdcard -iname "*.kdbx" 2>/dev/null' | tr -d '\r')
local kdbx=""
for kdbx in "${kdbx_paths[@]}"; do
[[ -n "${kdbx}" ]] || continue
_info "Pulling KeePass database: ${kdbx}"
mkdir -p "${dir}/sdcard/keepass"
adb_cmd pull "${kdbx}" "${dir}/sdcard/keepass/" >/dev/null 2>&1 || true
done
}
dump_settings() {
local dir="$1" ns=""
for ns in system secure global; do
adb_cmd shell settings list "${ns}" | tr -d '\r' | sort \
>"${dir}/settings-${ns}.txt"
done
_info "Settings captured (system/secure/global)"
}
dump_focus_state() {
local dir="$1"
local out="${dir}/focus-state.md" rethink="no" purged=""
# Built as one printf rather than a `{ ... } >file` group of many: kcov
# counts the group's closing brace as an instrumented line that bash never
# reports, and a run of individual redirects instead draws SC2129. One
# statement satisfies both, and the repo forbids suppressing either.
grep -qx 'com.celzero.bravedns' "${dir}/packages-all.txt" && rethink="yes"
purged="$(cat "${dir}/packages-removed-for-user0.txt")"
printf '%s\n%s\n\n%s\n%s\n\n%s%s\n%s\n%s\n' \
'# Focus-mode state at backup time' \
'# Restore with: phone_focus_mode/distraction_purge.sh' \
'## Packages removed for user 0' \
"${purged}" \
'## RethinkDNS installed: ' "${rethink}" \
'## uBlock filters live in the Firefox profile and CANNOT be' \
'## backed up from adb; see docs/youtube-block-unrooted.md' \
>"${out}"
}