package com.wj.dexknife.shell.apkparser;
import com.wj.dexknife.shell.apkparser.bean.ApkMeta;
import com.wj.dexknife.shell.apkparser.bean.ApkSignStatus;
import com.wj.dexknife.shell.apkparser.bean.CertificateMeta;
import com.wj.dexknife.shell.apkparser.bean.DexClass;
import com.wj.dexknife.shell.apkparser.bean.Icon;
import com.wj.dexknife.shell.apkparser.exception.ParserException;
import com.wj.dexknife.shell.apkparser.parser.ApkMetaTranslator;
import com.wj.dexknife.shell.apkparser.parser.BinaryXmlParser;
import com.wj.dexknife.shell.apkparser.parser.CertificateParser;
import com.wj.dexknife.shell.apkparser.parser.CompositeXmlStreamer;
import com.wj.dexknife.shell.apkparser.parser.DexParser;
import com.wj.dexknife.shell.apkparser.parser.ResourceTableParser;
import com.wj.dexknife.shell.apkparser.parser.XmlStreamer;
import com.wj.dexknife.shell.apkparser.parser.XmlTranslator;
import com.wj.dexknife.shell.apkparser.struct.AndroidConstants;
import com.wj.dexknife.shell.apkparser.struct.resource.ResourceTable;
import com.wj.dexknife.shell.apkparser.utils.Utils;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* ApkParser and result holder.
* This class is not thread-safe.
*
* @author dongliu
*/
public class ApkParser implements Closeable {
private DexClass[] dexClasses;
private ResourceTable resourceTable;
private String manifestXml;
private ApkMeta apkMeta;
private Set<Locale> locales;
private List<CertificateMeta> certificateMetaList;
private final ZipFile zf;
private File apkFile;
private static final Locale DEFAULT_LOCALE = Locale.US;
/**
* default use empty locale
*/
private Locale preferredLocale = DEFAULT_LOCALE;
public ApkParser(File apkFile) throws IOException {
this.apkFile = apkFile;
// create zip file cost time, use one zip file for apk parser life cycle
this.zf = new ZipFile(apkFile);
}
public ApkParser(String filePath) throws IOException {
this(new File(filePath));
}
/**
* return decoded AndroidManifest.xml
*
* @return decoded AndroidManifest.xml
*/
public String getManifestXml() throws IOException {
if (this.manifestXml == null) {
parseManifestXml();
}
return this.manifestXml;
}
/**
* return decoded AndroidManifest.xml
*
* @return decoded AndroidManifest.xml
*/
public ApkMeta getApkMeta() throws IOException {
if (this.apkMeta == null) {
parseApkMeta();
}
return this.apkMeta;
}
/**
* get locales supported from resource file
*
* @return decoded AndroidManifest.xml
* @throws IOException
*/
public Set<Locale> getLocales() throws IOException {
if (this.locales == null) {
parseResourceTable();
}
return this.locales;
}
/**
* get the apk's certificates.
*/
public List<CertificateMeta> getCertificateMetaList() throws IOException,
CertificateException {
if (this.certificateMetaList == null) {
parseCertificate();
}
return this.certificateMetaList;
}
private void parseCertificate() throws IOException, CertificateException {
ZipEntry entry = null;
Enumeration<? extends ZipEntry> enu = zf.entries();
while (enu.hasMoreElements()) {
ZipEntry ne = enu.nextElement();
if (ne.isDirectory()) {
continue;
}
if (ne.getName().toUpperCase().endsWith(".RSA")
|| ne.getName().toUpperCase().endsWith(".DSA")) {
entry = ne;
break;
}
}
if (entry == null) {
throw new ParserException("ApkParser certificate not found");
}
try (InputStream in = zf.getInputStream(entry)) {
CertificateParser parser = new CertificateParser(in);
parser.parse();
this.certificateMetaList = parser.getCertificateMetas();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* parse manifest.xml, get apkMeta.
*
* @throws IOException
*/
private void parseApkMeta() throws IOException {
if (this.manifestXml == null) {
parseManifestXml();
}
}
/**
* parse manifest.xml, get manifestXml as xml text.
*
* @throws IOException
*/
private void parseManifestXml() throws IOException {
XmlTranslator xmlTranslator = new XmlTranslator();
ApkMetaTranslator translator = new ApkMetaTranslator();
XmlStreamer xmlStreamer = new CompositeXmlStreamer(xmlTranslator, translator);
transBinaryXml(AndroidConstants.MANIFEST_FILE, xmlStreamer);
this.manifestXml = xmlTranslator.getXml();
if (this.manifestXml == null) {
throw new ParserException("manifest xml not exists");
}
this.apkMeta = translator.getApkMeta();
}
/**
* trans binary xml file to text xml file.
*
* @param path the xml file path in apk file
* @return the text. null if file not exists
* @throws IOException
*/
public String transBinaryXml(String path) throws IOException {
ZipEntry entry = Utils.getEntry(zf, path);
if (entry == null) {
return null;
}
if (this.resourceTable == null) {
parseResourceTable();
}
XmlTranslator xmlTranslator = new XmlTranslator();
transBinaryXml(path, xmlTranslator);
return xmlTranslator.getXml();
}
/**
* get the apk icon file as bytes.
*
* @return the apk icon data,null if icon not found
* @throws IOException
*/
public Icon getIconFile() throws IOException {
ApkMeta apkMeta = getApkMeta();
String iconPath = apkMeta.getIcon();
if (iconPath == null) {
return null;
}
return new Icon(iconPath, getFileData(iconPath));
}
private void transBinaryXml(String path, XmlStreamer xmlStreamer) throws IOException {
ZipEntry entry = Utils.getEntry(zf, path);
if (entry == null) {
return;
}
if (this.resourceTable == null) {
parseResourceTable();
}
InputStream in = zf.getInputStream(entry);
ByteBuffer buffer = ByteBuffer.wrap(Utils.toByteArray(in));
BinaryXmlParser binaryXmlParser = new BinaryXmlParser(buffer, resourceTable);
binaryXmlParser.setLocale(preferredLocale);
binaryXmlParser.setXmlStreamer(xmlStreamer);
binaryXmlParser.parse();
}
/**
* get class infos form dex file. currently only class name
*/
public DexClass[] getDexClasses() throws IOException {
if (this.dexClasses == null) {
parseDexFile();
}
return this.dexClasses;
}
private void parseDexFile() throws IOException {
ZipEntry resourceEntry = Utils.getEntry(zf, AndroidConstants.DEX_FILE);
if (resourceEntry == null) {
throw new ParserException("Resource table not found");
}
InputStream in = zf.getInputStream(resourceEntry);
ByteBuffer buffer = ByteBuffer.wrap(Utils.toByteArray(in));
DexParser dexParser = new DexParser(buffer);
dexParser.parse();
this.dexClasses = dexParser.getDexClasses();
}
/**
* read file in apk into bytes
*/
public byte[] getFileData(String path) throws IOException {
ZipEntry entry = Utils.getEntry(zf, path);
if (entry == null) {
return null;
}
InputStream inputStream = zf.getInputStream(entry);
return Utils.toByteArray(inputStream);
}
/**
* check apk sign
*
* @throws IOException
*/
public ApkSignStatus verifyApk() throws IOException {
ZipEntry entry = Utils.getEntry(zf, "META-INF/MANIFEST.MF");
if (entry == null) {
// apk is not signed;
return ApkSignStatus.notSigned;
}
JarFile jarFile = new JarFile(this.apkFile);
Enumeration<JarEntry> entries = jarFile.entries();
byte[] buffer = new byte[8192];
while (entries.hasMoreElements()) {
JarEntry e = entries.nextElement();
if (e.isDirectory()) {
continue;
}
try (InputStream in = jarFile.getInputStream(e)) {
// Read in each jar entry. A security exception will be thrown if a signature/digest check fails.
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
// Don't care
}
} catch (SecurityException se) {
return ApkSignStatus.incorrect;
}
}
return ApkSignStatus.signed;
}
/**
* parse resource table.
*/
private void parseResourceTable() throws IOException {
ZipEntry entry = Utils.getEntry(zf, AndroidConstants.RESOURCE_FILE);
if (entry == null) {
// if no resource entry has been found, we assume it is not needed by this APK
this.resourceTable = new ResourceTable();
this.locales = Collections.emptySet();
return;
}
this.resourceTable = new ResourceTable();
this.locales = Collections.emptySet();
InputStream in = zf.getInputStream(entry);
ByteBuffer buffer = ByteBuffer.wrap(Utils.toByteArray(in));
ResourceTableParser resourceTableParser = new ResourceTableParser(buffer);
resourceTableParser.parse();
this.resourceTable = resourceTableParser.getResourceTable();
this.locales = resourceTableParser.getLocales();
}
@Override
public void close() throws IOException {
this.certificateMetaList = null;
this.resourceTable = null;
this.certificateMetaList = null;
zf.close();
}
public Locale getPreferredLocale() {
return preferredLocale;
}
/**
* The locale preferred. Will cause getManifestXml / getApkMeta to return different values.
* The default value is from os default locale setting.
*/
public void setPreferredLocale(Locale preferredLocale) {
if (!Objects.equals(this.preferredLocale, preferredLocale)) {
this.preferredLocale = preferredLocale;
this.manifestXml = null;
this.apkMeta = null;
}
}
}