// 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; using System.Collections.Generic; using System.Text; namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary; /// /// Decoder for Face.dat entries used by QQ EIF packages. /// Re-implements the behavior of the provided Python scripts: /// - Finds special marker sequence e_str_file_org inside each line /// - Skips 4 bytes after the marker (same as the Python implementation) /// - Locates a repeating-key pattern and extracts the XOR-encrypted block /// - XOR-decodes the block and parses group\filename entries /// Provides a method to build the same group -> (filename -> index) mapping as the Python tool. /// Reference: https://github.com/readme9txt/QQEIF-Extractor /// public static class FaceDatDecoder { /// /// Marker sequence used in the Python script /// private static readonly byte[] EStrFileOrg = [0x98, 0xEB, 0x9F, 0xEB, 0x99, 0xEB, 0xAD, 0xEB, 0x82, 0xEB, 0x87, 0xEB, 0x8E, 0xEB, 0x84, 0xEB, 0x99, 0xEB, 0x8C, 0xEB]; /// /// Decode returns a mapping of group name to a dictionary mapping file name to index within the group. /// This matches the Python script's group_dict structure. /// /// The raw bytes of Face.dat. /// Nested dictionary: group -> (filename -> index). public static Dictionary> Decode(byte[] fileBytes) { return BuildGroupIndex(fileBytes); } /// /// Build group index mapping from Face.dat bytes like the Python script does. /// /// The raw bytes of Face.dat. /// Dictionary where key is group name and value maps filename to index. public static Dictionary> BuildGroupIndex(byte[] fileBytes) { var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); if (fileBytes == null || fileBytes.Length == 0) return result; // Split into lines by LF, trimming optional CR (same semantics as Python's strip on lines) int lineStart = 0; for (int i = 0; i <= fileBytes.Length; i++) { if (i == fileBytes.Length || fileBytes[i] == (byte)'\n') { int len = i - lineStart; if (len > 0) { // Trim trailing CR if present if (fileBytes[lineStart + len - 1] == (byte)'\r') len--; var line = new byte[len]; Buffer.BlockCopy(fileBytes, lineStart, line, 0, len); ProcessLineForIndex(line, result); } lineStart = i + 1; } } return result; } /// /// Process a single decoded line and update the group dictionary if a valid entry is found. /// private static void ProcessLineForIndex(byte[] line, Dictionary> groupDict) { // Find marker sequence int start = IndexOfSequence(line, EStrFileOrg, 0); if (start == -1) return; // Take bytes after marker plus 4 (matches Python behavior) int partStart = start + EStrFileOrg.Length + 4; if (partStart >= line.Length) return; int partLen = line.Length - partStart; var part = new byte[partLen]; Buffer.BlockCopy(line, partStart, part, 0, partLen); var (key, idx) = FindKey(part, 0); if (key == null) return; var (eStrFileOrgValue, _) = GetPart(part, key.Value, idx); string dPart = XorDecodeToString(eStrFileOrgValue, key.Value); if (string.IsNullOrEmpty(dPart)) return; // If the decoded part contains a colon, the Python script expects it to start with the prefix const string prefix = "UserDataCustomFace"; string remainder; var colonParts = dPart.Split([':'], 2); if (colonParts.Length > 1) { if (!dPart.StartsWith(prefix, StringComparison.Ordinal)) return; // same as Python: skip if prefix missing // Strip prefix and the following ':' int removeLen = prefix.Length + 1; if (dPart.Length <= removeLen) return; remainder = dPart.Substring(removeLen); } else { remainder = dPart; } // Split remainder by backslash to get group and filename var arr = remainder.Split(['\\'], StringSplitOptions.None); if (arr.Length < 2) return; string group = arr[0]; string filename = arr[1]; if (!groupDict.TryGetValue(group, out var files)) { files = new Dictionary(StringComparer.OrdinalIgnoreCase); groupDict[group] = files; } if (!files.ContainsKey(filename)) { files[filename] = files.Count; } } /// /// Locate subsequence in data starting at fromIndex, return -1 if not found /// private static int IndexOfSequence(byte[] data, byte[] seq, int fromIndex) { if (seq.Length == 0) return fromIndex <= data.Length ? fromIndex : -1; for (int i = fromIndex; i <= data.Length - seq.Length; i++) { bool ok = true; for (int j = 0; j < seq.Length; j++) { if (data[i + j] != seq[j]) { ok = false; break; } } if (ok) return i; } return -1; } /// /// Equivalent to Python find_key: find a byte that repeats at offsets +2 and +4 /// private static (byte? key, int seek) FindKey(byte[] data, int startIdx) { for (int i = startIdx; i + 4 < data.Length; i++) { byte b = data[i]; if (b == data[i + 2] && b == data[i + 4]) return (b, i); } return (null, 0); } /// /// Equivalent to Python get_part: extract the encrypted part starting at startIdx-1 up to end /// private static (byte[] part, int end) GetPart(byte[] data, byte key, int startIdx) { int end = 0; for (int i = startIdx; i < data.Length; i += 2) { if (data[i] != key) { end = i - 1; break; } } if (end == 0) end = data.Length - 1; int start = startIdx - 1; int length = end - start; // Python slice end is exclusive => length = end - (startIdx-1) if (length <= 0) return (Array.Empty(), end); var part = new byte[length]; Buffer.BlockCopy(data, start, part, 0, length); return (part, end); } /// /// XOR-decode bytes and build a string, ignoring zero bytes (matches Python behavior) /// private static string XorDecodeToString(byte[] data, byte key) { if (data == null || data.Length == 0) return string.Empty; var sb = new StringBuilder(); foreach (var b in data) { byte v = (byte)(b ^ key); if (v != 0) sb.Append((char)v); } return sb.ToString(); } }