blob: 8c240312099f2ef7c10dd45da068266aaf85a083 [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.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.not;
import static java.util.stream.Collectors.toList;
import android.aapt.pb.internal.ResourcesInternal.CompiledFile;
import com.android.SdkConstants;
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.Visibility.Level;
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.common.base.Preconditions;
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.io.LittleEndianDataInputStream;
import com.google.devtools.build.android.FullyQualifiedName.Factory;
import com.google.devtools.build.android.proto.SerializeFormat;
import com.google.devtools.build.android.proto.SerializeFormat.Header;
import com.google.devtools.build.android.xml.ResourcesAttribute.AttributeType;
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.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
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());
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);
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);
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);
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();
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);
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);
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);
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);
static final ImmutableMap<Configuration.NavHidden, NavigationState> NAV_STATE_MAP =
ImmutableMap.of(
NavHidden.NAV_HIDDEN_NAVHIDDEN,
NavigationState.HIDDEN,
NavHidden.NAV_HIDDEN_NAVEXPOSED,
NavigationState.EXPOSED);
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);
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 ImmutableSet<String> filteredResources;
/**
* @param filteredResources resources that were filtered out of this target and should be ignored
* if they are referenced in symbols files.
*/
public static AndroidCompiledDataDeserializer withFilteredResources(
Collection<String> filteredResources) {
return new AndroidCompiledDataDeserializer(ImmutableSet.copyOf(filteredResources));
}
public static AndroidCompiledDataDeserializer create() {
return new AndroidCompiledDataDeserializer(ImmutableSet.of());
}
private AndroidCompiledDataDeserializer(ImmutableSet<String> filteredResources) {
this.filteredResources = filteredResources;
}
private void readResourceTable(
LittleEndianDataInputStream resourceTableStream, KeyValueConsumers consumers)
throws IOException {
long alignedSize = resourceTableStream.readLong();
Preconditions.checkArgument(alignedSize <= Integer.MAX_VALUE);
byte[] tableBytes = new byte[(int) alignedSize];
resourceTableStream.read(tableBytes, 0, (int) alignedSize);
ResourceTable resourceTable = ResourceTable.parseFrom(tableBytes);
readPackages(consumers, resourceTable);
}
private void readPackages(KeyValueConsumers consumers, ResourceTable resourceTable)
throws UnsupportedEncodingException, InvalidProtocolBufferException {
List<String> sourcePool =
decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray());
ReferenceResolver resolver = ReferenceResolver.asRoot();
for (int i = resourceTable.getPackageCount() - 1; i >= 0; i--) {
Package resourceTablePackage = resourceTable.getPackage(i);
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 (resource.getConfigValueList().isEmpty()
&& resource.getVisibility().getLevel() == Level.PUBLIC) {
// This is a public resource definition.
int sourceIndex = resource.getVisibility().getSource().getPathIdx();
String source = sourcePool.get(sourceIndex);
DataSource dataSource = DataSource.of(Paths.get(source));
DataResourceXml dataResourceXml =
DataResourceXml.fromPublic(dataSource, resourceType, resource.getEntryId().getId());
final FullyQualifiedName fqn =
createAndRecordFqn(
packageResolver, packageName, resourceType, resource, ImmutableList.of());
consumers.combiningConsumer.accept(fqn, dataResourceXml);
} else 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));
int sourceIndex = configValue.getValue().getSource().getPathIdx();
String source = sourcePool.get(sourceIndex);
DataSource dataSource = DataSource.of(Paths.get(source));
Value resourceValue = configValue.getValue();
DataResource dataResource =
resourceValue.getItem().hasFile()
? DataValueFile.of(dataSource)
: DataResourceXml.from(
resourceValue, 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())
.toPrettyString();
}
}
}
}
}
/** Maintains state for all references in each package of a resource table. */
@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 HashMap<>());
}
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) {
return checkNotNull(
qualifiedReferenceInlineStatus.get(reference),
"%s reference is unsatisfied. Available names: %s",
reference,
qualifiedReferenceInlineStatus.keySet())
.equals(InlineStatus.INLINEABLE)
// Only inline if it's in the current package.
&& reference.isInPackage(packageName.orElse(FullyQualifiedName.DEFAULT_PACKAGE));
}
/** Update the reference's inline state. */
public FullyQualifiedName markInlined(FullyQualifiedName reference) {
qualifiedReferenceInlineStatus.put(reference, InlineStatus.INLINED);
return reference;
}
}
private 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;
}
private List<String> convertToQualifiers(ConfigValue configValue) {
FolderConfiguration configuration = new FolderConfiguration();
final Configuration protoConfig = configValue.getConfig();
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().replaceAll("-", "+"), 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());
}
/**
* Reads compiled resource data files and adds them to consumers
*
* @param compiledFileStream First byte is number of compiled files represented in this file. Next
* 8 bytes is a long indicating the length of the metadata describing the compiled file. Next
* N bytes is the metadata describing the compiled file. The remaining bytes are the actual
* original file.
* @param consumers
* @param fqnFactory
* @throws IOException
*/
private void readCompiledFile(
LittleEndianDataInputStream compiledFileStream,
KeyValueConsumers consumers,
Factory fqnFactory)
throws IOException {
// Skip aligned size. We don't need it here.
Preconditions.checkArgument(compiledFileStream.skipBytes(8) == 8);
int resFileHeaderSize = compiledFileStream.readInt();
// Skip data payload size. We don't need it here.
Preconditions.checkArgument(compiledFileStream.skipBytes(8) == 8);
byte[] file = new byte[resFileHeaderSize];
compiledFileStream.read(file, 0, resFileHeaderSize);
CompiledFile compiledFile = CompiledFile.parseFrom(file);
Path sourcePath = Paths.get(compiledFile.getSourcePath());
FullyQualifiedName fqn = fqnFactory.parse(sourcePath);
DataSource dataSource = DataSource.of(sourcePath);
if (consumers != null) {
consumers.overwritingConsumer.accept(fqn, DataValueFile.of(dataSource));
}
for (CompiledFile.Symbol exportedSymbol : compiledFile.getExportedSymbolList()) {
if (!exportedSymbol.getResourceName().startsWith("android:")) {
// Skip writing resource xml's for resources in the sdk
FullyQualifiedName symbolFqn =
fqnFactory.create(
ResourceType.ID, exportedSymbol.getResourceName().replaceFirst("id/", ""));
DataResourceXml dataResourceXml =
DataResourceXml.from(null, dataSource, ResourceType.ID, null);
consumers.combiningConsumer.accept(symbolFqn, dataResourceXml);
}
}
}
private void readAttributesFile(
InputStream resourceFileStream, FileSystem fileSystem, KeyValueConsumers consumers)
throws IOException {
Header header = Header.parseDelimitedFrom(resourceFileStream);
List<DataKey> 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(resourceFileStream, fileSystem, header);
for (DataKey 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()) {
consumers.combiningConsumer.accept(fullyQualifiedName, dataResourceXml);
} else {
consumers.overwritingConsumer.accept(fullyQualifiedName, dataResourceXml);
}
}
}
public void readTable(InputStream in, KeyValueConsumers consumers) throws IOException {
final ResourceTable resourceTable = ResourceTable.parseFrom(in);
readPackages(consumers, resourceTable);
}
@Override
public void read(Path inPath, KeyValueConsumers consumers) {
Stopwatch timer = Stopwatch.createStarted();
try (ZipFile zipFile = new ZipFile(inPath.toFile())) {
Enumeration<? extends ZipEntry> resourceFiles = zipFile.entries();
while (resourceFiles.hasMoreElements()) {
ZipEntry resourceFile = resourceFiles.nextElement();
String fileZipPath = resourceFile.getName();
int resourceSubdirectoryIndex = fileZipPath.indexOf('_', fileZipPath.lastIndexOf('/'));
Path filePath =
Paths.get(
String.format(
"%s%c%s",
fileZipPath.substring(0, resourceSubdirectoryIndex),
'/',
fileZipPath.substring(resourceSubdirectoryIndex + 1)));
String shortPath = filePath.getParent().getFileName() + "/" + filePath.getFileName();
if (filteredResources.contains(shortPath) && !Files.exists(filePath)) {
// Skip files that were filtered out during analysis.
// TODO(asteinb): Properly filter out these files from android_library symbol files during
// analysis instead, and remove this list.
continue;
}
try (InputStream resourceFileStream = zipFile.getInputStream(resourceFile)) {
final String[] dirNameAndQualifiers =
filePath.getParent().getFileName().toString().split(SdkConstants.RES_QUALIFIER_SEP);
Factory fqnFactory = Factory.fromDirectoryName(dirNameAndQualifiers);
if (fileZipPath.endsWith(".attributes")) {
readAttributesFile(resourceFileStream, inPath.getFileSystem(), consumers);
} else {
LittleEndianDataInputStream dataInputStream =
new LittleEndianDataInputStream(resourceFileStream);
int magicNumber = dataInputStream.readInt();
int formatVersion = dataInputStream.readInt();
int numberOfEntries = dataInputStream.readInt();
int resourceType = dataInputStream.readInt();
if (resourceType == 0) { // 0 is a resource table
readResourceTable(dataInputStream, consumers);
} else if (resourceType == 1) { // 1 is a resource file
readCompiledFile(dataInputStream, consumers, fqnFactory);
} else {
throw new DeserializationException(
"aapt2 version mismatch.",
new DeserializationException(
String.format(
"Unexpected tag for resourceType %s expected 0 or 1 in %s."
+ "\n Last known good values:"
+ "\n\tmagicNumber 1414545729 (is %s)"
+ "\n\tformatVersion 1 (is %s)"
+ "\n\tnumberOfEntries 1 (is %s)",
resourceType, fileZipPath, magicNumber, formatVersion, numberOfEntries)));
}
}
}
}
} 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 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 {
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;
}
}