├── .gitignore ├── LICENSE.md ├── README.md ├── convertFromJSON.swift ├── docs └── apple_crash_report_format.pdf └── symbolicate.swift /.gitignore: -------------------------------------------------------------------------------- 1 | *.dSYM 2 | *.crash 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomasz Kucharski (tomieq) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppleCrashScripts 2 | 3 | convertFromJSON.swift 4 | - script for converting .ips files from new Apple JSON Crash format (used on iOS15 devices) to old style type crash report: 5 | 6 | `swift convertFromJSON.swift -i {your_json_ips_file} -o {name_for_file_where_crash_will_be_saved}` 7 | 8 | symbolicate.swift 9 | - script symbolicates crash report. It looks for the right dSYM files itself - you don't have to specify which one to use. Especially useful when you have a lof of dSYM files and you don't remember which one is the one. Simply put the script on the file system in the same directory where dSYM files are stored(unzipped!). Then call the script telling which crash report you want to symbolicate: 10 | 11 | `swift symbolicate.swift -crash {your_crash_file_name}` 12 | 13 | ## Documentation 14 | The Apple crash report fields are described in [official article](https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report) 15 | 16 | As Apple likes to remove docs, the copy can be found [here](./docs/apple_crash_report_format.pdf) 17 | -------------------------------------------------------------------------------- /convertFromJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // convertFromJSON.swift 3 | // 4 | // Created by Tomasz Kucharski on 07/10/2021. 5 | // 6 | 7 | import Foundation 8 | 9 | final class CrashTranslator { 10 | 11 | func run(jsonFile: String, outputFile: String) { 12 | let pwd = shell("pwd") 13 | print("Working in \(pwd)") 14 | 15 | do { 16 | let jsonUrl = URL(fileURLWithPath: pwd).appendingPathComponent(jsonFile) 17 | 18 | guard FileManager.default.fileExists(atPath: jsonUrl.path) else { 19 | print("JSON file \(jsonUrl.path) does not exists!") 20 | exit(0) 21 | } 22 | 23 | let loadedFile = try String(contentsOfFile: jsonUrl.path) 24 | var linesInFile = loadedFile.components(separatedBy: "\n") 25 | let ipsHeader = JSON(parseJSON: linesInFile.removeFirst()) 26 | let payload = JSON(parseJSON: linesInFile.joined(separator: "\n")) 27 | let content = try self.convert(ipsHeader: ipsHeader, payload: payload) 28 | try content.write(to: URL(fileURLWithPath: pwd).appendingPathComponent(outputFile), atomically: true, encoding: .utf8) 29 | } catch { 30 | print("\(error)") 31 | } 32 | } 33 | 34 | private func convert(ipsHeader: JSON, payload: JSON) throws -> String { 35 | 36 | 37 | var content = self.buildHeader(ipsHeader, payload) 38 | let binaryImages = payload["usedImages"] 39 | 40 | if let threads = payload["threads"].array { 41 | for (id, thread) in threads.enumerated() { 42 | content.append("\n") 43 | if let name = thread["name"].string ?? thread["queue"].string { 44 | content.append("Thread %d name: %@\n".format(id, name)) 45 | } 46 | if thread["triggered"].bool ?? false { 47 | content.append("Thread %d Crashed:\n".format(id)) 48 | } else { 49 | content.append("Thread %d:\n".format(id)) 50 | } 51 | content.append(self.buildFrameStack(frames: thread["frames"], binaryImages: binaryImages)) 52 | } 53 | } 54 | content.append(self.buildBinaryImages(binaryImages)) 55 | return content 56 | } 57 | 58 | private func buildFrameStack(frames: JSON, binaryImages: JSON) -> String { 59 | var content = "" 60 | for (id, frame) in (frames.array ?? []).enumerated() { 61 | let binaryImage = binaryImages[frame["imageIndex"].intValue] 62 | let address = frame["imageOffset"].intValue + binaryImage["base"].intValue 63 | content.append("%d".format(id).padding(length: 5)) 64 | content.append(binaryImage["name"].stringValue.padding(length: 40)) 65 | content.append("0x%llx ".format(address)) 66 | if let symbol = frame["symbol"].string, let symbolLocation = frame["symbolLocation"].int { 67 | content.append("\(symbol) + \(symbolLocation)") 68 | } else { 69 | content.append("0x%llx + %d".format(binaryImage["base"].int64Value, frame["imageOffset"].intValue)) 70 | } 71 | if let sourceFile = frame["sourceFile"].string, let sourceLine = frame["sourceLine"].int { 72 | content.append(" (\(sourceFile):\(sourceLine))") 73 | } 74 | content.append("\n") 75 | } 76 | return content 77 | } 78 | 79 | private func buildBinaryImages(_ binaryImages: JSON) -> String { 80 | var content = "\nBinary Images:\n" 81 | for image in binaryImages.arrayValue { 82 | content.append("0x%llx - 0x%llx ".format(image["base"].intValue, image["base"].intValue + image["size"].intValue - 1)) 83 | content.append("%@ %@ ".format(image["name"].stringValue, image["arch"].stringValue)) 84 | content.append("<%@> %@\n".format(image["uuid"].stringValue.replacingOccurrences(of: "-", with: ""), image["path"].stringValue)) 85 | } 86 | return content 87 | } 88 | 89 | private func buildHeader(_ ipsHeader: JSON, _ payload: JSON) -> String { 90 | var content = "" 91 | content.append("Incident Identifier: %@\n".format(ipsHeader["incident_id"].stringValue)) 92 | content.append("CrashReporter Key: %@\n".format(payload["crashReporterKey"].stringValue)) 93 | content.append("Hardware Model: %@\n".format(payload["modelCode"].stringValue)) 94 | content.append("Process: %@ [%@]\n".format(payload["procName"].stringValue, payload["pid"].stringValue)) 95 | content.append("Path: %@\n".format(payload["procPath"].stringValue)) 96 | if payload["bundleInfo"].exists() { 97 | let bundleInfo = payload["bundleInfo"] 98 | content.append("Identifier: %@\n".format(bundleInfo["CFBundleIdentifier"].stringValue)) 99 | content.append("Version: %@ (%@)\n" 100 | .format(bundleInfo["CFBundleShortVersionString"].stringValue, bundleInfo["CFBundleVersion"].stringValue)) 101 | } 102 | content.append("Report Version: 104\n") 103 | content.append("Code Type: %@ (Native(?))\n".format(payload["cpuType"].stringValue)) 104 | content.append("Role: %@\n".format(payload["procRole"].stringValue)) 105 | content.append("Parent Process: %@ [%@]\n".format(payload["parentProc"].stringValue, payload["parentPid"].stringValue)) 106 | content.append("Coalition: %@ [%@]\n".format(payload["coalitionName"].stringValue, payload["coalitionID"].stringValue)) 107 | content.append("\n") 108 | content.append("Date/Time: %@\n".format(payload["captureTime"].stringValue)) 109 | content.append("Launch Time: %@\n".format(payload["procLaunch"].stringValue)) 110 | content.append("OS Version: %@\n".format(ipsHeader["os_version"].stringValue)) 111 | content.append("Release Type: %@\n".format(payload["osVersion"]["releaseType"].stringValue)) 112 | content.append("Baseband Version: %@\n".format(payload["basebandVersion"].stringValue)) 113 | content.append("\n") 114 | let exception = payload["exception"] 115 | content.append("Exception Type: %@ (%@)\n".format(exception["type"].stringValue, exception["signal"].stringValue)) 116 | content.append("Exception Codes: %@\n".format(exception["codes"].stringValue)) 117 | content.append("Triggered by Thread: %@\n".format(payload["faultingThread"].stringValue)) 118 | content.append("\n") 119 | return content 120 | } 121 | 122 | @discardableResult 123 | func shell(_ command: String) -> String { 124 | let task = Process() 125 | let pipe = Pipe() 126 | 127 | task.standardOutput = pipe 128 | task.standardError = pipe 129 | task.arguments = ["-c", command] 130 | task.launchPath = "/bin/zsh" 131 | task.launch() 132 | 133 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 134 | let output = String(data: data, encoding: .utf8)! 135 | 136 | return output.trimmingCharacters(in: .newlines) 137 | } 138 | } 139 | 140 | extension String { 141 | func format(_ args: CVarArg...) -> String { 142 | return String(format: self, arguments: args) 143 | } 144 | 145 | func padding(length: Int) -> String { 146 | return self.padding(toLength: length, withPad: " ", startingAt: 0) 147 | } 148 | } 149 | 150 | // SwiftyJSON.swift 151 | // 152 | // Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang 153 | // 154 | // Permission is hereby granted, free of charge, to any person obtaining a copy 155 | // of this software and associated documentation files (the "Software"), to deal 156 | // in the Software without restriction, including without limitation the rights 157 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 158 | // copies of the Software, and to permit persons to whom the Software is 159 | // furnished to do so, subject to the following conditions: 160 | // 161 | // The above copyright notice and this permission notice shall be included in 162 | // all copies or substantial portions of the Software. 163 | // 164 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 165 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 166 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 167 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 168 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 169 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 170 | // THE SOFTWARE. 171 | 172 | // MARK: - Error 173 | // swiftlint:disable line_length 174 | public enum SwiftyJSONError: Int, Swift.Error { 175 | case unsupportedType = 999 176 | case indexOutOfBounds = 900 177 | case elementTooDeep = 902 178 | case wrongType = 901 179 | case notExist = 500 180 | case invalidJSON = 490 181 | } 182 | 183 | extension SwiftyJSONError: CustomNSError { 184 | 185 | /// return the error domain of SwiftyJSONError 186 | public static var errorDomain: String { return "com.swiftyjson.SwiftyJSON" } 187 | 188 | /// return the error code of SwiftyJSONError 189 | public var errorCode: Int { return self.rawValue } 190 | 191 | /// return the userInfo of SwiftyJSONError 192 | public var errorUserInfo: [String: Any] { 193 | switch self { 194 | case .unsupportedType: 195 | return [NSLocalizedDescriptionKey: "It is an unsupported type."] 196 | case .indexOutOfBounds: 197 | return [NSLocalizedDescriptionKey: "Array Index is out of bounds."] 198 | case .wrongType: 199 | return [NSLocalizedDescriptionKey: "Couldn't merge, because the JSONs differ in type on top level."] 200 | case .notExist: 201 | return [NSLocalizedDescriptionKey: "Dictionary key does not exist."] 202 | case .invalidJSON: 203 | return [NSLocalizedDescriptionKey: "JSON is invalid."] 204 | case .elementTooDeep: 205 | return [NSLocalizedDescriptionKey: "Element too deep. Increase maxObjectDepth and make sure there is no reference loop."] 206 | } 207 | } 208 | } 209 | 210 | // MARK: - JSON Type 211 | 212 | /** 213 | JSON's type definitions. 214 | 215 | See http://www.json.org 216 | */ 217 | public enum Type: Int { 218 | case number 219 | case string 220 | case bool 221 | case array 222 | case dictionary 223 | case null 224 | case unknown 225 | } 226 | 227 | // MARK: - JSON Base 228 | 229 | public struct JSON { 230 | 231 | /** 232 | Creates a JSON using the data. 233 | 234 | - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary 235 | - parameter opt: The JSON serialization reading options. `[]` by default. 236 | 237 | - returns: The created JSON 238 | */ 239 | public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws { 240 | let object: Any = try JSONSerialization.jsonObject(with: data, options: opt) 241 | self.init(jsonObject: object) 242 | } 243 | 244 | /** 245 | Creates a JSON object 246 | - note: this does not parse a `String` into JSON, instead use `init(parseJSON: String)` 247 | 248 | - parameter object: the object 249 | 250 | - returns: the created JSON object 251 | */ 252 | public init(_ object: Any) { 253 | switch object { 254 | case let object as Data: 255 | do { 256 | try self.init(data: object) 257 | } catch { 258 | self.init(jsonObject: NSNull()) 259 | } 260 | default: 261 | self.init(jsonObject: object) 262 | } 263 | } 264 | 265 | /** 266 | Parses the JSON string into a JSON object 267 | 268 | - parameter json: the JSON string 269 | 270 | - returns: the created JSON object 271 | */ 272 | public init(parseJSON jsonString: String) { 273 | if let data = jsonString.data(using: .utf8) { 274 | self.init(data) 275 | } else { 276 | self.init(NSNull()) 277 | } 278 | } 279 | 280 | /** 281 | Creates a JSON using the object. 282 | 283 | - parameter jsonObject: The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity. 284 | 285 | - returns: The created JSON 286 | */ 287 | fileprivate init(jsonObject: Any) { 288 | object = jsonObject 289 | } 290 | 291 | /** 292 | Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added, 293 | present values getting overwritten, array values getting appended and nested JSONs getting merged the same way. 294 | 295 | - parameter other: The JSON which gets merged into this JSON 296 | 297 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level. 298 | */ 299 | public mutating func merge(with other: JSON) throws { 300 | try self.merge(with: other, typecheck: true) 301 | } 302 | 303 | /** 304 | Merges another JSON into this JSON and returns a new JSON, whereas primitive values which are not present in this JSON are getting added, 305 | present values getting overwritten, array values getting appended and nested JSONS getting merged the same way. 306 | 307 | - parameter other: The JSON which gets merged into this JSON 308 | 309 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level. 310 | 311 | - returns: New merged JSON 312 | */ 313 | public func merged(with other: JSON) throws -> JSON { 314 | var merged = self 315 | try merged.merge(with: other, typecheck: true) 316 | return merged 317 | } 318 | 319 | /** 320 | Private woker function which does the actual merging 321 | Typecheck is set to true for the first recursion level to prevent total override of the source JSON 322 | */ 323 | fileprivate mutating func merge(with other: JSON, typecheck: Bool) throws { 324 | if type == other.type { 325 | switch type { 326 | case .dictionary: 327 | for (key, _) in other { 328 | try self[key].merge(with: other[key], typecheck: false) 329 | } 330 | case .array: 331 | self = JSON(arrayValue + other.arrayValue) 332 | default: 333 | self = other 334 | } 335 | } else { 336 | if typecheck { 337 | throw SwiftyJSONError.wrongType 338 | } else { 339 | self = other 340 | } 341 | } 342 | } 343 | 344 | /// Private object 345 | fileprivate var rawArray: [Any] = [] 346 | fileprivate var rawDictionary: [String: Any] = [:] 347 | fileprivate var rawString: String = "" 348 | fileprivate var rawNumber: NSNumber = 0 349 | fileprivate var rawNull: NSNull = NSNull() 350 | fileprivate var rawBool: Bool = false 351 | 352 | /// JSON type, fileprivate setter 353 | public fileprivate(set) var type: Type = .null 354 | 355 | /// Error in JSON, fileprivate setter 356 | public fileprivate(set) var error: SwiftyJSONError? 357 | 358 | /// Object in JSON 359 | public var object: Any { 360 | get { 361 | switch type { 362 | case .array: return rawArray 363 | case .dictionary: return rawDictionary 364 | case .string: return rawString 365 | case .number: return rawNumber 366 | case .bool: return rawBool 367 | default: return rawNull 368 | } 369 | } 370 | set { 371 | error = nil 372 | switch unwrap(newValue) { 373 | case let number as NSNumber: 374 | if number.isBool { 375 | type = .bool 376 | rawBool = number.boolValue 377 | } else { 378 | type = .number 379 | rawNumber = number 380 | } 381 | case let string as String: 382 | type = .string 383 | rawString = string 384 | case _ as NSNull: 385 | type = .null 386 | case Optional.none: 387 | type = .null 388 | case let array as [Any]: 389 | type = .array 390 | rawArray = array 391 | case let dictionary as [String: Any]: 392 | type = .dictionary 393 | rawDictionary = dictionary 394 | default: 395 | type = .unknown 396 | error = SwiftyJSONError.unsupportedType 397 | } 398 | } 399 | } 400 | 401 | /// The static null JSON 402 | @available(*, unavailable, renamed:"null") 403 | public static var nullJSON: JSON { return null } 404 | public static var null: JSON { return JSON(NSNull()) } 405 | } 406 | 407 | /// Private method to unwarp an object recursively 408 | private func unwrap(_ object: Any) -> Any { 409 | switch object { 410 | case let json as JSON: 411 | return unwrap(json.object) 412 | case let array as [Any]: 413 | return array.map(unwrap) 414 | case let dictionary as [String: Any]: 415 | var d = dictionary 416 | dictionary.forEach { pair in 417 | d[pair.key] = unwrap(pair.value) 418 | } 419 | return d 420 | default: 421 | return object 422 | } 423 | } 424 | 425 | public enum Index: Comparable { 426 | case array(Int) 427 | case dictionary(DictionaryIndex) 428 | case null 429 | 430 | static public func == (lhs: Index, rhs: Index) -> Bool { 431 | switch (lhs, rhs) { 432 | case (.array(let left), .array(let right)): return left == right 433 | case (.dictionary(let left), .dictionary(let right)): return left == right 434 | case (.null, .null): return true 435 | default: return false 436 | } 437 | } 438 | 439 | static public func < (lhs: Index, rhs: Index) -> Bool { 440 | switch (lhs, rhs) { 441 | case (.array(let left), .array(let right)): return left < right 442 | case (.dictionary(let left), .dictionary(let right)): return left < right 443 | default: return false 444 | } 445 | } 446 | } 447 | 448 | public typealias JSONIndex = Index 449 | public typealias JSONRawIndex = Index 450 | 451 | extension JSON: Swift.Collection { 452 | 453 | public typealias Index = JSONRawIndex 454 | 455 | public var startIndex: Index { 456 | switch type { 457 | case .array: return .array(rawArray.startIndex) 458 | case .dictionary: return .dictionary(rawDictionary.startIndex) 459 | default: return .null 460 | } 461 | } 462 | 463 | public var endIndex: Index { 464 | switch type { 465 | case .array: return .array(rawArray.endIndex) 466 | case .dictionary: return .dictionary(rawDictionary.endIndex) 467 | default: return .null 468 | } 469 | } 470 | 471 | public func index(after i: Index) -> Index { 472 | switch i { 473 | case .array(let idx): return .array(rawArray.index(after: idx)) 474 | case .dictionary(let idx): return .dictionary(rawDictionary.index(after: idx)) 475 | default: return .null 476 | } 477 | } 478 | 479 | public subscript (position: Index) -> (String, JSON) { 480 | switch position { 481 | case .array(let idx): return (String(idx), JSON(rawArray[idx])) 482 | case .dictionary(let idx): return (rawDictionary[idx].key, JSON(rawDictionary[idx].value)) 483 | default: return ("", JSON.null) 484 | } 485 | } 486 | } 487 | 488 | // MARK: - Subscript 489 | 490 | /** 491 | * To mark both String and Int can be used in subscript. 492 | */ 493 | public enum JSONKey { 494 | case index(Int) 495 | case key(String) 496 | } 497 | 498 | public protocol JSONSubscriptType { 499 | var jsonKey: JSONKey { get } 500 | } 501 | 502 | extension Int: JSONSubscriptType { 503 | public var jsonKey: JSONKey { 504 | return JSONKey.index(self) 505 | } 506 | } 507 | 508 | extension String: JSONSubscriptType { 509 | public var jsonKey: JSONKey { 510 | return JSONKey.key(self) 511 | } 512 | } 513 | 514 | extension JSON { 515 | 516 | /// If `type` is `.array`, return json whose object is `array[index]`, otherwise return null json with error. 517 | fileprivate subscript(index index: Int) -> JSON { 518 | get { 519 | if type != .array { 520 | var r = JSON.null 521 | r.error = self.error ?? SwiftyJSONError.wrongType 522 | return r 523 | } else if rawArray.indices.contains(index) { 524 | return JSON(rawArray[index]) 525 | } else { 526 | var r = JSON.null 527 | r.error = SwiftyJSONError.indexOutOfBounds 528 | return r 529 | } 530 | } 531 | set { 532 | if type == .array && 533 | rawArray.indices.contains(index) && 534 | newValue.error == nil { 535 | rawArray[index] = newValue.object 536 | } 537 | } 538 | } 539 | 540 | /// If `type` is `.dictionary`, return json whose object is `dictionary[key]` , otherwise return null json with error. 541 | fileprivate subscript(key key: String) -> JSON { 542 | get { 543 | var r = JSON.null 544 | if type == .dictionary { 545 | if let o = rawDictionary[key] { 546 | r = JSON(o) 547 | } else { 548 | r.error = SwiftyJSONError.notExist 549 | } 550 | } else { 551 | r.error = self.error ?? SwiftyJSONError.wrongType 552 | } 553 | return r 554 | } 555 | set { 556 | if type == .dictionary && newValue.error == nil { 557 | rawDictionary[key] = newValue.object 558 | } 559 | } 560 | } 561 | 562 | /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`. 563 | fileprivate subscript(sub sub: JSONSubscriptType) -> JSON { 564 | get { 565 | switch sub.jsonKey { 566 | case .index(let index): return self[index: index] 567 | case .key(let key): return self[key: key] 568 | } 569 | } 570 | set { 571 | switch sub.jsonKey { 572 | case .index(let index): self[index: index] = newValue 573 | case .key(let key): self[key: key] = newValue 574 | } 575 | } 576 | } 577 | 578 | /** 579 | Find a json in the complex data structures by using array of Int and/or String as path. 580 | 581 | Example: 582 | 583 | ``` 584 | let json = JSON[data] 585 | let path = [9,"list","person","name"] 586 | let name = json[path] 587 | ``` 588 | 589 | The same as: let name = json[9]["list"]["person"]["name"] 590 | 591 | - parameter path: The target json's path. 592 | 593 | - returns: Return a json found by the path or a null json with error 594 | */ 595 | public subscript(path: [JSONSubscriptType]) -> JSON { 596 | get { 597 | return path.reduce(self) { $0[sub: $1] } 598 | } 599 | set { 600 | switch path.count { 601 | case 0: return 602 | case 1: self[sub:path[0]].object = newValue.object 603 | default: 604 | var aPath = path 605 | aPath.remove(at: 0) 606 | var nextJSON = self[sub: path[0]] 607 | nextJSON[aPath] = newValue 608 | self[sub: path[0]] = nextJSON 609 | } 610 | } 611 | } 612 | 613 | /** 614 | Find a json in the complex data structures by using array of Int and/or String as path. 615 | 616 | - parameter path: The target json's path. Example: 617 | 618 | let name = json[9,"list","person","name"] 619 | 620 | The same as: let name = json[9]["list"]["person"]["name"] 621 | 622 | - returns: Return a json found by the path or a null json with error 623 | */ 624 | public subscript(path: JSONSubscriptType...) -> JSON { 625 | get { 626 | return self[path] 627 | } 628 | set { 629 | self[path] = newValue 630 | } 631 | } 632 | } 633 | 634 | // MARK: - LiteralConvertible 635 | 636 | extension JSON: Swift.ExpressibleByStringLiteral { 637 | 638 | public init(stringLiteral value: StringLiteralType) { 639 | self.init(value) 640 | } 641 | 642 | public init(extendedGraphemeClusterLiteral value: StringLiteralType) { 643 | self.init(value) 644 | } 645 | 646 | public init(unicodeScalarLiteral value: StringLiteralType) { 647 | self.init(value) 648 | } 649 | } 650 | 651 | extension JSON: Swift.ExpressibleByIntegerLiteral { 652 | 653 | public init(integerLiteral value: IntegerLiteralType) { 654 | self.init(value) 655 | } 656 | } 657 | 658 | extension JSON: Swift.ExpressibleByBooleanLiteral { 659 | 660 | public init(booleanLiteral value: BooleanLiteralType) { 661 | self.init(value) 662 | } 663 | } 664 | 665 | extension JSON: Swift.ExpressibleByFloatLiteral { 666 | 667 | public init(floatLiteral value: FloatLiteralType) { 668 | self.init(value) 669 | } 670 | } 671 | 672 | extension JSON: Swift.ExpressibleByDictionaryLiteral { 673 | public init(dictionaryLiteral elements: (String, Any)...) { 674 | let dictionary = elements.reduce(into: [String: Any](), { $0[$1.0] = $1.1}) 675 | self.init(dictionary) 676 | } 677 | } 678 | 679 | extension JSON: Swift.ExpressibleByArrayLiteral { 680 | 681 | public init(arrayLiteral elements: Any...) { 682 | self.init(elements) 683 | } 684 | } 685 | 686 | // MARK: - Raw 687 | 688 | extension JSON: Swift.RawRepresentable { 689 | 690 | public init?(rawValue: Any) { 691 | if JSON(rawValue).type == .unknown { 692 | return nil 693 | } else { 694 | self.init(rawValue) 695 | } 696 | } 697 | 698 | public var rawValue: Any { 699 | return object 700 | } 701 | 702 | public func rawData(options opt: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0)) throws -> Data { 703 | guard JSONSerialization.isValidJSONObject(object) else { 704 | throw SwiftyJSONError.invalidJSON 705 | } 706 | 707 | return try JSONSerialization.data(withJSONObject: object, options: opt) 708 | } 709 | 710 | public func rawString(_ encoding: String.Encoding = .utf8, options opt: JSONSerialization.WritingOptions = .prettyPrinted) -> String? { 711 | do { 712 | return try _rawString(encoding, options: [.jsonSerialization: opt]) 713 | } catch { 714 | print("Could not serialize object to JSON because:", error.localizedDescription) 715 | return nil 716 | } 717 | } 718 | 719 | public func rawString(_ options: [writingOptionsKeys: Any]) -> String? { 720 | let encoding = options[.encoding] as? String.Encoding ?? String.Encoding.utf8 721 | let maxObjectDepth = options[.maxObjextDepth] as? Int ?? 10 722 | do { 723 | return try _rawString(encoding, options: options, maxObjectDepth: maxObjectDepth) 724 | } catch { 725 | print("Could not serialize object to JSON because:", error.localizedDescription) 726 | return nil 727 | } 728 | } 729 | 730 | fileprivate func _rawString(_ encoding: String.Encoding = .utf8, options: [writingOptionsKeys: Any], maxObjectDepth: Int = 10) throws -> String? { 731 | guard maxObjectDepth > 0 else { throw SwiftyJSONError.invalidJSON } 732 | switch type { 733 | case .dictionary: 734 | do { 735 | if !(options[.castNilToNSNull] as? Bool ?? false) { 736 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted 737 | let data = try rawData(options: jsonOption) 738 | return String(data: data, encoding: encoding) 739 | } 740 | 741 | guard let dict = object as? [String: Any?] else { 742 | return nil 743 | } 744 | let body = try dict.keys.map { key throws -> String in 745 | guard let value = dict[key] else { 746 | return "\"\(key)\": null" 747 | } 748 | guard let unwrappedValue = value else { 749 | return "\"\(key)\": null" 750 | } 751 | 752 | let nestedValue = JSON(unwrappedValue) 753 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else { 754 | throw SwiftyJSONError.elementTooDeep 755 | } 756 | if nestedValue.type == .string { 757 | return "\"\(key)\": \"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" 758 | } else { 759 | return "\"\(key)\": \(nestedString)" 760 | } 761 | } 762 | 763 | return "{\(body.joined(separator: ","))}" 764 | } catch _ { 765 | return nil 766 | } 767 | case .array: 768 | do { 769 | if !(options[.castNilToNSNull] as? Bool ?? false) { 770 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted 771 | let data = try rawData(options: jsonOption) 772 | return String(data: data, encoding: encoding) 773 | } 774 | 775 | guard let array = object as? [Any?] else { 776 | return nil 777 | } 778 | let body = try array.map { value throws -> String in 779 | guard let unwrappedValue = value else { 780 | return "null" 781 | } 782 | 783 | let nestedValue = JSON(unwrappedValue) 784 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else { 785 | throw SwiftyJSONError.invalidJSON 786 | } 787 | if nestedValue.type == .string { 788 | return "\"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" 789 | } else { 790 | return nestedString 791 | } 792 | } 793 | 794 | return "[\(body.joined(separator: ","))]" 795 | } catch _ { 796 | return nil 797 | } 798 | case .string: return rawString 799 | case .number: return rawNumber.stringValue 800 | case .bool: return rawBool.description 801 | case .null: return "null" 802 | default: return nil 803 | } 804 | } 805 | } 806 | 807 | // MARK: - Printable, DebugPrintable 808 | 809 | extension JSON: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible { 810 | 811 | public var description: String { 812 | return rawString(options: .prettyPrinted) ?? "unknown" 813 | } 814 | 815 | public var debugDescription: String { 816 | return description 817 | } 818 | } 819 | 820 | // MARK: - Array 821 | 822 | extension JSON { 823 | 824 | //Optional [JSON] 825 | public var array: [JSON]? { 826 | return type == .array ? rawArray.map { JSON($0) } : nil 827 | } 828 | 829 | //Non-optional [JSON] 830 | public var arrayValue: [JSON] { 831 | return self.array ?? [] 832 | } 833 | 834 | //Optional [Any] 835 | public var arrayObject: [Any]? { 836 | get { 837 | switch type { 838 | case .array: return rawArray 839 | default: return nil 840 | } 841 | } 842 | set { 843 | self.object = newValue ?? NSNull() 844 | } 845 | } 846 | } 847 | 848 | // MARK: - Dictionary 849 | 850 | extension JSON { 851 | 852 | //Optional [String : JSON] 853 | public var dictionary: [String: JSON]? { 854 | if type == .dictionary { 855 | var d = [String: JSON](minimumCapacity: rawDictionary.count) 856 | rawDictionary.forEach { pair in 857 | d[pair.key] = JSON(pair.value) 858 | } 859 | return d 860 | } else { 861 | return nil 862 | } 863 | } 864 | 865 | //Non-optional [String : JSON] 866 | public var dictionaryValue: [String: JSON] { 867 | return dictionary ?? [:] 868 | } 869 | 870 | //Optional [String : Any] 871 | 872 | public var dictionaryObject: [String: Any]? { 873 | get { 874 | switch type { 875 | case .dictionary: return rawDictionary 876 | default: return nil 877 | } 878 | } 879 | set { 880 | object = newValue ?? NSNull() 881 | } 882 | } 883 | } 884 | 885 | // MARK: - Bool 886 | 887 | extension JSON { // : Swift.Bool 888 | 889 | //Optional bool 890 | public var bool: Bool? { 891 | get { 892 | switch type { 893 | case .bool: return rawBool 894 | default: return nil 895 | } 896 | } 897 | set { 898 | object = newValue ?? NSNull() 899 | } 900 | } 901 | 902 | //Non-optional bool 903 | public var boolValue: Bool { 904 | get { 905 | switch type { 906 | case .bool: return rawBool 907 | case .number: return rawNumber.boolValue 908 | case .string: return ["true", "y", "t", "yes", "1"].contains { rawString.caseInsensitiveCompare($0) == .orderedSame } 909 | default: return false 910 | } 911 | } 912 | set { 913 | object = newValue 914 | } 915 | } 916 | } 917 | 918 | // MARK: - String 919 | 920 | extension JSON { 921 | 922 | //Optional string 923 | public var string: String? { 924 | get { 925 | switch type { 926 | case .string: return object as? String 927 | default: return nil 928 | } 929 | } 930 | set { 931 | object = newValue ?? NSNull() 932 | } 933 | } 934 | 935 | //Non-optional string 936 | public var stringValue: String { 937 | get { 938 | switch type { 939 | case .string: return object as? String ?? "" 940 | case .number: return rawNumber.stringValue 941 | case .bool: return (object as? Bool).map { String($0) } ?? "" 942 | default: return "" 943 | } 944 | } 945 | set { 946 | object = newValue 947 | } 948 | } 949 | } 950 | 951 | // MARK: - Number 952 | 953 | extension JSON { 954 | 955 | //Optional number 956 | public var number: NSNumber? { 957 | get { 958 | switch type { 959 | case .number: return rawNumber 960 | case .bool: return NSNumber(value: rawBool ? 1 : 0) 961 | default: return nil 962 | } 963 | } 964 | set { 965 | object = newValue ?? NSNull() 966 | } 967 | } 968 | 969 | //Non-optional number 970 | public var numberValue: NSNumber { 971 | get { 972 | switch type { 973 | case .string: 974 | let decimal = NSDecimalNumber(string: object as? String) 975 | return decimal == .notANumber ? .zero : decimal 976 | case .number: return object as? NSNumber ?? NSNumber(value: 0) 977 | case .bool: return NSNumber(value: rawBool ? 1 : 0) 978 | default: return NSNumber(value: 0.0) 979 | } 980 | } 981 | set { 982 | object = newValue 983 | } 984 | } 985 | } 986 | 987 | // MARK: - Null 988 | 989 | extension JSON { 990 | 991 | public var null: NSNull? { 992 | set { 993 | object = NSNull() 994 | } 995 | get { 996 | switch type { 997 | case .null: return rawNull 998 | default: return nil 999 | } 1000 | } 1001 | } 1002 | public func exists() -> Bool { 1003 | if let errorValue = error, (400...1000).contains(errorValue.errorCode) { 1004 | return false 1005 | } 1006 | return true 1007 | } 1008 | } 1009 | 1010 | // MARK: - URL 1011 | 1012 | extension JSON { 1013 | 1014 | //Optional URL 1015 | public var url: URL? { 1016 | get { 1017 | switch type { 1018 | case .string: 1019 | // Check for existing percent escapes first to prevent double-escaping of % character 1020 | if rawString.range(of: "%[0-9A-Fa-f]{2}", options: .regularExpression, range: nil, locale: nil) != nil { 1021 | return Foundation.URL(string: rawString) 1022 | } else if let encodedString_ = rawString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) { 1023 | // We have to use `Foundation.URL` otherwise it conflicts with the variable name. 1024 | return Foundation.URL(string: encodedString_) 1025 | } else { 1026 | return nil 1027 | } 1028 | default: 1029 | return nil 1030 | } 1031 | } 1032 | set { 1033 | object = newValue?.absoluteString ?? NSNull() 1034 | } 1035 | } 1036 | } 1037 | 1038 | // MARK: - Int, Double, Float, Int8, Int16, Int32, Int64 1039 | 1040 | extension JSON { 1041 | 1042 | public var double: Double? { 1043 | get { 1044 | return number?.doubleValue 1045 | } 1046 | set { 1047 | if let newValue = newValue { 1048 | object = NSNumber(value: newValue) 1049 | } else { 1050 | object = NSNull() 1051 | } 1052 | } 1053 | } 1054 | 1055 | public var doubleValue: Double { 1056 | get { 1057 | return numberValue.doubleValue 1058 | } 1059 | set { 1060 | object = NSNumber(value: newValue) 1061 | } 1062 | } 1063 | 1064 | public var float: Float? { 1065 | get { 1066 | return number?.floatValue 1067 | } 1068 | set { 1069 | if let newValue = newValue { 1070 | object = NSNumber(value: newValue) 1071 | } else { 1072 | object = NSNull() 1073 | } 1074 | } 1075 | } 1076 | 1077 | public var floatValue: Float { 1078 | get { 1079 | return numberValue.floatValue 1080 | } 1081 | set { 1082 | object = NSNumber(value: newValue) 1083 | } 1084 | } 1085 | 1086 | public var int: Int? { 1087 | get { 1088 | return number?.intValue 1089 | } 1090 | set { 1091 | if let newValue = newValue { 1092 | object = NSNumber(value: newValue) 1093 | } else { 1094 | object = NSNull() 1095 | } 1096 | } 1097 | } 1098 | 1099 | public var intValue: Int { 1100 | get { 1101 | return numberValue.intValue 1102 | } 1103 | set { 1104 | object = NSNumber(value: newValue) 1105 | } 1106 | } 1107 | 1108 | public var uInt: UInt? { 1109 | get { 1110 | return number?.uintValue 1111 | } 1112 | set { 1113 | if let newValue = newValue { 1114 | object = NSNumber(value: newValue) 1115 | } else { 1116 | object = NSNull() 1117 | } 1118 | } 1119 | } 1120 | 1121 | public var uIntValue: UInt { 1122 | get { 1123 | return numberValue.uintValue 1124 | } 1125 | set { 1126 | object = NSNumber(value: newValue) 1127 | } 1128 | } 1129 | 1130 | public var int8: Int8? { 1131 | get { 1132 | return number?.int8Value 1133 | } 1134 | set { 1135 | if let newValue = newValue { 1136 | object = NSNumber(value: Int(newValue)) 1137 | } else { 1138 | object = NSNull() 1139 | } 1140 | } 1141 | } 1142 | 1143 | public var int8Value: Int8 { 1144 | get { 1145 | return numberValue.int8Value 1146 | } 1147 | set { 1148 | object = NSNumber(value: Int(newValue)) 1149 | } 1150 | } 1151 | 1152 | public var uInt8: UInt8? { 1153 | get { 1154 | return number?.uint8Value 1155 | } 1156 | set { 1157 | if let newValue = newValue { 1158 | object = NSNumber(value: newValue) 1159 | } else { 1160 | object = NSNull() 1161 | } 1162 | } 1163 | } 1164 | 1165 | public var uInt8Value: UInt8 { 1166 | get { 1167 | return numberValue.uint8Value 1168 | } 1169 | set { 1170 | object = NSNumber(value: newValue) 1171 | } 1172 | } 1173 | 1174 | public var int16: Int16? { 1175 | get { 1176 | return number?.int16Value 1177 | } 1178 | set { 1179 | if let newValue = newValue { 1180 | object = NSNumber(value: newValue) 1181 | } else { 1182 | object = NSNull() 1183 | } 1184 | } 1185 | } 1186 | 1187 | public var int16Value: Int16 { 1188 | get { 1189 | return numberValue.int16Value 1190 | } 1191 | set { 1192 | object = NSNumber(value: newValue) 1193 | } 1194 | } 1195 | 1196 | public var uInt16: UInt16? { 1197 | get { 1198 | return number?.uint16Value 1199 | } 1200 | set { 1201 | if let newValue = newValue { 1202 | object = NSNumber(value: newValue) 1203 | } else { 1204 | object = NSNull() 1205 | } 1206 | } 1207 | } 1208 | 1209 | public var uInt16Value: UInt16 { 1210 | get { 1211 | return numberValue.uint16Value 1212 | } 1213 | set { 1214 | object = NSNumber(value: newValue) 1215 | } 1216 | } 1217 | 1218 | public var int32: Int32? { 1219 | get { 1220 | return number?.int32Value 1221 | } 1222 | set { 1223 | if let newValue = newValue { 1224 | object = NSNumber(value: newValue) 1225 | } else { 1226 | object = NSNull() 1227 | } 1228 | } 1229 | } 1230 | 1231 | public var int32Value: Int32 { 1232 | get { 1233 | return numberValue.int32Value 1234 | } 1235 | set { 1236 | object = NSNumber(value: newValue) 1237 | } 1238 | } 1239 | 1240 | public var uInt32: UInt32? { 1241 | get { 1242 | return number?.uint32Value 1243 | } 1244 | set { 1245 | if let newValue = newValue { 1246 | object = NSNumber(value: newValue) 1247 | } else { 1248 | object = NSNull() 1249 | } 1250 | } 1251 | } 1252 | 1253 | public var uInt32Value: UInt32 { 1254 | get { 1255 | return numberValue.uint32Value 1256 | } 1257 | set { 1258 | object = NSNumber(value: newValue) 1259 | } 1260 | } 1261 | 1262 | public var int64: Int64? { 1263 | get { 1264 | return number?.int64Value 1265 | } 1266 | set { 1267 | if let newValue = newValue { 1268 | object = NSNumber(value: newValue) 1269 | } else { 1270 | object = NSNull() 1271 | } 1272 | } 1273 | } 1274 | 1275 | public var int64Value: Int64 { 1276 | get { 1277 | return numberValue.int64Value 1278 | } 1279 | set { 1280 | object = NSNumber(value: newValue) 1281 | } 1282 | } 1283 | 1284 | public var uInt64: UInt64? { 1285 | get { 1286 | return number?.uint64Value 1287 | } 1288 | set { 1289 | if let newValue = newValue { 1290 | object = NSNumber(value: newValue) 1291 | } else { 1292 | object = NSNull() 1293 | } 1294 | } 1295 | } 1296 | 1297 | public var uInt64Value: UInt64 { 1298 | get { 1299 | return numberValue.uint64Value 1300 | } 1301 | set { 1302 | object = NSNumber(value: newValue) 1303 | } 1304 | } 1305 | } 1306 | 1307 | // MARK: - Comparable 1308 | 1309 | extension JSON: Swift.Comparable {} 1310 | 1311 | public func == (lhs: JSON, rhs: JSON) -> Bool { 1312 | 1313 | switch (lhs.type, rhs.type) { 1314 | case (.number, .number): return lhs.rawNumber == rhs.rawNumber 1315 | case (.string, .string): return lhs.rawString == rhs.rawString 1316 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1317 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1318 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1319 | case (.null, .null): return true 1320 | default: return false 1321 | } 1322 | } 1323 | 1324 | public func <= (lhs: JSON, rhs: JSON) -> Bool { 1325 | 1326 | switch (lhs.type, rhs.type) { 1327 | case (.number, .number): return lhs.rawNumber <= rhs.rawNumber 1328 | case (.string, .string): return lhs.rawString <= rhs.rawString 1329 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1330 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1331 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1332 | case (.null, .null): return true 1333 | default: return false 1334 | } 1335 | } 1336 | 1337 | public func >= (lhs: JSON, rhs: JSON) -> Bool { 1338 | 1339 | switch (lhs.type, rhs.type) { 1340 | case (.number, .number): return lhs.rawNumber >= rhs.rawNumber 1341 | case (.string, .string): return lhs.rawString >= rhs.rawString 1342 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1343 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1344 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1345 | case (.null, .null): return true 1346 | default: return false 1347 | } 1348 | } 1349 | 1350 | public func > (lhs: JSON, rhs: JSON) -> Bool { 1351 | 1352 | switch (lhs.type, rhs.type) { 1353 | case (.number, .number): return lhs.rawNumber > rhs.rawNumber 1354 | case (.string, .string): return lhs.rawString > rhs.rawString 1355 | default: return false 1356 | } 1357 | } 1358 | 1359 | public func < (lhs: JSON, rhs: JSON) -> Bool { 1360 | 1361 | switch (lhs.type, rhs.type) { 1362 | case (.number, .number): return lhs.rawNumber < rhs.rawNumber 1363 | case (.string, .string): return lhs.rawString < rhs.rawString 1364 | default: return false 1365 | } 1366 | } 1367 | 1368 | private let trueNumber = NSNumber(value: true) 1369 | private let falseNumber = NSNumber(value: false) 1370 | private let trueObjCType = String(cString: trueNumber.objCType) 1371 | private let falseObjCType = String(cString: falseNumber.objCType) 1372 | 1373 | // MARK: - NSNumber: Comparable 1374 | 1375 | extension NSNumber { 1376 | fileprivate var isBool: Bool { 1377 | let objCType = String(cString: self.objCType) 1378 | if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) { 1379 | return true 1380 | } else { 1381 | return false 1382 | } 1383 | } 1384 | } 1385 | 1386 | func == (lhs: NSNumber, rhs: NSNumber) -> Bool { 1387 | switch (lhs.isBool, rhs.isBool) { 1388 | case (false, true): return false 1389 | case (true, false): return false 1390 | default: return lhs.compare(rhs) == .orderedSame 1391 | } 1392 | } 1393 | 1394 | func != (lhs: NSNumber, rhs: NSNumber) -> Bool { 1395 | return !(lhs == rhs) 1396 | } 1397 | 1398 | func < (lhs: NSNumber, rhs: NSNumber) -> Bool { 1399 | 1400 | switch (lhs.isBool, rhs.isBool) { 1401 | case (false, true): return false 1402 | case (true, false): return false 1403 | default: return lhs.compare(rhs) == .orderedAscending 1404 | } 1405 | } 1406 | 1407 | func > (lhs: NSNumber, rhs: NSNumber) -> Bool { 1408 | 1409 | switch (lhs.isBool, rhs.isBool) { 1410 | case (false, true): return false 1411 | case (true, false): return false 1412 | default: return lhs.compare(rhs) == ComparisonResult.orderedDescending 1413 | } 1414 | } 1415 | 1416 | func <= (lhs: NSNumber, rhs: NSNumber) -> Bool { 1417 | 1418 | switch (lhs.isBool, rhs.isBool) { 1419 | case (false, true): return false 1420 | case (true, false): return false 1421 | default: return lhs.compare(rhs) != .orderedDescending 1422 | } 1423 | } 1424 | 1425 | func >= (lhs: NSNumber, rhs: NSNumber) -> Bool { 1426 | 1427 | switch (lhs.isBool, rhs.isBool) { 1428 | case (false, true): return false 1429 | case (true, false): return false 1430 | default: return lhs.compare(rhs) != .orderedAscending 1431 | } 1432 | } 1433 | 1434 | public enum writingOptionsKeys { 1435 | case jsonSerialization 1436 | case castNilToNSNull 1437 | case maxObjextDepth 1438 | case encoding 1439 | } 1440 | 1441 | // MARK: - JSON: Codable 1442 | extension JSON: Codable { 1443 | private static var codableTypes: [Codable.Type] { 1444 | return [ 1445 | Bool.self, 1446 | Int.self, 1447 | Int8.self, 1448 | Int16.self, 1449 | Int32.self, 1450 | Int64.self, 1451 | UInt.self, 1452 | UInt8.self, 1453 | UInt16.self, 1454 | UInt32.self, 1455 | UInt64.self, 1456 | Double.self, 1457 | String.self, 1458 | [JSON].self, 1459 | [String: JSON].self 1460 | ] 1461 | } 1462 | public init(from decoder: Decoder) throws { 1463 | var object: Any? 1464 | 1465 | if let container = try? decoder.singleValueContainer(), !container.decodeNil() { 1466 | for type in JSON.codableTypes { 1467 | if object != nil { 1468 | break 1469 | } 1470 | // try to decode value 1471 | switch type { 1472 | case let boolType as Bool.Type: 1473 | object = try? container.decode(boolType) 1474 | case let intType as Int.Type: 1475 | object = try? container.decode(intType) 1476 | case let int8Type as Int8.Type: 1477 | object = try? container.decode(int8Type) 1478 | case let int32Type as Int32.Type: 1479 | object = try? container.decode(int32Type) 1480 | case let int64Type as Int64.Type: 1481 | object = try? container.decode(int64Type) 1482 | case let uintType as UInt.Type: 1483 | object = try? container.decode(uintType) 1484 | case let uint8Type as UInt8.Type: 1485 | object = try? container.decode(uint8Type) 1486 | case let uint16Type as UInt16.Type: 1487 | object = try? container.decode(uint16Type) 1488 | case let uint32Type as UInt32.Type: 1489 | object = try? container.decode(uint32Type) 1490 | case let uint64Type as UInt64.Type: 1491 | object = try? container.decode(uint64Type) 1492 | case let doubleType as Double.Type: 1493 | object = try? container.decode(doubleType) 1494 | case let stringType as String.Type: 1495 | object = try? container.decode(stringType) 1496 | case let jsonValueArrayType as [JSON].Type: 1497 | object = try? container.decode(jsonValueArrayType) 1498 | case let jsonValueDictType as [String: JSON].Type: 1499 | object = try? container.decode(jsonValueDictType) 1500 | default: 1501 | break 1502 | } 1503 | } 1504 | } 1505 | self.init(object ?? NSNull()) 1506 | } 1507 | public func encode(to encoder: Encoder) throws { 1508 | var container = encoder.singleValueContainer() 1509 | if object is NSNull { 1510 | try container.encodeNil() 1511 | return 1512 | } 1513 | switch object { 1514 | case let intValue as Int: 1515 | try container.encode(intValue) 1516 | case let int8Value as Int8: 1517 | try container.encode(int8Value) 1518 | case let int32Value as Int32: 1519 | try container.encode(int32Value) 1520 | case let int64Value as Int64: 1521 | try container.encode(int64Value) 1522 | case let uintValue as UInt: 1523 | try container.encode(uintValue) 1524 | case let uint8Value as UInt8: 1525 | try container.encode(uint8Value) 1526 | case let uint16Value as UInt16: 1527 | try container.encode(uint16Value) 1528 | case let uint32Value as UInt32: 1529 | try container.encode(uint32Value) 1530 | case let uint64Value as UInt64: 1531 | try container.encode(uint64Value) 1532 | case let doubleValue as Double: 1533 | try container.encode(doubleValue) 1534 | case let boolValue as Bool: 1535 | try container.encode(boolValue) 1536 | case let stringValue as String: 1537 | try container.encode(stringValue) 1538 | case is [Any]: 1539 | let jsonValueArray = array ?? [] 1540 | try container.encode(jsonValueArray) 1541 | case is [String: Any]: 1542 | let jsonValueDictValue = dictionary ?? [:] 1543 | try container.encode(jsonValueDictValue) 1544 | default: 1545 | break 1546 | } 1547 | } 1548 | } 1549 | 1550 | let namedArguments = UserDefaults.standard 1551 | guard let jsonFile = namedArguments.string(forKey: "i") else { 1552 | print("Provide json crash file name with -i flag") 1553 | exit(0) 1554 | } 1555 | guard let outputFile = namedArguments.string(forKey: "o") else { 1556 | print("Provide output file name with -o flag") 1557 | exit(0) 1558 | } 1559 | CrashTranslator().run(jsonFile: jsonFile, outputFile: outputFile) 1560 | 1561 | -------------------------------------------------------------------------------- /docs/apple_crash_report_format.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomieq/AppleCrashScripts/522209be9f4daf82422cba3602b986d43d57e733/docs/apple_crash_report_format.pdf -------------------------------------------------------------------------------- /symbolicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // symbolicate.swift 3 | // 4 | // Created by Tomasz Kucharski on 07/10/2021. 5 | 6 | // This script loads all *.dSYM files in current directory and tries do apply them 7 | // to symbolicate the crash report 8 | // 9 | 10 | import Foundation 11 | 12 | struct BacktraceLine { 13 | let lineNumber: String 14 | let binaryImageName: String 15 | let address: String 16 | let loadAddress: String 17 | } 18 | 19 | struct BinaryImage { 20 | let loadAddress: String 21 | let name: String 22 | let architecture: String 23 | let uuid: String 24 | } 25 | 26 | struct DwarfFileInfo { 27 | let path: String 28 | let uuid: String 29 | let architecture: String 30 | 31 | var dSYMFileName: String { 32 | self.path.components(separatedBy: "/").first{ $0.hasSuffix(".dSYM") } ?? self.path 33 | } 34 | 35 | func matches(_ binaryImage: BinaryImage) -> Bool { 36 | guard self.uuid.lowercased().replacingOccurrences(of: "-", with: "") == binaryImage.uuid.lowercased().replacingOccurrences(of: "-", with: "") else { 37 | return false 38 | } 39 | guard self.architecture == binaryImage.architecture else { return false } 40 | return true 41 | } 42 | } 43 | 44 | final class CrashSymbolicator { 45 | let crashFileName: String 46 | 47 | init(crashFileName: String) { 48 | self.crashFileName = crashFileName 49 | } 50 | 51 | func run() { 52 | let pwd = shell("pwd") 53 | print("Working in \(pwd)") 54 | 55 | do { 56 | let crashUrl = URL(fileURLWithPath: pwd).appendingPathComponent(self.crashFileName) 57 | 58 | guard FileManager.default.fileExists(atPath: crashUrl.path) else { 59 | print("Crash file \(crashUrl.path) does not exists!") 60 | exit(0) 61 | } 62 | 63 | let dSYMFileNames = try FileManager.default.contentsOfDirectory(atPath: pwd).filter { $0.hasSuffix(".dSYM") } 64 | 65 | var dwarfFiles: [DwarfFileInfo] = [] 66 | 67 | for dSYMFileName in dSYMFileNames { 68 | var dSYMUrl = URL(fileURLWithPath: pwd).appendingPathComponent(dSYMFileName) 69 | dSYMUrl.appendPathComponent("Contents/Resources/DWARF") 70 | let binariesIndSym = try FileManager.default.contentsOfDirectory(atPath: dSYMUrl.path) 71 | for binaryFile in binariesIndSym { 72 | let dwarfUrl = dSYMUrl.appendingPathComponent(binaryFile) 73 | let dwarfPath = dwarfUrl.path.replacingOccurrences(of: " ", with: "\\ ") 74 | let dwarfDumpOutput = shell("dwarfdump --uuid \(dwarfPath)") 75 | .trimmingCharacters(in: .whitespaces) 76 | .components(separatedBy: .newlines) 77 | dwarfFiles.append(contentsOf: dwarfDumpOutput.compactMap{ makeDwarfFileInfo(line: $0) }) 78 | } 79 | } 80 | print("---------------------------------------") 81 | print("Found \(dwarfFiles.count) DWARF files in working directory:\n\(dwarfFiles.map{ $0.description }.joined(separator: "\n"))") 82 | print("---------------------------------------") 83 | 84 | let crashReport = try String(contentsOf: crashUrl, encoding: .utf8) 85 | let rawStack = getCrashedThreadLines(crashReport: crashReport) 86 | let binaryImages = getBinaryImages(crashReport: crashReport) 87 | 88 | print("This crash report has \(binaryImages.count) binary images assosiated") 89 | let binaryImagesWithDwarfFiles = binaryImages.compactMap { binaryImage in dwarfFiles.first{ $0.matches(binaryImage) } } 90 | print("But only \(binaryImagesWithDwarfFiles.count) has corresponding DWARF file") 91 | print("Desymbolication will use: \(binaryImagesWithDwarfFiles.map{ $0.path.relative(to: pwd) }.joined(separator: ", "))") 92 | print("---------------------------------------") 93 | 94 | print("Crash stack") 95 | for line in rawStack { 96 | 97 | if let stackEntry = makeBacktraceLine(line: line), 98 | let binaryImage = (binaryImages.first{ $0.loadAddress == stackEntry.loadAddress && $0.name == stackEntry.binaryImageName }), 99 | let dwarfFile = (dwarfFiles.first{ $0.matches(binaryImage) }) { 100 | let command = "atos -arch \(binaryImage.architecture) -o \(dwarfFile.path) -l \(binaryImage.loadAddress) \(stackEntry.address)" 101 | let atosOutput = shell(command) 102 | 103 | if let index = line.range(of: "0x")?.lowerBound { 104 | print("\(line[.. String { 119 | let task = Process() 120 | let pipe = Pipe() 121 | 122 | task.standardOutput = pipe 123 | task.standardError = pipe 124 | task.arguments = ["-c", command] 125 | task.launchPath = "/bin/zsh" 126 | task.launch() 127 | 128 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 129 | let output = String(data: data, encoding: .utf8)! 130 | 131 | return output.trimmingCharacters(in: .newlines) 132 | } 133 | 134 | @discardableResult 135 | func shellWithLiveOutput(_ args: String...) -> Int32 { 136 | let task = Process() 137 | task.launchPath = "/usr/bin/env" 138 | task.arguments = args 139 | task.launch() 140 | task.waitUntilExit() 141 | return task.terminationStatus 142 | } 143 | 144 | func getCrashedThreadLines(crashReport: String) -> [String] { 145 | let crashLines = crashReport.components(separatedBy: .newlines) 146 | var stack: [String] = [] 147 | var stackRecording = false 148 | for line in crashLines { 149 | if stackRecording, line.contains("Thread") { 150 | return stack 151 | } 152 | if stackRecording { 153 | stack.append(line) 154 | } 155 | if line.contains("Thread"), line.contains("Crashed:") { 156 | stackRecording = true 157 | } 158 | } 159 | return stack 160 | } 161 | 162 | func getBinaryImages(crashReport: String) -> [BinaryImage] { 163 | var output: [String] = [] 164 | let crashLines = crashReport.components(separatedBy: .newlines) 165 | var recording = false 166 | for line in crashLines { 167 | let cleanLine = line.trimmingCharacters(in: .whitespaces) 168 | if recording { 169 | if cleanLine.starts(with: "0x") { 170 | output.append(cleanLine) 171 | } else { 172 | return output.compactMap{ makeBinaryImage(line: $0) } 173 | } 174 | } 175 | if line.contains("Binary Images:") { 176 | recording = true 177 | } 178 | } 179 | return output.compactMap{ makeBinaryImage(line: $0) } 180 | } 181 | 182 | func makeBacktraceLine(line: String) -> BacktraceLine? { 183 | var parts = line.components(separatedBy: .whitespaces) 184 | parts.reverse() 185 | guard let lineNumber = parts.popLast() else { return nil } 186 | let clearedLine = line 187 | .trimmingCharacters(in: CharacterSet(charactersIn: "1234567890")) 188 | .trimmingCharacters(in: .whitespaces) 189 | .trimmingCharacters(in: CharacterSet(charactersIn: "+")) 190 | .trimmingCharacters(in: .whitespaces) 191 | parts = clearedLine.components(separatedBy: " ").filter{ !$0.isEmpty } 192 | 193 | guard let loadAddress = parts.popLast(), let address = parts.popLast() else { return nil } 194 | let moduleName = parts.joined(separator: " ") 195 | return BacktraceLine(lineNumber: lineNumber, binaryImageName: moduleName, address: address, loadAddress: loadAddress) 196 | 197 | } 198 | 199 | func makeBinaryImage(line: String) -> BinaryImage? { 200 | var parts = line.components(separatedBy: .whitespaces) 201 | parts.reverse() 202 | guard let loadAddress = parts.popLast() else { return nil } 203 | parts.removeLast(2) 204 | parts.reverse() 205 | parts = parts.joined(separator: " ").components(separatedBy: "<") 206 | guard let uuid = parts[1].components(separatedBy: ">").first else { return nil } 207 | parts = parts[0].trimmingCharacters(in: .whitespaces).components(separatedBy: " ") 208 | guard let architecture = parts.popLast() else { return nil } 209 | let moduleName = parts.joined(separator: " ").trimmingCharacters(in: .whitespaces) 210 | return BinaryImage(loadAddress: loadAddress, name: moduleName, architecture: architecture, uuid: uuid) 211 | } 212 | 213 | func makeDwarfFileInfo(line: String) -> DwarfFileInfo? { 214 | var parts = line.components(separatedBy: ")") 215 | guard parts.count > 1 else { return nil } 216 | let path = parts[1].trimmingCharacters(in: .whitespaces).replacingOccurrences(of: " ", with: "\\ ") 217 | parts = parts[0].components(separatedBy: "(") 218 | let architecture = parts[1] 219 | let uuid = parts[0].replacingOccurrences(of: "UUID:", with: "").trimmingCharacters(in: .whitespaces) 220 | return DwarfFileInfo(path: path, uuid: uuid, architecture: architecture) 221 | } 222 | } 223 | 224 | 225 | extension BacktraceLine: CustomStringConvertible { 226 | var description: String { 227 | "{ line: \(lineNumber), name: \(binaryImageName), address: \(address), loadAddress: \(loadAddress) }" 228 | } 229 | } 230 | 231 | extension BinaryImage: CustomStringConvertible { 232 | var description: String { 233 | "{ uuid: \(uuid), loadAddress: \(loadAddress), name: \(name), architecture: \(architecture) }" 234 | } 235 | } 236 | 237 | extension DwarfFileInfo: CustomStringConvertible { 238 | var description: String { 239 | "{ UUID: \(uuid), arch: \(architecture), dSYMFileName: \(dSYMFileName) }" 240 | } 241 | } 242 | 243 | extension String { 244 | func relative(to path: String) -> String { 245 | return self.replacingOccurrences(of: "\(path)/", with: "") 246 | } 247 | } 248 | 249 | let namedArguments = UserDefaults.standard 250 | guard let crashFile = namedArguments.string(forKey: "crash") else { 251 | print("Provide crash file name with -crash flag") 252 | exit(0) 253 | } 254 | CrashSymbolicator(crashFileName: crashFile).run() 255 | 256 | --------------------------------------------------------------------------------