All Downloads are FREE. Search and download functionalities are using the official Maven repository.

soot.jimple.infoflow.android.resources.ARSCFileParser Maven / Gradle / Ivy

There is a newer version: 2.14.1
Show newest version
/*******************************************************************************
 * Copyright (c) 2012 Secure Software Engineering Group at EC SPRIDE.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors: Christian Fritz, Steven Arzt, Siegfried Rasthofer, Eric
 * Bodden, and others.
 ******************************************************************************/
package soot.jimple.infoflow.android.resources;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import soot.jimple.infoflow.android.axml.ApkHandler;

/**
 * Parser for reading out the contents of Android's resource.arsc file.
 * Structure declarations and comments taken from the Android source code and
 * ported from C to Java.
 * 
 * @author Steven Arzt
 */
public class ARSCFileParser extends AbstractResourceParser {

	/**
	 * If true any encountered resource format violation like reserved
	 * fields which should be zero but have a value will raise an
	 * {@link RuntimeException}.
	 * 
	 * If false format violations will only be logged as errors.
	 */
	public static boolean STRICT_MODE = true;

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	protected final static int RES_STRING_POOL_TYPE = 0x0001;
	protected final static int RES_TABLE_TYPE = 0x0002;
	protected final static int RES_TABLE_PACKAGE_TYPE = 0x0200;
	protected final static int RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
	protected final static int RES_TABLE_TYPE_TYPE = 0x0201;

	protected final static int SORTED_FLAG = 1 << 0;
	protected final static int UTF8_FLAG = 1 << 8;

	protected final static int SPEC_PUBLIC = 0x40000000;

	/**
	 * Contains no data
	 */
	protected final static int TYPE_NULL = 0x00;
	/**
	 * The 'data' holds a ResTable_ref, a reference to another resource table entry.
	 */
	protected final static int TYPE_REFERENCE = 0x01;
	/**
	 * The 'data' holds an attribute resource identifier.
	 */
	protected final static int TYPE_ATTRIBUTE = 0x02;
	/**
	 * The 'data' holds an index into the containing resource table's global value
	 * string pool.
	 */
	protected final static int TYPE_STRING = 0x03;
	/**
	 * The 'data' holds a single-precision floating point number.
	 */
	protected final static int TYPE_FLOAT = 0x04;
	/**
	 * The 'data' holds a complex number encoding a dimension value, such as
	 * "100in".
	 */
	protected final static int TYPE_DIMENSION = 0x05;
	/**
	 * The 'data' holds a complex number encoding a fraction of a container.
	 */
	protected final static int TYPE_FRACTION = 0x06;
	/**
	 * Beginning of integer flavors...
	 */
	protected final static int TYPE_FIRST_INT = 0x10;
	/**
	 * The 'data' is a raw integer value of the form n..n.
	 */
	protected final static int TYPE_INT_DEC = 0x10;
	/**
	 * The 'data' is a raw integer value of the form 0xn..n.
	 */
	protected final static int TYPE_INT_HEX = 0x11;
	/**
	 * The 'data' is either 0 or 1, for input "false" or "true" respectively.
	 */
	protected final static int TYPE_INT_BOOLEAN = 0x12;
	/**
	 * Beginning of color integer flavors...
	 */
	protected final static int TYPE_FIRST_COLOR_INT = 0x1c;
	/**
	 * The 'data' is a raw integer value of the form #aarrggbb.
	 */
	protected final static int TYPE_INT_COLOR_ARGB8 = 0x1c;
	/**
	 * The 'data' is a raw integer value of the form #rrggbb.
	 */
	protected final static int TYPE_INT_COLOR_RGB8 = 0x1d;
	/**
	 * The 'data' is a raw integer value of the form #argb.
	 */
	protected final static int TYPE_INT_COLOR_ARGB4 = 0x1e;
	/**
	 * The 'data' is a raw integer value of the form #rgb.
	 */
	protected final static int TYPE_INT_COLOR_RGB4 = 0x1f;
	/**
	 * ...end of integer flavors.
	 */
	protected final static int TYPE_LAST_COLOR_INT = 0x1f;
	/**
	 * ...end of integer flavors.
	 */
	protected final static int TYPE_LAST_INT = 0x1f;

	/**
	 * This entry holds the attribute's type code.
	 */
	protected final static int ATTR_TYPE = (0x01000000 | (0 & 0xFFFF));
	/**
	 * For integral attributes, this is the minimum value it can hold.
	 */
	protected final static int ATTR_MIN = (0x01000000 | (1 & 0xFFFF));
	/**
	 * For integral attributes, this is the maximum value it can hold.
	 */
	protected final static int ATTR_MAX = (0x01000000 | (2 & 0xFFFF));
	/**
	 * Localization of this resource is can be encouraged or required with an aapt
	 * flag if this is set
	 */
	protected final static int ATTR_L10N = (0x01000000 | (3 & 0xFFFF));

	// for plural support, see
	// android.content.res.PluralRules#attrForQuantity(int)
	protected final static int ATTR_OTHER = (0x01000000 | (4 & 0xFFFF));
	protected final static int ATTR_ZERO = (0x01000000 | (5 & 0xFFFF));
	protected final static int ATTR_ONE = (0x01000000 | (6 & 0xFFFF));
	protected final static int ATTR_TWO = (0x01000000 | (7 & 0xFFFF));
	protected final static int ATTR_FEW = (0x01000000 | (8 & 0xFFFF));
	protected final static int ATTR_MANY = (0x01000000 | (9 & 0xFFFF));

	protected final static int NO_ENTRY = 0xFFFFFFFF;

	/**
	 * Where the unit type information is. This gives us 16 possible types, as
	 * defined below.
	 */
	protected final static int COMPLEX_UNIT_SHIFT = 0x0;
	protected final static int COMPLEX_UNIT_MASK = 0xf;
	/**
	 * TYPE_DIMENSION: Value is raw pixels.
	 */
	protected final static int COMPLEX_UNIT_PX = 0;
	/**
	 * TYPE_DIMENSION: Value is Device Independent Pixels.
	 */
	protected final static int COMPLEX_UNIT_DIP = 1;
	/**
	 * TYPE_DIMENSION: Value is a Scaled device independent Pixels.
	 */
	protected final static int COMPLEX_UNIT_SP = 2;
	/**
	 * TYPE_DIMENSION: Value is in points.
	 */
	protected final static int COMPLEX_UNIT_PT = 3;
	/**
	 * TYPE_DIMENSION: Value is in inches.
	 */
	protected final static int COMPLEX_UNIT_IN = 4;
	/**
	 * TYPE_DIMENSION: Value is in millimeters.
	 */
	protected final static int COMPLEX_UNIT_MM = 5;
	/**
	 * TYPE_FRACTION: A basic fraction of the overall size.
	 */
	protected final static int COMPLEX_UNIT_FRACTION = 0;
	/**
	 * TYPE_FRACTION: A fraction of the parent size.
	 */
	protected final static int COMPLEX_UNIT_FRACTION_PARENT = 1;
	/**
	 * Where the radix information is, telling where the decimal place appears in
	 * the mantissa. This give us 4 possible fixed point representations as defined
	 * below.
	 */
	protected final static int COMPLEX_RADIX_SHIFT = 4;
	protected final static int COMPLEX_RADIX_MASK = 0x3;
	/**
	 * The mantissa is an integral number -- i.e., 0xnnnnnn.0
	 */
	protected final static int COMPLEX_RADIX_23p0 = 0;
	/**
	 * The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn
	 */
	protected final static int COMPLEX_RADIX_16p7 = 1;
	/**
	 * The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn
	 */
	protected final static int COMPLEX_RADIX_8p15 = 2;
	/**
	 * The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn
	 */
	protected final static int COMPLEX_RADIX_0p23 = 3;
	/**
	 * Where the actual value is. This gives us 23 bits of precision. The top bit is
	 * the sign.
	 */
	protected final static int COMPLEX_MANTISSA_SHIFT = 8;
	protected final static int COMPLEX_MANTISSA_MASK = 0xffffff;

	protected static final float MANTISSA_MULT = 1.0f / (1 << COMPLEX_MANTISSA_SHIFT);
	protected static final float[] RADIX_MULTS = new float[] { 1.0f * MANTISSA_MULT, 1.0f / (1 << 7) * MANTISSA_MULT,
			1.0f / (1 << 15) * MANTISSA_MULT, 1.0f / (1 << 23) * MANTISSA_MULT };

	/**
	 * If set, this is a complex entry, holding a set of name/value mappings. It is
	 * followed by an array of ResTable_Map structures.
	 */
	public final static int FLAG_COMPLEX = 0x0001;
	/**
	 * If set, this resource has been declared public, so libraries are allowed to
	 * reference it.
	 */
	public final static int FLAG_PUBLIC = 0x0002;

	private final Map stringTable = new HashMap();
	private final List packages = new ArrayList();

	public static class ResPackage {
		private int packageId;
		private String packageName;
		private List types = new ArrayList<>();

		public int getPackageId() {
			return this.packageId;
		}

		public String getPackageName() {
			return this.packageName;
		}

