Files
luma/Luma/ITraceDiffView.swift
Ole André Vadla Ravnås 54868d8b9d Extract portable model types into LumaCore
Move pure data types out of Luma/ into Sources/LumaCore/
so they can be shared with the GTK frontend:

- AddressAnchor, CFGGraph, ITraceDecoder (+ all related
  types: TraceEntry, ITraceMetadata, RegisterState, etc.)
- JSInspectValue, SpawnConfig, ProcessModule
- HookPackConfig, HookPackManifest, InstrumentConfigCodec
- InstrumentIcon, InstrumentKind, ConsoleLevel

CFGGraph uses its own Point2D instead of CoreGraphics
types, keeping the core free of Apple frameworks.

Remove premature stubs (Engine, ProcessNode,
InstrumentRuntime, EventStream) that will be properly
built in a later phase once the real implementations
are ready to move.
2026-04-06 19:19:20 +02:00

304 lines
9.2 KiB
Swift

import Frida
import LumaCore
import SwiftUI
struct ITraceDiffView: View {
let left: DecodedITrace
let right: DecodedITrace
let leftName: String
let rightName: String
@State private var diffResult: [DiffEntry] = []
@State private var firstDivergenceIndex: Int?
enum Side {
case both
case leftOnly
case rightOnly
}
struct DiffEntry: Identifiable {
let id = UUID()
let side: Side
let leftEntry: TraceEntry?
let rightEntry: TraceEntry?
let registerDiffs: [RegisterDiff]
}
struct RegisterDiff {
let registerName: String
let leftValue: UInt64?
let rightValue: UInt64?
}
var body: some View {
VStack(spacing: 0) {
header
Divider()
if diffResult.isEmpty {
Text("Traces are identical.")
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollViewReader { scrollProxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(Array(diffResult.enumerated()), id: \.element.id) { index, entry in
DiffRow(entry: entry)
.id(index)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
}
.onAppear {
if let idx = firstDivergenceIndex {
scrollProxy.scrollTo(idx, anchor: .center)
}
}
}
}
}
.onAppear { computeDiff() }
}
private var header: some View {
HStack {
Text("Diff: \(leftName) vs \(rightName)")
.font(.headline)
Spacer()
if let idx = firstDivergenceIndex {
Text("First divergence at entry \(idx)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
private func computeDiff() {
let leftAddrs = left.entries.map(\.blockAddress)
let rightAddrs = right.entries.map(\.blockAddress)
let lcs = longestCommonSubsequence(leftAddrs, rightAddrs)
var result: [DiffEntry] = []
var li = 0, ri = 0, ci = 0
var foundDivergence: Int?
while li < left.entries.count || ri < right.entries.count {
if ci < lcs.count,
li < left.entries.count,
ri < right.entries.count,
left.entries[li].blockAddress == lcs[ci],
right.entries[ri].blockAddress == lcs[ci]
{
let regDiffs = compareRegisters(
left.entries[li], right.entries[ri])
let entry = DiffEntry(
side: .both,
leftEntry: left.entries[li],
rightEntry: right.entries[ri],
registerDiffs: regDiffs
)
if !regDiffs.isEmpty && foundDivergence == nil {
foundDivergence = result.count
}
result.append(entry)
li += 1
ri += 1
ci += 1
} else if li < left.entries.count
&& (ci >= lcs.count || left.entries[li].blockAddress != lcs[ci])
{
if foundDivergence == nil {
foundDivergence = result.count
}
result.append(DiffEntry(
side: .leftOnly,
leftEntry: left.entries[li],
rightEntry: nil,
registerDiffs: []
))
li += 1
} else if ri < right.entries.count
&& (ci >= lcs.count || right.entries[ri].blockAddress != lcs[ci])
{
if foundDivergence == nil {
foundDivergence = result.count
}
result.append(DiffEntry(
side: .rightOnly,
leftEntry: nil,
rightEntry: right.entries[ri],
registerDiffs: []
))
ri += 1
} else {
break
}
}
diffResult = result
firstDivergenceIndex = foundDivergence
}
private func compareRegisters(
_ a: TraceEntry, _ b: TraceEntry
) -> [RegisterDiff] {
var diffs: [RegisterDiff] = []
var aByIndex: [Int: RegisterWrite] = [:]
for w in a.registerWrites { aByIndex[w.registerIndex] = w }
var bByIndex: [Int: RegisterWrite] = [:]
for w in b.registerWrites { bByIndex[w.registerIndex] = w }
let allIndices = Set(aByIndex.keys).union(bByIndex.keys).sorted()
for idx in allIndices {
let aVal = aByIndex[idx]?.value
let bVal = bByIndex[idx]?.value
if aVal != bVal {
let name = aByIndex[idx]?.registerName
?? bByIndex[idx]?.registerName ?? "r\(idx)"
diffs.append(RegisterDiff(
registerName: name,
leftValue: aVal,
rightValue: bVal
))
}
}
return diffs
}
}
private struct DiffRow: View {
let entry: ITraceDiffView.DiffEntry
var body: some View {
HStack(spacing: 8) {
sideIndicator
let name = entry.leftEntry?.blockName
?? entry.rightEntry?.blockName ?? ""
Text(name)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.cyan)
if !entry.registerDiffs.isEmpty {
HStack(spacing: 6) {
ForEach(
entry.registerDiffs.prefix(3),
id: \.registerName
) { diff in
registerDiffText(diff)
}
if entry.registerDiffs.count > 3 {
Text("+\(entry.registerDiffs.count - 3)")
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(.secondary)
}
}
}
}
.padding(.vertical, 2)
.padding(.horizontal, 4)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundColor)
}
@ViewBuilder
private var sideIndicator: some View {
switch entry.side {
case .both:
Text(" ")
.font(.system(.caption, design: .monospaced))
.frame(width: 12)
case .leftOnly:
Text("-")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.red)
.frame(width: 12)
case .rightOnly:
Text("+")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.green)
.frame(width: 12)
}
}
private var backgroundColor: Color {
switch entry.side {
case .both:
if entry.registerDiffs.isEmpty {
return .clear
}
return .yellow.opacity(0.1)
case .leftOnly:
return .red.opacity(0.1)
case .rightOnly:
return .green.opacity(0.1)
}
}
private func registerDiffText(_ diff: ITraceDiffView.RegisterDiff) -> some View {
HStack(spacing: 2) {
Text(diff.registerName)
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(.secondary)
if let l = diff.leftValue {
Text(String(format: "0x%llx", l))
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(.red)
}
Text("→")
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(.secondary)
if let r = diff.rightValue {
Text(String(format: "0x%llx", r))
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(.green)
}
}
}
}
private func longestCommonSubsequence(
_ a: [UInt64], _ b: [UInt64]
) -> [UInt64] {
let m = a.count, n = b.count
guard m > 0, n > 0 else { return [] }
var dp = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1)
for i in 1...m {
for j in 1...n {
if a[i - 1] == b[j - 1] {
dp[i][j] = dp[i - 1][j - 1] + 1
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
}
}
}
var result: [UInt64] = []
var i = m, j = n
while i > 0, j > 0 {
if a[i - 1] == b[j - 1] {
result.append(a[i - 1])
i -= 1
j -= 1
} else if dp[i - 1][j] > dp[i][j - 1] {
i -= 1
} else {
j -= 1
}
}
return result.reversed()
}