Skip to content
82 changes: 50 additions & 32 deletions pyaxmlparser/arscparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,31 @@ def __init__(self, raw_buff):

entries = []
for i in range(0, a_res_type.entryCount):
current_package.mResId = current_package.mResId & 0xffff0000 | i
entries.append((unpack('<i', self.buff.read(4))[0], current_package.mResId))
if a_res_type.is_sparse():
idx = unpack('<H', self.buff.read(2))[0]
offset = unpack('<H', self.buff.read(2))[0]
offset *= 4
elif a_res_type.is_offset16():
idx = i
offset = unpack('<H', self.buff.read(2))[0]
offset = ARSCResType.NO_ENTRY if offset == 0xFFFF else offset * 4
else:
idx = i
offset = unpack('<I', self.buff.read(4))[0]

current_package.mResId = current_package.mResId & 0xffff0000 | idx
entries.append((offset, current_package.mResId))

self.packages[package_name].append(entries)

for entry, res_id in entries:
for offset, res_id in entries:
if offset != ARSCResType.NO_ENTRY:
self.buff.set_idx(pkg_chunk_header.start + a_res_type.entriesStart + offset)

if self.buff.end():
break

if entry != -1:
if offset != ARSCResType.NO_ENTRY:
ate = ARSCResTableEntry(self.buff, res_id, pc)
self.packages[package_name].append(ate)
if ate.is_weak():
Expand Down Expand Up @@ -181,13 +196,13 @@ def _analyse(self):
entries = self.packages[package_name][nb + 2]
nb_i = 0
for entry, res_id in entries:
if entry != -1:
if entry != ARSCResType.NO_ENTRY:
ate = self.packages[package_name][nb + 3 + nb_i]

self.resource_values[ate.mResId][a_res_type.config] = ate
self.resource_keys[package_name][a_res_type.get_type()][ate.get_value()] = ate.mResId

if ate.get_index() != -1:
if ate.get_key() != ARSCResType.NO_ENTRY:
c_value["public"].append(
(a_res_type.get_type(), ate.get_value(),
ate.mResId))
Expand Down Expand Up @@ -226,29 +241,29 @@ def _analyse(self):
nb += 1

def get_resource_string(self, ate):
return [ate.get_value(), ate.get_key_data()]
return [ate.get_value(), ate.get_value_data()]

def get_resource_id(self, ate):
x = [ate.get_value()]
if ate.key.get_data() == 0:
if ate.value.get_data() == 0:
x.append("false")
elif ate.key.get_data() == 1:
elif ate.value.get_data() == 1:
x.append("true")
return x

def get_resource_bool(self, ate):
x = [ate.get_value()]
if ate.key.get_data() == 0:
if ate.value.get_data() == 0:
x.append("false")
elif ate.key.get_data() == -1:
elif ate.value.get_data() == 0xFFFFFFFF:
x.append("true")
return x

def get_resource_integer(self, ate):
return [ate.get_value(), ate.key.get_data()]
return [ate.get_value(), ate.value.get_data()]

def get_resource_color(self, ate):
entry_data = ate.key.get_data()
entry_data = ate.value.get_data()
return [
ate.get_value(),
"#%02x%02x%02x%02x" % (
Expand All @@ -262,14 +277,14 @@ def get_resource_dimen(self, ate):
try:
return [
ate.get_value(), "%s%s" % (
complexToFloat(ate.key.get_data()),
const.DIMENSION_UNITS[ate.key.get_data() & const.COMPLEX_UNIT_MASK])
complexToFloat(ate.value.get_data()),
const.DIMENSION_UNITS[ate.value.get_data() & const.COMPLEX_UNIT_MASK])
]
except IndexError:
log.debug("Out of range dimension unit index for %s: %s" % (
complexToFloat(ate.key.get_data()),
ate.key.get_data() & const.COMPLEX_UNIT_MASK))
return [ate.get_value(), ate.key.get_data()]
complexToFloat(ate.value.get_data()),
ate.value.get_data() & const.COMPLEX_UNIT_MASK))
return [ate.get_value(), ate.value.get_data()]

# FIXME
def get_resource_style(self, ate):
Expand Down Expand Up @@ -553,28 +568,31 @@ def resolve(self, res_id):
return result

def _resolve_into_result(self, result, res_id, config):
configs = self.resources.get_res_configs(res_id, config)
if configs:
for config, ate in configs:
self.put_ate_value(result, ate, config)

def put_ate_value(self, result, ate, config):
queue = [(res_id, config)]
index = 0

while index < len(queue):
current_res_id, current_config = queue[index]
index += 1
configs = self.resources.get_res_configs(current_res_id, current_config)
if configs:
for config, ate in configs:
self.put_ate_value(result, ate, config, queue)