		public List getDeclaredTypes() {
			return this.types;
		}

		/**
		 * Gets the resource with the given type
		 * 
		 * @param type The type string for which to look, e.g., "string"
		 * @return The resource type with the given identifier string, or null if no
		 *         such type exists
		 */
		public ResType getResourceType(String type) {
			for (ResType resType : types) {
				if (resType.typeName.equals(type))
					return resType;
			}
			return null;
		}

		/**
		 * Adds all contents of the given package to this data object
		 * 
		 * @param other The other package that shall be integrated into this one
		 */
		private void addAll(ResPackage other) {
			for (ResType tp : other.types) {
				ResType existingType = getType(tp.id, tp.typeName);
				if (existingType == null)
					types.add(tp);
				else
					existingType.addAll(tp);
			}
		}

		public ResType getType(int id, String typeName) {
			return types.stream().filter(t -> t.id == id && t.typeName.equals(typeName)).findFirst().orElse(null);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + packageId;
			result = prime * result + ((packageName == null) ? 0 : packageName.hashCode());
			result = prime * result + ((types == null) ? 0 : types.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResPackage other = (ResPackage) obj;
			if (packageId != other.packageId)
				return false;
			if (packageName == null) {
				if (other.packageName != null)
					return false;
			} else if (!packageName.equals(other.packageName))
				return false;
			if (types == null) {
				if (other.types != null)
					return false;
			} else if (!types.equals(other.types))
				return false;
			return true;
		}

	}

	/**
	 * A resource type in an Android resource file. All resources are associated
	 * with a type.
	 */
	public static class ResType {
		private int id;
		private String typeName;
		private List configurations = new ArrayList();

		public String getTypeName() {
			return this.typeName;
		}

		/**
		 * Adds all data from the given type into this type
		 * 
		 * @param tp The type from which to add all data into this type
		 */
		private void addAll(ResType tp) {
			for (ResConfig config : tp.configurations) {
				ResConfig existingConfig = getConfiguration(config.getConfig());
				if (existingConfig == null)
					configurations.add(config);
				else
					existingConfig.addAll(config);
			}
		}

		/**
		 * Gets the configuration object for the given settings
		 * 
		 * @param config The settings to look for
		 * @return The configuration object that is associated with the given settings,
		 *         or null if no such configuration object exists
		 */
		public ResConfig getConfiguration(ResTable_Config config) {
			return configurations.stream().filter(c -> c.config.equals(config)).findFirst().orElse(null);
		}

		public List getConfigurations() {
			return this.configurations;
		}

		/**
		 * Gets a list of all resources in this type regardless of the configuration.
		 * Resources sharing the same ID will only be returned once, taking the value
		 * from the first applicable configuration.
		 * 
		 * @return A list of all resources of this type.
		 */
		public Collection getAllResources() {
			Map resources = new HashMap();
			for (ResConfig rc : this.configurations)
				for (AbstractResource res : rc.getResources())
					if (!resources.containsKey(res.resourceName))
						resources.put(res.resourceName, res);
			return resources.values();
		}

		/**
		 * Gets the names of all resources defined for this resource type
		 * 
		 * @return The names of all resources in the current type
		 */
		public Collection getAllResourceNames() {
			return this.configurations.stream().flatMap(c -> c.getResources().stream()).map(r -> r.getResourceName())
					.collect(Collectors.toSet());
		}

		/**
		 * Gets all resource of the current type that have the given id
		 * 
		 * @param resourceID The resource id to look for
		 * @return A list containing all resources with the given id
		 */
		public List getAllResources(int resourceID) {
			List resourceList = new ArrayList<>();
			for (ResConfig rc : this.configurations)
				for (AbstractResource res : rc.getResources())
					if (res.resourceID == resourceID)
						resourceList.add(res);
			return resourceList;
		}

		/**
		 * Gets the first resource with the given name or null if no such resource
		 * exists
		 * 
		 * @param resourceName The resource name to look for
		 * @return The first resource with the given name or null if no such resource
		 *         exists
		 */
		public AbstractResource getResourceByName(String resourceName) {
			for (ResConfig rc : this.configurations)
				for (AbstractResource res : rc.getResources())
					if (res.getResourceName().equals(resourceName))
						return res;
			return null;
		}

		/**
		 * Gets the first resource of the current type that has the given name
		 * 
		 * @param resourceName The resource name to look for
		 * @return The resource with the given name if it exists, otherwise null
		 */
		public AbstractResource getFirstResource(String resourceName) {
			for (ResConfig rc : this.configurations)
				for (AbstractResource res : rc.getResources())
					if (res.resourceName.equals(resourceName))
						return res;
			return null;
		}

		/**
		 * Gets the first resource of the current type with the given ID
		 * 
		 * @param resourceID The resource ID to look for
		 * @return The resource with the given ID if it exists, otherwise null
		 */
		public AbstractResource getFirstResource(int resourceID) {
			for (ResConfig rc : this.configurations)
				for (AbstractResource res : rc.getResources())
					if (res.resourceID == resourceID)
						return res;
			return null;
		}

		@Override
		public String toString() {
			return this.typeName;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((configurations == null) ? 0 : configurations.hashCode());
			result = prime * result + id;
			result = prime * result + ((typeName == null) ? 0 : typeName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResType other = (ResType) obj;
			if (configurations == null) {
				if (other.configurations != null)
					return false;
			} else if (!configurations.equals(other.configurations))
				return false;
			if (id != other.id)
				return false;
			if (typeName == null) {
				if (other.typeName != null)
					return false;
			} else if (!typeName.equals(other.typeName))
				return false;
			return true;
		}
	}

	/**
	 * A configuration in an Android resource file. All resources are associated
	 * with a configuration (which may be the default one).
	 */
	public static class ResConfig {
		private ResTable_Config config;
		private List resources = new ArrayList<>();

		public ResTable_Config getConfig() {
			return config;
		}

		/**
		 * Adds all data from the given configuration into this data object
		 * 
		 * @param other The configuration object from which to read the data
		 */
		private void addAll(ResConfig other) {
			this.resources.addAll(other.resources);
		}

		public List getResources() {
			return this.resources;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((config == null) ? 0 : config.hashCode());
			result = prime * result + ((resources == null) ? 0 : resources.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResConfig other = (ResConfig) obj;
			if (config == null) {
				if (other.config != null)
					return false;
			} else if (!config.equals(other.config))
				return false;
			if (resources == null) {
				if (other.resources != null)
					return false;
			} else if (!resources.equals(other.resources))
				return false;
			return true;
		}
	}

	/**
	 * Abstract base class for all Android resources.
	 */
	public static abstract class AbstractResource {
		private String resourceName;
		private int resourceID;

		public String getResourceName() {
			return this.resourceName;
		}

		public int getResourceID() {
			return this.resourceID;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + resourceID;
			result = prime * result + ((resourceName == null) ? 0 : resourceName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			AbstractResource other = (AbstractResource) obj;
			if (resourceID != other.resourceID)
				return false;
			if (resourceName == null) {
				if (other.resourceName != null)
					return false;
			} else if (!resourceName.equals(other.resourceName))
				return false;
			return true;
		}
	}

	/**
	 * Android resource that does not contain any data
	 */
	public static class NullResource extends AbstractResource {
	}

	/**
	 * Android resource containing a reference to another resource.
	 */
	public static class ReferenceResource extends AbstractResource {
		private int referenceID;

		public ReferenceResource(int id) {
			this.referenceID = id;
		}

		public int getReferenceID() {
			return this.referenceID;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + referenceID;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ReferenceResource other = (ReferenceResource) obj;
			if (referenceID != other.referenceID)
				return false;
			return true;
		}
	}

	/**
	 * Android resource containing an attribute resource identifier.
	 */
	public static class AttributeResource extends AbstractResource {
		private int attributeID;

		public AttributeResource(int id) {
			this.attributeID = id;
		}

		public int getAttributeID() {
			return this.attributeID;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + attributeID;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			AttributeResource other = (AttributeResource) obj;
			if (attributeID != other.attributeID)
				return false;
			return true;
		}
	}

	/**
	 * Android resource containing string data.
	 */
	public static class StringResource extends AbstractResource {
		private String value;

		public StringResource(String value) {
			this.value = value;
		}

		public String getValue() {
			return this.value;
		}

		@Override
		public String toString() {
			return this.value;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			StringResource other = (StringResource) obj;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}

	}

	/**
	 * Android resource containing integer data.
	 */
	public static class IntegerResource extends AbstractResource {
		private int value;

		public IntegerResource(int value) {
			this.value = value;
		}

		public int getValue() {
			return this.value;
		}

		@Override
		public String toString() {
			return Integer.toString(value);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + value;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			IntegerResource other = (IntegerResource) obj;
			if (value != other.value)
				return false;
			return true;
		}
	}

	/**
	 * Android resource containing a single-precision floating point number
	 */
	public static class FloatResource extends AbstractResource {
		private float value;

		public FloatResource(float value) {
			this.value = value;
		}

		public float getValue() {
			return this.value;
		}

