From d18b33a18ef760fb24a1dc5a103b9c0da7d9b0e9 Mon Sep 17 00:00:00 2001 From: ema Date: Sun, 1 Jun 2025 13:54:27 +0800 Subject: [PATCH] Speed up the apk reader --- .../ApkInfoPanel.xaml.cs | 2 +- .../ApkPackageParser/ApkInfo.cs | 64 +-- .../ApkPackageParser/ApkManifest.cs | 270 ---------- .../ApkPackageParser/ApkParser.cs | 41 +- .../ApkPackageParser/ApkReader.cs | 280 ---------- .../ApkPackageParser/ApkResourceFinder.cs | 502 ------------------ .../IpaInfoPanel.xaml.cs | 3 +- .../IpaPackageParser/IpaReader.cs | 2 +- .../QuickLook.Plugin.AppViewer.csproj | 7 + 9 files changed, 52 insertions(+), 1119 deletions(-) delete mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs delete mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs delete mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs index f8a1655..bfcb9a9 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkInfoPanel.xaml.cs @@ -77,7 +77,7 @@ public partial class ApkInfoPanel : UserControl, IAppInfoPanel modDate.Text = last.ToString(CultureInfo.CurrentCulture); permissions.ItemsSource = apkInfo.Permissions; - if (!apkInfo.HasIcon) + if (apkInfo.HasIcon) { using var stream = new MemoryStream(apkInfo.Logo); var icon = new BitmapImage(); diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs index f8c2c36..808002a 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkInfo.cs @@ -1,65 +1,47 @@ -// 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 . - -using System.Collections.Generic; +using System.Collections.Generic; namespace QuickLook.Plugin.AppViewer.ApkPackageParser; public class ApkInfo { - public string Label { get; set; } - public string VersionName { get; set; } public string VersionCode { get; set; } - public string MinSdkVersion { get; set; } - public string TargetSdkVersion { get; set; } - public string PackageName { get; set; } - - public string Debuggable { get; set; } - public List Permissions { get; set; } = []; - public List IconFileName { get; set; } + public string PackageName { get; set; } + + public string MinSdkVersion { get; set; } + + public string Icon { get; set; } + + public Dictionary Icons { 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 Labels { get; set; } = []; - public bool SupportNormalScreens { get; set; } = false; - - public bool SupportLargeScreens { get; set; } = false; - - public bool SupportAnyDensity { get; set; } = true; - - public Dictionary> ResStrings { get; set; } - - public bool IsDebuggable + public bool HasIcon { get { - if (Debuggable == null) return false; // debugabble is not in the manifest - if (Debuggable.Equals("-1")) return true; // Debuggable == true - else return false; + if (Icons.Count <= 0) + { + return !string.IsNullOrEmpty(Icon); + } + + return true; } } + + public List Locales { get; set; } = []; + + public List Densities { get; set; } = []; + + public string LaunchableActivity { get; set; } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs deleted file mode 100644 index 3b456b2..0000000 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkManifest.cs +++ /dev/null @@ -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 . - -using System; - -namespace QuickLook.Plugin.AppViewer.ApkPackageParser; - -/// -/// https://github.com/hylander0/Iteedee.ApkReader -/// -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"); - 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, " \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 -} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs index 99e77f6..fa9131c 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkParser.cs @@ -25,36 +25,33 @@ public static class ApkParser { public static ApkInfo Parse(string path) { - byte[] manifestData = null; - byte[] resourcesData = null; - 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"); - using var s = new BinaryReader(zip.GetInputStream(entry)); - manifestData = s.ReadBytes((int)entry.Size); - } + VersionName = baseInfo.VersionName, + VersionCode = baseInfo.VersionCode, + 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"); - 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()); + ZipEntry entry = zip.GetEntry(baseInfo.Icons.Values.LastOrDefault()); using var s = new BinaryReader(zip.GetInputStream(entry)); info.Logo = s.ReadBytes((int)entry.Size); } - return info; } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs deleted file mode 100644 index d658a8a..0000000 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkReader.cs +++ /dev/null @@ -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 . - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Xml; - -namespace QuickLook.Plugin.AppViewer.ApkPackageParser; - -/// -/// https://github.com/hylander0/Iteedee.ApkReader -/// -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 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 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 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 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; - } -} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs deleted file mode 100644 index 446de22..0000000 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/ApkPackageParser/ApkResourceFinder.cs +++ /dev/null @@ -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 . - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; - -namespace QuickLook.Plugin.AppViewer.ApkPackageParser; - -/// -/// https://github.com/hylander0/Iteedee.ApkReader -/// -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 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> responseMap; - - private Dictionary> entryMap = []; - - public Dictionary> Initialize() - { - byte[] data = File.ReadAllBytes("resources.arsc"); - return ProcessResourceTable(data, []); - } - - public Dictionary> ProcessResourceTable(byte[] data, List 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 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 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 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 refKs = [.. refKeys.Keys]; - - foreach (string refK in refKs) - { - List 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; - } -} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaInfoPanel.xaml.cs index ced6af1..8b14ed6 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaInfoPanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaInfoPanel.xaml.cs @@ -18,7 +18,6 @@ using QuickLook.Common.ExtensionMethods; using QuickLook.Common.Helpers; using QuickLook.Common.Plugin; -using QuickLook.Plugin.AppViewer.ApkPackageParser; using QuickLook.Plugin.AppViewer.IpaPackageParser; using System; using System.Globalization; @@ -79,7 +78,7 @@ public partial class IpaInfoPanel : UserControl, IAppInfoPanel modDate.Text = last.ToString(CultureInfo.CurrentCulture); permissions.ItemsSource = ipaInfo.Permissions; - if (!ipaInfo.HasIcon) + if (ipaInfo.HasIcon) { using var stream = new MemoryStream(ipaInfo.Logo); var icon = new BitmapImage(); diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaPackageParser/IpaReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaPackageParser/IpaReader.cs index 8cfe598..010cd63 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaPackageParser/IpaReader.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/IpaPackageParser/IpaReader.cs @@ -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 iconFiles) { diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj index 89f038c..b00000d 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj @@ -64,6 +64,12 @@ + + + + + + Properties\GitVersion.cs @@ -76,6 +82,7 @@ +