Files
luma/Sources/LumaCore/Models/ModuleSymbolBundle.swift
Ole André Vadla Ravnås 035160a1e8 Paginate module symbols and focus filter with Cmd+F
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.
2026-07-10 18:40:22 +02:00

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
)
}
}