// 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. /// If true, append guessed file extension to the filename (e.g., "000000001.png"). public static void ExtractToDirectory(string fileName, string outputDirectory, 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); 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"); if (appendExtension) { string ext = GuessFileExtension(buffer, length); resourceName += ext; } 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);