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"); });