Android Reverse Travel - Resolve Compiled Resource.arsc File Format

Links to the original text: http://www.520monkey.com/archives/577

I. Preface

It's almost New Year's Day. I'd like to congratulate you all in advance for a happy New Year. This article is also the last one of this year. Today we'll continue to look at reverse. In the previous article, we introduced how to parse Android Manifest. XML file format after compilation in Android: Click Enter

At that time, I said that in fact, I will continue to introduce two files in the future: resource.arsc and classes.dex. Today we will look at resource.arsc file format parsing, classes.dex parsing will wait years.

II. Preparations

When we decompile using the apktool tool, we will find a file: res/values/public.xml:


Let's take a look at the contents of the public.xml file:

See, this file saves all the types and corresponding id values in the apk, and we can see that every entry in this file is:

Type: type name

Name: resource name

id: the id of the resource

There are several types of words:

drawable, menu, layout, string, attr, color, style, etc.

So we'll see these types of file xml content in the decompiled folder.

Above we introduced how to use apktool decompiled content, the next thing we need to do is how to parse the resource.arsc file, parse these files.

We extract an apk to get the corresponding resource.arsc file. According to international practice, the format description of each file has its corresponding data structure, and resource is no exception: framework ks base include androidfw ResourceTypes. h, which is all the data structure defined in resource.

Here's another picture of God:


Every time we parse a file, we have a magic map. We parse the data according to this map.

Definition of data structure

This is the project engineering structure. We see a lot of data structures defined.

First, header information

The Resources.arsc file format consists of a series of chunks, each of which contains ResChunk_header with the following structure to describe the basic information of the chunk

package com.wjdiankong.parseresource.type;

import com.wjdiankong.parseresource.Utils;

/**
struct ResChunk_header
{
// Type identifier for this chunk. The meaning of this value depends
// on the containing chunk.
uint16_t type;

// Size of the chunk header (in bytes).  Adding this value to
// the address of the chunk allows you to find its associated data
// (if any).
uint16_t headerSize;

// Total size of this chunk (in bytes).  This is the chunkSize plus
// the size of any data associated with the chunk.  Adding this value
// to the chunk allows you to completely skip its contents (including
// any child chunks).  If this value is the same as chunkSize, there is
// no data associated with the chunk.
uint32_t size;

};

  • @author i

*/
public class ResChunkHeader {

public short type;
public short headerSize;
public int size;

public int getHeaderSize(){
	return 2+2+4;
}

@Override
public String toString(){
	return "type:"+Utils.bytesToHexString(Utils.int2Byte(type))+",headerSize:"+headerSize+",size:"+size;
}

}

Type: is the type of the current chunk

headerSize: The size of the current chunk's head

Size: The size of the current chunk

Second, the header information of the resource index table

The first structure of the Resources.arsc file is the header of the resource index table. It is structured as follows, describing the size of the Resources. ArsC file and the number of resource packages.

package com.wjdiankong.parseresource.type;

/**
struct ResTable_header
{
struct ResChunk_header header;

// The number of ResTable_package structures.
uint32_t packageCount;

};

  • @author i

*/
public class ResTableHeader {

public ResChunkHeader header;
public int packageCount;

public ResTableHeader(){
	header = new ResChunkHeader();
}

public int getHeaderSize(){
	return header.getHeaderSize() + 4;
}

@Override
public String toString(){
	return "header:"+header.toString()+"\n" + "packageCount:"+packageCount;
}

}

Header: The standard Chunk header format

packageCount: Number of resource packages compiled

An apk in Android may contain multiple resource bundles, and by default only one is the resource bundle where the package name of the application is located.

Example:

The blue highlighted part of the figure is the header of the resource index table. By parsing, we can get the following information: the type of the chunk is RES_TABLE_TYPE, the size of the head is 0XC, the size of the whole chunk is 1400252 byte, and there is a compiled resource bundle.


Third, the value string resource pool of resource items

Following the header of the resource index table is the value string resource pool of the resource item, which contains all the value strings of the resource item defined in the resource bundle. The structure of the header of the string resource pool is as follows.

package com.wjdiankong.parseresource.type;

/**
struct ResStringPool_header
{
struct ResChunk_header header;

// Number of strings in this pool (number of uint32_t indices that follow
// in the data).
uint32_t stringCount;

// Number of style span arrays in the pool (number of uint32_t indices
// follow the string indices).
uint32_t styleCount;

// Flags.
enum {
    // If set, the string index is sorted by the string values (based
    // on strcmp16()).
    SORTED_FLAG = 1<<0,

    // String pool is encoded in UTF-8
    UTF8_FLAG = 1<<8
};
uint32_t flags;

// Index from header of the string data.
uint32_t stringsStart;

// Index from header of the style data.
uint32_t stylesStart;

};

  • @author i

*/
public class ResStringPoolHeader {

public ResChunkHeader header;
public int stringCount;
public int styleCount;

public final static int SORTED_FLAG = 1;
public final static int UTF8_FLAG = (1<<8);

public int flags;
public int stringsStart;
public int stylesStart;

public ResStringPoolHeader(){
	header = new ResChunkHeader();
}

public int getHeaderSize(){
	return header.getHeaderSize() + 4 + 4 + 4 + 4 + 4;
}

@Override
public String toString(){
	return "header:"+header.toString()+"\n" + "stringCount:"+stringCount+",styleCount:"+styleCount+",flags:"+flags+",stringStart:"+stringsStart+",stylesStart:"+stylesStart;
}

}

Header: The standard Chunk header information structure

stringCount: Number of strings

styleCount: Number of string styles

flags: Attributes of strings with values of 0x000 (UTF-16), 0x001 (sorted strings), 0X100(UTF-8) and their combinations

stringStart: The distance between the content block of a string and its head

stylesStart: The distance between a string style block and its head

Example:

The green highlighted part of the figure is the head of the string resource pool. By parsing, we can get the following information. The type of the chunk is RES_STRING_POOL_TYPE, that is, the string resource pool. Head size is 0X1C, the whole chunk size is 369 524 byte, there are 8073 strings, 72 string styles, UTF-8 encoding, no sorting, string content block relative to the chunk header offset is 0X7F60, string style block relative to the chunk header offset is 0X5A054.
Next to the head are two offset arrays, string offset arrays and string style offset arrays. The size of these two offset arrays is equal to the values of stringCount and styleCount, respectively, and the type of each element is an unsigned integer. The resource pool structure in the whole character is as follows.

The first two bytes of the string in the string resource pool are the length of the string. The length calculation method is as follows. In addition, if the string encoding format is UTF-8, the string is terminated by 0X00 and UTF-16 is terminated by 0X0000.
len = (((hbyte & 0x7F) << 8)) | lbyte;
String has a one-to-one correspondence with string style, that is, if the nth string has a style, its style description is located in the nth element of the style block. String-style structures include the following two structures, ResStringPool_ref and ResStringPool_span. A string can correspond to multiple ResStringPool_span and one ResStringPool_ref. ResStringPool_span describes the style of the string before, and ResStringPool_ref has a fixed value of 0XFFFFFFFFFFFF as placeholder. The style block ends with two ResStringPool_ref values of 0XFFFFFFFFFF.

package com.wjdiankong.parseresource.type;

/**
struct ResStringPool_ref
{
uint32_t index;
};

  • @author i

*/
public class ResStringPoolRef {

public int index;

public int getSize(){
	return 4;
}

@Override
public String toString(){
	return "index:"+index;
}

}

Example:


The blue highlighted part of the figure is the style content block. According to the format analysis, the first string and the second string have no style. The position style of the fourth character to the seventh character of the third string is 0X1F88 character in the string resource pool, and so on.