def put_ate_value(self, result, ate, config, queue):
if ate.is_complex():
complex_array = []
result.append((config, complex_array))
for _, item in ate.item.items:
self.put_item_value(complex_array, item, config, complex_=True)
self.put_item_value(complex_array, item, config, queue, complex_=True)
else:
self.put_item_value(result, ate.key, config, complex_=False)
self.put_item_value(result, ate.value, config, queue, complex_=False)

def put_item_value(self, result, item, config, complex_):
def put_item_value(self, result, item, config, queue, complex_):
if item.is_reference():
res_id = item.get_data()
if res_id:
self._resolve_into_result(
result,
item.get_data(),
self.wanted_config)
if res_id and (res_id, self.wanted_config) not in queue:
queue.append((res_id, self.wanted_config))
else:
if complex_:
result.append(item.format_value())
Expand Down
93 changes: 58 additions & 35 deletions pyaxmlparser/arscutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,27 +125,26 @@ def __init__(self, buff, parent=None):
self.parent = parent
self.id = unpack('<B', buff.read(1))[0]
self.res0 = unpack('<B', buff.read(1))[0]
self.res1 = unpack('<H', buff.read(2))[0]
res_error = False
if self.res0 != 0:
log.warning("res0 is not zero!")
res_error = True
self.res1 = unpack('<H', buff.read(2))[0]
if self.res1 != 0:
log.warning("res1 is not zero!")
res_error = True

if not res_error: # Skips processing attempt if there was an error
self.entryCount = unpack("<I", buff.read(4))[0]
self.entryCount = unpack("<I", buff.read(4))[0]

self.typespec_entries = []
for i in range(0, self.entryCount):
self.typespec_entries.append(unpack("<I", buff.read(4))[0])
self.typespec_entries = []
for i in range(0, self.entryCount):
self.typespec_entries.append(unpack("<I", buff.read(4))[0])


class ARSCResType(object):
"""
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1364
"""
FLAG_SPARSE = 1
FLAG_OFFSET16 = 2
NO_ENTRY = 0xFFFFFFFF

def __init__(self, buff, parent=None):
self.start = buff.get_idx()
self.parent = parent
Expand All @@ -168,6 +167,12 @@ def get_type(self):
def get_package_name(self):
return self.parent.get_package_name()

def is_sparse(self):
return bool(self.flags & self.FLAG_SPARSE)

def is_offset16(self):
return bool(self.flags & self.FLAG_OFFSET16)

