import { expect, it, vi } from "vitest";
import { androidResultSchemas } from "../../../src/domain/android/androidAnalysis.js";
import {
ANDROID_MANIFEST_LIMITS,
parseAndroidManifestComponents,
resolveAndroidComponentName,
} from "../../../src/android/AndroidManifestComponents.js";
it.each([
[".sub.Target", "sub.Target", "partial"],
["com.example.sub.Target", "sub.Target", "partial"],
[".sub.Target", ".sub.Target", "complete"],
[".sub.Target", "com.example.sub.Target", "complete"],
["com.example.sub.Target", ".sub.Target", "complete"],
["com.example.sub.Target", "com.example.sub.Target", "complete"],
[".Target", "Target", "complete"],
["sub.Target", "sub.Target", "complete"],
] as const)(
"keeps alias class identity for %s targeting %s",
(activity, target, status) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.status).toBe(status);
expect(inventory.items.at(-1)?.target_activity).toBe(target);
const limitations = inventory.limitations.join(" ");
if (status === "partial")
expect(limitations).toContain(
"do not match a preceding activity declaration",
);
else expect(limitations).not.toContain("targetActivity value(s)");
},
);
it("keeps pre-projection package parses independent", () => {
const older = {
engine: {
name: "jadx",
version: null,
artifact_sha256: "a".repeat(64),
source_revision: null,
worker_count: 1,
heap_limit_mib: null,
},
package_name: "com.example",
version_name: null,
version_code: null,
min_sdk: null,
target_sdk: "33",
permissions: [],
manifest: {
status: "complete",
text: '',
reported_total_bytes: null,
},
class_count: 0,
resource_count: 0,
signature_verification: "not_performed",
};
const first = androidResultSchemas.inspect_android_package.parse(older);
expect(first.components).toMatchObject({ status: "unavailable", items: [] });
first.components.limitations.push("local parse mutation");
try {
const second = androidResultSchemas.inspect_android_package.parse(older);
expect(second.components.limitations).not.toContain("local parse mutation");
} finally {
first.components.limitations.pop();
}
});
const MANIFEST = `
`;
it.each([".9Activity", ".hyphen-name", ".😀Activity", ".nested.7"])(
"resolves DEX component name %s and its alias reference",
(name) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.items[0]?.name).toBe(`com.example${name}`);
expect(inventory.items[1]?.target_activity).toBe(name);
expect(inventory.status).toBe("complete");
},
);
it.each([
["0x10", true, true],
["0X11", false, true],
["0x1f", false, null],
] as const)(
"interprets decoded hexadecimal target SDK %s",
(sdk, providerDefault, activityDefault) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
sdk,
);
expect(inventory.items.map((item) => item.exported_default)).toEqual([
providerDefault,
activityDefault,
]);
},
);
it("uses a decoded hexadecimal minimum SDK when target SDK is omitted", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
null,
"0x10",
);
expect(inventory.items[0]?.exported_default).toBe(true);
});
it.each([
"0x",
"0xgg",
"1e1",
"-1",
"0",
"2147483648",
"0x80000000",
"Preview",
])("keeps unsupported SDK spelling %s unknown", (sdk) => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
sdk,
);
expect(inventory.items[0]?.exported_default).toBeNull();
});
it.each([
[
"application",
'',
[],
[],
],
[
"component",
'',
[],
[],
],
[
"filter",
'',
["activity"],
[],
],
[
"action",
'',
["activity"],
["REAL"],
],
[
"category",
'',
["activity"],
["REAL"],
],
] as const)(
"ignores foreign namespace %s elements",
(_kind, body, kinds, actions) => {
const inventory = parseAndroidManifestComponents(
`${body}`,
"complete",
"30",
);
expect(inventory.status).toBe("complete");
expect(inventory.items.map((item) => item.kind)).toEqual(kinds);
expect(inventory.items[0]?.actions ?? []).toEqual(actions);
expect(inventory.items[0]?.categories ?? []).toEqual([]);
if (_kind === "filter")
expect(inventory.items[0]?.intent_filter_count).toBe(0);
},
);
it("refuses a namespaced manifest root", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"30",
);
expect(inventory.status).toBe("unavailable");
expect(inventory.items).toEqual([]);
});
it("does not index foreign activities as alias targets", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"30",
);
expect(inventory.items.map((item) => item.kind)).toEqual(["activity-alias"]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"do not match a preceding activity declaration",
);
});
it("projects declared components with resolved names and export defaults", () => {
const inventory = parseAndroidManifestComponents(MANIFEST, "complete", "30");
expect(inventory.status).toBe("complete");
expect(inventory.limitations).toEqual([]);
expect(inventory.items.map(({ kind, name }) => [kind, name])).toEqual([
["activity", "com.example.app.MainActivity"],
["activity", "com.example.app.DetailActivity"],
["activity", "com.example.app.HiddenActivity"],
["activity-alias", "com.example.app.AliasActivity"],
["service", "com.example.app.SyncService"],
["receiver", "com.example.app.BootReceiver"],
["provider", "com.example.app.DataProvider"],
]);
expect(inventory.items[0]).toMatchObject({
declared_name: ".MainActivity",
exported: true,
exported_default: true,
actions: ["android.intent.action.MAIN"],
categories: ["android.intent.category.LAUNCHER"],
});
// Absent android:exported keeps the declaration and the default separate.
expect(inventory.items[1]).toMatchObject({
exported: null,
exported_default: true,
});
expect(inventory.items[2]).toMatchObject({
exported: false,
exported_default: true,
});
expect(inventory.items[3]).toMatchObject({
target_activity: ".MainActivity",
});
expect(inventory.items[4]).toMatchObject({
permission: "com.example.app.SYNC",
foreground_service_type: "dataSync",
actions: [],
});
expect(inventory.items[5]).toMatchObject({
actions: ["android.intent.action.BOOT_COMPLETED"],
categories: [],
});
expect(inventory.items[6]?.authorities).toEqual([
"com.example.app.data",
"com.example.app.backup",
]);
});
it("inherits an application permission when a component omits its own", () => {
const manifest = MANIFEST.replace(
'',
'',
);
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(inventory.items[0]?.permission).toBe("com.example.APP");
expect(inventory.items[0]?.permission_source).toBe("application");
expect(inventory.items[4]?.permission).toBe("com.example.app.SYNC");
expect(inventory.items[4]?.permission_source).toBe("component");
});
it("preserves exact action, category, and effective permission strings", () => {
const manifest =
'';
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(inventory.items.map(({ permission }) => permission)).toEqual([
" P ",
" C ",
]);
expect(inventory.items[0]).toMatchObject({
actions: [" OPEN "],
categories: [" "],
permission_source: "application",
});
expect(inventory.status).toBe("complete");
const whitespacePermission = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(whitespacePermission.items[0]).toMatchObject({
permission: " ",
permission_source: "application",
});
});
it.each([
".Bad/Main",
".Bad//Main",
"com.example.Bad/Name",
"com..example.Bad",
"com.example.Bad;Name",
"com.example.Bad[Name",
])(
"retains malformed component class name %s without resolving it",
(declaredName) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.items[0]).toMatchObject({
declared_name: declaredName,
name: declaredName,
});
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"do not fit DEX binary class-name syntax",
);
},
);
it("accepts Java nested and Unicode class identifiers", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.items.map(({ name }) => name)).toEqual([
"com.example.Outer$Inner",
"com.example.Δelta",
]);
expect(inventory.status).toBe("complete");
});
it("does not inherit application permission through an explicit empty override", () => {
const manifest =
'';
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(inventory.items[0]).toMatchObject({
permission: null,
permission_source: "none",
});
});
it("marks unprojected provider read and write permission overrides partial", () => {
const manifest =
'';
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(
inventory.items.map(({ permission, permission_source }) => [
permission,
permission_source,
]),
).toEqual([
[null, "none"],
[null, "none"],
["com.example.GENERIC", "component"],
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"operation-specific permission overrides are retained",
);
});
it("does not apply application or target permissions through an activity alias", () => {
const manifest =
'';
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(inventory.items[0]).toMatchObject({
permission: "com.example.TARGET",
permission_source: "component",
});
expect(inventory.items[1]).toMatchObject({
permission: null,
permission_source: "none",
});
});
it("keeps symbolic effective permissions unknown without resource resolution", () => {
const manifest =
'';
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(
inventory.items.map(({ permission, permission_source }) => [
permission,
permission_source,
]),
).toEqual([
[null, "unknown"],
[null, "unknown"],
[null, "unknown"],
["com.example.APP", "application"],
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"unresolved resource or manifest-placeholder tokens",
);
});
it("bounds inherited permission copies across the inventory", () => {
const permission = "p".repeat(2_050);
const declarations = Array.from(
{ length: ANDROID_MANIFEST_LIMITS.components },
(_, index) => ``,
).join("");
const inventory = parseAndroidManifestComponents(
`${declarations}`,
"complete",
"33",
);
expect(inventory.items[0]?.permission_source).toBe("application");
expect(inventory.items.at(-1)).toMatchObject({
permission: null,
permission_source: "unknown",
});
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain("total permission bytes");
expect(
inventory.items.reduce(
(bytes, item) => bytes + Buffer.byteLength(item.permission ?? ""),
0,
),
).toBeLessThanOrEqual(ANDROID_MANIFEST_LIMITS.permissionBytes);
});
it("measures an oversized inherited permission once for the whole inventory", () => {
const permission = "p".repeat(ANDROID_MANIFEST_LIMITS.permissionBytes + 1);
const declarations = Array.from(
{ length: ANDROID_MANIFEST_LIMITS.components },
(_, index) => ``,
).join("");
const byteLength = vi.spyOn(Buffer, "byteLength");
const includes = vi.spyOn(String.prototype, "includes");
try {
const inventory = parseAndroidManifestComponents(
`${declarations}`,
"complete",
"33",
);
expect(
byteLength.mock.calls.filter(([value]) => value === permission),
).toHaveLength(1);
expect(
byteLength.mock.calls.filter(([value]) => value === "com.example"),
).toHaveLength(1);
// Unresolved-token classification checks three markers once at the
// application owner, not once for every inherited component copy.
expect(
includes.mock.contexts.filter((value) => value === permission),
).toHaveLength(3);
expect(inventory.items).toHaveLength(ANDROID_MANIFEST_LIMITS.components);
expect(
inventory.items.every((item) => item.permission_source === "unknown"),
).toBe(true);
expect(inventory.status).toBe("partial");
} finally {
includes.mockRestore();
byteLength.mockRestore();
}
});
it("retains forbidden controls and empty component names", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(
inventory.items.map(({ declared_name, name }) => [declared_name, name]),
).toEqual([
["\t.Main\t", "\t.Main\t"],
["", ""],
[".Valid", "com.example.Valid"],
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"DEX binary class-name syntax",
);
});
it("applies the provider export default from the target SDK", () => {
// API 16 and below default providers to exported; API 17 and up do not.
const legacy = parseAndroidManifestComponents(MANIFEST, "complete", "16");
expect(legacy.items[6]).toMatchObject({
kind: "provider",
exported: false,
exported_default: true,
});
const modern = parseAndroidManifestComponents(MANIFEST, "complete", "33");
expect(modern.items[6]?.exported_default).toBe(false);
// A reported but unrecognized SDK stays unknown; omission has a legacy default.
const unknown = parseAndroidManifestComponents(
MANIFEST,
"complete",
"Preview",
);
expect(unknown.items[6]?.exported_default).toBeNull();
expect(unknown.limitations.join(" ")).toContain("target SDK is unknown");
});
it("leaves target-31-or-later filtered components without an inferred export default", () => {
const manifest =
'';
const modern = parseAndroidManifestComponents(manifest, "complete", "31");
expect(modern.items[0]).toMatchObject({
exported: null,
exported_default: null,
});
expect(modern.limitations.join(" ")).toContain("requires an explicit value");
expect(
parseAndroidManifestComponents(manifest, "complete", "30").items[0],
).toMatchObject({ exported_default: true });
expect(
parseAndroidManifestComponents(manifest, "complete", "Preview").items[0],
).toMatchObject({ exported_default: null });
});
it("ignores same-local-name attributes from other namespaces", () => {
const manifest = [
'',
'',
" ",
' ',
" ",
"",
].join("\n");
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(inventory.items).toHaveLength(1);
expect(inventory.items[0]).toMatchObject({
declared_name: ".Real",
name: "com.example.app.Real",
exported: false,
exported_default: false,
});
});
it("resolves relative names against the manifest package", () => {
expect(resolveAndroidComponentName("com.example.app", ".Main")).toBe(
"com.example.app.Main",
);
expect(resolveAndroidComponentName("com.example.app", "Main")).toBe(
"com.example.app.Main",
);
expect(resolveAndroidComponentName("com.example.app", "org.other.Main")).toBe(
"org.other.Main",
);
expect(resolveAndroidComponentName(null, ".Main")).toBe(".Main");
});
it("keeps relative names unresolved when the manifest package is absent", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.items.map(({ name }) => name)).toEqual([
".Relative",
"Absolute.Name",
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"manifest package is absent",
);
});
it("does not trim malformed package names into resolved component identities", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.items[0]).toMatchObject({
declared_name: ".Main",
name: ".Main",
});
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"package has surrounding whitespace",
);
});
it("does not resolve package-relative names against malformed package syntax", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(
inventory.items.map(({ declared_name, name }) => [declared_name, name]),
).toEqual([
[".Foo", ".Foo"],
["Worker", "Worker"],
["org.other.Valid", "org.other.Valid"],
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"does not fit the supported Java package-name syntax",
);
expect(inventory.limitations.join(" ")).toContain(
"package-relative component name(s) remain unresolved",
);
});
it("preserves exact activity-alias targets and marks malformed references unknown", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.items.map(({ target_activity }) => target_activity)).toEqual(
[
null,
null,
" .Target ",
"",
".Target",
"com.example.Target",
".Bare",
null,
".DeclaredLater",
null,
],
);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"do not match a preceding activity declaration",
);
});
it("preserves unresolved aliases without expanding an unsupported huge package", () => {
const packageName = "p".repeat(900 * 1024);
const aliases = Array.from(
{ length: 511 },
(_, index) =>
``,
).join("");
const inventory = parseAndroidManifestComponents(
`${aliases}`,
"complete",
"33",
);
expect(inventory.items[0]?.name).toBe(".Target");
expect(
inventory.items.every(({ name }) => !name.startsWith(packageName)),
).toBe(true);
expect(inventory.items[1]).toMatchObject({
declared_name: ".Alias0",
target_activity: ".Target",
});
expect(inventory.items.at(-1)).toMatchObject({
declared_name: ".Alias510",
target_activity: ".Target",
});
expect(inventory.items).toHaveLength(ANDROID_MANIFEST_LIMITS.components);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain("targetActivity value(s)");
});
it("does not call resource and manifest placeholder names resolved identities", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.items.map(({ name }) => name)).toEqual([
"@string/component_class",
"${applicationId}.Worker",
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"unresolved resource or manifest placeholder",
);
const placeholderPackage = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(placeholderPackage.items[0]?.name).toBe(".Main");
expect(placeholderPackage.status).toBe("partial");
expect(placeholderPackage.limitations.join(" ")).toContain(
"package is absent or contains",
);
});
it("does not establish a component inventory from truncated manifest text", () => {
const inventory = parseAndroidManifestComponents(MANIFEST, "partial");
expect(inventory.status).toBe("partial");
expect(inventory.items).toEqual([]);
expect(inventory.limitations[0]).toContain("truncated");
});
it("reports unavailable inventory for malformed or unexpected manifests", () => {
expect(
parseAndroidManifestComponents("", "complete"),
).toMatchObject({ status: "unavailable", items: [] });
expect(
parseAndroidManifestComponents(
'',
"complete",
),
).toMatchObject({ status: "unavailable", items: [] });
expect(
parseAndroidManifestComponents(
'',
"complete",
),
).toMatchObject({ status: "unavailable", items: [] });
});
it("bounds the component inventory and says so", () => {
const repeated = Array.from(
{ length: ANDROID_MANIFEST_LIMITS.components + 1 },
(_, index) => ``,
).join("");
const inventory = parseAndroidManifestComponents(
`${repeated}`,
"complete",
"33",
);
expect(inventory.status).toBe("partial");
expect(inventory.items).toHaveLength(ANDROID_MANIFEST_LIMITS.components);
expect(inventory.limitations.join(" ")).toContain(
String(ANDROID_MANIFEST_LIMITS.components),
);
});
it("shares one intent-value budget across actions and categories", () => {
const values = Array.from(
{ length: 80 },
(_, index) =>
``,
).join("");
const inventory = parseAndroidManifestComponents(
`${values}`,
"complete",
"33",
);
const first = inventory.items[0];
expect(first).toBeDefined();
expect((first?.actions.length ?? 0) + (first?.categories.length ?? 0)).toBe(
ANDROID_MANIFEST_LIMITS.intentValues,
);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain("values per component");
});
it("bounds provider authorities", () => {
const authorities = Array.from(
{ length: ANDROID_MANIFEST_LIMITS.authorities + 5 },
(_, index) => `auth${String(index)}`,
).join(";");
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.items[0]?.authorities).toHaveLength(
ANDROID_MANIFEST_LIMITS.authorities,
);
expect(inventory.status).toBe("partial");
});
it("bounds total authority bytes across providers", () => {
const first = "a".repeat(900_000);
const second = "b".repeat(200_000);
const manifest = ``;
const inventory = parseAndroidManifestComponents(manifest, "complete", "33");
expect(inventory.items[0]?.authorities?.[0]?.length).toBe(900_000);
expect(inventory.items[1]?.authorities).toEqual([]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain("total authority bytes");
});
it("preserves malformed provider authority segments and marks them unknown", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.items[0]?.authorities).toEqual([
" com.example.p ",
"",
"com.example.q",
"",
]);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"provider authority segment(s) are empty or have surrounding whitespace",
);
});
it("leaves later names declared when the resolved-name budget is spent", () => {
const hugeName = "A".repeat(64 * 1024);
const names = Array.from(
{ length: 20 },
(_, index) => ``,
).join("");
const inventory = parseAndroidManifestComponents(
`${names}`,
"complete",
"33",
);
expect(inventory.items.length).toBeGreaterThan(1);
expect(inventory.items[0]?.name).toBe(`com.example.${hugeName}0`);
const unresolved = inventory.items.filter(({ name }) => name.startsWith("."));
expect(unresolved.length).toBeGreaterThan(0);
expect(unresolved.every(({ name }) => name.startsWith(`.${hugeName}`))).toBe(
true,
);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"resolved-name byte budget",
);
expect(inventory.limitations.join(" ")).not.toContain(
"component inventory stops at",
);
});
it("does not let the first component bypass the resolved-name byte budget", () => {
const hugeName =
"." + "A".repeat(ANDROID_MANIFEST_LIMITS.resolvedNameBytes + 1);
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.items[0]?.name).toBe(hugeName);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"resolved-name byte budget",
);
});
it("measures resolved names in UTF-8 bytes before concatenating them", () => {
const multibyteName = "." + "界".repeat(400_000);
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.items[0]?.name).toBe(multibyteName);
expect(inventory.status).toBe("partial");
expect(inventory.limitations.join(" ")).toContain(
"resolved-name byte budget",
);
});
it("marks the inventory partial when nameless declarations are dropped", () => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.status).toBe("partial");
expect(inventory.items).toHaveLength(1);
expect(inventory.limitations.join(" ")).toContain(
"1 component declaration(s) without android:name",
);
expect(inventory.limitations.join(" ")).not.toContain(
"component inventory stops at",
);
});
it("does not warn about providers that declare android:exported", () => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"Preview",
);
expect(inventory.limitations.join(" ")).toContain("1 provider(s) omit");
expect(inventory.limitations.join(" ")).not.toContain("2 provider(s) omit");
});
it("counts unnamed declarations against the component bound", () => {
const unnamed = Array.from(
{ length: ANDROID_MANIFEST_LIMITS.components + 1 },
() => "",
).join("");
const inventory = parseAndroidManifestComponents(
`${unnamed}`,
"complete",
"33",
);
expect(inventory.status).toBe("partial");
expect(inventory.items).toHaveLength(0);
expect(inventory.limitations.join(" ")).toContain(
`${String(ANDROID_MANIFEST_LIMITS.components)} component`,
);
});
it("distinguishes nonliteral android:exported values from omission", () => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"Preview",
);
const resource = inventory.items.find(({ name }) =>
name.endsWith(".Resource"),
);
expect(resource).toMatchObject({ exported: null, exported_default: null });
const empty = inventory.items.find(({ name }) => name.endsWith(".Empty"));
expect(empty).toMatchObject({ exported: null, exported_default: null });
const trueCase = inventory.items.find(({ name }) => name.endsWith(".True"));
expect(trueCase).toMatchObject({ exported: null, exported_default: null });
const joined = inventory.limitations.join(" ");
expect(joined).toContain("1 provider(s) omit");
expect(joined).toContain(
"3 component declaration(s) set android:exported to a value other than the literals true or false",
);
expect(inventory.status).toBe("complete");
});
it("keeps a known SDK default from governing a nonliteral exported value", () => {
const legacy = parseAndroidManifestComponents(
``,
"complete",
"16",
);
expect(legacy.items[0]).toMatchObject({
exported: null,
exported_default: null,
});
const modern = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(modern.items[0]).toMatchObject({
exported: null,
exported_default: null,
});
expect(modern.limitations.join(" ")).toContain(
"1 component declaration(s) set android:exported",
);
});
it.each([
"com._hidden",
"com.$hidden",
"com.Δelta",
"com.9bad",
"Single",
"com..example",
".com.example",
"com.example.",
"a." + "b".repeat(222),
])(
"preserves relative class identities under an unsupported Android package %s",
(packageName) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.status).toBe("partial");
expect(inventory.items.map(({ name }) => name)).toEqual([
".Main",
".Alias",
]);
expect(inventory.items[1]?.target_activity).toBe(".Main");
},
);
it.each(["android", "com.Example_9", "a." + "b".repeat(221)])(
"qualifies names under the supported Android package %s",
(packageName) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.status).toBe("complete");
expect(inventory.items.map(({ name }) => name)).toEqual([
`${packageName}.Main`,
`${packageName}.Alias`,
]);
},
);
it.each([
[".Hidden ", ".Hidden ", "com.example.Hidden "],
[" Hidden", ". Hidden", "com.example. Hidden"],
[". Hidden ", "com.example. Hidden ", "com.example. Hidden "],
[" ", ". ", "com.example. "],
[" other.Hidden ", " other.Hidden ", " other.Hidden "],
])(
"preserves DEX boundary spaces in %j and alias %j",
(declared, target, expected) => {
const inventory = parseAndroidManifestComponents(
``,
"complete",
"33",
);
expect(inventory.status).toBe("complete");
expect(inventory.items[0]).toMatchObject({
declared_name: declared,
name: expected,
});
expect(inventory.items[1]?.target_activity).toBe(target);
},
);
it("keeps distinct DEX spellings distinct in alias matching", () => {
const inventory = parseAndroidManifestComponents(
'',
"complete",
"33",
);
expect(inventory.status).toBe("partial");
expect(inventory.items[1]?.target_activity).toBe(".Hidden ");
});
it.each([
null,
"",
" ",
" location ",
"location|dataSync",
"@string/service_type",
])("preserves the declared foreground service type %j", (declared) => {
const attribute =
declared === null ? "" : ` android:foregroundServiceType="${declared}"`;
const inventory = parseAndroidManifestComponents(
``,
"complete",
"30",
);
expect(inventory.items[0]?.foreground_service_type).toBe(declared);
expect(inventory.status).toBe("complete");
});