mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
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.
268 lines
8.7 KiB
TypeScript
268 lines
8.7 KiB
TypeScript
import type { Element } from "@xmldom/xmldom";
|
|
import type { z } from "zod";
|
|
import type {
|
|
AndroidManifestComponent,
|
|
androidIntentDataSchema,
|
|
androidIntentFilterSchema,
|
|
androidComponentMetadataSchema,
|
|
} from "../domain/android/androidManifestComponents.js";
|
|
import {
|
|
androidAttribute,
|
|
createManifestLocationReader,
|
|
isAndroidAttribute,
|
|
isManifestElement,
|
|
} from "./AndroidManifestXml.js";
|
|
|
|
type Filter = z.output<typeof androidIntentFilterSchema>;
|
|
type Data = z.output<typeof androidIntentDataSchema>;
|
|
type Metadata = z.output<typeof androidComponentMetadataSchema>;
|
|
|
|
const projectedDataAttributes = new Set([
|
|
"scheme",
|
|
"host",
|
|
"port",
|
|
"path",
|
|
"pathPrefix",
|
|
"pathPattern",
|
|
"pathAdvancedPattern",
|
|
"pathSuffix",
|
|
"mimeType",
|
|
]);
|
|
|
|
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,
|
|
limits: {
|
|
readonly intentFilters: number;
|
|
readonly intentValues: number;
|
|
readonly processBytes: number;
|
|
readonly detailBytes: number;
|
|
},
|
|
) => {
|
|
const locationOf = createManifestLocationReader(source);
|
|
let processRemaining = limits.processBytes;
|
|
let detailsRemaining = limits.detailBytes;
|
|
return (component: Element, declarationIndex: number) => {
|
|
let partial = false;
|
|
let detailLimited = false;
|
|
let processLimited = false;
|
|
let unsupported = 0;
|
|
let remainingRecords = limits.intentValues;
|
|
const take = (value: string | null): string | null => {
|
|
if (value === null) return null;
|
|
const bytes = Buffer.byteLength(value);
|
|
if (bytes > detailsRemaining) {
|
|
partial = true;
|
|
detailLimited = true;
|
|
return null;
|
|
}
|
|
detailsRemaining -= bytes;
|
|
return value;
|
|
};
|
|
const admit = (): boolean => {
|
|
if (remainingRecords === 0) {
|
|
partial = true;
|
|
detailLimited = true;
|
|
return false;
|
|
}
|
|
remainingRecords -= 1;
|
|
return true;
|
|
};
|
|
const enabledDeclaration = androidAttribute(component, "enabled");
|
|
const enabled = literalBoolean(enabledDeclaration);
|
|
const grantDeclaration =
|
|
component.nodeName === "provider"
|
|
? androidAttribute(component, "grantUriPermissions")
|
|
: null;
|
|
const grant = literalBoolean(grantDeclaration);
|
|
const declaredProcess = androidAttribute(component, "process");
|
|
let process: string | null = null;
|
|
if (declaredProcess !== null) {
|
|
const bytes = Buffer.byteLength(declaredProcess);
|
|
if (bytes > processRemaining) {
|
|
partial = true;
|
|
processLimited = true;
|
|
} else {
|
|
process = declaredProcess;
|
|
processRemaining -= bytes;
|
|
}
|
|
}
|
|
const filters: Filter[] = [];
|
|
const metadata: Metadata[] = [];
|
|
const actions: string[] = [];
|
|
const categories: string[] = [];
|
|
const actionNames = new Set<string>();
|
|
const categoryNames = new Set<string>();
|
|
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;
|
|
if (child.nodeName === "meta-data") {
|
|
if (admit())
|
|
metadata.push({
|
|
name: take(androidAttribute(child, "name")),
|
|
location: locationOf(child),
|
|
});
|
|
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;
|
|
legacyTruncated = true;
|
|
continue;
|
|
}
|
|
const filterActions: string[] = [];
|
|
const filterCategories: string[] = [];
|
|
const data: Data[] = [];
|
|
let filterPartial = false;
|
|
const priority = take(androidAttribute(child, "priority"));
|
|
for (let at = 0; at < child.childNodes.length; at += 1) {
|
|
const member = child.childNodes.item(at);
|
|
if (!isManifestElement(member)) continue;
|
|
if (member.nodeName === "action" || member.nodeName === "category") {
|
|
const value = androidAttribute(member, "name");
|
|
if (value === null) continue;
|
|
const names =
|
|
member.nodeName === "action" ? actionNames : categoryNames;
|
|
if (!names.has(value)) {
|
|
if (legacyValuesRemaining === 0) legacyTruncated = true;
|
|
else {
|
|
legacyValuesRemaining -= 1;
|
|
names.add(value);
|
|
(member.nodeName === "action" ? actions : categories).push(value);
|
|
}
|
|
}
|
|
if (!admit()) {
|
|
filterPartial = true;
|
|
continue;
|
|
}
|
|
const copied = take(value);
|
|
if (copied === null) {
|
|
filterPartial = true;
|
|
continue;
|
|
}
|
|
(member.nodeName === "action"
|
|
? filterActions
|
|
: filterCategories
|
|
).push(copied);
|
|
} else if (member.nodeName === "data") {
|
|
if (!admit()) {
|
|
filterPartial = true;
|
|
continue;
|
|
}
|
|
for (
|
|
let attributeIndex = 0;
|
|
attributeIndex < member.attributes.length;
|
|
attributeIndex += 1
|
|
) {
|
|
const attribute = member.attributes.item(attributeIndex);
|
|
if (
|
|
attribute !== null &&
|
|
isAndroidAttribute(attribute) &&
|
|
(attribute.localName === null ||
|
|
!projectedDataAttributes.has(attribute.localName))
|
|
) {
|
|
// A retained data declaration can have unprojected matching constraints.
|
|
unsupported += 1;
|
|
filterPartial = true;
|
|
partial = true;
|
|
break;
|
|
}
|
|
}
|
|
data.push({
|
|
location: locationOf(member),
|
|
scheme: take(androidAttribute(member, "scheme")),
|
|
host: take(androidAttribute(member, "host")),
|
|
port: take(androidAttribute(member, "port")),
|
|
path: take(androidAttribute(member, "path")),
|
|
path_prefix: take(androidAttribute(member, "pathPrefix")),
|
|
path_pattern: take(androidAttribute(member, "pathPattern")),
|
|
path_advanced_pattern: take(
|
|
androidAttribute(member, "pathAdvancedPattern"),
|
|
),
|
|
path_suffix: take(androidAttribute(member, "pathSuffix")),
|
|
mime_type: take(androidAttribute(member, "mimeType")),
|
|
});
|
|
} else {
|
|
// Newer platform filter constructs remain source-accessible, never an empty match claim.
|
|
unsupported += 1;
|
|
filterPartial = true;
|
|
partial = true;
|
|
}
|
|
}
|
|
filters.push({
|
|
source_index: filterIndex,
|
|
location: locationOf(child),
|
|
priority,
|
|
actions: filterActions,
|
|
categories: filterCategories,
|
|
data,
|
|
coverage: filterPartial || detailLimited ? "partial" : "complete",
|
|
});
|
|
}
|
|
const enabledRaw = take(enabledDeclaration);
|
|
const grantRaw = take(grantDeclaration);
|
|
const facts = {
|
|
declaration_index: declarationIndex,
|
|
location: locationOf(component),
|
|
declaration_coverage: partial ? "partial" : "complete",
|
|
enabled,
|
|
enabled_declaration: enabledRaw,
|
|
process,
|
|
grant_uri_permissions: grant,
|
|
grant_uri_permissions_declaration: grantRaw,
|
|
intent_filters: filters,
|
|
meta_data: metadata,
|
|
} satisfies Pick<
|
|
AndroidManifestComponent,
|
|
| "declaration_index"
|
|
| "location"
|
|
| "declaration_coverage"
|
|
| "enabled"
|
|
| "enabled_declaration"
|
|
| "process"
|
|
| "grant_uri_permissions"
|
|
| "grant_uri_permissions_declaration"
|
|
| "intent_filters"
|
|
| "meta_data"
|
|
>;
|
|
return {
|
|
facts,
|
|
actions,
|
|
categories,
|
|
filterCount,
|
|
actionFilter,
|
|
legacyTruncated,
|
|
partial,
|
|
detailLimited,
|
|
processLimited,
|
|
unsupported,
|
|
nonliteralEnabled: enabledDeclaration !== null && enabled === null,
|
|
nonliteralGrant: grantDeclaration !== null && grant === null,
|
|
};
|
|
};
|
|
};
|