Extract mtk_root, with the libraries that lived outside it
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

github.com/kuhyx/mtk-root

Unlike the app extractions, mtk_root could not travel alone: its four
libraries and three of its four test suites sat in scripts/lib/, one
directory up from the stages that source them. Taking only the directory
would have orphaned eight files here and left the new repo unable to
source anything, so all twelve moved in one filter-repo pass. The
mtk_root/ + lib/ sibling layout is preserved, so every
`source "$SCRIPT_DIR/../lib/..."` resolves with no call site edited.

Two things the new repo needed that the monorepo provided implicitly:

  lib/tests/run_all.sh  -- the copy here is shared with the non-mtk suite
                           and stays. Without a new one, shell-tests.yml's
                           glob over */lib/tests/run_all.sh would have
                           stopped covering these suites on a green build.
  .shellcheckrc         -- external-sources + source-path=SCRIPTDIR, which
                           is what lets shellcheck run with no severity
                           filter and still pass on the ../lib/ sources.

mtk_harness.sh stays here despite the name: test_common_datetime.sh
sources it. Removing it broke that suite outright, which running the
suite caught immediately. Its header now describes what it actually
serves; the misleading name is kept on purpose, since renaming would
churn its one consumer for nothing.

Verified: 84 tests pass in the new repo with shellcheck clean, 55 still
pass here, and check_shell_coverage.sh --all exits 0 with the four
mtk_root allowlist entries gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
This commit is contained in:
Krzysztof Rudnicki 2026-08-23 17:39:27 +02:00
parent a0459b99a0
commit 6b5fe8e7a7
37 changed files with 92 additions and 3226 deletions

View File