		@Override
		public String toString() {
			return Float.toString(value);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + Float.floatToIntBits(value);
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			FloatResource other = (FloatResource) obj;
			if (Float.floatToIntBits(value) != Float.floatToIntBits(other.value))
				return false;
			return true;
		}
	}

	/**
	 * Android resource containing boolean data.
	 */
	public static class BooleanResource extends AbstractResource {
		private boolean value;

		public BooleanResource(int value) {
			this.value = (value != 0);
		}

		public boolean getValue() {
			return this.value;
		}

		@Override
		public String toString() {
			return Boolean.toString(value);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + (value ? 1231 : 1237);
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			BooleanResource other = (BooleanResource) obj;
			if (value != other.value)
				return false;
			return true;
		}

	}

	/**
	 * Android resource containing color data.
	 */
	public static class ColorResource extends AbstractResource {
		private int a;
		private int r;
		private int g;
		private int b;

		public ColorResource(int a, int r, int g, int b) {
			this.a = a;
			this.r = r;
			this.g = g;
			this.b = b;
		}

		public int getA() {
			return this.a;
		}

		public int getR() {
			return this.r;
		}

		public int getG() {
			return this.g;
		}

		public int getB() {
			return this.b;
		}

		public int getARGB() {
			return (this.a << 24) | (this.r << 16) | (this.g << 8) | b;
		}

		@Override
		public String toString() {
			return String.format("#%02x%02x%02x%02x", a, r, g, b);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + a;
			result = prime * result + b;
			result = prime * result + g;
			result = prime * result + r;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ColorResource other = (ColorResource) obj;
			if (a != other.a)
				return false;
			if (b != other.b)
				return false;
			if (g != other.g)
				return false;
			if (r != other.r)
				return false;
			return true;
		}

	}

	/**
	 * Special Android resource that contains an array of other resources
	 */
	public static class ArrayResource extends AbstractResource {

		private final List arrayElements;

		public ArrayResource() {
			this.arrayElements = new ArrayList<>();
		}

		public ArrayResource(List arrayElements) {
			this.arrayElements = arrayElements;
		}

		public void add(AbstractResource resource) {
			this.arrayElements.add(resource);
		}

		@Override
		public String toString() {
			return this.arrayElements.toString();
		}

		public List getArrayElements() {
			return Collections.unmodifiableList(arrayElements);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((arrayElements == null) ? 0 : arrayElements.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ArrayResource other = (ArrayResource) obj;
			if (arrayElements == null) {
				if (other.arrayElements != null)
					return false;
			} else if (!arrayElements.equals(other.arrayElements))
				return false;
			return true;
		}

	}

	/**
	 * Enumeration containing the types of fractions supported in Android
	 */
	public enum FractionType {
		/**
		 * A basic fraction of the overall size.
		 */
		Fraction,

		/**
		 * A fraction of the parent size.
		 */
		FractionParent
	}

	/**
	 * Android resource containing fraction data (e.g. element width relative to
	 * some other control).
	 */
	public static class FractionResource extends AbstractResource {
		private FractionType type;
		private float value;

		public FractionResource(FractionType type, float value) {
			this.type = type;
			this.value = value;
		}

		public FractionType getType() {
			return this.type;
		}

		public float getValue() {
			return this.value;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = super.hashCode();
			result = prime * result + ((type == null) ? 0 : type.hashCode());
			result = prime * result + Float.floatToIntBits(value);
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (!super.equals(obj))
				return false;
			if (getClass() != obj.getClass())
				return false;
			FractionResource other = (FractionResource) obj;
			if (type != other.type)
				return false;
			if (Float.floatToIntBits(value) != Float.floatToIntBits(other.value))
				return false;
			return true;
		}
	}

	/**
	 * Enumeration containing all dimension units available in Android
	 */
	public enum Dimension {
		PX, DIP, SP, PT, IN, MM
	}

	/**
	 * Android resource containing dimension data like "11pt".
	 */
	public static class DimensionResource extends AbstractResource {
		private int value;
		private Dimension unit;

		public DimensionResource(int value, Dimension unit) {
			this.value = value;
			this.unit = unit;
		}

		DimensionResource(int dimension, int value) {
			this.value = value;
			switch (dimension) {
			case COMPLEX_UNIT_PX:
				this.unit = Dimension.PX;
				break;
			case COMPLEX_UNIT_DIP:
				this.unit = Dimension.DIP;
				break;
			case COMPLEX_UNIT_SP:
				this.unit = Dimension.SP;
				break;
			case COMPLEX_UNIT_PT:
				this.unit = Dimension.PT;
				break;
			case COMPLEX_UNIT_IN:
				this.unit = Dimension.IN;
				break;
			case COMPLEX_UNIT_MM:
				this.unit = Dimension.MM;
				break;
			default:
				throw new RuntimeException("Invalid dimension: " + dimension);
			}
		}

		public int getValue() {
			return this.value;
		}

		public Dimension getUnit() {
			return this.unit;
		}

		@Override
		public String toString() {
			return Integer.toString(this.value) + unit.toString().toLowerCase();
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = super.hashCode();
			result = prime * result + ((unit == null) ? 0 : unit.hashCode());
			result = prime * result + value;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (!super.equals(obj))
				return false;
			if (getClass() != obj.getClass())
				return false;
			DimensionResource other = (DimensionResource) obj;
			if (unit != other.unit)
				return false;
			if (value != other.value)
				return false;
			return true;
		}
	}

	/**
	 * Android resource containing complex map data.
	 */
	public static class ComplexResource extends AbstractResource {
		private final String resType;
		private final Map value;

		public ComplexResource(String resType) {
			this.resType = resType;
			this.value = new HashMap<>();
		}

		public ComplexResource(String resType, Map value) {
			this.resType = resType;
			this.value = value;
		}

		public Map getValue() {
			return this.value;
		}

		public String getResType() {
			return resType;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = super.hashCode();
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (!super.equals(obj))
				return false;
			if (getClass() != obj.getClass())
				return false;
			ComplexResource other = (ComplexResource) obj;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}
	}

	protected static class ResTable_Header {
		ResChunk_Header header = new ResChunk_Header();
		/**
		 * The number of ResTable_package structures
		 */
		int packageCount; // uint32

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((header == null) ? 0 : header.hashCode());
			result = prime * result + packageCount;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Header other = (ResTable_Header) obj;
			if (header == null) {
				if (other.header != null)
					return false;
			} else if (!header.equals(other.header))
				return false;
			if (packageCount != other.packageCount)
				return false;
			return true;
		}
	}

	/**
	 * Header that appears at the front of every data chunk in a resource
	 */
	protected static class ResChunk_Header {
		/**
		 * Type identifier of this chunk. The meaning of this value depends on the
		 * containing class.
		 */
		int type; // uint16
		/**
		 * Size of the chunk header (in bytes). Adding this value to the address of the
		 * chunk allows you to find the associated data (if any).
		 */
		int headerSize; // uint16
		/**
		 * 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. If this value is the same as chunkSize,
		 * there is no data associated with the chunk.
		 */
		int size; // uint32

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + headerSize;
			result = prime * result + size;
			result = prime * result + type;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResChunk_Header other = (ResChunk_Header) obj;
			if (headerSize != other.headerSize)
				return false;
			if (size != other.size)
				return false;
			if (type != other.type)
				return false;
			return true;
		}
	}

	protected static class ResStringPool_Header {
		ResChunk_Header header;

		/**
		 * Number of strings in this pool (number of uint32_t indices that follow in the
		 * data).
		 */
		int stringCount; // uint32
		/**
		 * Number of style span arrays in the pool (number of uint32_t indices follow
		 * the string indices).
		 */
		int styleCount; // uint32
		/**
		 * If set, the string index is sorted by the string values (based on
		 * strcmp16()).
		 */
		boolean flagsSorted; // 1<<0
		/**
		 * String pool is encoded in UTF-8.
		 */
		boolean flagsUTF8; // 1<<8
		/**
		 * Index from the header of the string data.
		 */
		int stringsStart; // uint32
		/**
		 * Index from the header of the style data.
		 */
		int stylesStart; // uint32

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + (flagsSorted ? 1231 : 1237);
			result = prime * result + (flagsUTF8 ? 1231 : 1237);
			result = prime * result + ((header == null) ? 0 : header.hashCode());
			result = prime * result + stringCount;
			result = prime * result + stringsStart;
			result = prime * result + styleCount;
			result = prime * result + stylesStart;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResStringPool_Header other = (ResStringPool_Header) obj;
			if (flagsSorted != other.flagsSorted)
				return false;
			if (flagsUTF8 != other.flagsUTF8)
				return false;
			if (header == null) {
				if (other.header != null)
					return false;
			} else if (!header.equals(other.header))
				return false;
			if (stringCount != other.stringCount)
				return false;
			if (stringsStart != other.stringsStart)
				return false;
			if (styleCount != other.styleCount)
				return false;
			if (stylesStart != other.stylesStart)
				return false;
			return true;
		}
	}

	protected static class ResTable_Package {
		ResChunk_Header header;