def __repr__(self):
return "ARSCResType(%x, %x, %x, %x, %x, %x, %x, %s)" % (
self.start,
Expand Down Expand Up @@ -521,29 +526,35 @@ class ARSCResTableEntry(object):
FLAG_COMPLEX = 1
FLAG_PUBLIC = 2
FLAG_WEAK = 4
FLAG_COMPACT = 8

def __init__(self, buff, mResId, parent=None):
self.start = buff.get_idx()
self.mResId = mResId
self.parent = parent
self.size = unpack('<H', buff.read(2))[0]
self.flags = unpack('<H', buff.read(2))[0]
self.index = unpack('<I', buff.read(4))[0]

if self.is_complex():
self.key = unpack('<I', buff.read(4))[0]

if self.is_compact():
self.compact_key = unpack('<H', buff.read(2))[0]
self.compact_flags = unpack('<H', buff.read(2))[0]
self.compact_data = unpack('<I', buff.read(4))[0]
self.value = ARSCResValue(data=self.compact_data, data_type=self.compact_flags >> 8, parent=parent)
elif self.is_complex():
self.item = ARSCComplex(buff, parent)
else:
# If FLAG_COMPLEX is not set, a Res_value structure will follow
self.key = ARSCResStringPoolRef(buff, self.parent)
self.value = ARSCResValue.fetch(buff, parent)

def get_index(self):
return self.index
def get_key(self):
return self.compact_key if self.is_compact() else self.key

def get_value(self):
return self.parent.mKeyStrings.getString(self.index)
return self.parent.mKeyStrings.getString(self.key)

def get_key_data(self):
return self.key.get_data_value()
def get_value_data(self):
return self.value.get_data_value()

def is_public(self):
return (self.flags & self.FLAG_PUBLIC) != 0
Expand All @@ -554,16 +565,19 @@ def is_complex(self):
def is_weak(self):
return (self.flags & self.FLAG_WEAK) != 0

def is_compact(self):
return (self.flags & self.FLAG_COMPACT) != 0

def __repr__(self):
return (
"<ARSCResTableEntry idx='0x{:08x}' mResId='0x{:08x}' size='{}' "
"flags='0x{:02x}' index='0x{:x}' holding={}>"
"flags='0x{:02x}' key='0x{:x}' holding={}>"
).format(
self.start,
self.mResId,
self.size,
self.flags,
self.index,
self.key,
self.item if self.is_complex() else self.key)


Expand All @@ -577,25 +591,35 @@ def __init__(self, buff, parent=None):

self.items = []
for i in range(0, self.count):
self.items.append((unpack('<I', buff.read(4))[0],
ARSCResStringPoolRef(buff, self.parent)))
name = unpack('<I', buff.read(4))[0]
value = ARSCResValue.fetch(buff, parent)
self.items.append((name, value))

def __repr__(self):
return "<ARSCComplex idx='0x{:08x}' parent='{}' count='{}'>".format(self.start, self.id_parent, self.count)


class ARSCResStringPoolRef(object):
def __init__(self, buff, parent=None):
self.start = buff.get_idx()
class ARSCResValue:
def __init__(self, data, data_type, res0=0, size=8, parent=None):
self.size = size
self.res0 = res0
self.data = data
self.data_type = data_type
self.parent = parent

self.size, = unpack("<H", buff.read(2))
self.res0, = unpack("<B", buff.read(1))
if self.res0 != 0:
@classmethod
def fetch(cls, buff, parent=None):
start = buff.get_idx()

size, = unpack("<H", buff.read(2))
res0, = unpack("<B", buff.read(1))
if res0 != 0:
log.warning("res0 is not zero!")
else:
self.data_type = unpack('<B', buff.read(1))[0]
self.data = unpack('<I', buff.read(4))[0]

data_type = unpack('<B', buff.read(1))[0]
data = unpack('<I', buff.read(4))[0]

buff.set_idx(start + size)
return ARSCResValue(size=size, res0=res0, data=data, data_type=data_type, parent=parent)

def get_data_value(self):
return self.parent.stringpool_main.getString(self.data)
Expand All @@ -620,8 +644,7 @@ def is_reference(self):
return self.data_type == const.TYPE_REFERENCE

def __repr__(self):
return "<ARSCResStringPoolRef idx='0x{:08x}' size='{}' type='{}' data='0x{:08x}'>".format(
self.start,
return "<ARSCResValue size='{}' type='{}' data='0x{:08x}'>".format(
self.size,
const.TYPE_TABLE.get(self.data_type, "0x%x" % self.data_type),
self.data)
Expand Down
12 changes: 10 additions & 2 deletions pyaxmlparser/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def __init__(self, filename, raw=False, skip_analysis=False, testzip=False):
self.androidversion = {}
self.permissions = []
self.uses_permissions = []
self.uses_permission_sdk_23 = []
self.declared_permissions = {}
self.valid_apk = False

Expand Down Expand Up @@ -358,6 +359,7 @@ def _apk_analysis(self):
self.androidversion["Code"] = self.get_attribute_value("manifest", "versionCode")
self.androidversion["Name"] = self.get_attribute_value("manifest", "versionName")
permission = list(self.get_all_attribute_value("uses-permission", "name"))
permission += list(self.get_all_attribute_value("uses-permission-sdk-23", "name"))
self.permissions = list(set(self.permissions + permission))

for uses_permission in self.find_tags("uses-permission"):
Expand All @@ -366,6 +368,12 @@ def _apk_analysis(self):
self._get_permission_maxsdk(uses_permission)
])

for uses_permission in self.find_tags("uses-permission-sdk-23"):
self.uses_permission_sdk_23.append([
self.get_value_from_tag(uses_permission, "name"),
self._get_permission_maxsdk(uses_permission)
])

# getting details of the declared permissions
for d_perm_item in self.find_tags('permission'):
d_perm_name = self._get_res_string_value(
Expand Down Expand Up @@ -439,7 +447,7 @@ def _get_permission_maxsdk(self, item):
try:
maxSdkVersion = int(self.get_value_from_tag(item, "maxSdkVersion"))
except ValueError:
log.warning(self.get_max_sdk_version() + 'is not a valid value for <uses-permission> maxSdkVersion')
log.warning(f"{self.get_max_sdk_version()} is not a valid value for <{item.tag}> maxSdkVersion")
except TypeError:
pass
return maxSdkVersion
Expand Down Expand Up @@ -1206,7 +1214,7 @@ def get_uses_implied_permission_list(self):
if (WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE) \
and READ_EXTERNAL_STORAGE not in self.permissions:
maxSdkVersion = None
for name, version in self.uses_permissions:
for name, version in self.uses_permissions + self.uses_permission_sdk_23:
if name == WRITE_EXTERNAL_STORAGE:
maxSdkVersion = version
break
Expand Down
2 changes: 1 addition & 1 deletion pyaxmlparser/stringblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def getString(self, idx):
if idx in self._cache:
return self._cache[idx]

if idx < 0 or not self.m_stringOffsets or idx > self.stringCount:
if idx < 0 or not self.m_stringOffsets or idx >= self.stringCount:
return ""

offset = self.m_stringOffsets[idx]
Expand Down