Files
reverse-engineering-anything/src/adb/AdbShellOutput.test.ts
T

493 lines
15 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
parseAmStartOutput,
parseDumpsysPackageOutput,
parseWmDensityOutput,
parseWindowFocusOutput,
} from "./AdbShellOutput.js";
const packageDump = (installerLine: string | undefined): string =>
[
"Packages:",
" Package [com.example] (abcdef):",
" versionName=1.2.3",
...(installerLine === undefined ? [] : [` ${installerLine}`]),
" pkgFlags=[ HAS_CODE ]",
"",
].join("\n");
describe("parseDumpsysPackageOutput", () => {
it.each(["", " My Build ", " ", "null"])(
"preserves the printed version name %j",
(versionName) => {
const result = parseDumpsysPackageOutput(
` pkg=Package{abcdef com.example}\n versionName=${versionName}\n`,
);
expect(result.versionName).toBe(versionName);
},
);
it("keeps a reported installer package name", () => {
expect(
parseDumpsysPackageOutput(
packageDump("installerPackageName=com.android.vending"),
).installerPackageName,
).toBe("com.android.vending");
});
// Package Manager prints an absent String field as the text "null"; a
// missing or empty declaration means the same absence (morluto/rea#3943).
it.each([
["the literal null sentinel", "installerPackageName=null"],
["an empty value", "installerPackageName="],
["a missing line", undefined],
])("reports an absent installer as null for %s", (_case, line) => {
expect(parseDumpsysPackageOutput(packageDump(line))).toMatchObject({
versionName: "1.2.3",
installerPackageName: null,
});
});
// A manifest may declare the literal string "null" as its version name and
// dumpsys prints it the same way as an absent one, so the text is kept.
it("keeps a version name printed as the text null", () => {
expect(
parseDumpsysPackageOutput(
packageDump("installerPackageName=null").replace(
"versionName=1.2.3",
"versionName=null",
),
).versionName,
).toBe("null");
});
});
describe("parseWindowFocusOutput display contexts", () => {
it.each([false, true])(
"retains both display owners independent of block order: %s",
(reverse) => {
const inactive =
" Display: mDisplayId=3 rootTasks=0\n mCurrentFocus=null\n mFocusedApp=null\n";
const active =
" Display: mDisplayId=0 rootTasks=12\n mCurrentFocus=Window{active}\n mFocusedApp=ActivityRecord{active}\n";
const result = parseWindowFocusOutput(
reverse ? active + inactive : inactive + active,
);
expect(result).toMatchObject({
focused_window: null,
focused_app: null,
focus_summary: "ambiguous",
focus_contexts: expect.arrayContaining([
{
scope: "display",
display_id: "3",
header_line: " Display: mDisplayId=3 rootTasks=0",
declarations: [
{
field: "mCurrentFocus",
value: null,
raw_line: " mCurrentFocus=null",
},
{
field: "mFocusedApp",
value: null,
raw_line: " mFocusedApp=null",
},
],
},
{
scope: "display",
display_id: "0",
header_line: " Display: mDisplayId=0 rootTasks=12",
declarations: [
{
field: "mCurrentFocus",
value: "Window{active}",
raw_line: " mCurrentFocus=Window{active}",
},
{
field: "mFocusedApp",
value: "ActivityRecord{active}",
raw_line: " mFocusedApp=ActivityRecord{active}",
},
],
},
]),
});
},
);
it("does not pair declarations from different displays", () => {
expect(
parseWindowFocusOutput(
"Display: mDisplayId=0\nmCurrentFocus=Window{one}\nDisplay: mDisplayId=1\nmFocusedApp=ActivityRecord{two}\n",
),
).toMatchObject({
focus_summary: "ambiguous",
focused_window: null,
focused_app: null,
focus_contexts: [
{
display_id: "0",
declarations: [{ field: "mCurrentFocus", value: "Window{one}" }],
},
{
display_id: "1",
declarations: [
{ field: "mFocusedApp", value: "ActivityRecord{two}" },
],
},
],
});
});
it("retains an unknown display identity and missing fields without borrowing its neighbor", () => {
expect(
parseWindowFocusOutput(
"Display: vendor-format\nmCurrentFocus=null\nDisplay: mDisplayId=0\n",
),
).toMatchObject({
focus_summary: "ambiguous",
focus_contexts: [
{
scope: "display",
display_id: null,
header_line: "Display: vendor-format",
declarations: [{ field: "mCurrentFocus", value: null }],
},
{ scope: "display", display_id: "0", declarations: [] },
],
});
});
});
describe("parseWindowFocusOutput declaration presence", () => {
it("separates missing declarations from explicit absence and retains source lines", () => {
expect(parseWindowFocusOutput("no declarations\n")).toEqual({
focused_window: null,
focused_app: null,
focus_summary: "unavailable",
focus_contexts: [],
observed_lines: [],
});
expect(
parseWindowFocusOutput(" mCurrentFocus=null\nmFocusedApp=null\n"),
).toEqual({
focused_window: null,
focused_app: null,
focus_summary: "single_context",
focus_contexts: [
{
scope: "unscoped",
display_id: null,
header_line: null,
declarations: [
{
field: "mCurrentFocus",
value: null,
raw_line: " mCurrentFocus=null",
},
{ field: "mFocusedApp", value: null, raw_line: "mFocusedApp=null" },
],
},
],
observed_lines: [" mCurrentFocus=null", "mFocusedApp=null"],
});
});
it("retains repeated declarations without choosing one or erasing explicit absence", () => {
expect(
parseWindowFocusOutput(
"mCurrentFocus=null\nmCurrentFocus=Window{later}\nmFocusedApp=ActivityRecord{current}\n",
),
).toMatchObject({
focus_summary: "ambiguous",
focused_window: null,
focused_app: null,
focus_contexts: [
{
declarations: [
{ field: "mCurrentFocus", value: null },
{ field: "mCurrentFocus", value: "Window{later}" },
{ field: "mFocusedApp", value: "ActivityRecord{current}" },
],
},
],
observed_lines: [
"mCurrentFocus=null",
"mCurrentFocus=Window{later}",
"mFocusedApp=ActivityRecord{current}",
],
});
});
});
describe("parseDumpsysPackageOutput", () => {
it("reports unknown requested permissions when the package is null", () => {
expect(
parseDumpsysPackageOutput("Package [com.example] (abcdef):\n pkg=null\n")
.requestedPermissions,
).toBeNull();
});
it("reports unknown when package state and the permission section are absent", () => {
expect(
parseDumpsysPackageOutput(
"Package [com.example] (abcdef):\n versionCode=45\n",
).requestedPermissions,
).toBeNull();
});
it("reports zero only for a confirmed loaded package without the section", () => {
expect(
parseDumpsysPackageOutput(
"Package [com.example] (abcdef):\n pkg=Package{abcdef com.example}\n",
).requestedPermissions,
).toBe(0);
});
it("counts listed permissions when the section is available", () => {
expect(
parseDumpsysPackageOutput(
"Package [com.example] (abcdef):\n requested permissions:\n android.permission.INTERNET\n",
).requestedPermissions,
).toBe(1);
});
it("does not count permissions in the hidden system packages section", () => {
expect(
parseDumpsysPackageOutput(
[
"Package [com.example] (abcdef):",
" pkg=Package{abcdef com.example}",
" requested permissions:",
" android.permission.INTERNET",
"Hidden system packages:",
" requested permissions:",
" com.android.hidden.permission",
].join("\n"),
).requestedPermissions,
).toBe(1);
});
it("counts permission names containing hyphens and dollar signs", () => {
expect(
parseDumpsysPackageOutput(
[
"Package [com.example] (abcdef):",
" pkg=Package{abcdef com.example}",
" requested permissions:",
" com.example.permission-Hyphen",
" com.example.permission$Dollar",
].join("\n"),
).requestedPermissions,
).toBe(2);
});
it("counts Unicode permission names without ending the section", () => {
expect(
parseDumpsysPackageOutput(
[
"Package [com.example] (abcdef):",
" pkg=Package{abcdef com.example}",
" requested permissions:",
" com.example.permission-First",
" com.example.permission.ÉCRIRE WITH SPACE",
" android.permission.CAMERA",
" install permissions:",
" android.permission.CAMERA: granted=true",
].join("\n"),
).requestedPermissions,
).toBe(3);
});
it("ends requested permissions at a same-indent field", () => {
expect(
parseDumpsysPackageOutput(
[
"Package [com.example] (abcdef):",
" pkg=Package{abcdef com.example}",
" requested permissions:",
" android.permission.INTERNET",
" gids=[1000]",
" android.permission.CAMERA",
].join("\n"),
).requestedPermissions,
).toBe(1);
});
});
describe("parseWmDensityOutput", () => {
it("reads labeled physical and override densities", () => {
expect(
parseWmDensityOutput("Physical density: 420\nOverride density: 480\n"),
).toEqual({ physical: 420, override: 480 });
expect(parseWmDensityOutput("Physical density: 420\n")).toEqual({
physical: 420,
override: null,
});
});
// Older builds print the physical density as a bare number
// (morluto/rea#3942); it must not be lost as an unread declaration.
it("reads a bare numeric density as the physical density", () => {
expect(parseWmDensityOutput("420\n")).toEqual({
physical: 420,
override: null,
});
expect(parseWmDensityOutput("420\r\n")).toEqual({
physical: 420,
override: null,
});
});
it("prefers a labeled physical density over a bare number", () => {
expect(parseWmDensityOutput("560\nPhysical density: 420\n")).toEqual({
physical: 420,
override: null,
});
});
it("reports no density for unrecognized output", () => {
expect(parseWmDensityOutput("")).toEqual({
physical: null,
override: null,
});
expect(parseWmDensityOutput("420dpi\n")).toEqual({
physical: null,
override: null,
});
});
});
const starting =
"Starting: Intent { act=android.intent.action.MAIN cmp=com.example/.Main }";
describe("parseAmStartOutput", () => {
it("recognizes a CRLF wait-timeout report", () => {
expect(
parseAmStartOutput(
`${starting}\r\nStatus: timeout\r\nActivity: com.example/.Main\r\nComplete\r\n`,
),
).toMatchObject({
status: "refused",
activity: "com.example/.Main",
reportedStatus: "timeout",
reason: "am start reported status: timeout",
});
});
it("treats every non-ok status as refusal, including a later status after ok", () => {
expect(
parseAmStartOutput(`${starting}\nStatus: ok\nStatus: timeout\n`),
).toMatchObject({
status: "refused",
reportedStatus: "timeout",
reason: "am start reported status: timeout",
});
expect(
parseAmStartOutput(`${starting}\nStatus: future-value\n`),
).toMatchObject({
status: "refused",
reportedStatus: "future-value",
});
});
it("gives hard errors in either stream priority over status and attempt", () => {
expect(
parseAmStartOutput(
`${starting}\nStatus: timeout\nActivity: com.example/.Main\n`,
"Exception: Activity class does not exist\n",
),
).toEqual({
status: "refused",
startedActivity: "act=android.intent.action.MAIN cmp=com.example/.Main",
activity: "com.example/.Main",
reportedStatus: "timeout",
reason: "Activity class does not exist",
});
expect(
parseAmStartOutput(
`${starting}\nStatus: ok\nError: Activity class does not exist\n`,
),
).toMatchObject({
status: "refused",
reason: "Activity class does not exist",
});
});
it("refuses an unprefixed Java exception even when Starting is present", () => {
expect(
parseAmStartOutput(
`${starting}\njava.lang.SecurityException: Permission Denial\n`,
),
).toMatchObject({
status: "refused",
reason: "java.lang.SecurityException: Permission Denial",
});
});
it("refuses a message-free Java exception even when Starting is present", () => {
expect(
parseAmStartOutput(`${starting}\njava.lang.NullPointerException\n`),
).toMatchObject({
status: "refused",
reason: "java.lang.NullPointerException",
});
});
it("recognizes a base Java exception type with a message", () => {
expect(
parseAmStartOutput(`${starting}\njava.lang.Exception: failure\n`),
).toMatchObject({
status: "refused",
reason: "java.lang.Exception: failure",
});
});
it("prefers a specific Java exception over generic error headers", () => {
expect(
parseAmStartOutput(
[
starting,
"Error type 3",
"Exception occurred while executing command",
"java.lang.SecurityException: Permission Denial",
].join("\n"),
),
).toMatchObject({
status: "refused",
reason: "java.lang.SecurityException: Permission Denial",
});
expect(
parseAmStartOutput(
[
starting,
"Error: Generic launch failure",
"java.lang.SecurityException: Permission Denial",
].join("\n"),
),
).toMatchObject({
status: "refused",
reason: "java.lang.SecurityException: Permission Denial",
});
});
it("keeps Starting-only output compatible and refuses no-attempt output", () => {
expect(parseAmStartOutput(starting)).toMatchObject({
status: "started",
reportedStatus: null,
activity: null,
});
expect(
parseAmStartOutput("Status: ok\nWarning: Activity not started\n"),
).toMatchObject({
status: "refused",
startedActivity: null,
reportedStatus: "ok",
});
});
});