486 lines
11 KiB
JavaScript
486 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
/**
|
|
* Code translated from a C# project https://github.com/hylander0/Iteedee.ApkReader/blob/master/Iteedee.ApkReader/ApkResourceFinder.cs
|
|
*
|
|
* Decode binary file `resources.arsc` from a .apk file to a JavaScript Object.
|
|
*/
|
|
|
|
var ByteBuffer = require("bytebuffer");
|
|
|
|
var DEBUG = false;
|
|
|
|
var RES_STRING_POOL_TYPE = 0x0001;
|
|
var RES_TABLE_TYPE = 0x0002;
|
|
var RES_TABLE_PACKAGE_TYPE = 0x0200;
|
|
var RES_TABLE_TYPE_TYPE = 0x0201;
|
|
var RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
|
|
|
|
// The 'data' holds a ResTable_ref, a reference to another resource
|
|
// table entry.
|
|
var TYPE_REFERENCE = 0x01;
|
|
// The 'data' holds an index into the containing resource table's
|
|
// global value string pool.
|
|
var TYPE_STRING = 0x03;
|
|
|
|
function ResourceFinder() {
|
|
this.valueStringPool = null;
|
|
this.typeStringPool = null;
|
|
this.keyStringPool = null;
|
|
|
|
this.package_id = 0;
|
|
|
|
this.responseMap = {};
|
|
this.entryMap = {};
|
|
}
|
|
|
|
/**
|
|
* Same to C# BinaryReader.readBytes
|
|
*
|
|
* @param bb ByteBuffer
|
|
* @param len length
|
|
* @returns {Buffer}
|
|
*/
|
|
ResourceFinder.readBytes = function (bb, len) {
|
|
var uint8Array = new Uint8Array(len);
|
|
for (var i = 0; i < len; i++) {
|
|
uint8Array[i] = bb.readUint8();
|
|
}
|
|
|
|
return ByteBuffer.wrap(uint8Array, "binary", true);
|
|
};
|
|
|
|
//
|
|
/**
|
|
*
|
|
* @param {ByteBuffer} bb
|
|
* @return {Map<String, Set<String>>}
|
|
*/
|
|
ResourceFinder.prototype.processResourceTable = function (resourceBuffer) {
|
|
const bb = ByteBuffer.wrap(resourceBuffer, "binary", true);
|
|
|
|
// Resource table structure
|
|
var type = bb.readShort(),
|
|
headerSize = bb.readShort(),
|
|
size = bb.readInt(),
|
|
packageCount = bb.readInt(),
|
|
buffer,
|
|
bb2;
|
|
if (type != RES_TABLE_TYPE) {
|
|
throw new Error("No RES_TABLE_TYPE found!");
|
|
}
|
|
if (size != bb.limit) {
|
|
throw new Error("The buffer size not matches to the resource table size.");
|
|
}
|
|
bb.offset = headerSize;
|
|
|
|
var realStringPoolCount = 0,
|
|
realPackageCount = 0;
|
|
|
|
while (true) {
|
|
var pos, t, hs, s;
|
|
try {
|
|
pos = bb.offset;
|
|
t = bb.readShort();
|
|
hs = bb.readShort();
|
|
s = bb.readInt();
|
|
} catch (e) {
|
|
break;
|
|
}
|
|
if (t == RES_STRING_POOL_TYPE) {
|
|
// Process the string pool
|
|
if (realStringPoolCount == 0) {
|
|
// Only the first string pool is processed.
|
|
if (DEBUG) {
|
|
console.log("Processing the string pool ...");
|
|
}
|
|
|
|
buffer = new ByteBuffer(s);
|
|
bb.offset = pos;
|
|
bb.prependTo(buffer);
|
|
|
|
bb2 = ByteBuffer.wrap(buffer, "binary", true);
|
|
|
|
bb2.LE();
|
|
this.valueStringPool = this.processStringPool(bb2);
|
|
}
|
|
realStringPoolCount++;
|
|
} else if (t == RES_TABLE_PACKAGE_TYPE) {
|
|
// Process the package
|
|
if (DEBUG) {
|
|
console.log("Processing the package " + realPackageCount + " ...");
|
|
}
|
|
|
|
buffer = new ByteBuffer(s);
|
|
bb.offset = pos;
|
|
bb.prependTo(buffer);
|
|
|
|
bb2 = ByteBuffer.wrap(buffer, "binary", true);
|
|
bb2.LE();
|
|
this.processPackage(bb2);
|
|
|
|
realPackageCount++;
|
|
} else {
|
|
throw new Error("Unsupported type");
|
|
}
|
|
bb.offset = pos + s;
|
|
if (!bb.remaining()) break;
|
|
}
|
|
|
|
if (realStringPoolCount != 1) {
|
|
throw new Error("More than 1 string pool found!");
|
|
}
|
|
if (realPackageCount != packageCount) {
|
|
throw new Error("Real package count not equals the declared count.");
|
|
}
|
|
|
|
return this.responseMap;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {ByteBuffer} bb
|
|
*/
|
|
ResourceFinder.prototype.processPackage = function (bb) {
|
|
// Package structure
|
|
var type = bb.readShort(),
|
|
headerSize = bb.readShort(),
|
|
size = bb.readInt(),
|
|
id = bb.readInt();
|
|
|
|
this.package_id = id;
|
|
|
|
for (var i = 0; i < 256; ++i) {
|
|
bb.readUint8();
|
|
}
|
|
|
|
var typeStrings = bb.readInt(),
|
|
lastPublicType = bb.readInt(),
|
|
keyStrings = bb.readInt(),
|
|
lastPublicKey = bb.readInt();
|
|
|
|
if (typeStrings != headerSize) {
|
|
throw new Error("TypeStrings must immediately following the package structure header.");
|
|
}
|
|
|
|
if (DEBUG) {
|
|
console.log("Type strings:");
|
|
}
|
|
|
|
var lastPosition = bb.offset;
|
|
bb.offset = typeStrings;
|
|
var bbTypeStrings = ResourceFinder.readBytes(bb, bb.limit - bb.offset);
|
|
bb.offset = lastPosition;
|
|
this.typeStringPool = this.processStringPool(bbTypeStrings);
|
|
|
|
// Key strings
|
|
if (DEBUG) {
|
|
console.log("Key strings:");
|
|
}
|
|
|
|
bb.offset = keyStrings;
|
|
var key_type = bb.readShort(),
|
|
key_headerSize = bb.readShort(),
|
|
key_size = bb.readInt();
|
|
|
|
lastPosition = bb.offset;
|
|
bb.offset = keyStrings;
|
|
var bbKeyStrings = ResourceFinder.readBytes(bb, bb.limit - bb.offset);
|
|
bb.offset = lastPosition;
|
|
this.keyStringPool = this.processStringPool(bbKeyStrings);
|
|
|
|
// Iterate through all chunks
|
|
var typeSpecCount = 0;
|
|
var typeCount = 0;
|
|
|
|
bb.offset = keyStrings + key_size;
|
|
|
|
var bb2;
|
|
|
|
while (true) {
|
|
var pos = bb.offset;
|
|
try {
|
|
var t = bb.readShort();
|
|
var hs = bb.readShort();
|
|
var s = bb.readInt();
|
|
} catch (e) {
|
|
break;
|
|
}
|
|
|
|
if (t == RES_TABLE_TYPE_SPEC_TYPE) {
|
|
bb.offset = pos;
|
|
bb2 = ResourceFinder.readBytes(bb, s);
|
|
this.processTypeSpec(bb2);
|
|
|
|
typeSpecCount++;
|
|
} else if (t == RES_TABLE_TYPE_TYPE) {
|
|
bb.offset = pos;
|
|
bb2 = ResourceFinder.readBytes(bb, s);
|
|
this.processType(bb2);
|
|
|
|
typeCount++;
|
|
}
|
|
|
|
if (s == 0) {
|
|
break;
|
|
}
|
|
|
|
bb.offset = pos + s;
|
|
|
|
if (!bb.remaining()) {
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {ByteBuffer} bb
|
|
*/
|
|
ResourceFinder.prototype.processType = function (bb) {
|
|
var type = bb.readShort(),
|
|
headerSize = bb.readShort(),
|
|
size = bb.readInt(),
|
|
id = bb.readByte(),
|
|
res0 = bb.readByte(),
|
|
res1 = bb.readShort(),
|
|
entryCount = bb.readInt(),
|
|
entriesStart = bb.readInt();
|
|
|
|
var refKeys = {};
|
|
|
|
var config_size = bb.readInt();
|
|
|
|
// Skip the config data
|
|
bb.offset = headerSize;
|
|
|
|
if (headerSize + entryCount * 4 != entriesStart) {
|
|
throw new Error("HeaderSize, entryCount and entriesStart are not valid.");
|
|
}
|
|
|
|
// Start to get entry indices
|
|
var entryIndices = new Array(entryCount);
|
|
for (var i = 0; i < entryCount; ++i) {
|
|
entryIndices[i] = bb.readInt();
|
|
}
|
|
|
|
// Get entries
|
|
for (var i = 0; i < entryCount; ++i) {
|
|
if (entryIndices[i] == -1) continue;
|
|
|
|
var resource_id = this.package_id << 24 | id << 16 | i;
|
|
|
|
var pos = bb.offset,
|
|
entry_size,
|
|
entry_flag,
|
|
entry_key,
|
|
value_size,
|
|
value_res0,
|
|
value_dataType,
|
|
value_data;
|
|
try {
|
|
entry_size = bb.readShort();
|
|
entry_flag = bb.readShort();
|
|
entry_key = bb.readInt();
|
|
} catch (e) {
|
|
break;
|
|
}
|
|
|
|
// Get the value (simple) or map (complex)
|
|
|
|
var FLAG_COMPLEX = 0x0001;
|
|
if ((entry_flag & FLAG_COMPLEX) == 0) {
|
|
// Simple case
|
|
value_size = bb.readShort();
|
|
value_res0 = bb.readByte();
|
|
value_dataType = bb.readByte();
|
|
value_data = bb.readInt();
|
|
|
|
var idStr = Number(resource_id).toString(16);
|
|
var keyStr = this.keyStringPool[entry_key];
|
|
|
|
var data = null;
|
|
|
|
if (DEBUG) {
|
|
console.log("Entry 0x" + idStr + ", key: " + keyStr + ", simple value type: ");
|
|
}
|
|
|
|
var key = parseInt(idStr, 16);
|
|
|
|
var entryArr = this.entryMap[key];
|
|
if (entryArr == null) {
|
|
entryArr = [];
|
|
}
|
|
entryArr.push(keyStr);
|
|
|
|
this.entryMap[key] = entryArr;
|
|
|
|
if (value_dataType == TYPE_STRING) {
|
|
data = this.valueStringPool[value_data];
|
|
|
|
if (DEBUG) {
|
|
console.log(", data: " + this.valueStringPool[value_data] + "");
|
|
}
|
|
} else if (value_dataType == TYPE_REFERENCE) {
|
|
var hexIndex = Number(value_data).toString(16);
|
|
|
|
refKeys[idStr] = value_data;
|
|
} else {
|
|
data = "" + value_data;
|
|
if (DEBUG) {
|
|
console.log(", data: " + value_data + "");
|
|
}
|
|
}
|
|
|
|
this.putIntoMap("@" + idStr, data);
|
|
} else {
|
|
// Complex case
|
|
var entry_parent = bb.readInt();
|
|
var entry_count = bb.readInt();
|
|
|
|
for (var j = 0; j < entry_count; ++j) {
|
|
var ref_name = bb.readInt();
|
|
value_size = bb.readShort();
|
|
value_res0 = bb.readByte();
|
|
value_dataType = bb.readByte();
|
|
value_data = bb.readInt();
|
|
}
|
|
|
|
if (DEBUG) {
|
|
console.log("Entry 0x" + Number(resource_id).toString(16) + ", key: " + this.keyStringPool[entry_key] + ", complex value, not printed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var refK in refKeys) {
|
|
var values = this.responseMap["@" + Number(refKeys[refK]).toString(16).toUpperCase()];
|
|
if (values != null && Object.keys(values).length < 1000) {
|
|
for (var value in values) {
|
|
this.putIntoMap("@" + refK, values[value]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {ByteBuffer} bb
|
|
* @return {Array}
|
|
*/
|
|
ResourceFinder.prototype.processStringPool = function (bb) {
|
|
// String pool structure
|
|
//
|
|
var type = bb.readShort(),
|
|
headerSize = bb.readShort(),
|
|
size = bb.readInt(),
|
|
stringCount = bb.readInt(),
|
|
styleCount = bb.readInt(),
|
|
flags = bb.readInt(),
|
|
stringsStart = bb.readInt(),
|
|
stylesStart = bb.readInt(),
|
|
u16len,
|
|
buffer;
|
|
|
|
var isUTF_8 = (flags & 256) != 0;
|
|
|
|
var offsets = new Array(stringCount);
|
|
for (var i = 0; i < stringCount; ++i) {
|
|
offsets[i] = bb.readInt();
|
|
}
|
|
|
|
var strings = new Array(stringCount);
|
|
|
|
for (var i = 0; i < stringCount; ++i) {
|
|
var pos = stringsStart + offsets[i];
|
|
bb.offset = pos;
|
|
|
|
strings[i] = "";
|
|
|
|
if (isUTF_8) {
|
|
u16len = bb.readUint8();
|
|
|
|
if ((u16len & 0x80) != 0) {
|
|
u16len = ((u16len & 0x7f) << 8) + bb.readUint8();
|
|
}
|
|
|
|
var u8len = bb.readUint8();
|
|
if ((u8len & 0x80) != 0) {
|
|
u8len = ((u8len & 0x7f) << 8) + bb.readUint8();
|
|
}
|
|
|
|
if (u8len > 0) {
|
|
buffer = ResourceFinder.readBytes(bb, u8len);
|
|
try {
|
|
strings[i] = ByteBuffer.wrap(buffer, "utf8", true).toString("utf8");
|
|
} catch (e) {
|
|
if (DEBUG) {
|
|
console.error(e);
|
|
console.log("Error when turning buffer to utf-8 string.");
|
|
}
|
|
}
|
|
} else {
|
|
strings[i] = "";
|
|
}
|
|
} else {
|
|
u16len = bb.readUint16();
|
|
if ((u16len & 0x8000) != 0) {
|
|
// larger than 32768
|
|
u16len = ((u16len & 0x7fff) << 16) + bb.readUint16();
|
|
}
|
|
|
|
if (u16len > 0) {
|
|
var len = u16len * 2;
|
|
buffer = ResourceFinder.readBytes(bb, len);
|
|
try {
|
|
strings[i] = ByteBuffer.wrap(buffer, "utf8", true).toString("utf8");
|
|
} catch (e) {
|
|
if (DEBUG) {
|
|
console.error(e);
|
|
console.log("Error when turning buffer to utf-8 string.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (DEBUG) {
|
|
console.log("Parsed value: {0}", strings[i]);
|
|
}
|
|
}
|
|
|
|
return strings;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {ByteBuffer} bb
|
|
*/
|
|
ResourceFinder.prototype.processTypeSpec = function (bb) {
|
|
var type = bb.readShort(),
|
|
headerSize = bb.readShort(),
|
|
size = bb.readInt(),
|
|
id = bb.readByte(),
|
|
res0 = bb.readByte(),
|
|
res1 = bb.readShort(),
|
|
entryCount = bb.readInt();
|
|
|
|
if (DEBUG) {
|
|
console.log("Processing type spec " + this.typeStringPool[id - 1] + "...");
|
|
}
|
|
|
|
var flags = new Array(entryCount);
|
|
|
|
for (var i = 0; i < entryCount; ++i) {
|
|
flags[i] = bb.readInt();
|
|
}
|
|
};
|
|
|
|
ResourceFinder.prototype.putIntoMap = function (resId, value) {
|
|
if (this.responseMap[resId.toUpperCase()] == null) {
|
|
this.responseMap[resId.toUpperCase()] = [];
|
|
}
|
|
if (value) {
|
|
this.responseMap[resId.toUpperCase()].push(value);
|
|
}
|
|
};
|
|
|
|
module.exports = ResourceFinder; |