Serve symbol pages by offset and step through them with prev/next controls showing the current range. Reset to the first page when the filter or category changes. Clamp the page index at the mutation site and floor the offset in the agent so hammering prev/next can't run past the ends into a negative range. Bind Cmd+F (Ctrl+F on GTK) to focus the filter input.
217 lines
7.0 KiB
Swift
217 lines
7.0 KiB
Swift
import Foundation
|
|
|
|
public struct ModuleSymbolBundle: Sendable {
|
|
public var exports: [Export]
|
|
public var imports: [Import]
|
|
public var symbols: [Symbol]
|
|
|
|
public init(exports: [Export] = [], imports: [Import] = [], symbols: [Symbol] = []) {
|
|
self.exports = exports
|
|
self.imports = imports
|
|
self.symbols = symbols
|
|
}
|
|
|
|
public enum SymbolKind: String, Sendable, Hashable {
|
|
case function
|
|
case variable
|
|
}
|
|
|
|
public struct Export: Sendable, Hashable, Identifiable {
|
|
public var id: String { "\(name)@0x\(String(address, radix: 16))" }
|
|
public let kind: SymbolKind
|
|
public let name: String
|
|
public let address: UInt64
|
|
|
|
public init(kind: SymbolKind, name: String, address: UInt64) {
|
|
self.kind = kind
|
|
self.name = name
|
|
self.address = address
|
|
}
|
|
}
|
|
|
|
public struct Import: Sendable, Hashable, Identifiable {
|
|
public var id: String { "\(module ?? "?")!\(name)@0x\(String(address ?? 0, radix: 16))" }
|
|
public let kind: SymbolKind?
|
|
public let name: String
|
|
public let module: String?
|
|
public let address: UInt64?
|
|
public let slot: UInt64?
|
|
|
|
public init(kind: SymbolKind?, name: String, module: String?, address: UInt64?, slot: UInt64?) {
|
|
self.kind = kind
|
|
self.name = name
|
|
self.module = module
|
|
self.address = address
|
|
self.slot = slot
|
|
}
|
|
}
|
|
|
|
public struct Symbol: Sendable, Hashable, Identifiable {
|
|
public var id: String { "\(name)@0x\(String(address, radix: 16))" }
|
|
public let name: String
|
|
public let type: String
|
|
public let address: UInt64
|
|
public let isGlobal: Bool
|
|
public let size: Int?
|
|
public let sectionID: String?
|
|
public let sectionProtection: String?
|
|
|
|
public init(
|
|
name: String,
|
|
type: String,
|
|
address: UInt64,
|
|
isGlobal: Bool,
|
|
size: Int?,
|
|
sectionID: String?,
|
|
sectionProtection: String?
|
|
) {
|
|
self.name = name
|
|
self.type = type
|
|
self.address = address
|
|
self.isGlobal = isGlobal
|
|
self.size = size
|
|
self.sectionID = sectionID
|
|
self.sectionProtection = sectionProtection
|
|
}
|
|
|
|
public var isCode: Bool {
|
|
if type == "undefined" { return false }
|
|
if type == "function" { return true }
|
|
if let prot = sectionProtection, prot.contains("x") { return true }
|
|
return false
|
|
}
|
|
|
|
public var isData: Bool {
|
|
type == "object" || type == "common" || type == "tls"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ModuleSymbolBundle {
|
|
static func parseAddress(_ s: String?) -> UInt64? {
|
|
guard let s else { return nil }
|
|
let trimmed = s.hasPrefix("0x") ? String(s.dropFirst(2)) : s
|
|
return UInt64(trimmed, radix: 16)
|
|
}
|
|
|
|
static func decodeExport(_ dict: [String: Any]) -> Export? {
|
|
guard let typeRaw = dict["type"] as? String,
|
|
let kind = SymbolKind(rawValue: typeRaw),
|
|
let name = dict["name"] as? String,
|
|
let address = parseAddress(dict["address"] as? String)
|
|
else { return nil }
|
|
return Export(kind: kind, name: name, address: address)
|
|
}
|
|
|
|
static func decodeImport(_ dict: [String: Any]) -> Import? {
|
|
guard let name = dict["name"] as? String else { return nil }
|
|
let kind = (dict["type"] as? String).flatMap(SymbolKind.init(rawValue:))
|
|
return Import(
|
|
kind: kind,
|
|
name: name,
|
|
module: dict["module"] as? String,
|
|
address: parseAddress(dict["address"] as? String),
|
|
slot: parseAddress(dict["slot"] as? String)
|
|
)
|
|
}
|
|
|
|
static func decodeSymbol(_ dict: [String: Any]) -> Symbol? {
|
|
guard let name = dict["name"] as? String,
|
|
let type = dict["type"] as? String,
|
|
let address = parseAddress(dict["address"] as? String),
|
|
let isGlobal = dict["isGlobal"] as? Bool
|
|
else { return nil }
|
|
return Symbol(
|
|
name: name,
|
|
type: type,
|
|
address: address,
|
|
isGlobal: isGlobal,
|
|
size: dict["size"] as? Int,
|
|
sectionID: dict["sectionID"] as? String,
|
|
sectionProtection: dict["sectionProtection"] as? String
|
|
)
|
|
}
|
|
|
|
public static func fromJSON(_ obj: [String: Any]) -> ModuleSymbolBundle {
|
|
let exportDicts = (obj["exports"] as? [[String: Any]]) ?? []
|
|
let importDicts = (obj["imports"] as? [[String: Any]]) ?? []
|
|
let symbolDicts = (obj["symbols"] as? [[String: Any]]) ?? []
|
|
|
|
return ModuleSymbolBundle(
|
|
exports: exportDicts.compactMap(decodeExport),
|
|
imports: importDicts.compactMap(decodeImport),
|
|
symbols: symbolDicts.compactMap(decodeSymbol)
|
|
)
|
|
}
|
|
}
|
|
|
|
public enum ModuleSymbolCategory: String, Sendable {
|
|
case exports
|
|
case imports
|
|
case symbols
|
|
}
|
|
|
|
public struct ModuleSymbolPage: Sendable {
|
|
public static let pageSize = 1000
|
|
|
|
public let rows: Rows
|
|
public let matched: Int
|
|
public let offset: Int
|
|
public let counts: Counts
|
|
|
|
public var count: Int {
|
|
switch rows {
|
|
case .exports(let r): return r.count
|
|
case .imports(let r): return r.count
|
|
case .symbols(let r): return r.count
|
|
}
|
|
}
|
|
|
|
public var hasPrevious: Bool { offset > 0 }
|
|
public var hasNext: Bool { offset + count < matched }
|
|
|
|
public enum Rows: Sendable {
|
|
case exports([ModuleSymbolBundle.Export])
|
|
case imports([ModuleSymbolBundle.Import])
|
|
case symbols([ModuleSymbolBundle.Symbol])
|
|
}
|
|
|
|
public struct Counts: Sendable {
|
|
public let exports: Int
|
|
public let imports: Int
|
|
public let symbols: Int
|
|
|
|
public subscript(_ category: ModuleSymbolCategory) -> Int {
|
|
switch category {
|
|
case .exports: return exports
|
|
case .imports: return imports
|
|
case .symbols: return symbols
|
|
}
|
|
}
|
|
}
|
|
|
|
public static func fromJSON(_ obj: [String: Any], category: ModuleSymbolCategory) -> ModuleSymbolPage {
|
|
let rowDicts = (obj["rows"] as? [[String: Any]]) ?? []
|
|
let rows: Rows
|
|
switch category {
|
|
case .exports: rows = .exports(rowDicts.compactMap(ModuleSymbolBundle.decodeExport))
|
|
case .imports: rows = .imports(rowDicts.compactMap(ModuleSymbolBundle.decodeImport))
|
|
case .symbols: rows = .symbols(rowDicts.compactMap(ModuleSymbolBundle.decodeSymbol))
|
|
}
|
|
|
|
let countsObj = (obj["counts"] as? [String: Any]) ?? [:]
|
|
let counts = Counts(
|
|
exports: countsObj["exports"] as? Int ?? 0,
|
|
imports: countsObj["imports"] as? Int ?? 0,
|
|
symbols: countsObj["symbols"] as? Int ?? 0
|
|
)
|
|
|
|
return ModuleSymbolPage(
|
|
rows: rows,
|
|
matched: obj["matched"] as? Int ?? rowDicts.count,
|
|
offset: obj["offset"] as? Int ?? 0,
|
|
counts: counts
|
|
)
|
|
}
|
|
}
|