@ -27,7 +27,8 @@ Extracted to their own repos: [`steam-backlog-enforcer`](https://github.com/kuhy
[`build-your-x`](https://github.com/kuhyx/build-your-x) (lives at `~/build_your_x`),
[`reverse-survivors`](https://github.com/kuhyx/reverse-survivors),
[`kcd2-dice-solver`](https://github.com/kuhyx/kcd2-dice-solver) (lives at `~/kcd2-dice-solver`),
[`focus-owner`](https://github.com/kuhyx/focus-owner).
[`focus-owner`](https://github.com/kuhyx/focus-owner),
[`mtk-root`](https://github.com/kuhyx/mtk-root).
Archived work: [`testsAndMisc-archive`](https://github.com/kuhyx/testsAndMisc-archive).
## Adding a Flutter app

View File

@ -0,0 +1,20 @@
{
"title": "Extract mtk_root together with the libraries that live outside it",
"objective": "Move the MediaTek rooting toolkit to its own public repo. Unlike the three app extractions, mtk_root's libraries and half its test suites live in linux_configuration/scripts/lib/, one directory up. Extracting the directory alone would orphan eight files here and leave the new repo unable to source anything.",
"acceptance_criteria": [
"The new repo preserves the mtk_root/ + lib/ sibling layout, so every `source \"$SCRIPT_DIR/../lib/...\"` resolves without editing a single call site.",
"All 84 mtk tests pass from a clean clone of the new repo.",
"shellcheck exits 0 over every script in the new repo with no severity filter, which requires carrying .shellcheckrc across.",
"The new repo has its own lib/tests/run_all.sh, because the monorepo's is shared with a non-mtk suite and must stay.",
"The suite remaining in the monorepo still passes: removing a shared fixture must not take test_common_datetime.sh down with it.",
"meta/shell-coverage-allowlist.txt has no entry naming a removed path.",
"check_shell_coverage.sh --all exits 0.",
"pre-commit passes without --no-verify."
],
"out_of_scope": [
"single_use/utils/lib/bl9000_*.sh, which is separate Android rooting tooling that merely mentions mtkclient.",
"Renaming mtk_harness.sh, whose name is now historical but whose one consumer would churn for no behavioural gain.",
"Verifying the toolkit against real hardware, which READY.md already records as pending."
],
"verifier": "./lib/tests/run_all.sh in the new repo reports 84 passed / 0 failed; shellcheck over the new repo exits 0; ./linux_configuration/scripts/lib/tests/run_all.sh here reports 55 passed / 0 failed; bash meta/scripts/check_shell_coverage.sh --all exits 0; gh confirms the repo is public on main."
}

View File

@ -0,0 +1,53 @@
{
"intent": "Extract the MediaTek rooting toolkit into its own public repo, taking the four mtk_* libraries and four test suites that lived outside mtk_root/ and would otherwise have been orphaned.",
"scope": [
"linux_configuration/scripts/mtk_root/ (24 files) -> github.com/kuhyx/mtk-root",
"linux_configuration/scripts/lib/mtk_{classify,common,device,partitions}.sh - moved with it",
"linux_configuration/scripts/lib/tests/test_mtk_{classify,device,partitions}.sh - moved with it",
"lib/tests/mtk_harness.sh - deliberately KEPT, it is shared with test_common_datetime.sh",
"meta/shell-coverage-allowlist.txt - four mtk_root entries removed",
"Non-goal: single_use/utils/lib/bl9000_*.sh, separate Android tooling that merely mentions mtkclient"
],
"changes": [
"Extracted mtk_root/ plus the eight external files in one filter-repo pass with --path-rename linux_configuration/scripts/:, preserving the mtk_root/ + lib/ sibling layout so every `source \"$SCRIPT_DIR/../lib/...\"` resolves unchanged.",
"Added lib/tests/run_all.sh to the new repo. The monorepo's copy is shared with the non-mtk suite and correctly stayed behind; without a new one, shell-tests.yml's glob over */lib/tests/run_all.sh would have silently stopped covering these suites.",
"Carried .shellcheckrc (external-sources, source-path=SCRIPTDIR) into the new repo, which is what lets its CI run shellcheck with no severity filter and still pass.",
"Restored mtk_harness.sh after the initial removal broke test_common_datetime.sh, and rewrote its header to describe what it actually serves now. The misleading name is kept deliberately, since renaming would churn its one consumer.",
"Removed the four mtk_root entries from meta/shell-coverage-allowlist.txt."
],
"verification": [
{
"command": "./lib/tests/run_all.sh (extracted repo)",
"result": "pass",
"evidence": "test_mtk_classify 22 passed, test_mtk_device 26 passed, test_mtk_partitions 36 passed - 84 total, 0 failed."
},
{
"command": "find . -name '*.sh' | xargs shellcheck (extracted repo)",
"result": "pass",
"evidence": "Exit 0 after adding .shellcheckrc. Before it, the ../lib/ sources reported SC1091 info findings, which CI treats as failures."
},
{
"command": "./linux_configuration/scripts/lib/tests/run_all.sh (monorepo, after removal)",
"result": "fail then pass",
"evidence": "First run: 'mtk_harness.sh: No such file or directory'. Removing the harness broke test_common_datetime.sh, which sources it. Restored; the suite reports 55 passed, 0 failed. This is the silent CI-coverage loss the plan warned about, caught by running the suite."
},
{
"command": "bash meta/scripts/check_shell_coverage.sh --all",
"result": "pass",
"evidence": "Exit 0 with the four mtk_root allowlist entries removed."
},
{
"command": "gh repo create + push",
"result": "pass",
"evidence": "github.com/kuhyx/mtk-root public on main, 6 commits of preserved history plus the standalone-setup commit."
}
],
"risks": [
"mtk_harness.sh keeps a name that no longer matches its contents; the header now says so explicitly rather than leaving a future reader to discover it.",
"The toolkit targets hardware not currently in hand (per READY.md the Ulefone props are unverified), so the extracted repo's tests exercise decision logic only - the same limitation it had here."
],
"rollback": [
"git revert the removal commit to restore mtk_root/ and the eight lib files.",
"Re-run ./linux_configuration/scripts/lib/tests/run_all.sh and check_shell_coverage.sh --all to confirm the restored state is coherent."
]
}

View File

@ -1,88 +0,0 @@
#!/bin/bash
# Device classification, partition discovery and layout checks.
#
# Sourced by mtk_common.sh, which stays the single entry point the
# mtk_root scripts source, so their public surface is unchanged.
# ----------------------------------------------------------------------------
# Classification
# ----------------------------------------------------------------------------
# classify_device
# Sets MTK_DEVICE_CLASS to ULEFONE, PIXEL or UNKNOWN, and MTK_CLASS_REASON to
# a human-readable explanation of how it got there.
#
# ULEFONE requires a positive MediaTek signal AND a vendor/model match - both,
# never either, and never "not a Pixel, so presumably the Ulefone". A third
# phone attached by accident must land in UNKNOWN.
classify_device() {
local manufacturer="" model="" platform="" hardware="" device=""
local mtk_match=0 ulefone_match=0 pixel_match=0
manufacturer="$(dev_getprop ro.product.manufacturer)"
model="$(dev_getprop ro.product.model)"
platform="$(dev_getprop ro.board.platform)"
hardware="$(dev_getprop ro.hardware)"
device="$(dev_getprop ro.product.device)"
MTK_PROP_MANUFACTURER="$manufacturer"
MTK_PROP_MODEL="$model"
MTK_PROP_PLATFORM="$platform"
MTK_PROP_HARDWARE="$hardware"
MTK_PROP_DEVICE="$device"
if [[ $platform =~ $MTK_PLATFORM_PATTERN ]] || [[ $hardware =~ $MTK_PLATFORM_PATTERN ]]; then
mtk_match=1
fi
if [[ $manufacturer =~ $ULEFONE_VENDOR_PATTERN ]] || [[ $model =~ $ULEFONE_MODEL_PATTERN ]]; then
ulefone_match=1
fi
if [[ $manufacturer =~ $PIXEL_VENDOR_PATTERN ]] && [[ $model =~ $PIXEL_MODEL_PATTERN ]]; then
pixel_match=1
fi
if [[ $mtk_match -eq 1 && $ulefone_match -eq 1 ]]; then
MTK_DEVICE_CLASS="ULEFONE"
MTK_CLASS_REASON="MediaTek platform [${platform:-unset}/${hardware:-unset}] and Ulefone identity [${manufacturer} ${model}]"
elif [[ $pixel_match -eq 1 ]]; then
MTK_DEVICE_CLASS="PIXEL"
MTK_CLASS_REASON="Google Pixel identity [${manufacturer} ${model}]"
else
MTK_DEVICE_CLASS="UNKNOWN"
MTK_CLASS_REASON="no rule matched: manufacturer=[${manufacturer}] model=[${model}] platform=[${platform}] hardware=[${hardware}]"
fi
export MTK_DEVICE_CLASS MTK_CLASS_REASON
export MTK_PROP_MANUFACTURER MTK_PROP_MODEL MTK_PROP_PLATFORM MTK_PROP_HARDWARE MTK_PROP_DEVICE
}
# mtk_explain_classification - why classify_device decided what it did.
mtk_explain_classification() {
printf 'ro.product.manufacturer = %s\n' "${MTK_PROP_MANUFACTURER:-}"
printf 'ro.product.model = %s\n' "${MTK_PROP_MODEL:-}"
printf 'ro.product.device = %s\n' "${MTK_PROP_DEVICE:-}"
printf 'ro.board.platform = %s\n' "${MTK_PROP_PLATFORM:-}"
printf 'ro.hardware = %s\n' "${MTK_PROP_HARDWARE:-}"
printf '\n'
printf 'MediaTek pattern : %s\n' "$MTK_PLATFORM_PATTERN"
printf 'Ulefone vendor : %s\n' "$ULEFONE_VENDOR_PATTERN"
printf 'Ulefone model : %s\n' "$ULEFONE_MODEL_PATTERN"
printf 'Pixel vendor : %s\n' "$PIXEL_VENDOR_PATTERN"
printf 'Pixel model : %s\n' "$PIXEL_MODEL_PATTERN"
printf '\n'
printf 'Verdict: %s\n' "${MTK_DEVICE_CLASS:-unset}"
printf 'Reason : %s\n' "${MTK_CLASS_REASON:-unset}"
}
# mtk_assert_mediatek
# A positive assertion, deliberately not "is not a Pixel". Used to gate the
# only script that touches partitions.
mtk_assert_mediatek() {
if [[ ${MTK_PROP_PLATFORM:-} =~ $MTK_PLATFORM_PATTERN ]]; then
return 0
fi
if [[ ${MTK_PROP_HARDWARE:-} =~ $MTK_PLATFORM_PATTERN ]]; then
return 0
fi
return 1
}

View File

@ -1,132 +0,0 @@
#!/bin/bash
# ============================================================================
# mtk_common.sh - Shared helpers for the mtk_root toolkit.
#
# Source this file; do not execute it directly.
#
# Every read from a device goes through dev_getprop() or dev_list_by_name().
# Those two functions are the ONLY seam between the toolkit and real hardware:
# when MTK_ROOT_FIXTURE points at a directory they read text files instead of
# talking to adb, which is what makes the whole test suite offline and
# hermetic. If you add a new device read, route it through these or the tests
# stop meaning anything.
# ============================================================================
# ----------------------------------------------------------------------------
# Device classification patterns.
#
# The Ulefone values are GUESSED - the device has not arrived. If it reports
# something outside these patterns, classify_device returns UNKNOWN and every
# caller refuses to act. That is the intended failure mode, but on day one it
# will look like a bug: run `10-recon.sh --explain-classification` to see which
# predicate missed, then fix the pattern here. This is the one block that
# should need editing when the hardware lands.
# ----------------------------------------------------------------------------
readonly MTK_PLATFORM_PATTERN='^mt[0-9]{4}'
readonly ULEFONE_VENDOR_PATTERN='[Uu]lefone'
readonly ULEFONE_MODEL_PATTERN='[Aa]rmor'
readonly PIXEL_VENDOR_PATTERN='^[Gg]oogle$'
readonly PIXEL_MODEL_PATTERN='[Pp]ixel'
# Plausible size band for a ramdisk-carrying partition, in bytes. Used by
# mtk_sane_ramdisk_size to flag a dump that is obviously wrong - a truncated
# read, or a whole-disk grab where a partition was meant.
MTK_RAMDISK_MIN_BYTES=$((1024 * 1024))
MTK_RAMDISK_MAX_BYTES=$((128 * 1024 * 1024))
readonly MTK_RAMDISK_MIN_BYTES MTK_RAMDISK_MAX_BYTES
# ----------------------------------------------------------------------------
# Logging. Everything goes to stderr so stdout stays clean for captured data.
# ----------------------------------------------------------------------------
mtk_info() {
printf '\033[0;34m[INFO]\033[0m %s\n' "$*" >&2
}
mtk_warn() {
printf '\033[0;33m[WARN]\033[0m %s\n' "$*" >&2
}
mtk_error() {
printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2
}
mtk_fatal() {
printf '\033[0;31m[FATAL]\033[0m %s\n' "$*" >&2
exit 1
}
mtk_heading() {
printf '\n\033[1;36m== %s ==\033[0m\n' "$*" >&2
}
# ----------------------------------------------------------------------------
# Paths
# ----------------------------------------------------------------------------
# Artifacts live outside the repo: the pre-commit hook blocks every binary
# file, and a factory image is several GB. Only text manifests get committed.
mtk_cache_dir() {
printf '%s' "${MTK_ROOT_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/mtk-root}"
}
mtk_stock_dir() {
printf '%s/stock' "$(mtk_cache_dir)"
}
mtk_mtkclient_dir() {
printf '%s/mtkclient' "$(mtk_cache_dir)"
}
mtk_venv_python() {
printf '%s/venv/bin/python' "$(mtk_mtkclient_dir)"
}
# ----------------------------------------------------------------------------
# Device read seam
# ----------------------------------------------------------------------------
# Strip anything a property value has no business containing. adb shell also
# returns CRLF line endings, which silently break string comparisons.
mtk_sanitize() {
printf '%s' "$1" | tr -d '\r' | tr -cd 'A-Za-z0-9 ._:/=+,-'
}
# dev_getprop <property-name>
# Prints the value, or nothing if unset. Never fails - an absent property and
# an empty one are the same thing to every caller here.
dev_getprop() {
local prop="$1"
local raw=""
if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then
# Fixture format is one `key=value` per line, matching `getprop` output
# once the [brackets] are stripped.
raw="$(grep -m1 -- "^${prop}=" "${MTK_ROOT_FIXTURE}/props.txt" 2>/dev/null || true)"
raw="${raw#*=}"
else
raw="$(mtk_adb shell getprop "$prop" 2>/dev/null || true)"
fi
mtk_sanitize "$raw"
}
# dev_list_by_name
# Prints one partition name per line, unsorted, as the device reports them.
dev_list_by_name() {
if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then
tr -d '\r' <"${MTK_ROOT_FIXTURE}/by-name.txt" 2>/dev/null || true
return 0
fi
mtk_adb shell 'ls -1 /dev/block/by-name/ 2>/dev/null' 2>/dev/null | tr -d '\r' || true
}
_MTK_LIB_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=mtk_device.sh
source "$_MTK_LIB_DIR/mtk_device.sh"
# shellcheck source=mtk_classify.sh
source "$_MTK_LIB_DIR/mtk_classify.sh"
# shellcheck source=mtk_partitions.sh
source "$_MTK_LIB_DIR/mtk_partitions.sh"

View File

@ -1,111 +0,0 @@
#!/bin/bash
# adb wrappers, device listing, selection and authorisation.
#
# Sourced by mtk_common.sh, which stays the single entry point the
# mtk_root scripts source, so their public surface is unchanged.
# mtk_adb <args...> - serial-pinned adb, so a second phone plugged in midway
# cannot silently become the target.
mtk_adb() {
adb -s "${MTK_SERIAL:?mtk_select_device must run first}" "$@"
}
# ----------------------------------------------------------------------------
# Device presence and authorization
# ----------------------------------------------------------------------------
# mtk_list_devices - prints "<serial> <state>" per attached device.
mtk_list_devices() {
adb devices 2>/dev/null |
awk 'NR > 1 && $2 ~ /^(device|offline|unauthorized|recovery|sideload)$/ { print $1, $2 }'
}
# mtk_device_state <serial> - device|unauthorized|offline|absent
mtk_device_state() {
local serial="$1"
local state=""
state="$(mtk_list_devices | awk -v s="$serial" '$1 == s { print $2; exit }')"
printf '%s' "${state:-absent}"
}
# mtk_select_device [requested-serial]
# Sets MTK_SERIAL. Refuses when the choice is ambiguous rather than picking
# one - a wrong guess here targets the wrong phone.
mtk_select_device() {
local requested="${1:-${MTK_SERIAL:-}}"
local -a device_rows=()
local -a serials=()
local row=""
if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then
MTK_SERIAL="${MTK_SERIAL:-fixture}"
export MTK_SERIAL
return 0
fi
mapfile -t device_rows < <(mtk_list_devices)
if [[ ${#device_rows[@]} -eq 0 ]]; then
mtk_error "No device visible to adb."
mtk_error "Check: cable seated, USB debugging enabled, and try 'adb devices'."
return 1
fi
for row in "${device_rows[@]}"; do
serials+=("${row%% *}")
done
if [[ -n $requested ]]; then
for row in "${serials[@]}"; do
if [[ $row == "$requested" ]]; then
MTK_SERIAL="$requested"
export MTK_SERIAL
return 0
fi
done
mtk_error "Requested serial '${requested}' is not attached. Attached: ${serials[*]}"
return 1
fi
if [[ ${#serials[@]} -gt 1 ]]; then
mtk_error "Multiple devices attached (${serials[*]}); refusing to guess."
mtk_error "Re-run with --serial <serial>."
return 1
fi
MTK_SERIAL="${serials[0]}"
export MTK_SERIAL
return 0
}
# mtk_require_authorized
# An unauthorized device answers every getprop with an empty string, which
# reads exactly like "this device has no properties". Catch it up front so the
# report never blames the device for a prompt sitting unanswered on its screen.
mtk_require_authorized() {
local state=""
[[ -n ${MTK_ROOT_FIXTURE:-} ]] && return 0
state="$(mtk_device_state "${MTK_SERIAL}")"
case "$state" in
device)
return 0
;;
unauthorized)
mtk_error "Device ${MTK_SERIAL} is UNAUTHORIZED."
mtk_error "The RSA authorization prompt is on the phone's screen - unlock it and tap Allow."
mtk_error "If no prompt appears: revoke USB debugging authorizations on the phone, then replug."
return 1
;;
offline)
mtk_error "Device ${MTK_SERIAL} is OFFLINE. Replug the cable, or run 'adb kill-server'."
return 1
;;
*)
mtk_error "Device ${MTK_SERIAL} is in state '${state}', which this toolkit does not read."
return 1
;;
esac
}

View File

@ -1,195 +0,0 @@
#!/bin/bash
# Partition discovery, ramdisk carrier and layout expectations.
#
# Sourced by mtk_common.sh, which stays the single entry point the
# mtk_root scripts source, so their public surface is unchanged.
# ----------------------------------------------------------------------------
# Partition discovery
# ----------------------------------------------------------------------------
# discover_partitions
# Sets MTK_AB_SCHEME, MTK_SLOT_SUFFIX, MTK_RAMDISK_PART, MTK_VBMETA_PART,
# MTK_BY_NAME (newline-separated listing).
#
# The governing rule: never name a partition in a conditional that was not
# first observed in the listing. The AOSP rule (devices launching on Android
# 13+ carry the ramdisk in init_boot; earlier ones keep it in boot) explains
# WHY the preference order below is correct, but the code never consults the
# API level to decide - it reads the filesystem. That is what lets one code
# path serve both a Pixel (no init_boot -> falls through to boot) and a
# 13-launch MediaTek device (init_boot present -> taken) without branching on
# device identity.
discover_partitions() {
local entries="" suffix="" candidate="" name=""
entries="$(dev_list_by_name)"
MTK_BY_NAME="$entries"
export MTK_BY_NAME
if [[ -z $entries ]]; then
MTK_AB_SCHEME="unknown"
MTK_SLOT_SUFFIX=""
MTK_RAMDISK_PART=""
MTK_VBMETA_PART=""
export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART
return 1
fi
# Slot scheme comes from observed suffixes, not from a product table.
if grep -qE '_a$' <<<"$entries"; then
MTK_AB_SCHEME="A/B"
else
MTK_AB_SCHEME="A-only"
fi
# Prefer the device's own view of the active slot; fall back to _a only
# because a dump has to start somewhere, and say so in the report.
suffix="$(dev_getprop ro.boot.slot_suffix)"
if [[ -z $suffix && $MTK_AB_SCHEME == "A/B" ]]; then
suffix="_a"
MTK_SLOT_ASSUMED=1
else
MTK_SLOT_ASSUMED=0
fi
MTK_SLOT_SUFFIX="$suffix"
# init_boot first: its presence is decisive. A device that has it keeps the
# ramdisk there even though boot also exists, so checking boot first would
# pick the wrong partition on exactly the hardware this toolkit is for.
MTK_RAMDISK_PART=""
for candidate in init_boot boot; do
for name in "${candidate}${suffix}" "${candidate}"; do
if grep -qxF -- "$name" <<<"$entries"; then
MTK_RAMDISK_PART="$name"
break 2
fi
done
done
MTK_VBMETA_PART=""
for name in "vbmeta${suffix}" "vbmeta"; do
if grep -qxF -- "$name" <<<"$entries"; then
MTK_VBMETA_PART="$name"
break
fi
done
export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART MTK_SLOT_ASSUMED
[[ -n $MTK_RAMDISK_PART ]]
}
# mtk_ramdisk_carrier - "init_boot" or "boot", suffix stripped.
mtk_ramdisk_carrier() {
local part="${MTK_RAMDISK_PART:-}"
printf '%s' "${part%_[ab]}"
}
# mtk_check_layout_expectation
# Cross-check only. first_api_level >= 33 predicts init_boot; below predicts
# boot. A mismatch means either a vendor deviation or a bad read - the human
# should look, but the filesystem still wins.
mtk_check_layout_expectation() {
local api_level="" carrier="" expected=""
api_level="$(dev_getprop ro.product.first_api_level)"
carrier="$(mtk_ramdisk_carrier)"
[[ -z $api_level || -z $carrier ]] && return 0
[[ $api_level =~ ^[0-9]+$ ]] || return 0
if [[ $api_level -ge 33 ]]; then
expected="init_boot"
else
expected="boot"
fi
if [[ $carrier != "$expected" ]]; then
mtk_warn "Layout disagrees with expectation: first_api_level=${api_level} predicts '${expected}', device shows '${carrier}'."
mtk_warn "Trusting the device. Verify before flashing anything."
return 1
fi
return 0
}
# ----------------------------------------------------------------------------
# Host environment checks (read-only; install.sh owns the mutating half)
# ----------------------------------------------------------------------------
# mtk_udev_catchall_files
# Prints any rules file containing an ACTIVE wildcard-vendor rule granting
# world read/write. Such a rule applies to every USB device on the system -
# storage, security keys, everything - not just phones.
#
# Commented-out lines are ignored: --fix-udev disables a rule by commenting it
# rather than deleting it, so matching those would leave the check permanently
# red after it had already been dealt with. Only *.rules files are considered,
# since udev itself skips any other suffix (including our .bak- backups).
mtk_udev_catchall_files() {
local file=""
for file in /etc/udev/rules.d/*.rules; do
[[ -f $file ]] || continue
if grep -qE '^[^#]*idVendor\}=="\*"[^#]*MODE="0666"|^[^#]*MODE="0666"[^#]*idVendor\}=="\*"' \
"$file" 2>/dev/null; then
printf '%s\n' "$file"
fi
done
}
# mtk_check_venv - is the mtkclient venv actually usable?
# Checks importability, not directory existence: the venv found on this host
# in Aug 2026 existed, looked complete, and failed at `import usb` because a
# Python upgrade had orphaned it.
mtk_check_venv() {
local python_bin=""
python_bin="$(mtk_venv_python)"
if [[ ! -x $python_bin ]]; then
printf 'missing: no interpreter at %s' "$python_bin"
return 1
fi
if ! "$python_bin" -c 'import usb, usb.core, serial' 2>/dev/null; then
local err=""
err="$("$python_bin" -c 'import usb, usb.core, serial' 2>&1 | tail -1)"
printf 'broken: %s' "$err"
return 1
fi
printf 'ok: %s' "$("$python_bin" --version 2>&1)"
return 0
}
# mtk_sha256 <file>
mtk_sha256() {
sha256sum "$1" | awk '{ print $1 }'
}
# mtk_file_size <file>
mtk_file_size() {
stat -c '%s' "$1"
}
# mtk_sane_ramdisk_size <bytes>
# A dump outside this band is not necessarily wrong, but it is worth stopping
# for: an empty or truncated read and an accidental whole-disk grab both look
# like success to mtkclient's exit code.
mtk_sane_ramdisk_size() {
local size="$1"
if [[ ! $size =~ ^[0-9]+$ ]]; then
printf 'size is not a number: %s' "$size"
return 1
fi
if [[ $size -lt $MTK_RAMDISK_MIN_BYTES ]]; then
printf 'implausibly small: %s bytes (< %s)' "$size" "$MTK_RAMDISK_MIN_BYTES"
return 1
fi
if [[ $size -gt $MTK_RAMDISK_MAX_BYTES ]]; then
printf 'implausibly large: %s bytes (> %s)' "$size" "$MTK_RAMDISK_MAX_BYTES"
return 1
fi
printf '%s bytes' "$size"
return 0
}

View File

@ -3,13 +3,14 @@
#
# Sourced, not executed.
#
# The name is historical: this fixture was written for the mtk_* libraries,
# which have since moved to github.com/kuhyx/mtk-root along with their suites.
# It stays because test_common_datetime.sh sources it, and renaming it would
# churn that file for no behavioural gain.
#
# WHAT IS EXERCISED
# The pure decision logic and everything reachable through the
# MTK_ROOT_FIXTURE seam that mtk_common.sh documents as its only boundary
# with real hardware: classify_device, discover_partitions,
# mtk_ramdisk_carrier, mtk_check_layout_expectation, mtk_assert_mediatek,
# mtk_sanitize, mtk_sane_ramdisk_size, mtk_select_device/require_authorized
# under a fixture, plus the string helpers in common_datetime.sh.
# The pure decision logic in the libraries that remain here — currently the
# string and clock helpers in common_datetime.sh.
#
# WHAT IS DELIBERATELY NOT EXERCISED, AND WHY
# The coverage gate only checks that this directory HAS a run_all.sh, so it
@ -22,11 +23,7 @@
# - collapse_mounts — calls `umount -l` on a live path.
# - install_missing_pacman_packages / require_imagemagick / notify —
# effecting code (pacman, desktop notifications).
# - mtk_udev_catchall_files — globs /etc/udev/rules.d with no seam, so it
# reads this host's real rules; the result differs between here and CI.
# - mtk_sha256 / mtk_file_size — one-line wrappers over sha256sum/stat.
# This matches Phase 1's measured pattern: decision logic reaches ~90%, the
# effecting code reaches 0%.
# The measured pattern holds: decision logic reaches ~90%, effecting code 0%.
#
# TIME AND HOST DEPENDENCE
# This suite runs in shell-tests.yml and again on every pre-push via

View File

@ -1,15 +1,16 @@
#!/usr/bin/env bash
# Runs every linux_configuration/scripts/lib test.
#
# The glob is test_*.sh, NOT one prefix per subject: this directory holds
# suites for several libraries (mtk_*, common_datetime, ...), and a prefix
# glob silently skips every suite added after it — including in the CI
# discovery step in shell-tests.yml, which runs this file.
# The glob is test_*.sh, NOT one prefix per subject: a prefix glob silently
# skips every suite added after it — including in the CI discovery step in
# shell-tests.yml, which runs this file. Only common_datetime is left here
# since the mtk_* libraries moved to github.com/kuhyx/mtk-root, but the glob
# stays generic for the same reason it was generic before.
#
# This file's existence is what meta/scripts/check_shell_coverage.sh checks:
# is_covered() tests for tests/run_all.sh and nothing more, so an empty one
# here would mark all nine libraries in the parent directory covered while
# testing nothing. The suites it runs, and the gaps they leave, are named in
# here would mark every library in the parent directory covered while testing
# nothing. The suites it runs, and the gaps they leave, are named in
# mtk_harness.sh's header.
set -euo pipefail
@ -22,8 +23,8 @@ for t in "$HERE"/test_*.sh; do
# hide the state of the others, then fail the run as a whole.
# Invoked directly, NOT as `bash "$t"`: under kcov the latter instruments
# bash itself and reports zero lines for every lib. A fresh process per
# suite also matters here, because mtk_common.sh declares readonly
# patterns and sourcing it twice in one shell aborts under set -e.
# suite also matters here: a library that declares readonly values aborts
# under set -e if it is sourced twice in one shell.
"$t" || rc=1
done

View File

@ -1,120 +0,0 @@
#!/usr/bin/env bash
# Covers classify_device, mtk_assert_mediatek, mtk_explain_classification
# and mtk_sanitize, all through the MTK_ROOT_FIXTURE seam.
#
# The load-bearing assertion here is the THIRD PHONE case. classify_device's
# contract is that ULEFONE requires a MediaTek signal AND a vendor/model
# match — both, never either — so an unrelated MediaTek handset must land in
# UNKNOWN rather than being treated as the Ulefone. Every caller refuses to
# act on UNKNOWN, and that is what stops this toolkit flashing a stranger's
# phone. A rule rewritten to `||` would still pass the ULEFONE and PIXEL
# cases below; only this one catches it.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../mtk_common.sh
source "$HERE/../mtk_common.sh"
# --- classify_device -------------------------------------------------------
MTK_ROOT_FIXTURE="$(_t_fixture ulefone \
'ro.product.manufacturer=Ulefone
ro.product.model=Armor 26 Ultra
ro.board.platform=mt6989
ro.hardware=mt6989
ro.product.device=Armor26Ultra' 'boot_a')"
export MTK_ROOT_FIXTURE
classify_device
_t_is "ULEFONE" "$MTK_DEVICE_CLASS" "MediaTek platform + Ulefone vendor -> ULEFONE"
_t_in "MediaTek platform" "$MTK_CLASS_REASON" "ULEFONE reason names the platform signal"
MTK_ROOT_FIXTURE="$(_t_fixture pixel \
'ro.product.manufacturer=Google
ro.product.model=Pixel 6a
ro.board.platform=gs101
ro.hardware=oriole
ro.product.device=bluejay' 'boot_a')"
classify_device
_t_is "PIXEL" "$MTK_DEVICE_CLASS" "Google + Pixel identity -> PIXEL"
# A MediaTek phone that is NOT the Ulefone. The single most important case:
# "not a Pixel, so presumably the Ulefone" is exactly the reasoning the
# implementation refuses, and it is what would target the wrong hardware.
MTK_ROOT_FIXTURE="$(_t_fixture third_party \
'ro.product.manufacturer=Xiaomi
ro.product.model=Redmi Note 12
ro.board.platform=mt6833
ro.hardware=mt6833
ro.product.device=tapas' 'boot_a')"
classify_device
_t_is "UNKNOWN" "$MTK_DEVICE_CLASS" "MediaTek but wrong vendor -> UNKNOWN, not ULEFONE"
# Ulefone identity with no MediaTek signal is equally not enough.
MTK_ROOT_FIXTURE="$(_t_fixture ulefone_no_mtk \
'ro.product.manufacturer=Ulefone
ro.product.model=Armor 26 Ultra
ro.board.platform=sm8650
ro.hardware=qcom
ro.product.device=Armor26Ultra' 'boot_a')"
classify_device
_t_is "UNKNOWN" "$MTK_DEVICE_CLASS" "Ulefone identity without MediaTek -> UNKNOWN"
# An unauthorized device answers every getprop with an empty string. That must
# not resolve to a class.
MTK_ROOT_FIXTURE="$(_t_fixture empty '' 'boot_a')"
classify_device
_t_is "UNKNOWN" "$MTK_DEVICE_CLASS" "no properties at all -> UNKNOWN"
_t_in "no rule matched" "$MTK_CLASS_REASON" "UNKNOWN reason explains which reads were empty"
# --- mtk_assert_mediatek ---------------------------------------------------
# A positive assertion, deliberately not "is not a Pixel". It gates the only
# script that touches partitions, so both directions are asserted.
MTK_ROOT_FIXTURE="$(_t_fixture assert_mtk \
'ro.board.platform=mt6989
ro.hardware=mt6989' 'boot_a')"
classify_device
_t_rc 0 "mtk_assert_mediatek accepts an mt#### platform" mtk_assert_mediatek
MTK_ROOT_FIXTURE="$(_t_fixture assert_qcom \
'ro.board.platform=sm8650
ro.hardware=qcom' 'boot_a')"
classify_device
_t_rc 1 "mtk_assert_mediatek rejects a non-MediaTek platform" mtk_assert_mediatek
# hardware alone is enough — some devices report the SoC there and leave
# ro.board.platform unset.
MTK_ROOT_FIXTURE="$(_t_fixture assert_hw_only 'ro.hardware=mt8183' 'boot_a')"
classify_device
_t_rc 0 "mtk_assert_mediatek accepts a MediaTek ro.hardware alone" mtk_assert_mediatek
# --- mtk_explain_classification --------------------------------------------
explain_out="$(mtk_explain_classification 2>&1)"
_t_in "Verdict:" "$explain_out" "explain output states a verdict"
_t_in "mt8183" "$explain_out" "explain output echoes the property it read"
# --- mtk_sanitize ----------------------------------------------------------
# adb shell returns CRLF, which silently breaks string comparisons — the
# reason this function exists at all.
_t_is "mt6989" "$(mtk_sanitize "$(printf 'mt6989\r')")" "sanitize strips a trailing CR"
_t_is "Armor 26 Ultra" "$(mtk_sanitize 'Armor 26 Ultra')" "sanitize keeps spaces and digits"
# tr -cd DELETES disallowed bytes rather than truncating at the first one, so
# the alphanumerics inside a substitution survive as inert text. What matters
# is that $ ( ) ` ; & | cannot come back out.
# The metacharacters are assembled from \-escapes rather than written inside
# single quotes, so that "these must not expand" is stated in the code rather
# than left to the reader (and to shellcheck) to infer.
hostile="rm -rf \$(whoami)\`id\`; reboot & echo|x"
sanitized="$(mtk_sanitize "$hostile")"
_t_is "rm -rf whoamiid reboot echox" "$sanitized" "sanitize deletes shell metacharacters"
for meta in '$' '(' ')' '`' ';' '&' '|'; do
_t_is "" "${sanitized//[^"$meta"]/}" "sanitize leaves no '${meta}' behind"
done
_t_done "test_mtk_classify.sh"

View File

@ -1,152 +0,0 @@
#!/usr/bin/env bash
# Covers mtk_list_devices, mtk_device_state, mtk_select_device and
# mtk_require_authorized.
#
# The load-bearing case is REFUSING TO GUESS. With two phones attached and no
# --serial, mtk_select_device must fail rather than pick one: a wrong choice
# here points a partition-flashing toolkit at the wrong device. A version
# that took serials[0] would pass every other assertion in this file.
#
# adb is shimmed onto PATH rather than mocked in-process, because
# mtk_list_devices calls the binary directly. _t_adb_says writes the shim's
# canned `adb devices` output; nothing here talks to real hardware, and the
# shim refuses any subcommand the tests do not expect.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../mtk_common.sh
source "$HERE/../mtk_common.sh"
# --- adb shim --------------------------------------------------------------
# Narrow PATH to a shim dir. Every external these functions reach for must be
# seeded here: a missing one aborts the suite under `set -e` with no stderr.
_t_shim_dir="$T_TMP/bin"
mkdir -p "$_t_shim_dir"
cat >"$_t_shim_dir/adb" <<'SHIM'
#!/usr/bin/env bash
# Test double. Prints the canned `adb devices` table and nothing else; any
# other subcommand is a bug in the test, so say so loudly rather than
# returning an empty string that would read as "no devices".
if [[ ${1:-} == "devices" ]]; then
cat "${T_ADB_DEVICES:?}"
exit 0
fi
printf 'adb shim: unexpected invocation: %s\n' "$*" >&2
exit 97
SHIM
chmod +x "$_t_shim_dir/adb"
for _t_ext in awk mapfile cat printf; do
_t_bin="$(command -v "$_t_ext" 2>/dev/null || true)"
[[ -n $_t_bin ]] && ln -sf "$_t_bin" "$_t_shim_dir/$_t_ext"
done
PATH="$_t_shim_dir:/usr/bin:/bin"
export PATH
T_ADB_DEVICES="$T_TMP/adb_devices.txt"
export T_ADB_DEVICES
_t_adb_says() {
printf 'List of devices attached\n%s\n' "$1" >"$T_ADB_DEVICES"
}
# --- mtk_list_devices ------------------------------------------------------
_t_adb_says 'ABC123 device'
_t_is "ABC123 device" "$(mtk_list_devices)" "one attached device is listed with its state"
_t_adb_says 'ABC123 device
DEF456 unauthorized'
_t_is "2" "$(mtk_list_devices | wc -l)" "both attached devices are listed"
# A blank table is what an empty `adb devices` looks like.
_t_adb_says ''
_t_is "" "$(mtk_list_devices)" "no attached devices lists nothing"
# --- mtk_device_state ------------------------------------------------------
_t_adb_says 'ABC123 device
DEF456 unauthorized
GHI789 offline'
_t_is "device" "$(mtk_device_state ABC123)" "an authorized device reports 'device'"
_t_is "unauthorized" "$(mtk_device_state DEF456)" "an unauthorized device reports so"
_t_is "offline" "$(mtk_device_state GHI789)" "an offline device reports so"
_t_is "absent" "$(mtk_device_state NOPE000)" "an unattached serial reports 'absent'"
# --- mtk_select_device -----------------------------------------------------
_t_adb_says 'ABC123 device'
unset MTK_SERIAL
_t_rc 0 "a single attached device is selected" mtk_select_device
unset MTK_SERIAL
mtk_select_device
_t_is "ABC123" "$MTK_SERIAL" "the selected serial is the attached one"
# Two devices, no request: must refuse.
_t_adb_says 'ABC123 device
DEF456 device'
unset MTK_SERIAL
_t_rc 1 "two devices with no --serial refuses to guess" mtk_select_device
unset MTK_SERIAL
refuse_out="$(mtk_select_device 2>&1 || true)"
_t_in "refusing to guess" "$refuse_out" "the refusal says why"
_t_in "--serial" "$refuse_out" "the refusal names the way out"
# Two devices WITH a request: the named one is honoured.
unset MTK_SERIAL
mtk_select_device DEF456
_t_is "DEF456" "$MTK_SERIAL" "an explicit serial disambiguates"
# A requested serial that is not attached must fail, not fall back.
unset MTK_SERIAL
_t_rc 1 "a requested serial that is absent fails" mtk_select_device ZZZ999
unset MTK_SERIAL
_t_in "is not attached" "$(mtk_select_device ZZZ999 2>&1 || true)" \
"the absent-serial error names the problem"
# No devices at all.
_t_adb_says ''
unset MTK_SERIAL
_t_rc 1 "no attached devices fails" mtk_select_device
unset MTK_SERIAL
_t_in "No device visible" "$(mtk_select_device 2>&1 || true)" "the no-device error is actionable"
# --- mtk_require_authorized ------------------------------------------------
_t_adb_says 'ABC123 device
DEF456 unauthorized
GHI789 offline'
MTK_SERIAL=ABC123
_t_rc 0 "an authorized device passes the gate" mtk_require_authorized
# The case this function exists for: an unauthorized device answers every
# getprop with an empty string, which reads exactly like "no properties".
MTK_SERIAL=DEF456
_t_rc 1 "an unauthorized device is caught up front" mtk_require_authorized
_t_in "UNAUTHORIZED" "$(mtk_require_authorized 2>&1 || true)" \
"the unauthorized error blames the prompt, not the device"
MTK_SERIAL=GHI789
_t_rc 1 "an offline device is caught" mtk_require_authorized
_t_in "OFFLINE" "$(mtk_require_authorized 2>&1 || true)" "the offline error is actionable"
MTK_SERIAL=NOPE000
_t_rc 1 "an absent device is caught" mtk_require_authorized
# Under a fixture both selection and authorization are bypassed entirely,
# which is what keeps the rest of this directory's suites offline.
MTK_ROOT_FIXTURE="$(_t_fixture selectable '' 'boot')"
export MTK_ROOT_FIXTURE
unset MTK_SERIAL
_t_rc 0 "a fixture selects without adb" mtk_select_device
unset MTK_SERIAL
mtk_select_device
_t_is "fixture" "$MTK_SERIAL" "the fixture serial is a placeholder"
_t_rc 0 "a fixture is always authorized" mtk_require_authorized
_t_done "test_mtk_device.sh"

View File

@ -1,142 +0,0 @@
#!/usr/bin/env bash
# Covers discover_partitions, mtk_ramdisk_carrier,
# mtk_check_layout_expectation, mtk_sane_ramdisk_size and mtk_check_venv.
#
# The load-bearing case is INIT_BOOT WINS OVER BOOT. A device launched on
# Android 13+ carries the ramdisk in init_boot even though boot also exists,
# so a loop that checked boot first would pick the wrong partition on exactly
# the hardware this toolkit targets — and would do it silently, because both
# names are present and both reads succeed.
#
# The second is that discover_partitions consults the FILESYSTEM, never the
# API level. mtk_check_layout_expectation may disagree and warn, but it must
# not change the answer.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../mtk_common.sh
source "$HERE/../mtk_common.sh"
# --- discover_partitions ---------------------------------------------------
# A/B device with init_boot: the ramdisk lives in init_boot, not boot.
MTK_ROOT_FIXTURE="$(_t_fixture ab_initboot 'ro.boot.slot_suffix=_a' \
'boot_a
boot_b
init_boot_a
init_boot_b
vbmeta_a
vbmeta_b
userdata')"
export MTK_ROOT_FIXTURE
_t_rc 0 "discover_partitions succeeds when a ramdisk carrier exists" discover_partitions
discover_partitions
_t_is "A/B" "$MTK_AB_SCHEME" "an _a suffix in the listing means A/B"
_t_is "_a" "$MTK_SLOT_SUFFIX" "slot suffix comes from ro.boot.slot_suffix"
_t_is "init_boot_a" "$MTK_RAMDISK_PART" "init_boot wins over boot when both exist"
_t_is "vbmeta_a" "$MTK_VBMETA_PART" "vbmeta picks up the active slot suffix"
_t_is "init_boot" "$(mtk_ramdisk_carrier)" "carrier strips the slot suffix"
_t_is "0" "$MTK_SLOT_ASSUMED" "a reported slot suffix is not an assumption"
# A-only device with no init_boot: must fall through to boot.
MTK_ROOT_FIXTURE="$(_t_fixture aonly_boot '' \
'boot
recovery
vbmeta
userdata')"
discover_partitions
_t_is "A-only" "$MTK_AB_SCHEME" "no _a suffix means A-only"
_t_is "" "$MTK_SLOT_SUFFIX" "A-only has no slot suffix"
_t_is "boot" "$MTK_RAMDISK_PART" "falls through to boot when init_boot is absent"
_t_is "boot" "$(mtk_ramdisk_carrier)" "carrier of an unsuffixed boot is boot"
# A/B device whose slot_suffix is unreadable. The code falls back to _a
# because a dump has to start somewhere, but must FLAG that it guessed.
MTK_ROOT_FIXTURE="$(_t_fixture ab_no_suffix '' \
'boot_a
boot_b
vbmeta_a
vbmeta_b')"
discover_partitions
_t_is "_a" "$MTK_SLOT_SUFFIX" "A/B with no reported suffix falls back to _a"
_t_is "1" "$MTK_SLOT_ASSUMED" "the _a fallback is recorded as an assumption"
_t_is "boot_a" "$MTK_RAMDISK_PART" "assumed suffix still resolves a carrier"
# An empty listing is what an unauthorized or absent device produces. It must
# fail rather than reporting a plausible-looking empty layout.
MTK_ROOT_FIXTURE="$(_t_fixture no_partitions '' '')"
_t_rc 1 "discover_partitions fails on an empty by-name listing" discover_partitions
discover_partitions || true
_t_is "unknown" "$MTK_AB_SCHEME" "an empty listing yields an unknown scheme"
_t_is "" "$MTK_RAMDISK_PART" "an empty listing names no ramdisk partition"
# A listing with neither boot nor init_boot: non-empty, but no carrier.
MTK_ROOT_FIXTURE="$(_t_fixture no_carrier '' \
'system
vendor
userdata')"
_t_rc 1 "discover_partitions fails when no carrier is present" discover_partitions
# --- mtk_check_layout_expectation ------------------------------------------
# first_api_level >= 33 predicts init_boot. Agreement is quiet.
MTK_ROOT_FIXTURE="$(_t_fixture api34_initboot \
'ro.boot.slot_suffix=_a
ro.product.first_api_level=34' \
'boot_a
init_boot_a')"
discover_partitions
_t_rc 0 "api 34 + init_boot carrier agrees" mtk_check_layout_expectation
# Disagreement warns and returns 1 — but the device still wins.
MTK_ROOT_FIXTURE="$(_t_fixture api34_boot_only \
'ro.boot.slot_suffix=_a
ro.product.first_api_level=34' \
'boot_a
vbmeta_a')"
discover_partitions
_t_rc 1 "api 34 + boot carrier disagrees and says so" mtk_check_layout_expectation
_t_is "boot_a" "$MTK_RAMDISK_PART" "a disagreeing expectation does NOT change the answer"
warn_out="$(mtk_check_layout_expectation 2>&1 || true)"
_t_in "Trusting the device" "$warn_out" "the warning states the filesystem wins"
# Below 33 predicts boot.
MTK_ROOT_FIXTURE="$(_t_fixture api30_boot 'ro.product.first_api_level=30' 'boot')"
discover_partitions
_t_rc 0 "api 30 + boot carrier agrees" mtk_check_layout_expectation
# An unreadable or non-numeric api level is not an error — it is simply no
# cross-check. This must not be mistaken for a disagreement.
MTK_ROOT_FIXTURE="$(_t_fixture api_missing '' 'boot')"
discover_partitions
_t_rc 0 "an absent api level skips the cross-check" mtk_check_layout_expectation
# --- mtk_sane_ramdisk_size -------------------------------------------------
# Both an empty read and a whole-disk grab look like success to mtkclient's
# exit code, which is the entire reason this band exists.
_t_rc 1 "zero bytes is implausible" mtk_sane_ramdisk_size 0
_t_in "implausibly small" "$(mtk_sane_ramdisk_size 0 || true)" "small failure says which way"
_t_rc 1 "a truncated read is implausible" mtk_sane_ramdisk_size 1024
_t_rc 0 "8 MiB is a plausible ramdisk" mtk_sane_ramdisk_size $((8 * 1024 * 1024))
_t_rc 0 "the lower bound itself is accepted" mtk_sane_ramdisk_size $((1024 * 1024))
_t_rc 0 "the upper bound itself is accepted" mtk_sane_ramdisk_size $((128 * 1024 * 1024))
_t_rc 1 "a whole-disk grab is implausible" mtk_sane_ramdisk_size $((4 * 1024 * 1024 * 1024))
_t_in "implausibly large" "$(mtk_sane_ramdisk_size $((4 * 1024 * 1024 * 1024)) || true)" \
"large failure says which way"
# `stat` failing yields an empty string, not a number.
_t_rc 1 "a non-numeric size is rejected" mtk_sane_ramdisk_size "not-a-number"
_t_rc 1 "an empty size is rejected" mtk_sane_ramdisk_size ""
# --- mtk_check_venv --------------------------------------------------------
# MTK_ROOT_CACHE points at an empty tmpdir, so this takes the deterministic
# "missing" branch on any host, CI included.
_t_rc 1 "an absent venv is reported as missing" mtk_check_venv
_t_in "missing:" "$(mtk_check_venv || true)" "the missing branch names the interpreter path"
_t_done "test_mtk_partitions.sh"

View File

@ -1,222 +0,0 @@
#!/bin/bash
# ============================================================================
# 00-preflight.sh - Verify the host toolkit is intact. No device required.
#
# Run this first on the day the phone arrives, to confirm nothing rotted while
# it was in transit. Every check reports independently; the script exits
# non-zero if any FAIL, so it is usable as a gate.
#
# This checks the HOST only. It cannot confirm that udev actually grants
# access to a MediaTek device in BROM mode - that needs the hardware.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
readonly SCRIPT_NAME="${0##*/}"
# shellcheck source=../lib/mtk_common.sh
source "$SCRIPT_DIR/../lib/mtk_common.sh"
readonly UDEV_RULES_FILE="/etc/udev/rules.d/60-mtk-root.rules"
PASS=0
FAIL=0
WARN=0
pass() {
PASS=$((PASS + 1))
printf ' \033[0;32m[PASS]\033[0m %-34s %s\n' "$1" "${2:-}"
}
fail() {
FAIL=$((FAIL + 1))
printf ' \033[0;31m[FAIL]\033[0m %-34s %s\n' "$1" "${2:-}"
}
warn() {
WARN=$((WARN + 1))
printf ' \033[0;33m[WARN]\033[0m %-34s %s\n' "$1" "${2:-}"
}
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--help]
Verifies host tooling for the mtk_root toolkit. No device needed.
Exits non-zero if any check fails.
EOF
exit 0
}
check_tools() {
mtk_heading "Tools"
local -a required=(adb fastboot python3 git sha256sum)
local tool="" version=""
for tool in "${required[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
fail "$tool" "not installed - run install.sh"
continue
fi
# Invoke it, don't just locate it: a binary on PATH that cannot run is a
# failure mode `command -v` reports as success.
#
# Each capture is wrapped in `|| true` and reads the whole stream before
# slicing. Piping straight into `head -1` closes the pipe early, and under
# `set -o pipefail` the resulting SIGPIPE kills this script outright.
version=""
case "$tool" in
adb) version="$(adb version 2>/dev/null || true)" ;;
fastboot) version="$(fastboot --version 2>/dev/null || true)" ;;
python3) version="$(python3 -V 2>&1 || true)" ;;
git) version="$(git --version 2>/dev/null || true)" ;;
*) version="$("$tool" --version 2>/dev/null || true)" ;;
esac
version="${version%%$'\n'*}"
if [[ -z $version ]]; then
fail "$tool" "present but did not respond to a version query"
else
pass "$tool" "$version"
fi
done
}
check_groups() {
mtk_heading "Group membership"
local -a wanted=(plugdev adbusers)
local group="" active_groups=""
active_groups=" $(id -nG) "
for group in "${wanted[@]}"; do
if ! getent group "$group" >/dev/null 2>&1; then
warn "$group" "group does not exist on this system"
continue
fi
if [[ $active_groups == *" $group "* ]]; then
pass "$group" "active in this session"
elif id -nG "$USER" 2>/dev/null | grep -qw "$group"; then
# Membership that the current login session predates. The rule is real
# but will not take effect until re-login, and USB access will fail in
# a way that looks like a udev problem.
warn "$group" "member, but not in this session - log out and back in"
else
fail "$group" "not a member - run install.sh"
fi
done
}
check_udev() {
mtk_heading "udev rules"
if [[ -f $UDEV_RULES_FILE ]]; then
if command -v udevadm >/dev/null 2>&1 && udevadm verify --help >/dev/null 2>&1; then
# A real syntax verdict. `udevadm control --reload` exits 0 regardless,
# so it cannot be used to validate anything.
if udevadm verify "$UDEV_RULES_FILE" >/dev/null 2>&1; then
pass "60-mtk-root.rules" "present, udevadm verify clean"
else
fail "60-mtk-root.rules" "present but udevadm verify REJECTED it"
fi
else
warn "60-mtk-root.rules" "present; udevadm verify unavailable, syntax unchecked"
fi
else
fail "60-mtk-root.rules" "missing - run install.sh"
fi
# A wildcard-vendor world-writable rule is a genuine finding, not a nit: it
# applies to every USB device attached to this machine, not just phones.
local catchall=""
catchall="$(mtk_udev_catchall_files)"
if [[ -n $catchall ]]; then
fail "no world-writable catch-all" "found in: $(tr '\n' ' ' <<<"$catchall")"
printf ' A rule matching idVendor=="*" with MODE="0666" grants every\n'
printf ' local process read/write access to EVERY USB device, including\n'
printf ' storage and security keys. Narrow it: install.sh --fix-udev\n'
else
pass "no world-writable catch-all" "no wildcard 0666 rule found"
fi
}
check_venv() {
mtk_heading "mtkclient"
local venv_status=""
if venv_status="$(mtk_check_venv)"; then
pass "venv importable" "$venv_status"
else
fail "venv importable" "$venv_status"
printf ' The venv existing is not enough - it must be able to import\n'
printf ' usb/serial. A Python upgrade orphans a venv while leaving it\n'
printf ' looking complete. Rebuild: install.sh\n'
fi
local clone_dir=""
clone_dir="$(mtk_mtkclient_dir)"
if [[ -d "$clone_dir/.git" ]]; then
local rev=""
rev="$(git -C "$clone_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)"
pass "mtkclient checkout" "rev ${rev}"
else
fail "mtkclient checkout" "not a git clone at ${clone_dir} - run install.sh"
fi
}
check_cache() {
mtk_heading "Workspace"
local cache_dir=""
cache_dir="$(mtk_cache_dir)"
if [[ ! -d $cache_dir ]]; then
fail "cache dir" "missing: ${cache_dir} - run install.sh"
return
fi
if [[ -w $cache_dir ]]; then
pass "cache dir writable" "$cache_dir"
else
fail "cache dir writable" "not writable: ${cache_dir}"
fi
}
main() {
printf '\033[1m%s - host readiness for the mtk_root toolkit\033[0m\n' "$SCRIPT_NAME"
check_tools
check_groups
check_udev
check_venv
check_cache
mtk_heading "Result"
printf ' %d passed, %d failed, %d warnings\n\n' "$PASS" "$FAIL" "$WARN"
if [[ $FAIL -gt 0 ]]; then
printf '\033[0;31mNOT READY\033[0m - resolve the failures above (usually: ./install.sh)\n'
return 1
fi
if [[ $WARN -gt 0 ]]; then
printf '\033[0;33mREADY, with warnings\033[0m - read them before trusting a USB connection.\n'
return 0
fi
printf '\033[0;32mHOST READY\033[0m - udev access to a real MediaTek device remains unverified\n'
printf 'until hardware is attached; that cannot be checked without the phone.\n'
return 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help) usage ;;
*)
mtk_error "Unknown option: $1"
exit 1
;;
esac
done
main

View File

@ -1,246 +0,0 @@
#!/bin/bash
# ============================================================================
# 10-recon.sh - Read every fact needed to choose a rooting path. READ-ONLY.
#
# This script never writes to the device. It issues no reboot, no fastboot
# command, no `settings put`, and touches no partition. tests/run_tests.sh
# enforces that with a static scan of this file, because the phone it will
# most often be pointed at is a daily driver.
#
# Output: a facts file under the cache dir, plus a decision summary naming
# which of the four paths applies and what is still unknown.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
readonly SCRIPT_NAME="${0##*/}"
# shellcheck source=../lib/mtk_common.sh
source "$SCRIPT_DIR/../lib/mtk_common.sh"
# Properties captured verbatim into the facts file. Grouped by what they tell
# us; all are read-only.
readonly -a FACT_PROPS=(
ro.product.manufacturer
ro.product.model
ro.product.name
ro.product.device
ro.product.first_api_level
ro.board.platform
ro.hardware
ro.build.fingerprint
ro.build.id
ro.build.version.release
ro.build.version.sdk
ro.build.version.security_patch
ro.build.type
ro.boot.slot_suffix
ro.boot.flash.locked
ro.boot.verifiedbootstate
ro.boot.vbmeta.device_state
ro.boot.veritymode
ro.boot.hardware
ro.boot.bootloader
ro.bootloader
ro.oem_unlock_supported
sys.oem_unlock_allowed
ro.carrier
gsm.sim.operator.alpha
ro.crypto.state
)
REQUESTED_SERIAL=""
EXPLAIN_ONLY=0
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--serial <serial>] [--explain-classification] [--help]
Reads device facts read-only and prints a path decision summary.
Writes a facts file to \$(mtk_cache_dir)/device-facts-<class>-<serial>-<ts>.txt
--serial <serial> Target a specific device (required if >1 attached)
--explain-classification Show why the device classified as it did, then exit
--help This text
Environment:
MTK_ROOT_FIXTURE=<dir> Read from fixture files instead of a device (tests)
EOF
exit 0
}
# ---------------------------------------------------------------------------
# Unlock / lock signal interpretation
# ---------------------------------------------------------------------------
# carrier_lock_verdict
#
# Deliberately hardcoded to UNDETERMINED. There is no read-only property that
# distinguishes "carrier-locked" from "the OEM unlocking toggle is simply off":
# - sys.oem_unlock_allowed mirrors the toggle, so it reads empty/0 in both cases
# - ro.oem_unlock_supported is not populated on Pixel devices at all
# - settings get global oem_unlock_allowed is null until the toggle is used
# Printing a negative here would wrongly rule out a whole path, so the script
# reports the raw signals and refuses to draw the conclusion.
carrier_lock_verdict() {
printf 'UNDETERMINED'
}
read_unlock_signals() {
UNLOCK_FLASH_LOCKED="$(dev_getprop ro.boot.flash.locked)"
UNLOCK_VBSTATE="$(dev_getprop ro.boot.verifiedbootstate)"
UNLOCK_DEVICE_STATE="$(dev_getprop ro.boot.vbmeta.device_state)"
UNLOCK_SYS_ALLOWED="$(dev_getprop sys.oem_unlock_allowed)"
UNLOCK_OEM_SUPPORTED="$(dev_getprop ro.oem_unlock_supported)"
UNLOCK_SETTING=""
if [[ -z ${MTK_ROOT_FIXTURE:-} ]]; then
UNLOCK_SETTING="$(mtk_adb shell settings get global oem_unlock_allowed 2>/dev/null | tr -d '\r' || true)"
fi
# The toggle's own state is the thing that matters for unlockability, and it
# is precisely the thing that cannot be read reliably. Track it as tri-state.
case "$UNLOCK_SYS_ALLOWED" in
1) UNLOCK_TOGGLE="ON" ;;
0) UNLOCK_TOGGLE="OFF" ;;
*) UNLOCK_TOGGLE="UNREADABLE" ;;
esac
}
# ---------------------------------------------------------------------------
# Facts file
# ---------------------------------------------------------------------------
write_facts_file() {
local out_path="$1"
local tmp_file="" prop="" value=""
tmp_file="$(mktemp)"
# shellcheck disable=SC2064 # expand tmp_file now; it must survive this scope
trap "rm -f '$tmp_file'" RETURN
{
printf '# mtk_root device facts\n'
printf '# generated: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf '# serial: %s\n' "$MTK_SERIAL"
printf '# class: %s\n' "$MTK_DEVICE_CLASS"
printf '#\n'
printf '# This file is read by 20-dump-stock.sh, and is the source for the\n'
printf '# tests/fixtures/ entries. Format is deliberately line-oriented.\n'
printf '\n[properties]\n'
for prop in "${FACT_PROPS[@]}"; do
value="$(dev_getprop "$prop")"
printf '%s=%s\n' "$prop" "$value"
done
printf '\n[settings]\n'
printf 'global.oem_unlock_allowed=%s\n' "${UNLOCK_SETTING:-}"
printf '\n[partitions]\n'
printf 'ab_scheme=%s\n' "$MTK_AB_SCHEME"
printf 'slot_suffix=%s\n' "${MTK_SLOT_SUFFIX:-}"
printf 'slot_suffix_assumed=%s\n' "${MTK_SLOT_ASSUMED:-0}"
printf 'ramdisk_partition=%s\n' "${MTK_RAMDISK_PART:-}"
printf 'ramdisk_carrier=%s\n' "$(mtk_ramdisk_carrier)"
printf 'vbmeta_partition=%s\n' "${MTK_VBMETA_PART:-}"
printf '\n[by-name]\n'
printf '%s\n' "$MTK_BY_NAME"
} >"$tmp_file"
mv "$tmp_file" "$out_path"
trap - RETURN
printf '%s' "$out_path"
}
# shellcheck source=lib/recon_report.sh
source "$SCRIPT_DIR/lib/recon_report.sh"
main() {
printf '\033[1m%s - read-only device recon\033[0m\n' "$SCRIPT_NAME"
if ! mtk_select_device "$REQUESTED_SERIAL"; then
exit 1
fi
# Before any getprop: an unauthorized device answers every property query
# with an empty string, which is indistinguishable from "device has no such
# property" and would produce a confidently wrong report.
if ! mtk_require_authorized; then
exit 1
fi
mtk_info "Target: ${MTK_SERIAL}"
classify_device
if [[ $EXPLAIN_ONLY -eq 1 ]]; then
mtk_explain_classification
exit 0
fi
mtk_heading "Identity"
printf ' serial : %s\n' "$MTK_SERIAL"
printf ' manufacturer : %s\n' "${MTK_PROP_MANUFACTURER:-<unset>}"
printf ' model : %s\n' "${MTK_PROP_MODEL:-<unset>}"
printf ' platform : %s / %s\n' "${MTK_PROP_PLATFORM:-<unset>}" "${MTK_PROP_HARDWARE:-<unset>}"
printf ' classified as : %s\n' "$MTK_DEVICE_CLASS"
printf ' because : %s\n' "$MTK_CLASS_REASON"
if [[ $MTK_DEVICE_CLASS == "UNKNOWN" ]]; then
mtk_error "Device did not match any known profile - refusing to report a path."
report_decision
exit 1
fi
if ! discover_partitions; then
mtk_error "Could not read /dev/block/by-name/ - no partition facts available."
mtk_error "On most devices that listing needs no root; if it is empty, the"
mtk_error "device may be in an unusual state. Do not flash anything."
exit 1
fi
read_unlock_signals
report_partitions
report_lock_state
report_manual_checks
local cache_dir="" facts_path=""
cache_dir="$(mtk_cache_dir)"
mkdir -p "$cache_dir"
facts_path="${cache_dir}/device-facts-${MTK_DEVICE_CLASS}-${MTK_SERIAL}-$(date -u '+%Y%m%dT%H%M%SZ').txt"
write_facts_file "$facts_path" >/dev/null
report_decision
mtk_heading "Facts file"
printf ' %s\n' "$facts_path"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--serial)
[[ $# -ge 2 ]] || {
mtk_error "--serial needs a value"
exit 1
}
REQUESTED_SERIAL="$2"
shift 2
;;
--explain-classification)
EXPLAIN_ONLY=1
shift
;;
-h | --help) usage ;;
*)
mtk_error "Unknown option: $1"
exit 1
;;
esac
done
main

