Files
QuickLook/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/FaceDatDecoder.cs
ema b385fa7439 Add EIF archive extraction with Face.dat ordering
Introduced EifExtractor to support extracting QQ EIF emoji archives, reordering images based on Face.dat metadata. Updated CompoundFileExtractor with in-memory extraction, enhanced the plugin menu and extraction workflow to prompt for Face.dat ordering, and added translations for the new prompt in Translations.config.
2025-12-26 23:46:29 +08:00

241 lines
8.2 KiB
C#

// 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 <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Text;
namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
/// <summary>
/// Decoder for Face.dat entries used by QQ EIF packages.
/// Re-implements the behavior of the provided Python scripts:
/// - Finds special marker sequence <c>e_str_file_org</c> 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
/// </summary>
public static class FaceDatDecoder
{
/// <summary>
/// Marker sequence used in the Python script
/// </summary>
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];
/// <summary>
/// Decode returns a mapping of group name to a dictionary mapping file name to index within the group.
/// This matches the Python script's <c>group_dict</c> structure.
/// </summary>
/// <param name="fileBytes">The raw bytes of Face.dat.</param>
/// <returns>Nested dictionary: group -> (filename -> index).</returns>
public static Dictionary<string, Dictionary<string, int>> Decode(byte[] fileBytes)
{
return BuildGroupIndex(fileBytes);
}
/// <summary>
/// Build group index mapping from Face.dat bytes like the Python script does.
/// </summary>
/// <param name="fileBytes">The raw bytes of Face.dat.</param>
/// <returns>Dictionary where key is group name and value maps filename to index.</returns>
public static Dictionary<string, Dictionary<string, int>> BuildGroupIndex(byte[] fileBytes)
{
var result = new Dictionary<string, Dictionary<string, int>>(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;
}
/// <summary>
/// Process a single decoded line and update the group dictionary if a valid entry is found.
/// </summary>
private static void ProcessLineForIndex(byte[] line, Dictionary<string, Dictionary<string, int>> 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<string, int>(StringComparer.OrdinalIgnoreCase);
groupDict[group] = files;
}
if (!files.ContainsKey(filename))
{
files[filename] = files.Count;
}
}
/// <summary>
/// Locate subsequence in data starting at fromIndex, return -1 if not found
/// </summary>
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;
}
/// <summary>
/// Equivalent to Python find_key: find a byte that repeats at offsets +2 and +4
/// </summary>
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);
}
/// <summary>
/// Equivalent to Python get_part: extract the encrypted part starting at startIdx-1 up to end
/// </summary>
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<byte>(), end);
var part = new byte[length];
Buffer.BlockCopy(data, start, part, 0, length);
return (part, end);
}
/// <summary>
/// XOR-decode bytes and build a string, ignoring zero bytes (matches Python behavior)
/// </summary>
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();
}
}