org.robolectric.res.android.ResStringPool Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of resources Show documentation
Show all versions of resources Show documentation
An alternative Android testing framework.
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