View File

@ -1,242 +0,0 @@
#!/bin/bash
# ============================================================================
# 20-dump-stock.sh - Dump the stock ramdisk + vbmeta off a MediaTek device.
#
# This is the reason the Ulefone firmware is not pre-downloaded: the stock
# images come off the device itself, so they match this exact unit and build.
#
# Reading requires mtkclient and BROM/preloader mode - the phone must be
# powered OFF and plugged in while mtkclient waits. Reading a partition does
# not modify it, but this is the first script here that talks to the bootrom,
# so it refuses loudly rather than proceeding on a guess.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
readonly SCRIPT_NAME="${0##*/}"
# shellcheck source=../lib/mtk_common.sh
source "$SCRIPT_DIR/../lib/mtk_common.sh"
REQUESTED_SERIAL=""
ASSUME_YES=0
FORCE=0
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--serial <serial>] [--yes] [--force] [--help]
Dumps the stock ramdisk-carrying partition and vbmeta from a MediaTek device
using mtkclient, then records sizes and sha256 sums.
--serial <serial> Target device (required if more than one is attached)
--yes Skip the confirmation prompt
--force Re-dump even if a complete dump already exists
--help This text
Refuses unless the device is positively identified as MediaTek. Never runs
against a Pixel or an unclassified device.
EOF
exit 0
}
# find_latest_facts <serial>
# The ramdisk partition name is read from the recon facts file rather than
# re-derived here. One source of truth, and it guarantees a dump can only ever
# target a partition that a human has already seen in a recon report.
find_latest_facts() {
local serial="$1" cache_dir=""
cache_dir="$(mtk_cache_dir)"
# A missing cache dir is an ordinary "no recon has run yet", not an error.
# Without the guard, `find` on a nonexistent path fails under `set -e` and
# the script exits silently before it can explain what to do instead.
[[ -d $cache_dir ]] || return 0
find "$cache_dir" -maxdepth 1 -name "device-facts-*-${serial}-*.txt" 2>/dev/null |
sort | tail -n 1
}
read_facts_value() {
local facts="$1" key="$2" line=""
line="$(grep -m1 -- "^${key}=" "$facts" 2>/dev/null || true)"
printf '%s' "${line#*=}"
}
# shellcheck source=lib/dump_partitions.sh
source "$SCRIPT_DIR/lib/dump_partitions.sh"
# shellcheck source=lib/dump_verify.sh
source "$SCRIPT_DIR/lib/dump_verify.sh"
main() {
printf '\033[1m%s - stock partition dump (MediaTek only)\033[0m\n' "$SCRIPT_NAME"
if ! mtk_select_device "$REQUESTED_SERIAL"; then
exit 1
fi
if [[ -z ${MTK_ROOT_FIXTURE:-} ]] && ! mtk_require_authorized; then
exit 1
fi
classify_device
# Positive assertion, in two independent parts. Deliberately never phrased
# as "not a Pixel": an unrecognised third device must fail here too.
if [[ $MTK_DEVICE_CLASS != "ULEFONE" ]]; then
mtk_error "Device classified as ${MTK_DEVICE_CLASS}; refusing to dump."
mtk_error "Reason: ${MTK_CLASS_REASON}"
mtk_error "This script only ever runs against a MediaTek Ulefone target."
exit 1
fi
if ! mtk_assert_mediatek; then
mtk_error "No MediaTek platform signal on this device; refusing to dump."
mtk_error "platform='${MTK_PROP_PLATFORM:-}' hardware='${MTK_PROP_HARDWARE:-}'"
exit 1
fi
local facts=""
facts="$(find_latest_facts "$MTK_SERIAL")"
if [[ -z $facts ]]; then
mtk_error "No recon facts file found for ${MTK_SERIAL}."
mtk_error "Run ./10-recon.sh --serial ${MTK_SERIAL} first - this script"
mtk_error "reads the partition names from that report rather than guessing."
exit 1
fi
mtk_info "Using facts: ${facts}"
local ramdisk_part="" vbmeta_part="" carrier=""
ramdisk_part="$(read_facts_value "$facts" ramdisk_partition)"
vbmeta_part="$(read_facts_value "$facts" vbmeta_partition)"
carrier="$(read_facts_value "$facts" ramdisk_carrier)"
if [[ -z $ramdisk_part ]]; then
mtk_error "Facts file records no ramdisk partition. Re-run 10-recon.sh."
exit 1
fi
# Skipped when the dump command is overridden, since mtkclient is then not
# the thing doing the reading.
if [[ -z ${MTK_DUMP_CMD:-} ]]; then
local venv_status=""
if ! venv_status="$(mtk_check_venv)"; then
mtk_error "mtkclient venv unusable: ${venv_status}"
mtk_error "Run ./install.sh, then ./00-preflight.sh."
exit 1
fi
fi
local out_dir=""
out_dir="$(mtk_stock_dir)/${MTK_SERIAL}"
if [[ -f "${out_dir}/manifest.txt" && $FORCE -eq 0 ]]; then
mtk_info "A dump already exists at ${out_dir}"
mtk_info "Re-run with --force to replace it. Existing manifest:"
cat "${out_dir}/manifest.txt"
exit 0
fi
mtk_heading "About to dump"
printf ' device : %s (%s)\n' "$MTK_SERIAL" "${MTK_PROP_MODEL:-}"
printf ' ramdisk : %s (carrier: %s)\n' "$ramdisk_part" "${carrier:-unknown}"
printf ' vbmeta : %s\n' "${vbmeta_part:-<none found>}"
printf ' output : %s\n' "$out_dir"
printf '\n Reading is non-destructive, but it needs BROM/preloader mode.\n'
if [[ $ASSUME_YES -eq 0 ]]; then
local reply=""
read -r -p " Proceed? [y/N] " reply
if [[ ${reply,,} != "y" ]]; then
mtk_info "Aborted by user."
exit 0
fi
fi
mkdir -p "$out_dir"
local failures=0
local ramdisk_file="${out_dir}/${ramdisk_part}.img"
if dump_partition "$ramdisk_part" "$ramdisk_file"; then
verify_dump "$ramdisk_part" "$ramdisk_file" || failures=$((failures + 1))
else
mtk_error "mtkclient failed to read ${ramdisk_part}"
failures=$((failures + 1))
fi
if [[ -n $vbmeta_part ]]; then
local vbmeta_file="${out_dir}/${vbmeta_part}.img"
if dump_partition "$vbmeta_part" "$vbmeta_file"; then
# vbmeta is legitimately far smaller than a ramdisk carrier, so the
# ramdisk size band does not apply; record it without a band check.
if [[ -f $vbmeta_file ]]; then
mtk_info "${vbmeta_part}: $(mtk_file_size "$vbmeta_file") bytes, sha256 $(mtk_sha256 "$vbmeta_file")"
else
mtk_error "${vbmeta_part}: no file produced"
failures=$((failures + 1))
fi
else
mtk_error "mtkclient failed to read ${vbmeta_part}"
failures=$((failures + 1))
fi
fi
# Manifest is plain text on purpose: it is the only artifact here that may
# be committed. The images themselves stay in the cache dir.
{
printf '# mtk_root stock dump manifest\n'
printf 'generated=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf 'serial=%s\n' "$MTK_SERIAL"
printf 'model=%s\n' "${MTK_PROP_MODEL:-}"
printf 'fingerprint=%s\n' "$(read_facts_value "$facts" ro.build.fingerprint)"
printf 'ramdisk_carrier=%s\n' "${carrier:-}"
printf '\n'
local f=""
for f in "$out_dir"/*.img; do
[[ -f $f ]] || continue
printf '%s %s %s bytes\n' "$(mtk_sha256 "$f")" "${f##*/}" "$(mtk_file_size "$f")"
done
} >"${out_dir}/manifest.txt"
mtk_heading "Result"
cat "${out_dir}/manifest.txt"
if [[ $failures -gt 0 ]]; then
mtk_error "${failures} problem(s) above. Do NOT treat this as a usable backup."
exit 1
fi
mtk_info "Stock images saved to ${out_dir}"
mtk_info "Back these up off this machine before flashing anything."
}
while [[ $# -gt 0 ]]; do
case "$1" in
--serial)
[[ $# -ge 2 ]] || {
mtk_error "--serial needs a value"
exit 1
}
REQUESTED_SERIAL="$2"
shift 2
;;
--yes)
ASSUME_YES=1
shift
;;
--force)
FORCE=1
shift
;;
-h | --help) usage ;;
*)
mtk_error "Unknown option: $1"
exit 1
;;
esac
done
main

