| /* |
| * Copyright 2016 Google Inc. 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.devrel.gmscore.tools.apk.arsc; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableMap.Builder; |
| import com.google.common.io.LittleEndianDataOutputStream; |
| import com.google.common.primitives.Shorts; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataOutput; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.util.Map; |
| |
| import javax.annotation.Nullable; |
| |
| /** Represents a generic chunk. */ |
| public abstract class Chunk implements SerializableResource { |
| |
| /** Types of chunks that can exist. */ |
| public enum Type { |
| NULL(0x0000), |
| STRING_POOL(0x0001), |
| TABLE(0x0002), |
| XML(0x0003), |
| XML_START_NAMESPACE(0x0100), |
| XML_END_NAMESPACE(0x0101), |
| XML_START_ELEMENT(0x0102), |
| XML_END_ELEMENT(0x0103), |
| XML_CDATA(0x0104), |
| XML_RESOURCE_MAP(0x0180), |
| TABLE_PACKAGE(0x0200), |
| TABLE_TYPE(0x0201), |
| TABLE_TYPE_SPEC(0x0202), |
| TABLE_LIBRARY(0x0203); |
| |
| private final short code; |
| |
| private static final Map<Short, Type> FROM_SHORT; |
| |
| static { |
| Builder<Short, Type> builder = ImmutableMap.builder(); |
| for (Type type : values()) { |
| builder.put(type.code(), type); |
| } |
| FROM_SHORT = builder.build(); |
| } |
| |
| Type(int code) { |
| this.code = Shorts.checkedCast(code); |
| } |
| |
| public short code() { |
| return code; |
| } |
| |
| public static Type fromCode(short code) { |
| return Preconditions.checkNotNull(FROM_SHORT.get(code), "Unknown chunk type: %s", code); |
| } |
| } |
| |
| /** The byte boundary to pad chunks on. */ |
| public static final int PAD_BOUNDARY = 4; |
| |
| /** The number of bytes in every chunk that describes chunk type, header size, and chunk size. */ |
| public static final int METADATA_SIZE = 8; |
| |
| /** The offset in bytes, from the start of the chunk, where the chunk size can be found. */ |
| private static final int CHUNK_SIZE_OFFSET = 4; |
| |
| /** The parent to this chunk, if any. */ |
| @Nullable |
| private final Chunk parent; |
| |
| /** Size of the chunk header in bytes. */ |
| protected final int headerSize; |
| |
| /** headerSize + dataSize. The total size of this chunk. */ |
| protected final int chunkSize; |
| |
| /** Offset of this chunk from the start of the file. */ |
| protected final int offset; |
| |
| protected Chunk(ByteBuffer buffer, @Nullable Chunk parent) { |
| this.parent = parent; |
| offset = buffer.position() - 2; |
| headerSize = (buffer.getShort() & 0xFFFF); |
| chunkSize = buffer.getInt(); |
| } |
| |
| /** |
| * Finishes initialization of a chunk. This should be called immediately after the constructor. |
| * This is separate from the constructor so that the header of a chunk can be fully initialized |
| * before the payload of that chunk is initialized for chunks that require such behavior. |
| * |
| * @param buffer The buffer that the payload will be initialized from. |
| */ |
| protected void init(ByteBuffer buffer) {} |
| |
| /** |
| * Returns the parent to this chunk, if any. A parent is a chunk whose payload contains this |
| * chunk. If there's no parent, null is returned. |
| */ |
| @Nullable |
| public Chunk getParent() { |
| return parent; |
| } |
| |
| protected abstract Type getType(); |
| |
| /** Returns the size of this chunk's header. */ |
| public final int getHeaderSize() { |
| return headerSize; |
| } |
| |
| /** |
| * Returns the size of this chunk when it was first read from a buffer. A chunk's size can deviate |
| * from this value when its data is modified (e.g. adding an entry, changing a string). |
| * |
| * <p>A chunk's current size can be determined from the length of the byte array returned from |
| * {@link #toByteArray}. |
| */ |
| public final int getOriginalChunkSize() { |
| return chunkSize; |
| } |
| |
| /** |
| * Reposition the buffer after this chunk. Use this at the end of a Chunk constructor. |
| * @param buffer The buffer to be repositioned. |
| */ |
| private final void seekToEndOfChunk(ByteBuffer buffer) { |
| buffer.position(offset + chunkSize); |
| } |
| |
| /** |
| * Writes the type and header size. We don't know how big this chunk will be (it could be |
| * different since the last time we checked), so this needs to be passed in. |
| * |
| * @param output The buffer that will be written to. |
| * @param chunkSize The total size of this chunk in bytes, including the header. |
| */ |
| protected final void writeHeader(ByteBuffer output, int chunkSize) { |
| int start = output.position(); |
| output.putShort(getType().code()); |
| output.putShort((short) headerSize); |
| output.putInt(chunkSize); |
| writeHeader(output); |
| int headerBytes = output.position() - start; |
| Preconditions.checkState(headerBytes == getHeaderSize(), |
| "Written header is wrong size. Got %s, want %s", headerBytes, getHeaderSize()); |
| } |
| |
| /** |
| * Writes the remaining header (after the type, {@code headerSize}, and {@code chunkSize}). |
| * |
| * @param output The buffer that the header will be written to. |
| */ |
| protected void writeHeader(ByteBuffer output) {} |
| |
| /** |
| * Writes the chunk payload. The payload is data in a chunk which is not in |
| * the first {@code headerSize} bytes of the chunk. |
| * |
| * @param output The stream that the payload will be written to. |
| * @param header The already-written header. This can be modified to fix payload offsets. |
| * @param shrink True if this payload should be optimized for size. |
| * @throws IOException Thrown if {@code output} could not be written to (out of memory). |
| */ |
| protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink) |
| throws IOException {} |
| |
| /** |
| * Pads {@code output} until {@code currentLength} is on a 4-byte boundary. |
| * |
| * @param output The {@link DataOutput} that will be padded. |
| * @param currentLength The current length, in bytes, of {@code output} |
| * @return The new length of {@code output} |
| * @throws IOException Thrown if {@code output} could not be written to. |
| */ |
| protected int writePad(DataOutput output, int currentLength) throws IOException { |
| while (currentLength % PAD_BOUNDARY != 0) { |
| output.write(0); |
| ++currentLength; |
| } |
| return currentLength; |
| } |
| |
| @Override |
| public final byte[] toByteArray() throws IOException { |
| return toByteArray(false); |
| } |
| |
| /** |
| * Converts this chunk into an array of bytes representation. Normally you will not need to |
| * override this method unless your header changes based on the contents / size of the payload. |
| */ |
| @Override |
| public final byte[] toByteArray(boolean shrink) throws IOException { |
| ByteBuffer header = ByteBuffer.allocate(getHeaderSize()).order(ByteOrder.LITTLE_ENDIAN); |
| writeHeader(header, 0); // The chunk size isn't known yet. This will be filled in later. |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) { |
| writePayload(payload, header, shrink); |
| } |
| |
| byte[] payloadBytes = baos.toByteArray(); |
| int chunkSize = getHeaderSize() + payloadBytes.length; |
| header.putInt(CHUNK_SIZE_OFFSET, chunkSize); |
| |
| // Combine results |
| ByteBuffer result = ByteBuffer.allocate(chunkSize).order(ByteOrder.LITTLE_ENDIAN); |
| result.put(header.array()); |
| result.put(payloadBytes); |
| return result.array(); |
| } |
| |
| /** |
| * Creates a new chunk whose contents start at {@code buffer}'s current position. |
| * |
| * @param buffer A buffer positioned at the start of a chunk. |
| * @return new chunk |
| */ |
| public static Chunk newInstance(ByteBuffer buffer) { |
| return newInstance(buffer, null); |
| } |
| |
| /** |
| * Creates a new chunk whose contents start at {@code buffer}'s current position. |
| * |
| * @param buffer A buffer positioned at the start of a chunk. |
| * @param parent The parent to this chunk (or null if there's no parent). |
| * @return new chunk |
| */ |
| public static Chunk newInstance(ByteBuffer buffer, @Nullable Chunk parent) { |
| Chunk result; |
| Type type = Type.fromCode(buffer.getShort()); |
| switch (type) { |
| case STRING_POOL: |
| result = new StringPoolChunk(buffer, parent); |
| break; |
| case TABLE: |
| result = new ResourceTableChunk(buffer, parent); |
| break; |
| case XML: |
| result = new XmlChunk(buffer, parent); |
| break; |
| case XML_START_NAMESPACE: |
| result = new XmlNamespaceStartChunk(buffer, parent); |
| break; |
| case XML_END_NAMESPACE: |
| result = new XmlNamespaceEndChunk(buffer, parent); |
| break; |
| case XML_START_ELEMENT: |
| result = new XmlStartElementChunk(buffer, parent); |
| break; |
| case XML_END_ELEMENT: |
| result = new XmlEndElementChunk(buffer, parent); |
| break; |
| case XML_CDATA: |
| result = new XmlCdataChunk(buffer, parent); |
| break; |
| case XML_RESOURCE_MAP: |
| result = new XmlResourceMapChunk(buffer, parent); |
| break; |
| case TABLE_PACKAGE: |
| result = new PackageChunk(buffer, parent); |
| break; |
| case TABLE_TYPE: |
| result = new TypeChunk(buffer, parent); |
| break; |
| case TABLE_TYPE_SPEC: |
| result = new TypeSpecChunk(buffer, parent); |
| break; |
| case TABLE_LIBRARY: |
| result = new LibraryChunk(buffer, parent); |
| break; |
| default: |
| result = new UnknownChunk(buffer, parent); |
| } |
| result.init(buffer); |
| result.seekToEndOfChunk(buffer); |
| return result; |
| } |
| } |