		/**
		 * If this is the base package, its ID. Package IDs start at 1 (corresponding to
		 * the value of the package bits in a resource identifier). 0 means that this is
		 * not a base package.
		 */
		int id; // uint32
		/**
		 * Actual name of this package, \0-terminated
		 */
		String name; // char16
		/**
		 * 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).
		 */
		int typeStrings; // uint32
		/**
		 * Last index into typeStrings that is for public use by others.
		 */
		int lastPublicType; // uint32
		/**
		 * 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).
		 */
		int keyStrings; // uint32
		/**
		 * Last index into keyStrings that is for public use by others.
		 */
		int lastPublicKey; // uint32

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((header == null) ? 0 : header.hashCode());
			result = prime * result + id;
			result = prime * result + keyStrings;
			result = prime * result + lastPublicKey;
			result = prime * result + lastPublicType;
			result = prime * result + ((name == null) ? 0 : name.hashCode());
			result = prime * result + typeStrings;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Package other = (ResTable_Package) obj;
			if (header == null) {
				if (other.header != null)
					return false;
			} else if (!header.equals(other.header))
				return false;
			if (id != other.id)
				return false;
			if (keyStrings != other.keyStrings)
				return false;
			if (lastPublicKey != other.lastPublicKey)
				return false;
			if (lastPublicType != other.lastPublicType)
				return false;
			if (name == null) {
				if (other.name != null)
					return false;
			} else if (!name.equals(other.name))
				return false;
			if (typeStrings != other.typeStrings)
				return false;
			return true;
		}
	}

	/**
	 * A specification of the resources defined by a particular type.
	 * 
	 * There should be one of these chunks for each resource type.
	 * 
	 * This structure is followed by an array of integers providing the set of
	 * configuration change flags (ResTable_Config::CONFIG_*) that have multiple
	 * resources for that configuration. In addition, the high bit is set if that
	 * resource has been made public.
	 */
	protected static class ResTable_TypeSpec {
		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.
		 */
		int id; // uint8
		/**
		 * Must be 0
		 */
		int res0; // uint8
		/**
		 * Must be 1.
		 */
		int res1; // uint16
		/**
		 * Number of uint32_t entry configuration masks that follow.
		 */
		int entryCount; // uint32

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + entryCount;
			result = prime * result + ((header == null) ? 0 : header.hashCode());
			result = prime * result + id;
			result = prime * result + res0;
			result = prime * result + res1;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_TypeSpec other = (ResTable_TypeSpec) obj;
			if (entryCount != other.entryCount)
				return false;
			if (header == null) {
				if (other.header != null)
					return false;
			} else if (!header.equals(other.header))
				return false;
			if (id != other.id)
				return false;
			if (res0 != other.res0)
				return false;
			if (res1 != other.res1)
				return false;
			return true;
		}
	}

	/**
	 * A collection of resource entries for a particular resource data type.
	 * Followed by an array of uint32_t defining the resource values, corresponding
	 * to the array of type strings in the ResTable_Package::typeStrings string
	 * block. Each of these hold an index from entriesStart; a value of NO_ENTRY
	 * means that entry is not defined.
	 * 
	 * There may be multiple of these chunks for a particular resource type, supply
	 * different configuration variations for the resource values of that type.
	 *
	 * It would be nice to have an additional ordered index of entries, so we can do
	 * a binary search if trying to find a resource by string name.
	 */
	protected static class ResTable_Type {
		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.
		 */
		int id; // uint8
		/**
		 * Must be 0 or 1 (sparse).
		 */
		int flags; // uint8
		/**
		 * Must be 0.
		 */
		int reserved; // uint16

