mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(android): ignore action-less filters in activity export defaults (#8030)
Android's package parser discards an activity, activity-alias or receiver intent filter that declares no action, so such a filter neither makes the component exported by default nor triggers the API 31 explicit-export requirement. Services keep action-less filters. The manifest inventory counted every declared filter, reporting exported_default true (or an explicit-export validation error) for components Android does not export. intent_filter_count still reports every declared filter.
This commit is contained in:
1 parent
810cebcba0
commit
d857b61bc5
4 files changed
+85
-3
No files matched your search
@@ -73,6 +73,9 @@ spellings; the original SDK declaration strings remain unchanged.
|
||||
it remains null for unknown provider target SDKs, nonliteral declarations, or
|
||||
components with filters targeting API 31 or later that omit the now-required
|
||||
explicit declaration. A limitation records the latter validation error.
|
||||
As in Android's package parser, an activity, alias or receiver filter without
|
||||
an `<action>` is discarded and does not count toward either rule; a service
|
||||
keeps such a filter. `intent_filter_count` still counts every declared filter.
|
||||
Provider authorities and all inventory dimensions are bounded. `status:
|
||||
"partial"` and `limitations` identify omitted declarations or values; a
|
||||
capacity limitation appears only when an actual collection limit was reached.
|
||||
|
||||
@@ -32,6 +32,20 @@ const projectedDataAttributes = new Set([
|
||||
const literalBoolean = (value: string | null): boolean | null =>
|
||||
value === "true" ? true : value === "false" ? false : null;
|
||||
|
||||
/** Whether a filter names an action, which Android requires to keep an activity filter. */
|
||||
const declaresAction = (filter: Element): boolean => {
|
||||
for (let index = 0; index < filter.childNodes.length; index += 1) {
|
||||
const member = filter.childNodes.item(index);
|
||||
if (
|
||||
isManifestElement(member) &&
|
||||
member.nodeName === "action" &&
|
||||
androidAttribute(member, "name") !== null
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** One decoded-manifest lifetime owns nested declarations and cumulative copy budgets. */
|
||||
export const createAndroidManifestDetailsReader = (
|
||||
source: string,
|
||||
@@ -99,6 +113,7 @@ export const createAndroidManifestDetailsReader = (
|
||||
let legacyValuesRemaining = limits.intentValues;
|
||||
let legacyTruncated = false;
|
||||
let filterCount = 0;
|
||||
let actionFilter = false;
|
||||
for (let index = 0; index < component.childNodes.length; index += 1) {
|
||||
const child = component.childNodes.item(index);
|
||||
if (!isManifestElement(child)) continue;
|
||||
@@ -111,6 +126,8 @@ export const createAndroidManifestDetailsReader = (
|
||||
continue;
|
||||
}
|
||||
if (child.nodeName !== "intent-filter") continue;
|
||||
// Checked past the filter bound too; one action-bearing filter suffices.
|
||||
actionFilter ||= declaresAction(child);
|
||||
const filterIndex = filterCount++;
|
||||
if (filterIndex >= limits.intentFilters) {
|
||||
partial = true;
|
||||
@@ -237,6 +254,7 @@ export const createAndroidManifestDetailsReader = (
|
||||
actions,
|
||||
categories,
|
||||
filterCount,
|
||||
actionFilter,
|
||||
legacyTruncated,
|
||||
partial,
|
||||
detailLimited,
|
||||
|
||||
@@ -454,11 +454,18 @@ export const parseAndroidManifestComponents = (
|
||||
: null;
|
||||
const exportedNonliteral =
|
||||
declaredExported !== null && exportedLiteral === null;
|
||||
// Android discards an activity, alias or receiver filter that declares
|
||||
// no action, while a service keeps it. Only kept filters select the
|
||||
// export default and the API 31 explicit-export requirement.
|
||||
const keptFilters =
|
||||
component.nodeName === "service"
|
||||
? details.filterCount > 0
|
||||
: details.actionFilter;
|
||||
const explicitExportRequired =
|
||||
declaredExported === null &&
|
||||
targetSdkLevel !== null &&
|
||||
targetSdkLevel >= 31 &&
|
||||
details.filterCount > 0 &&
|
||||
keptFilters &&
|
||||
(component.nodeName === "activity" ||
|
||||
component.nodeName === "activity-alias" ||
|
||||
component.nodeName === "service" ||
|
||||
@@ -466,7 +473,7 @@ export const parseAndroidManifestComponents = (
|
||||
const filteredExportUnknown =
|
||||
declaredExported === null &&
|
||||
targetSdkLevel === null &&
|
||||
details.filterCount > 0 &&
|
||||
keptFilters &&
|
||||
(component.nodeName === "activity" ||
|
||||
component.nodeName === "activity-alias" ||
|
||||
component.nodeName === "service" ||
|
||||
@@ -491,7 +498,7 @@ export const parseAndroidManifestComponents = (
|
||||
? null
|
||||
: component.nodeName === "provider"
|
||||
? providerDefault
|
||||
: details.filterCount > 0,
|
||||
: keptFilters,
|
||||
permission,
|
||||
permission_source: effectivePermissionSource,
|
||||
intent_filter_count: details.filterCount,
|
||||
|
||||
@@ -689,6 +689,60 @@ it("leaves target-31-or-later filtered components without an inferred export def
|
||||
).toMatchObject({ exported_default: null });
|
||||
});
|
||||
|
||||
it("ignores action-less activity, alias and receiver filters for export defaults", () => {
|
||||
// Android's package parser drops these filters; a service keeps them.
|
||||
const manifest = [
|
||||
'<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example"><application>',
|
||||
'<activity android:name=".Main"><intent-filter><category android:name="android.intent.category.DEFAULT"/></intent-filter></activity>',
|
||||
'<activity-alias android:name=".Alias" android:targetActivity=".Main"><intent-filter><data android:scheme="https"/></intent-filter></activity-alias>',
|
||||
'<receiver android:name=".Receiver"><intent-filter android:priority="1"/></receiver>',
|
||||
'<service android:name=".Service"><intent-filter><category android:name="example.BIND"/></intent-filter></service>',
|
||||
'<activity android:name=".Mixed"><intent-filter/><intent-filter><action android:name="example.OPEN"/></intent-filter></activity>',
|
||||
"</application></manifest>",
|
||||
].join("");
|
||||
const defaults = (sdk: string) =>
|
||||
parseAndroidManifestComponents(manifest, "complete", sdk).items.map(
|
||||
(item) => [item.intent_filter_count, item.exported_default],
|
||||
);
|
||||
expect(defaults("30")).toEqual([
|
||||
[1, false],
|
||||
[1, false],
|
||||
[1, false],
|
||||
[1, true],
|
||||
[2, true],
|
||||
]);
|
||||
const modern = parseAndroidManifestComponents(manifest, "complete", "31");
|
||||
expect(modern.items.map((item) => item.exported_default)).toEqual([
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
expect(modern.limitations.join(" ")).toContain(
|
||||
"2 component(s) with intent filters target API 31",
|
||||
);
|
||||
expect(defaults("Preview").map(([, value]) => value)).toEqual([
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
const filters = "<intent-filter/>".repeat(
|
||||
ANDROID_MANIFEST_LIMITS.intentFilters,
|
||||
);
|
||||
const late = parseAndroidManifestComponents(
|
||||
`<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example"><application><activity android:name=".Late">${filters}<intent-filter><action android:name="example.OPEN"/></intent-filter></activity></application></manifest>`,
|
||||
"complete",
|
||||
"30",
|
||||
);
|
||||
expect(late.items[0]).toMatchObject({
|
||||
intent_filter_count: ANDROID_MANIFEST_LIMITS.intentFilters + 1,
|
||||
exported_default: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores same-local-name attributes from other namespaces", () => {
|
||||
const manifest = [
|
||||
'<?xml version="1.0" encoding="utf-8"?>',
|
||||
|
||||
Reference in new issue
Block a user