Fourth, Package Data Block

Then the value string of the resource item is followed by the Package data block, which records the metadata of the compiled package. The header structure is as follows:

package com.wjdiankong.parseresource.type;

/**
struct ResTable_package
{
struct ResChunk_header header;

// If this is a base package, its ID.  Package IDs start
// at 1 (corresponding to the value of the package bits in a
// resource identifier).  0 means this is not a base package.
uint32_t id;

// Actual name of this package, \0-terminated.
char16_t name[128];

// Offset to a ResStringPool_header defining the resource
// type symbol table.  If zero, this package is inheriting from
// another base package (overriding specific values in it).
uint32_t typeStrings;

// Last index into typeStrings that is for public use by others.
uint32_t lastPublicType;

// Offset to a ResStringPool_header defining the resource
// key symbol table.  If zero, this package is inheriting from
// another base package (overriding specific values in it).
uint32_t keyStrings;

// Last index into keyStrings that is for public use by others.
uint32_t lastPublicKey;

};

  • @author i

*/
public class ResTablePackage {

public ResChunkHeader header;
public int id;
public char[] name = new char[128];
public int typeStrings;
public int lastPublicType;
public int keyStrings;
public int lastPublicKey;

public ResTablePackage(){
	header = new ResChunkHeader();
}

@Override
public String toString(){
	return "header:"+header.toString()+"\n"+",id="+id+",name:"+name.toString()+",typeStrings:"+typeStrings+",lastPublicType:"+lastPublicType+",keyStrings:"+keyStrings+",lastPublicKey:"+lastPublicKey;
}

}

header: Chunk's Head Information Data Structure

ID: The ID of the package is equal to Package Id, the value of the general user package is 0X7F, and the Package Id of the system resource package is 0X01; this value is very important and needs to be used when we build the ID value in the public.xml mentioned earlier.

Name: package name

TypeeString: The offset of the type string resource pool relative to the header

lastPublicType: The index of the last exported PublicType string in the type string resource pool, which is currently set to the number of elements in the type string resource pool. His use was not found in the process of parsing.

keyStrings: The offset of the resource item name string relative to the header

lastPublicKey: The index of the last exported PublicKey resource item name string in the resource item name string resource pool, which is currently set to the number of elements in the resource item name string resource pool. His use was not found in the process of parsing.

Example:


The purple highlight in the figure is ResTable_package. By parsing the data in the format above, we can conclude that the Chunk's Type is RES_TABLE_PACKAGE_TYPE, its head size is 0X120, the whole chunk size is 1030716 byte, its Package Id is 0X7F, its package name is co.runner.app, and the offset of the type string resource pool from the head is 0X120. There are 15 strings, resource item name string resource pool 0X1EC, 6249 strings.
The overall structure of the Packege data block can be represented by the following schematic diagram:


Among them, Type String Pool and Key String Pool are two string resource pools, which have the same structure as the value string resource pool of resource items, and correspond to the type string resource pool and the resource item name string resource pool respectively.
The next structure may be the Type specification data block or the Type resource item data block, which can be identified by their Type. The Type of the Type specification data block is RES_TABLE_TYPE_SPEC_TYPE, and the Type of the Type resource item data block is RES_TABLE_TYPE_TYPE.

Fifth, Type Specification Data Block

Type specification data blocks are used to describe the allocation differences of resource items. Through this differential description, we can know the allocation status of each resource item. Knowing the configuration status of a resource item, the Android resource management framework can know if it needs to reload the resource item after it detects that the configuration information of the device has changed. Type specification data blocks are organized by type, that is to say, each type corresponds to a type specification data block. The header structure of the data block is as follows.

package com.wjdiankong.parseresource.type;

/**
struct ResTable_typeSpec
{
struct ResChunk_header header;

// The type identifier this chunk is holding.  Type IDs start
// at 1 (corresponding to the value of the type bits in a
// resource identifier).  0 is invalid.
uint8_t id;

// Must be 0.
uint8_t res0;
// Must be 0.
uint16_t res1;

// Number of uint32_t entry configuration masks that follow.
uint32_t entryCount;

enum {
    // Additional flag indicating an entry is public.
    SPEC_PUBLIC = 0x40000000
};

};

  • @author i

*/
public class ResTableTypeSpec {

public final static int SPEC_PUBLIC = 0x40000000;

public ResChunkHeader header;
public byte id;
public byte res0;
public short res1;
public int entryCount;

public ResTableTypeSpec(){
	header = new ResChunkHeader();
}

@Override
public String toString(){
	return "header:"+header.toString()+",id:"+id+",res0:"+res0+",res1:"+res1+",entryCount:"+entryCount;
}

}

header: Chunk's Head Information Structure

Id: Type ID identifying the resource, which refers to the type ID of the resource. The types of resources are animator, anim, color, drawable, layout, menu, raw, string, xml, and so on, each of which is given an ID.

res0: Reservation, always 0

res1: Reservation, always 0

entryCount: Equal to the number of resource items of this type, referring to the number of resource items with the same name.

Example:


The highlighted green part of the figure is ResTable_typeSpec. By parsing the data in the above format, we can conclude that the Chunk's Type is RES_TABLE_TYPE_SPEC_TYPE, the header size is 0X10, the whole chunk size is 564 byte, the resource ID is 1, and the number of resource items of this Type is 137.
The ResTable_typeSpec is followed by an uint32_t array of entryCount size, each of which is used to describe the configuration differences of a resource item.

Sixth, Resource Type Item Data Block

The type resource item data block is used to describe the specific information of the resource item, so that we can know the name, value and configuration of each resource item. Type resource item data is also organized according to type and configuration, that is to say, a type with n configurations corresponds to a total of N type resource item data blocks. The header structure of the data block is as follows

package com.wjdiankong.parseresource.type;

/**
struct ResTable_type
{
struct ResChunk_header header;

enum {
    NO_ENTRY = 0xFFFFFFFF
};

// The type identifier this chunk is holding.  Type IDs start
// at 1 (corresponding to the value of the type bits in a
// resource identifier).  0 is invalid.
uint8_t id;

// Must be 0.
uint8_t res0;
// Must be 0.
uint16_t res1;

// Number of uint32_t entry indices that follow.
uint32_t entryCount;

// Offset from header where ResTable_entry data starts.
uint32_t entriesStart;

// Configuration this collection of entries is designed for.
ResTable_config config;

};

  • @author i

*/
public class ResTableType {

public ResChunkHeader header;

public final static int NO_ENTRY = 0xFFFFFFFF;

public byte id;
public byte res0;
public short res1;
public int entryCount;
public int entriesStart;

public ResTableConfig resConfig;

public ResTableType(){
	header = new ResChunkHeader();
	resConfig = new ResTableConfig();
}

public int getSize(){
	return header.getHeaderSize() + 1 + 1 + 2 + 4 + 4;
}

@Override
public String toString(){
	return "header:"+header.toString()+",id:"+id+",res0:"+res0+",res1:"+res1+",entryCount:"+entryCount+",entriesStart:"+entriesStart;
}

}

header: Chunk's Head Information Structure

id: Type ID identifying the resource

res0: Reservation, always 0

res1: Reservation, always 0

entryCount: Equal to the number of resource items of this type, referring to the number of resource items with the same name.

Entries Start: Equal to the offset value of the resource item data block relative to the header.

resConfig: Points to a ResTable_config to describe configuration information, locale, language, resolution, etc.

 

Example:


The highlighted red part of the figure is ResTable_type. By parsing the data in the above format, we can draw that RES_TABLE_TYPE_TYPE has a header size of 0X44, the size of the whole chunk is 4086 byte, the resource ID is 1, the number of resource items of this type is 137, and the deviation of resource data blocks from the header is 0X268.
ResTable_type is followed by an uint32_t array of entryCount size, each array element being used to describe the offset of a resource item data block. Following this offset array is a ResTable_entry array of entryCount size, each array element being used to describe the specific information of a resource item. The structure of ResTable_entry is as follows:

package com.wjdiankong.parseresource.type;

import com.wjdiankong.parseresource.ParseResourceUtils;

/**
struct ResTable_entry
{
// Number of bytes in this structure.
uint16_t size;

enum {
    // If set, this is a complex entry, holding a set of name/value
    // mappings.  It is followed by an array of ResTable_map structures.
    FLAG_COMPLEX = 0x0001,
    // If set, this resource has been declared public, so libraries
    // are allowed to reference it.
    FLAG_PUBLIC = 0x0002
};
uint16_t flags;

// Reference into ResTable_package::keyStrings identifying this entry.
struct ResStringPool_ref key;

};

  • @author i

*/
public class ResTableEntry {

public final static int FLAG_COMPLEX = 0x0001;
public final static int FLAG_PUBLIC = 0x0002;

public short size;
public short flags;

public ResStringPoolRef key;

public ResTableEntry(){
	key = new ResStringPoolRef();
}

public int getSize(){
	return 2+2+key.getSize();
}

@Override
public String toString(){
	return "size:"+size+",flags:"+flags+",key:"+key.toString()+",str:"+ParseResourceUtils.getKeyString(key.index);
}

}

ResTable_entry follows different data according to flags. If this bit of flags is 1, ResTable_entry is ResTable_map_entry. ResTable_map_entry inherits from ResTable_entry. Its structure is as follows.

package com.wjdiankong.parseresource.type;

/**
struct ResTable_map_entry : public ResTable_entry
{
// The resource ID pointing to the parent ResTable_map_entry is equal to 0 if there is no parent ResTable_map_entry.
ResTable_ref parent;
// Equal to the number of ResTable_map s that follow
uint32_t count;
};

  • @author i

*/
public class ResTableMapEntry extends ResTableEntry{

public ResTableRef parent;
public int count;

public ResTableMapEntry(){
	parent = new ResTableRef();
}

@Override
public int getSize(){
	return super.getSize() + parent.getSize() + 4;
}

@Override
public String toString(){
	return super.toString() + ",parent:"+parent.toString()+",count:"+count;
}

}

ResTable_map_entry is followed by count s of arrays of ResTable_map type. The structure of ResTable_map is as follows:

package com.wjdiankong.parseresource.type;

/**
struct ResTable_map
{
// bag resource item ID
ResTable_ref name;
// bag resource item value
Res_value value;
};

  • @author i

*/
public class ResTableMap {

public ResTableRef name;
public ResValue value;

public ResTableMap(){
	name = new ResTableRef();
	value = new ResValue();
}

public int getSize(){
	return name.getSize() + value.getSize();
}

@Override
public String toString(){
	return name.toString()+",value:"+value.toString();
}

}

Example:


The color from dark to light is a complete flags 1 resource item. Now let's interpret the meaning of this data together. The size of the header of this resource item is 0X10,flags is 1, so the following is the ResTable_map array. The name is not in the resource item reference pool, there is no parent map_entry, and there is a ResTable_map.
If flags is zero, then ResTable_entry is followed by a Res_value, which describes the value of a common resource, and the Res_value structure is as follows.

package com.wjdiankong.parseresource.type;

import com.wjdiankong.parseresource.ParseResourceUtils;

/**
struct Res_value
{
// Res_value Head Size
uint16_t size;
// Reservation, always 0
uint8_t res0;

 enum {
     TYPE_NULL = 0x00,
     TYPE_REFERENCE = 0x01,
     TYPE_ATTRIBUTE = 0x02,
     TYPE_STRING = 0x03,
     TYPE_FLOAT = 0x04,
     TYPE_DIMENSION = 0x05,
     TYPE_FRACTION = 0x06,
     TYPE_FIRST_INT = 0x10,
     TYPE_INT_DEC = 0x10,
     TYPE_INT_HEX = 0x11,
     TYPE_INT_BOOLEAN = 0x12,
     TYPE_FIRST_COLOR_INT = 0x1c,
     TYPE_INT_COLOR_ARGB8 = 0x1c,
     TYPE_INT_COLOR_ARGB8 = 0x1c,
     TYPE_INT_COLOR_RGB8 = 0x1d,
     TYPE_INT_COLOR_ARGB4 = 0x1e,
     TYPE_INT_COLOR_RGB4 = 0x1f,
     TYPE_LAST_COLOR_INT = 0x1f,
     TYPE_LAST_INT = 0x1f
 };
 //Data type, which can be obtained from the above enumeration type
 uint8_t dataType;

 //Index corresponding to data
 uint32_t data;

};

  • @author i

*/
public class ResValue {

//Constants used in dataType fields
public final static int TYPE_NULL = 0x00;
public final static int TYPE_REFERENCE = 0x01;
public final static int TYPE_ATTRIBUTE = 0x02;
public final static int TYPE_STRING = 0x03;
public final static int TYPE_FLOAT = 0x04;
public final static int TYPE_DIMENSION = 0x05;
public final static int TYPE_FRACTION = 0x06;
public final static int TYPE_FIRST_INT = 0x10;
public final static int TYPE_INT_DEC = 0x10;
public final static int TYPE_INT_HEX = 0x11;
public final static int TYPE_INT_BOOLEAN = 0x12;
public final static int TYPE_FIRST_COLOR_INT = 0x1c;
public final static int TYPE_INT_COLOR_ARGB8 = 0x1c;
public final static int TYPE_INT_COLOR_RGB8 = 0x1d;
public final static int TYPE_INT_COLOR_ARGB4 = 0x1e;
public final static int TYPE_INT_COLOR_RGB4 = 0x1f;
public final static int TYPE_LAST_COLOR_INT = 0x1f;
public final static int TYPE_LAST_INT = 0x1f;

public static final int
COMPLEX_UNIT_PX			=0,
COMPLEX_UNIT_DIP		=1,
COMPLEX_UNIT_SP			=2,
COMPLEX_UNIT_PT			=3,
COMPLEX_UNIT_IN			=4,
COMPLEX_UNIT_MM			=5,
COMPLEX_UNIT_SHIFT		=0,
COMPLEX_UNIT_MASK		=15,
COMPLEX_UNIT_FRACTION	=0,
COMPLEX_UNIT_FRACTION_PARENT=1,
COMPLEX_RADIX_23p0		=0,
COMPLEX_RADIX_16p7		=1,
COMPLEX_RADIX_8p15		=2,
COMPLEX_RADIX_0p23		=3,
COMPLEX_RADIX_SHIFT		=4,
COMPLEX_RADIX_MASK		=3,
COMPLEX_MANTISSA_SHIFT	=8,
COMPLEX_MANTISSA_MASK	=0xFFFFFF;


public short size;
public byte res0;
public byte dataType;
public int data;

public int getSize(){
	return 2 + 1 + 1 + 4;
}

public String getTypeStr(){
	switch(dataType){
		case TYPE_NULL:
			return "TYPE_NULL";
		case TYPE_REFERENCE:
			return "TYPE_REFERENCE";
		case TYPE_ATTRIBUTE:
			return "TYPE_ATTRIBUTE";
		case TYPE_STRING:
			return "TYPE_STRING";
		case TYPE_FLOAT:
			return "TYPE_FLOAT";
		case TYPE_DIMENSION:
			return "TYPE_DIMENSION";
		case TYPE_FRACTION:
			return "TYPE_FRACTION";
		case TYPE_FIRST_INT:
			return "TYPE_FIRST_INT";
		case TYPE_INT_HEX:
			return "TYPE_INT_HEX";
		case TYPE_INT_BOOLEAN:
			return "TYPE_INT_BOOLEAN";
		case TYPE_FIRST_COLOR_INT:
			return "TYPE_FIRST_COLOR_INT";
		case TYPE_INT_COLOR_RGB8:
			return "TYPE_INT_COLOR_RGB8";
		case TYPE_INT_COLOR_ARGB4:
			return "TYPE_INT_COLOR_ARGB4";
		case TYPE_INT_COLOR_RGB4:
			return "TYPE_INT_COLOR_RGB4";
	}
	return "";
}

/*public String getDataStr(){
	if(dataType == TYPE_STRING){
		return ParseResourceUtils.getResString(data);
	}else if(dataType == TYPE_FIRST_COLOR_INT){
		return Utils.bytesToHexString(Utils.int2Byte(data));
	}else if(dataType == TYPE_INT_BOOLEAN){
		return data==0 ? "false" : "true";
	}
	return data+"";
}*/

public String getDataStr() {
	if (dataType == TYPE_STRING) {
		return ParseResourceUtils.getResString(data);
	}
	if (dataType == TYPE_ATTRIBUTE) {
		return String.format("?%s%08X",getPackage(data),data);
	}
	if (dataType == TYPE_REFERENCE) {
		return String.format("@%s%08X",getPackage(data),data);
	}
	if (dataType == TYPE_FLOAT) {
		return String.valueOf(Float.intBitsToFloat(data));
	}
	if (dataType == TYPE_INT_HEX) {
		return String.format("0x%08X",data);
	}
	if (dataType == TYPE_INT_BOOLEAN) {
		return data!=0?"true":"false";
	}
	if (dataType == TYPE_DIMENSION) {
		return Float.toString(complexToFloat(data))+
			DIMENSION_UNITS[data &amp; COMPLEX_UNIT_MASK];
	}
	if (dataType == TYPE_FRACTION) {
		return Float.toString(complexToFloat(data))+
			FRACTION_UNITS[data &amp; COMPLEX_UNIT_MASK];
	}
	if (dataType &gt;= TYPE_FIRST_COLOR_INT &amp;&amp; dataType &lt;= TYPE_LAST_COLOR_INT) {
		return String.format("#%08X",data);
	}
	if (dataType &gt;= TYPE_FIRST_INT &amp;&amp; dataType &lt;= TYPE_LAST_INT) {
		return String.valueOf(data);
	}
	return String.format("&lt;0x%X, type 0x%02X&gt;",data, dataType);
}

private static String getPackage(int id) {
	if (id&gt;&gt;&gt;24==1) {
		return "android:";
	}
	return "";
}

public static float complexToFloat(int complex) {
	return (float)(complex &amp; 0xFFFFFF00)*RADIX_MULTS[(complex&gt;&gt;4) &amp; 3];
}

private static final float RADIX_MULTS[]={
	0.00390625F,3.051758E-005F,1.192093E-007F,4.656613E-010F
};

private static final String DIMENSION_UNITS[]={
	"px","dip","sp","pt","in","mm","",""
};

private static final String FRACTION_UNITS[]={
	"%","%p","","","","","",""
};

@Override
public String toString(){
	return "size:"+size+",res0:"+res0+",dataType:"+getTypeStr()+",data:"+getDataStr();
}

}

size: ResValue Head size

res0: Reservation, always 0

dataType: The type of data that can be obtained from the above enumeration types

Data: Index for data

Here we see a transformation method, which we also used when parsing Android Manifest files.

Example:

The red line is followed by an example of Res_value, from which we can get the following information: the header size is 8,flags is equal to 0, so it is followed by Res_value, which is 150 in the resource pool of the resource item name string, and the corresponding value is badge_continue_months, Res_val is equal to 0. The size of UE is 8, the type of data is TYPE_STRING, the index of resource pool is 1912 in the value string of resource item, and the corresponding value is res/drawable-nodpi-v4/badge_continue_months.png.
After we know the file format of arsc, we can start our exploratory journey. Because we encounter many obstacles when debugging Apktool source code with Android studio, we can debug it smoothly under the guidance of our predecessors. So the following is a brief introduction of the method of setting up Android studio to debug Apktool source code.

 

4. Analysis of Parsing Code

Because of the length of the page, we will not paste all the code here. The download address of the code will be listed later.

package com.wjdiankong.parseresource;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;

public class ParseResourceMain {

public static void main(String[] args){
	
	byte[] srcByte = null;
	FileInputStream fis = null;
	ByteArrayOutputStream bos = null;
	try{
		fis = new FileInputStream("resource/resources_gdt1.arsc");
		bos = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int len = 0;
		while((len=fis.read(buffer)) != -1){
			bos.write(buffer, 0, len);
		}
		srcByte = bos.toByteArray();
	}catch(Exception e){
		System.out.println("read res file error:"+e.toString());
	}finally{
		try{
			fis.close();
			bos.close();
		}catch(Exception e){
			System.out.println("close file error:"+e.toString());
		}
	}
	
	if(srcByte == null){
		System.out.println("get src error...");
		return;
	}
	
	System.out.println("parse restable header...");
	ParseResourceUtils.parseResTableHeaderChunk(srcByte);
	System.out.println("++++++++++++++++++++++++++++++++++++++");
	System.out.println();
	
	System.out.println("parse resstring pool chunk...");
	ParseResourceUtils.parseResStringPoolChunk(srcByte);
	System.out.println("++++++++++++++++++++++++++++++++++++++");
	System.out.println();
	
	System.out.println("parse package chunk...");
	ParseResourceUtils.parsePackage(srcByte);
	System.out.println("++++++++++++++++++++++++++++++++++++++");
	System.out.println();
	
	System.out.println("parse typestring pool chunk...");
	ParseResourceUtils.parseTypeStringPoolChunk(srcByte);
	System.out.println("++++++++++++++++++++++++++++++++++++++");
	System.out.println();
	
	System.out.println("parse keystring pool chunk...");
	ParseResourceUtils.parseKeyStringPoolChunk(srcByte);
	System.out.println("++++++++++++++++++++++++++++++++++++++");
	System.out.println();
	
	int resCount = 0;
	while(!ParseResourceUtils.isEnd(srcByte.length)){
		resCount++;
		boolean isSpec = ParseResourceUtils.isTypeSpec(srcByte);
		if(isSpec){
			System.out.println("parse restype spec chunk...");
			ParseResourceUtils.parseResTypeSpec(srcByte);
			System.out.println("++++++++++++++++++++++++++++++++++++++");
			System.out.println();
		}else{
			System.out.println("parse restype info chunk...");
			ParseResourceUtils.parseResTypeInfo(srcByte);
			System.out.println("++++++++++++++++++++++++++++++++++++++");
			System.out.println();
		}
	}
	System.out.println("res count:"+resCount);
	
}

}

We see the code, first we read the resource.arsc file to a byte array, and then we start parsing.

First, parse header information

/**
 * Parsing header information
 * @param src
 */
public static void parseResTableHeaderChunk(byte[] src){
	ResTableHeader resTableHeader = new ResTableHeader();
resTableHeader.header = parseResChunkHeader(src, 0);

resStringPoolChunkOffset = resTableHeader.header.headerSize;

//Parse the number of PackageCount s (an apk may contain multiple Package resources)
byte[] packageCountByte = Utils.copyByte(src, resTableHeader.header.getHeaderSize(), 4);
resTableHeader.packageCount = Utils.byte2int(packageCountByte);

}