		/**
		 * Number of uint32_t entry indices that follow.
		 */
		int entryCount; // uint32
		/**
		 * Offset from header where ResTable_Entry data starts.
		 */
		int entriesStart; // uint32
		/**
		 * Configuration this collection of entries is designed for,
		 */
		ResTable_Config config = new ResTable_Config();

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((config == null) ? 0 : config.hashCode());
			result = prime * result + entriesStart;
			result = prime * result + entryCount;
			result = prime * result + ((header == null) ? 0 : header.hashCode());
			result = prime * result + id;
			result = prime * result + flags;
			result = prime * result + reserved;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Type other = (ResTable_Type) obj;
			if (config == null) {
				if (other.config != null)
					return false;
			} else if (!config.equals(other.config))
				return false;
			if (entriesStart != other.entriesStart)
				return false;
			if (entryCount != other.entryCount)
				return false;
			if (header == null) {
				if (other.header != null)
					return false;
			} else if (!header.equals(other.header))
				return false;
			if (id != other.id)
				return false;
			if (flags != other.flags)
				return false;
			if (reserved != other.reserved)
				return false;
			return true;
		}
	}

	/**
	 * Describes a particular resource configuration.
	 */
	public static class ResTable_Config {
		/**
		 * Number of bytes in this structure
		 */
		int size; // uint32
		/**
		 * Mobile country code (from SIM). "0" means any.
		 */
		int mmc; // uint16
		/**
		 * Mobile network code (from SIM). "0" means any.
		 */
		int mnc; // uint16
		/**
		 * \0\0 means "any". Otherwise, en, fr, etc.
		 */
		char[] language = new char[2]; // char[2]
		/**
		 * \0\0 means "any". Otherwise, US, CA, etc.
		 */
		char[] country = new char[2]; // char[2]

		int orientation; // uint8
		int touchscreen; // uint8
		int density; // uint16

		int keyboard; // uint8
		int navigation; // uint8
		int inputFlags; // uint8
		int inputPad0; // uint8

		int screenWidth; // uint16
		int screenHeight; // uint16

		int sdkVersion; // uint16
		int minorVersion; // uint16

		int screenLayout; // uint8
		int uiMode; // uint8
		int smallestScreenWidthDp; // uint16

		int screenWidthDp; // uint16
		int screenHeightDp; // uint16

		char[] localeScript = new char[4]; // char[4]
		char[] localeVariant = new char[8]; // char[8]

		public int getMmc() {
			return mmc;
		}

		public int getMnc() {
			return mnc;
		}

		public String getLanguage() {
			return new String(language);
		}

		public String getCountry() {
			return new String(country);
		}

		public int getOrientation() {
			return orientation;
		}

		public int getTouchscreen() {
			return touchscreen;
		}

		public int getDensity() {
			return density;
		}

		public int getKeyboard() {
			return keyboard;
		}

		public int getNavigation() {
			return navigation;
		}

		public int getInputFlags() {
			return inputFlags;
		}

		public int getInputPad0() {
			return inputPad0;
		}

		public int getScreenWidth() {
			return screenWidth;
		}

		public int getScreenHeight() {
			return screenHeight;
		}

		public int getSdkVersion() {
			return sdkVersion;
		}

		public int getMinorVersion() {
			return minorVersion;
		}

		public int getScreenLayout() {
			return screenLayout;
		}

		public int getUiMode() {
			return uiMode;
		}

		public int getSmallestScreenWidthDp() {
			return smallestScreenWidthDp;
		}

		public int getScreenWidthDp() {
			return screenWidthDp;
		}

		public int getScreenHeightDp() {
			return screenHeightDp;
		}

		public String getLocaleScript() {
			return new String(localeScript);
		}

		public String getLocaleVariant() {
			return new String(localeVariant);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + Arrays.hashCode(country);
			result = prime * result + density;
			result = prime * result + inputFlags;
			result = prime * result + inputPad0;
			result = prime * result + keyboard;
			result = prime * result + Arrays.hashCode(language);
			result = prime * result + Arrays.hashCode(localeScript);
			result = prime * result + Arrays.hashCode(localeVariant);
			result = prime * result + minorVersion;
			result = prime * result + mmc;
			result = prime * result + mnc;
			result = prime * result + navigation;
			result = prime * result + orientation;
			result = prime * result + screenHeight;
			result = prime * result + screenHeightDp;
			result = prime * result + screenLayout;
			result = prime * result + screenWidth;
			result = prime * result + screenWidthDp;
			result = prime * result + sdkVersion;
			result = prime * result + size;
			result = prime * result + smallestScreenWidthDp;
			result = prime * result + touchscreen;
			result = prime * result + uiMode;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Config other = (ResTable_Config) obj;
			if (!Arrays.equals(country, other.country))
				return false;
			if (density != other.density)
				return false;
			if (inputFlags != other.inputFlags)
				return false;
			if (inputPad0 != other.inputPad0)
				return false;
			if (keyboard != other.keyboard)
				return false;
			if (!Arrays.equals(language, other.language))
				return false;
			if (!Arrays.equals(localeScript, other.localeScript))
				return false;
			if (!Arrays.equals(localeVariant, other.localeVariant))
				return false;
			if (minorVersion != other.minorVersion)
				return false;
			if (mmc != other.mmc)
				return false;
			if (mnc != other.mnc)
				return false;
			if (navigation != other.navigation)
				return false;
			if (orientation != other.orientation)
				return false;
			if (screenHeight != other.screenHeight)
				return false;
			if (screenHeightDp != other.screenHeightDp)
				return false;
			if (screenLayout != other.screenLayout)
				return false;
			if (screenWidth != other.screenWidth)
				return false;
			if (screenWidthDp != other.screenWidthDp)
				return false;
			if (sdkVersion != other.sdkVersion)
				return false;
			if (size != other.size)
				return false;
			if (smallestScreenWidthDp != other.smallestScreenWidthDp)
				return false;
			if (touchscreen != other.touchscreen)
				return false;
			if (uiMode != other.uiMode)
				return false;
			return true;
		}

	}

	/**
	 * This is the beginning of information about an entry in the resource table. It
	 * holds the reference to the name of this entry, and is immediately followed by
	 * one of: * A Res_value structure, if FLAG_COMPLEX is -not- set * An array of
	 * ResTable_Map structures, if FLAG_COMPLEX is set. These supply a set of
	 * name/value mappings of data.
	 */
	protected static class ResTable_Entry {
		/**
		 * Number of bytes in this structure
		 */
		int size; // uint16
		boolean flagsComplex;
		boolean flagsPublic;
		/**
		 * Reference into ResTable_Package::KeyStrings identifying this entry.
		 */
		int key;

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + (flagsComplex ? 1231 : 1237);
			result = prime * result + (flagsPublic ? 1231 : 1237);
			result = prime * result + key;
			result = prime * result + size;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Entry other = (ResTable_Entry) obj;
			if (flagsComplex != other.flagsComplex)
				return false;
			if (flagsPublic != other.flagsPublic)
				return false;
			if (key != other.key)
				return false;
			if (size != other.size)
				return false;
			return true;
		}
	}

	/**
	 * Extended form of a ResTable_Entry for map entries, defining a parent map
	 * resource from which to inherit values.
	 */
	protected static class ResTable_Map_Entry extends ResTable_Entry {
		/**
		 * Resource identifier of the parent mapping, or 0 if there is none.
		 */
		int parent;
		/**
		 * Number of name/value pairs that follow for FLAG_COMPLEX.
		 */
		int count; // uint32

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = super.hashCode();
			result = prime * result + count;
			result = prime * result + parent;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (!super.equals(obj))
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Map_Entry other = (ResTable_Map_Entry) obj;
			if (count != other.count)
				return false;
			if (parent != other.parent)
				return false;
			return true;
		}
	}

	/**
	 * Representation of a value in a resource, supplying type information.
	 */
	protected static class Res_Value {
		/**
		 * Number of bytes in this structure.
		 */
		int size; // uint16

		/**
		 * Always set to 0.
		 */
		int res0; // uint8

		int dataType; // uint8
		/**
		 * The data for this type, as interpreted according to dataType.
		 */
		int data; // uint16

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + data;
			result = prime * result + dataType;
			result = prime * result + res0;
			result = prime * result + size;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Res_Value other = (Res_Value) obj;
			if (data != other.data)
				return false;
			if (dataType != other.dataType)
				return false;
			if (res0 != other.res0)
				return false;
			if (size != other.size)
				return false;
			return true;
		}
	}

	/**
	 * A single name/value mapping that is part of a complex resource entry.
	 */
	protected static class ResTable_Map {
		/**
		 * The resource identifier defining this mapping's name. For attribute
		 * resources, 'name' can be one of the following special resource types to
		 * supply meta-data about the attribute; for all other resource types it must be
		 * an attribute resource.
		 */
		int name; // uint32

		/**
		 * This mapping's value.
		 */
		Res_Value value = new Res_Value();

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + name;
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResTable_Map other = (ResTable_Map) obj;
			if (name != other.name)
				return false;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}
	}

	/**
	 * Class containing the data encoded in an Android resource ID
	 */
	public static class ResourceId {
		private int packageId;
		private int typeId;
		private int itemIndex;

		public ResourceId(int packageId, int typeId, int itemIndex) {
			this.packageId = packageId;
			this.typeId = typeId;
			this.itemIndex = itemIndex;
		}

		public int getPackageId() {
			return this.packageId;
		}

		public int getTypeId() {
			return this.typeId;
		}

		public int getItemIndex() {
			return this.itemIndex;
		}

		@Override
		public String toString() {
			return "Package " + this.packageId + ", type " + this.typeId + ", item " + this.itemIndex;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + itemIndex;
			result = prime * result + packageId;
			result = prime * result + typeId;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResourceId other = (ResourceId) obj;
			if (itemIndex != other.itemIndex)
				return false;
			if (packageId != other.packageId)
				return false;
			if (typeId != other.typeId)
				return false;
			return true;
		}
	}

	public ARSCFileParser() {
	}

	/**
	 * Parses the resource definition file in the given APK
	 * 
	 * @param apkFile The APK file in which to parse the resource definition file
	 * @throws IOException Thrown if the given APK file cannot be opened
	 */
	public void parse(String apkFile) throws IOException {
		this.handleAndroidResourceFiles(apkFile, null, new IResourceHandler() {

			@Override
			public void handleResourceFile(String fileName, Set fileNameFilter, InputStream stream) {
				try {
					if (fileName.equals("resources.arsc"))
						parse(stream);
				} catch (IOException ex) {
					logger.error("Could not read resource file", ex);
				}
			}

		});
	}

	public void parse(InputStream stream) throws IOException {
		readResourceHeader(stream);
	}

	private void readResourceHeader(InputStream stream) throws IOException {
		final int BLOCK_SIZE = 2048;

		ResTable_Header resourceHeader = new ResTable_Header();
		readChunkHeader(stream, resourceHeader.header);
		resourceHeader.packageCount = readUInt32(stream);
		logger.debug("Package Groups ({})", resourceHeader.packageCount);

		// Do we have any packages to read?
		int remainingSize = resourceHeader.header.size - resourceHeader.header.headerSize;
		if (remainingSize <= 0)
			return;

		// Load the remaining data
		byte[] remainingData = new byte[remainingSize];

		int totalBytesRead = 0;
		while (totalBytesRead < remainingSize) {
			byte[] block = new byte[Math.min(BLOCK_SIZE, remainingSize - totalBytesRead)];
			int bytesRead = stream.read(block);
			if (bytesRead < 0) {
				logger.error("Could not read block from resource file");
				return;
			}
			System.arraycopy(block, 0, remainingData, totalBytesRead, bytesRead);
			totalBytesRead += bytesRead;
		}
		int offset = 0;
		int beforeBlock = 0;

		// Read the next chunk
		int packageCtr = 0;
		Map keyStrings = new HashMap<>();
		Map typeStrings = new HashMap<>();
		while (offset < remainingData.length - 1) {
			beforeBlock = offset;
			ResChunk_Header nextChunkHeader = new ResChunk_Header();
			offset = readChunkHeader(nextChunkHeader, remainingData, offset);
			if (nextChunkHeader.type == RES_STRING_POOL_TYPE) {
				// Read the string pool header
				ResStringPool_Header stringPoolHeader = new ResStringPool_Header();
				stringPoolHeader.header = nextChunkHeader;
				offset = parseStringPoolHeader(stringPoolHeader, remainingData, offset);

				// Read the string data
				offset = readStringTable(remainingData, offset, beforeBlock, stringPoolHeader, this.stringTable);
				assert this.stringTable.size() == stringPoolHeader.stringCount;
			} else if (nextChunkHeader.type == RES_TABLE_PACKAGE_TYPE) {
				// Read the package header
				ResTable_Package packageTable = new ResTable_Package();
				packageTable.header = nextChunkHeader;
				offset = parsePackageTable(packageTable, remainingData, offset);

				logger.debug("\tPackage {} id={} name={}", packageCtr, packageTable.id, packageTable.name);

				// Record the end of the object to know then to stop looking for
				// internal records
				int endOfRecord = beforeBlock + nextChunkHeader.size;

				// Create the data object and set the base data
				ResPackage resPackage = new ResPackage();
				this.packages.add(resPackage);
				resPackage.packageId = packageTable.id;
				resPackage.packageName = packageTable.name;

				{
					// Find the type strings
					int typeStringsOffset = beforeBlock + packageTable.typeStrings;
					int beforeStringBlock = typeStringsOffset;
					ResChunk_Header typePoolHeader = new ResChunk_Header();
					typeStringsOffset = readChunkHeader(typePoolHeader, remainingData, typeStringsOffset);
					if (typePoolHeader.type != RES_STRING_POOL_TYPE)
						throw new RuntimeException("Unexpected block type for package type strings");

					ResStringPool_Header typePool = new ResStringPool_Header();
					typePool.header = typePoolHeader;
					typeStringsOffset = parseStringPoolHeader(typePool, remainingData, typeStringsOffset);

					// Attention: String offset starts at the beginning of the
					// StringPool
					// block, not the at the beginning of the Package block
					// referring to it.
					readStringTable(remainingData, typeStringsOffset, beforeStringBlock, typePool, typeStrings);

					// Find the key strings
					int keyStringsOffset = beforeBlock + packageTable.keyStrings;
					beforeStringBlock = keyStringsOffset;
					ResChunk_Header keyPoolHeader = new ResChunk_Header();
					keyStringsOffset = readChunkHeader(keyPoolHeader, remainingData, keyStringsOffset);
					if (keyPoolHeader.type != RES_STRING_POOL_TYPE)
						throw new RuntimeException("Unexpected block type for package key strings");

					ResStringPool_Header keyPool = new ResStringPool_Header();
					keyPool.header = keyPoolHeader;
					keyStringsOffset = parseStringPoolHeader(keyPool, remainingData, keyStringsOffset);

					// Attention: String offset starts at the beginning of the
					// StringPool
					// block, not the at the beginning of the Package block
					// referring to it.
					readStringTable(remainingData, keyStringsOffset, beforeStringBlock, keyPool, keyStrings);

					// Jump to the end of the string block
					offset = beforeStringBlock + keyPoolHeader.size;
				}

				while (offset < endOfRecord) {
					// Read the next inner block
					ResChunk_Header innerHeader = new ResChunk_Header();
					int beforeInnerBlock = offset;
					offset = readChunkHeader(innerHeader, remainingData, offset);
					if (innerHeader.type == RES_TABLE_TYPE_SPEC_TYPE) {
						// Type specification block
						ResTable_TypeSpec typeSpecTable = new ResTable_TypeSpec();
						typeSpecTable.header = innerHeader;
						offset = readTypeSpecTable(typeSpecTable, remainingData, offset);
						assert offset == beforeInnerBlock + typeSpecTable.header.headerSize;

						// Create the data object
						ResType tp = new ResType();
						tp.id = typeSpecTable.id;
						tp.typeName = typeStrings.get(typeSpecTable.id - 1);
						resPackage.types.add(tp);

						// Normally, we also have a set of configurations
						// following, but
						// we don't implement that at the moment
					} else if (innerHeader.type == RES_TABLE_TYPE_TYPE) {
						// Type resource entries. The id field maps to the type
						// for which we have a record. We create a mapping from
						// type IDs to declare resources.
						ResTable_Type typeTable = new ResTable_Type();
						typeTable.header = innerHeader;
						offset = readTypeTable(typeTable, remainingData, offset);
						assert offset == beforeInnerBlock + typeTable.header.headerSize;

						// Create the data object
						ResType resType = null;
						for (ResType rt : resPackage.types)
							if (rt.id == typeTable.id) {
								resType = rt;
								break;
							}
						if (resType == null)
							throw new RuntimeException("Reference to undeclared type found");
						ResConfig config = new ResConfig();
						config.config = typeTable.config;
						resType.configurations.add(config);

						boolean isSparse = (typeTable.flags == 1);

						// Read the table entries
						for (int i = 0; i < typeTable.entryCount; i++) {
							int entryOffset;
							int resourceIdx;
							if (isSparse) {
								// read inner struct of ResTable_sparseTypeEntry
								resourceIdx = readUInt16(remainingData, offset);
								offset += 2;
								// The offset from ResTable_type::entriesStart, divided by 4.
								entryOffset = readUInt16(remainingData, offset);
								entryOffset *= 4;
								offset += 2;
							} else {
								resourceIdx = i;
								entryOffset = readUInt32(remainingData, offset);
								offset += 4;
							}
							if (entryOffset == 0xFFFFFFFF) { // NoEntry
								continue;
							}
							entryOffset += beforeInnerBlock + typeTable.entriesStart;
							ResTable_Entry entry = readEntryTable(remainingData, entryOffset);
							entryOffset += entry.size;

							AbstractResource res;

							// If this is a simple entry, the data structure is
							// followed by RES_VALUE
							if (entry.flagsComplex) {
								ComplexResource cmpRes = new ComplexResource(resType.typeName);
								res = cmpRes;

								for (int j = 0; j < ((ResTable_Map_Entry) entry).count; j++) {
									ResTable_Map map = new ResTable_Map();
									entryOffset = readComplexValue(map, remainingData, entryOffset);

									final String mapName = Integer.toString(map.name);
									AbstractResource value = parseValue(map.value);

									// If we are dealing with an array, we put it into a special array container
									if (resType.typeName != null && resType.typeName.equals("array")
											&& value instanceof StringResource) {

										AbstractResource existingResource = cmpRes.value.get(mapName);
										if (existingResource == null) {
											existingResource = new ArrayResource();
											cmpRes.value.put(mapName, existingResource);
										}

										// We silently ignore inconsistencies at thze moment
										if (existingResource instanceof ArrayResource)
											((ArrayResource) existingResource).add(value);
									} else {
										cmpRes.value.put(mapName, value);
									}
								}
							} else {
								Res_Value val = new Res_Value();
								entryOffset = readValue(val, remainingData, entryOffset);
								res = parseValue(val);
								if (res == null) {
									logger.error(
											String.format("Could not parse resource %s of type 0x%x, skipping entry",
													keyStrings.get(entry.key), val.dataType));
									continue;
								}
							}

							// Create the data object. For finding the correct ID, we
							// must check whether the entry is really new - if so, it
							// gets a new ID, otherwise, we reuse the old one
							if (keyStrings.containsKey(entry.key)) {
								res.resourceName = keyStrings.get(entry.key);
							} else {
								res.resourceName = "";
							}

							if (res.resourceID <= 0) {
								res.resourceID = (packageTable.id << 24) + (typeTable.id << 16) + resourceIdx;
							}
							config.resources.add(res);
						}
					}
					offset = beforeInnerBlock + innerHeader.size;
				}

				// Create the data objects for the types in the package
				if (logger.isTraceEnabled()) {
					for (ResType resType : resPackage.types) {
						logger.trace("\t\tType {} ({}), configCount={}, entryCount={}", resType.typeName,
								resType.id - 1, resType.configurations.size(),
								resType.configurations.size() > 0 ? resType.configurations.get(0).resources.size() : 0);
						for (ResConfig resConfig : resType.configurations) {
							logger.trace("\t\t\tconfig");
							for (AbstractResource res : resConfig.resources)
								logger.trace("\t\t\t\tresource {}: {}", Integer.toHexString(res.resourceID),
										res.resourceName);
						}
					}
				}
				packageCtr++;
			}

			// Skip the block
			offset = beforeBlock + nextChunkHeader.size;
			remainingSize -= nextChunkHeader.size;
		}
	}

	/**
	 * Checks whether the given complex map entry is one of the well-known
	 * attributes.
	 * 
	 * @param map The map entry to check
	 * @return True if the given entry is one of the well-known attributes,
	 *         otherwise false.
	 */
	protected boolean isAttribute(ResTable_Map map) {
		return map.name == ATTR_TYPE || map.name == ATTR_MIN || map.name == ATTR_MAX || map.name == ATTR_L10N
				|| map.name == ATTR_OTHER || map.name == ATTR_ZERO || map.name == ATTR_ONE || map.name == ATTR_TWO
				|| map.name == ATTR_FEW || map.name == ATTR_MANY;
	}

	/**
	 * Taken from
	 * https://github.com/menethil/ApkTool/blob/master/src/android/util/TypedValue.java
	 * 
	 * @param complex
	 * @return
	 */
	protected static float complexToFloat(int complex) {
		return (complex & (COMPLEX_MANTISSA_MASK << COMPLEX_MANTISSA_SHIFT))
				* RADIX_MULTS[(complex >> COMPLEX_RADIX_SHIFT) & COMPLEX_RADIX_MASK];
	}

	private AbstractResource parseValue(Res_Value val) {
		AbstractResource res;
		switch (val.dataType) {
		case TYPE_NULL:
			res = new NullResource();
			break;
		case TYPE_REFERENCE:
			res = new ReferenceResource(val.data);
			break;
		case TYPE_ATTRIBUTE:
			res = new AttributeResource(val.data);
			break;
		case TYPE_STRING:
			res = new StringResource(stringTable.get(val.data));
			break;
		case TYPE_INT_DEC:
		case TYPE_INT_HEX:
			res = new IntegerResource(val.data);
			break;
		case TYPE_INT_BOOLEAN:
			res = new BooleanResource(val.data);
			break;
		case TYPE_INT_COLOR_ARGB8:
		case TYPE_INT_COLOR_RGB8:
		case TYPE_INT_COLOR_ARGB4:
		case TYPE_INT_COLOR_RGB4:
			res = new ColorResource(val.data & 0xFF000000 >> 3 * 8, val.data & 0x00FF0000 >> 2 * 8,
					val.data & 0x0000FF00 >> 8, val.data & 0x000000FF);
			break;
		case TYPE_DIMENSION:
			res = new DimensionResource(val.data & COMPLEX_UNIT_MASK, val.data >> COMPLEX_UNIT_SHIFT);
			break;
		case TYPE_FLOAT:
			res = new FloatResource(Float.intBitsToFloat(val.data));
			break;
		case TYPE_FRACTION:
			int fracType = (val.data >> COMPLEX_UNIT_SHIFT) & COMPLEX_UNIT_MASK;
			float data = complexToFloat(val.data);
			if (fracType == COMPLEX_UNIT_FRACTION)
				res = new FractionResource(FractionType.Fraction, data);
			else
				res = new FractionResource(FractionType.FractionParent, data);
			break;
		default:
			logger.warn(String.format("Unsupported data type: 0x%x", val.dataType));
			return null;
		}
		return res;
	}

	private int readComplexValue(ResTable_Map map, byte[] remainingData, int offset) throws IOException {
		map.name = readUInt32(remainingData, offset);
		offset += 4;

		return readValue(map.value, remainingData, offset);
	}

	private int readValue(Res_Value val, byte[] remainingData, int offset) throws IOException {
		int initialOffset = offset;

		val.size = readUInt16(remainingData, offset);
		offset += 2;
		if (val.size > 8) // This should always be 8. Check to not fail on
							// broken resources in apps
			return 0;

		val.res0 = readUInt8(remainingData, offset);
		if (val.res0 != 0) {
			raiseFormatViolationIssue("File format violation: res0 is not zero", offset);
		}
		offset += 1;

		val.dataType = readUInt8(remainingData, offset);
		offset += 1;

		val.data = readUInt32(remainingData, offset);
		offset += 4;

		assert offset == initialOffset + val.size;
		return offset;
	}

	private ResTable_Entry readEntryTable(byte[] data, int offset) throws IOException {
		// The exact type of entry depends on the size
		int size = readUInt16(data, offset);
		offset += 2;
		ResTable_Entry entry;
		if (size == 0x8)
			entry = new ResTable_Entry();
		else if (size == 0x10)
			entry = new ResTable_Map_Entry();
		else
			throw new RuntimeException("Unknown entry type of size 0x" + Integer.toHexString(size));
		entry.size = size;

		int flags = readUInt16(data, offset);
		offset += 2;
		entry.flagsComplex = (flags & FLAG_COMPLEX) == FLAG_COMPLEX;
		entry.flagsPublic = (flags & FLAG_PUBLIC) == FLAG_PUBLIC;

		entry.key = readUInt32(data, offset);
		offset += 4;

		if (entry instanceof ResTable_Map_Entry) {
			ResTable_Map_Entry mapEntry = (ResTable_Map_Entry) entry;
			mapEntry.parent = readUInt32(data, offset);
			offset += 4;
			mapEntry.count = readUInt32(data, offset);
			offset += 4;
		}

		return entry;
	}

	/**
	 * Parse data struct ResTable_type as defined in AOSP
	 * https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h
	 * 
	 * Also parses subsequent config table.
	 * 
	 * @param typeTable
	 * @param data
	 * @param offset
	 * @return
	 * @throws IOException
	 */
	private int readTypeTable(ResTable_Type typeTable, byte[] data, int offset) throws IOException {
		typeTable.id = readUInt8(data, offset);
		if (typeTable.id == 0) {
			raiseFormatViolationIssue("File format violation in type table: id is zero", offset);
		}
		offset += 1;

		typeTable.flags = readUInt8(data, offset);
		if (typeTable.flags != 0 && typeTable.flags != 1) {
			raiseFormatViolationIssue("File format violation in type table: flags is not zero or one", offset);
		}
		offset += 1;

		typeTable.reserved = readUInt16(data, offset);
		if (typeTable.reserved != 0) {
			raiseFormatViolationIssue("File format violation in type table: reserved is not zero", offset);
		}
		offset += 2;

		typeTable.entryCount = readUInt32(data, offset);
		offset += 4;

		typeTable.entriesStart = readUInt32(data, offset);
		offset += 4;

		return readConfigTable(typeTable.config, data, offset);
	}

	private int readConfigTable(ResTable_Config config, byte[] data, int offset) throws IOException {
		config.size = readUInt32(data, offset);
		offset += 4;

		config.mmc = readUInt16(data, offset);
		offset += 2;

		config.mnc = readUInt16(data, offset);
		offset += 2;

		config.language[0] = (char) data[offset];
		config.language[1] = (char) data[offset + 1];
		offset += 2;

		config.country[0] = (char) data[offset];
		config.country[1] = (char) data[offset + 1];
		offset += 2;

		config.orientation = readUInt8(data, offset);
		offset += 1;
		config.touchscreen = readUInt8(data, offset);
		offset += 1;
		config.density = readUInt16(data, offset);
		offset += 2;

		config.keyboard = readUInt8(data, offset);
		offset += 1;
		config.navigation = readUInt8(data, offset);
		offset += 1;
		config.inputFlags = readUInt8(data, offset);
		offset += 1;
		config.inputPad0 = readUInt8(data, offset);
		offset += 1;

		config.screenWidth = readUInt16(data, offset);
		offset += 2;
		config.screenHeight = readUInt16(data, offset);
		offset += 2;

		config.sdkVersion = readUInt16(data, offset);
		offset += 2;
		config.minorVersion = readUInt16(data, offset);
		offset += 2;
		if (config.size <= 28)
			return offset;

		config.screenLayout = readUInt8(data, offset);
		offset += 1;
		config.uiMode = readUInt8(data, offset);
		offset += 1;
		config.smallestScreenWidthDp = readUInt16(data, offset);
		offset += 2;
		if (config.size <= 32)
			return offset;

		config.screenWidthDp = readUInt16(data, offset);
		offset += 2;
		config.screenHeightDp = readUInt16(data, offset);
		offset += 2;
		if (config.size <= 36)
			return offset;

		for (int i = 0; i < 4; i++)
			config.localeScript[i] = (char) data[offset + i];
		offset += 4;
		if (config.size <= 40)
			return offset;

		for (int i = 0; i < 8; i++)
			config.localeVariant[i] = (char) data[offset + i];
		offset += 8;
		if (config.size <= 48)
			return offset;

		// Read in the remaining bytes. If they're all zero, we're fine.
		// Otherwise, we print a warning.
		int remainingSize = config.size - 48;
		if (remainingSize > 0) {
			byte[] remainingBytes = new byte[remainingSize];
			System.arraycopy(data, offset, remainingBytes, 0, remainingSize);
			BigInteger remainingData = new BigInteger(1, remainingBytes);
			if (!(remainingData.equals(BigInteger.ZERO))) {
				logger.debug("Excessive {} non-null bytes in ResTable_Config ignored", remainingSize);
				if (logger.isTraceEnabled()) {
					logger.trace("remaining data: 0x" + remainingData.toString(16));
				}
			}
			offset += remainingSize;
		}

		return offset;
	}

	/**
	 * Parse data struct ResTable_typeSpec as defined in AOSP
	 * https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h
	 * 
	 * @param typeSpecTable
	 * @param data
	 * @param offset
	 * @return
	 * @throws IOException
	 */
	private int readTypeSpecTable(ResTable_TypeSpec typeSpecTable, byte[] data, int offset) throws IOException {
		typeSpecTable.id = readUInt8(data, offset);
		if (typeSpecTable.id == 0) {
			raiseFormatViolationIssue("File format violation in type spec table: id is zero", offset);
		}
		offset += 1;

		typeSpecTable.res0 = readUInt8(data, offset);
		if (typeSpecTable.res0 != 0) {
			raiseFormatViolationIssue("File format violation in type spec table: res0 is not zero", offset);
		}
		offset += 1;

		typeSpecTable.res1 = readUInt16(data, offset);
		if (typeSpecTable.res1 != 0) {
			raiseFormatViolationIssue("File format violation in type spec table: res1 is not zero", offset);
		}
		offset += 2;

		typeSpecTable.entryCount = readUInt32(data, offset);
		offset += 4;

		return offset;
	}

	private int readStringTable(byte[] remainingData, int offset, int blockStart, ResStringPool_Header stringPoolHeader,
			Map stringList) throws IOException {
		// Read the strings
		for (int i = 0; i < stringPoolHeader.stringCount; i++) {
			int stringIdx = readUInt32(remainingData, offset);
			offset += 4;

			// Offset begins at block start
			stringIdx += stringPoolHeader.stringsStart + blockStart;
			String str = "";
			if (stringPoolHeader.flagsUTF8)
				str = readStringUTF8(remainingData, stringIdx).trim();
			else
				str = readString(remainingData, stringIdx).trim();
			stringList.put(i, str);
		}
		return offset;
	}

	private int parsePackageTable(ResTable_Package packageTable, byte[] data, int offset) throws IOException {
		packageTable.id = readUInt32(data, offset);
		offset += 4;

		// Read the package name, zero-terminated string
		StringBuilder bld = new StringBuilder();
		for (int i = 0; i < 128; i++) {
			int curChar = readUInt16(data, offset);
			bld.append((char) curChar);
			offset += 2;
		}
		packageTable.name = bld.toString().trim();

		packageTable.typeStrings = readUInt32(data, offset);
		offset += 4;

		packageTable.lastPublicType = readUInt32(data, offset);
		offset += 4;

		packageTable.keyStrings = readUInt32(data, offset);
		offset += 4;

		packageTable.lastPublicKey = readUInt32(data, offset);
		offset += 4;

		return offset;
	}

	private String readString(byte[] remainingData, int stringIdx) throws IOException {
		int strLen = readUInt16(remainingData, stringIdx);
		if (strLen == 0)
			return "";
		stringIdx += 2;
		byte[] str = new byte[strLen * 2];
		System.arraycopy(remainingData, stringIdx, str, 0, strLen * 2);
		return new String(remainingData, stringIdx, strLen * 2, StandardCharsets.UTF_16LE);
	}

	private String readStringUTF8(byte[] remainingData, int stringIdx) throws IOException {
		// skip the length, will usually be 0x1A1A
		// int strLen = readUInt16(remainingData, stringIdx);
		// the length here is somehow weird
		int strLen = readUInt8(remainingData, stringIdx + 1);
		stringIdx += 2;
		String str = new String(remainingData, stringIdx, strLen, StandardCharsets.UTF_8);
		return str;
	}

	private int parseStringPoolHeader(ResStringPool_Header stringPoolHeader, byte[] data, int offset)
			throws IOException {
		stringPoolHeader.stringCount = readUInt32(data, offset);
		stringPoolHeader.styleCount = readUInt32(data, offset + 4);

		int flags = readUInt32(data, offset + 8);
		stringPoolHeader.flagsSorted = (flags & SORTED_FLAG) == SORTED_FLAG;
		stringPoolHeader.flagsUTF8 = (flags & UTF8_FLAG) == UTF8_FLAG;

		stringPoolHeader.stringsStart = readUInt32(data, offset + 12);
		stringPoolHeader.stylesStart = readUInt32(data, offset + 16);
		return offset + 20;
	}

	/**
	 * Reads a chunk header from the input stream and stores the data in the given
	 * object.
	 * 
	 * @param stream          The stream from which to read the chunk header
	 * @param nextChunkHeader The data object in which to put the chunk header
	 * @throws IOException Thrown if an error occurs during read
	 */
	private void readChunkHeader(InputStream stream, ResChunk_Header nextChunkHeader) throws IOException {
		byte[] header = new byte[8];
		stream.read(header);
		readChunkHeader(nextChunkHeader, header, 0);
	}

	/**
	 * Reads a chunk header from the input stream and stores the data in the given
	 * object.
	 * 
	 * @param nextChunkHeader The data object in which to put the chunk header
	 * @param data            The data array containing the structure
	 * @param offset          The offset from which to start reading
	 * @throws IOException Thrown if an error occurs during read
	 */
	private int readChunkHeader(ResChunk_Header nextChunkHeader, byte[] data, int offset) throws IOException {
		nextChunkHeader.type = readUInt16(data, offset);
		offset += 2;

		nextChunkHeader.headerSize = readUInt16(data, offset);
		offset += 2;

		nextChunkHeader.size = readUInt32(data, offset);
		offset += 4;

		return offset;
	}

	private int readUInt8(byte[] uint16, int offset) throws IOException {
		int b0 = uint16[0 + offset] & 0x000000FF;
		return b0;
	}

	private int readUInt16(byte[] uint16, int offset) throws IOException {
		int b0 = uint16[0 + offset] & 0x000000FF;
		int b1 = uint16[1 + offset] & 0x000000FF;
		return (b1 << 8) + b0;
	}

	private int readUInt32(InputStream stream) throws IOException {
		byte[] uint32 = new byte[4];
		stream.read(uint32);
		return readUInt32(uint32, 0);
	}

	private int readUInt32(byte[] uint32, int offset) throws IOException {
		int b0 = uint32[0 + offset] & 0x000000FF;
		int b1 = uint32[1 + offset] & 0x000000FF;
		int b2 = uint32[2 + offset] & 0x000000FF;
		int b3 = uint32[3 + offset] & 0x000000FF;
		return (Math.abs(b3) << 24) + (Math.abs(b2) << 16) + (Math.abs(b1) << 8) + Math.abs(b0);
	}

	public Map getGlobalStringPool() {
		return this.stringTable;
	}

	public List getPackages() {
		return this.packages;
	}

	public ResPackage getPackage(int pkgID, String pkgName) {
		return this.packages.stream().filter(p -> p.packageId == pkgID && p.packageName.equals(pkgName)).findFirst()
				.orElse(null);
	}

	/**
	 * Finds the resource with the given Android resource ID. This method is
	 * configuration-agnostic and simply returns the first match it finds.
	 * 
	 * @param resourceId The Android resource ID for which to the find the resource
	 *                   object
	 * @return The resource object with the given Android resource ID if it has been
	 *         found, otherwise null.
	 */
	public AbstractResource findResource(int resourceId) {
		ResourceId id = parseResourceId(resourceId);
		for (ResPackage resPackage : this.packages)
			if (resPackage.packageId == id.packageId) {
				for (ResType resType : resPackage.types)
					if (resType.id == id.typeId) {
						return resType.getFirstResource(resourceId);
					}
				break;
			}
		return null;
	}

	/**
	 * Finds all resources with the given Android resource ID. This method returns
	 * all matching resources, regardless of their respective configuration.
	 * 
	 * @param resourceId The Android resource ID for which to the find the resource
	 *                   object
	 * @return The resource object with the given Android resource ID if it has been
	 *         found, otherwise null.
	 */
	public List findAllResources(int resourceId) {
		List resourceList = new ArrayList<>();
		ResourceId id = parseResourceId(resourceId);
		for (ResPackage resPackage : this.packages)
			if (resPackage.packageId == id.packageId) {
				for (ResType resType : resPackage.types)
					if (resType.id == id.typeId) {
						resourceList.addAll(resType.getAllResources(resourceId));
					}
				break;
			}
		return resourceList;
	}

	/**
	 * Gets the resource type with the package and type specified in the given
	 * resource ID
	 * 
	 * @param resourceId The resource ID
	 * @return The resource type with the given type and package ID, or null if no
	 *         such type exists
	 */
	public ResType findResourceType(int resourceId) {
		ResourceId id = parseResourceId(resourceId);
		for (ResPackage resPackage : this.packages)
			if (resPackage.packageId == id.packageId) {
				for (ResType resType : resPackage.types)
					if (resType.id == id.typeId) {
						return resType;
					}
				break;
			}
		return null;
	}

	/**
	 * Parses an Android resource ID into its components
	 * 
	 * @param resourceId The numeric resource ID to parse
	 * @return The data contained in the given Android resource ID
	 */
	public ResourceId parseResourceId(int resourceId) {
		return new ResourceId((resourceId & 0xFF000000) >> 24, (resourceId & 0x00FF0000) >> 16,
				resourceId & 0x0000FFFF);
	}

	/**
	 * Gets the resource with the given name and type
	 * 
	 * @param type         The type of the resource to retrieve, e.g., "string"
	 * @param resourceName The name of the resource to retrieve
	 * @return The resource with the given name and type if such a resource exists,
	 *         null otherwise
	 */
	public AbstractResource findResourceByName(String type, String resourceName) {
		for (ResPackage resPackage : this.packages) {
			ResType resType = resPackage.getResourceType(type);
			if (resType != null) {
				for (AbstractResource res : resType.getAllResources()) {
					if (res.resourceName.equals(resourceName))
						return res;
				}
			}
		}
		return null;
	}

	/**
	 * Convenience method for loading a string resource with a given name
	 * 
	 * @param resourceName The name of the resource to locate
	 * @return The string contents of the resource with the given name, if such a
	 *         resource exists and is of type "string", null otherwise
	 */
	public String findStringResource(String resourceName) {
		AbstractResource res = findResourceByName("string", resourceName);
		if (res instanceof StringResource) {
			StringResource stringRes = (StringResource) res;
			return stringRes.value;
		}
		return null;
	}

	/**
	 * Finds all resources that have the given type
	 * 
	 * @param type The resource type to look for
	 * @return All resources of the given type
	 */
	public List findResourcesByType(String type) {
		List resourceList = new ArrayList<>();
		for (ResPackage resPackage : this.packages) {
			ResType resType = resPackage.getResourceType(type);
			if (resType != null)
				resourceList.addAll(resType.getAllResources());
		}
		return resourceList;
	}

	/**
	 * Creates a new instance of the {@link ARSCFileParser} class and parses the
	 * Android resource database in the given APK file
	 * 
	 * @param apkFile The APK file in which to parse the resource database
	 * @return The new {@link ARSCFileParser} instance, or null if the
	 *         file could not be read
	 * @throws IOException
	 */
	public static ARSCFileParser getInstance(File apkFile) throws IOException {
		ARSCFileParser parser = new ARSCFileParser();
		try (ApkHandler handler = new ApkHandler(apkFile); InputStream is = handler.getInputStream("resources.arsc")) {
			if (is == null)
				return null;
			parser.parse(is);
		}
		return parser;
	}

	/**
	 * Adds all resources loaded from another {@link ARSCFileParser} to this data
	 * object
	 * 
	 * @param otherParser The other parser
	 */
	public void addAll(ARSCFileParser otherParser) {
		// Merge the packages
		for (ResPackage pkg : otherParser.packages) {
			ResPackage existingPackage = getPackage(pkg.packageId, pkg.packageName);
			if (existingPackage == null)
				packages.add(pkg);
			else
				existingPackage.addAll(pkg);
		}

		// Merge the string table
		stringTable.putAll(otherParser.stringTable);
	}

	protected void raiseFormatViolationIssue(String message, int offset) {
		if (STRICT_MODE) {
			throw new RuntimeException(String.format("%s offset=0x%x", message, offset));
		}
		logger.error("{} offset=0x{}", message, Integer.toHexString(offset));
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy