From 00917481fe06b50591fbd0a61278cc140a2e2b6e Mon Sep 17 00:00:00 2001 From: Henglie Date: Thu, 12 Nov 2026 15:49:54 +0800 Subject: [PATCH] Fix IPA icon colors and Info.plist parsing (#2014) Apple CgBI PNG icons store premultiplied BGRA pixels, so WPF decoded red and blue swapped. Convert CgBI icons to standard PNG before preview. Also ignore XML comment nodes in plists, which made IPAs such as OurMaps fail to parse. --- .../CgbiPngTests.cs | 56 +++ .../CgbiSample.cs | 113 ++++++ .../IpaReaderTests.cs | 106 ++++++ .../PlistTests.cs | 56 +++ .../QuickLook.Plugin.AppViewer.Tests.csproj | 28 ++ .../PackageParsers/Ipa/CgbiPng.cs | 329 ++++++++++++++++++ .../PackageParsers/Ipa/IpaReader.cs | 2 +- .../PackageParsers/Ipa/Plist.cs | 17 +- 8 files changed, 703 insertions(+), 4 deletions(-) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiPngTests.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiSample.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/IpaReaderTests.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/PlistTests.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/QuickLook.Plugin.AppViewer.Tests.csproj create mode 100644 QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/CgbiPng.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiPngTests.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiPngTests.cs new file mode 100644 index 000000000..da3dd224b --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiPngTests.cs @@ -0,0 +1,56 @@ +// Copyright © 2017-2026 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using QuickLook.Plugin.AppViewer.PackageParsers.Ipa; +using System.Drawing; +using System.IO; + +namespace QuickLook.Plugin.AppViewer.Tests; + +[TestClass] +public class CgbiPngTests +{ + [TestMethod] + public void ConvertToStandardPng_RestoresRedAndBlueChannels() + { + byte[] cgbi = CgbiSample.Create(2, 2, 7, 139, 237, 255); + + Assert.IsTrue(CgbiPng.IsCgbi(cgbi)); + + byte[] png = CgbiPng.ConvertToStandardPng(cgbi); + + Assert.IsFalse(CgbiPng.IsCgbi(png)); + using var stream = new MemoryStream(png); + using var bitmap = new Bitmap(stream); + Color color = bitmap.GetPixel(0, 0); + Assert.AreEqual(237, color.R); + Assert.AreEqual(139, color.G); + Assert.AreEqual(7, color.B); + Assert.AreEqual(255, color.A); + } + + [TestMethod] + public void ConvertToStandardPng_LeavesRegularPngUntouched() + { + byte[] png = System.Convert.FromBase64String( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="); + + Assert.IsFalse(CgbiPng.IsCgbi(png)); + Assert.AreSame(png, CgbiPng.ConvertToStandardPng(png)); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiSample.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiSample.cs new file mode 100644 index 000000000..f1bd5a99f --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/CgbiSample.cs @@ -0,0 +1,113 @@ +// Copyright © 2017-2026 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 ICSharpCode.SharpZipLib.Checksum; +using ICSharpCode.SharpZipLib.Zip.Compression; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.IO; +using System.Text; + +namespace QuickLook.Plugin.AppViewer.Tests; + +internal static class CgbiSample +{ + private static readonly byte[] Signature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + + public static byte[] Create(int width, int height, byte blue, byte green, byte red, byte alpha) + { + int rowBytes = width * 4; + byte[] raw = new byte[(rowBytes + 1) * height]; + + for (int y = 0; y < height; y++) + { + int row = y * (rowBytes + 1) + 1; + for (int x = 0; x < width; x++) + { + int pixel = row + x * 4; + raw[pixel] = blue; + raw[pixel + 1] = green; + raw[pixel + 2] = red; + raw[pixel + 3] = alpha; + } + } + + byte[] compressed; + using (var buffer = new MemoryStream()) + { + using (DeflaterOutputStream deflater = new(buffer, new Deflater(Deflater.BEST_COMPRESSION, true)) + { + IsStreamOwner = false, + }) + { + deflater.Write(raw, 0, raw.Length); + } + + compressed = buffer.ToArray(); + } + + using var output = new MemoryStream(); + output.Write(Signature, 0, Signature.Length); + + WriteChunk(output, "CgBI", []); + + byte[] header = new byte[13]; + WriteInt32BigEndian(header, 0, width); + WriteInt32BigEndian(header, 4, height); + header[8] = 8; + header[9] = 6; + WriteChunk(output, "IHDR", header); + + WriteChunk(output, "IDAT", compressed); + WriteChunk(output, "IEND", []); + + return output.ToArray(); + } + + public static void WriteChunk(Stream stream, string type, byte[] data) + { + byte[] typeBytes = Encoding.ASCII.GetBytes(type); + WriteInt32BigEndian(stream, data.Length); + stream.Write(typeBytes, 0, typeBytes.Length); + stream.Write(data, 0, data.Length); + + Crc32 crc = new(); + crc.Update(typeBytes); + if (data.Length > 0) + { + crc.Update(data); + } + + WriteInt32BigEndian(stream, unchecked((int)crc.Value)); + } + + private static void WriteInt32BigEndian(byte[] buffer, int offset, int value) + { + buffer[offset] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)value; + } + + private static void WriteInt32BigEndian(Stream stream, int value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/IpaReaderTests.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/IpaReaderTests.cs new file mode 100644 index 000000000..b36748606 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/IpaReaderTests.cs @@ -0,0 +1,106 @@ +// Copyright © 2017-2026 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 ICSharpCode.SharpZipLib.Zip; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using QuickLook.Plugin.AppViewer.PackageParsers.Ipa; +using System; +using System.Drawing; +using System.IO; +using System.Text; + +namespace QuickLook.Plugin.AppViewer.Tests; + +[TestClass] +public class IpaReaderTests +{ + private const string InfoPlist = """ + + + + + CFBundleDisplayName + Sample + CFBundleShortVersionString + 1.2.3 + CFBundleVersion + 123 + CFBundleIdentifier + com.example.sample + MinimumOSVersion + 6.1 + UIDeviceFamily + + 1 + 2 + + + CFBundleIconFiles + + AppIcon57x57 + AppIcon60x60 + + + + """; + + [TestMethod] + public void Parse_ReadsInfoPlistWithCommentsAndConvertsCgbiIcon() + { + byte[] cgbi = CgbiSample.Create(2, 2, 7, 139, 237, 255); + + using var stream = new MemoryStream(); + using (var zip = new ZipOutputStream(stream) + { + IsStreamOwner = false, + }) + { + AddEntry(zip, "Payload/Sample.app/Info.plist", Encoding.UTF8.GetBytes(InfoPlist)); + AddEntry(zip, "Payload/Sample.app/AppIcon60x60@2x.png", cgbi); + } + + stream.Position = 0; + IpaReader reader = new(stream); + + Assert.AreEqual("Sample", reader.DisplayName); + Assert.AreEqual("1.2.3", reader.ShortVersionString); + Assert.AreEqual("123", reader.Version); + Assert.AreEqual("com.example.sample", reader.Identifier); + Assert.AreEqual("iPhone, iPad", reader.DeviceFamily); + Assert.IsNotNull(reader.Icon); + + using var iconStream = new MemoryStream(reader.Icon); + using var bitmap = new Bitmap(iconStream); + Color color = bitmap.GetPixel(0, 0); + Assert.AreEqual(237, color.R); + Assert.AreEqual(139, color.G); + Assert.AreEqual(7, color.B); + } + + private static void AddEntry(ZipOutputStream zip, string name, byte[] data) + { + ZipEntry entry = new(name) + { + Size = data.Length, + DateTime = new DateTime(2020, 1, 1), + }; + + zip.PutNextEntry(entry); + zip.Write(data, 0, data.Length); + zip.CloseEntry(); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/PlistTests.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/PlistTests.cs new file mode 100644 index 000000000..75aae8e50 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/PlistTests.cs @@ -0,0 +1,56 @@ +// Copyright © 2017-2026 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using QuickLook.Plugin.AppViewer.PackageParsers.Ipa; +using System.Collections.Generic; +using System.Text; + +namespace QuickLook.Plugin.AppViewer.Tests; + +[TestClass] +public class PlistTests +{ + [TestMethod] + public void ReadPlist_IgnoresXmlComments() + { + const string xml = """ + + + + + + CFBundleDisplayName + Test App + + CFBundleIconFiles + + + Icon-57.png + Icon-180.png + + + + """; + + var result = (Dictionary)Plist.ReadPlist(Encoding.UTF8.GetBytes(xml)); + + Assert.AreEqual("Test App", result["CFBundleDisplayName"]); + var icons = (List)result["CFBundleIconFiles"]; + CollectionAssert.AreEqual(new object[] { "Icon-57.png", "Icon-180.png" }, icons); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/QuickLook.Plugin.AppViewer.Tests.csproj b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/QuickLook.Plugin.AppViewer.Tests.csproj new file mode 100644 index 000000000..e50eb6e5d --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer.Tests/QuickLook.Plugin.AppViewer.Tests.csproj @@ -0,0 +1,28 @@ + + + + net462 + false + latest + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/CgbiPng.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/CgbiPng.cs new file mode 100644 index 000000000..28df7ae01 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/CgbiPng.cs @@ -0,0 +1,329 @@ +// Copyright © 2017-2026 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 ICSharpCode.SharpZipLib.Checksum; +using ICSharpCode.SharpZipLib.Zip.Compression; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.IO; +using System.Text; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Ipa; + +/// +/// Converts Apple's CgBI PNG variant into a standard PNG. +/// CgBI files store premultiplied BGRA pixels and compress the image data as raw deflate, +/// which regular PNG decoders interpret as ABGR and therefore render with swapped colors. +/// +public static class CgbiPng +{ + private static readonly byte[] Signature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + + public static bool IsCgbi(byte[] data) + { + if (data == null || data.Length < 20 || !HasSignature(data)) + { + return false; + } + + return Encoding.ASCII.GetString(data, 12, 4) == "CgBI"; + } + + public static byte[] ConvertToStandardPng(byte[] data) + { + if (!IsCgbi(data)) + { + return data; + } + + int width = 0; + int height = 0; + int bitDepth = 0; + int colorType = -1; + int interlace = 0; + using var idat = new MemoryStream(); + + int position = 8; + while (position + 12 <= data.Length) + { + int length = ReadInt32BigEndian(data, position); + if (length < 0 || (long)position + 12 + length > data.Length) + { + return data; + } + + string type = Encoding.ASCII.GetString(data, position + 4, 4); + int dataStart = position + 8; + + if (type == "IEND") + { + break; + } + + if (type == "IHDR") + { + if (length < 13) + { + return data; + } + + width = ReadInt32BigEndian(data, dataStart); + height = ReadInt32BigEndian(data, dataStart + 4); + bitDepth = data[dataStart + 8]; + colorType = data[dataStart + 9]; + interlace = data[dataStart + 12]; + } + else if (type == "IDAT") + { + idat.Write(data, dataStart, length); + } + + position = dataStart + length + 4; + } + + // Only the 8-bit non-interlaced RGB/RGBA layouts produced by Apple's pngcrush are handled. + if (width <= 0 || height <= 0 || bitDepth != 8 || (colorType != 2 && colorType != 6) || interlace != 0) + { + return data; + } + + int bytesPerPixel = colorType == 6 ? 4 : 3; + long rowBytesLong = (long)width * bytesPerPixel; + long filteredLength = (rowBytesLong + 1) * height; + if (rowBytesLong > int.MaxValue || filteredLength > int.MaxValue) + { + return data; + } + + int rowBytes = (int)rowBytesLong; + byte[] filtered = new byte[(int)filteredLength]; + + try + { + Inflater inflater = new(true); + byte[] compressed = idat.ToArray(); + inflater.SetInput(compressed); + + int totalRead = 0; + while (totalRead < filtered.Length) + { + int read = inflater.Inflate(filtered, totalRead, filtered.Length - totalRead); + if (read == 0) + { + break; + } + + totalRead += read; + } + + if (totalRead < filtered.Length) + { + return data; + } + } + catch (Exception) + { + return data; + } + + byte[] pixels = Unfilter(filtered, width, height, bytesPerPixel); + if (pixels == null) + { + return data; + } + + byte[] rgba = SwapChannels(pixels, bytesPerPixel); + return EncodePng(rgba, width, height, colorType, rowBytes); + } + + private static byte[] Unfilter(byte[] filtered, int width, int height, int bytesPerPixel) + { + int rowBytes = width * bytesPerPixel; + byte[] pixels = new byte[rowBytes * height]; + + for (int y = 0; y < height; y++) + { + int source = y * (rowBytes + 1); + int filter = filtered[source]; + int destination = y * rowBytes; + int previous = destination - rowBytes; + source++; + + for (int x = 0; x < rowBytes; x++) + { + int raw = filtered[source + x]; + int left = x >= bytesPerPixel ? pixels[destination + x - bytesPerPixel] : 0; + int up = y > 0 ? pixels[previous + x] : 0; + int upLeft = y > 0 && x >= bytesPerPixel ? pixels[previous + x - bytesPerPixel] : 0; + + int value = filter switch + { + 0 => raw, + 1 => raw + left, + 2 => raw + up, + 3 => raw + ((left + up) >> 1), + 4 => raw + Paeth(left, up, upLeft), + _ => -1, + }; + + if (value < 0) + { + return null; + } + + pixels[destination + x] = (byte)value; + } + } + + return pixels; + } + + private static byte[] SwapChannels(byte[] pixels, int bytesPerPixel) + { + byte[] rgba = new byte[pixels.Length]; + + for (int i = 0; i < pixels.Length; i += bytesPerPixel) + { + int blue = pixels[i]; + int green = pixels[i + 1]; + int red = pixels[i + 2]; + + if (bytesPerPixel == 4) + { + int alpha = pixels[i + 3]; + if (alpha > 0 && alpha < 255) + { + red = Math.Min(255, (red * 255 + alpha / 2) / alpha); + green = Math.Min(255, (green * 255 + alpha / 2) / alpha); + blue = Math.Min(255, (blue * 255 + alpha / 2) / alpha); + } + + rgba[i] = (byte)red; + rgba[i + 1] = (byte)green; + rgba[i + 2] = (byte)blue; + rgba[i + 3] = (byte)alpha; + } + else + { + rgba[i] = (byte)red; + rgba[i + 1] = (byte)green; + rgba[i + 2] = (byte)blue; + } + } + + return rgba; + } + + private static byte[] EncodePng(byte[] pixels, int width, int height, int colorType, int rowBytes) + { + using var output = new MemoryStream(); + output.Write(Signature, 0, Signature.Length); + + byte[] header = new byte[13]; + WriteInt32BigEndian(header, 0, width); + WriteInt32BigEndian(header, 4, height); + header[8] = 8; + header[9] = (byte)colorType; + WriteChunk(output, "IHDR", header); + + using (var raw = new MemoryStream()) + { + using (DeflaterOutputStream deflater = new(raw, new Deflater(Deflater.BEST_COMPRESSION)) + { + IsStreamOwner = false, + }) + { + for (int y = 0; y < height; y++) + { + deflater.WriteByte(0); + deflater.Write(pixels, y * rowBytes, rowBytes); + } + } + + WriteChunk(output, "IDAT", raw.ToArray()); + } + + WriteChunk(output, "IEND", []); + return output.ToArray(); + } + + private static void WriteChunk(Stream stream, string type, byte[] data) + { + byte[] typeBytes = Encoding.ASCII.GetBytes(type); + WriteInt32BigEndian(stream, data.Length); + stream.Write(typeBytes, 0, typeBytes.Length); + stream.Write(data, 0, data.Length); + + Crc32 crc = new(); + crc.Update(typeBytes); + if (data.Length > 0) + { + crc.Update(data); + } + + WriteInt32BigEndian(stream, unchecked((int)crc.Value)); + } + + private static bool HasSignature(byte[] data) + { + for (int i = 0; i < Signature.Length; i++) + { + if (data[i] != Signature[i]) + { + return false; + } + } + + return true; + } + + private static int ReadInt32BigEndian(byte[] data, int offset) + { + return (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]; + } + + private static void WriteInt32BigEndian(byte[] buffer, int offset, int value) + { + buffer[offset] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)value; + } + + private static void WriteInt32BigEndian(Stream stream, int value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + private static int Paeth(int left, int up, int upLeft) + { + int estimate = left + up - upLeft; + int distanceLeft = Math.Abs(estimate - left); + int distanceUp = Math.Abs(estimate - up); + int distanceUpLeft = Math.Abs(estimate - upLeft); + + if (distanceLeft <= distanceUp && distanceLeft <= distanceUpLeft) + { + return left; + } + + return distanceUp <= distanceUpLeft ? up : upLeft; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs index f329e1041..e8d5c35b1 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs @@ -187,7 +187,7 @@ private void Open(Stream stream) if (fileName.StartsWith(IconName)) { using var s = new BinaryReader(zip.GetInputStream(entry)); - Icon = s.ReadBytes((int)entry.Size); + Icon = CgbiPng.ConvertToStandardPng(s.ReadBytes((int)entry.Size)); break; } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/Plist.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/Plist.cs index f57cb43ec..d5b78a648 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/Plist.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/Plist.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Xml; @@ -199,10 +200,20 @@ public static byte[] WriteBinary(object value) private static object ReadXml(XmlDocument xml) { - XmlNode rootNode = xml.DocumentElement.ChildNodes[0]; + XmlNode rootNode = GetElementChildren(xml.DocumentElement).FirstOrDefault(); + if (rootNode == null) + { + throw new ApplicationException("Plist root node is missing"); + } + return Parse(rootNode); } + private static IEnumerable GetElementChildren(XmlNode node) + { + return node.ChildNodes.Cast().Where(child => child.NodeType == XmlNodeType.Element); + } + private static object ReadBinary(byte[] data) { offsetTable.Clear(); @@ -229,7 +240,7 @@ private static object ReadBinary(byte[] data) private static Dictionary ParseDictionary(XmlNode node) { - XmlNodeList children = node.ChildNodes; + List children = [.. GetElementChildren(node)]; if (children.Count % 2 != 0) { throw new DataMisalignedException("Dictionary elements must have an even number of child nodes"); @@ -264,7 +275,7 @@ private static List ParseArray(XmlNode node) { List array = []; - foreach (XmlNode child in node.ChildNodes) + foreach (XmlNode child in GetElementChildren(node)) { object result = Parse(child); if (result != null)