Files
luma/Sources/LumaCore/Patterns/PatternVisualization.swift
Ole André Vadla Ravnås ad61503b47 core: Decode pattern visualizers
Interpret each of ImHex's visualizers from its arguments, and
disassemble through r2.
2026-10-01 20:49:11 +02:00

411 lines
16 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Frida
public struct DecodedVisualizer: Sendable {
public let name: String
public let presentation: Presentation
public let arguments: [Argument]
public enum Presentation: Sendable {
case detached
case inline
}
public enum Argument: Sendable {
case value(DecodedScalar?)
case pattern(PatternArgument)
}
public struct PatternArgument: Sendable {
public let nodeID: UInt?
public let value: DecodedScalar?
public let address: UInt64
public let data: Data
}
init(_ visualizer: PatternVisualizer) {
name = visualizer.name
presentation = visualizer.presentation == .inline ? .inline : .detached
arguments = visualizer.arguments.map(Argument.init)
}
}
extension DecodedVisualizer.Argument {
init(_ argument: PatternVisualizerArgument) {
let value = argument.value.flatMap(DecodedScalar.init)
switch argument.kind {
case .value:
self = .value(value)
case .pattern:
self = .pattern(
DecodedVisualizer.PatternArgument(
nodeID: argument.pattern == 0 ? nil : argument.pattern,
value: value,
address: argument.address,
data: Data(argument.data ?? [])))
}
}
}
public enum PatternVisualization: Sendable {
case linePlot([Float])
case scatterPlot(x: [Float], y: [Float])
case image(Data)
case bitmap(Bitmap)
case model(Model)
case sound(Sound)
case coordinates(latitude: Double, longitude: Double)
case timestamp(Date)
case table(Table)
case digitalSignal([SignalSegment])
case hexViewer(Data, address: UInt64)
case chunkEntropy(ChunkEntropy)
case disassembly(Disassembly)
case color(red: Double, green: Double, blue: Double, alpha: Double)
case gauge(Double)
case button(function: String, label: String)
public struct Bitmap: Sendable {
public let width: Int
public let height: Int
public let rgba: Data
}
public struct Model: Sendable {
public let vertices: [Float]
public let indices: [UInt32]?
public let normals: [Float]
public let colors: [Float]
public let uv: [Float]
public let texturePath: String?
}
public struct Sound: Sendable {
public let samples: [Int16]
public let channels: Int
public let sampleRate: Int
}
public struct Table: Sendable {
public let columns: Int
public let rows: Int
public let cells: [String]
public func cell(row: Int, column: Int) -> String {
let index = row * columns + column
return index < cells.count ? cells[index] : "??"
}
}
public struct SignalSegment: Sendable {
public let label: String
public let value: String
public let color: String?
public let bits: Int
public let isHigh: Bool
}
public struct ChunkEntropy: Sendable {
public let chunkSize: Int
public let values: [Double]
}
public struct Disassembly: Sendable {
public let data: Data
public let address: UInt64
public let architecture: String
}
public init(_ visualizer: DecodedVisualizer, of owner: DecodedPattern, in root: DecodedPattern) throws {
let arguments = VisualizerArguments(name: visualizer.name, values: visualizer.arguments, root: root)
switch visualizer.name {
case "line_plot":
try arguments.expect(1)
self = .linePlot(try arguments.pattern(0).data.elements(of: Float.self))
case "scatter_plot":
try arguments.expect(2)
self = .scatterPlot(
x: try arguments.pattern(0).data.elements(of: Float.self), y: try arguments.pattern(1).data.elements(of: Float.self))
case "image":
try arguments.expect(1)
self = .image(try arguments.pattern(0).data)
case "bitmap":
try arguments.expect(3...4)
self = .bitmap(try Self.bitmap(arguments))
case "3d":
try arguments.expect(2...6)
self = .model(try Self.model(arguments))
case "sound":
try arguments.expect(3)
self = .sound(try Self.sound(arguments))
case "coordinates":
try arguments.expect(2)
self = .coordinates(latitude: try arguments.number(0), longitude: try arguments.number(1))
case "timestamp":
try arguments.expect(1)
self = .timestamp(Date(timeIntervalSince1970: try arguments.number(0)))
case "table":
try arguments.expect(3)
self = .table(try Self.table(arguments))
case "digital_signal":
try arguments.expect(1)
self = .digitalSignal(try Self.signal(arguments))
case "hex_viewer":
try arguments.expect(1)
let pattern = try arguments.pattern(0)
self = .hexViewer(pattern.data, address: pattern.address)
case "chunk_entropy":
try arguments.expect(2)
let chunkSize = try arguments.count(1)
self = .chunkEntropy(
ChunkEntropy(chunkSize: chunkSize, values: try Self.chunkEntropy(of: arguments.pattern(0).data, chunkSize: chunkSize)))
case "disassembler":
try arguments.expect(3)
self = .disassembly(
Disassembly(
data: try arguments.pattern(0).data, address: UInt64(try arguments.number(1)), architecture: try arguments.string(2)))
case "color":
try arguments.expect(4)
self = .color(
red: try arguments.number(0) / 255, green: try arguments.number(1) / 255, blue: try arguments.number(2) / 255,
alpha: try arguments.number(3) / 255)
case "gauge":
try arguments.expect(1)
self = .gauge(try arguments.number(0) / 100)
case "button":
try arguments.expect(1)
self = .button(function: try arguments.string(0), label: owner.summary)
default:
throw PatternVisualizationError("Unknown visualizer \"\(visualizer.name)\".")
}
}
private static func bitmap(_ arguments: VisualizerArguments) throws -> Bitmap {
let pixels = try arguments.pattern(0).data
let width = try arguments.count(1)
let height = try arguments.count(2)
if arguments.values.count == 4 {
let colorTable = try arguments.pattern(3).data
if !colorTable.isEmpty {
return Bitmap(width: width, height: height, rgba: palettized(pixels, width: width, height: height, colorTable: colorTable))
}
}
guard pixels.count >= width * height * 4 else {
throw PatternVisualizationError(
"A \(width)×\(height) bitmap needs \(width * height * 4) bytes of RGBA, but got \(pixels.count).")
}
return Bitmap(width: width, height: height, rgba: pixels.prefix(width * height * 4))
}
private static func palettized(_ pixels: Data, width: Int, height: Int, colorTable: Data) -> Data {
let colors = colorTable.count / 4
var rgba = Data(capacity: width * height * 4)
for index in paletteIndices(pixels, count: width * height) {
let entry = index < colors ? index : 0
rgba.append(colorTable[colorTable.startIndex + entry * 4..<colorTable.startIndex + entry * 4 + 4])
}
return rgba
}
private static func paletteIndices(_ pixels: Data, count: Int) -> [Int] {
guard !pixels.isEmpty, count > 0 else { return [] }
if pixels.count >= count {
switch pixels.count / count {
case 1:
return pixels.map(Int.init)
case 2:
return pixels.elements(of: UInt16.self).map(Int.init)
default:
return []
}
}
guard count / pixels.count == 2 else { return [] }
return pixels.flatMap { [Int($0 & 0xf), Int($0 >> 4)] }
}
private static func model(_ arguments: VisualizerArguments) throws -> Model {
let vertices = try arguments.pattern(0).data.elements(of: Float.self)
guard vertices.count >= 3, vertices.count % 3 == 0 else {
throw PatternVisualizationError("Vertex positions must be a multiple of three floats, but got \(vertices.count).")
}
let indices = try modelIndices(arguments.node(1), data: arguments.pattern(1).data)
if let indices {
guard indices.count >= 3, indices.count % 3 == 0 else {
throw PatternVisualizationError("Indices must come in triangles, but got \(indices.count).")
}
let vertexCount = UInt32(vertices.count / 3)
let outOfRange = indices.filter { $0 >= vertexCount }
guard outOfRange.isEmpty else {
let listed = outOfRange.prefix(8).map(String.init).joined(separator: ", ")
throw PatternVisualizationError("Indices \(listed) are out of range for \(vertexCount) vertices.")
}
}
let count = arguments.values.count
return Model(
vertices: vertices,
indices: indices,
normals: count > 2 ? try arguments.pattern(2).data.elements(of: Float.self) : [],
colors: count > 3 ? try arguments.pattern(3).data.elements(of: Float.self) : [],
uv: count > 4 ? try arguments.pattern(4).data.elements(of: Float.self) : [],
texturePath: count > 5 ? try arguments.string(5) : nil)
}
private static func modelIndices(_ node: DecodedPattern, data: Data) throws -> [UInt32]? {
guard var entry = node.children.first else { return nil }
while (entry.size ?? 0) == 0 {
guard let first = entry.children.first else {
throw PatternVisualizationError("Indices must be an array of integers.")
}
entry = first
}
switch entry.size {
case 1:
return data.map(UInt32.init)
case 2:
return data.elements(of: UInt16.self).map(UInt32.init)
case 4:
return data.elements(of: UInt32.self)
default:
throw PatternVisualizationError("Indices must be 8, 16 or 32 bits wide.")
}
}
private static func sound(_ arguments: VisualizerArguments) throws -> Sound {
let channels = try arguments.count(1)
let sampleRate = try arguments.count(2)
guard channels > 0 else { throw PatternVisualizationError("A sound needs at least one channel.") }
guard sampleRate > 0 else { throw PatternVisualizationError("A sound needs a sample rate.") }
return Sound(samples: try arguments.pattern(0).data.elements(of: Int16.self), channels: channels, sampleRate: sampleRate)
}
private static func table(_ arguments: VisualizerArguments) throws -> Table {
let array = try arguments.node(0)
guard array.fields.isEmpty else {
throw PatternVisualizationError("The table visualizer needs an array.")
}
return Table(columns: try arguments.count(1), rows: try arguments.count(2), cells: array.elements.map(\.summary))
}
private static func signal(_ arguments: VisualizerArguments) throws -> [SignalSegment] {
let bitfield = try arguments.node(0)
guard !bitfield.fields.isEmpty, bitfield.fields.allSatisfy({ $0.bitOffset != nil }) else {
throw PatternVisualizationError("The digital signal visualizer needs a bitfield.")
}
return bitfield.fields.map { field in
SignalSegment(
label: field.displayName ?? field.name,
value: field.summary,
color: field.color,
bits: field.bits > 0 ? field.bits : (field.size ?? 0) * 8,
isHigh: (field.value?.number ?? 0) > 0)
}
}
private static func chunkEntropy(of data: Data, chunkSize: Int) throws -> [Double] {
guard chunkSize > 0 else { throw PatternVisualizationError("The chunk size must be positive.") }
return stride(from: data.startIndex, to: data.endIndex, by: chunkSize).map { start in
normalizedEntropy(of: data[start..<min(start + chunkSize, data.endIndex)])
}
}
private static func normalizedEntropy(of chunk: Data) -> Double {
var counts = [Int](repeating: 0, count: 256)
for byte in chunk {
counts[Int(byte)] += 1
}
let total = Double(chunk.count)
let bits = counts.reduce(0.0) { sum, count in
guard count > 0 else { return sum }
let probability = Double(count) / total
return sum - probability * log2(probability)
}
return bits / 8
}
}
public struct PatternVisualizationError: LocalizedError, Sendable {
public let message: String
init(_ message: String) {
self.message = message
}
public var errorDescription: String? { message }
}
private struct VisualizerArguments {
let name: String
let values: [DecodedVisualizer.Argument]
let root: DecodedPattern
func expect(_ count: Int) throws {
try expect(count...count)
}
func expect(_ counts: ClosedRange<Int>) throws {
guard counts.contains(values.count) else {
let expected = counts.count == 1 ? "\(counts.lowerBound)" : "\(counts.lowerBound) to \(counts.upperBound)"
throw PatternVisualizationError("\(name) takes \(expected) arguments after its name, but got \(values.count).")
}
}
func pattern(_ index: Int) throws -> DecodedVisualizer.PatternArgument {
guard case .pattern(let pattern) = values[index] else {
throw PatternVisualizationError("Argument \(index + 1) of \(name) must be a pattern.")
}
return pattern
}
func node(_ index: Int) throws -> DecodedPattern {
guard let nodeID = try pattern(index).nodeID, let node = root.descendant(withNodeID: nodeID) else {
throw PatternVisualizationError("Argument \(index + 1) of \(name) is not part of the decoded value.")
}
return node
}
func number(_ index: Int) throws -> Double {
guard let number = scalar(index)?.number else {
throw PatternVisualizationError("Argument \(index + 1) of \(name) must be a number.")
}
return number
}
func count(_ index: Int) throws -> Int {
let number = try number(index)
guard number >= 0, number <= Double(Int32.max) else {
throw PatternVisualizationError("Argument \(index + 1) of \(name) must be a count, but is \(number).")
}
return Int(number)
}
func string(_ index: Int) throws -> String {
guard case .string(let text) = scalar(index) else {
throw PatternVisualizationError("Argument \(index + 1) of \(name) must be a string.")
}
return text
}
private func scalar(_ index: Int) -> DecodedScalar? {
switch values[index] {
case .value(let scalar):
return scalar
case .pattern(let pattern):
return pattern.value
}
}
}
extension Data {
fileprivate func elements<T: FixedWidthInteger>(of type: T.Type) -> [T] {
withUnsafeBytes { raw in
(0..<count / MemoryLayout<T>.size).map {
T(littleEndian: raw.loadUnaligned(fromByteOffset: $0 * MemoryLayout<T>.size, as: T.self))
}
}
}
fileprivate func elements(of type: Float.Type) -> [Float] {
elements(of: UInt32.self).map(Float.init(bitPattern:))
}
}