
org.robolectric.res.android.ResStringPool Maven / Gradle / Ivy
package org.robolectric.res.android;
// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/ResourceTypes.cpp
// and
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/include/androidfw/ResourceTypes.h
import static org.robolectric.res.android.Errors.BAD_TYPE;
import static org.robolectric.res.android.Errors.NAME_NOT_FOUND;
import static org.robolectric.res.android.Errors.NO_ERROR;
import static org.robolectric.res.android.Errors.NO_INIT;
import static org.robolectric.res.android.ResourceString.decodeString;
import static org.robolectric.res.android.ResourceTypes.validate_chunk;
import static org.robolectric.res.android.Util.ALOGI;
import static org.robolectric.res.android.Util.ALOGW;
import static org.robolectric.res.android.Util.SIZEOF_INT;
import static org.robolectric.res.android.Util.isTruthy;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
import org.robolectric.res.android.ResourceTypes.ResChunk_header;
import org.robolectric.res.android.ResourceTypes.ResStringPool_header;
import org.robolectric.res.android.ResourceTypes.ResStringPool_header.Writer;
import org.robolectric.res.android.ResourceTypes.ResStringPool_ref;
import org.robolectric.res.android.ResourceTypes.ResStringPool_span;
import org.robolectric.res.android.ResourceTypes.WithOffset;
/** Convenience class for accessing data in a ResStringPool resource. */
@SuppressWarnings("NewApi")
public class ResStringPool {
private static boolean kDebugStringPoolNoisy = false;
private final long myNativePtr;
private int mError;
byte[] mOwnedData;
// private Object mOwnedData;
private ResStringPool_header mHeader;
private int mSize;
// private mutable Mutex mDecodeLock;
// const uint32_t* mEntries;
private IntArray mEntries;
// const uint32_t* mEntryStyles;
private IntArray mEntryStyles;
// const void* mStrings;
private int mStrings;
// private List mStrings;
// private String[] mCache;
// private char16_t mutable** mCache;
private int mStringPoolSize; // number of uint16_t
// const uint32_t* mStyles;
private int mStyles;
private int mStylePoolSize; // number of int
public ResStringPool() {
mError = NO_INIT;
myNativePtr = Registries.NATIVE_STRING_POOLS.register(new WeakReference<>(this));
}
@Override
protected void finalize() throws Throwable {
Registries.NATIVE_STRING_POOLS.unregister(myNativePtr);
}
public long getNativePtr() {
return myNativePtr;
}
public static ResStringPool getNativeObject(long nativeId) {
return Registries.NATIVE_STRING_POOLS.getNativeObject(nativeId).get();
}
static class IntArray extends WithOffset {
IntArray(ByteBuffer buf, int offset) {
super(buf, offset);
}
int get(int idx) {
return myBuf().getInt(myOffset() + idx * SIZEOF_INT);
}
}
void setToEmpty() {
uninit();
ByteBuffer buf = ByteBuffer.allocate(16 * 1024).order(ByteOrder.LITTLE_ENDIAN);
Writer resStringPoolWriter = new Writer();
resStringPoolWriter.write(buf);
mOwnedData = new byte[buf.position()];
buf.position();
buf.get(mOwnedData);
ResStringPool_header header = new ResStringPool_header(buf, 0);
mSize = 0;
mEntries = null;
mStrings = 0;
mStringPoolSize = 0;
mEntryStyles = null;
mStyles = 0;
mStylePoolSize = 0;
mHeader = header;
}
// status_t setTo(const void* data, size_t size, bool copyData=false);
public int setTo(ByteBuffer buf, int offset, int size, boolean copyData) {
if (!isTruthy(buf) || !isTruthy(size)) {
return (mError = BAD_TYPE);
}
uninit();
// The chunk must be at least the size of the string pool header.
if (size < ResStringPool_header.SIZEOF) {
ALOGW("Bad string block: data size %d is too small to be a string block", size);
return (mError = BAD_TYPE);
}
// The data is at least as big as a ResChunk_header, so we can safely validate the other
// header fields.
// `data + size` is safe because the source of `size` comes from the kernel/filesystem.
if (validate_chunk(
new ResChunk_header(buf, offset),
ResStringPool_header.SIZEOF,
size,
"ResStringPool_header")
!= NO_ERROR) {
ALOGW("Bad string block: malformed block dimensions");
return (mError = BAD_TYPE);
}
// final boolean notDeviceEndian = htods((short) 0xf0) != 0xf0;
//
// if (copyData || notDeviceEndian) {
// mOwnedData = data;
// if (mOwnedData == null) {
// return (mError=NO_MEMORY);
// }
//// memcpy(mOwnedData, data, size);
// data = mOwnedData;
// }
// The size has been checked, so it is safe to read the data in the ResStringPool_header
// data structure.
mHeader = new ResStringPool_header(buf, offset);
// if (notDeviceEndian) {
// ResStringPool_header h = final_cast(mHeader);
// h.header.headerSize = dtohs(mHeader.header.headerSize);
// h.header.type = dtohs(mHeader.header.type);
// h.header.size = dtohl(mHeader.header.size);
// h.stringCount = dtohl(mHeader.stringCount);
// h.styleCount = dtohl(mHeader.styleCount);
// h.flags = dtohl(mHeader.flags);
// h.stringsStart = dtohl(mHeader.stringsStart);
// h.stylesStart = dtohl(mHeader.stylesStart);
// }
if (mHeader.header.headerSize > mHeader.header.size || mHeader.header.size > size) {
ALOGW(
"Bad string block: header size %d or total size %d is larger than data size %d\n",
(int) mHeader.header.headerSize, (int) mHeader.header.size, (int) size);
return (mError = BAD_TYPE);
}
mSize = mHeader.header.size;
mEntries = new IntArray(mHeader.myBuf(), mHeader.myOffset() + mHeader.header.headerSize);
if (mHeader.stringCount > 0) {
if ((mHeader.stringCount * 4 /*sizeof(uint32_t)*/ < mHeader.stringCount) // uint32 overflow?
|| (mHeader.header.headerSize + (mHeader.stringCount * 4 /*sizeof(uint32_t)*/)) > size) {
ALOGW(
"Bad string block: entry of %d items extends past data size %d\n",
(int) (mHeader.header.headerSize + (mHeader.stringCount * 4 /*sizeof(uint32_t)*/)),
(int) size);
return (mError = BAD_TYPE);
}
int charSize;
if (isTruthy(mHeader.flags & ResStringPool_header.UTF8_FLAG)) {
charSize = 1 /*sizeof(uint8_t)*/;
} else {
charSize = 2 /*sizeof(uint16_t)*/;
}
// There should be at least space for the smallest string
// (2 bytes length, null terminator).
if (mHeader.stringsStart >= (mSize - 2 /*sizeof(uint16_t)*/)) {
ALOGW(
"Bad string block: string pool starts at %d, after total size %d\n",
(int) mHeader.stringsStart, (int) mHeader.header.size);
return (mError = BAD_TYPE);
}
mStrings = mHeader.stringsStart;
if (mHeader.styleCount == 0) {
mStringPoolSize = (mSize - mHeader.stringsStart) / charSize;
} else {
// check invariant: styles starts before end of data
if (mHeader.stylesStart >= (mSize - 2 /*sizeof(uint16_t)*/)) {
ALOGW(
"Bad style block: style block starts at %d past data size of %d\n",
(int) mHeader.stylesStart, (int) mHeader.header.size);
return (mError = BAD_TYPE);
}
// check invariant: styles follow the strings
if (mHeader.stylesStart <= mHeader.stringsStart) {
ALOGW(
"Bad style block: style block starts at %d, before strings at %d\n",
(int) mHeader.stylesStart, (int) mHeader.stringsStart);
return (mError = BAD_TYPE);
}
mStringPoolSize = (mHeader.stylesStart - mHeader.stringsStart) / charSize;
}
// check invariant: stringCount > 0 requires a string pool to exist
if (mStringPoolSize == 0) {
ALOGW(
"Bad string block: stringCount is %d but pool size is 0\n", (int) mHeader.stringCount);
return (mError = BAD_TYPE);
}
// if (notDeviceEndian) {
// int i;
// uint32_t* e = final_cast(mEntries);
// for (i=0; i(strings);
// for (i=0; iflags&ResStringPool_header::UTF8_FLAG &&
// ((uint8_t*)mStrings)[mStringPoolSize-1] != 0) ||
// (!(mHeader->flags&ResStringPool_header::UTF8_FLAG) &&
// ((uint16_t*)mStrings)[mStringPoolSize-1] != 0)) {
if ((isTruthy(mHeader.flags & ResStringPool_header.UTF8_FLAG)
&& (mHeader.getByte(mStrings + mStringPoolSize - 1) != 0))
|| (!isTruthy(mHeader.flags & ResStringPool_header.UTF8_FLAG)
&& (mHeader.getShort(mStrings + mStringPoolSize * 2 - 2) != 0))) {
ALOGW("Bad string block: last string is not 0-terminated\n");
return (mError = BAD_TYPE);
}
} else {
mStrings = -1;
mStringPoolSize = 0;
}
if (mHeader.styleCount > 0) {
mEntryStyles =
new IntArray(mEntries.myBuf(), mEntries.myOffset() + mHeader.stringCount * SIZEOF_INT);
// invariant: integer overflow in calculating mEntryStyles
if (mEntryStyles.myOffset() < mEntries.myOffset()) {
ALOGW("Bad string block: integer overflow finding styles\n");
return (mError = BAD_TYPE);
}
// if (((const uint8_t*)mEntryStyles-(const uint8_t*)mHeader) > (int)size) {
if ((mEntryStyles.myOffset() - mHeader.myOffset()) > (int) size) {
ALOGW(
"Bad string block: entry of %d styles extends past data size %d\n",
(int) mEntryStyles.myOffset(), (int) size);
return (mError = BAD_TYPE);
}
mStyles = mHeader.stylesStart;
if (mHeader.stylesStart >= mHeader.header.size) {
ALOGW(
"Bad string block: style pool starts %d, after total size %d\n",
(int) mHeader.stylesStart, (int) mHeader.header.size);
return (mError = BAD_TYPE);
}
mStylePoolSize = (mHeader.header.size - mHeader.stylesStart) /* / sizeof(uint32_t)*/;
// if (notDeviceEndian) {
// size_t i;
// uint32_t* e = final_cast(mEntryStyles);
// for (i=0; i(mStyles);
// for (i=0; i();
// Collections.addAll(mStrings, xmlStringPool.strings());
// }
private int setError(int error) {
mError = error;
return mError;
}
void uninit() {
setError(NO_INIT);
mHeader = null;
}
public String stringAt(int idx) {
if (mError == NO_ERROR && idx < mHeader.stringCount) {
final boolean isUTF8 = (mHeader.flags & ResStringPool_header.UTF8_FLAG) != 0;
// const uint32_t off = mEntries[idx]/(isUTF8?sizeof(uint8_t):sizeof(uint16_t));
ByteBuffer buf = mHeader.myBuf();
int bufOffset = mHeader.myOffset();
// const uint32_t off = mEntries[idx]/(isUTF8?sizeof(uint8_t):sizeof(uint16_t));
final int off = mEntries.get(idx) / (isUTF8 ? 1 /*sizeof(uint8_t)*/ : 2 /*sizeof(uint16_t)*/);
if (off < (mStringPoolSize - 1)) {
if (!isUTF8) {
final int strings = mStrings;
final int str = strings + off * 2;
return decodeString(buf, bufOffset + str, ResourceString.Type.UTF16);
// int u16len = decodeLengthUTF16(buf, bufOffset + str);
// if ((str+u16len*2-strings) < mStringPoolSize) {
// // Reject malformed (non null-terminated) strings
// if (buf.getShort(bufOffset + str + u16len*2) != 0x0000) {
// ALOGW("Bad string block: string #%d is not null-terminated",
// (int)idx);
// return null;
// }
// byte[] bytes = new byte[u16len * 2];
// buf.position(bufOffset + str);
// buf.get(bytes);
// // Reject malformed (non null-terminated) strings
// if (str[encLen] != 0x00) {
// ALOGW("Bad string block: string #%d is not null-terminated",
// (int)idx);
// return NULL;
// }
// return new String(bytes, StandardCharsets.UTF_16);
// } else {
// ALOGW("Bad string block: string #%d extends to %d, past end at %d\n",
// (int)idx, (int)(str+u16len-strings), (int)mStringPoolSize);
// }
} else {
final int strings = mStrings;
final int u8str = strings + off;
return decodeString(buf, bufOffset + u8str, ResourceString.Type.UTF8);
// *u16len = decodeLength(&u8str);
// size_t u8len = decodeLength(&u8str);
//
// // encLen must be less than 0x7FFF due to encoding.
// if ((uint32_t)(u8str+u8len-strings) < mStringPoolSize) {
// AutoMutex lock(mDecodeLock);
//
// if (mCache != NULL && mCache[idx] != NULL) {
// return mCache[idx];
// }
//
// // Retrieve the actual length of the utf8 string if the
// // encoded length was truncated
// if (stringDecodeAt(idx, u8str, u8len, &u8len) == NULL) {
// return NULL;
// }
//
// // Since AAPT truncated lengths longer than 0x7FFF, check
// // that the bits that remain after truncation at least match
// // the bits of the actual length
// ssize_t actualLen = utf8_to_utf16_length(u8str, u8len);
// if (actualLen < 0 || ((size_t)actualLen & 0x7FFF) != *u16len) {
// ALOGW("Bad string block: string #%lld decoded length is not correct "
// "%lld vs %llu\n",
// (long long)idx, (long long)actualLen, (long long)*u16len);
// return NULL;
// }
//
// utf8_to_utf16(u8str, u8len, u16str, *u16len + 1);
//
// if (mCache == NULL) {
// #ifndef __ANDROID__
// if (kDebugStringPoolNoisy) {
// ALOGI("CREATING STRING CACHE OF %zu bytes",
// mHeader->stringCount*sizeof(char16_t**));
// }
// #else
// // We do not want to be in this case when actually running Android.
// ALOGW("CREATING STRING CACHE OF %zu bytes",
// static_cast(mHeader->stringCount*sizeof(char16_t**)));
// #endif
// mCache = (char16_t**)calloc(mHeader->stringCount, sizeof(char16_t*));
// if (mCache == NULL) {
// ALOGW("No memory trying to allocate decode cache table of %d
// bytes\n",
// (int)(mHeader->stringCount*sizeof(char16_t**)));
// return NULL;
// }
// }
// *u16len = (size_t) actualLen;
// char16_t *u16str = (char16_t *)calloc(*u16len+1, sizeof(char16_t));
// if (!u16str) {
// ALOGW("No memory when trying to allocate decode cache for string #%d\n",
// (int)idx);
// return NULL;
// }
//
// if (kDebugStringPoolNoisy) {
// ALOGI("Caching UTF8 string: %s", u8str);
// }
//
// mCache[idx] = u16str;
// return u16str;
// } else {
// ALOGW("Bad string block: string #%lld extends to %lld, past end at %lld\n",
// (long long)idx, (long long)(u8str+u8len-strings),
// (long long)mStringPoolSize);
// }
}
} else {
ALOGW(
"Bad string block: string #%d entry is at %d, past end at %d\n",
(int) idx,
(int) (off * 2 /*sizeof(uint16_t)*/),
(int) (mStringPoolSize * 2 /*sizeof(uint16_t)*/));
}
}
return null;
}
String stringAt(int idx, Ref outLen) {
String s = stringAt(idx);
if (s != null && outLen != null) {
outLen.set(s.length());
}
return s;
}
public String string8At(int id, Ref outLen) {
return stringAt(id, outLen);
}
final ResStringPool_span styleAt(final ResStringPool_ref ref) {
return styleAt(ref.index);
}
public final ResStringPool_span styleAt(int idx) {
if (mError == NO_ERROR && idx < mHeader.styleCount) {
// const uint32_t off = (mEntryStyles[idx]/sizeof(uint32_t));
final int off = mEntryStyles.get(idx) / SIZEOF_INT;
if (off < mStylePoolSize) {
// return (const ResStringPool_span*)(mStyles+off);
return new ResStringPool_span(
mHeader.myBuf(), mHeader.myOffset() + mStyles + off * SIZEOF_INT);
} else {
ALOGW(
"Bad string block: style #%d entry is at %d, past end at %d\n",
(int) idx, (int) (off * SIZEOF_INT), (int) (mStylePoolSize * SIZEOF_INT));
}
}
return null;
}
public int indexOfString(String str) {
if (mError != NO_ERROR) {
return mError;
}
if (kDebugStringPoolNoisy) {
ALOGI("indexOfString : %s", str);
}
if ((mHeader.flags & ResStringPoolHeader.SORTED_FLAG) != 0) {
// Do a binary search for the string... this is a little tricky,
// because the strings are sorted with strzcmp16(). So to match
// the ordering, we need to convert strings in the pool to UTF-16.
// But we don't want to hit the cache, so instead we will have a
// local temporary allocation for the conversions.
int l = 0;
int h = mHeader.stringCount - 1;
int mid;
while (l <= h) {
mid = l + (h - l) / 2;
String s = stringAt(mid);
int c = s != null ? s.compareTo(str) : -1;
if (kDebugStringPoolNoisy) {
ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n", s, c, (int) l, (int) mid, (int) h);
}
if (c == 0) {
if (kDebugStringPoolNoisy) {
ALOGI("MATCH!");
}
return mid;
} else if (c < 0) {
l = mid + 1;
} else {
h = mid - 1;
}
}
} else {
// It is unusual to get the ID from an unsorted string block...
// most often this happens because we want to get IDs for style
// span tags; since those always appear at the end of the string
// block, start searching at the back.
for (int i = mHeader.stringCount; i >= 0; i--) {
String s = stringAt(i);
if (kDebugStringPoolNoisy) {
ALOGI("Looking at %s, i=%d\n", s, i);
}
if (Objects.equals(s, str)) {
if (kDebugStringPoolNoisy) {
ALOGI("MATCH!");
}
return i;
}
}
}
return NAME_NOT_FOUND;
}
//
public int size() {
return mError == NO_ERROR ? mHeader.stringCount : 0;
}
int styleCount() {
return mError == NO_ERROR ? mHeader.styleCount : 0;
}
int bytes() {
return mError == NO_ERROR ? mHeader.header.size : 0;
}
public boolean isUTF8() {
return true;
}
public int getError() {
return mError;
}
// int styleCount() final;
// int bytes() final;
//
// boolean isSorted() final;
// boolean isUTF8() final;
//
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy