swiftbncslib/Sources/SwiftBncsLib/CheckRevision.swift

166 lines
5.4 KiB
Swift

//
// CheckRevision.swift
// SwiftBncsLibPackageDescription
//
// Created by William LaFrance on 3/7/18.
//
import Foundation
enum CheckRevisionError: Error {
case malformedChallenge
case fileError
}
enum CheckRevisionOperation: CChar {
case add = 0x2B
case subtract = 0x2D
case multiply = 0x2A
case xor = 0x5E
}
public typealias CheckRevisionResult = (
version: UInt32,
hash: UInt32,
info: String
)
private typealias CheckRevisionEquation = (
assignIndex: Int,
leftOperandIndex: Int,
rightOperandIndex: Int,
operation: CheckRevisionOperation
)
public enum CheckRevision {
private static let InitialXorValues: [UInt32] = [ 0xE7F4CB62, 0xF6A14FFC, 0xAA5504AF, 0x871FCDC2, 0x11BF6A18, 0xC57292E6, 0x7927D27E, 0x2FEC8733 ]
private static func fileInfoString(file: String) throws -> String {
let attributes = try FileManager.default.attributesOfItem(atPath: file)
guard let creationDate = attributes[.creationDate] as? Date else {
throw CheckRevisionError.fileError
}
guard let size = attributes[.size] as? Int else {
throw CheckRevisionError.fileError
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yy HH:mm:ss"
dateFormatter.timeZone = TimeZone.init(secondsFromGMT: 0)
let timestamp = dateFormatter.string(from: creationDate)
let filename = URL(fileURLWithPath: file).lastPathComponent
return "\(filename) \(timestamp) \(size)"
}
public static func numberForMpqFilename(_ mpqFilename: String) -> Int {
// ver-IX86-6.mpq
return Int(mpqFilename.cString(using: .ascii)![9] - 0x30)
}
public static func hash(mpqFileNumber: Int, challenge: String, files: [String]) throws -> CheckRevisionResult {
func valueIndex(character: CChar) -> Int? {
switch character {
case 0x41: return 0
case 0x42: return 1
case 0x43: return 2
case 0x53: return 3
default: return nil
}
}
var values: [UInt64] = [0, 0, 0, 0]
var equations = [CheckRevisionEquation]()
for challengeToken in challenge.split(separator: " ") {
if !challengeToken.contains("=") {
continue;
}
let challengeTokenChars = challengeToken.cString(using: .ascii)!
if nil != challengeToken.range(of: "[ABC]=\\d+", options: .regularExpression) {
// A=2059673008
guard let index = valueIndex(character: challengeTokenChars[0]) else {
throw CheckRevisionError.malformedChallenge
}
let value = UInt64(challengeToken.split(separator: "=")[1])!
values[index] = value
} else if nil != challengeToken.range(of: "[ABC]=[ABC][+-\\^][ABCS]", options: .regularExpression) {
//A=A-S
guard let assignIndex = valueIndex(character: challengeTokenChars[0]),
let leftOperandIndex = valueIndex(character: challengeTokenChars[2]),
let operation = CheckRevisionOperation(rawValue: challengeTokenChars[3]),
let rightOperandIndex = valueIndex(character: challengeTokenChars[4]) else {
throw CheckRevisionError.malformedChallenge
}
equations.append((
assignIndex: assignIndex,
leftOperandIndex: leftOperandIndex,
rightOperandIndex: rightOperandIndex,
operation: operation
))
} else {
throw CheckRevisionError.malformedChallenge
}
}
values[0] ^= UInt64(InitialXorValues[mpqFileNumber])
for filePath in files {
guard let fileData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
throw CheckRevisionError.fileError
}
var fill: UInt8 = 0xFF
var fileBytes = fileData.arrayOfBytes()
while fileBytes.count % 1024 != 0 {
fileBytes.append(fill)
fill = fill &- 1
}
for index in stride(from: 0, to: fileBytes.count, by: 4) {
values[3] = UInt64(fileBytes[index])
values[3] |= UInt64(fileBytes[index + 1]) << 8
values[3] |= UInt64(fileBytes[index + 2]) << 16
values[3] |= UInt64(fileBytes[index + 3]) << 24
for (assign, lhs, rhs, operation) in equations {
switch operation {
case .add: values[assign] = values[lhs] &+ values[rhs]
case .subtract: values[assign] = values[lhs] &- values[rhs]
case .multiply: values[assign] = values[lhs] &* values[rhs]
case .xor: values[assign] = values[lhs] ^ values[rhs]
}
}
}
}
var exeVersion: UInt32 = 0
do {
exeVersion = try PortableExecutableUtil.getVersion(file: files[0])
} catch (let error) {
print("Caught error trying to get portable executable version: \(files[0]), \(error)")
}
return (
version: exeVersion,
hash: UInt32(truncatingIfNeeded: values[2]),
info: try fileInfoString(file: files[0])
)
}
}