diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/ChromiumResourcePackage/PakExtractor.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/ChromiumResourcePackage/PakExtractor.cs new file mode 100644 index 0000000..dab9e21 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/ChromiumResourcePackage/PakExtractor.cs @@ -0,0 +1,231 @@ +// 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 System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; + +namespace QuickLook.Plugin.ArchiveViewer.ChromiumResourcePackage; + +/// +/// Provides static methods for extracting resources from Chrome .pak archive files. +/// +public static class PakExtractor +{ + /// + /// Extracts all resources from a Chrome .pak file and saves them as individual files in the specified output directory. + /// Each resource file is named using a 9-digit zero-padded decimal string representing its resource ID (e.g., "000000001"). + /// + /// The path to the .pak file to extract. + /// The directory where extracted resource files will be saved. + public static void ExtractToDirectory(string fileName, string outputDirectory) + { + using var stream = File.OpenRead(fileName); + using var br = new BinaryReader(stream); + var version = br.ReadUInt32(); + var encoding = br.ReadByte(); + stream.Seek(3, SeekOrigin.Current); // Skip 3 reserved bytes + var resourceCount = br.ReadUInt16(); + var aliasCount = br.ReadUInt16(); + + Entry[] entries = new Entry[resourceCount + 1]; + for (int i = 0; i < resourceCount + 1; i++) + { + var resourceId = br.ReadUInt16(); + var fileOffset = br.ReadUInt32(); + entries[i] = new Entry(resourceId, fileOffset); + } + // Aliases are not used in extraction, so just skip reading if not needed + stream.Seek(aliasCount * 4, SeekOrigin.Current); + + Directory.CreateDirectory(outputDirectory); + + // Use a single buffer for all resources (max resource size) + int maxLength = 0; + for (int i = 0; i < resourceCount; i++) + { + int len = (int)(entries[i + 1].FileOffset - entries[i].FileOffset); + if (len > maxLength) maxLength = len; + } + byte[] buffer = ArrayPool.Shared.Rent(maxLength); + + try + { + for (int i = 0; i < resourceCount; i++) + { + int length = (int)(entries[i + 1].FileOffset - entries[i].FileOffset); + stream.Seek(entries[i].FileOffset, SeekOrigin.Begin); + int read = 0; + while (read < length) + { + int n = stream.Read(buffer, read, length - read); + if (n == 0) break; + read += n; + } + string resourceName = entries[i].ResourceId.ToString("D9"); + using var file = File.Create(Path.Combine(outputDirectory, resourceName)); + file.Write(buffer, 0, length); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Extracts all resources from a Chrome .pak file and returns them as a dictionary. + /// The dictionary key is a 9-digit zero-padded decimal string representing the resource ID, + /// optionally with a guessed file extension, and the value is the resource content as a byte array. + /// + /// The path to the .pak file to extract. + /// If true, append guessed file extension to the key (e.g., "000000001.png"). + /// A dictionary mapping resource names to their byte content. + public static Dictionary ExtractToDictionary(string fileName, bool appendExtension = true) + { + using var stream = File.OpenRead(fileName); + using var br = new BinaryReader(stream); + var version = br.ReadUInt32(); + var encoding = br.ReadByte(); + stream.Seek(3, SeekOrigin.Current); // Skip 3 reserved bytes + var resourceCount = br.ReadUInt16(); + var aliasCount = br.ReadUInt16(); + + Entry[] entries = new Entry[resourceCount + 1]; + for (int i = 0; i < resourceCount + 1; i++) + { + var resourceId = br.ReadUInt16(); + var fileOffset = br.ReadUInt32(); + entries[i] = new Entry(resourceId, fileOffset); + } + // Aliases are not used in extraction, so just skip reading if not needed + stream.Seek(aliasCount * 4, SeekOrigin.Current); + + // Use a single buffer for all resources (max resource size) + int maxLength = 0; + for (int i = 0; i < resourceCount; i++) + { + int len = (int)(entries[i + 1].FileOffset - entries[i].FileOffset); + if (len > maxLength) maxLength = len; + } + byte[] buffer = ArrayPool.Shared.Rent(maxLength); + var dict = new Dictionary(resourceCount); + + try + { + for (int i = 0; i < resourceCount; i++) + { + int length = (int)(entries[i + 1].FileOffset - entries[i].FileOffset); + stream.Seek(entries[i].FileOffset, SeekOrigin.Begin); + int read = 0; + while (read < length) + { + int n = stream.Read(buffer, read, length - read); + if (n == 0) break; + read += n; + } + string resourceName = entries[i].ResourceId.ToString("D9"); + if (appendExtension) + { + string ext = GuessFileExtension(buffer, length); + resourceName += ext; + } + // Copy only the valid part of buffer + var data = new byte[length]; + Buffer.BlockCopy(buffer, 0, data, 0, length); + dict[resourceName] = data; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + return dict; + } + + /// + /// Guesses the file extension based on the content of the given byte array. + /// Returns common extensions such as "png", "jpg", "gif", "bmp", "pdf", "zip", etc. + /// Returns ".bin" if the type cannot be determined. + /// + /// The byte array containing the file data. + /// The valid length of data to check (for pooled buffer usage). + /// The guessed file extension with dot, e.g. ".png". + public static string GuessFileExtension(byte[] data, int length = -1) + { + if (data == null || (length < 0 ? data.Length : length) < 4) + return ".bin"; + int len = length < 0 ? data.Length : length; + // PNG + if (len > 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) + return ".png"; + // JPEG + if (data[0] == 0xFF && data[1] == 0xD8) + return ".jpg"; + // GIF + if (data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) + return ".gif"; + // BMP + if (data[0] == 0x42 && data[1] == 0x4D) + return ".bmp"; + // PDF + if (data[0] == 0x25 && data[1] == 0x50 && data[2] == 0x44 && data[3] == 0x46) + return ".pdf"; + // ZIP/Office + if (data[0] == 0x50 && data[1] == 0x4B && (data[2] == 0x03 || data[2] == 0x05 || data[2] == 0x07) && (data[3] == 0x04 || data[3] == 0x06 || data[3] == 0x08)) + return ".zip"; + // RAR + if (len > 7 && data[0] == 0x52 && data[1] == 0x61 && data[2] == 0x72 && data[3] == 0x21 && data[4] == 0x1A && data[5] == 0x07 && (data[6] == 0x00 || data[6] == 0x01)) + return ".rar"; + // 7z + if (len > 5 && data[0] == 0x37 && data[1] == 0x7A && data[2] == 0xBC && data[3] == 0xAF && data[4] == 0x27 && data[5] == 0x1C) + return ".7z"; + // MP3 + if (len > 2 && data[0] == 0x49 && data[1] == 0x44 && data[2] == 0x33) + return ".mp3"; + // MP4 + if (len > 11 && data[4] == 0x66 && data[5] == 0x74 && data[6] == 0x79 && data[7] == 0x70) + return ".mp4"; + // WebP + if (len > 11 && data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 && data[8] == 0x57 && data[9] == 0x45 && data[10] == 0x42 && data[11] == 0x50) + return ".webp"; + // TXT (heuristic: printable ASCII, no nulls) + bool isText = true; + for (int i = 0; i < Math.Min(64, len); i++) + { + if (data[i] == 0 || (data[i] < 0x09) || (data[i] > 0x0D && data[i] < 0x20)) + { + isText = false; + break; + } + } + if (isText) + return ".txt"; + return ".bin"; + } +} + +/// +/// Represents a resource entry in the .pak file, containing the resource ID and its file offset. +/// +public record struct Entry(ushort ResourceId, uint FileOffset); + +/// +/// Represents an alias entry in the .pak file, mapping a resource ID to an entry index. +/// +public record struct Alias(ushort ResourceId, ushort EntryIndex); diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs index fb51bd5..084ea7b 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/Plugin.cs @@ -18,6 +18,7 @@ using QuickLook.Common.Plugin; using QuickLook.Common.Plugin.MoreMenu; using QuickLook.Plugin.ArchiveViewer.ArchiveFile; +using QuickLook.Plugin.ArchiveViewer.ChromiumResourcePackage; using QuickLook.Plugin.ArchiveViewer.CompoundFileBinary; using System; using System.Collections.Generic; @@ -55,6 +56,9 @@ public sealed partial class Plugin : IViewer, IMoreMenu // List of supported compound file binary file extensions ".cfb", // Compound File Binary format (used by older Microsoft Office files) ".eif", // QQ emoji file (Compound File Binary format) + + // List of supported chromium resource package file extensions + ".pak", // Chromium resource package file, used by Chromium-based applications (e.g., Google Chrome) ]; private IDisposable _panel; @@ -86,6 +90,13 @@ public sealed partial class Plugin : IViewer, IMoreMenu { _panel = new CompoundInfoPanel(path); } + else if (path.EndsWith(".pak", StringComparison.OrdinalIgnoreCase)) + { + var dict = PakExtractor.ExtractToDictionary(path); + + // TODO + _ = dict; + } else { _panel = new ArchiveInfoPanel(path);