pkNX/pkNX.Containers/Misc/GFPack.cs
Kurt e432370a40 Add Pokémon Legends: Arceus support
.NET5.0 -> .NET6.0

Co-Authored-By: SciresM <8676005+SciresM@users.noreply.github.com>
2022-02-04 18:43:21 -08:00

442 lines
17 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace pkNX.Containers
{
public class GFPack : IEnumerable<byte[]>, IFileContainer
{
public const ulong Magic = 0x4B434150_584C4647; // GFLXPACK
// Overall structure: Header, metadata, and the raw compressed files
public GFPackHeader Header { get; set; }
public GFPackPointers Pointers { get; set; }
public FileHashAbsolute[] HashAbsolute { get; set; }
public FileHashFolder[] HashInFolder { get; set; }
public FileData[] FileTable { get; set; }
public byte[][] CompressedFiles { get; set; }
public byte[][] DecompressedFiles { get; set; }
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public GFPack(string path) : this(FileMitm.ReadAllBytes(path)) => FilePath = path;
public GFPack(string[] directories, string parent = @"\bin") => LoadFiles(directories, parent);
/// <summary>
/// Initializes a packed <see cref="GFPack"/> object, and unpacks the data to accessible properties.
/// </summary>
/// <param name="data">Packed file</param>
public GFPack(byte[] data)
{
using var ms = new MemoryStream(data);
using var br = new BinaryReader(ms);
ReadPack(br);
}
public GFPack(BinaryReader br) => ReadPack(br);
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private void ReadPack(BinaryReader br)
{
Header = br.ReadBytes(GFPackHeader.SIZE).ToClass<GFPackHeader>();
Debug.Assert(Header.MAGIC == Magic);
Pointers = new GFPackPointers(br, Header.CountFolders);
Debug.Assert(Pointers.PtrHashPaths == br.BaseStream.Position);
HashAbsolute = new FileHashAbsolute[Header.CountFiles];
for (int i = 0; i < HashAbsolute.Length; i++)
HashAbsolute[i] = br.ReadBytes(FileHashAbsolute.SIZE).ToClass<FileHashAbsolute>();
HashInFolder = new FileHashFolder[Header.CountFolders];
for (int f = 0; f < HashInFolder.Length; f++)
{
Debug.Assert(Pointers.PtrHashFolders[f] == br.BaseStream.Position);
var table = HashInFolder[f] = new FileHashFolder
{
Folder = br.ReadBytes(FileHashIndex.SIZE).ToClass<FileHashFolderInfo>()
};
table.Files = new FileHashIndex[table.Folder.FileCount];
for (int i = 0; i < table.Files.Length; i++)
table.Files[i] = br.ReadBytes(FileHashIndex.SIZE).ToClass<FileHashIndex>();
}
Debug.Assert(Pointers.PtrFileTable == br.BaseStream.Position);
FileTable = new FileData[Header.CountFiles];
for (int i = 0; i < FileTable.Length; i++)
FileTable[i] = br.ReadBytes(FileData.SIZE).ToClass<FileData>();
CompressedFiles = new byte[Header.CountFiles][];
for (int i = 0; i < CompressedFiles.Length; i++)
{
br.BaseStream.Position = FileTable[i].OffsetPacked;
CompressedFiles[i] = br.ReadBytes(FileTable[i].SizeCompressed);
}
DecompressedFiles = new byte[Header.CountFiles][];
for (int i = 0; i < DecompressedFiles.Length; i++)
DecompressedFiles[i] = Decompress(CompressedFiles[i], FileTable[i].SizeDecompressed, FileTable[i].Type);
}
public IEnumerator<byte[]> GetEnumerator() => (IEnumerator<byte[]>)DecompressedFiles.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => DecompressedFiles.GetEnumerator();
public int Count => Header.CountFiles;
public byte[] this[int index]
{
get => (byte[])DecompressedFiles[index].Clone();
set
{
Modified |= !DecompressedFiles[index].SequenceEqual(value);
DecompressedFiles[index] = value;
}
}
public int GetIndexFull(ulong hash) => Array.FindIndex(HashAbsolute, z => z.HashFnv1aPathFull == hash);
public int GetIndexFileName(ulong hash)
{
foreach (var f in HashInFolder)
{
int index = f.GetIndexFileName(hash);
if (index >= 0)
return f.Files[index].Index;
}
return -1;
}
public int GetIndexFileName(string name)
{
foreach (var f in HashInFolder)
{
int index = f.GetIndexFileName(name);
if (index >= 0)
return f.Files[index].Index;
}
return -1;
}
public byte[] GetDataFileName(string name)
{
int index = GetIndexFileName(name);
return DecompressedFiles[index];
}
public byte[] GetDataFull(ulong hash) => DecompressedFiles[GetIndexFull(hash)];
public byte[] GetDataFullPath(string path) => GetDataFull(FnvHash.HashFnv1a_64(path));
public void SetDataFileName(string name, byte[] data)
{
int index = GetIndexFileName(name);
DecompressedFiles[index] = data;
}
public void LoadFiles(string[] directories, string parent, CompressionType type = CompressionType.Lz4)
{
var groups = directories.Select(Directory.GetFiles).ToArray();
var files = groups.SelectMany(z => z).ToArray();
Header = new GFPackHeader { CountFolders = directories.Length, CountFiles = files.Length };
HashAbsolute = new FileHashAbsolute[files.Length];
HashInFolder = new FileHashFolder[groups.Length];
FileTable = new FileData[files.Length];
DecompressedFiles = new byte[files.Length][];
CompressedFiles = new byte[files.Length][];
for (int f = 0; f < groups.Length; f++)
{
var folderFiles = groups[f];
var folderName = Path.GetDirectoryName(directories[f]);
var table = HashInFolder[f] = new FileHashFolder();
table.Folder = new FileHashFolderInfo
{
FileCount = folderFiles.Length,
HashFnv1aPathFolderName = FnvHash.HashFnv1a_64(folderName),
};
table.Files = new FileHashIndex[folderFiles.Length];
for (int i = 0; i < folderFiles.Length; i++)
{
var file = folderFiles[i];
int index = Array.IndexOf(files, folderFiles[i]);
var nameshort = Path.GetFileName(file);
ulong hashShort = FnvHash.HashFnv1a_64(nameshort);
table.Files[i] = new FileHashIndex { HashFnv1aPathFileName = hashShort, Index = index };
}
}
for (var i = 0; i < files.Length; i++)
{
var file = files[i];
var namelong = file[file.IndexOf(parent, StringComparison.Ordinal)..];
ulong hashFull = FnvHash.HashFnv1a_64(namelong);
HashAbsolute[i] = new FileHashAbsolute { HashFnv1aPathFull = hashFull };
FileTable[i] = new FileData { Type = type };
DecompressedFiles[i] = FileMitm.ReadAllBytes(file);
}
Modified = true;
}
public byte[] Write()
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);
WriteHeaderTableList(bw);
for (var i = 0; i < DecompressedFiles.Length; i++)
{
var entry = FileTable[i];
var f = DecompressedFiles[i];
var c = Compress(f, entry.Type);
CompressedFiles[i] = c;
// update entry details
entry.SizeDecompressed = f.Length;
entry.SizeCompressed = c.Length;
entry.OffsetPacked = (int)bw.BaseStream.Position;
bw.Write(c);
while (bw.BaseStream.Position % 0x10 != 0) // pad to nearest 0x10 alignment
bw.Write((byte)0);
}
bw.BaseStream.Position = 0;
WriteHeaderTableList(bw);
return ms.ToArray();
}
private static byte[] Decompress(byte[] encryptedData, int decryptedLength, CompressionType type)
{
return type switch
{
CompressionType.None => encryptedData,
CompressionType.Zlib => throw new NotSupportedException(nameof(CompressionType.Zlib)), // not implemented
CompressionType.Lz4 => LZ4.Decode(encryptedData, decryptedLength),
CompressionType.OodleKraken => Oodle.Decompress(encryptedData, decryptedLength)!,
CompressionType.OodleLeviathan => Oodle.Decompress(encryptedData, decryptedLength)!,
CompressionType.OodleMermaid => Oodle.Decompress(encryptedData, decryptedLength)!,
CompressionType.OodleSelkie => Oodle.Decompress(encryptedData, decryptedLength)!,
CompressionType.OodleHydra => Oodle.Decompress(encryptedData, decryptedLength)!,
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
}
private static byte[] Compress(byte[] decryptedData, CompressionType type)
{
return type switch
{
CompressionType.None => decryptedData,
CompressionType.Zlib => throw new NotSupportedException(nameof(CompressionType.Zlib)), // not implemented
CompressionType.Lz4 => LZ4.Encode(decryptedData),
CompressionType.OodleKraken => Oodle.Compress(decryptedData, out _, OodleFormat.Kraken).ToArray(),
CompressionType.OodleLeviathan => Oodle.Compress(decryptedData, out _, OodleFormat.Leviathan).ToArray(),
CompressionType.OodleMermaid => Oodle.Compress(decryptedData, out _, OodleFormat.Mermaid).ToArray(),
CompressionType.OodleSelkie => Oodle.Compress(decryptedData, out _, OodleFormat.Selkie).ToArray(),
CompressionType.OodleHydra => Oodle.Compress(decryptedData, out _, OodleFormat.Hydra).ToArray(),
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
}
public void CancelEdits()
{
for (int i = 0; i < DecompressedFiles.Length; i++)
DecompressedFiles[i] = Decompress(CompressedFiles[i], FileTable[i].SizeDecompressed, FileTable[i].Type);
Modified = false;
}
private void WriteHeaderTableList(BinaryWriter bw)
{
bw.Write(Header.ToBytesClass());
Pointers.Write(bw);
Pointers.PtrHashPaths = bw.BaseStream.Position;
foreach (var hp in HashAbsolute)
bw.Write(hp.ToBytesClass());
for (var f = 0; f < Pointers.PtrHashFolders.Length; f++)
{
Pointers.PtrHashFolders[f] = bw.BaseStream.Position;
var folder = HashInFolder[f];
folder.Folder.FileCount = folder.Files.Length;
bw.Write(folder.Folder.ToBytesClass());
foreach (var hi in folder.Files)
bw.Write(hi.ToBytesClass());
}
Pointers.PtrFileTable = bw.BaseStream.Position;
foreach (var ft in FileTable)
bw.Write(ft.ToBytesClass());
}
public string? FilePath { get; set; }
public bool Modified { get; set; }
public Task<byte[][]> GetFiles() => Task.FromResult(DecompressedFiles);
public Task<byte[]> GetFile(int file, int subFile = 0) => Task.FromResult(this[file]);
public Task SetFile(int file, byte[] value, int subFile = 0) => Task.FromResult(this[file] = value);
public Task SaveAs(string path, ContainerHandler handler, CancellationToken token) => new(() => FileMitm.WriteAllBytes(path, Write()), token);
public void Dump(string path, ContainerHandler handler)
{
handler.Initialize(FileTable.Length);
string format = this.GetFileFormatString();
foreach (var grp in HashInFolder)
{
var dirName = grp.Folder.HashFnv1aPathFolderName;
foreach (var f in grp.Files)
{
var index = f.Index;
var name = f.HashFnv1aPathFileName;
var fn = $"{index.ToString(format)} {name:X16}.bin";
var data = DecompressedFiles[f.Index];
var subfolder = HashInFolder.Length == 1 ? fn : Path.Combine(dirName.ToString("X16"), fn);
var loc = Path.Combine(path, subfolder);
FileMitm.WriteAllBytes(loc, data);
}
}
}
}
/// <summary>
/// Intro bytes to the packed binary.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public class GFPackHeader
{
public const int SIZE = 0x18;
public ulong MAGIC = GFPack.Magic;
public uint Version = 0x1000;
public uint IsRelocated; // bit0
/// <summary>
/// Count of Files packed into the binary.
/// </summary>
public int CountFiles;
/// <summary>
/// Count of Folders packed into the binary.
/// </summary>
public int CountFolders;
}
public class GFPackPointers
{
/// <summary>
/// Data offset for the <see cref="FileData"/> table.
/// </summary>
public long PtrFileTable; // array stored at end
/// <summary>
/// Data offset for the <see cref="FileHashAbsolute"/> table.
/// </summary>
public long PtrHashPaths; // array stored first
/// <summary>
/// Data offset for the <see cref="FileHashFolder"/> table, which has a leading <see cref="FileHashFolderInfo"/>.
/// </summary>
public long[] PtrHashFolders; // array stored in middle
// immediately after the pointers are the arrays
public GFPackPointers(BinaryReader br, int folderCount)
{
PtrFileTable = br.ReadInt64();
PtrHashPaths = br.ReadInt64();
PtrHashFolders = new long[folderCount];
for (int i = 0; i < PtrHashFolders.Length; i++)
PtrHashFolders[i] = br.ReadInt64();
}
public void Write(BinaryWriter bw)
{
bw.Write(PtrFileTable);
bw.Write(PtrHashPaths);
foreach (var table in PtrHashFolders)
bw.Write(table);
}
}
[StructLayout(LayoutKind.Sequential)]
public class FileHashAbsolute
{
public const int SIZE = 0x08;
/// <summary>
/// Filename (with directory details) hash.
/// </summary>
public ulong HashFnv1aPathFull;
public bool IsMatch(string fileName) => FnvHash.HashFnv1a_64(fileName) == HashFnv1aPathFull;
}
public class FileHashFolder
{
public FileHashFolderInfo Folder = new();
public FileHashIndex[] Files = Array.Empty<FileHashIndex>();
public int GetIndexFileName(ulong hash) => Array.FindIndex(Files, z => z.HashFnv1aPathFileName == hash);
public int GetIndexFileName(string name) => Array.FindIndex(Files, z => z.IsMatch(name));
}
[StructLayout(LayoutKind.Sequential)]
public class FileHashFolderInfo
{
public const int SIZE = 0x10;
/// <summary>
/// Filename (without directory details) hash.
/// </summary>
public ulong HashFnv1aPathFolderName;
public int FileCount;
public uint Padding = 0xCC;
public bool IsMatch(string fileName) => FnvHash.HashFnv1a_64(fileName) == HashFnv1aPathFolderName;
}
[StructLayout(LayoutKind.Sequential)]
public class FileHashIndex
{
public const int SIZE = 0x10;
/// <summary>
/// Filename (without directory details) hash.
/// </summary>
public ulong HashFnv1aPathFileName;
public int Index;
public uint Padding = 0xCC;
public bool IsMatch(string fileName) => FnvHash.HashFnv1a_64(fileName) == HashFnv1aPathFileName;
}
[StructLayout(LayoutKind.Sequential)]
public class FileData
{
public const int SIZE = 0x18;
public ushort Level = 9; // quality?
public CompressionType Type;
public int SizeDecompressed;
public int SizeCompressed;
public int Padding = 0xCC;
public int OffsetPacked;
public uint unused;
}
public enum CompressionType : ushort
{
None = 0,
Zlib = 1,
Lz4 = 2,
OodleKraken = 3,
OodleLeviathan = 4,
OodleMermaid = 5,
OodleSelkie = 6,
OodleHydra = 7,
}
}