blob: 7dbaa02afe1767c94a4a1615b159e04a7655496c [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.android;
import static com.google.common.base.Predicates.not;
import static com.google.common.base.Verify.verify;
import static java.util.stream.Collectors.toList;
import android.aapt.pb.internal.ResourcesInternal.CompiledFile;
import com.android.aapt.ConfigurationOuterClass.Configuration;
import com.android.aapt.ConfigurationOuterClass.Configuration.KeysHidden;
import com.android.aapt.ConfigurationOuterClass.Configuration.NavHidden;
import com.android.aapt.ConfigurationOuterClass.Configuration.Orientation;
import com.android.aapt.ConfigurationOuterClass.Configuration.ScreenLayoutLong;
import com.android.aapt.ConfigurationOuterClass.Configuration.ScreenLayoutSize;
import com.android.aapt.ConfigurationOuterClass.Configuration.Touchscreen;
import com.android.aapt.ConfigurationOuterClass.Configuration.UiModeNight;
import com.android.aapt.ConfigurationOuterClass.Configuration.UiModeType;
import com.android.aapt.Resources;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Package;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.Value;
import com.android.aapt.Resources.XmlNode;
import com.android.ide.common.resources.configuration.CountryCodeQualifier;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.KeyboardStateQualifier;
import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.ide.common.resources.configuration.NavigationMethodQualifier;
import com.android.ide.common.resources.configuration.NavigationStateQualifier;
import com.android.ide.common.resources.configuration.NetworkCodeQualifier;
import com.android.ide.common.resources.configuration.NightModeQualifier;
import com.android.ide.common.resources.configuration.ResourceQualifier;
import com.android.ide.common.resources.configuration.ScreenDimensionQualifier;
import com.android.ide.common.resources.configuration.ScreenHeightQualifier;
import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
import com.android.ide.common.resources.configuration.ScreenRatioQualifier;
import com.android.ide.common.resources.configuration.ScreenRoundQualifier;
import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
import com.android.ide.common.resources.configuration.ScreenWidthQualifier;
import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier;
import com.android.ide.common.resources.configuration.TextInputMethodQualifier;
import com.android.ide.common.resources.configuration.TouchScreenQualifier;
import com.android.ide.common.resources.configuration.UiModeQualifier;
import com.android.ide.common.resources.configuration.VersionQualifier;
import com.android.resources.Density;
import com.android.resources.Keyboard;
import com.android.resources.KeyboardState;
import com.android.resources.LayoutDirection;
import com.android.resources.Navigation;
import com.android.resources.NavigationState;
import com.android.resources.NightMode;
import com.android.resources.ResourceType;
import com.android.resources.ScreenOrientation;
import com.android.resources.ScreenRatio;
import com.android.resources.ScreenRound;
import com.android.resources.ScreenSize;
import com.android.resources.TouchScreen;
import com.android.resources.UiMode;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.LittleEndianDataInputStream;
import com.google.devtools.build.android.aapt2.CompiledResources;
import com.google.devtools.build.android.aapt2.ResourceCompiler;
import com.google.devtools.build.android.proto.SerializeFormat;
import com.google.devtools.build.android.proto.SerializeFormat.Header;
import com.google.devtools.build.android.resources.Visibility;
import com.google.devtools.build.android.xml.ResourcesAttribute.AttributeType;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
/** Deserializes {@link DataKey}, {@link DataValue} entries from compiled resource files. */
public class AndroidCompiledDataDeserializer implements AndroidDataDeserializer {
private static final Logger logger =
Logger.getLogger(AndroidCompiledDataDeserializer.class.getName());
// Magic numbers from
// https://android.googlesource.com/platform/frameworks/base.git/+/refs/tags/platform-tools-29.0.2/tools/aapt2/formats.md
//
// TODO(b/143116130): aapt2 should just use protobuf directly instead of this custom
// layer on top.
private static final int AAPT_CONTAINER_MAGIC = 0x54504141; // "AAPT" as little-endian
private static final int AAPT_CONTAINER_VERSION = 1;
private static final int AAPT_CONTAINER_ENTRY_RES_TABLE = 0;
private static final int AAPT_CONTAINER_ENTRY_RES_FILE = 1;
// aapt2 insists on aligning things to 4-byte boundaries in *.flat files, though there's no actual
// performance or maintainability benefit.
private static final int AAPT_FLAT_FILE_ALIGNMENT = 4;
private static final ImmutableMap<Configuration.LayoutDirection, LayoutDirection>
LAYOUT_DIRECTION_MAP =
ImmutableMap.of(
Configuration.LayoutDirection.LAYOUT_DIRECTION_LTR,
LayoutDirection.LTR,
Configuration.LayoutDirection.LAYOUT_DIRECTION_RTL,
LayoutDirection.RTL);
private static final ImmutableMap<Configuration.ScreenLayoutSize, ScreenSize> LAYOUT_SIZE_MAP =
ImmutableMap.of(
ScreenLayoutSize.SCREEN_LAYOUT_SIZE_SMALL,
ScreenSize.SMALL,
ScreenLayoutSize.SCREEN_LAYOUT_SIZE_NORMAL,
ScreenSize.NORMAL,
ScreenLayoutSize.SCREEN_LAYOUT_SIZE_LARGE,
ScreenSize.LARGE,
ScreenLayoutSize.SCREEN_LAYOUT_SIZE_XLARGE,
ScreenSize.XLARGE);
private static final ImmutableMap<Configuration.ScreenLayoutLong, ScreenRatio> SCREEN_LONG_MAP =
ImmutableMap.of(
ScreenLayoutLong.SCREEN_LAYOUT_LONG_LONG,
ScreenRatio.LONG,
ScreenLayoutLong.SCREEN_LAYOUT_LONG_NOTLONG,
ScreenRatio.NOTLONG);
private static final ImmutableMap<Configuration.ScreenRound, ScreenRound> SCREEN_ROUND_MAP =
ImmutableMap.of(
Configuration.ScreenRound.SCREEN_ROUND_ROUND, ScreenRound.ROUND,
Configuration.ScreenRound.SCREEN_ROUND_NOTROUND, ScreenRound.NOTROUND);
private static final ImmutableMap<Configuration.Orientation, ScreenOrientation>
SCREEN_ORIENTATION_MAP =
ImmutableMap.of(
Orientation.ORIENTATION_LAND, ScreenOrientation.LANDSCAPE,
Orientation.ORIENTATION_PORT, ScreenOrientation.PORTRAIT,
Orientation.ORIENTATION_SQUARE, ScreenOrientation.SQUARE);
private static final ImmutableMap<UiModeType, UiMode> SCREEN_UI_MODE =
ImmutableMap.<UiModeType, UiMode>builder()
.put(UiModeType.UI_MODE_TYPE_APPLIANCE, UiMode.APPLIANCE)
.put(UiModeType.UI_MODE_TYPE_CAR, UiMode.CAR)
.put(UiModeType.UI_MODE_TYPE_DESK, UiMode.DESK)
.put(UiModeType.UI_MODE_TYPE_NORMAL, UiMode.NORMAL)
.put(UiModeType.UI_MODE_TYPE_TELEVISION, UiMode.TELEVISION)
.put(UiModeType.UI_MODE_TYPE_VRHEADSET, UiMode.NORMAL)
.put(UiModeType.UI_MODE_TYPE_WATCH, UiMode.WATCH)
.build();
private static final ImmutableMap<Configuration.UiModeNight, NightMode> NIGHT_MODE_MAP =
ImmutableMap.of(
UiModeNight.UI_MODE_NIGHT_NIGHT, NightMode.NIGHT,
UiModeNight.UI_MODE_NIGHT_NOTNIGHT, NightMode.NOTNIGHT);
private static final ImmutableMap<Configuration.KeysHidden, KeyboardState> KEYBOARD_STATE_MAP =
ImmutableMap.of(
KeysHidden.KEYS_HIDDEN_KEYSEXPOSED,
KeyboardState.EXPOSED,
KeysHidden.KEYS_HIDDEN_KEYSSOFT,
KeyboardState.SOFT,
KeysHidden.KEYS_HIDDEN_KEYSHIDDEN,
KeyboardState.HIDDEN);
private static final ImmutableMap<Configuration.Touchscreen, TouchScreen> TOUCH_TYPE_MAP =
ImmutableMap.of(
Touchscreen.TOUCHSCREEN_FINGER,
TouchScreen.FINGER,
Touchscreen.TOUCHSCREEN_NOTOUCH,
TouchScreen.NOTOUCH,
Touchscreen.TOUCHSCREEN_STYLUS,
TouchScreen.STYLUS);
private static final ImmutableMap<Configuration.Keyboard, Keyboard> KEYBOARD_MAP =
ImmutableMap.of(
Configuration.Keyboard.KEYBOARD_NOKEYS,
Keyboard.NOKEY,
Configuration.Keyboard.KEYBOARD_QWERTY,
Keyboard.QWERTY,
Configuration.Keyboard.KEYBOARD_TWELVEKEY,
Keyboard.TWELVEKEY);
private static final ImmutableMap<Configuration.NavHidden, NavigationState> NAV_STATE_MAP =
ImmutableMap.of(
NavHidden.NAV_HIDDEN_NAVHIDDEN,
NavigationState.HIDDEN,
NavHidden.NAV_HIDDEN_NAVEXPOSED,
NavigationState.EXPOSED);
private static final ImmutableMap<Configuration.Navigation, Navigation> NAVIGATION_MAP =
ImmutableMap.of(
Configuration.Navigation.NAVIGATION_DPAD,
Navigation.DPAD,
Configuration.Navigation.NAVIGATION_NONAV,
Navigation.NONAV,
Configuration.Navigation.NAVIGATION_TRACKBALL,
Navigation.TRACKBALL,
Configuration.Navigation.NAVIGATION_WHEEL,
Navigation.WHEEL);
private static final ImmutableMap<Integer, Density> DENSITY_MAP =
ImmutableMap.<Integer, Density>builder()
.put(0xfffe, Density.ANYDPI)
.put(0xffff, Density.NODPI)
.put(120, Density.LOW)
.put(160, Density.MEDIUM)
.put(213, Density.TV)
.put(240, Density.HIGH)
.put(320, Density.XHIGH)
.put(480, Density.XXHIGH)
.put(640, Density.XXXHIGH)
.build();
private final boolean includeFileContentsForValidation;
public static AndroidCompiledDataDeserializer create(boolean includeFileContentsForValidation) {
return new AndroidCompiledDataDeserializer(includeFileContentsForValidation);
}
private AndroidCompiledDataDeserializer(boolean includeFileContentsForValidation) {
this.includeFileContentsForValidation = includeFileContentsForValidation;
}
private static void consumeResourceTable(
DependencyInfo dependencyInfo,
KeyValueConsumers consumers,
ResourceTable resourceTable,
VisibilityRegistry registry)
throws UnsupportedEncodingException, InvalidProtocolBufferException {
List<String> sourcePool =
decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray());
ReferenceResolver resolver = ReferenceResolver.asRoot();
for (Package resourceTablePackage : resourceTable.getPackageList()) {
ReferenceResolver packageResolver =
resolver.resolveFor(resourceTablePackage.getPackageName());
String packageName = resourceTablePackage.getPackageName();
for (Resources.Type resourceFormatType : resourceTablePackage.getTypeList()) {
ResourceType resourceType = ResourceType.getEnum(resourceFormatType.getName());
for (Resources.Entry resource : resourceFormatType.getEntryList()) {
if (!"android".equals(packageName)) {
// This means this resource is not in the android sdk, add it to the set.
for (ConfigValue configValue : resource.getConfigValueList()) {
FullyQualifiedName fqn =
createAndRecordFqn(
packageResolver,
packageName,
resourceType,
resource,
convertToQualifiers(configValue.getConfig()));
Visibility visibility =
registry.getVisibility(
ResourceName.create(packageName, resourceType, resource.getName()));
int sourceIndex = configValue.getValue().getSource().getPathIdx();
String source = sourcePool.get(sourceIndex);
DataSource dataSource = DataSource.of(dependencyInfo, Paths.get(source));
Value resourceValue = configValue.getValue();
DataResource dataResource =
resourceValue.getItem().hasFile()
? DataValueFile.of(
visibility, dataSource, /*fingerprint=*/ null, /*rootXmlNode=*/ null)
: DataResourceXml.from(
resourceValue, visibility, dataSource, resourceType, packageResolver);
if (!fqn.isOverwritable()) {
consumers.combiningConsumer.accept(fqn, dataResource);
} else {
consumers.overwritingConsumer.accept(fqn, dataResource);
}
}
} else {
// In the sdk, just add the fqn for styleables
createAndRecordFqn(
packageResolver, packageName, resourceType, resource, ImmutableList.of());
}
}
}
}
}
/** Maintains state for all references in each package of a resource table. */
// TODO(b/112848607): Remove this! This machinery is all really for pretty-printing styleables,
// and only ever used for emitting XML with tools:keep attributes.
// https://github.com/bazelbuild/bazel/blob/2419d4b2780fc68a0e501c1fab558b045eb054d3/src/tools/android/java/com/google/devtools/build/android/aapt2/ResourceLinker.java#L523
@NotThreadSafe
public static class ReferenceResolver {
enum InlineStatus {
INLINEABLE,
INLINED,
}
private final Optional<String> packageName;
private final Map<FullyQualifiedName, InlineStatus> qualifiedReferenceInlineStatus;
private ReferenceResolver(
Optional<String> packageName,
Map<FullyQualifiedName, InlineStatus> qualifiedReferenceInlineStatus) {
this.packageName = packageName;
this.qualifiedReferenceInlineStatus = qualifiedReferenceInlineStatus;
}
static ReferenceResolver asRoot() {
return new ReferenceResolver(Optional.empty(), new LinkedHashMap<>());
}
public ReferenceResolver resolveFor(String packageName) {
return new ReferenceResolver(
Optional.of(packageName).filter(not(String::isEmpty)), qualifiedReferenceInlineStatus);
}
public FullyQualifiedName parse(String reference) {
return FullyQualifiedName.fromReference(reference, packageName);
}
public FullyQualifiedName register(FullyQualifiedName fullyQualifiedName) {
// The default is that the name can be inlined.
qualifiedReferenceInlineStatus.put(fullyQualifiedName, InlineStatus.INLINEABLE);
return fullyQualifiedName;
}
/** Indicates if a reference can be inlined in a styleable. */
public boolean shouldInline(FullyQualifiedName reference) {
// Only inline if it's in the current package.
if (!reference.isInPackage(packageName.orElse(FullyQualifiedName.DEFAULT_PACKAGE))) {
return false;
}
return InlineStatus.INLINEABLE.equals(qualifiedReferenceInlineStatus.get(reference));
}
/** Update the reference's inline state. */
public FullyQualifiedName markInlined(FullyQualifiedName reference) {
qualifiedReferenceInlineStatus.put(reference, InlineStatus.INLINED);
return reference;
}
}
private static FullyQualifiedName createAndRecordFqn(
ReferenceResolver packageResolver,
String packageName,
ResourceType resourceType,
Resources.Entry resource,
List<String> qualifiers) {
final FullyQualifiedName fqn =
FullyQualifiedName.of(
packageName.isEmpty() ? FullyQualifiedName.DEFAULT_PACKAGE : packageName,
qualifiers,
resourceType,
resource.getName());
packageResolver.register(fqn);
return fqn;
}
// TODO(b/146498565): remove this and use 'Configuration' directly, which is typesafe and free.
private static List<String> convertToQualifiers(Configuration protoConfig) {
FolderConfiguration configuration = new FolderConfiguration();
if (protoConfig.getMcc() > 0) {
configuration.setCountryCodeQualifier(new CountryCodeQualifier(protoConfig.getMcc()));
}
// special code for 0, as MNC can be zero
// https://android.googlesource.com/platform/frameworks/native/+/master/include/android/configuration.h#473
if (protoConfig.getMnc() != 0) {
configuration.setNetworkCodeQualifier(
NetworkCodeQualifier.getQualifier(
String.format(
Locale.US,
"mnc%1$03d",
protoConfig.getMnc() == 0xffff ? 0 : protoConfig.getMnc())));
}
if (!protoConfig.getLocale().isEmpty()) {
// The proto stores it in a BCP-47 format, but the parser requires a b+ and all the - as +.
// It's a nice a little impedance mismatch.
new LocaleQualifier()
.checkAndSet("b+" + protoConfig.getLocale().replace("-", "+"), configuration);
}
if (LAYOUT_DIRECTION_MAP.containsKey(protoConfig.getLayoutDirection())) {
configuration.setLayoutDirectionQualifier(
new LayoutDirectionQualifier(LAYOUT_DIRECTION_MAP.get(protoConfig.getLayoutDirection())));
}
if (protoConfig.getSmallestScreenWidthDp() > 0) {
configuration.setSmallestScreenWidthQualifier(
new SmallestScreenWidthQualifier(protoConfig.getSmallestScreenWidthDp()));
}
// screen dimension is defined if one number is greater than 0
if (Math.max(protoConfig.getScreenHeight(), protoConfig.getScreenWidth()) > 0) {
configuration.setScreenDimensionQualifier(
new ScreenDimensionQualifier(
Math.max(
protoConfig.getScreenHeight(),
protoConfig.getScreenWidth()), // biggest is always first
Math.min(protoConfig.getScreenHeight(), protoConfig.getScreenWidth())));
}
if (protoConfig.getScreenWidthDp() > 0) {
configuration.setScreenWidthQualifier(
new ScreenWidthQualifier(protoConfig.getScreenWidthDp()));
}
if (protoConfig.getScreenHeightDp() > 0) {
configuration.setScreenHeightQualifier(
new ScreenHeightQualifier(protoConfig.getScreenHeightDp()));
}
if (LAYOUT_SIZE_MAP.containsKey(protoConfig.getScreenLayoutSize())) {
configuration.setScreenSizeQualifier(
new ScreenSizeQualifier(LAYOUT_SIZE_MAP.get(protoConfig.getScreenLayoutSize())));
}
if (SCREEN_LONG_MAP.containsKey(protoConfig.getScreenLayoutLong())) {
configuration.setScreenRatioQualifier(
new ScreenRatioQualifier(SCREEN_LONG_MAP.get(protoConfig.getScreenLayoutLong())));
}
if (SCREEN_ROUND_MAP.containsKey(protoConfig.getScreenRound())) {
configuration.setScreenRoundQualifier(
new ScreenRoundQualifier(SCREEN_ROUND_MAP.get(protoConfig.getScreenRound())));
}
if (SCREEN_ORIENTATION_MAP.containsKey(protoConfig.getOrientation())) {
configuration.setScreenOrientationQualifier(
new ScreenOrientationQualifier(SCREEN_ORIENTATION_MAP.get(protoConfig.getOrientation())));
}
if (SCREEN_UI_MODE.containsKey(protoConfig.getUiModeType())) {
configuration.setUiModeQualifier(
new UiModeQualifier(SCREEN_UI_MODE.get(protoConfig.getUiModeType())));
}
if (NIGHT_MODE_MAP.containsKey(protoConfig.getUiModeNight())) {
configuration.setNightModeQualifier(
new NightModeQualifier(NIGHT_MODE_MAP.get(protoConfig.getUiModeNight())));
}
if (DENSITY_MAP.containsKey(protoConfig.getDensity())) {
configuration.setDensityQualifier(
new DensityQualifier(DENSITY_MAP.get(protoConfig.getDensity())));
}
if (TOUCH_TYPE_MAP.containsKey(protoConfig.getTouchscreen())) {
configuration.setTouchTypeQualifier(
new TouchScreenQualifier(TOUCH_TYPE_MAP.get(protoConfig.getTouchscreen())));
}
if (KEYBOARD_STATE_MAP.containsKey(protoConfig.getKeysHidden())) {
configuration.setKeyboardStateQualifier(
new KeyboardStateQualifier(KEYBOARD_STATE_MAP.get(protoConfig.getKeysHidden())));
}
if (KEYBOARD_MAP.containsKey(protoConfig.getKeyboard())) {
configuration.setTextInputMethodQualifier(
new TextInputMethodQualifier(KEYBOARD_MAP.get(protoConfig.getKeyboard())));
}
if (NAV_STATE_MAP.containsKey(protoConfig.getNavHidden())) {
configuration.setNavigationStateQualifier(
new NavigationStateQualifier(NAV_STATE_MAP.get(protoConfig.getNavHidden())));
}
if (NAVIGATION_MAP.containsKey(protoConfig.getNavigation())) {
configuration.setNavigationMethodQualifier(
new NavigationMethodQualifier(NAVIGATION_MAP.get(protoConfig.getNavigation())));
}
if (protoConfig.getSdkVersion() > 0) {
configuration.setVersionQualifier(new VersionQualifier(protoConfig.getSdkVersion()));
}
return Arrays.stream(configuration.getQualifiers())
.map(ResourceQualifier::getFolderSegment)
.collect(toList());
}
/**
* Forwards compiled resource data files to consumers, extracting any embedded IDs along the way.
*/
private static void consumeCompiledFile(
DependencyInfo dependencyInfo,
KeyValueConsumers consumers,
CompiledFileWithData compiledFileWithData,
VisibilityRegistry registry)
throws InvalidProtocolBufferException {
CompiledFile compiledFile = compiledFileWithData.compiledFile();
Path sourcePath = Paths.get(compiledFile.getSourcePath());
DataSource dataSource = DataSource.of(dependencyInfo, sourcePath);
ResourceName resourceName = ResourceName.parse(compiledFile.getResourceName());
FullyQualifiedName fqn =
FullyQualifiedName.of(
resourceName.pkg(),
convertToQualifiers(compiledFile.getConfig()),
resourceName.type(),
resourceName.entry());
consumers.overwritingConsumer.accept(
fqn,
DataValueFile.of(
registry.getVisibility(resourceName),
dataSource,
compiledFileWithData.fingerprint(),
compiledFileWithData.rootXmlNode()));
for (CompiledFile.Symbol exportedSymbol : compiledFile.getExportedSymbolList()) {
if (exportedSymbol.getResourceName().startsWith("android:")) {
// Skip writing resource xml's for resources in the sdk
continue;
}
ResourceName exportedResourceName = ResourceName.parse(exportedSymbol.getResourceName());
verify(exportedResourceName.type() == ResourceType.ID);
FullyQualifiedName symbolFqn =
FullyQualifiedName.of(
exportedResourceName.pkg(),
/*qualifiers=*/ ImmutableList.of(),
exportedResourceName.type(),
exportedResourceName.entry());
DataResourceXml dataResourceXml =
DataResourceXml.from(
Value.getDefaultInstance(),
registry.getVisibility(exportedResourceName),
dataSource,
ResourceType.ID,
/*packageResolver=*/ null);
consumers.combiningConsumer.accept(symbolFqn, dataResourceXml);
}
}
private static void readAttributesFile(
DependencyInfo dependencyInfo,
InputStream resourceFileStream,
FileSystem fileSystem,
ParsedAndroidData.KeyValueConsumer<DataKey, DataResource> combine,
ParsedAndroidData.KeyValueConsumer<DataKey, DataResource> overwrite)
throws IOException {
Header header = Header.parseDelimitedFrom(resourceFileStream);
List<FullyQualifiedName> fullyQualifiedNames = new ArrayList<>();
for (int i = 0; i < header.getEntryCount(); i++) {
SerializeFormat.DataKey protoKey =
SerializeFormat.DataKey.parseDelimitedFrom(resourceFileStream);
fullyQualifiedNames.add(FullyQualifiedName.fromProto(protoKey));
}
DataSourceTable sourceTable =
DataSourceTable.read(dependencyInfo, resourceFileStream, fileSystem, header);
for (FullyQualifiedName fullyQualifiedName : fullyQualifiedNames) {
SerializeFormat.DataValue protoValue =
SerializeFormat.DataValue.parseDelimitedFrom(resourceFileStream);
DataSource source = sourceTable.sourceFromId(protoValue.getSourceId());
DataResourceXml dataResourceXml = (DataResourceXml) DataResourceXml.from(protoValue, source);
AttributeType attributeType = AttributeType.valueOf(protoValue.getXmlValue().getValueType());
if (attributeType.isCombining()) {
combine.accept(fullyQualifiedName, dataResourceXml);
} else {
overwrite.accept(fullyQualifiedName, dataResourceXml);
}
}
}
public static Map<DataKey, DataResource> readAttributes(CompiledResources resources) {
try (ZipInputStream zipStream = new ZipInputStream(Files.newInputStream(resources.getZip()))) {
Map<DataKey, DataResource> attributes = new LinkedHashMap<>();
for (ZipEntry entry = zipStream.getNextEntry();
entry != null;
entry = zipStream.getNextEntry()) {
if (entry.getName().endsWith(CompiledResources.ATTRIBUTES_FILE_EXTENSION)) {
readAttributesFile(
// Don't care about origin of ".attributes" values, since they don't feed into field
// initializers.
DependencyInfo.UNKNOWN,
zipStream,
FileSystems.getDefault(),
(key, value) ->
attributes.put(
key,
attributes.containsKey(key) ? attributes.get(key).combineWith(value) : value),
(key, value) ->
attributes.put(
key,
attributes.containsKey(key) ? attributes.get(key).overwrite(value) : value));
}
}
return attributes;
} catch (IOException e) {
throw new DeserializationException(e);
}
}
@VisibleForTesting
public static void readTable(
DependencyInfo dependencyInfo, InputStream in, KeyValueConsumers consumers)
throws IOException {
final ResourceTable resourceTable =
ResourceTable.parseFrom(in, ExtensionRegistry.getEmptyRegistry());
consumeResourceTable(
dependencyInfo,
consumers,
resourceTable,
new VisibilityRegistry(ImmutableSet.of(), ImmutableSet.of()));
}
public Map<DataKey, DataResource> read(DependencyInfo dependencyInfo, Path inPath) {
Map<DataKey, DataResource> resources = new LinkedHashMap<>();
read(
dependencyInfo,
inPath,
KeyValueConsumers.of(
resources::put,
resources::put,
(key, value) -> {
throw new IllegalStateException(String.format("Unexpected asset in %s", inPath));
}));
return resources;
}
@Override
public void read(DependencyInfo dependencyInfo, Path inPath, KeyValueConsumers consumers) {
Stopwatch timer = Stopwatch.createStarted();
try (ZipFile zipFile = new ZipFile(inPath.toFile())) {
ResourceContainer resourceContainer =
readResourceContainer(zipFile, includeFileContentsForValidation);
VisibilityRegistry registry = computeResourceVisibility(resourceContainer);
for (ResourceTable resourceTable : resourceContainer.resourceTables()) {
consumeResourceTable(dependencyInfo, consumers, resourceTable, registry);
}
for (CompiledFileWithData compiledFile : resourceContainer.compiledFiles()) {
consumeCompiledFile(dependencyInfo, consumers, compiledFile, registry);
}
Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries();
while (resourceFiles.hasMoreElements()) {
ZipEntry resourceFile = resourceFiles.nextElement();
String fileZipPath = resourceFile.getName();
try (InputStream resourceFileStream = zipFile.getInputStream(resourceFile)) {
if (fileZipPath.endsWith(CompiledResources.ATTRIBUTES_FILE_EXTENSION)) {
readAttributesFile(
dependencyInfo,
resourceFileStream,
inPath.getFileSystem(),
consumers.combiningConsumer,
consumers.overwritingConsumer);
}
}
}
} catch (IOException e) {
throw new DeserializationException("Error deserializing " + inPath, e);
} finally {
logger.fine(
String.format(
"Deserialized in compiled merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
}
}
private static ResourceContainer readResourceContainer(
ZipFile zipFile, boolean includeFileContentsForValidation) throws IOException {
List<ResourceTable> resourceTables = new ArrayList<>();
List<CompiledFileWithData> compiledFiles = new ArrayList<>();
Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries();
while (resourceFiles.hasMoreElements()) {
ZipEntry resourceFile = resourceFiles.nextElement();
String fileZipPath = resourceFile.getName();
if (fileZipPath.endsWith(CompiledResources.ATTRIBUTES_FILE_EXTENSION)) {
continue;
}
try (LittleEndianDataInputStream dataInputStream =
new LittleEndianDataInputStream(zipFile.getInputStream(resourceFile))) {
int magic = dataInputStream.readInt();
verify(
magic == AAPT_CONTAINER_MAGIC,
"Unexpected magic number in %s: %s",
resourceFile,
magic);
int version = dataInputStream.readInt();
verify(
version == AAPT_CONTAINER_VERSION,
"Unexpected version number in %s: %s",
resourceFile,
version);
int numberOfEntries = dataInputStream.readInt();
for (int i = 0; i < numberOfEntries; i++) {
int entryType = dataInputStream.readInt();
verify(
entryType == AAPT_CONTAINER_ENTRY_RES_TABLE
|| entryType == AAPT_CONTAINER_ENTRY_RES_FILE,
"Unexpected entry type in %s: %s",
resourceFile,
entryType);
if (entryType == AAPT_CONTAINER_ENTRY_RES_TABLE) {
long size = dataInputStream.readLong();
verify(size <= Integer.MAX_VALUE);
byte[] tableBytes = readBytesAndSkipPadding(dataInputStream, (int) size);
resourceTables.add(
ResourceTable.parseFrom(tableBytes, ExtensionRegistry.getEmptyRegistry()));
} else {
// useless and wrong "size" data; see
// https://android-review.googlesource.com/c/platform/frameworks/base/+/1161789
ByteStreams.skipFully(dataInputStream, 8);
int headerSize = dataInputStream.readInt();
long payloadSize = dataInputStream.readLong();
byte[] headerBytes = readBytesAndSkipPadding(dataInputStream, headerSize);
CompiledFile compiledFile =
CompiledFile.parseFrom(headerBytes, ExtensionRegistry.getEmptyRegistry());
HashCode fingerprint;
XmlNode rootXmlNode;
if (includeFileContentsForValidation) {
byte[] payloadBytes = readBytesAndSkipPadding(dataInputStream, (int) payloadSize);
fingerprint = Hashing.goodFastHash(/*minimumBits=*/ 64).hashBytes(payloadBytes);
rootXmlNode =
compiledFile.getType() == Resources.FileReference.Type.PROTO_XML
? XmlNode.parseFrom(payloadBytes, ExtensionRegistry.getEmptyRegistry())
: null;
} else {
ByteStreams.skipFully(
dataInputStream, roundUpToNearest(payloadSize, AAPT_FLAT_FILE_ALIGNMENT));
fingerprint = null;
rootXmlNode = null;
}
compiledFiles.add(CompiledFileWithData.create(compiledFile, fingerprint, rootXmlNode));
}
}
}
}
return ResourceContainer.create(resourceTables, compiledFiles);
}
/** Rounds {@code n} up to the nearest multiple of {@code k}. */
private static long roundUpToNearest(long n, int k) {
return ((n + k - 1) / k) * k;
}
/**
* Classifies resources as public/private/unknown.
*
* <p>Per https://developer.android.com/studio/projects/android-library#PrivateResources, the
* {@code <public/>} tag marks a resource as public with all others inferred to be private. Since
* {@code android_library} allows mixing directories (with varying ownership) in {@code
* resource_files} (b/148110689), we perform the classification on a per-directory basis, so that
* marking something in {@code foo/res} public has no impact on {@code bar/res}.
*/
private static VisibilityRegistry computeResourceVisibility(ResourceContainer resourceContainer)
throws UnsupportedEncodingException {
if (!ResourceCompiler.USE_VISIBILITY_FROM_AAPT2) {
return new VisibilityRegistry(ImmutableSet.of(), ImmutableSet.of());
}
// decode source pools ahead of time to avoid having to repeatedly do so later.
List<List<String>> sourcePools = new ArrayList<>();
for (ResourceTable resourceTable : resourceContainer.resourceTables()) {
sourcePools.add(decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray()));
}
PublicResources publicResources = findExplicitlyPublicResources(resourceContainer, sourcePools);
return new VisibilityRegistry(
publicResources.explicitlyPublicResources(),
findImpliedPrivateResources(resourceContainer, sourcePools, publicResources));
}
private static PublicResources findExplicitlyPublicResources(
ResourceContainer resourceContainer, List<List<String>> sourcePools) {
Set<ResourceName> explicitlyPublicResources = new HashSet<>();
Set<Path> directoriesWithPublicResources = new HashSet<>();
for (int i = 0; i < resourceContainer.resourceTables().size(); i++) {
ResourceTable resourceTable = resourceContainer.resourceTables().get(i);
List<String> sourcePool = sourcePools.get(i);
for (Package pkg : resourceTable.getPackageList()) {
for (Resources.Type type : pkg.getTypeList()) {
ResourceType resourceType = ResourceType.getEnum(type.getName());
for (Resources.Entry entry : type.getEntryList()) {
if (entry.getVisibility().getLevel() != Resources.Visibility.Level.PUBLIC) {
continue;
}
explicitlyPublicResources.add(
ResourceName.create(pkg.getPackageName(), resourceType, entry.getName()));
directoriesWithPublicResources.add(
getNormalizedResourceDirectory(
sourcePool.get(entry.getVisibility().getSource().getPathIdx())));
}
}
}
}
return PublicResources.create(explicitlyPublicResources, directoriesWithPublicResources);
}
private static ImmutableSet<ResourceName> findImpliedPrivateResources(
ResourceContainer resourceContainer,
List<List<String>> sourcePools,
PublicResources publicResources) {
Set<ResourceName> explicitlyPublicResources = publicResources.explicitlyPublicResources();
Set<Path> directoriesWithPublicResources = publicResources.directoriesWithPublicResources();
Set<ResourceName> impliedPrivateResources = new HashSet<>();
for (int i = 0; i < resourceContainer.resourceTables().size(); i++) {
ResourceTable resourceTable = resourceContainer.resourceTables().get(i);
List<String> sourcePool = sourcePools.get(i);
for (Package pkg : resourceTable.getPackageList()) {
for (Resources.Type type : pkg.getTypeList()) {
ResourceType resourceType = ResourceType.getEnum(type.getName());
for (Resources.Entry entry : type.getEntryList()) {
if (entry.getVisibility().getLevel() == Resources.Visibility.Level.PUBLIC) {
continue;
}
ResourceName resourceName =
ResourceName.create(pkg.getPackageName(), resourceType, entry.getName());
if (explicitlyPublicResources.contains(resourceName)
|| impliedPrivateResources.contains(resourceName)) {
continue; // we already figured out a classification for this resource.
}
boolean inDirectoryWithPublic =
entry.getConfigValueList().stream()
.map(
configValue ->
getNormalizedResourceDirectory(
sourcePool.get(configValue.getValue().getSource().getPathIdx())))
.anyMatch(directoriesWithPublicResources::contains);
if (inDirectoryWithPublic) {
impliedPrivateResources.add(resourceName);
}
}
}
}
}
for (CompiledFileWithData compiledFileWithData : resourceContainer.compiledFiles()) {
CompiledFile compiledFile = compiledFileWithData.compiledFile();
ResourceName resourceName = ResourceName.parse(compiledFile.getResourceName());
if (explicitlyPublicResources.contains(resourceName)
|| impliedPrivateResources.contains(resourceName)) {
continue; // we already figured out a classification for this resource.
}
if (directoriesWithPublicResources.contains(
getNormalizedResourceDirectory(compiledFile.getSourcePath()))) {
impliedPrivateResources.add(resourceName);
}
}
return ImmutableSet.copyOf(impliedPrivateResources);
}
/**
* Returns the resource directory (e.g. {@code java/com/pkg/res/}) which contains a file,
* stripping off any {@code blaze-*} prefix for normalization.
*/
private static Path getNormalizedResourceDirectory(String filename) {
Path resDir = Paths.get(filename).getParent().getParent();
if (resDir.getName(0).toString().startsWith("blaze-")) {
// strip off stuff like blaze-out/k8-fastbuild/bin/.
return resDir.subpath(3, resDir.getNameCount());
} else {
return resDir;
}
}
private static byte[] readBytesAndSkipPadding(LittleEndianDataInputStream input, int size)
throws IOException {
byte[] result = new byte[size];
input.readFully(result);
long overage = size % AAPT_FLAT_FILE_ALIGNMENT;
if (overage != 0) {
ByteStreams.skipFully(input, AAPT_FLAT_FILE_ALIGNMENT - overage);
}
return result;
}
private static List<String> decodeSourcePool(byte[] bytes) throws UnsupportedEncodingException {
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
int stringCount = byteBuffer.getInt(8);
boolean isUtf8 = (byteBuffer.getInt(16) & (1 << 8)) != 0;
int stringsStart = byteBuffer.getInt(20);
// Position the ByteBuffer after the metadata
byteBuffer.position(28);
List<String> strings = new ArrayList<>();
for (int i = 0; i < stringCount; i++) {
int stringOffset = stringsStart + byteBuffer.getInt();
if (isUtf8) {
int characterCount = byteBuffer.get(stringOffset) & 0xFF;
if ((characterCount & 0x80) != 0) {
characterCount =
((characterCount & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
}
stringOffset += (characterCount >= 0x80 ? 2 : 1);
int length = byteBuffer.get(stringOffset) & 0xFF;
if ((length & 0x80) != 0) {
length = ((length & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
}
stringOffset += (length >= 0x80 ? 2 : 1);
strings.add(new String(bytes, stringOffset, length, "UTF8"));
} else {
// TODO(b/148817379): this next block of lines is forming an int with holes in it.
int characterCount = byteBuffer.get(stringOffset) & 0xFFFF;
if ((characterCount & 0x8000) != 0) {
characterCount =
((characterCount & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
}
stringOffset += 2 * (characterCount >= 0x8000 ? 2 : 1);
int length = byteBuffer.get(stringOffset) & 0xFFFF;
if ((length & 0x8000) != 0) {
length = ((length & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
}
stringOffset += 2 * (length >= 0x8000 ? 2 : 1);
strings.add(new String(bytes, stringOffset, length, "UTF16"));
}
}
return strings;
}
@AutoValue
abstract static class ResourceContainer {
abstract ImmutableList<ResourceTable> resourceTables();
abstract ImmutableList<CompiledFileWithData> compiledFiles();
static ResourceContainer create(
List<ResourceTable> resourceTables, List<CompiledFileWithData> compiledFiles) {
return new AutoValue_AndroidCompiledDataDeserializer_ResourceContainer(
ImmutableList.copyOf(resourceTables), ImmutableList.copyOf(compiledFiles));
}
}
@AutoValue
abstract static class CompiledFileWithData {
abstract CompiledFile compiledFile();
@Nullable
abstract HashCode fingerprint();
@Nullable
abstract XmlNode rootXmlNode();
static CompiledFileWithData create(
CompiledFile compiledFile, @Nullable HashCode fingerprint, @Nullable XmlNode xmlNode) {
return new AutoValue_AndroidCompiledDataDeserializer_CompiledFileWithData(
compiledFile, fingerprint, xmlNode);
}
}
/**
* Wraps metadata regarding {@code <public/>} tags and their locations to help work around targets
* which mix res/ directories they own with ones they don't (b/148110689).
*/
@AutoValue
abstract static class PublicResources {
abstract ImmutableSet<ResourceName> explicitlyPublicResources();
abstract ImmutableSet<Path> directoriesWithPublicResources();
static PublicResources create(
Set<ResourceName> explicitlyPublicResources, Set<Path> directoriesWithPublicResources) {
return new AutoValue_AndroidCompiledDataDeserializer_PublicResources(
ImmutableSet.copyOf(explicitlyPublicResources),
ImmutableSet.copyOf(directoriesWithPublicResources));
}
}
private static class VisibilityRegistry {
private final Set<ResourceName> explicitlyPublicResources;
private final Set<ResourceName> impliedPrivateResources;
VisibilityRegistry(
Set<ResourceName> explicitlyPublicResources, Set<ResourceName> impliedPrivateResources) {
this.explicitlyPublicResources = ImmutableSet.copyOf(explicitlyPublicResources);
this.impliedPrivateResources = ImmutableSet.copyOf(impliedPrivateResources);
}
Visibility getVisibility(ResourceName resourceName) {
if (explicitlyPublicResources.contains(resourceName)) {
return Visibility.PUBLIC;
}
// TODO(b/146647897): make resources private by default
return impliedPrivateResources.contains(resourceName)
? Visibility.PRIVATE
: Visibility.UNKNOWN;
}
}
}