Files
reverse-engineering-anything/src/adb/AdbDeviceOutput.test.ts
T
fef6f73064 feat(adb): complete ADB device support for Android reverse engineering (#1552)
* feat(adb): acquire installed APK sets from connected Android devices

Add a read-only ADB device-acquisition family over the caller's adb binary:
inspect_adb_client, list_adb_devices, inspect_adb_device, list_adb_packages,
and pull_adb_package, exposed through the CLI and MCP with shared admission,
Evidence provenance (binary, version, serial, transport), and fixed argv
vectors that never reach a shell.

pull_adb_package acquires the complete split set reported by pm path into a
package-named directory that must not exist, digesting every file with
SHA-256 and keeping per-file failure reasons and partial coverage. Device
kind is reported as an inference with its basis; getprop whitelisting keeps
build identity bounded. REA installs nothing and never manages devices or
emulators; the first device query may start the caller's local adb server,
recorded when observed.

Verified against adb 34.0.5 on Linux with an Android 14 (API 34) x86_64
emulator: full 224-package inventory, a real two-APK split set pulled with
byte-exact digests, and a 73.9 MB system-package pull. npm run verify:adb
records the lane; parser goldens come from the same device. READMEs carry
the new row in every supported language.

* feat(adb): complete the adb surface with observation, transfer, and lifecycle tools

Extend the ADB family from five to twenty-four operations, all through
fixed adb argument vectors with no shell exposure:

Observation: read_adb_logcat (bounded dump, count/buffer/pid), inspect_adb_package
(dumpsys package projection), list_adb_processes (ps -A), list_adb_directory
(ls -la with kinds and symlink targets), list_adb_features (hex GL versions),
list_adb_services (AIDL slash-suffixed names), inspect_adb_display (wm size and
density), inspect_adb_window (focus declarations), read_adb_setting (device-side
nulls reported as such), capture_adb_screen (exec-out screencap with PNG
dimensions), collect_adb_bugreport (digested device archive).

Transfer and search: pull_adb_file and push_adb_file (device-side sha256sum
verification, explicit overwrite authority), resolve_adb_packages (substring
package search with exact-match flag — the find step of an easy get).

Lifecycle for dynamic analysis, each declaring mutatesTarget:
install_adb_package and uninstall_adb_package preserving the device's
Success/Failure verdict, start_adb_app (resolve-activity then am start -W),
start_adb_activity (typed extras as --es/--ez/--ei/--el/--ef), and stop_adb_app
(am force-stop).

Verified end-to-end against adb 34.0.5 on Linux with an Android 14 (API 34)
x86_64 emulator: 287 services, 92 features, split-set pulls with byte-exact
digests, verified push/pull roundtrip, sized screen capture, and the full
install → resolve → start → observe-in-ps → force-stop → uninstall lifecycle.
Fixed a v-mode regex incompatibility in the settings key pattern and added
argument descriptions required by the CLI catalog audit.

* fix(adb): quote remote shell operands and protect captured output

* test(adb): cover bugreport destinations containing spaces

---------

Co-authored-by: REA Contributor <contributor@rea.tools>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 23:01:24 +08:00

243 lines
8.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
parseAdbDevicesOutput,
parseGetpropOutput,
parsePackageListOutput,
parsePmPathOutput,
} from "./AdbDeviceOutput.js";
describe("parseAdbDevicesOutput", () => {
it("parses emulator, USB, and network devices with their metadata", () => {
const output = [
"List of devices attached",
"emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1",
"07ef9c2a device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2",
"192.168.1.42:5555 device product:mock model:mock transport_id:3",
"",
].join("\n");
const { devices, unparsedLines, daemonStarted } =
parseAdbDevicesOutput(output);
expect(unparsedLines).toEqual([]);
expect(daemonStarted).toBe(false);
expect(devices).toEqual([
{
serial: "emulator-5554",
state: "device",
transport: "tcp",
product: "sdk_gphone64_x86_64",
model: "sdk_gphone64_x86_64",
device: "emu64xa",
transport_id: 1,
kind: "emulator",
kind_basis: "emulator_serial_prefix",
},
{
serial: "07ef9c2a",
state: "device",
transport: "usb",
product: "raven",
model: "Pixel_6_Pro",
device: "raven",
transport_id: 2,
kind: "physical",
kind_basis: "usb_transport",
},
{
serial: "192.168.1.42:5555",
state: "device",
transport: "tcp",
product: "mock",
model: "mock",
device: null,
transport_id: 3,
kind: "unknown",
kind_basis: "none",
},
]);
});
it("recognizes daemon startup notices instead of failing the listing", () => {
const output = [
"* daemon not running; starting now at tcp:5037",
"* daemon started successfully",
"List of devices attached",
"emulator-5554 device transport_id:1",
"",
].join("\n");
const { devices, daemonStarted } = parseAdbDevicesOutput(output);
expect(daemonStarted).toBe(true);
expect(devices).toHaveLength(1);
});
it("keeps non-device states such as unauthorized and offline", () => {
const output = [
"List of devices attached",
"emulator-5556 offline transport_id:2",
"HT84L0240512 unauthorized usb:1-2 transport_id:3",
"",
].join("\n");
const { devices } = parseAdbDevicesOutput(output);
expect(devices.map((device) => [device.serial, device.state])).toEqual([
["emulator-5556", "offline"],
["HT84L0240512", "unauthorized"],
]);
});
it("returns unrecognized lines instead of dropping them", () => {
const output = [
"List of devices attached",
"this is not a device line",
"",
].join("\n");
const { unparsedLines } = parseAdbDevicesOutput(output);
expect(unparsedLines).toEqual(["this is not a device line"]);
});
it("reports a non-numeric transport_id as unknown", () => {
const output = [
"List of devices attached",
"emulator-5554 device transport_id:not-a-number",
"",
].join("\n");
const { devices } = parseAdbDevicesOutput(output);
expect(devices[0]?.transport_id).toBeNull();
});
});
describe("parseGetpropOutput", () => {
it("projects the whitelisted build identity from a real dump", () => {
const dump = [
"[ro.build.version.release]: [14]",
"[ro.build.version.sdk]: [34]",
"[ro.build.version.security_patch]: [2024-06-05]",
"[ro.build.id]: [AP21.240617.014]",
"[ro.build.fingerprint]: [google/sdk_gphone64_x86_64/emu64xa:14/AP21.240617.014/12094027:userdebug/dev-keys]",
"[ro.product.brand]: [google]",
"[ro.product.device]: [emu64xa]",
"[ro.product.manufacturer]: [Google]",
"[ro.product.model]: [sdk_gphone64_x86_64]",
"[ro.product.cpu.abilist]: [x86_64,x86,arm64-v8a,armeabi-v7a,armeabi]",
"[ro.kernel.qemu]: [1]",
"[ro.debuggable]: [1]",
"[sys.unlisted.property]: [dropped]",
"",
].join("\n");
const { properties, emulatorObserved } = parseGetpropOutput(dump);
expect(emulatorObserved).toBe(true);
expect(properties).not.toContainEqual({
name: "sys.unlisted.property",
value: "dropped",
});
expect(properties).toEqual([
{ name: "ro.build.version.release", value: "14" },
{ name: "ro.build.version.sdk", value: "34" },
{ name: "ro.build.version.security_patch", value: "2024-06-05" },
{ name: "ro.build.id", value: "AP21.240617.014" },
{
name: "ro.build.fingerprint",
value:
"google/sdk_gphone64_x86_64/emu64xa:14/AP21.240617.014/12094027:userdebug/dev-keys",
},
{ name: "ro.debuggable", value: "1" },
{ name: "ro.kernel.qemu", value: "1" },
{ name: "ro.product.brand", value: "google" },
{ name: "ro.product.device", value: "emu64xa" },
{ name: "ro.product.manufacturer", value: "Google" },
{ name: "ro.product.model", value: "sdk_gphone64_x86_64" },
{
name: "ro.product.cpu.abilist",
value: "x86_64,x86,arm64-v8a,armeabi-v7a,armeabi",
},
]);
});
it("reports an explicit non-emulator build without inventing a value", () => {
const dump = [
"[ro.build.version.release]: [13]",
"[ro.kernel.qemu]: [0]",
"",
].join("\n");
const { emulatorObserved } = parseGetpropOutput(dump);
expect(emulatorObserved).toBe(false);
});
it("reports the emulator indicator unknown when the build omits it", () => {
const { emulatorObserved } = parseGetpropOutput(
"[ro.build.version.sdk]: [33]\n",
);
expect(emulatorObserved).toBeNull();
});
});
describe("parsePackageListOutput", () => {
it("parses package lines with their base APK device paths", () => {
const output = [
"package:/data/app/~~xy==/com.example.npzyCvVb1I-YzKIsGFvVYw==/base.apk=com.example",
"package:/system/app/WebView/WebView.apk=com.android.webview",
"",
].join("\n");
const { packages, unparsedLines } = parsePackageListOutput(output);
expect(unparsedLines).toEqual([]);
expect(packages).toEqual([
{
package_name: "com.example",
base_apk_device_path:
"/data/app/~~xy==/com.example.npzyCvVb1I-YzKIsGFvVYw==/base.apk",
},
{
package_name: "com.android.webview",
base_apk_device_path: "/system/app/WebView/WebView.apk",
},
]);
});
it("keeps unrecognized lines for partial coverage reporting", () => {
const { packages, unparsedLines } = parsePackageListOutput(
"package:/data/app/x/base.apk=com.ok\ngarbage line\n",
);
expect(packages).toHaveLength(1);
expect(unparsedLines).toEqual(["garbage line"]);
});
});
describe("parsePmPathOutput", () => {
it("classifies base and split APKs from their file names", () => {
const output = [
"package:/data/app/~~a==/com.example.b==/base.apk",
"package:/data/app/~~a==/com.example.b==/split_config.arm64_v8a.apk",
"package:/data/app/~~a==/com.example.b==/split_config.en.apk",
"",
].join("\n");
const { paths, unparsedLines } = parsePmPathOutput(output);
expect(unparsedLines).toEqual([]);
expect(paths).toEqual([
{
device_path: "/data/app/~~a==/com.example.b==/base.apk",
file_name: "base.apk",
role: "base",
},
{
device_path:
"/data/app/~~a==/com.example.b==/split_config.arm64_v8a.apk",
file_name: "split_config.arm64_v8a.apk",
role: "split",
},
{
device_path: "/data/app/~~a==/com.example.b==/split_config.en.apk",
file_name: "split_config.en.apk",
role: "split",
},
]);
});
it("reports an unknown role for names that follow neither convention", () => {
const { paths } = parsePmPathOutput("package:/system/app/X/Y.apk\n");
expect(paths[0]).toEqual({
device_path: "/system/app/X/Y.apk",
file_name: "Y.apk",
role: "unknown",
});
});
});