import Foundation struct UBJsonReader { private var file: FileHandle init(file: FileHandle) { self.file = file } func read() throws -> UBJsonToken { guard try readCharacter() == "{" else { throw UBReaderError.badFormat("Stream does not start with an object") } return try readObject() } private func parse(type: Character) throws -> UBJsonToken { let oldFormat = true return switch type { case "{": try readObject() case "[": try readArray() case "Z": .null case "N": .noop case "T": .bool(true) case "F": .bool(false) case "i": oldFormat ? .int16(try file.read(as: Int16.self).bigEndian) : .int8(try file.read(as: Int8.self)) case "U": .uint8(try file.read(as: UInt8.self)) case "I": oldFormat ? .int32(try file.read(as: Int32.self).bigEndian) : .int16(try file.read(as: Int16.self).bigEndian) case "l": .int32(try file.read(as: Int32.self).bigEndian) case "L": .int64(try file.read(as: Int64.self).bigEndian) case "d": .float32(try readBeFloatPiss()) case "D": .float64(try readBeFloat()) case "H": throw UBReaderError.badFormat("High-precision numbers are unsupported") case "C": .char(try readCharacter()) case "S": .string(try readString()) default: throw UBReaderError.badToken("Unexpected token \"\(type)\"") } } private func readObject() throws -> UBJsonToken { var items = Dictionary() while true { let type = try readCharacter() var name: String switch type { case "S", "i": name = try readString(type) case "}": return .object(items) default: throw UBReaderError.badToken("Unexpected token while reading object field key") } if items.keys.contains(name) { throw UBReaderError.badFormat("Object contains overlapping keys") } items[name] = try parse(type: try readCharacter()) } } private func readArray() throws -> UBJsonToken { var array = [UBJsonToken]() while true { let type = try readCharacter() switch type { case "]": return .array(array) default: array.append(try parse(type: type)) } } } private func readBeFloatPiss() throws -> Float { guard var bytes = try? file.read(upToCount: 4) else { throw UBReaderError.readError("Read failure while reading float data") } return Float(bitPattern: bytes.withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian) } private func readBeFloat() throws -> T where T.RawSignificand == U { guard var bytes = try? file.read(upToCount: MemoryLayout.size) else { throw UBReaderError.readError("Read failure while reading float data") } bytes.reverse() return T(bytes.withUnsafeBytes { $0.load(as: U.self) }) } private func readCharacter() throws -> Character { guard let raw = try? file.read(as: UInt8.self), let uni = UnicodeScalar(Int(raw)) else { throw UBReaderError.readError("Read failure while reading character") } return Character(uni) } private func readString(_ optType: Character? = nil) throws -> String { let type = optType == nil ? try readCharacter() : optType! var length: Int switch type { case "S": guard try readCharacter() == "i" else { throw UBReaderError.badToken("Malformed string") } fallthrough case "i": length = Int(try file.read(as: Int8.self)) case "s": length = Int(try file.read(as: UInt8.self)) default: throw UBReaderError.badToken("Unexpected token while reading string") } if length < 0 { throw UBReaderError.badToken("Negative string length") } if length == 0 { return "" } guard let data = try file.read(upToCount: length) else { throw UBReaderError.readError("Error reading string") } return String(decoding: data, as: UTF8.self) } } enum UBReaderError: Error { case badFormat(_ message: String) case badToken(_ message: String) case readError(_ message: String) case valueError(_ message: String) } enum UBJsonToken { case object(_ fields: Dictionary) case array(_ items: [UBJsonToken]) case null case noop case bool(_ value: Bool) case int8(_ value: Int8) case uint8(_ value: UInt8) case int16(_ value: Int16) case int32(_ value: Int32) case int64(_ value: Int64) case highPrecision case float32(_ value: Float32) case float64(_ value: Float64) case char(_ value: Character) case string(_ value: String) } extension UBJsonToken { var array: [UBJsonToken] { get throws { if case .array(let items) = self { return items } throw UBReaderError.valueError("Not an array") } } func getArray(key: String) throws -> [UBJsonToken] { if case .object(let fields) = self { guard let child = fields[key] else { throw UBReaderError.valueError("Field \"\(key)\" not found") } return try child.array } throw UBReaderError.valueError("Not an object") } var int16: Int16 { get throws { if case .int16(let value) = self { return value } throw UBReaderError.valueError("Not an int16") } } func getInt16Array(key: String) throws -> [Int16] { try getArray(key: key).map( { i in if case .int8(let value) = i { return Int16(value) } else if case .uint8(let value) = i { return Int16(value) } else if case .int16(let value) = i { return value } else if case .int32(let value) = i { if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") } return Int16(truncatingIfNeeded: value) } else if case .int64(let value) = i { if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") } return Int16(truncatingIfNeeded: value) } else { throw UBReaderError.valueError("Can't read array as int16s") } }) } var float: Float { get throws { if case .float32(let value) = self { return value } throw UBReaderError.valueError("Not a float32") } } func getFloatArray(key: String) throws -> [Float] { try getArray(key: key).map({ try $0.float }) } var string: String { get throws { if case .string(let value) = self { return value } throw UBReaderError.valueError("Not a string") } } func getString(key: String, default defStr: String? = nil) throws -> String { if case .object(let fields) = self { guard let child = fields[key] else { if defStr == nil { throw UBReaderError.valueError("Field \"\(key)\" not found") } return defStr! } return try child.string } throw UBReaderError.valueError("Not an object") } }