
pink.madis.apk.arsc.StringPoolChunk Maven / Gradle / Ivy
/*
* 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 pink.madis.apk.arsc;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.io.LittleEndianDataOutputStream;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Represents a string pool structure. */
public final class StringPoolChunk extends Chunk {
// These are the defined flags for the "flags" field of ResourceStringPoolHeader
private static final int SORTED_FLAG = 1 << 0;
private static final int UTF8_FLAG = 1 << 8;
/** The offset from the start of the header that the stylesStart field is at. */
private static final int STYLE_START_OFFSET = 24;
/** Flags. */
private final int flags;
/** Index from header of the string data. */
private final int stringsStart;
/** Index from header of the style data. */
private final int stylesStart;
/**
* Number of strings in the original buffer. This is not necessarily the number of strings in
* {@code strings}.
*/
private final int stringCount;
/**
* Number of styles in the original buffer. This is not necessarily the number of styles in
* {@code styles}.
*/
private final int styleCount;
/**
* The strings ordered as they appear in the arsc file. e.g. strings.get(1234) gets the 1235th
* string in the arsc file.
*/
private final List strings = new ArrayList<>();
/**
* These styles have a 1:1 relationship with the strings. For example, styles.get(3) refers to
* the string at location strings.get(3). There are never more styles than strings (though there
* may be less). Inside of that are all of the styles referenced by that string.
*/
private final List styles = new ArrayList<>();
/**
* True if the original {@link StringPoolChunk} shows signs of being deduped. Specifically, this
* is set to true if there exists a string whose offset is <= the previous offset. This is used to
* preserve the deduping of strings for pools that have been deduped.
*/
private boolean isOriginalDeduped = false;
protected StringPoolChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
stringCount = buffer.getInt();
styleCount = buffer.getInt();
flags = buffer.getInt();
stringsStart = buffer.getInt();
stylesStart = buffer.getInt();
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
strings.addAll(readStrings(buffer, offset + stringsStart, stringCount));
styles.addAll(readStyles(buffer, offset + stylesStart, styleCount));
}
/**
* Returns the 0-based index of the first occurrence of the given string, or -1 if the string is
* not in the pool. This runs in O(n) time.
*
* @param string The string to check the pool for.
* @return Index of the string, or -1 if not found.
*/
public int indexOf(String string) {
return strings.indexOf(string);
}
/**
* Returns a string at the given (0-based) index.
*
* @param index The (0-based) index of the string to return.
* @throws IndexOutOfBoundsException If the index is out of range (index < 0 || index >= size()).
*/
public String getString(int index) {
return strings.get(index);
}
/** Returns the number of strings in this pool. */
public int getStringCount() {
return strings.size();
}
/**
* Returns a style at the given (0-based) index.
*
* @param index The (0-based) index of the style to return.
* @throws IndexOutOfBoundsException If the index is out of range (index < 0 || index >= size()).
*/
public StringPoolStyle getStyle(int index) {
return styles.get(index);
}
/** Returns the number of styles in this pool. */
public int getStyleCount() {
return styles.size();
}
/** Returns the type of strings in this pool. */
public ResourceString.Type getStringType() {
return isUTF8() ? ResourceString.Type.UTF8 : ResourceString.Type.UTF16;
}
@Override
protected Type getType() {
return Chunk.Type.STRING_POOL;
}
/** Returns the number of bytes needed for offsets based on {@code strings} and {@code styles}. */
private int getOffsetSize() {
return (strings.size() + styles.size()) * 4;
}
/**
* True if this string pool contains strings in UTF-8 format. Otherwise, strings are in UTF-16.
*
* @return true if @{code strings} are in UTF-8; false if they're in UTF-16.
*/
public boolean isUTF8() {
return (flags & UTF8_FLAG) != 0;
}
/**
* True if this string pool contains already-sorted strings.
*
* @return true if @{code strings} are sorted.
*/
public boolean isSorted() {
return (flags & SORTED_FLAG) != 0;
}
private List readStrings(ByteBuffer buffer, int offset, int count) {
List result = new ArrayList<>();
int previousOffset = -1;
// After the header, we now have an array of offsets for the strings in this pool.
for (int i = 0; i < count; ++i) {
int stringOffset = offset + buffer.getInt();
result.add(ResourceString.decodeString(buffer, stringOffset, getStringType()));
if (stringOffset <= previousOffset) {
isOriginalDeduped = true;
}
previousOffset = stringOffset;
}
return result;
}
private List readStyles(ByteBuffer buffer, int offset, int count) {
List result = new ArrayList<>();
// After the array of offsets for the strings in the pool, we have an offset for the styles
// in this pool.
for (int i = 0; i < count; ++i) {
int styleOffset = offset + buffer.getInt();
result.add(StringPoolStyle.create(buffer, styleOffset, this));
}
return result;
}
private int writeStrings(DataOutput payload, ByteBuffer offsets, boolean shrink)
throws IOException {
int stringOffset = 0;
Map used = new HashMap<>(); // Keeps track of strings already written
for (String string : strings) {
// Dedupe everything except stylized strings, unless shrink is true (then dedupe everything)
if (used.containsKey(string) && (shrink || isOriginalDeduped)) {
Integer offset = used.get(string);
offsets.putInt(offset == null ? 0 : offset);
} else {
byte[] encodedString = ResourceString.encodeString(string, getStringType());
payload.write(encodedString);
used.put(string, stringOffset);
offsets.putInt(stringOffset);
stringOffset += encodedString.length;
}
}
// ARSC files pad to a 4-byte boundary. We should do so too.
stringOffset = writePad(payload, stringOffset);
return stringOffset;
}
private int writeStyles(DataOutput payload, ByteBuffer offsets, boolean shrink)
throws IOException {
int styleOffset = 0;
if (styles.size() > 0) {
Map used = new HashMap<>(); // Keeps track of bytes already written
for (StringPoolStyle style : styles) {
if (!used.containsKey(style) || !shrink) {
byte[] encodedStyle = style.toByteArray(shrink);
payload.write(encodedStyle);
used.put(style, styleOffset);
offsets.putInt(styleOffset);
styleOffset += encodedStyle.length;
} else { // contains key and shrink is true
Integer offset = used.get(style);
offsets.putInt(offset == null ? 0 : offset);
}
}
// The end of the spans are terminated with another sentinel value
payload.writeInt(StringPoolStyle.RES_STRING_POOL_SPAN_END);
styleOffset += 4;
// TODO(acornwall): There appears to be an extra SPAN_END here... why?
payload.writeInt(StringPoolStyle.RES_STRING_POOL_SPAN_END);
styleOffset += 4;
styleOffset = writePad(payload, styleOffset);
}
return styleOffset;
}
@Override
protected void writeHeader(ByteBuffer output) {
int stringsStart = getHeaderSize() + getOffsetSize();
output.putInt(strings.size());
output.putInt(styles.size());
output.putInt(flags);
output.putInt(strings.isEmpty() ? 0 : stringsStart);
output.putInt(0); // Placeholder. The styles starting offset cannot be computed at this point.
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int stringOffset = 0;
ByteBuffer offsets = ByteBuffer.allocate(getOffsetSize());
offsets.order(ByteOrder.LITTLE_ENDIAN);
// Write to a temporary payload so we can rearrange this and put the offsets first
try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {
stringOffset = writeStrings(payload, offsets, shrink);
writeStyles(payload, offsets, shrink);
}
output.write(offsets.array());
output.write(baos.toByteArray());
if (!styles.isEmpty()) {
header.putInt(STYLE_START_OFFSET, getHeaderSize() + getOffsetSize() + stringOffset);
}
}
/**
* Represents all of the styles for a particular string. The string is determined by its index
* in {@link StringPoolChunk}.
*/
@AutoValue
protected abstract static class StringPoolStyle implements SerializableResource {
// Styles are a list of integers with 0xFFFFFFFF serving as a sentinel value.
static final int RES_STRING_POOL_SPAN_END = 0xFFFFFFFF;
public abstract List spans();
static StringPoolStyle create(ByteBuffer buffer, int offset, StringPoolChunk parent) {
Builder spans = ImmutableList.builder();
int nameIndex = buffer.getInt(offset);
while (nameIndex != RES_STRING_POOL_SPAN_END) {
spans.add(StringPoolSpan.create(buffer, offset, parent));
offset += StringPoolSpan.SPAN_LENGTH;
nameIndex = buffer.getInt(offset);
}
return new AutoValue_StringPoolChunk_StringPoolStyle(spans.build());
}
@Override
public byte[] toByteArray() throws IOException {
return toByteArray(false);
}
@Override
public byte[] toByteArray(boolean shrink) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {
for (StringPoolSpan span : spans()) {
byte[] encodedSpan = span.toByteArray(shrink);
if (encodedSpan.length != StringPoolSpan.SPAN_LENGTH) {
throw new IllegalStateException("Encountered a span of invalid length.");
}
payload.write(encodedSpan);
}
payload.writeInt(RES_STRING_POOL_SPAN_END);
}
return baos.toByteArray();
}
/**
* Returns a brief description of the contents of this style. The representation of this
* information is subject to change, but below is a typical example:
*
* "StringPoolStyle{spans=[StringPoolSpan{foo, start=0, stop=5}, ...]}"
*/
@Override
public String toString() {
return String.format("StringPoolStyle{spans=%s}", spans());
}
}
/** Represents a styled span associated with a specific string. */
@AutoValue
protected abstract static class StringPoolSpan implements SerializableResource {
static final int SPAN_LENGTH = 12;
public abstract int nameIndex();
public abstract int start();
public abstract int stop();
public abstract StringPoolChunk parent();
static StringPoolSpan create(ByteBuffer buffer, int offset, StringPoolChunk parent) {
int nameIndex = buffer.getInt(offset);
int start = buffer.getInt(offset + 4);
int stop = buffer.getInt(offset + 8);
return new AutoValue_StringPoolChunk_StringPoolSpan(nameIndex, start, stop, parent);
}
@Override
public final byte[] toByteArray() {
return toByteArray(false);
}
@Override
public final byte[] toByteArray(boolean shrink) {
ByteBuffer buffer = ByteBuffer.allocate(SPAN_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(nameIndex());
buffer.putInt(start());
buffer.putInt(stop());
return buffer.array();
}
/**
* Returns a brief description of this span. The representation of this information is subject
* to change, but below is a typical example:
*
* "StringPoolSpan{foo, start=0, stop=5}"
*/
@Override
public String toString() {
return String.format("StringPoolSpan{%s, start=%d, stop=%d}",
parent().getString(nameIndex()), start(), stop());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy