mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-12 10:19:07 +00:00
Speed up the apk reader
This commit is contained in:
@@ -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();
|
||||
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<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 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 SupportLargeScreens { get; set; } = false;
|
||||
|
||||
public bool SupportAnyDensity { get; set; } = true;
|
||||
|
||||
public Dictionary<string, List<string>> 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<string> Locales { get; set; } = [];
|
||||
|
||||
public List<string> Densities { get; set; } = [];
|
||||
|
||||
public string LaunchableActivity { get; set; }
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -64,6 +64,12 @@
|
||||
<Resource Include="Resources\*.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="ApkPackageParser\ApkManifest.cs" />
|
||||
<Compile Remove="ApkPackageParser\ApkReader.cs" />
|
||||
<Compile Remove="ApkPackageParser\ApkResourceFinder.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\GitVersion.cs">
|
||||
<Link>Properties\GitVersion.cs</Link>
|
||||
@@ -76,6 +82,7 @@
|
||||
<PackageReference Include="WixToolset.Dtf.WindowsInstaller" Version="6.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="ApkReader" Version="2.0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
Reference in New Issue
Block a user