45 changes: 38 additions & 7 deletions GzsTool/Pftxs/PftxsFile.cs
Expand Up @@ -5,12 +5,17 @@
using System.Xml.Serialization;
using GzsTool.Common;
using GzsTool.Common.Interfaces;
using GzsTool.Utility;

namespace GzsTool.Pftxs
{
[XmlType("PftxsFile")]
public class PftxsFile : ArchiveFile
{
private const int PftxMagicNumber = 0x58544650; // PFTX
private const int TexlMagicNumber = 0x4C584554; // TEXL
private const int FtexMagicNumber = 0x58455446; // FTEX

private const long FtexHeaderSize = 16;
private const long TexlHeaderSize = 16;

Expand All @@ -22,6 +27,9 @@ public PftxsFile()
[XmlAttribute("Name")]
public string Name { get; set; }

[XmlAttribute("Endianness")]
public string Endianness { get; set; }

[XmlArray("Entries")]
public List<PftxsFtexFile> Files { get; set; }

Expand All @@ -40,11 +48,28 @@ public static PftxsFile ReadPftxsFile(Stream input)

public override void Read(Stream input)
{
BinaryReader reader = new BinaryReader(input, Encoding.Default, true);
X360Reader reader = new X360Reader(input, Encoding.Default, true, false);
int pftxsMagicNumber = reader.ReadInt32(); // PFTXS
int unknown1 = reader.ReadInt32();
int unknown2 = reader.ReadInt32();
int unknown3 = reader.ReadInt32();
if (pftxsMagicNumber != PftxMagicNumber)
return;

int unknown1 = reader.ReadInt32(); // 0x40 00 00 00
if (unknown1 != 0x40000000)
{
if(unknown1 != 0x40)
return;
reader.BaseStream.Position -= 4;
reader.FlipEndian = true;
unknown1 = reader.ReadInt32();
}
Endianness = reader.FlipEndian ? "Big" : "Little";

int unknown2 = reader.ReadInt32(); // 0x10
if (unknown2 != 0x10)
return;
int unknown3 = reader.ReadInt32(); // 0x1
if (unknown3 != 0x1)
return;

int texlistMagicNumber = reader.ReadInt32(); // TEXL
Size = reader.ReadInt32();
Expand All @@ -54,7 +79,7 @@ public override void Read(Stream input)
for (int i = 0; i < FileCount; i++)
{
PftxsFtexFile pftxsFtexFile = new PftxsFtexFile();
pftxsFtexFile.Read(input);
pftxsFtexFile.Read(reader);
Files.Add(pftxsFtexFile);
}
}
Expand All @@ -77,7 +102,7 @@ public override IEnumerable<FileDataStreamContainer> ExportFiles(Stream input)

public override void Write(Stream output, IDirectory inputDirectory)
{
BinaryWriter writer = new BinaryWriter(output, Encoding.Default, true);
X360Writer writer = new X360Writer(output, Encoding.Default, true, Endianness == "Big");
long ftexHeaderPosition = output.Position;
output.Position += FtexHeaderSize;
long texlHeaderPosition = output.Position;
Expand All @@ -89,13 +114,19 @@ public override void Write(Stream output, IDirectory inputDirectory)

long endPosition = output.Position;
output.Position = ftexHeaderPosition;
writer.Write(0x58544650); // PFTX
writer.FlipEndian = false;
writer.Write(PftxMagicNumber);
writer.FlipEndian = Endianness == "Big";

writer.Write(0x40000000);
writer.Write(0x00000010);
writer.Write(0x00000001);

output.Position = texlHeaderPosition;
writer.FlipEndian = false;
writer.Write(0x4C584554); // TEXL
writer.FlipEndian = Endianness == "Big";

writer.Write(Convert.ToUInt32(endPosition - texlHeaderPosition)); // Size
writer.Write(Convert.ToUInt32(Files.Count));

Expand Down
14 changes: 9 additions & 5 deletions GzsTool/Pftxs/PftxsFtexFile.cs
Expand Up @@ -28,9 +28,8 @@ public class PftxsFtexFile
[XmlArray("Entries")]
public List<PftxsFtexsFileEntry> Entries { get; set; }

public void Read(Stream input)
public void Read(X360Reader reader)
{
BinaryReader reader = new BinaryReader(input, Encoding.Default, true);
long ftexBaseOffset = reader.BaseStream.Position;
int magicNumber = reader.ReadInt32(); // FTEX
int size = reader.ReadInt32();
Expand All @@ -44,10 +43,10 @@ public void Read(Stream input)
for (int i = 0; i < count; i++)
{
PftxsFtexsFileEntry entry = new PftxsFtexsFileEntry();
entry.Read(input);
entry.Read(reader);

string name;
Hashing.TryGetFileNameFromHash(entry.Hash, out name);
Hashing.TryGetFileNameFromHash(entry.Hash, out name, false);
entry.FilePath = Hashing.NormalizeFilePath(name);
Entries.Add(entry);
}
Expand All @@ -59,11 +58,13 @@ public void Read(Stream input)
}
}

public void WriteData(BinaryWriter writer, IDirectory inputDirectory)
public void WriteData(X360Writer writer, IDirectory inputDirectory)
{
long ftexHeaderPosition = writer.BaseStream.Position;
writer.BaseStream.Position += HeaderSize + Entries.Count * PftxsFtexsFileEntry.HeaderSize;

bool flipEndian = writer.FlipEndian;

foreach (var entry in Entries)
{
var data = inputDirectory.ReadFile(entry.FilePath);
Expand All @@ -74,7 +75,10 @@ public void WriteData(BinaryWriter writer, IDirectory inputDirectory)
long endPosition = writer.BaseStream.Position;

writer.BaseStream.Position = ftexHeaderPosition;
writer.FlipEndian = false;
writer.Write(Convert.ToUInt32(0x58455446)); // FTEX
writer.FlipEndian = flipEndian;

writer.Write(Convert.ToUInt32(endPosition - ftexHeaderPosition)); // Size
writer.Write(Hash);
writer.Write(Convert.ToUInt32(Entries.Count));
Expand Down
6 changes: 3 additions & 3 deletions GzsTool/Pftxs/PftxsFtexsFileEntry.cs
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using System.Xml.Serialization;
using GzsTool.Common.Interfaces;
using GzsTool.Utility;

namespace GzsTool.Pftxs
{
Expand All @@ -27,15 +28,14 @@ public class PftxsFtexsFileEntry
public byte[] Data { get; set; }


public void Read(Stream input)
public void Read(X360Reader reader)
{
BinaryReader reader = new BinaryReader(input, Encoding.Default, true);
Hash = reader.ReadUInt64();
Offset = reader.ReadInt32();
Size = reader.ReadInt32();
}

public void Write(BinaryWriter writer)
public void Write(X360Writer writer)
{
writer.Write(Hash);
writer.Write(Offset);
Expand Down
36 changes: 35 additions & 1 deletion GzsTool/Program.cs
Expand Up @@ -27,7 +27,18 @@ private static void Main(string[] args)
string path = args[0];
if (File.Exists(path))
{
if (path.EndsWith(".dat", StringComparison.CurrentCultureIgnoreCase))
bool isQar = false;
using (var fs = File.OpenRead(path))
isQar = QarFile.GetQarVersion(fs) != 0;

if (!isQar && FileIsCryptedSection(path))
{
ReadCryptedSection(path);
return;
}
if (path.EndsWith(".dat", StringComparison.CurrentCultureIgnoreCase) ||
path.EndsWith(".qar", StringComparison.CurrentCultureIgnoreCase) ||
path.EndsWith(".g0s", StringComparison.CurrentCultureIgnoreCase))
{
ReadQarFile(path);
return;
Expand Down Expand Up @@ -97,12 +108,35 @@ private static void ShowUsageInfo()
" GzsTool file_path.fpkd - Unpacks the fpkd file\n" +
" GzsTool file_path.pftxs - Unpacks the pftxs file\n" +
" GzsTool folder_path - Unpacks all fpk and fpkd files in the folder\n" +
" GzsTool encrypted_file.dat - Decrypts encrypted file (eg console foxfs.dat)\n\n" +
" GzsTool file_path.dat.xml - Repacks the qar file\n" +
" GzsTool file_path.fpk.xml - Repacks the fpk file\n" +
" GzsTool file_path.fpkd.xml - Repacks the fpkd file\n" +
" GzsTool file_path.pftxs.xml- Repacks the pftxs file");
}

public static bool FileIsCryptedSection(string filePath)
{
using (var stream = File.OpenRead(filePath))
{
using (var reader = new BinaryReader(stream))
{
var magic = reader.ReadUInt32();
return magic == 0xE3F8EFE6 || magic == 0xA0F8EFE6;
}
}
}

public static void ReadCryptedSection(string path)
{
using (FileStream input = new FileStream(path, FileMode.Open))
{
var retVal = QarEntry.GetOriginalData(input, 0, 0, (uint)input.Length, 2);

File.WriteAllBytes(path + ".dec", retVal.Item3.ToArray());
}
}

private static void ReadQarFile(string path)
{
string fileDirectory = Path.GetDirectoryName(path);
Expand Down
126 changes: 92 additions & 34 deletions GzsTool/Qar/QarEntry.cs
Expand Up @@ -37,6 +37,9 @@ public class QarEntry
[XmlIgnore]
public long DataOffset { get; set; }

[XmlIgnore]
public int QarVersion { get; set; }

// TODO: Enable when the hashing is fixed
////public bool ShouldSerializeHash()
////{
Expand Down Expand Up @@ -66,32 +69,48 @@ private void DebugAssertHashMatches()
ulong newHash = Hashing.HashFileNameWithExtension(FilePath);
Debug.Assert(Hash == newHash);
}

public void Read(BinaryReader reader)
public void Read(BinaryReader reader, int qarVersion = 3)
{
QarVersion = qarVersion;

const uint xorMask1 = 0x41441043;
const uint xorMask2 = 0x11C22050;
const uint xorMask3 = 0xD05608C3;
const uint xorMask4 = 0x532C7319;

uint hashLow = reader.ReadUInt32() ^ xorMask1;
uint hashHigh = reader.ReadUInt32() ^ xorMask1;

uint hashLow = reader.ReadUInt32();
uint hashHigh = reader.ReadUInt32();

if(qarVersion == 3)
{
hashLow = hashLow ^ xorMask1;
hashHigh = hashHigh ^ xorMask1;
}
Hash = (ulong)hashHigh << 32 | hashLow;
UncompressedSize = reader.ReadUInt32() ^ xorMask2;
CompressedSize = reader.ReadUInt32() ^ xorMask3;

Compressed = UncompressedSize != CompressedSize;
if (qarVersion != 3)
DataOffset = reader.ReadUInt32() * 16;

uint md51 = reader.ReadUInt32() ^ xorMask4;
uint md52 = reader.ReadUInt32() ^ xorMask1;
uint md53 = reader.ReadUInt32() ^ xorMask1;
uint md54 = reader.ReadUInt32() ^ xorMask2;
UncompressedSize = reader.ReadUInt32();
if (qarVersion == 3)
{
UncompressedSize = UncompressedSize ^ xorMask2;
CompressedSize = reader.ReadUInt32() ^ xorMask3;
Compressed = UncompressedSize != CompressedSize;

uint md51 = reader.ReadUInt32() ^ xorMask4;
uint md52 = reader.ReadUInt32() ^ xorMask1;
uint md53 = reader.ReadUInt32() ^ xorMask1;
uint md54 = reader.ReadUInt32() ^ xorMask2;
}

string filePath;
FileNameFound = TryGetFilePath(out filePath);
FilePath = filePath;

DataOffset = reader.BaseStream.Position;
if(qarVersion == 3)
DataOffset = reader.BaseStream.Position;
}

public FileDataStreamContainer Export(Stream input)
Expand All @@ -115,50 +134,89 @@ private Func<Stream> ReadDataLazy(Stream input)
};
}

private Stream ReadData(Stream input)
public static Tuple<uint, bool, Stream> GetOriginalData(Stream input, ulong hash, long dataOffset, uint dataSize, int qarVersion = 3)
{
input.Position = DataOffset;
input.Position = dataOffset;
BinaryReader reader = new BinaryReader(input, Encoding.Default, true);

byte[] sectionData = reader.ReadBytes((int)UncompressedSize);
Decrypt1(sectionData, hashLow: (uint) (Hash & 0xFFFFFFFF));
int size = (int)dataSize;
uint key = 0;

byte[] sectionData = reader.ReadBytes(size);

if(qarVersion == 1)
{
sectionData = Encryption.DeEncryptQar(sectionData, (uint)(dataOffset / 16));
const uint keyConstant = 0xA0F8EFE6;
uint peekData = BitConverter.ToUInt32(sectionData, 0);
if (peekData == keyConstant)
{
key = BitConverter.ToUInt32(sectionData, 4);
dataSize -= 8;
byte[] data2 = new byte[sectionData.Length - 8];
Array.Copy(sectionData, 8, data2, 0, sectionData.Length - 8);
sectionData = Encryption.DeEncrypt(data2, key);
}
return new Tuple<uint, bool, Stream>(key, false, new MemoryStream(sectionData));
}

if (qarVersion == 3)
Decrypt1(sectionData, hashLow: (uint)(hash & 0xFFFFFFFF));

bool compressed = false;

uint magicEntry = BitConverter.ToUInt32(sectionData, 0);
if (magicEntry == 0xA0F8EFE6)
{
const int headerSize = 8;
Key = BitConverter.ToUInt32(sectionData, 4);
UncompressedSize -= headerSize;
byte[] newSectionData = new byte[UncompressedSize];
Array.Copy(sectionData, headerSize, newSectionData, 0, UncompressedSize);
Decrypt2(newSectionData, Key);
key = BitConverter.ToUInt32(sectionData, 4);
size -= headerSize;
byte[] newSectionData = new byte[size];
Array.Copy(sectionData, headerSize, newSectionData, 0, size);
Decrypt2(newSectionData, key);
}
else if (magicEntry == 0xE3F8EFE6)
{
const int headerSize = 16;
Key = BitConverter.ToUInt32(sectionData, 4);
UncompressedSize -= headerSize;
byte[] newSectionData = new byte[UncompressedSize];
Array.Copy(sectionData, headerSize, newSectionData, 0, UncompressedSize);
Decrypt2(newSectionData, Key);
key = BitConverter.ToUInt32(sectionData, 4);
if (qarVersion != 3)
{
uint ucsize = BitConverter.ToUInt32(sectionData, 8);
uint csize = BitConverter.ToUInt32(sectionData, 0xC);
compressed = ucsize != csize;
if (ucsize == 0 || csize == 0)
compressed = false;
}
size -= headerSize;

byte[] newSectionData = new byte[size];
Array.Copy(sectionData, headerSize, newSectionData, 0, size);
Decrypt2(newSectionData, key);
sectionData = newSectionData;
}

if (Compressed)
{
if (compressed)
sectionData = Compression.Inflate(sectionData);
}

return new MemoryStream(sectionData);
return new Tuple<uint, bool, Stream>(key, compressed, new MemoryStream(sectionData));
}

private Stream ReadData(Stream input)
{
var retVal = GetOriginalData(input, Hash, DataOffset, UncompressedSize, QarVersion);
Key = retVal.Item1;
Compressed = retVal.Item2;
return retVal.Item3;
}

private bool TryGetFilePath(out string filePath)
{
bool filePathFound = Hashing.TryGetFileNameFromHash(Hash, out filePath);
bool filePathFound = Hashing.TryGetFileNameFromHash(Hash, out filePath, QarVersion == 1);
filePath = Hashing.NormalizeFilePath(filePath);
return filePathFound;
}

private void Decrypt1(byte[] sectionData, uint hashLow)
private static void Decrypt1(byte[] sectionData, uint hashLow)
{
// TODO: Use a ulong array instead.
uint[] decryptionTable =
Expand Down Expand Up @@ -210,7 +268,7 @@ private void Decrypt1(byte[] sectionData, uint hashLow)
}
}

private unsafe void Decrypt2(byte[] input, uint key)
private static unsafe void Decrypt2(byte[] input, uint key)
{
int size = input.Length;
uint currentKey = key | ((key ^ 25974) << 16);
Expand Down
55 changes: 48 additions & 7 deletions GzsTool/Qar/QarFile.cs
Expand Up @@ -12,7 +12,10 @@ namespace GzsTool.Qar
[XmlType("QarFile")]
public class QarFile : ArchiveFile
{
private const int QarMagicNumber = 0x52415153;
private const int QarMagicNumber = 0x52415153; // SQAR

[XmlAttribute("QarVersion")]
public int QarVersion { get; set; } // 1 = GZ (0x14 byte footer), 2 = TPP consoles (0x24 byte footer), 3 = TPP PC (SQAR header)

[XmlAttribute("Name")]
public string Name { get; set; }
Expand All @@ -30,13 +33,27 @@ public static QarFile ReadQarFile(Stream input)
return qarFile;
}

public static bool IsQarFile(Stream input)
public static int GetQarVersion(Stream input)
{
long startPosition = input.Position;
BinaryReader reader = new BinaryReader(input, Encoding.ASCII, true);
reader.BaseStream.Position = 0;
int magicNumber = reader.ReadInt32();
if(magicNumber == QarMagicNumber)
{
input.Position = startPosition;
return 3;
}

reader.BaseStream.Position = reader.BaseStream.Length - 4;
magicNumber = reader.ReadInt32();
input.Position = startPosition;
return magicNumber == QarMagicNumber;
if (magicNumber == 0x14)
return 1;
else if (magicNumber == 0x24)
return 2;

return 0;
}

public override void Read(Stream input)
Expand All @@ -47,9 +64,34 @@ public override void Read(Stream input)
const uint xorMask4 = 0x532C7319;

BinaryReader reader = new BinaryReader(input, Encoding.Default, true);
uint fileCount = 0;
QarVersion = GetQarVersion(input);
if (QarVersion == 0)
return;
if (QarVersion == 1 || QarVersion == 2)
{
reader.BaseStream.Position = reader.BaseStream.Length - 0x14;
fileCount = reader.ReadUInt32();
uint magicNumber1 = reader.ReadUInt32();
uint entryBlock = reader.ReadUInt32();
uint magicNumber2 = reader.ReadUInt32();
uint footerSize = reader.ReadUInt32();

reader.BaseStream.Position = 16 * entryBlock;

Entries = new List<QarEntry>();
for(int i = 0; i < fileCount; i++)
{
var entry = new QarEntry();
entry.Read(reader, QarVersion);
Entries.Add(entry);
}
return;
}

uint magicNumber = reader.ReadUInt32(); // SQAR
Flags = reader.ReadUInt32() ^ xorMask1;
uint fileCount = reader.ReadUInt32() ^ xorMask2;
fileCount = reader.ReadUInt32() ^ xorMask2;
uint unknownCount = reader.ReadUInt32() ^ xorMask3;
uint blockFileEnd = reader.ReadUInt32() ^ xorMask4;
uint offsetFirstFile = reader.ReadUInt32() ^ xorMask1;
Expand All @@ -63,7 +105,7 @@ public override void Read(Stream input)
ulong[] sections = DecryptSectionList(fileCount, sectionsData);
byte[] unknownSectionData = reader.ReadBytes((int)(16 * unknownCount));

List<QarEntry> entries = new List<QarEntry>();
Entries = new List<QarEntry>();
foreach (var section in sections)
{
ulong sectionBlock = section >> 40;
Expand All @@ -73,9 +115,8 @@ public override void Read(Stream input)

var entry = new QarEntry();
entry.Read(reader);
entries.Add(entry);
Entries.Add(entry);
}
Entries = entries;
}

private static ulong[] DecryptSectionList(uint fileCount, byte[] sections)
Expand Down
88 changes: 0 additions & 88 deletions GzsTool/Utility/BigEndianBinaryReader.cs

This file was deleted.

113 changes: 113 additions & 0 deletions GzsTool/Utility/Encryption.cs
@@ -0,0 +1,113 @@
using System;

namespace GzsTool.Utility
{
internal static class Encryption
{
private static ulong ToULong(uint high, uint low)
{
return ((ulong)high << 32) + low;
}

public static byte[] DeEncryptQar(byte[] pData, uint offset)
{
int blockCount = pData.Length / 8;
uint v5 = 8 * ((uint)blockCount + 2 * offset);
int low = (int)(101436752 * offset + 12679594);
uint high = 0;
int bufferOffset = 0;
for (int i = 0; i < blockCount; i++)
{
ulong xorData = (ulong)low - 12679594;
pData[bufferOffset] ^= (byte)(xorData >> 16);
xorData = (ulong)low - 6339797;
pData[bufferOffset + 1] ^= (byte)(xorData >> 16);
xorData = (ulong)low;
pData[bufferOffset + 2] ^= (byte)(xorData >> 16);
xorData = (ulong)low + 6339797;
pData[bufferOffset + 3] ^= (byte)(xorData >> 16);
xorData = (ulong)low + 12679594;
pData[bufferOffset + 4] ^= (byte)(xorData >> 16);
xorData = (ulong)low + 19019391;
pData[bufferOffset + 5] ^= (byte)(xorData >> 16);
xorData = (ulong)low + 25359188;
pData[bufferOffset + 6] ^= (byte)(xorData >> 16);
xorData = (ulong)low + 31698985;
pData[bufferOffset + 7] ^= (byte)(xorData >> 16);
bufferOffset += 8;

low += 50718376;
high = (uint)((ToULong(high, (uint)low) + 50718376) >> 32);
}

int remainingBytes = pData.Length & 7;
uint v10 = 6339797 * v5;
uint v11 = 0;
for (int i = 0; i < remainingBytes; i++)
{
var pair = ToULong(v11, v10);
pData[bufferOffset] ^= (byte)(pair >> 16);
v11 = (uint)((pair + 6339797) >> 32);
v10 += 6339797;
bufferOffset++;
}
return pData;
}

private static void ReplaceUInt(byte[] destinationArray, int offset, uint value)
{
Buffer.BlockCopy(BitConverter.GetBytes(value), 0, destinationArray, offset, sizeof(uint));
}

public static byte[] DeEncrypt(byte[] data, uint key)
{
int offset = 0;
uint i;
int len = data.Length;
byte[] result = new byte[len];
uint v5 = key | ((key ^ 0xFFFFCDEC) << 16);
for (i = 69069 * key; len >= 64; len -= 64)
{
int n = 16;
do
{
uint block = v5 ^ BitConverter.ToUInt32(data, offset);
ReplaceUInt(result, offset, block);
offset += 4;
v5 = 3 * (i + 23023 * v5);
} while (n-- != 1);
}

for (uint v12; len >= 16; v5 = 3 * (i + 23023 * v12))
{
uint b0 = BitConverter.ToUInt32(data, offset);
uint b1 = BitConverter.ToUInt32(data, offset + 4);
uint b2 = BitConverter.ToUInt32(data, offset + 8);
uint b3 = BitConverter.ToUInt32(data, offset + 12);
uint v9 = 3 * (i + 23023 * v5);
ReplaceUInt(result, offset, v5 ^ b0);
uint v11 = 3 * (i + 23023 * v9);
ReplaceUInt(result, offset + 4, v9 ^ b1);
ReplaceUInt(result, offset + 8, v11 ^ b2);
v12 = 3 * (i + 23023 * v11);
ReplaceUInt(result, offset + 12, v12 ^ b3);
len -= 16;
offset += 16;
}

for (; len >= 4; v5 = 3 * (i + 23023 * v5))
{
uint block = v5 ^ BitConverter.ToUInt32(data, offset);
ReplaceUInt(result, offset, block);
len -= 4;
offset += 4;
}

if (len > 0)
{
Buffer.BlockCopy(data, offset, result, offset, len);
}
return result;
}
}
}
168 changes: 164 additions & 4 deletions GzsTool/Utility/Hashing.cs
Expand Up @@ -14,6 +14,7 @@ internal static class Hashing
private static readonly MD5 Md5 = MD5.Create();
private static readonly PathIdFile PathIdFile = new PathIdFile();
private static readonly Dictionary<ulong, string> HashNameDictionary = new Dictionary<ulong, string>();
private static readonly Dictionary<ulong, string> LegacyHashNameDictionary = new Dictionary<ulong, string>();

private static readonly Dictionary<byte[], string> Md5HashNameDictionary =
new Dictionary<byte[], string>(new StructuralEqualityComparer<byte[]>());
Expand Down Expand Up @@ -162,6 +163,117 @@ internal static class Hashing

private static readonly Dictionary<ulong, string> ExtensionsMap = FileExtensions.ToDictionary(HashFileExtension);

private static readonly Dictionary<ulong, string> LegacyExtensionsMap = new Dictionary<ulong, string>
{
{0, ""},
{1, "xml"},
{2, "json"},
{3, "ese"},
{4, "fxp"},
{5, "fpk"},
{6, "fpkd"},
{7, "fpkl"},
{8, "aib"},
{9, "frig"},
{10, "mtar"},
{11, "gani"},
{12, "evb"},
{13, "evf"},
{14, "ag.evf"},
{15, "cc.evf"},
{16, "fx.evf"},
{17, "sd.evf"},
{18, "vo.evf"},
{19, "fsd"},
{20, "fage"},
{21, "fago"},
{22, "fag"},
{23, "fagx"},
{24, "fagp"},
{25, "frdv"},
{26, "fdmg"},
{27, "des"},
{28, "fdes"},
{29, "aibc"},
{30, "mtl"},
{31, "fsml"},
{32, "fox"},
{33, "fox2"},
{34, "las"},
{35, "fstb"},
{36, "lua"},
{37, "fcnp"},
{38, "fcnpx"},
{39, "sub"},
{40, "fova"},
{41, "lad"},
{42, "lani"},
{43, "vfx"},
{44, "vfxbin"},
{45, "frt"},
{46, "gpfp"},
{47, "gskl"},
{48, "geom"},
{49, "tgt"},
{50, "path"},
{51, "fmdl"},
{52, "ftex"},
{53, "htre"},
{54, "tre2"},
{55, "grxla"},
{56, "grxoc"},
{57, "mog"},
{58, "pftxs"},
{59, "nav2"},
{60, "bnd"},
{61, "parts"},
{62, "phsd"},
{63, "ph"},
{64, "veh"},
{65, "sdf"},
{66, "sad"},
{67, "sim"},
{68, "fclo"},
{69, "clo"},
{70, "lng"},
{71, "uig"},
{72, "uil"},
{73, "uif"},
{74, "uia"},
{75, "fnt"},
{76, "utxl"},
{77, "uigb"},
{78, "vfxdb"},
{79, "rbs"},
{80, "aia"},
{81, "aim"},
{82, "aip"},
{83, "aigc"},
{84, "aig"},
{85, "ait"},
{86, "fsm"},
{87, "obr"},
{88, "obrb"},
{89, "lpsh"},
{90, "sani"},
{91, "rdb"},
{92, "phep"},
{93, "simep"},
{94, "atsh"},
{95, "txt"},
{96, "1.ftexs"},
{97, "2.ftexs"},
{98, "3.ftexs"},
{99, "4.ftexs"},
{100, "5.ftexs"},
{101, "sbp"},
{102, "mas"},
{103, "rdf"},
{104, "wem"},
{105, "lba"},
{106, "uilb"}
};

private static ulong HashFileExtension(string fileExtension)
{
return HashFileName(fileExtension, false) & 0x1FFF;
Expand Down Expand Up @@ -193,6 +305,19 @@ private static ulong HashFileName(string text, bool removeExtension = true)
return setFlag ? maskedHash | 0x4000000000000 : maskedHash;
}

private static ulong HashFileNameLegacy(string text, bool removeExtension = true)
{
if (removeExtension)
{
int index = text.LastIndexOf('.');
text = index == -1 ? text : text.Substring(0, index);
}

const ulong seed0 = 0x9ae16a3b2f90404f;
ulong seed1 = text.Length > 0 ? (uint)((text[0]) << 16) + (uint)text.Length : 0;
return CityHash.CityHash.CityHash64WithSeeds(text + "\0", seed0, seed1) & 0xFFFFFFFFFFFF;
}

public static ulong HashFileNameWithExtension(string filePath)
{
filePath = DenormalizeFilePath(filePath);
Expand Down Expand Up @@ -227,7 +352,7 @@ private static string DenormalizeFilePath(string filePath)
return filePath.Replace("\\", "/");
}

internal static bool TryGetFileNameFromHash(ulong hash, out string fileName)
internal static bool TryGetFileNameFromHash(ulong hash, out string fileName, bool legacyQar = false)
{
bool foundFileName = true;
string filePath;
Expand All @@ -236,23 +361,37 @@ internal static bool TryGetFileNameFromHash(ulong hash, out string fileName)
ulong extensionHash = hash >> 51;
ulong pathHash = hash & 0x3FFFFFFFFFFFF;

var extMap = ExtensionsMap;
var altExtMap = LegacyExtensionsMap;

fileName = "";
if (!HashNameDictionary.TryGetValue(pathHash, out filePath))

if(legacyQar)
{
extensionHash = hash >> 52 & 0xFFFF;

// swap the local maps around so it'll try legacy map first, in case the extension ID is the same as a known non-legacy extension hash
altExtMap = ExtensionsMap;
extMap = LegacyExtensionsMap;
}

if (!HashNameDictionary.TryGetValue(pathHash, out filePath) && !LegacyHashNameDictionary.TryGetValue(pathHash, out filePath))
{
filePath = pathHash.ToString("x");
foundFileName = false;
}

fileName += filePath;

if (!ExtensionsMap.TryGetValue(extensionHash, out fileExtension))
if (!extMap.TryGetValue(extensionHash, out fileExtension) && !altExtMap.TryGetValue(extensionHash, out fileExtension))
{
fileExtension = "_unknown";
foundFileName = false;
}
else
{
fileName += ".";
if(!String.IsNullOrEmpty(fileExtension))
fileName += ".";
}
fileName += fileExtension;

Expand Down Expand Up @@ -284,6 +423,27 @@ public static void ReadDictionary(string path)
{
HashNameDictionary.Add(hash, line);
}

hash = HashFileNameLegacy(line) & 0x3FFFFFFFFFFFF;
if (LegacyHashNameDictionary.ContainsKey(hash) == false)
{
LegacyHashNameDictionary.Add(hash, line);
}

if (!line.Contains("."))
continue;

hash = HashFileName(line, false) & 0x3FFFFFFFFFFFF;
if (HashNameDictionary.ContainsKey(hash) == false)
{
HashNameDictionary.Add(hash, line);
}

hash = HashFileNameLegacy(line, false) & 0x3FFFFFFFFFFFF;
if (LegacyHashNameDictionary.ContainsKey(hash) == false)
{
LegacyHashNameDictionary.Add(hash, line);
}
}
}

Expand Down
591 changes: 591 additions & 0 deletions GzsTool/Utility/X360IO.cs

Large diffs are not rendered by default.