Analytical results:

 

Second, parse the resource string content

/**
 * Parse all string contents in the Resource.arsc file
 * @param src
 */
public static void parseResStringPoolChunk(byte[] src){
	ResStringPoolHeader stringPoolHeader = parseStringPoolChunk(src, resStringList, resStringPoolChunkOffset);
	packageChunkOffset = resStringPoolChunkOffset + stringPoolHeader.header.size;
}

Here's a core approach: parseStringPool Chunk

/**
 * Unified parsing of string content
 * @param src
 * @param stringList
 * @param stringOffset
 * @return
 */
public static ResStringPoolHeader parseStringPoolChunk(byte[] src, ArrayList<String> stringList, int stringOffset){
	ResStringPoolHeader stringPoolHeader = new ResStringPoolHeader();
//Parsing header information
stringPoolHeader.header = parseResChunkHeader(src, stringOffset);

System.out.println("header size:"+stringPoolHeader.header.headerSize);
System.out.println("size:"+stringPoolHeader.header.size);

int offset = stringOffset + stringPoolHeader.header.getHeaderSize();

//Gets the number of strings
byte[] stringCountByte = Utils.copyByte(src, offset, 4);
stringPoolHeader.stringCount = Utils.byte2int(stringCountByte);

//Number of parsing styles
byte[] styleCountByte = Utils.copyByte(src, offset+4, 4);
stringPoolHeader.styleCount = Utils.byte2int(styleCountByte);

//Here is the format of the string: UTF-8/UTF-16
byte[] flagByte = Utils.copyByte(src, offset+8, 4);
System.out.println("flag:"+Utils.bytesToHexString(flagByte));
stringPoolHeader.flags = Utils.byte2int(flagByte);

//Starting position of string content
byte[] stringStartByte = Utils.copyByte(src, offset+12, 4);
stringPoolHeader.stringsStart = Utils.byte2int(stringStartByte);
System.out.println("string start:"+Utils.bytesToHexString(stringStartByte));

//Starting position of style content
byte[] sytleStartByte = Utils.copyByte(src, offset+16, 4);
stringPoolHeader.stylesStart = Utils.byte2int(sytleStartByte);
System.out.println("style start:"+Utils.bytesToHexString(sytleStartByte));

//Gets an index array of string content and an index array of style content
int[] stringIndexAry = new int[stringPoolHeader.stringCount];
int[] styleIndexAry = new int[stringPoolHeader.styleCount];

System.out.println("string count:"+stringPoolHeader.stringCount);
System.out.println("style count:"+stringPoolHeader.styleCount);

int stringIndex = offset + 20;
for(int i=0;i&lt;stringPoolHeader.stringCount;i++){
	stringIndexAry[i] = Utils.byte2int(Utils.copyByte(src, stringIndex+i*4, 4));
}

int styleIndex = stringIndex + 4*stringPoolHeader.stringCount;
for(int i=0;i&lt;stringPoolHeader.styleCount;i++){
	styleIndexAry[i] = Utils.byte2int(Utils.copyByte(src,  styleIndex+i*4, 4));
}

//The last byte of the first two bytes of each string is the length of the string.
//Get the contents of all strings here
int stringContentIndex = styleIndex + stringPoolHeader.styleCount*4;
System.out.println("string index:"+Utils.bytesToHexString(Utils.int2Byte(stringContentIndex)));
int index = 0;
while(index &lt; stringPoolHeader.stringCount){
	byte[] stringSizeByte = Utils.copyByte(src, stringContentIndex, 2);
	int stringSize = (stringSizeByte[1] &amp; 0x7F);
	if(stringSize != 0){
		String val = "";
		try{
			val = new String(Utils.copyByte(src, stringContentIndex+2, stringSize), "utf-8");
		}catch(Exception e){
			System.out.println("string encode error:"+e.toString());
		}
		stringList.add(val);
	}else{
		stringList.add("");
	}
	stringContentIndex += (stringSize+3);
	index++;
}
for(String str : stringList){
	System.out.println("str:"+str);
}

return stringPoolHeader;

}

When you get a string, you need to get the starting position and the size of the string. This is the same as parsing the string in Android Manifest. XML file. The last byte in the first two bytes of a string block is the length of the string. Here, after parsing the string, we need to store it in a list, which can be used later. We need to index the string content.

Analytical results:


Third, parse package information

/**
 * Parsing Package Information
 * @param src
 */
public static void parsePackage(byte[] src){
	System.out.println("pchunkoffset:"+Utils.bytesToHexString(Utils.int2Byte(packageChunkOffset)));
	ResTablePackage resTabPackage = new ResTablePackage();
	//Parsing header information
	resTabPackage.header = parseResChunkHeader(src, packageChunkOffset);
System.out.println("package size:"+resTabPackage.header.headerSize);

int offset = packageChunkOffset + resTabPackage.header.getHeaderSize();

//Parsing packId
byte[] idByte = Utils.copyByte(src, offset, 4);
resTabPackage.id = Utils.byte2int(idByte);
packId = resTabPackage.id;

//Resolve package names
System.out.println("package offset:"+Utils.bytesToHexString(Utils.int2Byte(offset+4)));
byte[] nameByte = Utils.copyByte(src, offset+4, 128*2);//Here 128 is the size of this field. You can see the type description. It's char type, so multiply by 2.
String packageName = new String(nameByte);
packageName = Utils.filterStringNull(packageName);
System.out.println("pkgName:"+packageName);

//Resolve the offset value of a type string
byte[] typeStringsByte = Utils.copyByte(src, offset+4+128*2, 4);
resTabPackage.typeStrings = Utils.byte2int(typeStringsByte);
System.out.println("typeString:"+resTabPackage.typeStrings);

//Resolving lastPublicType fields
byte[] lastPublicType = Utils.copyByte(src, offset+8+128*2, 4);
resTabPackage.lastPublicType = Utils.byte2int(lastPublicType);

//Resolving the offset of keyString strings
byte[] keyStrings = Utils.copyByte(src, offset+12+128*2, 4);
resTabPackage.keyStrings = Utils.byte2int(keyStrings);
System.out.println("keyString:"+resTabPackage.keyStrings);

//Parsing lastPublicKey
byte[] lastPublicKey = Utils.copyByte(src, offset+12+128*2, 4);
resTabPackage.lastPublicKey = Utils.byte2int(lastPublicKey);

//Here we get the offset value of the type string and the offset value of the type string.
keyStringPoolChunkOffset = (packageChunkOffset+resTabPackage.keyStrings);
typeStringPoolChunkOffset = (packageChunkOffset+resTabPackage.typeStrings);

}

Here we see a special place, that is, the last two lines, where we need to get two important parses later, one is the offset value of the resource value string and the offset value of the resource type string.

Analytical results:


Fourth, parse the string content of resource type

/**
 * Parsing type string content
 * @param src
 */
public static void parseTypeStringPoolChunk(byte[] src){
	System.out.println("typestring offset:"+Utils.bytesToHexString(Utils.int2Byte(typeStringPoolChunkOffset)));
	ResStringPoolHeader stringPoolHeader = parseStringPoolChunk(src, typeStringList, typeStringPoolChunkOffset);
	System.out.println("size:"+stringPoolHeader.header.size);
}

ParseStringPool Chunk is also used for parsing, and a list of strings is also needed to store content.

Analytical results:


Fifth, parse the resource value string content

/**
 * Parsing key string content
 * @param src
 */
