Compile, then decode through the module. Pass platform before arch, and default both to the host rather than darwin/arm64.
302 lines
16 KiB
Swift
302 lines
16 KiB
Swift
import Foundation
|
|
|
|
extension MissionTools {
|
|
static func registerPatterns(in catalog: ToolCatalog, engine: Engine) {
|
|
registerListPatterns(in: catalog, engine: engine)
|
|
registerReadPattern(in: catalog, engine: engine)
|
|
registerCheckPattern(in: catalog, engine: engine)
|
|
registerWritePattern(in: catalog, engine: engine)
|
|
registerDecodeMemory(in: catalog, engine: engine)
|
|
registerCallPatternFunction(in: catalog, engine: engine)
|
|
registerPlacePattern(in: catalog, engine: engine)
|
|
}
|
|
|
|
private static let patternSourceSchema = """
|
|
"pattern_id":{"type":"string","description":"Library file, as list_patterns names it, e.g. \\"macho.hexpat\\""},\
|
|
"source":{"type":"string","description":"Inline pattern source, instead of pattern_id"},\
|
|
"type":{"type":"string","description":"Type to decode; defaults to the source's root placement"}
|
|
"""
|
|
|
|
private static func registerListPatterns(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "list_patterns",
|
|
description: """
|
|
List the pattern library: ImHex-style pattern language files (.hexpat patterns, .pat \
|
|
libraries) with the types each declares and any compile errors. Patterns describe binary \
|
|
layouts; decode_memory applies one to process memory.
|
|
""",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{},"additionalProperties":false}
|
|
""",
|
|
isObserve: true,
|
|
requiresSession: false
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] _ in
|
|
guard let engine else { return errorResult("engine unavailable", code: .unavailable) }
|
|
let listed = await engine.describePatternLibrary()
|
|
return makeResult(jsonObject: listed, summary: "\(listed.count) pattern file\(listed.count == 1 ? "" : "s")")
|
|
}
|
|
}
|
|
|
|
private static func registerReadPattern(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "read_pattern",
|
|
description: "Read a pattern library file's source.",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{"pattern_id":{"type":"string"}},"required":["pattern_id"],"additionalProperties":false}
|
|
""",
|
|
isObserve: true,
|
|
requiresSession: false
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] invocation in
|
|
guard let engine else { return errorResult("engine unavailable", code: .unavailable) }
|
|
guard let id = invocation.args["pattern_id"] as? String, let source = engine.patterns.source(withID: id) else {
|
|
return errorResult("no pattern file with that pattern_id; see list_patterns", code: .notFound)
|
|
}
|
|
return makeResult(jsonObject: ["id": source.id, "source": source.text], summary: "Read \(source.id)")
|
|
}
|
|
}
|
|
|
|
private static func registerCheckPattern(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "check_pattern",
|
|
description: """
|
|
Compile pattern source without saving it, and report its diagnostics and the types it \
|
|
declares. Use to iterate on a pattern before write_pattern.
|
|
""",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{"source":{"type":"string"}},"required":["source"],"additionalProperties":false}
|
|
""",
|
|
isObserve: true,
|
|
requiresSession: false
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] invocation in
|
|
guard let engine else { return errorResult("engine unavailable", code: .unavailable) }
|
|
guard let text = invocation.args["source"] as? String else {
|
|
return errorResult("source is required", code: .invalidInput)
|
|
}
|
|
do {
|
|
let summary = try await engine.patternDecoder.summary(ofText: text)
|
|
return makeResult(
|
|
jsonObject: summary.jsonObject,
|
|
summary: summary.diagnostics.isEmpty ? "Compiles cleanly" : "\(summary.diagnostics.count) diagnostic(s)")
|
|
} catch {
|
|
return errorResult("compile failed: \(error.localizedDescription)", code: .failed)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func registerWritePattern(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "write_pattern",
|
|
description: """
|
|
Create or replace a pattern library file. The id is a file name ending in .hexpat \
|
|
(a pattern) or .pat (a library other files import). Returns the diagnostics of what was \
|
|
written. Requires user approval.
|
|
""",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{"pattern_id":{"type":"string","description":"e.g. \\"elf.hexpat\\""},"source":{"type":"string"}},"required":["pattern_id","source"],"additionalProperties":false}
|
|
""",
|
|
isObserve: false,
|
|
requiresSession: false,
|
|
codePreview: CodePreviewArg(field: "source", language: .patternLanguage)
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] invocation in
|
|
guard let engine else { return errorResult("engine unavailable", code: .unavailable) }
|
|
guard let id = invocation.args["pattern_id"] as? String, let text = invocation.args["source"] as? String else {
|
|
return errorResult("pattern_id and source are required", code: .invalidInput)
|
|
}
|
|
let url = URL(fileURLWithPath: id)
|
|
guard id == url.lastPathComponent, !id.hasPrefix("."), PatternSource.extensions.contains(url.pathExtension.lowercased()) else {
|
|
return errorResult("pattern_id must be a plain file name ending in .hexpat or .pat", code: .invalidInput)
|
|
}
|
|
do {
|
|
try engine.patterns.write(text, to: id)
|
|
let summary = try await engine.patternDecoder.summary(ofText: text)
|
|
var payload = summary.jsonObject
|
|
payload["id"] = id
|
|
return makeResult(jsonObject: payload, summary: "Wrote \(id)")
|
|
} catch {
|
|
return errorResult("write failed: \(error.localizedDescription)", code: .failed)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func registerDecodeMemory(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "decode_memory",
|
|
description: """
|
|
Decode process memory at an address with a pattern type, and return the decoded tree: \
|
|
each node's name, type, address, size and formatted value. Memory is read as far as the \
|
|
type needs, up to 1 MiB. The tree is cut at 'depth' levels, with arrays showing their \
|
|
first 'element_limit' elements; use 'path' (e.g. \\"commands[3].segname\\") to start \
|
|
deeper instead of widening the whole tree.
|
|
""",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{"session_id":{"type":"string"},"address":{"type":"string","description":"Hex address"},\
|
|
\(patternSourceSchema),\
|
|
"path":{"type":"string","description":"Node to return, as field names and [index] steps from the root"},\
|
|
"depth":{"type":"integer","minimum":0,"maximum":8,"default":2},\
|
|
"element_limit":{"type":"integer","minimum":0,"maximum":256,"default":8}},\
|
|
"required":["session_id","address"],"additionalProperties":false}
|
|
""",
|
|
isObserve: true,
|
|
requiresSession: true
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] invocation in
|
|
guard let engine else { return errorResult("engine unavailable", code: .unavailable) }
|
|
do {
|
|
let request = try await PatternRequest(invocation.args, engine: engine)
|
|
let decoding = try await request.decode(engine: engine)
|
|
let node = try decoding.value.descendant(at: request.path)
|
|
var payload = node.projection(
|
|
depth: min((invocation.args["depth"] as? Int) ?? 2, 8),
|
|
elementLimit: min((invocation.args["element_limit"] as? Int) ?? 8, 256))
|
|
payload["bytes_read"] = decoding.data.count
|
|
return makeResult(
|
|
jsonObject: payload, summary: "Decoded \(request.typeName) at \(String(format: "0x%llx", request.address))")
|
|
} catch {
|
|
return errorResult(error.localizedDescription, code: .failed)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func registerCallPatternFunction(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "call_pattern_function",
|
|
description: """
|
|
Decode like decode_memory, then call one of the pattern's functions with the node at \
|
|
'path', the way ImHex's button visualizer does, and return what it printed. Functions run \
|
|
in the pattern evaluator and cannot touch the process.
|
|
""",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{"session_id":{"type":"string"},"address":{"type":"string","description":"Hex address"},\
|
|
\(patternSourceSchema),\
|
|
"path":{"type":"string","description":"Node to pass, as field names and [index] steps from the root"},\
|
|
"function":{"type":"string"}},\
|
|
"required":["session_id","address","function"],"additionalProperties":false}
|
|
""",
|
|
isObserve: true,
|
|
requiresSession: true
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] invocation in
|
|
guard let engine else { return errorResult("engine unavailable", code: .unavailable) }
|
|
guard let function = invocation.args["function"] as? String else {
|
|
return errorResult("function is required", code: .invalidInput)
|
|
}
|
|
do {
|
|
let request = try await PatternRequest(invocation.args, engine: engine)
|
|
let decoding = try await request.decode(engine: engine)
|
|
let node = try decoding.value.descendant(at: request.path)
|
|
guard let target = engine.node(forSessionID: request.sessionID)?.processInfo else {
|
|
throw PatternMemoryError.detached
|
|
}
|
|
let output = try await engine.patternDecoder.callFunction(
|
|
function, on: node.nodeID, ofText: request.text, typeName: request.typeName, data: decoding.data,
|
|
address: request.address, platform: target.platform, arch: target.arch)
|
|
return makeResult(jsonObject: ["output": output], summary: "Called \(function)")
|
|
} catch {
|
|
return errorResult(error.localizedDescription, code: .failed)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func registerPlacePattern(in catalog: ToolCatalog, engine: Engine) {
|
|
let spec = ActionSpec(
|
|
name: "place_pattern",
|
|
description: """
|
|
Decode a pattern type in a memory insight's view, so the user sees the fields outlined \
|
|
over its hex dump. 'offset' is relative to the insight's address. Pin the memory with \
|
|
pin_as_insight first. Requires user approval.
|
|
""",
|
|
inputSchemaJSON: """
|
|
{"type":"object","properties":{"session_id":{"type":"string"},"insight_id":{"type":"string"},\
|
|
"pattern_id":{"type":"string"},"type":{"type":"string"},"offset":{"type":"integer","minimum":0,"default":0}},\
|
|
"required":["session_id","insight_id","pattern_id","type"],"additionalProperties":false}
|
|
""",
|
|
isObserve: false,
|
|
requiresSession: true
|
|
)
|
|
catalog.register(spec: spec) { [weak engine] invocation in
|
|
guard let engine, let sessionID = parseSessionID(invocation.args) else {
|
|
return errorResult("missing or invalid session_id", code: .invalidInput)
|
|
}
|
|
guard let idString = invocation.args["insight_id"] as? String, let insightID = UUID(uuidString: idString),
|
|
let insight = engine.insightsBySession[sessionID]?.first(where: { $0.id == insightID })
|
|
else {
|
|
return errorResult("no such insight on this session; see list_address_insights", code: .notFound)
|
|
}
|
|
guard insight.kind == .memory else {
|
|
return errorResult("patterns can only be placed on memory insights", code: .invalidInput)
|
|
}
|
|
guard let patternID = invocation.args["pattern_id"] as? String, engine.patterns.source(withID: patternID) != nil,
|
|
let typeName = invocation.args["type"] as? String
|
|
else {
|
|
return errorResult("pattern_id must name a library file and type is required", code: .invalidInput)
|
|
}
|
|
let placement = PatternPlacement(sourceID: patternID, typeName: typeName, offset: (invocation.args["offset"] as? Int) ?? 0)
|
|
engine.setPlacements(insight.placements + [placement], forInsight: insight)
|
|
return makeResult(
|
|
jsonObject: ["insight_id": insightID.uuidString, "placement_id": placement.id.uuidString],
|
|
summary: "Placed \(typeName) on \(engine.displayTitle(for: insight))")
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct PatternRequest {
|
|
let sessionID: UUID
|
|
let address: UInt64
|
|
let text: String
|
|
let typeName: String
|
|
let path: PatternPath
|
|
|
|
private static let initialByteCount = 0x200
|
|
|
|
@MainActor
|
|
init(_ args: [String: Any], engine: Engine) async throws {
|
|
guard let sessionID = MissionTools.parseSessionID(args) else { throw PatternRequestError("missing or invalid session_id") }
|
|
guard let addressText = args["address"] as? String, let address = MissionTools.parseHexAddress(addressText) else {
|
|
throw PatternRequestError("missing or invalid address")
|
|
}
|
|
if let id = args["pattern_id"] as? String {
|
|
guard let source = engine.patterns.source(withID: id) else {
|
|
throw PatternRequestError("no pattern file \(id); see list_patterns")
|
|
}
|
|
text = source.text
|
|
} else if let source = args["source"] as? String {
|
|
text = source
|
|
} else {
|
|
throw PatternRequestError("pass pattern_id or source")
|
|
}
|
|
if let typeName = args["type"] as? String {
|
|
self.typeName = typeName
|
|
} else {
|
|
let summary = try await engine.patternDecoder.summary(ofText: text)
|
|
guard let rootType = summary.rootType else {
|
|
let names = summary.decodableTypes.map(\.name).joined(separator: ", ")
|
|
throw PatternRequestError("the source places nothing at its root; pass type, one of: \(names)")
|
|
}
|
|
typeName = rootType
|
|
}
|
|
self.sessionID = sessionID
|
|
self.address = address
|
|
path = try PatternPath((args["path"] as? String) ?? "")
|
|
}
|
|
|
|
@MainActor
|
|
func decode(engine: Engine) async throws -> PatternMemoryDecoding {
|
|
try await engine.decodePattern(
|
|
text: text, typeName: typeName, sessionID: sessionID, address: address, byteCount: Self.initialByteCount)
|
|
}
|
|
}
|
|
|
|
private struct PatternRequestError: LocalizedError {
|
|
let message: String
|
|
|
|
init(_ message: String) {
|
|
self.message = message
|
|
}
|
|
|
|
var errorDescription: String? { message }
|
|
}
|