blob: c57d8adf3dbb75428b32b795dd9244a9a1a5b791 [file]
// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.repackaging;
import static com.android.tools.r8.dex.DataResourceWriter.shouldDropDataDirectoryResource;
import static com.android.tools.r8.dex.DataResourceWriter.shouldDropDataEntryResource;
import static com.android.tools.r8.utils.internal.MapUtils.ignoreKey;
import com.android.tools.r8.DataDirectoryResource;
import com.android.tools.r8.DataEntryResource;
import com.android.tools.r8.DataResourceProvider;
import com.android.tools.r8.DataResourceProvider.Visitor;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.dex.ResourceAdapter;
import com.android.tools.r8.features.FeatureSplitConfiguration;
import com.android.tools.r8.features.FeatureSplitConfiguration.DataResourceProvidersAndConsumer;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.ProgramPackage;
import com.android.tools.r8.graph.SortedProgramPackageCollection;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
import com.android.tools.r8.utils.internal.MapUtils;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
public class RepackagingResourceCollisionResolver {
// Map from each package to the set of packages it collides with.
private final Map<ProgramPackage, Set<ProgramPackage>> collisions = new HashMap<>();
private final Set<ProgramPackage> blocked = Sets.newIdentityHashSet();
private RepackagingResourceCollisionResolver() {}
private RepackagingResourceCollisionResolver(Collection<RepackagingCollision> collisions) {
for (RepackagingCollision collision : collisions) {
if (collision.size() > 1) {
for (ProgramPackage pkg : collision.packages) {
this.collisions
.computeIfAbsent(pkg, ignoreKey(Sets::newIdentityHashSet))
.addAll(collision.packages);
}
}
}
}
public static RepackagingResourceCollisionResolver create(
AppView<AppInfoWithLiveness> appView,
SortedProgramPackageCollection packages,
PackageObfuscationMode packageObfuscationMode) {
// If there is no resource consumer then collisions don't matter.
if (appView.options().dataResourceConsumer == null) {
return empty();
}
// If we are not repackaging then there are no collisions.
if (!packageObfuscationMode.isRepackageClasses()) {
return empty();
}
// Visit all data resources meanwhile repackaging all packages to the same target package and
// collecting the collisions.
InterceptingResourceAdapter adapter = new InterceptingResourceAdapter(appView, packages);
Map<String, RepackagingCollision> collisions = new HashMap<>();
InternalOptions options = appView.options();
forEachDataResourceProvider(
appView,
provider -> {
try {
provider.accept(
new Visitor() {
@Override
public void visit(DataDirectoryResource directory) {
if (shouldDropDataDirectoryResource(directory, options)) {
return;
}
collisions
.computeIfAbsent(
adapter.adaptDirectoryName(directory),
ignoreKey(RepackagingCollision::new))
.addInterceptedPackagesFrom(adapter);
}
@Override
public void visit(DataEntryResource file) {
if (shouldDropDataEntryResource(file, options)) {
return;
}
collisions
.computeIfAbsent(
adapter.adaptFileNameIfNeeded(file),
ignoreKey(RepackagingCollision::new))
.addInterceptedPackagesFrom(adapter);
}
});
} catch (ResourceException e) {
appView.reporter().error(new ExceptionDiagnostic(e));
}
});
return new RepackagingResourceCollisionResolver(collisions.values());
}
private static void forEachDataResourceProvider(
AppView<AppInfoWithLiveness> appView, Consumer<DataResourceProvider> consumer) {
appView.app().dataResourceProviders.forEach(consumer);
if (appView.options().hasFeatureSplitConfiguration()) {
FeatureSplitConfiguration featureSplitConfiguration =
appView.options().getFeatureSplitConfiguration();
for (DataResourceProvidersAndConsumer entry :
featureSplitConfiguration.getDataResourceProvidersAndConsumers()) {
entry.providers.forEach(consumer);
}
}
}
private static RepackagingResourceCollisionResolver empty() {
return new RepackagingResourceCollisionResolver();
}
// Called when a package is repackaged to the target package (e.g., the default package "").
// When this happens we prohibit repackaging of all packages that collide with the current one,
// by adding them to the `blocked` set. These packages will be subject to -flattenpackagehierarchy
// instead of -repackageclasses.
void acceptRepackagedPackage(ProgramPackage pkg) {
Set<ProgramPackage> pkgCollisions =
MapUtils.removeOrDefault(collisions, pkg, Collections.emptySet());
for (ProgramPackage pkgBlocked : pkgCollisions) {
if (pkgBlocked != pkg) {
blocked.add(pkgBlocked);
collisions.remove(pkgBlocked);
}
}
}
boolean isBlocked(ProgramPackage pkg) {
return blocked.contains(pkg);
}
// A resource adapter implementation that stores the set of packages that are being queried during
// calls to adaptDirectoryName() or adaptFileNameIfNeeded().
//
// If two resources are mapped to the same file name, then we conservatively treat the packages
// that were queried during the renaming as colliding.
//
// This only intercepts calls to adaptPackage() (and not also adaptType()), since repackaging does
// not cause any collisions among types.
private static class InterceptingResourceAdapter extends ResourceAdapter {
private final SortedProgramPackageCollection packages;
private final Set<ProgramPackage> interceptedPackages = Sets.newIdentityHashSet();
InterceptingResourceAdapter(
AppView<AppInfoWithLiveness> appView, SortedProgramPackageCollection packages) {
super(appView);
this.packages = packages;
}
@Override
public String adaptDirectoryName(DataDirectoryResource directory) {
assert interceptedPackages.isEmpty();
return super.adaptDirectoryName(directory);
}
@Override
public String adaptFileNameIfNeeded(DataEntryResource file) {
assert interceptedPackages.isEmpty();
return super.adaptFileNameIfNeeded(file);
}
@Override
protected String adaptPackage(String packageDescriptor) {
ProgramPackage pkg = packages.getPackage(packageDescriptor);
if (pkg != null) {
interceptedPackages.add(pkg);
return "";
}
return packageDescriptor;
}
}
private static class RepackagingCollision {
private final Set<ProgramPackage> packages = Sets.newIdentityHashSet();
void addInterceptedPackagesFrom(InterceptingResourceAdapter adapter) {
packages.addAll(adapter.interceptedPackages);
adapter.interceptedPackages.clear();
}
int size() {
return packages.size();
}
}
}