public static void parseKeyStringPoolChunk(byte[] src){
	System.out.println("keystring offset:"+Utils.bytesToHexString(Utils.int2Byte(keyStringPoolChunkOffset)));
	ResStringPoolHeader stringPoolHeader  = parseStringPoolChunk(src, keyStringList, keyStringPoolChunkOffset);
	System.out.println("size:"+stringPoolHeader.header.size);
	//After parsing the key string, you need to assign the offset value to resType, and you need to continue parsing later.
	resTypeOffset = (keyStringPoolChunkOffset+stringPoolHeader.header.size);
}

The same is true here. Parse StringPool Chunk method is used to parse. After parsing, a list of strings is saved, and index values are used to access them later.

Analytical results:

 

Sixth, Analyzing the Content of the Text

The main body here is ResValue, which is to start building entry information in public.xml, and separating different XML files from different types, so the parsing of this part of the content is a bit complicated.

int resCount = 0;
while(!ParseResourceUtils.isEnd(srcByte.length)){
	resCount++;
	boolean isSpec = ParseResourceUtils.isTypeSpec(srcByte);
	if(isSpec){
		System.out.println("parse restype spec chunk...");
		ParseResourceUtils.parseResTypeSpec(srcByte);
		System.out.println("++++++++++++++++++++++++++++++++++++++");
		System.out.println();
	}else{
		System.out.println("parse restype info chunk...");
		ParseResourceUtils.parseResTypeInfo(srcByte);
		System.out.println("++++++++++++++++++++++++++++++++++++++");
		System.out.println();
	}
}
System.out.println("res count:"+resCount);

Here is a circular parsing. There are two methods. One is the isEnd method and the other is the isTypeSpec method.

If we look closely at the above myth map, we can see that the latter ResType and ResTypeSpec appear alternately until the end of the file.

So the isEnd method is to determine whether the end of the file is reached:

/**
 * Determine whether it's at the end of the document
 * @param length
 * @return
 */
public static boolean isEnd(int length){
	if(resTypeOffset>=length){
		return true;
	}
	return false;
}

Another way is to determine whether it is ResType or ResTypeSpec, which can be distinguished by header information in Chunk:

/**
 * Determine whether it is a type descriptor
 * @param src
 * @return
 */
public static boolean isTypeSpec(byte[] src){
	ResChunkHeader header = parseResChunkHeader(src, resTypeOffset);
	if(header.type == 0x0202){
		return true;
	}
	return false;
}

Then we will parse ResTypeSpec and ResType separately:

1. Parsing ResTypeSpec

Mainly get each type name of Res

/**
 * Parsing ResTypeSepc Type Description Content
 * @param src
 */
public static void parseResTypeSpec(byte[] src){
	System.out.println("res type spec offset:"+Utils.bytesToHexString(Utils.int2Byte(resTypeOffset)));
	ResTableTypeSpec typeSpec = new ResTableTypeSpec();
	//Parsing header information
	typeSpec.header = parseResChunkHeader(src, resTypeOffset);
int offset = (resTypeOffset + typeSpec.header.getHeaderSize());

//Parsing id type
byte[] idByte = Utils.copyByte(src, offset, 1);
typeSpec.id = (byte)(idByte[0] &amp; 0xFF);
resTypeId = typeSpec.id;

//Resolve the res0 field, which is standby, always 0
byte[] res0Byte = Utils.copyByte(src, offset+1, 1);
typeSpec.res0 = (byte)(res0Byte[0] &amp; 0xFF);

//Resolve the res1 field, which is standby, always 0
byte[] res1Byte = Utils.copyByte(src, offset+2, 2);
typeSpec.res1 = Utils.byte2Short(res1Byte);

//Total number of entries
byte[] entryCountByte = Utils.copyByte(src, offset+4, 4);
typeSpec.entryCount = Utils.byte2int(entryCountByte);

System.out.println("res type spec:"+typeSpec);

System.out.println("type_name:"+typeStringList.get(typeSpec.id-1));

//Get the entryCount number of int arrays
int[] intAry = new int[typeSpec.entryCount];
int intAryOffset = resTypeOffset + typeSpec.header.headerSize;
System.out.print("int element:");
for(int i=0;i&lt;typeSpec.entryCount;i++){
	int element = Utils.byte2int(Utils.copyByte(src, intAryOffset+i*4, 4));
	intAry[i] = element;
	System.out.print(element+",");
}
System.out.println();

resTypeOffset += typeSpec.header.size;

}

Analytical results:


2. Parsing ResType

Mainly get all the item contents for each res type

/**
 * Parsing Type Information Content
 * @param src
 */
public static void parseResTypeInfo(byte[] src){
	System.out.println("type chunk offset:"+Utils.bytesToHexString(Utils.int2Byte(resTypeOffset)));
	ResTableType type = new ResTableType();
	//Parsing header information
	type.header = parseResChunkHeader(src, resTypeOffset);
int offset = (resTypeOffset + type.header.getHeaderSize());

//Parsing id value of type
byte[] idByte = Utils.copyByte(src, offset, 1);
type.id = (byte)(idByte[0] &amp; 0xFF);

//Resolve the value of the res0 field, the alternate field, always 0
byte[] res0 = Utils.copyByte(src, offset+1, 1);
type.res0 = (byte)(res0[0] &amp; 0xFF);

//Resolve the value of the res1 field, the alternate field, always 0
byte[] res1 = Utils.copyByte(src, offset+2, 2);
type.res1 = Utils.byte2Short(res1);

byte[] entryCountByte = Utils.copyByte(src, offset+4, 4);
type.entryCount = Utils.byte2int(entryCountByte);

byte[] entriesStartByte = Utils.copyByte(src, offset+8, 4);
type.entriesStart = Utils.byte2int(entriesStartByte);

ResTableConfig resConfig = new ResTableConfig();
resConfig = parseResTableConfig(Utils.copyByte(src, offset+12, resConfig.getSize()));
System.out.println("config:"+resConfig);

System.out.println("res type info:"+type);

System.out.println("type_name:"+typeStringList.get(type.id-1));

//Get the entryCount number of int arrays first
System.out.print("type int elements:");
int[] intAry = new int[type.entryCount];
for(int i=0;i&lt;type.entryCount;i++){
	int element = Utils.byte2int(Utils.copyByte(src, resTypeOffset+type.header.headerSize+i*4, 4));
	intAry[i] = element;
	System.out.print(element+",");
}
System.out.println();

//Here we begin to parse the ResEntry and ResValue that follow.
int entryAryOffset = resTypeOffset + type.entriesStart;
ResTableEntry[] tableEntryAry = new ResTableEntry[type.entryCount];
ResValue[] resValueAry = new ResValue[type.entryCount];
System.out.println("entry offset:"+Utils.bytesToHexString(Utils.int2Byte(entryAryOffset)));

//The problem here is that if it is ResMapEntry, the migration values are different, so different migration values need to be calculated here.
int bodySize = 0, valueOffset = entryAryOffset;
for(int i=0;i&lt;type.entryCount;i++){
	int resId = getResId(i);
	System.out.println("resId:"+Utils.bytesToHexString(Utils.int2Byte(resId)));
	ResTableEntry entry = new ResTableEntry();
	ResValue value = new ResValue();
	valueOffset += bodySize;
	System.out.println("valueOffset:"+Utils.bytesToHexString(Utils.int2Byte(valueOffset)));
	entry = parseResEntry(Utils.copyByte(src, valueOffset, entry.getSize()));

	//It's important to note that the flag variable of entry is 1, and if it's 1, then ResTable_map_entry
	if(entry.flags == 1){
		//Here is the value of the complex type
		ResTableMapEntry mapEntry = new ResTableMapEntry();
		mapEntry = parseResMapEntry(Utils.copyByte(src, valueOffset, mapEntry.getSize()));
		System.out.println("map entry:"+mapEntry);
		ResTableMap resMap = new ResTableMap();
		for(int j=0;j&lt;mapEntry.count;j++){
			int mapOffset = valueOffset + mapEntry.getSize() + resMap.getSize()*j;
			resMap = parseResTableMap(Utils.copyByte(src, mapOffset, resMap.getSize()));
			System.out.println("map:"+resMap);
		}
		bodySize = mapEntry.getSize() + resMap.getSize()*mapEntry.count;
	}else{
		System.out.println("entry:"+entry);
		//Here is a simple type of value
		value = parseResValue(Utils.copyByte(src, valueOffset+entry.getSize(), value.getSize()));
		System.out.println("value:"+value);
		bodySize = entry.getSize()+value.getSize();
	}

	tableEntryAry[i] = entry;
	resValueAry[i] = value;

	System.out.println("======================================");
}

resTypeOffset += type.header.size;

}

