swiftbncslib/Sources/SwiftBncsLib/MpqReader.swift

241 lines
9.5 KiB
Swift

import Foundation
public struct MpqReader {
private enum Crypto {
static let MPQ_HASH_FILE_KEY = 0x300 as UInt32
static let MPQ_HASH_KEY2_MIX = 0x400 as UInt32
static let MPQ_KEY_HASH_TABLE = 0xC3AF3770 as UInt32 // Obtained by HashString("(hash table)", MPQ_HASH_FILE_KEY)
static let MPQ_KEY_BLOCK_TABLE = 0xEC83B3A3 as UInt32
static let StormBuffer: [UInt32] = {
var stormBuffer = [UInt32](repeating: 0, count: 0x500)
var seed = 0x00100001 as UInt32
for i in 0..<0x100 {
for j in stride(from: i, to: i + 0x500, by: 0x100) {
seed = (seed * 125 + 3) % 0x2AAAAB
let temp1 = (seed & 0xFFFF) << 0x10
seed = (seed * 125 + 3) % 0x2AAAAB
let temp2 = (seed & 0xFFFF)
stormBuffer[j] = temp1|temp2
}
}
return stormBuffer
}()
static func decryptMpqBlock(data: Data, length: Int, key: UInt32) -> Data {
var consumer = RawMessageConsumer(message: data)
var composer = RawMessageComposer()
var key1 = key
var key2 = 0xEEEEEEEE as UInt32
let length = length >> 2 // length is expressed in bytes, but decrypting in DWORDs
for _ in 0..<length {
key2 = key2 &+ Crypto.StormBuffer[Int(Crypto.MPQ_HASH_KEY2_MIX &+ (key1 & 0xFF))]
let decrypted = consumer.readUInt32() ^ (key1 &+ key2)
composer.write(decrypted)
key1 = ((~key1 &<< 0x15) + 0x11111111) | (key1 >> 0x0B)
key2 = decrypted &+ key2 &+ (key2 &<< 5) &+ 3
}
return composer.build()
}
enum HashStringMode: Int {
case tableOffset = 0
case nameA = 1
case nameB = 2
case fileKey = 3
}
static func hashString(_ x: String, mode: HashStringMode) -> Int {
var seed1 = 0x7FED7FED as UInt32
var seed2 = 0xEEEEEEEE as UInt32
for x in x.uppercased().bytes {
seed1 = UInt32(Crypto.StormBuffer[mode.rawValue * 0x100 + Int(x)]) ^ (seed1 &+ seed2)
seed2 = UInt32(x) + seed1 &+ seed2 &+ (seed2 &<< 5) + 3
}
return Int(seed1)
}
}
private struct TMPQHeader: CustomDebugStringConvertible {
let identifier: UInt32
let headerSize: UInt32
let archiveSize: UInt32
let formatVersion: UInt16
let blockSize: UInt16
let hashTablePosition: UInt32
let blockTablePosition: UInt32
let hashTableSize: UInt32
let blockTableSize: UInt32
var debugDescription: String {
return String(format: "TMPQHeader<format: \(formatVersion + 1), archiveSize: \(archiveSize), hashTableSize: \(hashTableSize), blockTableSize: \(blockTableSize), hashTablePosition: 0x%X, blockTablePosition: 0x%X>", hashTablePosition, blockTablePosition)
}
}
private struct TMPQHash: CustomDebugStringConvertible {
let nameA: UInt32
let nameB: UInt32
let locale: UInt16
let platform: UInt16
let blockIndex: UInt32
var debugDescription: String {
return "TMPQHash<name: \(nameA) \(nameB), locale: \(locale), platform: \(platform), blockIndex: \(blockIndex)>"
}
}
private struct TMPQBlock: CustomDebugStringConvertible {
let filePosition: UInt32
let compressedSize: UInt32
let uncompressedSize: UInt32
let flags: TMPQBlockFlags
var debugDescription: String {
return "TMPQBlock<pos: \(filePosition), size: \(uncompressedSize), \(flags)>"
}
}
private struct TMPQBlockFlags: OptionSet, CustomDebugStringConvertible {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public static let implode = TMPQBlockFlags(rawValue: 0x00000100)
public static let compress = TMPQBlockFlags(rawValue: 0x00000200)
public static let encrypted = TMPQBlockFlags(rawValue: 0x00010000)
public static let fixKey = TMPQBlockFlags(rawValue: 0x00020000)
public static let patchFile = TMPQBlockFlags(rawValue: 0x00100000)
public static let singleUnit = TMPQBlockFlags(rawValue: 0x01000000)
public static let deleteMarker = TMPQBlockFlags(rawValue: 0x02000000)
public static let sectorCrc = TMPQBlockFlags(rawValue: 0x04000000)
public static let exists = TMPQBlockFlags(rawValue: 0x80000000)
var debugDescription: String {
var flagStrings = [String]()
if self.contains(.implode) { flagStrings.append("implode") }
if self.contains(.compress) { flagStrings.append("compress") }
if self.contains(.encrypted) { flagStrings.append("encrypted") }
if self.contains(.fixKey) { flagStrings.append("fixKey") }
if self.contains(.patchFile) { flagStrings.append("patchFile") }
if self.contains(.singleUnit) { flagStrings.append("singleUnit") }
if self.contains(.deleteMarker) { flagStrings.append("deleteMarker") }
if self.contains(.sectorCrc) { flagStrings.append("sectorCrc") }
if self.contains(.exists) { flagStrings.append("exists") }
return "TMPQBlockFlags<\(flagStrings.joined(separator: ", "))>"
}
}
private let header: TMPQHeader
private let hashTable: [TMPQHash]
private let blockTable: [TMPQBlock]
private var consumer: RawMessageConsumer
public init?(path: String) {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
self.init(data: data)
}
public init(data: Data) {
consumer = RawMessageConsumer(message: data)
var dwID = consumer.readUInt32()
while dwID == 0x1B51504D { // MPQ\1B
print("Encountered TMPQUserData, skipping")
consumer.readIndex -= 4
consumer.readIndex += 0x200
dwID = consumer.readUInt32()
}
guard dwID == 0x1A51504D else { // MPQ\1A
preconditionFailure("Encountered header other than TMPQHeader or TMPQUserData -- bailing")
}
header = TMPQHeader(
identifier: dwID,
headerSize: consumer.readUInt32(),
archiveSize: consumer.readUInt32(),
formatVersion: consumer.readUInt16(),
blockSize: consumer.readUInt16(),
hashTablePosition: consumer.readUInt32(),
blockTablePosition: consumer.readUInt32(),
hashTableSize: consumer.readUInt32(),
blockTableSize: consumer.readUInt32())
print("Parsing MPQ file version \(header)")
let encryptedHashtable = data.subdataFromIndex(Data.Index(header.hashTablePosition), length: Data.Index(header.hashTableSize) * 16)
let decryptedHashtable = Crypto.decryptMpqBlock(data: encryptedHashtable, length: Int(header.hashTableSize) * 16, key: Crypto.MPQ_KEY_HASH_TABLE)
var hashtableConsumer = RawMessageConsumer(message: decryptedHashtable)
var _hashTable = [TMPQHash]()
for _ in 0..<header.hashTableSize {
_hashTable.append(TMPQHash(
nameA: hashtableConsumer.readUInt32(),
nameB: hashtableConsumer.readUInt32(),
locale: hashtableConsumer.readUInt16(),
platform: hashtableConsumer.readUInt16(),
blockIndex: hashtableConsumer.readUInt32()))
}
let encryptedBlocktable = data.subdataFromIndex(Data.Index(header.blockTablePosition), length: Data.Index(header.blockTableSize) * 16)
let decryptedBlocktable = Crypto.decryptMpqBlock(data: encryptedBlocktable, length: Int(header.hashTableSize) * 16, key: Crypto.MPQ_KEY_BLOCK_TABLE)
var blocktableConsumer = RawMessageConsumer(message: decryptedBlocktable)
var _blockTable = [TMPQBlock]()
for _ in 0..<header.blockTableSize {
_blockTable.append(TMPQBlock(
filePosition: blocktableConsumer.readUInt32(),
compressedSize: blocktableConsumer.readUInt32(),
uncompressedSize: blocktableConsumer.readUInt32(),
flags: TMPQBlockFlags(rawValue: blocktableConsumer.readUInt32())))
}
hashTable = _hashTable
blockTable = _blockTable
}
private func hashTableEntryForFilename(name: String) -> TMPQHash? {
let offset = Crypto.hashString(name, mode: .tableOffset) & (Int(header.hashTableSize) - 1)
let nameA = Crypto.hashString(name, mode: .nameA), nameB = Crypto.hashString(name, mode: .nameB)
for index in offset ..< Int(header.hashTableSize) {
let entry = hashTable[index]
if entry.blockIndex == 0xFFFFFFFF {
return nil
}
if entry.nameA == nameA && entry.nameB == nameB {
return hashTable[index]
}
}
return nil
}
public func openFile(name: String) {
guard let entry = hashTableEntryForFilename(name: name) else {
print("unable to open file, does not seem to exist: \(name)")
return
}
let block = blockTable[Int(entry.blockIndex)]
let rawData = consumer.message.subdataFromIndex(Data.Index(block.filePosition), length: Data.Index(block.compressedSize))
// print("entry ): \(blockTable[Int(entry.blockIndex)])")
}
}