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