/**

  • Parsing ResEntry Content

  • @param src

  • @return
    */
    public static ResTableEntry parseResEntry(byte[] src){
    ResTableEntry entry = new ResTableEntry();

    byte[] sizeByte = Utils.copyByte(src, 0, 2);
    entry.size = Utils.byte2Short(sizeByte);

    byte[] flagByte = Utils.copyByte(src, 2, 2);
    entry.flags = Utils.byte2Short(flagByte);

    ResStringPoolRef key = new ResStringPoolRef();
    byte[] keyByte = Utils.copyByte(src, 4, 4);
    key.index = Utils.byte2int(keyByte);
    entry.key = key;

    return entry;
    }

/**

  • Parsing ResMapEntry content

  • @param src

  • @return
    */
    public static ResTableMapEntry parseResMapEntry(byte[] src){
    ResTableMapEntry entry = new ResTableMapEntry();

    byte[] sizeByte = Utils.copyByte(src, 0, 2);
    entry.size = Utils.byte2Short(sizeByte);

    byte[] flagByte = Utils.copyByte(src, 2, 2);
    entry.flags = Utils.byte2Short(flagByte);

    ResStringPoolRef key = new ResStringPoolRef();
    byte[] keyByte = Utils.copyByte(src, 4, 4);
    key.index = Utils.byte2int(keyByte);
    entry.key = key;

    ResTableRef ref = new ResTableRef();
    byte[] identByte = Utils.copyByte(src, 8, 4);
    ref.ident = Utils.byte2int(identByte);
    entry.parent = ref;
    byte[] countByte = Utils.copyByte(src, 12, 4);
    entry.count = Utils.byte2int(countByte);

    return entry;
    }

/**

  • Parsing ResValue content

  • @param src

  • @return
    */
    public static ResValue parseResValue(byte[] src){
    ResValue resValue = new ResValue();
    byte[] sizeByte = Utils.copyByte(src, 0, 2);
    resValue.size = Utils.byte2Short(sizeByte);

    byte[] res0Byte = Utils.copyByte(src, 2, 1);
    resValue.res0 = (byte)(res0Byte[0] & 0xFF);

    byte[] dataType = Utils.copyByte(src, 3, 1);
    resValue.dataType = (byte)(dataType[0] & 0xFF);

    byte[] data = Utils.copyByte(src, 4, 4);
    resValue.data = Utils.byte2int(data);

    return resValue;
    }

/**

  • Parsing ResTableMap content

  • @param src

  • @return
    */
    public static ResTableMap parseResTableMap(byte[] src){
    ResTableMap tableMap = new ResTableMap();

    ResTableRef ref = new ResTableRef();
    byte[] identByte = Utils.copyByte(src, 0, ref.getSize());
    ref.ident = Utils.byte2int(identByte);
    tableMap.name = ref;

    ResValue value = new ResValue();
    value = parseResValue(Utils.copyByte(src, ref.getSize(), value.getSize()));
    tableMap.value = value;

    return tableMap;

}

/**

  • Parsing ResTableConfig configuration information

  • @param src

  • @return
    */
    public static ResTableConfig parseResTableConfig(byte[] src){
    ResTableConfig config = new ResTableConfig();

    byte[] sizeByte = Utils.copyByte(src, 0, 4);
    config.size = Utils.byte2int(sizeByte);

    // The following structure is Union
    byte[] mccByte = Utils.copyByte(src, 4, 2);
    config.mcc = Utils.byte2Short(mccByte);
    byte[] mncByte = Utils.copyByte(src, 6, 2);
    config.mnc = Utils.byte2Short(mncByte);
    byte[] imsiByte = Utils.copyByte(src, 4, 4);
    config.imsi = Utils.byte2int(imsiByte);

    // The following structure is Union
    byte[] languageByte = Utils.copyByte(src, 8, 2);
    config.language = languageByte;
    byte[] countryByte = Utils.copyByte(src, 10, 2);
    config.country = countryByte;
    byte[] localeByte = Utils.copyByte(src, 8, 4);
    config.locale = Utils.byte2int(localeByte);

    // The following structure is Union
    byte[] orientationByte = Utils.copyByte(src, 12, 1);
    config.orientation = orientationByte[0];
    byte[] touchscreenByte = Utils.copyByte(src, 13, 1);
    config.touchscreen = touchscreenByte[0];
    byte[] densityByte = Utils.copyByte(src, 14, 2);
    config.density = Utils.byte2Short(densityByte);
    byte[] screenTypeByte = Utils.copyByte(src, 12, 4);
    config.screenType = Utils.byte2int(screenTypeByte);

    // The following structure is Union
    byte[] keyboardByte = Utils.copyByte(src, 16, 1);
    config.keyboard = keyboardByte[0];
    byte[] navigationByte = Utils.copyByte(src, 17, 1);
    config.navigation = navigationByte[0];
    byte[] inputFlagsByte = Utils.copyByte(src, 18, 1);
    config.inputFlags = inputFlagsByte[0];
    byte[] inputPad0Byte = Utils.copyByte(src, 19, 1);
    config.inputPad0 = inputPad0Byte[0];
    byte[] inputByte = Utils.copyByte(src, 16, 4);
    config.input = Utils.byte2int(inputByte);

    // The following structure is Union
    byte[] screenWidthByte = Utils.copyByte(src, 20, 2);
    config.screenWidth = Utils.byte2Short(screenWidthByte);
    byte[] screenHeightByte = Utils.copyByte(src, 22, 2);
    config.screenHeight = Utils.byte2Short(screenHeightByte);
    byte[] screenSizeByte = Utils.copyByte(src, 20, 4);
    config.screenSize = Utils.byte2int(screenSizeByte);

    // The following structure is Union
    byte[] sdVersionByte = Utils.copyByte(src, 24, 2);
    config.sdVersion = Utils.byte2Short(sdVersionByte);
    byte[] minorVersionByte = Utils.copyByte(src, 26, 2);
    config.minorVersion = Utils.byte2Short(minorVersionByte);
    byte[] versionByte = Utils.copyByte(src, 24, 4);
    config.version = Utils.byte2int(versionByte);

    // The following structure is Union
    byte[] screenLayoutByte = Utils.copyByte(src, 28, 1);
    config.screenLayout = screenLayoutByte[0];
    byte[] uiModeByte = Utils.copyByte(src, 29, 1);
    config.uiMode = uiModeByte[0];
    byte[] smallestScreenWidthDpByte = Utils.copyByte(src, 30, 2);
    config.smallestScreenWidthDp = Utils.byte2Short(smallestScreenWidthDpByte);
    byte[] screenConfigByte = Utils.copyByte(src, 28, 4);
    config.screenConfig = Utils.byte2int(screenConfigByte);

    // The following structure is Union
    byte[] screenWidthDpByte = Utils.copyByte(src, 32, 2);
    config.screenWidthDp = Utils.byte2Short(screenWidthDpByte);
    byte[] screenHeightDpByte = Utils.copyByte(src, 34, 2);
    config.screenHeightDp = Utils.byte2Short(screenHeightDpByte);
    byte[] screenSizeDpByte = Utils.copyByte(src, 32, 4);
    config.screenSizeDp = Utils.byte2int(screenSizeDpByte);

    byte[] localeScriptByte = Utils.copyByte(src, 36, 4);
    config.localeScript = localeScriptByte;

    byte[] localeVariantByte = Utils.copyByte(src, 40, 8);
    config.localeVariant = localeVariantByte;
    return config;
    }

Looking at this, we find that the parsing here is very complicated, and as we explained the data structure there, he needs to parse a lot of content:

ResValue,ResTableMap,ResTableMapEntry,ResTableEntry,ResConfig

How to parse each data structure is not much to say here, that is, to read bytes. Here is a core code:

//It's important to note that the flag variable of entry is 1, and if it's 1, then ResTable_map_entry
if(entry.flags == 1){
	//Here is the value of the complex type
	ResTableMapEntry mapEntry = new ResTableMapEntry();
	mapEntry = parseResMapEntry(Utils.copyByte(src, valueOffset, mapEntry.getSize()));
	System.out.println("map entry:"+mapEntry);
	ResTableMap resMap = new ResTableMap();
	for(int j=0;j<mapEntry.count;j++){
		int mapOffset = valueOffset + mapEntry.getSize() + resMap.getSize()*j;
		resMap = parseResTableMap(Utils.copyByte(src, mapOffset, resMap.getSize()));
		System.out.println("map:"+resMap);
	}
	bodySize = mapEntry.getSize() + resMap.getSize()*mapEntry.count;
}else{
	System.out.println("entry:"+entry);
	//Here is a simple type of value
	value = parseResValue(Utils.copyByte(src, valueOffset+entry.getSize(), value.getSize()));
	System.out.println("value:"+value);
	bodySize = entry.getSize()+value.getSize();
}

Judge the value of flag to perform different analytical operations. This should be noted here.

Analytical results:

It's nice to see the parsing results, because the hardest part is that we parsed successfully, and we're very excited to see the results, which is what we want, but what we need to explain here is that with these values, it's very easy to build public.xml content and all kinds of xml content, of course, here we build it. Yes, interested students can try it.

Note: Here, ResId is constructed by:

/**
 * Get the resource id
 * Here the top is packid, the middle is restypeid, and the position is entryid.
 * @param entryid
 * @return
 */
public static int getResId(int entryid){
	return (((packId)<<24) | (((resTypeId) & 0xFF)<<16) | (entryid & 0xFFFF));
}

Here we can see a resId of type int.

His highest two bytes represent packId, the system resource id is: 0x01, and the common application resource id is: 0x7F.

The two bytes in his middle represent resTypeId, type id, which starts at 0. For example, the first type in our example is attr, and his resTypeId is 00.

His lowest four bytes represent the sequence id of the resource, starting with 1 and gradually accumulating 1.

Project download address: http://download.csdn.net/detail/jiangwei0910410003/9426712

V. Overview of Technology

Above we have painfully parsed all the resources. ArsC files, of course, a little more content, so some places may not be introduced clearly or there are errors, please correct more. Of course, we have introduced three of the four file formats after Android compilation:

so file format, AndroidManifest.xml format / resource file. xml, resource.arsc

So all that's left is the file format of classes.dex, and we're done. But what I want to say here is that this article is mainly about parsing the resource.arsc file format, so what is the purpose of writing this article?

There are two:

1. When we decompile with apktool tools, we often get some inexplicable information, most of which are NotFound ResId 0x0000XXX. Then we can fix it. Of course, we can get apktool source code to solve this problem, or we can just use ourselves. Write this set of parsing code is also possible.

2. As we mentioned earlier, after parsing the resource.arsc file, if you understand the format of the resource.arsc file, you can confuse the name of the resource file to reduce the size of the apk package. In my previous article:

The signature mechanism of Apk: http://blog.csdn.net/jiangwei0910410003/article/details/50402000

Because the three files under META-INF folder are very large, the reason is that they keep each resource name internally. Sometimes, in order to avoid conflict, we name the resource very long in the project, which will lead to the large package of apk.

The resource.arsc file is also very large, because the resource names need to be saved, but we know that confusion in Android will not confuse the resource files, so we can reduce the size of the package apk through this idea. I will continue to explain this later.

Be careful:

One more thing we need to tell you here is that there is actually a simpler way to do the above analysis work. That's the aapt command? What is this aapt for? There's a lot of information on the Internet. He's actually very simple to package the resource files in Android into resource.arsc.


Only resource files of types res/animator, res/anim, res/color, res/drawable (non-Bitmap files, i.e. non-. png,.9.png,.jpg,.gif files), res/layout, res/menu, res/values and res/xml are compiled into binary XML files from text-formatted XML files.
These XML resource files are compiled from text format to binary format because:
1. XML files in binary format occupy less space. This is because the strings involved in tags, attribute names, attribute values, and content of all XML elements are collected in a pool of string resources and duplicated. With this string resource pool, the original place where the string was used would be replaced by an integer value indexed to the string resource pool, thus reducing the size of the file.
2. Binary format XML file parsing speed is faster. This is because XML elements in binary format no longer contain string values, so string parsing is avoided, thus speeding up.
Compiling XML resource files from text format to binary format solves the problem of space occupation and parsing efficiency, but for Android resource management framework, this is only part of the work. Another important task of Android resource management framework is to quickly find the corresponding resources according to the resource ID.

So let's look at it with the aapt command?

The aapt command is in our Android Sdk directory:


See the path: Android-SDK directory / build-tools / below

As we know, all the tools built into an apk in Android are in this directory. Here's another look at the use of these tools:


1. Generating R.java class files using aapt.exe provided by Android SDK
2. Use the aidl.exe provided by Android SDK to convert. Aidl into. java files (if there is no aidl, skip this step)
3. Compile. java class files using javac.exe provided by JDK
4. Generate classes.dex file using dx.bat command line script provided by Android SDK
5. Use aapt.exe provided by Android SDK to generate resource package files (including res, assets, Android manifest.xml, etc.)
6. Generate unsigned apk installation files using apkbuilder.bat provided by Android SDK
7. Use jarsigner.exe of jdk to sign unsigned packages with apk

See. We could have produced an apk package without any IDE tools. Haha~~

Keep looking at the usage of the aapt command. The command is simple:

AAPT L-A APK name > demo.txt

Directing the input results to demo.txt


Seeing what we've got, we find that it's the Android Manifest. XML content that we parsed above, so this is also a method. Of course, why did I end up with the aapt command? The Android Manifest. XML format we explained earlier is certainly useful. The aapt command is just a good tool for us. It is also a good choice for us to use this tool in the decompilation process. So I want to say that we will remember an aapt command in the future. It has many uses. It can be compiled into a resource.arsc file separately. We will use this command later.

VI. SUMMARY

This article is a bit long, so I wrote a lot of pain, but have to be patient, because the resource.arsc file format is much more complex than Android Manifest. XML file format, so it is very difficult to parse. I also hope you will have more support after reading, and there is a parse of classes.dex file format, of course, this article will not start until a few years later, so please look forward to it, it is best to note you happy New Year.~~

 

 

Pay attention to Wechat Public Number, the latest Android technology real-time push

Tags: Android xml SDK Java

Posted on Sat, 07 Sep 2019 02:30:24 -0700 by Dinosoles