Files
luma/Sources/LumaCore/Models/JSInspectValue.swift

555 lines
18 KiB
Swift

import Foundation
import Frida
public enum JSInspectValue: nonisolated Codable, nonisolated Equatable, @unchecked Sendable {
case number(Double)
case string(String)
case object(id: Int, properties: [Property])
case array(id: Int, elements: [JSInspectValue])
case nativePointer(String)
case null
case boolean(Bool)
case bytes(Bytes)
case function(String)
case error(name: String, message: String, stack: String)
case undefined
case bigInt(String)
case symbol(String)
case date(String)
case regExp(pattern: String, flags: String)
case map(id: Int, entries: [Property])
case set(id: Int, elements: [JSInspectValue])
case promise
case weakMap
case weakSet
case depthLimit(container: ContainerKind)
case circular(id: Int)
public struct Property: nonisolated Codable, nonisolated Equatable {
public let key: JSInspectValue
public let value: JSInspectValue
nonisolated public init(key: JSInspectValue, value: JSInspectValue) {
self.key = key
self.value = value
}
nonisolated public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let key = try container.decode(JSInspectValue.self)
let value = try container.decode(JSInspectValue.self)
self.init(key: key, value: value)
}
nonisolated public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(key)
try container.encode(value)
}
}
public struct Bytes: nonisolated Equatable {
public let data: Data
public let kind: BytesKind
nonisolated public init(data: Data, kind: BytesKind) {
self.data = data
self.kind = kind
}
}
public enum BytesKind: String, nonisolated Codable, nonisolated Equatable {
case arrayBuffer = "ArrayBuffer"
case dataView = "DataView"
case int8Array = "Int8Array"
case uint8Array = "Uint8Array"
case uint8ClampedArray = "Uint8ClampedArray"
case int16Array = "Int16Array"
case uint16Array = "Uint16Array"
case int32Array = "Int32Array"
case uint32Array = "Uint32Array"
case float32Array = "Float32Array"
case float64Array = "Float64Array"
case bigInt64Array = "BigInt64Array"
case bigUint64Array = "BigUint64Array"
}
public enum ContainerKind: nonisolated Equatable {
case object
case array
case map
case set
}
private enum Kind: Int {
case number = 0
case string = 1
case object = 2
case array = 3
case nativePointer = 4
case null = 5
case boolean = 6
case bytes = 7
case function = 8
case error = 9
case undefined = 10
case bigInt = 11
case symbol = 12
case date = 13
case regExp = 14
case map = 15
case set = 16
case promise = 17
case weakMap = 18
case weakSet = 19
case depthLimit = 20
case circular = 21
}
nonisolated public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let rawKind = try container.decode(Int.self)
guard let kind = Kind(rawValue: rawKind) else {
throw Error.invalidArgument("Invalid kind")
}
switch kind {
case .number:
self = .number(try container.decode(Double.self))
case .string:
self = .string(try container.decode(String.self))
case .object:
if container.isAtEnd {
self = .object(id: 0, properties: [])
} else {
let id = try container.decode(Int.self)
let props = try container.decode([Property].self)
self = .object(id: id, properties: props)
}
case .array:
if container.isAtEnd {
self = .array(id: 0, elements: [])
} else {
let id = try container.decode(Int.self)
let elements = try container.decode([JSInspectValue].self)
self = .array(id: id, elements: elements)
}
case .nativePointer:
self = .nativePointer(try container.decode(String.self))
case .null:
self = .null
case .boolean:
self = .boolean(try container.decode(Bool.self))
case .bytes:
let data = try container.decode(Data.self)
let kind = try container.decode(BytesKind.self)
self = .bytes(Bytes(data: data, kind: kind))
case .function:
self = .function(try container.decode(String.self))
case .error:
let name = try container.decode(String.self)
let message = try container.decode(String.self)
let stack = try container.decode(String.self)
self = .error(name: name, message: message, stack: stack)
case .undefined:
self = .undefined
case .bigInt:
self = .bigInt(try container.decode(String.self))
case .symbol:
self = .symbol(try container.decode(String.self))
case .date:
self = .date(try container.decode(String.self))
case .regExp:
let pattern = try container.decode(String.self)
let flags = try container.decode(String.self)
self = .regExp(pattern: pattern, flags: flags)
case .map:
if container.isAtEnd {
self = .map(id: 0, entries: [])
} else {
let id = try container.decode(Int.self)
let entries = try container.decode([Property].self)
self = .map(id: id, entries: entries)
}
case .set:
if container.isAtEnd {
self = .set(id: 0, elements: [])
} else {
let id = try container.decode(Int.self)
let elements = try container.decode([JSInspectValue].self)
self = .set(id: id, elements: elements)
}
case .promise:
self = .promise
case .weakMap:
self = .weakMap
case .weakSet:
self = .weakSet
case .depthLimit:
let containerTag = try container.decode(Int.self)
let containerKind: ContainerKind
switch containerTag {
case Kind.object.rawValue:
containerKind = .object
case Kind.array.rawValue:
containerKind = .array
case Kind.map.rawValue:
containerKind = .map
case Kind.set.rawValue:
containerKind = .set
default:
containerKind = .object
}
self = .depthLimit(container: containerKind)
case .circular:
let id = try container.decode(Int.self)
self = .circular(id: id)
}
}
nonisolated public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .number(let n):
try container.encode(Kind.number.rawValue)
try container.encode(n)
case .string(let s):
try container.encode(Kind.string.rawValue)
try container.encode(s)
case .object(let id, let props):
try container.encode(Kind.object.rawValue)
try container.encode(id)
try container.encode(props)
case .array(let id, let elements):
try container.encode(Kind.array.rawValue)
try container.encode(id)
try container.encode(elements)
case .nativePointer(let s):
try container.encode(Kind.nativePointer.rawValue)
try container.encode(s)
case .null:
try container.encode(Kind.null.rawValue)
case .boolean(let b):
try container.encode(Kind.boolean.rawValue)
try container.encode(b)
case .bytes(let bytes):
try container.encode(Kind.bytes.rawValue)
try container.encode(bytes.data)
try container.encode(bytes.kind)
case .function(let t):
try container.encode(Kind.function.rawValue)
try container.encode(t)
case .error(let name, let message, let stack):
try container.encode(Kind.error.rawValue)
try container.encode(name)
try container.encode(message)
try container.encode(stack)
case .undefined:
try container.encode(Kind.undefined.rawValue)
case .bigInt(let s):
try container.encode(Kind.bigInt.rawValue)
try container.encode(s)
case .symbol(let t):
try container.encode(Kind.symbol.rawValue)
try container.encode(t)
case .date(let s):
try container.encode(Kind.date.rawValue)
try container.encode(s)
case .regExp(let pattern, let flags):
try container.encode(Kind.regExp.rawValue)
try container.encode(pattern)
try container.encode(flags)
case .map(let id, let entries):
try container.encode(Kind.map.rawValue)
try container.encode(id)
try container.encode(entries)
case .set(let id, let elements):
try container.encode(Kind.set.rawValue)
try container.encode(id)
try container.encode(elements)
case .promise:
try container.encode(Kind.promise.rawValue)
case .weakMap:
try container.encode(Kind.weakMap.rawValue)
case .weakSet:
try container.encode(Kind.weakSet.rawValue)
case .depthLimit(let containerKind):
try container.encode(Kind.depthLimit.rawValue)
let tag: Int
switch containerKind {
case .object:
tag = Kind.object.rawValue
case .array:
tag = Kind.array.rawValue
case .map:
tag = Kind.map.rawValue
case .set:
tag = Kind.set.rawValue
}
try container.encode(tag)
case .circular(let id):
try container.encode(Kind.circular.rawValue)
try container.encode(id)
}
}
public var nativePointerAddress: UInt64? {
guard case .nativePointer(let s) = self else { return nil }
return UInt64(s.dropFirst(2), radix: 16)
}
}
extension JSInspectValue {
public enum DecodePackedError: Swift.Error {
case invalidRoot
case invalidBlob
case invalidNode
case invalidTag(Int)
case invalidBytesRange
}
nonisolated public static func decodePacked(from root: Any) throws -> JSInspectValue {
guard let pair = root as? [Any], pair.count == 2 else {
throw DecodePackedError.invalidRoot
}
let tree = pair[0]
let blobData: Data
let blobCandidate = pair[1]
if let bytes = blobCandidate as? [UInt8] {
blobData = Data(bytes)
} else if let d = blobCandidate as? Data {
blobData = d
} else if blobCandidate is NSNull {
blobData = Data()
} else {
throw DecodePackedError.invalidBlob
}
return try decodePacked(tree: tree, blobData: blobData)
}
nonisolated public static func decodePacked(tree: Any, blobBytes: [UInt8]?) throws -> JSInspectValue {
return try decodePacked(tree: tree, blobData: Data(blobBytes ?? []))
}
nonisolated public static func decodePacked(tree: Any, blobData: Data) throws -> JSInspectValue {
func intFrom(_ any: Any) throws -> Int {
if let i = any as? Int { return i }
if let n = any as? NSNumber { return n.intValue }
throw DecodePackedError.invalidNode
}
func doubleFrom(_ any: Any) throws -> Double {
if let d = any as? Double { return d }
if let n = any as? NSNumber { return n.doubleValue }
throw DecodePackedError.invalidNode
}
func boolFrom(_ any: Any) throws -> Bool {
if let b = any as? Bool { return b }
if let n = any as? NSNumber { return n.boolValue }
throw DecodePackedError.invalidNode
}
func stringFrom(_ any: Any) throws -> String {
if let s = any as? String { return s }
throw DecodePackedError.invalidNode
}
func decodeNode(_ node: Any) throws -> JSInspectValue {
guard let arr = node as? [Any], !arr.isEmpty else {
throw DecodePackedError.invalidNode
}
let rawTagAny = arr[0]
let rawTag = try intFrom(rawTagAny)
guard let kind = Kind(rawValue: rawTag) else {
throw DecodePackedError.invalidTag(rawTag)
}
switch kind {
case .number:
let value = try doubleFrom(arr[1])
return .number(value)
case .string:
let s = try stringFrom(arr[1])
return .string(s)
case .object:
let id = try intFrom(arr[1])
let rawEntries = (arr.count > 2 ? arr[2] : []) as? [Any] ?? []
let properties: [Property] = try rawEntries.map { entryAny in
guard let entry = entryAny as? [Any], entry.count == 2 else {
throw DecodePackedError.invalidNode
}
let key = try decodeNode(entry[0])
let value = try decodeNode(entry[1])
return Property(key: key, value: value)
}
return .object(id: id, properties: properties)
case .array:
let id = try intFrom(arr[1])
let rawElements = (arr.count > 2 ? arr[2] : []) as? [Any] ?? []
let elements: [JSInspectValue] = try rawElements.map { try decodeNode($0) }
return .array(id: id, elements: elements)
case .nativePointer:
let s = try stringFrom(arr[1])
return .nativePointer(s)
case .null:
return .null
case .boolean:
let b = try boolFrom(arr[1])
return .boolean(b)
case .bytes:
let offset = try intFrom(arr[1])
let length = try intFrom(arr[2])
let kindRaw = try stringFrom(arr[3])
let kind = BytesKind(rawValue: kindRaw) ?? .arrayBuffer
let start = offset
let end = offset + length
guard start >= 0, length >= 0, end <= blobData.count else {
throw DecodePackedError.invalidBytesRange
}
let slice = blobData.subdata(in: start..<end)
return .bytes(Bytes(data: slice, kind: kind))
case .function:
let sig = try stringFrom(arr[1])
return .function(sig)
case .error:
let name = try stringFrom(arr[1])
let message = try stringFrom(arr[2])
let stack = try stringFrom(arr[3])
return .error(name: name, message: message, stack: stack)
case .undefined:
return .undefined
case .bigInt:
let s = try stringFrom(arr[1])
return .bigInt(s)
case .symbol:
let s = try stringFrom(arr[1])
return .symbol(s)
case .date:
let s = try stringFrom(arr[1])
return .date(s)
case .regExp:
let pattern = try stringFrom(arr[1])
let flags = try stringFrom(arr[2])
return .regExp(pattern: pattern, flags: flags)
case .map:
let id = try intFrom(arr[1])
let rawEntries = (arr.count > 2 ? arr[2] : []) as? [Any] ?? []
let entries: [Property] = try rawEntries.map { entryAny in
guard let entry = entryAny as? [Any], entry.count == 2 else {
throw DecodePackedError.invalidNode
}
let key = try decodeNode(entry[0])
let value = try decodeNode(entry[1])
return Property(key: key, value: value)
}
return .map(id: id, entries: entries)
case .set:
let id = try intFrom(arr[1])
let rawElements = (arr.count > 2 ? arr[2] : []) as? [Any] ?? []
let elements: [JSInspectValue] = try rawElements.map { try decodeNode($0) }
return .set(id: id, elements: elements)
case .promise:
return .promise
case .weakMap:
return .weakMap
case .weakSet:
return .weakSet
case .depthLimit:
let containerTag = try intFrom(arr[1])
let containerKind: ContainerKind
switch containerTag {
case Kind.object.rawValue:
containerKind = .object
case Kind.array.rawValue:
containerKind = .array
case Kind.map.rawValue:
containerKind = .map
case Kind.set.rawValue:
containerKind = .set
default:
containerKind = .object
}
return .depthLimit(container: containerKind)
case .circular:
let id = try intFrom(arr[1])
return .circular(id: id)
}
}
return try decodeNode(tree)
}
}