View File

@ -1,192 +0,0 @@
#!/bin/bash
# ============================================================================
# 90-verify-root.sh - Post-flash checks, reported one by one.
#
# There is deliberately NO aggregate "success" verdict. Root can be partially
# working in ways that matter: su present but Magisk's daemon absent, or the
# ramdisk flashed while vbmeta verification still rejects it. A single
# pass/fail hides exactly the case worth knowing about.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
readonly SCRIPT_NAME="${0##*/}"
# shellcheck source=../lib/mtk_common.sh
source "$SCRIPT_DIR/../lib/mtk_common.sh"
# Known-current Magisk at the time of writing. Checked so a stale APK is
# noticed; a mismatch is informational, not a failure.
readonly MAGISK_KNOWN_VERSION="30.7"
REQUESTED_SERIAL=""
PASS=0
FAIL=0
INFO=0
check_pass() {
PASS=$((PASS + 1))
printf ' \033[0;32m[PASS]\033[0m %-28s %s\n' "$1" "${2:-}"
}
check_fail() {
FAIL=$((FAIL + 1))
printf ' \033[0;31m[FAIL]\033[0m %-28s %s\n' "$1" "${2:-}"
}
check_info() {
INFO=$((INFO + 1))
printf ' \033[0;34m[INFO]\033[0m %-28s %s\n' "$1" "${2:-}"
}
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--serial <serial>] [--help]
Runs post-flash root checks and reports each independently.
Read-only: queries the device, changes nothing.
EOF
exit 0
}
check_su() {
local result=""
result="$(mtk_adb shell 'su -c id' 2>/dev/null | tr -d '\r' || true)"
if [[ $result == *"uid=0"* ]]; then
check_pass "su grants uid=0" "$result"
return 0
fi
# An unanswered Superuser prompt looks identical to no root at all.
check_fail "su grants uid=0" "got: ${result:-<no output>}"
printf ' If a Superuser prompt is waiting on the phone, grant it and re-run.\n'
return 1
}
check_magisk() {
local version="" version_code=""
version="$(mtk_adb shell 'su -c "magisk -c"' 2>/dev/null | tr -d '\r' || true)"
version_code="$(mtk_adb shell 'su -c "magisk -V"' 2>/dev/null | tr -d '\r' || true)"
if [[ -z $version && -z $version_code ]]; then
check_fail "magisk binary responds" "no output from 'magisk -c'"
return 1
fi
check_pass "magisk binary responds" "${version} (code ${version_code})"
if [[ $version == *"$MAGISK_KNOWN_VERSION"* ]]; then
check_info "magisk version" "matches expected v${MAGISK_KNOWN_VERSION}"
else
check_info "magisk version" "device has '${version}', toolkit expected v${MAGISK_KNOWN_VERSION}"
fi
return 0
}
check_daemon() {
local result=""
result="$(mtk_adb shell 'su -c "magisk --ping"' 2>/dev/null | tr -d '\r' || true)"
if [[ $result == "Pong" || $result == "pong" ]]; then
check_pass "magisk daemon running" "$result"
return 0
fi
check_fail "magisk daemon running" "ping returned: ${result:-<nothing>}"
return 1
}
check_boot_state() {
local vbstate="" locked=""
vbstate="$(dev_getprop ro.boot.verifiedbootstate)"
locked="$(dev_getprop ro.boot.flash.locked)"
# After unlocking, verified boot should no longer report green. Still-green
# alongside a claim of successful flashing means the flash did not take.
case "$vbstate" in
orange | yellow)
check_pass "verified boot state" "${vbstate} (expected after unlock)"
;;
green)
check_fail "verified boot state" "green - bootloader still locked, flash did not take"
;;
*)
check_info "verified boot state" "${vbstate:-<unset>}"
;;
esac
if [[ $locked == "0" ]]; then
check_pass "bootloader unlocked" "ro.boot.flash.locked=0"
elif [[ $locked == "1" ]]; then
check_fail "bootloader unlocked" "ro.boot.flash.locked=1 - still locked"
else
check_info "bootloader unlocked" "ro.boot.flash.locked=${locked:-<unset>}"
fi
}
check_ramdisk_changed() {
local manifest="" carrier=""
manifest="$(mtk_stock_dir)/${MTK_SERIAL}/manifest.txt"
if [[ ! -f $manifest ]]; then
check_info "stock manifest" "none at ${manifest} - cannot compare against stock"
return 0
fi
carrier="$(grep -m1 '^ramdisk_carrier=' "$manifest" 2>/dev/null || true)"
check_info "stock manifest" "present (${carrier#*=}) - compare hashes manually before relocking"
}
main() {
printf '\033[1m%s - post-flash verification\033[0m\n' "$SCRIPT_NAME"
if ! mtk_select_device "$REQUESTED_SERIAL"; then
exit 1
fi
if ! mtk_require_authorized; then
exit 1
fi
classify_device
mtk_info "Target: ${MTK_SERIAL} (${MTK_DEVICE_CLASS})"
mtk_heading "Checks"
# Each runs regardless of earlier failures - a partial result is the point.
check_su || true
check_magisk || true
check_daemon || true
check_boot_state
check_ramdisk_changed
mtk_heading "Result"
printf ' %d passed, %d failed, %d informational\n\n' "$PASS" "$FAIL" "$INFO"
printf ' Read the checks individually. There is no combined verdict here on\n'
printf ' purpose: partial root is a real state and it matters which part failed.\n\n'
printf ' Play Integrity / SafetyNet is deliberately NOT checked - it needs\n'
printf ' Zygisk plus a hiding config, and is a separate exercise.\n'
[[ $FAIL -eq 0 ]]
}
while [[ $# -gt 0 ]]; do
case "$1" in
--serial)
[[ $# -ge 2 ]] || {
mtk_error "--serial needs a value"
exit 1
}
REQUESTED_SERIAL="$2"
shift 2
;;
-h | --help) usage ;;
*)
mtk_error "Unknown option: $1"
exit 1
;;
esac
done
main

