diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/DmgInfoPanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/DmgInfoPanel.xaml new file mode 100644 index 0000000..77da66d --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/DmgInfoPanel.xaml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/DmgInfoPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/DmgInfoPanel.xaml.cs new file mode 100644 index 0000000..4b92fc5 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/InfoPanels/DmgInfoPanel.xaml.cs @@ -0,0 +1,96 @@ +// 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 QuickLook.Common.ExtensionMethods; +using QuickLook.Common.Helpers; +using QuickLook.Common.Plugin; +using QuickLook.Plugin.AppViewer.PackageParsers.Dmg; +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Media.Imaging; + +namespace QuickLook.Plugin.AppViewer.InfoPanels; + +public partial class DmgInfoPanel : UserControl, IAppInfoPanel +{ + private readonly ContextObject _context; + + public DmgInfoPanel(ContextObject context) + { + _context = context; + + DataContext = this; + InitializeComponent(); + + string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config"); + applicationNameTitle.Text = TranslationHelper.Get("APP_NAME", translationFile); + versionNameTitle.Text = TranslationHelper.Get("APP_VERSION_NAME", translationFile); + versionCodeTitle.Text = TranslationHelper.Get("APP_VERSION_CODE", translationFile); + packageNameTitle.Text = TranslationHelper.Get("PACKAGE_NAME", translationFile); + minimumOSVersionTitle.Text = TranslationHelper.Get("APP_MIN_OS_VERSION", translationFile); + deviceFamilyTitle.Text = TranslationHelper.Get("DEVICE_FAMILY", translationFile); + platformVersionTitle.Text = TranslationHelper.Get("APP_TARGET_OS_VERSION", translationFile); + totalSizeTitle.Text = TranslationHelper.Get("TOTAL_SIZE", translationFile); + modDateTitle.Text = TranslationHelper.Get("LAST_MODIFIED", translationFile); + permissionsGroupBox.Header = TranslationHelper.Get("PERMISSIONS", translationFile); + } + + public void DisplayInfo(string path) + { + var name = Path.GetFileName(path); + filename.Text = string.IsNullOrEmpty(name) ? path : name; + + _ = Task.Run(() => + { + if (File.Exists(path)) + { + var size = new FileInfo(path).Length; + DmgInfo dmgInfo = DmgParser.Parse(path); + var last = File.GetLastWriteTime(path); + + Dispatcher.Invoke(() => + { + applicationName.Text = dmgInfo.DisplayName; + versionName.Text = dmgInfo.VersionName; + versionCode.Text = dmgInfo.VersionCode; + packageName.Text = dmgInfo.Identifier; + minimumOSVersion.Text = dmgInfo.MinimumOSVersion; + platformVersion.Text = dmgInfo.PlatformVersion; + deviceFamily.Text = dmgInfo.SupportedPlatforms; + totalSize.Text = size.ToPrettySize(2); + modDate.Text = last.ToString(CultureInfo.CurrentCulture); + permissions.ItemsSource = dmgInfo.Permissions; + + if (dmgInfo.HasIcon) + { + image.Source = dmgInfo.Logo.ToBitmapSource(); + } + else + { + image.Source = new BitmapImage(new Uri("pack://application:,,,/QuickLook.Plugin.AppViewer;component/Resources/ios.png")); + } + + _context.IsBusy = false; + }); + } + }); + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgArchive.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgArchive.cs new file mode 100644 index 0000000..5a54fda --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgArchive.cs @@ -0,0 +1,51 @@ +// 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 DiscUtils.HfsPlus; +using System; +using System.IO; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Dmg; + +public class DmgArchive : IDisposable +{ + public string Entry { get; set; } + + public HfsPlusFileSystem FileSystem { get; set; } + + public void Dispose() + { + FileSystem?.Dispose(); + FileSystem = null; + } + + public byte[] GetBytes() + { + if (Entry is null) + return null; + + if (FileSystem is null) + return null; + + using var stream = FileSystem.OpenFile(Entry, FileMode.Open, FileAccess.Read); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + byte[] fileBytes = ms.ToArray(); + + return fileBytes; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgInfo.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgInfo.cs new file mode 100644 index 0000000..f35b143 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgInfo.cs @@ -0,0 +1,43 @@ +// 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.Drawing; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Dmg; + +public class DmgInfo +{ + public string DisplayName { get; set; } + + public string VersionName { get; set; } + + public string VersionCode { get; set; } + + public string Identifier { get; set; } + + public string MinimumOSVersion { get; set; } + + public string PlatformVersion { get; set; } + + public string SupportedPlatforms { get; set; } + + public string[] Permissions { get; set; } = []; + + public Bitmap Logo { get; set; } + + public bool HasIcon => Logo is not null; +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgParser.cs new file mode 100644 index 0000000..8f3f58c --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgParser.cs @@ -0,0 +1,41 @@ +// 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.Linq; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Dmg; + +public static class DmgParser +{ + public static DmgInfo Parse(string path) + { + using DmgReader reader = new(path); + + return new DmgInfo() + { + DisplayName = reader.DisplayName, + VersionName = reader.ShortVersionString, + VersionCode = reader.Version, + Identifier = reader.Identifier, + MinimumOSVersion = reader.MinimumOSVersion, + SupportedPlatforms = reader.SupportedPlatforms, + PlatformVersion = reader.PlatformVersion, + Permissions = [.. reader.InfoPlistDict.Keys.Where(key => key.StartsWith("NS") && key.EndsWith("UsageDescription"))], + Logo = reader.Logo, + }; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgReader.cs new file mode 100644 index 0000000..8f064eb --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/DmgReader.cs @@ -0,0 +1,216 @@ +// 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 DiscUtils; +using DiscUtils.HfsPlus; +using QuickLook.Plugin.AppViewer.PackageParsers.Ipa; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Text.RegularExpressions; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Dmg; + +public class DmgReader : IDisposable +{ + public string VolumeLabel { get; set; } + + public string ContentsEntry { get; set; } + + public Dictionary Archives { get; } = []; + + public Dictionary InfoPlistDict { get; set; } + + public string DisplayName { get; set; } + + public string ShortVersionString { get; set; } + + public string Version { get; set; } + + public string Identifier { get; set; } + + public byte[] Icon { get; set; } + + public string IconName { get; set; } + + public string IconEntry { get; set; } + + public Bitmap Logo { get; set; } + + public string MinimumOSVersion { get; set; } + + public string PlatformVersion { get; set; } + + public string SupportedPlatforms { get; set; } + + static DmgReader() + { + DiscUtils.Complete.SetupHelper.SetupComplete(); + } + + public DmgReader(string path) + { + Open(path); + } + + public void Dispose() + { + if (Archives is not null) + { + foreach (var archive in Archives.Values) + { + archive.Dispose(); + } + Archives.Clear(); + } + } + + private void Open(string path) + { + using var disk = VirtualDisk.OpenDisk(path, FileAccess.Read, useAsync: false); + if (disk is null) + { + Debug.WriteLine($"Failed to open '{path}' as virtual disk."); + return; + } + + // Find the first (and supposedly, only, HFS partition) + foreach (var volume in VolumeManager.GetPhysicalVolumes(disk)) + { + foreach (var fileSystem in FileSystemManager.DetectFileSystems(volume)) + { + // Apple HFS+ + if (fileSystem.Name == "HFS+") + { + using var hfs = (HfsPlusFileSystem)fileSystem.Open(volume); + + VolumeLabel = hfs.VolumeLabel; + ListFiles(hfs, string.Empty); + } + } + } + + byte[] infoPlistData = null; + + foreach (var archive in Archives.Values) + { + Match m = Regex.Match(archive.Entry, @".*\.app\\Contents\\Info\.plist$"); + + if (m.Success) + { + ContentsEntry = Path.GetDirectoryName(archive.Entry); + infoPlistData = archive.GetBytes(); + if (Plist.ReadPlist(infoPlistData) is Dictionary dict) + { + InfoPlistDict = dict; + } + break; + } + } + + { + if (InfoPlistDict.TryGetValue("CFBundleDisplayName", out object value) && value is string stringValue) + { + DisplayName = stringValue; + } + } + { + if (InfoPlistDict.TryGetValue("CFBundleShortVersionString", out object value) && value is string stringValue) + { + ShortVersionString = stringValue; + } + } + { + if (InfoPlistDict.TryGetValue("CFBundleVersion", out object value) && value is string stringValue) + { + Version = stringValue; + } + } + { + if (InfoPlistDict.TryGetValue("CFBundleIdentifier", out object value) && value is string stringValue) + { + Identifier = stringValue; + } + } + { + if (InfoPlistDict.TryGetValue("LSMinimumSystemVersion", out object value) && value is string stringValue) + { + MinimumOSVersion = $"macOS {stringValue}"; + } + } + { + if (InfoPlistDict.TryGetValue("DTPlatformVersion", out object value) && value is string stringValue) + { + PlatformVersion = $"macOS {stringValue}"; + } + } + { + if (InfoPlistDict.TryGetValue("CFBundleSupportedPlatforms", out object familyNode) && familyNode is IEnumerable list) + { + SupportedPlatforms = string.Join(", ", list); + } + } + { + if (InfoPlistDict.TryGetValue("CFBundleIconFile", out object iconFilesNode) && iconFilesNode is object iconFile) + { + IconName = iconFile as string; + } + } + { + if (!string.IsNullOrWhiteSpace(IconName)) + { + foreach (var archive in Archives.Values) + { + if (archive.Entry.StartsWith($@"{ContentsEntry}\Resources\{IconName}.")) + { + IconEntry = archive.Entry; + Icon = archive.GetBytes(); + + if (Path.GetExtension(IconEntry).ToLower() == ".icns") + { + Logo = IcnsParser.Parse(Icon); + } + break; + } + } + } + } + } + + private void ListFiles(HfsPlusFileSystem fs, string path) + { + foreach (var entry in fs.GetFileSystemEntries(path)) + { + Debug.WriteLine(entry); + + if (fs.DirectoryExists(entry)) + { + ListFiles(fs, entry); + } + else if (fs.FileExists(entry)) + { + Archives.Add(entry, new DmgArchive() + { + Entry = entry, + FileSystem = fs, + }); + } + } + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/IcnsParser.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/IcnsParser.cs new file mode 100644 index 0000000..3c727fb --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Dmg/IcnsParser.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace QuickLook.Plugin.AppViewer.PackageParsers.Dmg; + +public static class IcnsParser +{ + public static Bitmap Parse(byte[] icnsBytes) + { + // Temporary method + + Assembly assembly = AppDomain.CurrentDomain.GetAssemblies() + .Where((assembly) => assembly.FullName.StartsWith("QuickLook.Plugin.ImageViewer")) + .FirstOrDefault(); + + if (assembly == null) + return null; + + Type type = assembly.GetTypes() + .Where(type => type.Name.StartsWith("IcnsImageParser")) + .FirstOrDefault(); + + if (type == null) + return null; + + MethodInfo method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(method => method.Name == "GetImages" + && method.GetParameters().FirstOrDefault()?.ParameterType == typeof(Stream)) + .FirstOrDefault(); + + if (method == null) + return null; + + using MemoryStream stream = new(icnsBytes); + dynamic[] images = method.Invoke(null, [stream]) as dynamic[]; + List bitmaps = []; + + foreach (dynamic image in images) + { + if (image.GetType().GetProperty("Bitmap") is PropertyInfo property) + { + var bitmap = property.GetValue(image); + bitmaps.Add(bitmap); + } + } + + Bitmap imageResult = bitmaps + .Where(bitmap => bitmap != null) + .OrderByDescending(bitmap => bitmap.Width) + .FirstOrDefault() + ?.Clone() as Bitmap; + + foreach (dynamic image in images) + { + if (image.GetType().GetProperty("Bitmap") is PropertyInfo property) + { + if (property.GetValue(image) is IDisposable disposable) + { + disposable.Dispose(); + } + } + } + + return imageResult; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs index 20b9eef..7ddf89e 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/PackageParsers/Ipa/IpaReader.cs @@ -156,6 +156,10 @@ public class IpaReader { IconName = iconFiles.LastOrDefault() as string; } + else if (primaryIcons.TryGetValue("CFBundleIconFile", out object iconFileNode) && iconFileNode is object iconFile) + { + IconName = iconFile as string; + } } } if (string.IsNullOrWhiteSpace(IconName)) @@ -165,6 +169,13 @@ public class IpaReader IconName = iconFiles.LastOrDefault() as string; } } + if (string.IsNullOrWhiteSpace(IconName)) + { + if (InfoPlistDict.TryGetValue("CFBundleIconFile", out object iconFilesNode) && iconFilesNode is object iconFile) + { + IconName = iconFile as string; + } + } if (!string.IsNullOrWhiteSpace(IconName)) { foreach (ZipEntry entry in zip) diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs index a3ffd6a..f796063 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/Plugin.cs @@ -39,7 +39,7 @@ public class Plugin : IViewer ".msix", ".msixbundle", // Windows MSIX installer // macOS - //".dmg", // macOS DMG + ".dmg", // macOS DMG // iOS ".ipa", // iOS IPA @@ -79,6 +79,7 @@ public class Plugin : IViewer ".msi" => new Size { Width = 560, Height = 230 }, ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new Size { Width = 560, Height = 328 }, ".deb" => new Size { Width = 600, Height = 345 }, + ".dmg" => new Size { Width = 560, Height = 510 }, ".wgt" or ".wgtu" => new Size { Width = 600, Height = 345 }, _ => throw new NotSupportedException("Extension is not supported."), }; @@ -100,6 +101,7 @@ public class Plugin : IViewer ".msi" => new MsiInfoPanel(context), ".msix" or ".msixbundle" or ".appx" or ".appxbundle" => new AppxInfoPanel(context), ".deb" => new DebInfoPanel(context), + ".dmg" => new DmgInfoPanel(context), ".wgt" or ".wgtu" => new WgtInfoPanel(context), _ => throw new NotSupportedException("Extension is not supported."), }; diff --git a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj index 63cb04c..0f355e0 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.AppViewer/QuickLook.Plugin.AppViewer.csproj @@ -77,6 +77,7 @@ +