Files
reverse-engineering-anything/src/android/AndroidManifestComponents.ts
T
f2fa7f648d feat(android): project a structured manifest component inventory (#5025)
* feat(android): project a structured manifest component inventory

* fix(android): derive provider export defaults from the target SDK and match namespaced attributes

* fix(android): bound resolved names and authorities in the component inventory

* fix(android): separate nonliteral exported values from omission

* fix(android): bound manifest component projection

* fix(android): preserve permission override semantics

* fix(android): retain unresolved package identities

* test(android): cover multibyte manifest name budget

* fix(android): measure inherited permission once

* fix(android): preserve unknown symbolic permissions

* fix(android): preserve manifest component uncertainty

* fix(android): report unsupported manifest facets

* fix(android): preserve manifest identities

* fix(android): preserve alias target references

* fix(android): validate alias targets and authorities

* fix(android): bound alias target expansion

* fix(android): index alias target spellings

* fix(android): preserve alias identities and isolate legacy defaults

* fix(android): keep legacy component defaults typed as unavailable

* docs(android): record manifest projection and real JADX costs

* fix(android): preserve SDK defaults and manifest namespaces

* docs(android): verify SDK and namespace ownership corrections

* fix(android): accept DEX class names and hexadecimal SDK levels

* docs(android): record DEX and SDK follow-up verification

* fix(android): validate manifest package identity separately

* docs(android): record final package-identity verification

* fix(android): preserve exact DEX component and alias names

* docs(android): record exact-name integration verification

* fix(android): retain declared foreground service type text

* docs(android): record final declared-value validation

---------

Co-authored-by: BaaRai <baarai@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 17:14:23 +08:00

702 lines
27 KiB
TypeScript

import type { Element, Node } from "@xmldom/xmldom";
import { parseXmlDocument } from "../domain/xmlDocument.js";
/** Component kinds a decoded AndroidManifest can declare under <application>. */
export const ANDROID_COMPONENT_KINDS = [
"activity",
"activity-alias",
"service",
"receiver",
"provider",
] as const;
export type AndroidComponentKind = (typeof ANDROID_COMPONENT_KINDS)[number];
/** Bounds keep a hostile manifest from exhausting the inspection. */
export const ANDROID_MANIFEST_LIMITS = {
components: 512,
intentFilters: 32,
intentValues: 64,
authorities: 64,
/** Total bytes of resolved component names, bounding package amplification. */
resolvedNameBytes: 1024 * 1024,
authorityBytes: 1024 * 1024,
/** Total copied permission-value bytes, including inherited defaults. */
permissionBytes: 1024 * 1024,
} as const;
/** The attribute namespace every component facet uses. */
const ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android";
export interface AndroidManifestComponent {
readonly kind: AndroidComponentKind;
/** Class name as written in the manifest. */
readonly declared_name: string;
/**
* Fully qualified name with the manifest package applied, or the declared
* name unchanged when it cannot be resolved or its syntax is not
* established.
*/
readonly name: string;
/**
* Declared android:exported, or null when the attribute is absent or set
* to a value other than the literals true or false, whose effective state
* the parser cannot resolve.
*/
readonly exported: boolean | null;
/**
* Android's default export state when android:exported is absent: the
* intent-filter rule for activities, services, receivers and aliases, and
* the target-SDK rule for providers. Null when the provider rule cannot be
* evaluated because the target SDK is unknown, or when the attribute is
* present but not a literal boolean, since the default does not govern a
* declared value.
*/
readonly exported_default: boolean | null;
readonly permission: string | null;
/** Source of the effective permission projection; unknown when its copy is capped. */
readonly permission_source: "component" | "application" | "none" | "unknown";
readonly intent_filter_count: number;
readonly actions: readonly string[];
readonly categories: readonly string[];
/** Declared android:foregroundServiceType on services; null otherwise. */
readonly foreground_service_type: string | null;
/** Exact declared provider authority segments, split on ";"; malformed segments remain raw. */
readonly authorities: readonly string[] | null;
/** Exact declared android:targetActivity on activity-alias; malformed references remain raw and make the inventory partial. */
readonly target_activity: string | null;
}
export interface AndroidManifestComponentInventory {
readonly status: "complete" | "partial" | "unavailable";
readonly items: readonly AndroidManifestComponent[];
readonly limitations: readonly string[];
}
const PARTIAL_LIMITATION =
"The decoded manifest text is truncated, so its component inventory is not established; the retained manifest is partial.";
const unavailable = (
limitation: string,
): AndroidManifestComponentInventory => ({
status: "unavailable",
items: [],
limitations: [limitation],
});
const childNodes = (element: Element): readonly Node[] =>
Array.from(element.childNodes);
/** Manifest tags are unnamespaced; Android's namespace qualifies attributes. */
const isManifestElement = (node: Node | null): node is Element =>
node !== null &&
node.nodeType === 1 &&
(node as Element).namespaceURI === null;
/** Read an unnamespaced attribute, the shape manifest `package` uses. */
const plainAttribute = (element: Element, name: string): string | null => {
const value = element.getAttribute(name);
return value === "" ? null : value;
};
/**
* Read an android-namespaced attribute. XML prefixes are aliases, so a
* same-local-name attribute from another namespace is never a match.
*/
const androidAttribute = (
element: Element,
localName: string,
): string | null => {
const attributes = element.attributes;
for (let index = 0; index < attributes.length; index += 1) {
const node = attributes.item(index);
if (node === null || node.localName !== localName) continue;
if (node.namespaceURI === ANDROID_NAMESPACE) return node.nodeValue;
// Without namespace information (an undeclared prefix), the conventional
// android: prefix is the only remaining signal.
if (node.namespaceURI === null && node.prefix === "android")
return node.nodeValue;
}
return null;
};
/** Android application-ID grammar, with the platform package exception. */
const hasSupportedAndroidPackageNameSyntax = (name: string): boolean =>
name === "android" ||
(name.length <= 223 &&
/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u.test(name));
// DEX SimpleName permits digits at the start, '-', supplemental code points,
// and (since format 040) spaces. A decoded manifest does not qualify DEX
// version or class existence; accept the documented character union here.
const dexSimpleNameCharacter =
/^[A-Za-z0-9$_\- \u00a0-\u1fff\u2000-\u200a\u2010-\u2027\u202f-\ud7ff\ue000-\uffef\u{10000}-\u{10ffff}]$/u;
const hasSupportedDexBinaryClassNameSyntax = (name: string): boolean => {
let segmentStart = true;
let first = true;
for (const character of name) {
if (first && character === ".") {
first = false;
continue;
}
first = false;
if (character === ".") {
if (segmentStart) return false;
segmentStart = true;
} else {
if (!dexSimpleNameCharacter.test(character)) return false;
segmentStart = false;
}
}
return !segmentStart;
};
const containsUnresolvedToken = (value: string): boolean =>
value.includes("@") || value.includes("?") || value.includes("${");
/**
* Android resolves a component name against the manifest package: a leading
* dot or a name without any dot is relative to the package, while a dotted
* name is already fully qualified.
*/
export const resolveAndroidComponentName = (
packageName: string | null,
declared: string,
): string => {
if (declared.startsWith(".")) return `${packageName ?? ""}${declared}`;
if (!declared.includes(".") && packageName !== null)
return `${packageName}.${declared}`;
return declared;
};
const isComponentKind = (tag: string): tag is AndroidComponentKind =>
(ANDROID_COMPONENT_KINDS as readonly string[]).includes(tag);
const androidTargetSdkLevel = (targetSdk: string | null): number | null => {
// JADX prints TYPE_INT_HEX as 0x..., while TYPE_INT_DEC stays decimal.
// Both originate in an Android signed 32-bit resource integer.
if (targetSdk === null || !/^(?:[0-9]+|0x[0-9a-f]+)$/iu.test(targetSdk))
return null;
const level = Number(targetSdk);
return Number.isInteger(level) && level >= 1 && level <= 0x7fffffff
? level
: null;
};
/**
* Collect unique intent-filter actions and categories. The value budget is
* shared across both arrays, matching the stated per-component bound.
*/
const intentValues = (
filters: readonly Element[],
): { actions: string[]; categories: string[]; truncated: boolean } => {
const actions: string[] = [];
const categories: string[] = [];
let truncated = filters.length > ANDROID_MANIFEST_LIMITS.intentFilters;
let budget = ANDROID_MANIFEST_LIMITS.intentValues;
for (const filter of filters.slice(0, ANDROID_MANIFEST_LIMITS.intentFilters))
for (const member of childNodes(filter)) {
if (!isManifestElement(member)) continue;
const element = member;
if (element.nodeName !== "action" && element.nodeName !== "category")
continue;
const value = androidAttribute(element, "name");
if (value === null) continue;
const target = element.nodeName === "action" ? actions : categories;
if (target.includes(value)) continue;
if (budget <= 0) {
truncated = true;
continue;
}
budget -= 1;
target.push(value);
}
return { actions, categories, truncated };
};
/** Split a provider authorities declaration within its own bound. */
const providerAuthorities = (
declared: string,
byteLimit: number,
): {
authorities: string[];
truncated: boolean;
bytes: number;
malformed: number;
} => {
const authorities: string[] = [];
let truncated = false;
let bytes = 0;
let malformed = 0;
let start = 0;
while (start <= declared.length) {
const separator = declared.indexOf(";", start);
const end = separator < 0 ? declared.length : separator;
const value = declared.slice(start, end);
const valueBytes = Buffer.byteLength(value);
if (
authorities.length >= ANDROID_MANIFEST_LIMITS.authorities ||
valueBytes > byteLimit - bytes
) {
truncated = true;
break;
}
if (value.length === 0 || value !== value.trim()) malformed += 1;
authorities.push(value);
bytes += valueBytes;
if (separator < 0) break;
start = separator + 1;
}
return { authorities, truncated, bytes, malformed };
};
/**
* Parse the decoded AndroidManifest XML text into a bounded, deterministic
* component inventory. The projection is static: it reports declarations and
* Android's documented export defaults, never merged-manifest or runtime
* state.
*/
export const parseAndroidManifestComponents = (
manifestText: string,
coverage: "complete" | "partial",
declaredTargetSdk: string | null = null,
minimumSdk: string | null = null,
): AndroidManifestComponentInventory => {
if (coverage === "partial")
return {
status: "partial",
items: [],
limitations: [PARTIAL_LIMITATION],
};
const parsed = parseXmlDocument(manifestText, {
doctype: "reject",
lineEndings: "preserve",
});
if (!parsed.ok)
return unavailable(
`The decoded manifest is not well-formed XML (${parsed.error.reason}), so its component inventory is unavailable.`,
);
const root = parsed.value.documentElement;
if (!isManifestElement(root) || root.nodeName !== "manifest")
return unavailable(
"The decoded manifest root is not <manifest>, so its component inventory is unavailable.",
);
const rawPackageName = plainAttribute(root, "package");
const packageName = rawPackageName?.trim() || null;
const packageHasInvalidWhitespace =
rawPackageName !== null && rawPackageName !== rawPackageName.trim();
const packageHasUnresolvedToken =
packageName !== null && containsUnresolvedToken(packageName);
const packageHasUnsupportedPackageSyntax =
packageName !== null &&
!packageHasUnresolvedToken &&
!hasSupportedAndroidPackageNameSyntax(packageName);
const packageNameBytes =
packageName === null ? 0 : Buffer.byteLength(packageName);
const limitations: string[] = [];
const items: AndroidManifestComponent[] = [];
let truncated = false;
let applicationSeen = false;
let unnamed = 0;
let providerDefaultsUnknown = 0;
let nonliteralExported = 0;
let unresolvedNames = 0;
let unresolvedPackages = 0;
let unresolvedTargetActivities = 0;
let unresolvedTokens = 0;
let invalidNames = 0;
let malformedAuthoritySegments = 0;
let providerPermissionOverridesUnknown = 0;
let invalidExported = 0;
let unknownFilteredExportDefaults = 0;
let capacityTruncated = false;
let permissionBudgetLimited = false;
let unresolvedPermissionTokens = 0;
let nameBudget = ANDROID_MANIFEST_LIMITS.resolvedNameBytes;
let authorityBudget = ANDROID_MANIFEST_LIMITS.authorityBytes;
let permissionBudget = ANDROID_MANIFEST_LIMITS.permissionBytes;
const children = childNodes(root);
let targetSdkDeclared = false;
let minimumSdkDeclared = false;
for (const child of children) {
if (!isManifestElement(child) || child.nodeName !== "uses-sdk") continue;
// Match the bridge's declaration owner: the last uses-sdk supplies values.
targetSdkDeclared = androidAttribute(child, "targetSdkVersion") !== null;
minimumSdkDeclared = androidAttribute(child, "minSdkVersion") !== null;
}
// Empty protocol values can mean omission or an empty declared attribute.
// XML presence disambiguates them; an explicit unknown never becomes API 1.
let effectiveTargetSdk = declaredTargetSdk;
if (effectiveTargetSdk === null && !targetSdkDeclared) {
effectiveTargetSdk = minimumSdk;
if (effectiveTargetSdk === null && !minimumSdkDeclared)
effectiveTargetSdk = "1";
}
const targetSdkLevel = androidTargetSdkLevel(effectiveTargetSdk);
// Parse the effective SDK once even when hundreds of providers share it.
const providerDefault = targetSdkLevel === null ? null : targetSdkLevel <= 16;
for (const node of children) {
if (!isManifestElement(node)) continue;
const element = node;
if (element.nodeName !== "application") continue;
applicationSeen = true;
const precedingActivityTargetNames = new Set<string>();
const declaredApplicationPermission = androidAttribute(
element,
"permission",
);
const applicationPermission =
declaredApplicationPermission === null ||
declaredApplicationPermission.length === 0
? null
: declaredApplicationPermission;
const applicationPermissionHasUnresolvedToken =
applicationPermission !== null &&
containsUnresolvedToken(applicationPermission);
const applicationPermissionBytes =
applicationPermission === null
? null
: Buffer.byteLength(applicationPermission);
for (const child of childNodes(element)) {
if (!isManifestElement(child)) continue;
const component = child;
if (!isComponentKind(component.nodeName)) continue;
// Every declaration counts against the bound, named or not, so an
// unnamed flood cannot exceed it while still reporting complete.
if (items.length + unnamed >= ANDROID_MANIFEST_LIMITS.components) {
truncated = true;
capacityTruncated = true;
break;
}
const declared = androidAttribute(component, "name");
if (declared === null) {
unnamed += 1;
continue;
}
const unresolvedName = containsUnresolvedToken(declared);
const filters = childNodes(component).filter(
(entry): entry is Element =>
isManifestElement(entry) && entry.nodeName === "intent-filter",
);
const collected = intentValues(filters);
if (collected.truncated) {
truncated = true;
capacityTruncated = true;
}
// A huge manifest package would be copied into every resolved name;
// once the byte budget is spent, later names keep their declared form.
const invalidDeclaredName =
declared.length === 0 ||
(!unresolvedName && !hasSupportedDexBinaryClassNameSyntax(declared));
const needsPackage = declared.startsWith(".") || !declared.includes(".");
const packageKnown =
!invalidDeclaredName &&
!unresolvedName &&
(!needsPackage ||
(packageName !== null &&
!packageHasInvalidWhitespace &&
!packageHasUnresolvedToken &&
!packageHasUnsupportedPackageSyntax));
const declaredBytes = Buffer.byteLength(declared);
const resolvedBytes =
needsPackage && packageKnown
? packageNameBytes +
(declared.startsWith(".") ? 0 : 1) +
declaredBytes
: declaredBytes;
const nameFits = packageKnown && resolvedBytes <= nameBudget;
const name = nameFits
? resolveAndroidComponentName(packageName, declared)
: declared;
if (invalidDeclaredName) invalidNames += 1;
if (!invalidDeclaredName) {
if (unresolvedName) unresolvedTokens += 1;
else if (!packageKnown) unresolvedPackages += 1;
else if (!nameFits) unresolvedNames += 1;
else nameBudget -= resolvedBytes;
}
if (component.nodeName === "activity" && nameFits) {
precedingActivityTargetNames.add(name);
precedingActivityTargetNames.add(declared);
if (
packageName !== null &&
!packageHasInvalidWhitespace &&
!packageHasUnresolvedToken &&
!packageHasUnsupportedPackageSyntax &&
name.length > packageName.length &&
name.startsWith(packageName) &&
name[packageName.length] === "."
) {
const relativeName = name.slice(packageName.length);
precedingActivityTargetNames.add(relativeName);
const bareName = relativeName.slice(1);
if (!bareName.includes("."))
precedingActivityTargetNames.add(bareName);
}
}
let authorities: readonly string[] | null = null;
if (component.nodeName === "provider") {
if (
androidAttribute(component, "readPermission") !== null ||
androidAttribute(component, "writePermission") !== null
)
providerPermissionOverridesUnknown += 1;
const declaredAuthorities = androidAttribute(component, "authorities");
if (declaredAuthorities !== null) {
const split = providerAuthorities(
declaredAuthorities,
authorityBudget,
);
malformedAuthoritySegments += split.malformed;
if (split.truncated) {
truncated = true;
capacityTruncated = true;
}
authorityBudget -= split.bytes;
if (authorityBudget < 0) authorityBudget = 0;
authorities = split.authorities;
}
}
const declaredPermission = androidAttribute(component, "permission");
const permissionValue =
declaredPermission === null
? component.nodeName === "activity-alias"
? null
: applicationPermission
: declaredPermission.length === 0
? null
: declaredPermission;
const permissionSource =
permissionValue === null
? "none"
: declaredPermission === null
? "application"
: "component";
let permission: string | null = permissionValue;
let effectivePermissionSource:
| "component"
| "application"
| "none"
| "unknown" = permissionSource;
if (permissionValue !== null) {
const permissionHasUnresolvedToken =
declaredPermission === null
? applicationPermissionHasUnresolvedToken
: containsUnresolvedToken(permissionValue);
if (permissionHasUnresolvedToken) {
permission = null;
effectivePermissionSource = "unknown";
unresolvedPermissionTokens += 1;
} else {
// Inherited application permission is the same string for every
// component. Measure it once at its owning boundary rather than
// rescanning a potentially large decoded value per inherited copy.
const permissionBytes =
declaredPermission === null
? (applicationPermissionBytes ?? 0)
: Buffer.byteLength(permissionValue);
if (permissionBytes > permissionBudget) {
permission = null;
effectivePermissionSource = "unknown";
truncated = true;
capacityTruncated = true;
permissionBudgetLimited = true;
} else permissionBudget -= permissionBytes;
}
}
const declaredExported = androidAttribute(component, "exported");
const targetActivity =
component.nodeName === "activity-alias"
? androidAttribute(component, "targetActivity")
: null;
if (component.nodeName === "activity-alias") {
if (targetActivity === null) {
unresolvedTargetActivities += 1;
} else {
const targetNeedsPackage =
targetActivity.startsWith(".") || !targetActivity.includes(".");
const targetUnresolved = containsUnresolvedToken(targetActivity);
const targetInvalid =
targetActivity.length === 0 ||
(!targetUnresolved &&
!hasSupportedDexBinaryClassNameSyntax(targetActivity));
const targetPackageKnown =
packageName !== null &&
!packageHasInvalidWhitespace &&
!packageHasUnresolvedToken &&
!packageHasUnsupportedPackageSyntax;
const targetResolved =
!targetInvalid &&
!targetUnresolved &&
(!targetNeedsPackage || targetPackageKnown);
if (
!targetResolved ||
!precedingActivityTargetNames.has(targetActivity)
)
unresolvedTargetActivities += 1;
}
}
const exportedLiteral =
declaredExported === "true"
? true
: declaredExported === "false"
? false
: null;
const exportedNonliteral =
declaredExported !== null && exportedLiteral === null;
const explicitExportRequired =
declaredExported === null &&
targetSdkLevel !== null &&
targetSdkLevel >= 31 &&
filters.length > 0 &&
(component.nodeName === "activity" ||
component.nodeName === "activity-alias" ||
component.nodeName === "service" ||
component.nodeName === "receiver");
const filteredExportUnknown =
declaredExported === null &&
targetSdkLevel === null &&
filters.length > 0 &&
(component.nodeName === "activity" ||
component.nodeName === "activity-alias" ||
component.nodeName === "service" ||
component.nodeName === "receiver");
// The default only matters when android:exported is absent.
if (
component.nodeName === "provider" &&
providerDefault === null &&
declaredExported === null
)
providerDefaultsUnknown += 1;
if (exportedNonliteral) nonliteralExported += 1;
if (explicitExportRequired) invalidExported += 1;
if (filteredExportUnknown) unknownFilteredExportDefaults += 1;
items.push({
kind: component.nodeName,
declared_name: declared,
name,
exported: exportedLiteral,
exported_default:
exportedNonliteral || explicitExportRequired || filteredExportUnknown
? null
: component.nodeName === "provider"
? providerDefault
: filters.length > 0,
permission,
permission_source: effectivePermissionSource,
intent_filter_count: filters.length,
actions: collected.actions,
categories: collected.categories,
foreground_service_type:
component.nodeName === "service"
? androidAttribute(component, "foregroundServiceType")
: null,
authorities,
target_activity: targetActivity,
});
}
}
if (!applicationSeen)
limitations.push(
"The manifest declares no <application> element, so no components were projected.",
);
// Dropped declarations mean the inventory is not the whole manifest.
if (unnamed > 0) {
limitations.push(
`${String(unnamed)} component declaration(s) without android:name are omitted from the inventory.`,
);
truncated = true;
}
if (invalidNames > 0) {
limitations.push(
`${String(invalidNames)} component name value(s) are empty or do not fit DEX binary class-name syntax; the raw declaration is retained without resolving it as an identity.`,
);
truncated = true;
}
if (packageHasInvalidWhitespace) {
limitations.push(
"The manifest package has surrounding whitespace; relative component names were not resolved against it.",
);
truncated = true;
}
if (packageHasUnsupportedPackageSyntax) {
limitations.push(
"The manifest package does not fit the supported Java package-name syntax; package-relative component names were not resolved against it.",
);
truncated = true;
}
if (providerPermissionOverridesUnknown > 0) {
limitations.push(
`${String(providerPermissionOverridesUnknown)} provider(s) declare android:readPermission or android:writePermission; operation-specific permission overrides are retained in the manifest text but are not projected.`,
);
truncated = true;
}
if (malformedAuthoritySegments > 0) {
limitations.push(
`${String(malformedAuthoritySegments)} provider authority segment(s) are empty or have surrounding whitespace; exact segments are retained and their registration is not established.`,
);
truncated = true;
}
if (providerDefaultsUnknown > 0)
limitations.push(
`${String(providerDefaultsUnknown)} provider(s) omit android:exported and the target SDK is unknown, so Android's export default for them is not established.`,
);
if (nonliteralExported > 0)
limitations.push(
`${String(nonliteralExported)} component declaration(s) set android:exported to a value other than the literals true or false (for example a resource reference), so their effective export state is unknown and the Android default does not apply.`,
);
if (unresolvedPackages > 0) {
limitations.push(
`${String(unresolvedPackages)} package-relative component name(s) remain unresolved because the manifest package is absent or contains a resource/placeholder token, or its syntax is unsupported; the declared name is retained.`,
);
truncated = true;
}
if (unresolvedTargetActivities > 0) {
limitations.push(
`${String(unresolvedTargetActivities)} activity-alias targetActivity value(s) are empty, malformed, unresolved against the manifest package, or do not match a preceding activity declaration; each exact declared value is retained.`,
);
truncated = true;
}
if (unresolvedTokens > 0) {
limitations.push(
`${String(unresolvedTokens)} component name(s) contain an unresolved resource or manifest placeholder token; the declared name is retained without claiming a resolved identity.`,
);
truncated = true;
}
if (permissionBudgetLimited)
limitations.push(
`Projected component permission values are capped at ${String(ANDROID_MANIFEST_LIMITS.permissionBytes)} total bytes; omitted copies have permission_source "unknown".`,
);
if (unresolvedPermissionTokens > 0) {
limitations.push(
`${String(unresolvedPermissionTokens)} permission value(s) contain unresolved resource or manifest-placeholder tokens; their effective values are unknown.`,
);
truncated = true;
}
if (invalidExported > 0) {
limitations.push(
`${String(invalidExported)} component(s) with intent filters target API 31 or later and omit android:exported; Android requires an explicit value, so the inventory does not infer a default.`,
);
}
if (unknownFilteredExportDefaults > 0)
limitations.push(
`${String(unknownFilteredExportDefaults)} filtered component(s) omit android:exported and the target SDK is unknown, so the applicable export rule is not established.`,
);
if (unresolvedNames > 0) {
limitations.push(
`${String(unresolvedNames)} component name(s) keep their declared form because the resolved-name byte budget (${String(ANDROID_MANIFEST_LIMITS.resolvedNameBytes)}) was exhausted.`,
);
truncated = true;
}
if (capacityTruncated)
limitations.push(
`The component inventory reached a capacity limit: ${String(ANDROID_MANIFEST_LIMITS.components)} declarations, ${String(ANDROID_MANIFEST_LIMITS.intentFilters)} filters or ${String(ANDROID_MANIFEST_LIMITS.intentValues)} values per component, ${String(ANDROID_MANIFEST_LIMITS.authorities)} authorities per provider, ${String(ANDROID_MANIFEST_LIMITS.authorityBytes)} total authority bytes, or ${String(ANDROID_MANIFEST_LIMITS.permissionBytes)} total permission bytes.`,
);
return {
status: truncated ? "partial" : "complete",
items,
limitations,
};
};