Speed up the apk reader
Some checks are pending
MSBuild / build (push) Waiting to run
MSBuild / publish (push) Blocked by required conditions

This commit is contained in:
ema
2025-06-01 13:54:27 +08:00
parent 1a83848c2c
commit d18b33a18e
9 changed files with 52 additions and 1119 deletions

View File

@@ -77,7 +77,7 @@ public partial class ApkInfoPanel : UserControl, IAppInfoPanel
modDate.Text = last.ToString(CultureInfo.CurrentCulture); modDate.Text = last.ToString(CultureInfo.CurrentCulture);
permissions.ItemsSource = apkInfo.Permissions; permissions.ItemsSource = apkInfo.Permissions;
if (!apkInfo.HasIcon) if (apkInfo.HasIcon)
{ {
using var stream = new MemoryStream(apkInfo.Logo); using var stream = new MemoryStream(apkInfo.Logo);
var icon = new BitmapImage(); var icon = new BitmapImage();

View File

@@ -1,65 +1,47 @@
// Copyright © 2017-2025 QL-Win Contributors using System.Collections.Generic;
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System.Collections.Generic;
namespace QuickLook.Plugin.AppViewer.ApkPackageParser; namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
public class ApkInfo public class ApkInfo
{ {
public string Label { get; set; }
public string VersionName { get; set; } public string VersionName { get; set; }
public string VersionCode { get; set; } public string VersionCode { get; set; }
public string MinSdkVersion { get; set; }
public string TargetSdkVersion { get; set; } public string TargetSdkVersion { get; set; }
public string PackageName { get; set; }
public string Debuggable { get; set; }
public List<string> Permissions { get; set; } = []; public List<string> Permissions { get; set; } = [];
public List<string> IconFileName { get; set; } public string PackageName { get; set; }
public string MinSdkVersion { get; set; }
public string Icon { get; set; }
public Dictionary<string, string> Icons { get; set; } = [];
public byte[] Logo { get; set; } public byte[] Logo { get; set; }
public bool HasIcon { get; set; } = false; public string Label { get; set; }
public bool SupportSmallScreens { get; set; } = false; public Dictionary<string, string> Labels { get; set; } = [];
public bool SupportNormalScreens { get; set; } = false; public bool HasIcon
public bool SupportLargeScreens { get; set; } = false;
public bool SupportAnyDensity { get; set; } = true;
public Dictionary<string, List<string>> ResStrings { get; set; }
public bool IsDebuggable
{ {
get get
{ {
if (Debuggable == null) return false; // debugabble is not in the manifest if (Icons.Count <= 0)
if (Debuggable.Equals("-1")) return true; // Debuggable == true {
else return false; return !string.IsNullOrEmpty(Icon);
}
return true;
} }
} }
public List<string> Locales { get; set; } = [];
public List<string> Densities { get; set; } = [];
public string LaunchableActivity { get; set; }
} }

View File

@@ -1,270 +0,0 @@
// Copyright © 2017-2025 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
/// <summary>
/// https://github.com/hylander0/Iteedee.ApkReader
/// </summary>
public class ApkManifest
{
private string result = string.Empty;
private bool isUtf8;
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
// such as for AndroidManifest.xml in .apk files
private const int startDocTag = 0x00100100;
private const int endDocTag = 0x00100101;
private const int startTag = 0x00100102;
private const int endTag = 0x00100103;
private const int textTag = 0x00100104;
public string ReadManifestFileIntoXml(byte[] manifestFileData)
{
if (manifestFileData.Length == 0)
throw new Exception("Failed to read manifest data. Byte array was empty");
// Compressed XML file/bytes starts with 24x bytes of data,
// 9 32 bit words in little endian order (LSB first):
// 0th word is 03 00 08 00
// 3rd word SEEMS TO BE: Offset at then of StringTable
// 4th word is: Number of strings in string table
// WARNING: Sometime I indiscriminently display or refer to word in
// little endian storage format, or in integer format (ie MSB first).
int numbStrings = LEW(manifestFileData, 4 * 4);
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// of the length/string data in the StringTable.
int sitOff = 0x24; // Offset of start of StringIndexTable
// StringTable, each string is represented with a 16 bit little endian
// character count, followed by that number of 16 bit (LE) (Unicode) chars.
int stOff = sitOff + numbStrings * 4; // StringTable follows StrIndexTable
// XMLTags, The XML tag tree starts after some unknown content after the
// StringTable. There is some unknown data after the StringTable, scan
// forward from this point to the flag for the start of an XML start tag.
int xmlTagOff = LEW(manifestFileData, 3 * 4); // Start from the offset in the 3rd word.
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal int)
// String pool is encoded in UTF-8
// https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h#451
int flag = LEW(manifestFileData, 4 * 6);
this.isUtf8 = (flag & (1 << 8)) > 0;
for (int ii = xmlTagOff; ii < manifestFileData.Length - 4; ii += 4)
{
if (LEW(manifestFileData, ii) == startTag)
{
xmlTagOff = ii; break;
}
} // end of hack, scanning for start of first start tag
// XML tags and attributes:
// Every XML start and end tag consists of 6 32 bit words:
// 0th word: 02011000 for startTag and 03011000 for endTag
// 1st word: a flag?, like 38000000
// 2nd word: Line of where this tag appeared in the original source file
// 3rd word: FFFFFFFF ??
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// 5th word: StringIndex of Element Name
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
// Start tags (not end tags) contain 3 more words:
// 6th word: 14001400 meaning??
// 7th word: Number of Attributes that follow this tag(follow word 8th)
// 8th word: 00000000 meaning??
// Attributes consist of 5 words:
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// 1st word: StringIndex of Attribute Name
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId used
// 3rd word: Flags?
// 4th word: str ind of attr value again, or ResourceId of value
// TMP, dump string table to tr for debugging
//tr.addSelect("strings", null);
//for (int ii=0; ii<numbStrings; ii++) {
// // Length of string starts at StringTable plus offset in StrIndTable
// String str = CompXmlString(xml, sitOff, stOff, ii);
// tr.add(String.valueOf(ii), str);
//}
//tr.parent();
// Step through the XML tree element tags and attributes
int off = xmlTagOff;
int indent = 0;
int startTagLineNo = -2;
int startDocTagCounter = 1;
while (off < manifestFileData.Length)
{
int tag0 = LEW(manifestFileData, off);
//int tag1 = LEW(manifestFileData, off+1*4);
int lineNo = LEW(manifestFileData, off + 2 * 4);
//int tag3 = LEW(manifestFileData, off+3*4);
int nameNsSi = LEW(manifestFileData, off + 4 * 4);
int nameSi = LEW(manifestFileData, off + 5 * 4);
if (tag0 == startTag)
{ // XML START TAG
int tag6 = LEW(manifestFileData, off + 6 * 4); // Expected to be 14001400
int numbAttrs = LEW(manifestFileData, off + 7 * 4); // Number of Attributes to follow
//int tag8 = LEW(manifestFileData, off+8*4); // Expected to be 00000000
off += 9 * 4; // Skip over 6+3 words of startTag data
string name = CompXmlString(manifestFileData, sitOff, stOff, nameSi);
//tr.addSelect(name, null);
startTagLineNo = lineNo;
// Look for the Attributes
string sb = string.Empty;
for (int ii = 0; ii < numbAttrs; ii++)
{
int attrNameNsSi = LEW(manifestFileData, off); // AttrName Namespace Str Ind, or FFFFFFFF
int attrNameSi = LEW(manifestFileData, off + 1 * 4); // AttrName String Index
int attrValueSi = LEW(manifestFileData, off + 2 * 4); // AttrValue Str Ind, or FFFFFFFF
int attrFlags = LEW(manifestFileData, off + 3 * 4);
int attrResId = LEW(manifestFileData, off + 4 * 4); // AttrValue ResourceId or dup AttrValue StrInd
off += 5 * 4; // Skip over the 5 words of an attribute
string attrName = CompXmlString(manifestFileData, sitOff, stOff, attrNameSi);
string attrValue = attrValueSi != -1
? CompXmlString(manifestFileData, sitOff, stOff, attrValueSi)
: /*"resourceID 0x" + */attrResId.ToString();
sb += " " + attrName + "=\"" + attrValue + "\"";
//tr.add(attrName, attrValue);
}
PrtIndent(indent, "<" + name + sb + ">");
indent++;
}
else if (tag0 == endTag)
{
// XML END TAG
indent--;
off += 6 * 4; // Skip over 6 words of endTag data
string name = CompXmlString(manifestFileData, sitOff, stOff, nameSi);
PrtIndent(indent, "</" + name + "> \r\n"/*+"(line " + startTagLineNo + "-" + lineNo + ")"*/);
//tr.parent(); // Step back up the NobTree
}
else if (tag0 == startDocTag)
{
startDocTagCounter++;
off += 4;
}
else if (tag0 == endDocTag)
{
// END OF XML DOC TAG
startDocTagCounter--;
if (startDocTagCounter == 0)
break;
}
else if (tag0 == textTag)
{
// code "copied" https://github.com/mikandi/php-apk-parser/blob/fixed-mikandi-versionName/lib/ApkParser/XmlParser.php
uint sentinal = 0xffffffff;
while (off < manifestFileData.Length)
{
uint curr = (uint)LEW(manifestFileData, off);
off += 4;
if (off > manifestFileData.Length)
{
throw new Exception("Sentinal not found before end of file");
}
if (curr == sentinal && sentinal == 0xffffffff)
{
sentinal = 0x00000000;
}
else if (curr == sentinal)
{
break;
}
}
}
else
{
Prt(" Unrecognized tag code '" + tag0.ToString("X")
+ "' at offset " + off);
break;
}
} // end of while loop scanning tags and attributes of XML tree
//Prt(" end at offset " + off);
return result;
} // end of decompressXML
public string CompXmlString(byte[] xml, int sitOff, int stOff, int strInd)
{
if (strInd < 0) return null;
int strOff = stOff + LEW(xml, sitOff + strInd * 4);
return CompXmlStringAt(xml, strOff);
}
public static string spaces = " ";
public void PrtIndent(int indent, string str)
{
Prt(spaces.Substring(0, Math.Min(indent * 2, spaces.Length)) + str);
}
private void Prt(string p)
{
result += p;
}
// CompXmlStringAt -- Return the string stored in StringTable format at
// offset strOff. This offset points to the 16 bit string length, which
// is followed by that number of 16 bit (Unicode) chars.
public string CompXmlStringAt(byte[] arr, int strOff)
{
/**
* Strings in UTF-8 format have length indicated by a length encoded in the
* stored data. It is either 1 or 2 characters of length data. This allows a
* maximum length of 0x7FFF (32767 bytes), but you should consider storing
* text in another way if you're using that much data in a single string.
*
* If the high bit is set, then there are two characters or 2 bytes of length
* data encoded. In that case, drop the high bit of the first character and
* add it together with the next character.
* https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/ResourceTypes.cpp#674
*/
int strLen = arr[strOff];
if ((strLen & 0x80) != 0)
strLen = ((strLen & 0x7f) << 8) + arr[strOff + 1];
if (!isUtf8)
strLen *= 2;
byte[] chars = new byte[strLen];
for (int ii = 0; ii < strLen; ii++)
{
chars[ii] = arr[strOff + 2 + ii];
}
return System.Text.Encoding.GetEncoding(isUtf8 ? "UTF-8" : "UTF-16").GetString(chars);
} // end of CompXmlStringAt
// LEW -- Return value of a Little Endian 32 bit word from the byte array
// at offset off.
public int LEW(byte[] arr, int off)
{
//return (int)(arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000 | arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF);
return (int)(((uint)arr[off + 3]) << 24 & 0xff000000 | ((uint)arr[off + 2]) << 16 & 0xff0000 | ((uint)arr[off + 1]) << 8 & 0xff00 | ((uint)arr[off]) & 0xFF);
} // end of LEW
}

View File

@@ -25,36 +25,33 @@ public static class ApkParser
{ {
public static ApkInfo Parse(string path) public static ApkInfo Parse(string path)
{ {
byte[] manifestData = null;
byte[] resourcesData = null;
using var zip = new ZipFile(path); using var zip = new ZipFile(path);
// AndroidManifest.xml var apkReader = new ApkReader.ApkReader();
ApkReader.ApkInfo baseInfo = apkReader.Read(path);
ApkInfo info = new()
{ {
ZipEntry entry = zip.GetEntry("AndroidManifest.xml"); VersionName = baseInfo.VersionName,
using var s = new BinaryReader(zip.GetInputStream(entry)); VersionCode = baseInfo.VersionCode,
manifestData = s.ReadBytes((int)entry.Size); TargetSdkVersion = baseInfo.TargetSdkVersion,
} Permissions = baseInfo.Permissions,
PackageName = baseInfo.PackageName,
MinSdkVersion = baseInfo.MinSdkVersion,
Icon = baseInfo.Icon,
Icons = baseInfo.Icons,
Label = baseInfo.Label,
Labels = baseInfo.Labels,
Locales = baseInfo.Locales,
Densities = baseInfo.Densities,
LaunchableActivity = baseInfo.LaunchableActivity,
};
// resources.arsc if (baseInfo.HasIcon)
{ {
ZipEntry entry = zip.GetEntry("resources.arsc"); ZipEntry entry = zip.GetEntry(baseInfo.Icons.Values.LastOrDefault());
using var s = new BinaryReader(zip.GetInputStream(entry));
resourcesData = s.ReadBytes((int)entry.Size);
}
ApkReader apkReader = new();
ApkInfo info = apkReader.ExtractInfo(manifestData, resourcesData);
// Logo
if (info.HasIcon)
{
ZipEntry entry = zip.GetEntry(info.IconFileName.LastOrDefault());
using var s = new BinaryReader(zip.GetInputStream(entry)); using var s = new BinaryReader(zip.GetInputStream(entry));
info.Logo = s.ReadBytes((int)entry.Size); info.Logo = s.ReadBytes((int)entry.Size);
} }
return info; return info;
} }
} }

View File

@@ -1,280 +0,0 @@
// Copyright © 2017-2025 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
/// <summary>
/// https://github.com/hylander0/Iteedee.ApkReader
/// </summary>
public class ApkReader
{
private const int VER_ID = 0;
private const int ICN_ID = 1;
private const int LABEL_ID = 2;
private readonly string[] VER_ICN = new string[3];
// Some possible tags and attributes
private readonly string[] TAGS = ["manifest", "application", "activity"];
public string FuzzFindInDocument(XmlDocument doc, string tag, string attr)
{
foreach (string t in TAGS)
{
XmlNodeList nodelist = doc.GetElementsByTagName(t);
for (int i = 0; i < nodelist.Count; i++)
{
XmlNode element = nodelist.Item(i);
if (element.NodeType == XmlNodeType.Element)
{
XmlAttributeCollection map = element.Attributes;
for (int j = 0; j < map.Count; j++)
{
XmlNode element2 = map.Item(j);
if (element2.Name.EndsWith(attr))
{
return element2.Value;
}
}
}
}
}
return null;
}
private void ExtractPermissions(ApkInfo info, XmlDocument doc)
{
ExtractPermission(info, doc, "uses-permission", "name");
ExtractPermission(info, doc, "permission-group", "name");
ExtractPermission(info, doc, "service", "permission");
ExtractPermission(info, doc, "provider", "permission");
ExtractPermission(info, doc, "activity", "permission");
}
private bool ReadBoolean(XmlDocument doc, string tag, string attribute)
{
try
{
string str = FindInDocument(doc, tag, attribute);
return Convert.ToBoolean(str);
}
catch
{
}
return false;
}
private void ExtractSupportScreens(ApkInfo info, XmlDocument doc)
{
info.SupportSmallScreens = ReadBoolean(doc, "supports-screens", "android:smallScreens");
info.SupportNormalScreens = ReadBoolean(doc, "supports-screens", "android:normalScreens");
info.SupportLargeScreens = ReadBoolean(doc, "supports-screens", "android:largeScreens");
if (info.SupportSmallScreens || info.SupportNormalScreens || info.SupportLargeScreens)
info.SupportAnyDensity = false;
}
public ApkInfo ExtractInfo(byte[] manifest_xml, byte[] resources_arsx)
{
string manifestXml;
ApkManifest manifest = new();
try
{
manifestXml = manifest.ReadManifestFileIntoXml(manifest_xml);
}
catch (Exception ex)
{
throw ex;
}
XmlDocument doc = new();
doc.LoadXml(manifestXml);
return ExtractInfo(doc, resources_arsx);
}
public ApkInfo ExtractInfo(XmlDocument manifestXml, byte[] resources_arsx)
{
ApkInfo info = new();
VER_ICN[VER_ID] = string.Empty;
VER_ICN[ICN_ID] = string.Empty;
VER_ICN[LABEL_ID] = string.Empty;
try
{
XmlDocument doc = manifestXml ?? throw new Exception("Document initialize failed");
// Fill up the permission field
ExtractPermissions(info, doc);
// Fill up some basic fields
info.MinSdkVersion = FindInDocument(doc, "uses-sdk", "minSdkVersion");
info.TargetSdkVersion = FindInDocument(doc, "uses-sdk", "targetSdkVersion");
info.VersionCode = FindInDocument(doc, "manifest", "versionCode");
info.VersionName = FindInDocument(doc, "manifest", "versionName");
info.PackageName = FindInDocument(doc, "manifest", "package");
info.Label = FindInDocument(doc, "application", "label");
if (info.Label.StartsWith("@"))
VER_ICN[LABEL_ID] = info.Label;
else if (int.TryParse(info.Label, out int labelID))
VER_ICN[LABEL_ID] = string.Format("@{0}", labelID.ToString("X4"));
// Get the value of android:Debuggable in the manifest
// "0" = false and "-1" = true
info.Debuggable = FindInDocument(doc, "application", "debuggable");
// Fill up the support screen field
ExtractSupportScreens(info, doc);
info.VersionCode ??= FuzzFindInDocument(doc, "manifest", "versionCode");
if (info.VersionName == null)
info.VersionName = FuzzFindInDocument(doc, "manifest", "versionName");
else if (info.VersionName.StartsWith("@"))
VER_ICN[VER_ID] = info.VersionName;
string id = FindInDocument(doc, "application", "android:icon");
if (null == id)
{
id = FuzzFindInDocument(doc, "manifest", "icon");
}
if (null == id)
{
Debug.WriteLine("icon resId Not Found!");
return info;
}
// Find real strings
if (!info.HasIcon && id != null)
{
if (id.StartsWith("@android:"))
VER_ICN[ICN_ID] = "@" + id.Substring("@android:".Length);
else
VER_ICN[ICN_ID] = string.Format("@{0}", Convert.ToInt32(id).ToString("X4"));
List<string> resId = [];
for (int i = 0; i < VER_ICN.Length; i++)
{
if (VER_ICN[i].StartsWith("@"))
resId.Add(VER_ICN[i]);
}
ApkResourceFinder finder = new();
info.ResStrings = finder.ProcessResourceTable(resources_arsx, resId);
if (!VER_ICN[VER_ID].Equals(string.Empty))
{
List<string> versions = null;
if (info.ResStrings.ContainsKey(VER_ICN[VER_ID].ToUpper()))
versions = info.ResStrings[VER_ICN[VER_ID].ToUpper()];
if (versions != null)
{
if (versions.Count > 0)
info.VersionName = versions[0];
}
else
{
throw new Exception("VersionName Cant Find in resource with id " + VER_ICN[VER_ID]);
}
}
List<string> iconPaths = null;
if (info.ResStrings.ContainsKey(VER_ICN[ICN_ID].ToUpper()))
iconPaths = info.ResStrings[VER_ICN[ICN_ID].ToUpper()];
if (iconPaths != null && iconPaths.Count > 0)
{
info.IconFileName = [];
foreach (string iconFileName in iconPaths)
{
if (iconFileName != null)
{
if (iconFileName.Contains(@"/"))
{
info.IconFileName.Add(iconFileName);
info.HasIcon = true;
}
}
}
}
else
{
throw new Exception("Icon Cant Find in resource with id " + VER_ICN[ICN_ID]);
}
if (!VER_ICN[LABEL_ID].Equals(string.Empty))
{
List<string> labels = null;
if (info.ResStrings.ContainsKey(VER_ICN[LABEL_ID]))
labels = info.ResStrings[VER_ICN[LABEL_ID]];
if (labels.Count > 0)
{
info.Label = labels[0];
}
}
}
}
catch (Exception e)
{
throw e;
}
return info;
}
private void ExtractPermission(ApkInfo info, XmlDocument doc, string keyName, string attribName)
{
XmlNodeList usesPermissions = doc.GetElementsByTagName(keyName);
if (usesPermissions != null)
{
for (int s = 0; s < usesPermissions.Count; s++)
{
XmlNode permissionNode = usesPermissions.Item(s);
if (permissionNode.NodeType == XmlNodeType.Element)
{
XmlNode node = permissionNode.Attributes.GetNamedItem(attribName);
if (node != null)
info.Permissions.Add(node.Value);
}
}
}
}
private string FindInDocument(XmlDocument doc, string keyName, string attribName)
{
XmlNodeList usesPermissions = doc.GetElementsByTagName(keyName);
if (usesPermissions != null)
{
for (int s = 0; s < usesPermissions.Count; s++)
{
XmlNode permissionNode = usesPermissions.Item(s);
if (permissionNode.NodeType == XmlNodeType.Element)
{
XmlNode node = permissionNode.Attributes.GetNamedItem(attribName);
if (node != null)
return node.Value;
}
}
}
return null;
}
}

View File

@@ -1,502 +0,0 @@
// Copyright © 2017-2025 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace QuickLook.Plugin.AppViewer.ApkPackageParser;
/// <summary>
/// https://github.com/hylander0/Iteedee.ApkReader
/// </summary>
public class ApkResourceFinder
{
private const short RES_STRING_POOL_TYPE = 0x0001;
private const short RES_TABLE_TYPE = 0x0002;
private const short RES_TABLE_PACKAGE_TYPE = 0x0200;
private const short RES_TABLE_TYPE_TYPE = 0x0201;
private const short RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
private string[] valueStringPool = null;
private string[] typeStringPool = null;
private string[] keyStringPool = null;
private int package_id = 0;
private List<string> resIdList;
//// Contains no data.
//static byte TYPE_NULL = 0x00;
//// The 'data' holds an attribute resource identifier.
//static byte TYPE_ATTRIBUTE = 0x02;
//// The 'data' holds a single-precision floating point number.
//static byte TYPE_FLOAT = 0x04;
//// The 'data' holds a complex number encoding a dimension value,
//// such as "100in".
//static byte TYPE_DIMENSION = 0x05;
//// The 'data' holds a complex number encoding a fraction of a
//// container.
//static byte TYPE_FRACTION = 0x06;
//// The 'data' is a raw integer value of the form n..n.
//static byte TYPE_INT_DEC = 0x10;
//// The 'data' is a raw integer value of the form 0xn..n.
//static byte TYPE_INT_HEX = 0x11;
//// The 'data' is either 0 or 1, for input "false" or "true" respectively.
//static byte TYPE_INT_BOOLEAN = 0x12;
//// The 'data' is a raw integer value of the form #aarrggbb.
//static byte TYPE_INT_COLOR_ARGB8 = 0x1c;
//// The 'data' is a raw integer value of the form #rrggbb.
//static byte TYPE_INT_COLOR_RGB8 = 0x1d;
//// The 'data' is a raw integer value of the form #argb.
//static byte TYPE_INT_COLOR_ARGB4 = 0x1e;
//// The 'data' is a raw integer value of the form #rgb.
//static byte TYPE_INT_COLOR_RGB4 = 0x1f;
// The 'data' holds a ResTable_ref, a reference to another resource
// table entry.
private const byte TYPE_REFERENCE = 0x01;
// The 'data' holds an index into the containing resource table's
// global value string pool.
private const byte TYPE_STRING = 0x03;
private Dictionary<string, List<string>> responseMap;
private Dictionary<int, List<string>> entryMap = [];
public Dictionary<string, List<string>> Initialize()
{
byte[] data = File.ReadAllBytes("resources.arsc");
return ProcessResourceTable(data, []);
}
public Dictionary<string, List<string>> ProcessResourceTable(byte[] data, List<string> resIdList)
{
this.resIdList = resIdList;
responseMap = [];
long lastPosition;
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
short type = br.ReadInt16();
short headerSize = br.ReadInt16();
int size = br.ReadInt32();
int packageCount = br.ReadInt32();
if (type != RES_TABLE_TYPE)
{
throw new Exception("No RES_TABLE_TYPE found!");
}
if (size != br.BaseStream.Length)
{
throw new Exception("The buffer size not matches to the resource table size.");
}
int realStringPoolCount = 0;
int realPackageCount = 0;
while (true)
{
long pos = br.BaseStream.Position;
short t = br.ReadInt16();
short hs = br.ReadInt16();
int s = br.ReadInt32();
if (t == RES_STRING_POOL_TYPE)
{
if (realStringPoolCount == 0)
{
// Only the first string pool is processed.
Debug.WriteLine("Processing the string pool ...");
byte[] buffer = new byte[s];
lastPosition = br.BaseStream.Position;
br.BaseStream.Seek(pos, SeekOrigin.Begin);
buffer = br.ReadBytes(s);
//br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
valueStringPool = ProcessStringPool(buffer);
}
realStringPoolCount++;
}
else if (t == RES_TABLE_PACKAGE_TYPE)
{
// Process the package
Debug.WriteLine("Processing package {0} ...", realPackageCount);
byte[] buffer = new byte[s];
lastPosition = br.BaseStream.Position;
br.BaseStream.Seek(pos, SeekOrigin.Begin);
buffer = br.ReadBytes(s);
//br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
ProcessPackage(buffer);
realPackageCount++;
}
else
{
throw new InvalidOperationException("Unsupported Type");
}
br.BaseStream.Seek(pos + s, SeekOrigin.Begin);
if (br.BaseStream.Position == br.BaseStream.Length)
break;
}
if (realStringPoolCount != 1)
{
throw new Exception("More than 1 string pool found!");
}
if (realPackageCount != packageCount)
{
throw new Exception("Real package count not equals the declared count.");
}
return responseMap;
}
private void ProcessPackage(byte[] data)
{
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
//HEADER
short type = br.ReadInt16();
short headerSize = br.ReadInt16();
int size = br.ReadInt32();
int id = br.ReadInt32();
package_id = id;
//PackageName
char[] name = new char[256];
for (int i = 0; i < 256; ++i)
{
name[i] = br.ReadChar();
}
int typeStrings = br.ReadInt32();
int lastPublicType = br.ReadInt32();
int keyStrings = br.ReadInt32();
int lastPublicKey = br.ReadInt32();
if (typeStrings != headerSize)
{
throw new Exception("TypeStrings must immediately follow the package structure header.");
}
Debug.WriteLine("Type strings:");
long lastPosition = br.BaseStream.Position;
br.BaseStream.Seek(typeStrings, SeekOrigin.Begin);
byte[] bbTypeStrings = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position));
br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
typeStringPool = ProcessStringPool(bbTypeStrings);
Debug.WriteLine("Key strings:");
br.BaseStream.Seek(keyStrings, SeekOrigin.Begin);
short key_type = br.ReadInt16();
short key_headerSize = br.ReadInt16();
int key_size = br.ReadInt32();
lastPosition = br.BaseStream.Position;
br.BaseStream.Seek(keyStrings, SeekOrigin.Begin);
byte[] bbKeyStrings = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position));
br.BaseStream.Seek(lastPosition, SeekOrigin.Begin);
keyStringPool = ProcessStringPool(bbKeyStrings);
// Iterate through all chunks
//
int typeSpecCount = 0;
int typeCount = 0;
br.BaseStream.Seek((keyStrings + key_size), SeekOrigin.Begin);
while (true)
{
int pos = (int)br.BaseStream.Position;
short t = br.ReadInt16();
short hs = br.ReadInt16();
int s = br.ReadInt32();
if (t == RES_TABLE_TYPE_SPEC_TYPE)
{
// Process the string pool
byte[] buffer = new byte[s];
br.BaseStream.Seek(pos, SeekOrigin.Begin);
buffer = br.ReadBytes(s);
ProcessTypeSpec(buffer);
typeSpecCount++;
}
else if (t == RES_TABLE_TYPE_TYPE)
{
// Process the package
byte[] buffer = new byte[s];
br.BaseStream.Seek(pos, SeekOrigin.Begin);
buffer = br.ReadBytes(s);
ProcessType(buffer);
typeCount++;
}
br.BaseStream.Seek(pos + s, SeekOrigin.Begin);
if (br.BaseStream.Position == br.BaseStream.Length)
break;
}
return;
}
private void PutIntoMap(string resId, string value)
{
List<string> valueList = null;
if (responseMap.ContainsKey(resId.ToUpper()))
valueList = responseMap[resId.ToUpper()];
valueList ??= [];
valueList.Add(value);
if (responseMap.ContainsKey(resId.ToUpper()))
responseMap[resId.ToUpper()] = valueList;
else
responseMap.Add(resId.ToUpper(), valueList);
return;
}
private void ProcessType(byte[] typeData)
{
using var ms = new MemoryStream(typeData);
using var br = new BinaryReader(ms);
short type = br.ReadInt16();
short headerSize = br.ReadInt16();
int size = br.ReadInt32();
byte id = br.ReadByte();
byte res0 = br.ReadByte();
short res1 = br.ReadInt16();
int entryCount = br.ReadInt32();
int entriesStart = br.ReadInt32();
Dictionary<string, int> refKeys = [];
int config_size = br.ReadInt32();
// Skip the config data
br.BaseStream.Seek(headerSize, SeekOrigin.Begin);
if (headerSize + entryCount * 4 != entriesStart)
{
throw new Exception("HeaderSize, entryCount and entriesStart are not valid.");
}
// Start to get entry indices
int[] entryIndices = new int[entryCount];
for (int i = 0; i < entryCount; ++i)
{
entryIndices[i] = br.ReadInt32();
}
// Get entries
for (int i = 0; i < entryCount; ++i)
{
if (entryIndices[i] == -1)
continue;
int resource_id = (package_id << 24) | (id << 16) | i;
long pos = br.BaseStream.Position;
short entry_size = br.ReadInt16();
short entry_flag = br.ReadInt16();
int entry_key = br.ReadInt32();
// Get the value (simple) or map (complex)
int FLAG_COMPLEX = 0x0001;
if ((entry_flag & FLAG_COMPLEX) == 0)
{
// Simple case
short value_size = br.ReadInt16();
byte value_res0 = br.ReadByte();
byte value_dataType = br.ReadByte();
int value_data = br.ReadInt32();
string idStr = resource_id.ToString("X4");
string keyStr = keyStringPool[entry_key];
string data = null;
Debug.WriteLine("Entry 0x" + idStr + ", key: " + keyStr + ", simple value type: ");
List<string> entryArr = null;
if (entryMap.ContainsKey(int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)))
entryArr = entryMap[int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)];
entryArr ??= [];
entryArr.Add(keyStr);
if (entryMap.ContainsKey(int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)))
entryMap[int.Parse(idStr, System.Globalization.NumberStyles.HexNumber)] = entryArr;
else
entryMap.Add(int.Parse(idStr, System.Globalization.NumberStyles.HexNumber), entryArr);
if (value_dataType == TYPE_STRING)
{
data = valueStringPool[value_data];
Debug.WriteLine(", data: " + valueStringPool[value_data]);
}
else if (value_dataType == TYPE_REFERENCE)
{
string hexIndex = value_data.ToString("X4");
refKeys.Add(idStr, value_data);
}
else
{
data = value_data.ToString();
Debug.WriteLine(", data: " + value_data);
}
// if (inReqList("@" + idStr)) {
PutIntoMap("@" + idStr, data);
}
else
{
int entry_parent = br.ReadInt32();
int entry_count = br.ReadInt32();
for (int j = 0; j < entry_count; ++j)
{
int ref_name = br.ReadInt32();
short value_size = br.ReadInt16();
byte value_res0 = br.ReadByte();
byte value_dataType = br.ReadByte();
int value_data = br.ReadInt32();
}
Debug.WriteLine("Entry 0x"
+ resource_id.ToString("X4") + ", key: "
+ keyStringPool[entry_key]
+ ", complex value, not printed.");
}
}
HashSet<string> refKs = [.. refKeys.Keys];
foreach (string refK in refKs)
{
List<string> values = null;
if (responseMap.ContainsKey("@" + refKeys[refK].ToString("X4").ToUpper()))
values = responseMap["@" + refKeys[refK].ToString("X4").ToUpper()];
if (values != null)
foreach (string value in values)
{
PutIntoMap("@" + refK, value);
}
}
return;
}
private string[] ProcessStringPool(byte[] data)
{
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
short type = br.ReadInt16();
short headerSize = br.ReadInt16();
int size = br.ReadInt32();
int stringCount = br.ReadInt32();
int styleCount = br.ReadInt32();
int flags = br.ReadInt32();
int stringsStart = br.ReadInt32();
int stylesStart = br.ReadInt32();
bool isUTF_8 = (flags & 256) != 0;
int[] offsets = new int[stringCount];
for (int i = 0; i < stringCount; ++i)
{
offsets[i] = br.ReadInt32();
}
string[] strings = new string[stringCount];
for (int i = 0; i < stringCount; i++)
{
int pos = stringsStart + offsets[i];
br.BaseStream.Seek(pos, SeekOrigin.Begin);
strings[i] = string.Empty;
if (isUTF_8)
{
int u16len = br.ReadByte(); // u16len
if ((u16len & 0x80) != 0)
{
// larger than 128
u16len = ((u16len & 0x7F) << 8) + br.ReadByte();
}
int u8len = br.ReadByte(); // u8len
if ((u8len & 0x80) != 0)
{
// larger than 128
u8len = ((u8len & 0x7F) << 8) + br.ReadByte();
}
if (u8len > 0)
strings[i] = Encoding.UTF8.GetString(br.ReadBytes(u8len));
else
strings[i] = string.Empty;
}
else // UTF_16
{
int u16len = br.ReadUInt16();
if ((u16len & 0x8000) != 0)
{
// larger than 32768
u16len = ((u16len & 0x7FFF) << 16) + br.ReadUInt16();
}
if (u16len > 0)
{
strings[i] = Encoding.Unicode.GetString(br.ReadBytes(u16len * 2));
}
}
Debug.WriteLine("Parsed value: {0}", strings[i]);
}
return strings;
}
private void ProcessTypeSpec(byte[] data)
{
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
short type = br.ReadInt16();
short headerSize = br.ReadInt16();
int size = br.ReadInt32();
byte id = br.ReadByte();
byte res0 = br.ReadByte();
short res1 = br.ReadInt16();
int entryCount = br.ReadInt32();
Debug.WriteLine("Processing type spec {0}", typeStringPool[id - 1]);
int[] flags = new int[entryCount];
for (int i = 0; i < entryCount; ++i)
{
flags[i] = br.ReadInt32();
}
return;
}
}

View File

@@ -18,7 +18,6 @@
using QuickLook.Common.ExtensionMethods; using QuickLook.Common.ExtensionMethods;
using QuickLook.Common.Helpers; using QuickLook.Common.Helpers;
using QuickLook.Common.Plugin; using QuickLook.Common.Plugin;
using QuickLook.Plugin.AppViewer.ApkPackageParser;
using QuickLook.Plugin.AppViewer.IpaPackageParser; using QuickLook.Plugin.AppViewer.IpaPackageParser;
using System; using System;
using System.Globalization; using System.Globalization;
@@ -79,7 +78,7 @@ public partial class IpaInfoPanel : UserControl, IAppInfoPanel
modDate.Text = last.ToString(CultureInfo.CurrentCulture); modDate.Text = last.ToString(CultureInfo.CurrentCulture);
permissions.ItemsSource = ipaInfo.Permissions; permissions.ItemsSource = ipaInfo.Permissions;
if (!ipaInfo.HasIcon) if (ipaInfo.HasIcon)
{ {
using var stream = new MemoryStream(ipaInfo.Logo); using var stream = new MemoryStream(ipaInfo.Logo);
var icon = new BitmapImage(); var icon = new BitmapImage();

View File

@@ -158,7 +158,7 @@ public class IpaReader
} }
} }
} }
if (!string.IsNullOrWhiteSpace(IconName)) if (string.IsNullOrWhiteSpace(IconName))
{ {
if (InfoPlistDict.TryGetValue("CFBundleIconFiles", out object iconFilesNode) && iconFilesNode is IList<object> iconFiles) if (InfoPlistDict.TryGetValue("CFBundleIconFiles", out object iconFilesNode) && iconFilesNode is IList<object> iconFiles)
{ {

View File

@@ -64,6 +64,12 @@
<Resource Include="Resources\*.png" /> <Resource Include="Resources\*.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove="ApkPackageParser\ApkManifest.cs" />
<Compile Remove="ApkPackageParser\ApkReader.cs" />
<Compile Remove="ApkPackageParser\ApkResourceFinder.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\GitVersion.cs"> <Compile Include="..\..\GitVersion.cs">
<Link>Properties\GitVersion.cs</Link> <Link>Properties\GitVersion.cs</Link>
@@ -76,6 +82,7 @@
<PackageReference Include="WixToolset.Dtf.WindowsInstaller" Version="6.0.0" /> <PackageReference Include="WixToolset.Dtf.WindowsInstaller" Version="6.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ApkReader" Version="2.0.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>