View File

@ -1,107 +0,0 @@
# mtk_root — build notes, surprises, and mistakes
Written 2026-08-04, while preparing for a Ulefone Armor X12 Pro that had not
yet arrived.
## Installed versions (this host, 2026-08-04)
| Tool | Version |
| ----------------- | -------------------------------- |
| adb | 1.0.41 (android-tools) |
| fastboot | 37.0.0-android-tools |
| python3 | 3.14.6 |
| shellcheck | present |
| shfmt | 3.13.1 |
| systemd / udevadm | 261 (`udevadm verify` available) |
| mtkclient | rev `0542a87`, 2026-08-02 |
| Magisk | v30.7 |
## Magisk provenance
Kept here as well as in `~/.cache/mtk-root/stock/README.md`, because the cache
is not version-controlled and vanishes with a cleanup.
| field | value |
| ------- | ------------------------------------------------------------------ |
| version | v30.7 (released 2026-02-23) |
| asset | `Magisk-v30.7.apk` |
| sha256 | `e0d32d2123532860f97123d927b1bb86c4e08e6fd8a48bfc6b5bee0afae9ebd5` |
| size | 11613864 bytes |
**Trap:** the same GitHub release also publishes **`app-debug.apk`** (~25 MB),
and a naive "first .apk asset" match picks it. That is the debug build, not the
one to install. I hit this during the fetch. Match the filename exactly.
Re-check before use:
```bash
curl -fsSL https://api.github.com/repos/topjohnwu/Magisk/releases/latest \
| grep -E '"(tag_name|browser_download_url)"'
```
## Surprises
**1. The Ulefone is probably the `init_boot` branch, not `boot`.**
The Armor X12 Pro launched on Android 13 (Aug 2023). Per AOSP, devices
_launching_ on 13+ carry the ramdisk in `init_boot`, so the Ulefone is likely
the opposite branch from the Pixel used to validate this toolkit. The Pixel run
therefore does **not** exercise the code path the Ulefone will need. Fixtures
cover it; hardware has not.
**2. The mtkclient venv on this host was broken, not missing.**
`~/.cache/bl9000-root/mtkclient/venv` existed and looked complete, but every
`import usb` failed under Python 3.14 — a Python upgrade had orphaned it. This
is why `00-preflight.sh` tests _importability_ rather than directory existence:
the failure mode that costs an evening is the one that looks fine on disk.
**3. `/etc/udev/rules.d/51-android.rules` had a world-writable catch-all.**
`ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666"` grants every local
process read/write access to _every_ USB device — storage, security keys, not
just phones. Commented out on 2026-08-04 via `install.sh --fix-udev`; backup at
`51-android.rules.bak-20260804T183038Z`. adb connectivity was re-verified
afterwards. Note `51-pinephone.rules` still sets `MODE="0666"`, but scoped to
specific devices rather than a wildcard.
**4. SIGPIPE killed the preflight script.**
`adb version | head -1` under `set -o pipefail` exits 141 when `head` closes the
pipe. The script aborted mid-run on its very first execution. Version strings
are now captured whole and sliced with parameter expansion.
**5. A test fix silently punched a hole in the read-only guard.**
To stop the static scan tripping on the word "fastboot" inside `printf` text, I
first stripped _all_ quoted strings — which also hid
`mtk_adb shell "reboot bootloader"` from every check. The guard protecting a
daily-driver phone was weaker after the "fix" than before it. Now only the
arguments of `printf`/`echo`/`cat` are stripped, and
`test_read_only_scan_catches_injection` asserts the scan actually fails on
injected commands.
**6. `20-dump-stock.sh` exited silently when no recon had run.**
`find` on a nonexistent cache dir fails under `set -e`, so the script printed
its title and stopped with no explanation. Guarded; it now says which command
to run first.
## Assumptions that need checking against the real device
- **Ulefone property values are guessed.** `ro.product.manufacturer=Ulefone`,
`ro.product.model=Armor X12 Pro`, `ro.board.platform=mt6765`. If the unit
reports otherwise, `classify_device` returns `UNKNOWN` and everything
refuses — safe, but it will look like a bug. Run
`./10-recon.sh --explain-classification`, then widen the patterns at the top
of `../lib/mtk_common.sh`.
- **Path semantics A/B/C/D are inferred.** The companion document defining them
was never provided, so `10-recon.sh` prints an assumption banner every run.
- **The Ulefone USB vendor ID is unknown.** `udev/60-mtk-root.rules` covers
MediaTek `0e8d`; if the device enumerates under a Ulefone-specific VID, add
it (`lsusb` with the phone attached).
- **mtkclient's install steps may drift.** `install.sh` falls back to
`pip install <clone>` when `requirements.txt` is absent, but read the current
README if the venv build ever fails.
## Verified good news
mtkclient rev `0542a87` has an explicit **MT6765** entry in
`mtkclient/config/brom_config.py` (`name="MT6765/MT8768t"`, loader
`mt6765_payload.bin`) — the Ulefone's exact chipset is supported. This does
**not** establish that this particular unit's BROM is accessible: SLA/DAA
authentication state is per-device and undocumented.

View File

@ -1,96 +0,0 @@
# mtk_root
Host tooling for rooting a MediaTek Android device: preflight, read-only
recon, stock partition dump, and post-flash verification.
Built ahead of a Ulefone Armor X12 Pro (MediaTek Helio G36 / MT6765) arriving,
and validated read-only against a Pixel 6a.
**There is no flashing script, deliberately.** Automating irreversible
partition writes would defeat the confirmation steps that catch a wrong
partition name or a missing slot suffix. Flashing stays manual.
## Scripts
| Script | Device needed | What it does |
| ------------------- | ------------- | ------------------------------------------------------------------------------- |
| `install.sh` | no | Installs deps, clones mtkclient + builds its venv, installs narrowed udev rules |
| `00-preflight.sh` | no | Verifies the host is intact. Run this first when the phone arrives |
| `10-recon.sh` | yes | **Read-only.** Reads every fact needed to pick a path, writes a facts file |
| `20-dump-stock.sh` | yes | MediaTek only. Dumps the stock ramdisk + vbmeta via mtkclient |
| `90-verify-root.sh` | yes | Post-flash checks, reported one by one |
## Usage
```bash
./install.sh # once, or after a Python upgrade
./00-preflight.sh # confirm the host is ready
./10-recon.sh # read the device, decide the path
./20-dump-stock.sh # Ulefone only, after recon
# ... patch with Magisk and flash MANUALLY ...
./90-verify-root.sh # check what actually worked
```
Add `--serial <serial>` to any device-facing script when more than one phone
is attached; they refuse to guess rather than picking one.
## Design notes
**Partition discovery never hardcodes `boot` vs `init_boot`.** It reads
`/dev/block/by-name/`, derives the slot scheme from observed `_a` suffixes, and
picks the ramdisk carrier by preference order `init_boot` → `boot` over what is
actually present. Per AOSP, devices launching on Android 13+ carry the ramdisk
in `init_boot`; earlier devices keep it in `boot` even after upgrading. That
rule explains why the preference order is right, but the code consults the
filesystem, not the API level — so one code path serves both a Pixel (no
`init_boot`, falls through to `boot_a`) and a 13-launch MediaTek device
(`init_boot_a` taken). Patching the wrong one produces a phone that will not
boot.
**Classification refuses rather than guesses.** `ULEFONE` requires a positive
MediaTek platform signal _and_ a vendor/model match. There is no "not a Pixel,
so presumably the Ulefone" fallback: a third phone attached by accident lands
in `UNKNOWN` and every caller stops.
**Carrier lock is reported as `UNDETERMINED`, always.** No readable property
distinguishes carrier-locked from "the OEM unlocking toggle is simply off":
`sys.oem_unlock_allowed` mirrors the toggle, `ro.oem_unlock_supported` is not
populated on Pixels, and `settings get global oem_unlock_allowed` is null until
the toggle is used. Asserting a negative here would wrongly rule out a path, so
the script reports the raw signals and refuses to conclude. Settle it out of
band via an IMEI check.
**`10-recon.sh` is read-only and the tests enforce it.** `tests/run_tests.sh`
statically scans it for `fastboot`, `magiskboot`, `adb reboot/push/install`,
`settings put`, `dd`, `mkfs` and `erase`, and separately asserts the scan
_fails_ when such a command is injected into a copy — a guard never observed
failing is not known to work.
## Artifacts
Images, APKs and facts files go to `~/.cache/mtk-root/`, never into the repo:
the pre-commit hook rejects all binaries, and a factory image runs to several
GB. Only text manifests belong in git.
Ulefone firmware is deliberately **not** pre-fetched — the stock ramdisk is
dumped off the device itself, so it matches that exact unit and build.
## Tests
```bash
./tests/run_tests.sh
```
58 assertions, fully offline. Fixtures are `props.txt` + `by-name.txt`
directories fed in via `MTK_ROOT_FIXTURE`; `MTK_DUMP_CMD` stubs the mtkclient
invocation so the dump/manifest logic is exercised without hardware.
`tests/fixtures/pixel-boot-ab/` is harvested from a real Pixel 6a. The
`ulefone-*` fixtures are synthetic and encode _assumed_ property values — see
`NOTES.md`.
## Related
`../single_use/utils/root_bl9000.sh` is an older, single-device rooting script
for a Blackview MT6893. It is untouched by this toolkit and still works; the
two share no code by design, so changes here cannot break it.

View File

@ -1,123 +0,0 @@
# READY — the day the Ulefone arrives
Prepared 2026-08-04. Everything below was built and tested before the device
existed on this desk. Read the status matrix before trusting any of it.
## Command sequence
Run from `~/testsAndMisc/linux_configuration/scripts/mtk_root/`.
```bash
# 1. Confirm nothing rotted in transit. Expect "HOST READY".
./00-preflight.sh
# 2. Enable Developer options -> USB debugging on the phone, plug it in,
# accept the RSA prompt on its screen, then:
./10-recon.sh
# Read the decision summary. If it says UNKNOWN, the property patterns need
# widening -- see below. If it names a path, continue.
# 3. Check the phone screen yourself:
# Settings > System > Developer options > "OEM unlocking"
# toggleable => unlocking permitted; greyed out => blocked.
# A property cannot tell you this. Only the UI can.
# 4. Dump the stock images BEFORE touching anything.
./20-dump-stock.sh
# Then copy ~/.cache/mtk-root/stock/ somewhere off this machine.
# 5. Patch and flash MANUALLY -- there is no script for this on purpose.
# Use the partition name 10-recon.sh reported, not one from memory.
# 6. Check what actually worked:
./90-verify-root.sh
```
## If recon says UNKNOWN
Expected on first contact: the Ulefone's property values were guessed.
```bash
./10-recon.sh --explain-classification
```
It prints each property and the pattern it failed. Edit the marked block at
the top of `../lib/mtk_common.sh`, then re-run. The refusal is deliberate —
the alternative is a toolkit that treats any unrecognised phone as the target.
## Status: what is validated and what is not
| Capability | Status |
| --------------------------------------------------------- | -------------------------------------------------------------------- |
| Host tool checks, groups, udev syntax, venv importability | **host-validated** — 12/12 on this machine |
| Device enumeration, `unauthorized` vs `device` triage | **device-validated** (Pixel 6a) |
| PIXEL classification | **device-validated** |
| Partition discovery, **`boot`** carrier, A/B, slot suffix | **device-validated** — real Pixel, 53 partitions |
| Facts-file format and contents | **device-validated** |
| Partition discovery, **`init_boot`** carrier | **fixture-tested only — never run against hardware** |
| A-only slot scheme | **fixture-tested only** |
| UNKNOWN-device refusal; non-MediaTek dump refusal | **fixture-tested only** |
| Dump manifest, sha256, size sanity, idempotence | **fixture-tested only** — mtkclient stubbed via `MTK_DUMP_CMD` |
| `10-recon.sh` is read-only | **statically enforced**, with a negative self-test |
| Carrier-lock determination | **not possible from the device — reported `UNDETERMINED` by design** |
| mtkclient reading a real partition over BROM | **UNTESTED** — needs the Ulefone |
| Magisk patch → flash → `90-verify-root.sh` | **UNTESTED** — no unlocked device exists |
| Ulefone property values; A/B/C/D path semantics | **GUESSED / ASSUMED** |
The single most important line: **the Pixel validated the `boot` branch, and
the Ulefone almost certainly needs the `init_boot` branch.** The selection
logic is proven by fixtures; it has never run against a device that has an
`init_boot` partition.
## Pixel 6a — resolved status
| | |
| ----------------- | -------------------------------------------------------------------- |
| serial | `23181JEGR08034` |
| build | `bluejay:16/CP1A.260405.005` (Android 16, security patch 2026-04-05) |
| bootloader | `bluejay-16.4-14548185` |
| launch API level | 32 (Android 12) → ramdisk in **`boot`**, no `init_boot` |
| partitions | `boot_a`/`boot_b`, `vbmeta_a`/`vbmeta_b`, 53 total |
| bootloader state | locked (`flash.locked=1`, `verifiedbootstate=green`) |
| OEM unlock toggle | never enabled (`settings global oem_unlock_allowed` = null) |
| **carrier lock** | **UNDETERMINED — needs an out-of-band check** |
| path | **C or D, indeterminate** |
**The carrier-lock question is not answered, and cannot be from software.**
`sys.oem_unlock_allowed` and `ro.oem_unlock_supported` are both unset, and
`settings get global oem_unlock_allowed` returns null — but those read
identically whether the phone is carrier-locked or the toggle has simply never
been switched on. `ro.carrier=unknown` suggests a non-carrier build and
`gsm.sim.operator.alpha=Orange` is only the current SIM; neither settles it.
**To settle it:** look at Developer options → OEM unlocking on the phone. If it
is toggleable, it is not carrier-locked and Path C is open. If greyed out,
check the IMEI with the carrier or against the Google Store order before
concluding — a toggle can grey out for no network, an unverified account, or a
pending update on a phone that is not carrier-locked at all.
## Re-check before use — these go stale
- **Magisk v30.7** (2026-02-23), sha256 `e0d32d2…afae9ebd5`. Verify it is still
current; beware `app-debug.apk` in the same release, which is _not_ the one
to install. See `NOTES.md`.
- **mtkclient rev `0542a87`** (2026-08-02). `./install.sh` pulls and rebuilds.
Good sign: this rev has an explicit MT6765 entry with an `mt6765_payload.bin`
loader — the Ulefone's exact chipset.
- **Pixel factory images** at `developers.google.com/android/images#bluejay`.
Not downloaded; the toolkit does not act on the Pixel. Google moved the
Pixel 6 series off monthly cadence, so the newest listed build may be ahead
of what the phone runs.
## Open questions that need the physical device
- The BROM-mode key combination for the X12 Pro
- Whether mtkclient enumerates this unit, and whether its BROM is open or
SLA/DAA-locked (chipset support ≠ this unit being accessible)
- Whether `fastboot flashing unlock` is enabled in its firmware build
- Whether the OEM unlocking toggle is available at all
- Its exact build ID
- Whether the udev rules genuinely grant access to the MediaTek VID in practice
— rules syntax is verified, real BROM access is not
- Its USB vendor ID, if it differs from MediaTek's `0e8d`

View File

@ -1,141 +0,0 @@
#!/bin/bash
# ============================================================================
# install.sh - Set up host tooling for the mtk_root toolkit.
#
# Idempotent: safe to run repeatedly. Ends by running 00-preflight.sh, because
# a tool that was never executed is not installed, only copied.
#
# Leaves the older ~/.cache/bl9000-root workspace completely alone - a
# different script still owns it.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
readonly SCRIPT_DIR
readonly SCRIPT_NAME="${0##*/}"
# shellcheck source=../lib/mtk_common.sh
source "$SCRIPT_DIR/../lib/mtk_common.sh"
readonly MTKCLIENT_REPO="https://github.com/bkerler/mtkclient"
readonly UDEV_SOURCE="$SCRIPT_DIR/udev/60-mtk-root.rules"
readonly UDEV_TARGET="/etc/udev/rules.d/60-mtk-root.rules"
readonly -a PACMAN_PACKAGES=(android-tools android-udev git python libusb)
FIX_UDEV=0
SKIP_PACMAN=0
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--fix-udev] [--skip-pacman] [--help]
Installs host dependencies, clones mtkclient into the cache dir, builds its
venv, and installs narrowed udev rules.
--fix-udev Also comment out any world-writable catch-all udev rule
(idVendor=="*" with MODE="0666"), keeping a timestamped
backup. Not done by default: another script may rely on it.
--skip-pacman Do not install system packages (useful when already present)
--help This text
EOF
exit 0
}
install_packages() {
mtk_heading "System packages"
if [[ $SKIP_PACMAN -eq 1 ]]; then
mtk_info "Skipping package installation (--skip-pacman)."
return 0
fi
local -a missing=()
local pkg=""
for pkg in "${PACMAN_PACKAGES[@]}"; do
if ! pacman -Qi "$pkg" >/dev/null 2>&1; then
missing+=("$pkg")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
mtk_info "All required packages already installed."
return 0
fi
mtk_info "Installing: ${missing[*]}"
# Interactive by design. A non-interactive sudo pacman can block forever on
# the database lock with no visible prompt; better to let it ask.
if ! sudo pacman -S --needed "${missing[@]}"; then
mtk_error "Package installation failed. Install manually: ${missing[*]}"
return 1
fi
return 0
}
ensure_groups() {
mtk_heading "Groups"
local group="" changed=0
for group in plugdev adbusers; do
if ! getent group "$group" >/dev/null 2>&1; then
mtk_info "Creating group ${group}"
sudo groupadd "$group" || {
mtk_warn "Could not create ${group}"
continue
}
fi
if id -nG "$USER" | grep -qw "$group"; then
mtk_info "Already a member of ${group}"
else
mtk_info "Adding ${USER} to ${group}"
sudo usermod -aG "$group" "$USER" && changed=1
fi
done
if [[ $changed -eq 1 ]]; then
mtk_warn "Group membership changed - log out and back in before USB access works."
fi
}
# shellcheck source=lib/mtk_udev.sh
source "$SCRIPT_DIR/lib/mtk_udev.sh"
main() {
printf '\033[1m%s - host setup for the mtk_root toolkit\033[0m\n' "$SCRIPT_NAME"
install_packages
ensure_groups
prepare_cache
install_udev
setup_mtkclient
mtk_heading "Verifying"
if "$SCRIPT_DIR/00-preflight.sh"; then
mtk_info "Install complete."
else
mtk_warn "Preflight reported problems - see above."
return 1
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
--fix-udev)
FIX_UDEV=1
shift
;;
--skip-pacman)
SKIP_PACMAN=1
shift
;;
-h | --help) usage ;;
*)
mtk_error "Unknown option: $1"
exit 1
;;
esac
done
main

View File

@ -1,39 +0,0 @@
#!/bin/bash
# Reading facts and dumping individual partitions.
#
# Sourced by 20-dump-stock.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
# dump_partition <partition> <destination>
#
# MTK_DUMP_CMD overrides the mtkclient invocation and is the seam the tests
# use: it is called as `$MTK_DUMP_CMD <partition> <destination>`. Without it
# the only way to exercise the manifest/verification logic below would be to
# attach a phone in BROM mode, so that code would ship unrun.
dump_partition() {
local partition="$1" dest="$2"
local mtk_dir="" python_bin=""
mtk_info "Dumping ${partition} -> ${dest}"
if [[ -n ${MTK_DUMP_CMD:-} ]]; then
# shellcheck disable=SC2086 # intentional word splitting: the override may
# carry arguments, e.g. MTK_DUMP_CMD="./stub.sh --size 4M"
${MTK_DUMP_CMD} "$partition" "$dest"
return $?
fi
mtk_dir="$(mtk_mtkclient_dir)"
python_bin="$(mtk_venv_python)"
mtk_info "mtkclient is waiting for the device in BROM/preloader mode."
mtk_info "Power the phone OFF, then connect USB (some units need vol- held)."
# `mtk.py r <partition> <outfile>` per the mtkclient README. Run from the
# clone directory because mtk.py resolves its Loader/ paths relative to cwd.
(
cd "$mtk_dir" || exit 1
"$python_bin" mtk.py r "$partition" "$dest"
)
}

View File

@ -1,31 +0,0 @@
#!/bin/bash
# Dump verification and reporting for the stock partition dump.
#
# Sourced by 20-dump-stock.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
verify_dump() {
local label="$1" path="$2"
local size="" verdict="" sha=""
if [[ ! -f $path ]]; then
mtk_error "${label}: no file produced at ${path}"
return 1
fi
size="$(mtk_file_size "$path")"
sha="$(mtk_sha256 "$path")"
if verdict="$(mtk_sane_ramdisk_size "$size")"; then
mtk_info "${label}: ${verdict}, sha256 ${sha}"
else
# mtkclient exits 0 on a truncated read, so size is the only signal that
# something went wrong short of parsing the image.
mtk_warn "${label}: ${verdict}"
mtk_warn "${label}: sha256 ${sha}"
mtk_warn "Inspect this image before trusting it as a backup."
return 1
fi
return 0
}

View File

@ -1,129 +0,0 @@
#!/bin/bash
# udev rules and the mtkclient checkout for MediaTek rooting.
#
# Sourced by install.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
install_udev() {
mtk_heading "udev rules"
if [[ ! -f $UDEV_SOURCE ]]; then
mtk_error "Missing rules source: ${UDEV_SOURCE}"
return 1
fi
if [[ -f $UDEV_TARGET ]] && sudo cmp -s "$UDEV_SOURCE" "$UDEV_TARGET"; then
mtk_info "Rules already current at ${UDEV_TARGET}"
else
mtk_info "Installing ${UDEV_TARGET}"
sudo install -m 0644 "$UDEV_SOURCE" "$UDEV_TARGET"
sudo udevadm control --reload-rules
sudo udevadm trigger
fi
if command -v udevadm >/dev/null 2>&1 && udevadm verify --help >/dev/null 2>&1; then
if udevadm verify "$UDEV_TARGET" >/dev/null 2>&1; then
mtk_info "udevadm verify: clean"
else
mtk_error "udevadm verify rejected ${UDEV_TARGET}"
return 1
fi
fi
handle_catchall
}
handle_catchall() {
local catchall=""
catchall="$(mtk_udev_catchall_files)"
[[ -z $catchall ]] && return 0
mtk_warn "World-writable catch-all udev rule found in:"
printf '%s\n' "$catchall" | sed 's/^/ /'
mtk_warn 'A rule matching idVendor=="*" with MODE="0666" grants every local'
mtk_warn 'process read/write access to EVERY USB device, not just phones.'
if [[ $FIX_UDEV -eq 0 ]]; then
mtk_warn "Not changing it. Another script may depend on it."
mtk_warn "To narrow it: ${SCRIPT_NAME} --fix-udev"
return 0
fi
local file="" stamp=""
stamp="$(date -u '+%Y%m%dT%H%M%SZ')"
while IFS= read -r file; do
[[ -n $file ]] || continue
mtk_info "Backing up ${file} -> ${file}.bak-${stamp}"
sudo cp -a "$file" "${file}.bak-${stamp}"
# Comment the offending line rather than deleting it, so the change is
# obvious and trivially reversible.
sudo sed -i -E 's|^([^#].*idVendor\}=="\*".*MODE="0666".*)$|# disabled by mtk_root install.sh --fix-udev: world-writable catch-all\n#\1|' "$file"
mtk_info "Commented catch-all in ${file}"
done <<<"$catchall"
sudo udevadm control --reload-rules
sudo udevadm trigger
}
setup_mtkclient() {
mtk_heading "mtkclient"
local clone_dir=""
clone_dir="$(mtk_mtkclient_dir)"
if [[ -d "$clone_dir/.git" ]]; then
mtk_info "Updating existing clone at ${clone_dir}"
git -C "$clone_dir" pull --ff-only || mtk_warn "Pull failed; continuing with current checkout."
else
mtk_info "Cloning ${MTKCLIENT_REPO}"
mkdir -p "$(dirname "$clone_dir")"
git clone --depth 1 "$MTKCLIENT_REPO" "$clone_dir"
fi
local rev=""
rev="$(git -C "$clone_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)"
mtk_info "mtkclient at rev ${rev}"
# Rebuild the venv when it cannot import its dependencies. A Python upgrade
# orphans a venv while leaving it looking complete on disk - the venv found
# on this host in Aug 2026 failed exactly that way.
if ! mtk_check_venv >/dev/null 2>&1; then
if [[ -d "$clone_dir/venv" ]]; then
mtk_warn "Existing venv is unusable; rebuilding."
rm -rf "$clone_dir/venv"
fi
mtk_info "Creating venv with $(python3 -V 2>&1)"
python3 -m venv "$clone_dir/venv"
"$clone_dir/venv/bin/pip" install --upgrade pip
if [[ -f "$clone_dir/requirements.txt" ]]; then
"$clone_dir/venv/bin/pip" install -r "$clone_dir/requirements.txt"
else
# Newer mtkclient revisions ship a pyproject instead of requirements.txt.
mtk_info "No requirements.txt; installing the package itself."
"$clone_dir/venv/bin/pip" install "$clone_dir"
fi
else
mtk_info "venv already usable."
fi
local status=""
if status="$(mtk_check_venv)"; then
mtk_info "venv check: ${status}"
else
mtk_error "venv still unusable after install: ${status}"
mtk_error "Read ${clone_dir}/README.md - its install steps may have changed."
return 1
fi
}
prepare_cache() {
mtk_heading "Workspace"
local cache_dir=""
cache_dir="$(mtk_cache_dir)"
mkdir -p "$cache_dir" "$(mtk_stock_dir)"
mtk_info "Cache: ${cache_dir}"
mtk_info "Artifacts stay here, outside the repo - it rejects binaries, and"
mtk_info "factory images run to several GB."
}

View File

@ -1,112 +0,0 @@
#!/bin/bash
# Partition, lock-state and manual-check reporting for MTK recon.
#
# Sourced by 10-recon.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
report_partitions() {
local carrier=""
carrier="$(mtk_ramdisk_carrier)"
mtk_heading "Partitions"
printf ' A/B scheme : %s\n' "$MTK_AB_SCHEME"
if [[ ${MTK_SLOT_ASSUMED:-0} -eq 1 ]]; then
printf ' slot suffix : %s (ASSUMED - ro.boot.slot_suffix was empty)\n' "${MTK_SLOT_SUFFIX:-none}"
else
printf ' slot suffix : %s\n' "${MTK_SLOT_SUFFIX:-none}"
fi
printf ' ramdisk lives in: %s -> partition %s\n' "${carrier:-UNKNOWN}" "${MTK_RAMDISK_PART:-NONE FOUND}"
printf ' vbmeta : %s\n' "${MTK_VBMETA_PART:-NONE FOUND}"
# boot vs init_boot is the single most consequential fact here: patching the
# wrong one produces a device that does not boot.
if [[ $carrier == "init_boot" ]]; then
printf ' => Magisk must patch INIT_BOOT on this device.\n'
elif [[ $carrier == "boot" ]]; then
printf ' => Magisk must patch BOOT on this device (no init_boot present).\n'
else
printf ' => Could not identify a ramdisk carrier. Do not flash anything.\n'
fi
mtk_check_layout_expectation || true
}
report_lock_state() {
mtk_heading "Lock state"
printf ' ro.boot.flash.locked : %s\n' "${UNLOCK_FLASH_LOCKED:-<unset>}"
printf ' ro.boot.verifiedbootstate : %s\n' "${UNLOCK_VBSTATE:-<unset>}"
printf ' ro.boot.vbmeta.device_state : %s\n' "${UNLOCK_DEVICE_STATE:-<unset>}"
printf ' sys.oem_unlock_allowed : %s\n' "${UNLOCK_SYS_ALLOWED:-<unset>}"
printf ' ro.oem_unlock_supported : %s\n' "${UNLOCK_OEM_SUPPORTED:-<unset>}"
printf ' settings oem_unlock_allowed : %s\n' "${UNLOCK_SETTING:-<unset>}"
printf '\n'
printf ' OEM-unlock toggle : %s\n' "$UNLOCK_TOGGLE"
printf ' Carrier lock : %s\n' "$(carrier_lock_verdict)"
printf '\n'
printf ' Carrier lock cannot be determined from any property. An unset\n'
printf ' sys.oem_unlock_allowed means the toggle is off, which is NOT the same\n'
printf ' as carrier-locked. Settle it out of band: check the IMEI with the\n'
printf ' carrier, or in the Google Store order record.\n'
}
report_manual_checks() {
mtk_heading "Check these on the phone screen yourself"
printf ' Settings > System > Developer options > "OEM unlocking":\n'
printf ' - toggleable => bootloader unlock is permitted by the firmware\n'
printf ' - greyed out => blocked (carrier policy, no network, or unsupported)\n'
printf ' A property can say the toggle is off; only the UI shows whether it\n'
printf ' CAN be turned on. That distinction decides the path.\n'
}
report_decision() {
mtk_heading "Decision summary"
# The companion document that defines paths A-D was not provided to the
# session that wrote this script, so the mapping below is inferred. Say so
# every run rather than let a plausible-looking verdict pass as authoritative.
printf ' \033[0;33mPATH SEMANTICS ARE ASSUMED\033[0m - the companion doc defining A/B/C/D was\n'
printf ' not available when this was written. Reconcile before acting.\n\n'
case "$MTK_DEVICE_CLASS" in
ULEFONE)
if [[ $UNLOCK_TOGGLE == "ON" ]]; then
printf ' => PATH A (Ulefone, OEM unlocking available)\n'
printf ' Next: ./20-dump-stock.sh --serial %s\n' "$MTK_SERIAL"
printf ' Then patch %s with Magisk and flash MANUALLY.\n' "${MTK_RAMDISK_PART:-<unknown>}"
else
printf ' => PATH A or B (Ulefone; toggle reads %s)\n' "$UNLOCK_TOGGLE"
printf ' Confirm the toggle on-screen before choosing.\n'
printf ' If greyed out => PATH B: fastboot unlock is blocked, leaving the\n'
printf ' mtkclient/BROM route, which is the untested one. Dump first:\n'
printf ' ./20-dump-stock.sh --serial %s\n' "$MTK_SERIAL"
fi
;;
PIXEL)
printf ' => PATH C or D (Pixel) - INDETERMINATE\n'
printf ' The toggle reads %s and carrier lock is UNDETERMINED, so C and D\n' "$UNLOCK_TOGGLE"
printf ' cannot be told apart from properties alone.\n\n'
printf ' \033[0;31mThis toolkit will not act on a Pixel.\033[0m It is the daily driver;\n'
printf ' recon only. No dump, unlock, or flash step here targets it.\n'
;;
*)
printf ' => NO PATH. Device did not classify.\n'
printf ' %s\n' "$MTK_CLASS_REASON"
printf ' Refusing to guess. Run --explain-classification, and if this is\n'
printf ' genuinely the Ulefone, widen the patterns at the top of\n'
printf ' lib/mtk_common.sh.\n'
;;
esac
mtk_heading "Still unknown"
printf ' - Whether the OEM unlocking toggle can be enabled (needs the screen)\n'
printf ' - Carrier lock status (needs an out-of-band IMEI check)\n'
if [[ $MTK_DEVICE_CLASS == "ULEFONE" ]]; then
printf ' - Whether this unit BROM is open or SLA/DAA-locked (needs mtkclient)\n'
printf ' - Whether fastboot flashing unlock is enabled in this firmware build\n'
fi
}

View File

@ -1,53 +0,0 @@
abl_a
abl_b
bl1_a
bl1_b
bl2_a
bl2_b
bl31_a
bl31_b
boot_a
boot_b
devinfo
dpm_a
dpm_b
dram_train_a
dram_train_b
dtbo_a
dtbo_b
efs
efs_backup
fips
frp
gsa_a
gsa_b
klog
ldfw_a
ldfw_b
metadata
mfg_data
misc
modem_a
modem_b
modem_userdata
pbl_a
pbl_b
persist
pvmfw_a
pvmfw_b
sda
sdb
sdc
sdd
super
tzsw_a
tzsw_b
userdata
vbmeta_a
vbmeta_b
vbmeta_system_a
vbmeta_system_b
vbmeta_vendor_a
vbmeta_vendor_b
vendor_boot_a
vendor_boot_b

View File

@ -1,24 +0,0 @@
ro.product.manufacturer=Google
ro.product.model=Pixel 6a
ro.product.name=bluejay
ro.product.device=bluejay
ro.product.first_api_level=32
ro.board.platform=gs101
ro.hardware=bluejay
ro.build.fingerprint=google/bluejay/bluejay:16/CP1A.260405.005/15001963:user/release-keys
ro.build.id=CP1A.260405.005
ro.build.version.release=16
ro.build.version.sdk=36
ro.build.version.security_patch=2026-04-05
ro.build.type=user
ro.boot.slot_suffix=_a
ro.boot.flash.locked=1
ro.boot.verifiedbootstate=green
ro.boot.vbmeta.device_state=locked
ro.boot.veritymode=enforcing
ro.boot.hardware=bluejay
ro.boot.bootloader=bluejay-16.4-14548185
ro.bootloader=bluejay-16.4-14548185
ro.carrier=unknown
gsm.sim.operator.alpha=Orange
ro.crypto.state=encrypted

View File

@ -1,8 +0,0 @@
boot_a
boot_b
init_boot_a
init_boot_b
vbmeta_a
vbmeta_b
super
userdata

View File

@ -1,7 +0,0 @@
ro.product.manufacturer=Ulefone
ro.product.model=Armor X12 Pro
ro.product.device=Armor_X12_Pro
ro.board.platform=mt6765
ro.hardware=mt6765
ro.product.first_api_level=33
ro.boot.slot_suffix=_a

View File

@ -1,5 +0,0 @@
boot
init_boot
vbmeta
super
userdata

View File

@ -1,5 +0,0 @@
ro.product.manufacturer=Ulefone
ro.product.model=Armor X12 Pro
ro.board.platform=mt6765
ro.hardware=mt6765
ro.product.first_api_level=33

View File

@ -1,2 +0,0 @@
boot_a
vbmeta_a

View File

@ -1,4 +0,0 @@
ro.product.manufacturer=Xiaomi
ro.product.model=Redmi Note 12
ro.board.platform=sm6225
ro.hardware=qcom

View File

@ -1,75 +0,0 @@
#!/bin/bash
# ============================================================================
# Assertion helpers and fixture drivers for the mtk_root fixture tests.
#
# Split out of run_tests.sh to keep it under the 250-line cap. The seam passes
# state deliberately: pass/fail increment the caller's PASS and FAIL counters,
# and run_recon reads WORKROOT, FIXTURES and RECON. The caller defines all of
# them before sourcing this file.
# ============================================================================
# shellcheck shell=bash
pass() {
PASS=$((PASS + 1))
printf ' [PASS] %s\n' "$1"
}
fail() {
FAIL=$((FAIL + 1))
printf ' [FAIL] %s\n' "$1"
[[ -n ${2:-} ]] && printf ' %s\n' "$2"
return 0
}
assert_contains() {
if [[ $2 == *"$3"* ]]; then
pass "$1"
else
fail "$1" "expected to find: $3"
fi
}
assert_not_contains() {
if [[ $2 != *"$3"* ]]; then
pass "$1"
else
fail "$1" "did not expect: $3"
fi
}
assert_eq() {
if [[ $2 == "$3" ]]; then
pass "$1"
else
fail "$1" "expected '$3', got '$2'"
fi
}
# strip_noncode <file>
# Reduce a script to the lines that could actually execute something, so the
# read-only scan below can look for dangerous verbs in command position.
#
# Drops comments, and drops the ARGUMENTS of output builtins only - 10-recon.sh
# exists to talk about flashing and unlocking, so those words appear
# legitimately inside printf/echo text. Everything else keeps its quotes: an
# earlier version stripped ALL quoted strings, which let
# `mtk_adb shell "reboot bootloader"` slip through the scan entirely.
strip_noncode() {
sed -E \
-e 's/(^|[[:space:]])#.*$/\1/' \
-e 's/^[[:space:]]*(printf|echo|cat)[[:space:]].*$//' \
-e 's/\|[[:space:]]*(printf|echo|cat)[[:space:]].*$//' \
"$1"
}
# run_recon <fixture-name> [extra args...] - prints combined output.
run_recon() {
local fixture="$1"
shift
MTK_ROOT_FIXTURE="$FIXTURES/$fixture" \
MTK_ROOT_CACHE="$WORKROOT/cache" \
MTK_SERIAL="TEST$RANDOM" \
"$RECON" "$@" 2>&1
}

View File

@ -1,157 +0,0 @@
#!/bin/bash
# ============================================================================
# Fixture tests for 20-dump-stock.sh: refusals, the happy path, truncation
# detection, ordering against 10-recon.sh, and the facts file it writes.
#
# Split out of run_tests.sh to keep both files under the 250-line cap. Sourced
# by run_tests.sh, which owns the counters and the fixture paths these use.
# ============================================================================
# shellcheck shell=bash
test_dump_refuses_non_mediatek() {
printf '\n20-dump-stock.sh refuses a non-MediaTek device\n'
[[ -x $DUMP ]] || {
fail "20-dump-stock.sh present" "not found or not executable"
return 0
}
local out="" rc=0
out="$(MTK_ROOT_FIXTURE="$FIXTURES/pixel-boot-ab" \
MTK_ROOT_CACHE="$WORKROOT/cache" \
MTK_SERIAL="TESTPIXEL" \
"$DUMP" --yes 2>&1)" || rc=$?
assert_eq "exits non-zero on Pixel" "$rc" "1"
assert_contains "refuses explicitly" "$out" "refus"
assert_not_contains "no dump attempted" "$out" "Dumping"
}
test_dump_happy_path() {
printf '\n20-dump-stock.sh produces a manifest from a stubbed read\n'
[[ -x $DUMP ]] || {
fail "20-dump-stock.sh present" "not found or not executable"
return 0
}
# Stub stands in for mtkclient: writes a plausibly-sized file. This exercises
# find_latest_facts, read_facts_value, verify_dump, the size band, the sha256
# column and the manifest - none of which the refusal test reaches.
local stub="$WORKROOT/stub-dump.sh"
cat >"$stub" <<'STUB'
#!/bin/bash
set -euo pipefail
truncate -s 8M "$2"
printf 'ANDROID!' | dd of="$2" conv=notrunc status=none
STUB
chmod +x "$stub"
local cache="$WORKROOT/dumpcache"
mkdir -p "$cache"
local serial="DUMPTEST1"
# Chain the real recon output in, rather than hand-writing a facts file:
# this also proves the two scripts agree on the format.
MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" "$RECON" >/dev/null 2>&1
local out="" rc=0
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" \
MTK_DUMP_CMD="$stub" "$DUMP" --yes 2>&1)" || rc=$?
assert_eq "exits zero on success" "$rc" "0"
assert_contains "dumped the init_boot partition" "$out" "init_boot_a"
local manifest="$cache/stock/$serial/manifest.txt"
if [[ ! -f $manifest ]]; then
fail "manifest written" "not found at $manifest"
return 0
fi
pass "manifest written"
local content=""
content="$(cat "$manifest")"
assert_contains "manifest names the image" "$content" "init_boot_a.img"
assert_contains "manifest records vbmeta" "$content" "vbmeta_a.img"
assert_contains "manifest records carrier" "$content" "ramdisk_carrier=init_boot"
assert_contains "manifest records size" "$content" "8388608 bytes"
# A sha256 is 64 hex chars; assert one is actually present.
if grep -qE '^[0-9a-f]{64} ' <<<"$content"; then
pass "manifest records sha256"
else
fail "manifest records sha256" "no 64-char hex digest found"
fi
# Idempotence: a second run must not silently re-dump over a good backup.
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" \
MTK_DUMP_CMD="$stub" "$DUMP" --yes 2>&1)" || true
assert_contains "second run is a no-op" "$out" "already exists"
}
test_dump_rejects_truncated_image() {
printf '\n20-dump-stock.sh flags an implausibly small dump\n'
[[ -x $DUMP ]] || return 0
# mtkclient exits 0 on a truncated read, so size is the only signal that the
# "backup" is worthless. A 512-byte init_boot must not pass silently.
local stub="$WORKROOT/stub-truncated.sh"
cat >"$stub" <<'STUB'
#!/bin/bash
set -euo pipefail
truncate -s 512 "$2"
STUB
chmod +x "$stub"
local cache="$WORKROOT/truncache"
mkdir -p "$cache"
local serial="DUMPTEST2"
MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" "$RECON" >/dev/null 2>&1
local out="" rc=0
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
MTK_ROOT_CACHE="$cache" MTK_SERIAL="$serial" \
MTK_DUMP_CMD="$stub" "$DUMP" --yes 2>&1)" || rc=$?
assert_eq "exits non-zero" "$rc" "1"
assert_contains "calls it implausibly small" "$out" "implausibly small"
assert_contains "warns against trusting it" "$out" "Do NOT treat this as a usable backup"
}
test_dump_requires_recon_first() {
printf '\n20-dump-stock.sh refuses without a recon facts file\n'
[[ -x $DUMP ]] || return 0
local out="" rc=0
out="$(MTK_ROOT_FIXTURE="$FIXTURES/ulefone-initboot-ab" \
MTK_ROOT_CACHE="$WORKROOT/emptycache" MTK_SERIAL="NOFACTS1" \
MTK_DUMP_CMD="/bin/true" "$DUMP" --yes 2>&1)" || rc=$?
assert_eq "exits non-zero" "$rc" "1"
assert_contains "points at 10-recon.sh" "$out" "10-recon.sh"
assert_contains "explains it will not guess" "$out" "rather than guessing"
}
test_facts_file_written() {
printf '\nfacts file is written and machine-readable\n'
local out="" facts=""
out="$(run_recon ulefone-initboot-ab)"
facts="$(find "$WORKROOT/cache" -name 'device-facts-ULEFONE-*.txt' -print -quit 2>/dev/null)"
if [[ -z $facts ]]; then
fail "facts file created" "none found under $WORKROOT/cache"
return 0
fi
pass "facts file created"
local content=""
content="$(cat "$facts")"
assert_contains "records ramdisk partition" "$content" "ramdisk_partition=init_boot_a"
assert_contains "records carrier" "$content" "ramdisk_carrier=init_boot"
assert_contains "records scheme" "$content" "ab_scheme=A/B"
assert_contains "includes by-name listing" "$content" "[by-name]"
}

View File

@ -1,218 +0,0 @@
#!/bin/bash
# ============================================================================
# Fixture tests for the mtk_root toolkit.
#
# Every test runs against a fixture directory (props.txt + by-name.txt) via
# MTK_ROOT_FIXTURE, so no phone is ever touched and the suite is hermetic.
#
# What these tests DO prove: the partition-selection logic, the classification
# refusals, and the read-only guarantee of 10-recon.sh.
# What they do NOT prove: that mtkclient can read any partition on real
# hardware. Fixtures cannot establish that.
# ============================================================================
set -uo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
readonly TOOLKIT_DIR="$SCRIPT_DIR/.."
readonly FIXTURES="$SCRIPT_DIR/fixtures"
readonly RECON="$TOOLKIT_DIR/10-recon.sh"
readonly DUMP="$TOOLKIT_DIR/20-dump-stock.sh"
PASS=0
FAIL=0
WORKROOT=""
cleanup() {
[[ -n $WORKROOT && -d $WORKROOT ]] && rm -rf "$WORKROOT"
}
trap cleanup EXIT
# shellcheck source=./lib/assertions.sh
source "$SCRIPT_DIR/lib/assertions.sh"
# ---------------------------------------------------------------------------
test_initboot_preferred_over_boot() {
printf '\ninit_boot is chosen over a co-present boot\n'
local out=""
out="$(run_recon ulefone-initboot-ab)"
# The decisive case: this fixture has BOTH boot_a and init_boot_a. Choosing
# boot_a here would produce a device that does not boot after flashing.
assert_contains "selects init_boot_a" "$out" "partition init_boot_a"
assert_contains "instructs patching INIT_BOOT" "$out" "must patch INIT_BOOT"
assert_not_contains "does not select boot_a" "$out" "partition boot_a"
assert_contains "detects A/B" "$out" "A/B scheme : A/B"
assert_contains "vbmeta suffixed" "$out" "vbmeta_a"
}
test_a_only_scheme() {
printf '\nA-only devices get bare partition names\n'
local out=""
out="$(run_recon ulefone-initboot-aonly)"
assert_contains "detects A-only" "$out" "A-only"
assert_contains "bare init_boot" "$out" "partition init_boot"
assert_contains "bare vbmeta" "$out" "vbmeta : vbmeta"
}
test_boot_carrier_when_no_initboot() {
printf '\nfalls back to boot when init_boot is absent\n'
local out=""
out="$(run_recon pixel-boot-ab)"
assert_contains "selects boot_a" "$out" "partition boot_a"
assert_contains "instructs patching BOOT" "$out" "must patch BOOT"
# The word "init_boot" legitimately appears in the explanatory text ("no
# init_boot present"), so assert on the selection line specifically rather
# than on the whole report.
local carrier_line=""
carrier_line="$(grep 'ramdisk lives in' <<<"$out" || true)"
assert_contains "carrier reported as boot" "$carrier_line" "ramdisk lives in: boot"
assert_not_contains "carrier is not init_boot" "$carrier_line" "init_boot"
}
test_unknown_device_refused() {
printf '\nunknown device is refused, never assumed\n'
local out="" rc=0
out="$(run_recon unknown-device)" || rc=$?
assert_eq "exits non-zero" "$rc" "1"
assert_contains "says NO PATH" "$out" "NO PATH"
assert_contains "refuses to guess" "$out" "Refusing to guess"
# The specific danger: treating "not a Pixel" as "therefore the Ulefone".
assert_not_contains "does not claim PATH A" "$out" "PATH A"
assert_not_contains "does not claim PATH B" "$out" "PATH B"
}
test_pixel_is_protected() {
printf '\nPixel is recognised and refused as a target\n'
local out=""
out="$(run_recon pixel-boot-ab)"
assert_contains "classified PIXEL" "$out" "classified as : PIXEL"
assert_contains "indeterminate C/D" "$out" "PATH C or D"
assert_contains "will not act on it" "$out" "will not act on a Pixel"
}
test_carrier_lock_never_asserted() {
printf '\ncarrier lock is reported as undetermined, never negative\n'
local out=""
out="$(run_recon pixel-boot-ab)"
assert_contains "reports UNDETERMINED" "$out" "Carrier lock : UNDETERMINED"
assert_contains "explains why" "$out" "NOT the same"
# A false "not carrier locked" would wrongly rule a path back in.
assert_not_contains "no false negative" "$out" "not carrier locked"
}
test_assumption_banner_always_shown() {
printf '\npath semantics are flagged as assumed on every run\n'
local out=""
for fixture in ulefone-initboot-ab pixel-boot-ab unknown-device; do
out="$(run_recon "$fixture")" || true
assert_contains "banner present ($fixture)" "$out" "PATH SEMANTICS ARE ASSUMED"
done
}
test_recon_is_read_only() {
printf '\n10-recon.sh contains no mutating command\n'
local code="" hits=""
code="$(strip_noncode "$RECON")"
hits="$(grep -nE '(^|[^[:alnum:]_.-])(fastboot|magiskboot|mtk\.py)([^[:alnum:]_-]|$)' <<<"$code" || true)"
assert_eq "no fastboot/magiskboot invocation" "${hits:-none}" "none"
hits="$(grep -nE 'adb[^|]*(reboot|push|install|remount)' <<<"$code" || true)"
assert_eq "no adb reboot/push/install/remount" "${hits:-none}" "none"
hits="$(grep -nE 'settings[[:space:]]+put|svc[[:space:]]+(power|data)' <<<"$code" || true)"
assert_eq "no settings put / svc" "${hits:-none}" "none"
hits="$(grep -nE '\bdd[[:space:]]+if=|\bmkfs|\berase\b|flashing[[:space:]]+unlock' <<<"$code" || true)"
assert_eq "no dd/mkfs/erase/unlock" "${hits:-none}" "none"
# Belt and braces: the only adb subcommands this script may ever run.
local subcommands=""
subcommands="$(grep -oE 'mtk_adb[[:space:]]+[a-z-]+' "$RECON" | awk '{ print $2 }' | sort -u | tr '\n' ' ')"
assert_eq "mtk_adb used only for shell" "${subcommands% }" "shell"
}
test_read_only_scan_catches_injection() {
printf '\nthe read-only scan actually fails on a mutating command\n'
# A guard that has never been observed failing is not known to work. Inject
# real dangerous commands into a copy and assert each is caught. The quoted
# form is the specific case a previous version of this scan let through.
local evil="$WORKROOT/evil.sh" code="" hits=""
sed 's|^ classify_device$| mtk_adb shell "reboot bootloader"\n classify_device|' \
"$RECON" >"$evil"
code="$(strip_noncode "$evil")"
hits="$(grep -cE 'adb[^|]*(reboot|push|install|remount)' <<<"$code" || true)"
if [[ ${hits:-0} -gt 0 ]]; then
pass "catches quoted 'adb shell \"reboot ...\"'"
else
fail "catches quoted 'adb shell \"reboot ...\"'" "injection went undetected"
fi
sed 's|^ classify_device$| fastboot flashing unlock\n classify_device|' \
"$RECON" >"$evil"
code="$(strip_noncode "$evil")"
hits="$(grep -cE '(^|[^[:alnum:]_.-])(fastboot|magiskboot|mtk\.py)([^[:alnum:]_-]|$)' <<<"$code" || true)"
if [[ ${hits:-0} -gt 0 ]]; then
pass "catches bare fastboot invocation"
else
fail "catches bare fastboot invocation" "injection went undetected"
fi
sed 's|^ classify_device$| mtk_adb shell settings put global foo 1\n classify_device|' \
"$RECON" >"$evil"
code="$(strip_noncode "$evil")"
hits="$(grep -cE 'settings[[:space:]]+put|svc[[:space:]]+(power|data)' <<<"$code" || true)"
if [[ ${hits:-0} -gt 0 ]]; then
pass "catches settings put"
else
fail "catches settings put" "injection went undetected"
fi
# And the scan must still pass on the real, unmodified script.
code="$(strip_noncode "$RECON")"
hits="$(grep -nE 'adb[^|]*(reboot|push|install|remount)' <<<"$code" || true)"
assert_eq "no false positive on the real script" "${hits:-none}" "none"
}
# shellcheck source=./lib/dump_tests.sh
source "$SCRIPT_DIR/lib/dump_tests.sh"
main() {
WORKROOT="$(mktemp -d)"
readonly WORKROOT
mkdir -p "$WORKROOT/cache"
printf '=== mtk_root fixture tests ===\n'
test_initboot_preferred_over_boot
test_a_only_scheme
test_boot_carrier_when_no_initboot
test_unknown_device_refused
test_pixel_is_protected
test_carrier_lock_never_asserted
test_assumption_banner_always_shown
test_recon_is_read_only
test_read_only_scan_catches_injection
test_dump_refuses_non_mediatek
test_dump_happy_path
test_dump_rejects_truncated_image
test_dump_requires_recon_first
test_facts_file_written
printf '\n=== %d passed, %d failed ===\n' "$PASS" "$FAIL"
[[ $FAIL -eq 0 ]]
}
main "$@"

View File

@ -1,24 +0,0 @@
# mtk_root - USB access for Android debugging and MediaTek BROM recovery.
#
# Installed by mtk_root/install.sh. Scoped to two vendor IDs on purpose.
#
# MODE="0660" plus GROUP + TAG+="uaccess", NOT MODE="0666":
# - 0666 grants every process on the machine read/write access to the device
# - uaccess grants access to the user logged in at the physical seat, which
# is what is actually wanted, and it is revoked on logout
# A rule matching idVendor=="*" with 0666 applies to every USB device attached
# to the system - storage, security keys, everything - not just phones.
# Google (Pixel): adb + fastboot
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="adbusers", TAG+="uaccess"
# MediaTek: BROM / preloader / DA modes, used by mtkclient.
# 0e8d covers the BROM (2000), preloader (2001) and DA download ports.
SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0660", GROUP="adbusers", TAG+="uaccess"
# MediaTek devices also expose a CDC-ACM serial port in preloader mode;
# mtkclient opens it directly.
SUBSYSTEM=="tty", ATTRS{idVendor}=="0e8d", MODE="0660", GROUP="uucp", TAG+="uaccess"
# Ulefone's own USB vendor ID, if it differs from MediaTek's, is UNKNOWN until
# the device is attached: run `lsusb` with it plugged in and add a line here.

View File

@ -15,10 +15,6 @@ linux_configuration/.githooks/lib/common.sh
linux_configuration/scripts/meta/lib/shell_check_discover.sh
linux_configuration/scripts/meta/lib/shell_check_install.sh
linux_configuration/scripts/meta/lib/shell_check_lint.sh
linux_configuration/scripts/mtk_root/lib/dump_partitions.sh
linux_configuration/scripts/mtk_root/lib/dump_verify.sh
linux_configuration/scripts/mtk_root/lib/mtk_udev.sh
linux_configuration/scripts/mtk_root/lib/recon_report.sh
linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/install_files.sh
linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/integrity.sh
linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/managed_copy.sh