Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ xcuserdata/
*.swp
*.swo
*~
.run

# Temporary files
*.tmp
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PackageDescription

let package = Package(
name: "Aether",
defaultLocalization: "en",
platforms: [
.macOS(.v14)
],
Expand Down
113 changes: 69 additions & 44 deletions Sources/Aether/App/AetherApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,187 +4,190 @@ import SwiftUI
struct AetherApp: App {
@StateObject private var appState = AppState()
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@AppStorage("appLanguage") private var appLanguage = AppLanguage.systemCode

var body: some Scene {
WindowGroup {
MainView()
.id(appLanguage)
.environmentObject(appState)
.environment(\.locale, AppLanguage.fromStored(appLanguage).locale)
.preferredColorScheme(.dark)
}
.windowStyle(.hiddenTitleBar)
.commands {
CommandGroup(replacing: .newItem) {
Button("Open Binary...") {
Button(translate("menu.file.openBinary")) {
appState.openFile()
}
.keyboardShortcut("o", modifiers: .command)

Button("Open Project...") {
Button(translate("menu.file.openProject")) {
appState.openProject()
}
.keyboardShortcut("o", modifiers: [.command, .shift])

Divider()

Button("Save Binary As...") {
Button(translate("menu.file.saveBinaryAs")) {
appState.saveFileAs()
}
.keyboardShortcut("s", modifiers: .command)
.disabled(appState.currentFile == nil)

Button("Save Project As...") {
Button(translate("menu.file.saveProjectAs")) {
appState.saveProjectAs()
}
.keyboardShortcut("s", modifiers: [.command, .shift])
.disabled(appState.currentFile == nil)

Divider()

Button("Close") {
Button(translate("menu.file.close")) {
appState.closeFile()
}
.keyboardShortcut("w", modifiers: .command)
.disabled(appState.currentFile == nil)
}

CommandGroup(replacing: .undoRedo) {
Button("Undo") {
Button(translate("menu.edit.undo")) {
appState.undo()
}
.keyboardShortcut("z", modifiers: .command)
.disabled(!appState.canUndo)

Button("Redo") {
Button(translate("menu.edit.redo")) {
appState.redo()
}
.keyboardShortcut("z", modifiers: [.command, .shift])
.disabled(!appState.canRedo)
}
CommandMenu("Analysis") {
Button("Analyze All") {
CommandMenu(translate("menu.analysis.title")) {
Button(translate("menu.analysis.analyzeAll")) {
appState.analyzeAll()
}
.keyboardShortcut("a", modifiers: [.command, .shift])
.disabled(appState.currentFile == nil)

Button("Find Functions") {
Button(translate("menu.analysis.findFunctions")) {
appState.findFunctions()
}
.keyboardShortcut("f", modifiers: [.command, .shift])
.disabled(appState.currentFile == nil)

Divider()

Button("Show CFG") {
Button(translate("menu.analysis.showCFG")) {
appState.showCFG = true
}
.keyboardShortcut("g", modifiers: .command)
.disabled(appState.selectedFunction == nil)

Button("Decompile") {
Button(translate("menu.analysis.decompile")) {
appState.decompileCurrentFunction()
}
.keyboardShortcut("d", modifiers: [.command, .shift])
.disabled(appState.selectedFunction == nil)

Button("Generate Pseudo-Code") {
Button(translate("menu.analysis.generatePseudoCode")) {
appState.generateStructuredCode()
}
.keyboardShortcut("p", modifiers: [.command, .shift])
.disabled(appState.selectedFunction == nil)

Divider()

Button("Call Graph") {
Button(translate("menu.analysis.callGraph")) {
appState.showCallGraph = true
}
.keyboardShortcut("k", modifiers: .command)
.disabled(appState.currentFile == nil)

Button("Crypto Detection") {
Button(translate("menu.analysis.cryptoDetection")) {
appState.runCryptoDetection()
}
.disabled(appState.currentFile == nil)

Button("Deobfuscation Analysis") {
Button(translate("menu.analysis.deobfuscation")) {
appState.runDeobfuscation()
}
.disabled(appState.selectedFunction == nil)

Button("Type Recovery") {
Button(translate("menu.analysis.typeRecovery")) {
appState.runTypeRecovery()
}
.disabled(appState.selectedFunction == nil)

Button("Idiom Recognition") {
Button(translate("menu.analysis.idiomRecognition")) {
appState.runIdiomRecognition()
}
.disabled(appState.selectedFunction == nil)

Divider()

Button("Show Jump Table") {
Button(translate("menu.analysis.showJumpTable")) {
appState.showJumpTable = true
}
.keyboardShortcut("j", modifiers: [.command, .shift])
.disabled(appState.selectedFunction == nil)
}

CommandMenu("Export") {
Button("Export to IDA Python...") {
CommandMenu(translate("menu.export.title")) {
Button(translate("menu.export.ida")) {
appState.showExportSheet = true
}
.disabled(appState.currentFile == nil)

Button("Export to Ghidra XML...") {
Button(translate("menu.export.ghidra")) {
exportWithFormat(.ghidraXML)
}
.disabled(appState.currentFile == nil)

Button("Export to Radare2...") {
Button(translate("menu.export.radare2")) {
exportWithFormat(.radare2)
}
.disabled(appState.currentFile == nil)

Button("Export to Binary Ninja...") {
Button(translate("menu.export.binaryNinja")) {
exportWithFormat(.binaryNinja)
}
.disabled(appState.currentFile == nil)

Divider()

Button("Export to JSON...") {
Button(translate("menu.export.json")) {
exportWithFormat(.json)
}
.disabled(appState.currentFile == nil)

Button("Export to CSV...") {
Button(translate("menu.export.csv")) {
exportWithFormat(.csv)
}
.disabled(appState.currentFile == nil)

Button("Export to HTML Report...") {
Button(translate("menu.export.html")) {
exportWithFormat(.html)
}
.disabled(appState.currentFile == nil)

Button("Export to Markdown...") {
Button(translate("menu.export.markdown")) {
exportWithFormat(.markdown)
}
.disabled(appState.currentFile == nil)

Button("Export C Header...") {
Button(translate("menu.export.cheader")) {
exportWithFormat(.cHeader)
}
.disabled(appState.currentFile == nil)
}
CommandMenu("Navigate") {
Button("Go to Address...") {
CommandMenu(translate("menu.navigate.title")) {
Button(translate("menu.navigate.gotoAddress")) {
appState.showGoToAddress = true
}
.keyboardShortcut("g", modifiers: [.command, .shift])

Button("Search...") {
Button(translate("menu.navigate.search")) {
appState.showSearch = true
}
.keyboardShortcut("f", modifiers: .command)
Expand All @@ -193,14 +196,16 @@ struct AetherApp: App {

Settings {
SettingsView()
.id(appLanguage)
.environmentObject(appState)
.environment(\.locale, AppLanguage.fromStored(appLanguage).locale)
}
}

private func exportWithFormat(_ format: ExportManager.ExportFormat) {
let panel = NSSavePanel()
panel.allowedContentTypes = [.data]
panel.nameFieldStringValue = "\(appState.currentFile?.name ?? "export").\(format.fileExtension)"
panel.nameFieldStringValue = "\(appState.currentFile?.name ?? translate("export.defaultFileName")).\(format.fileExtension)"

if panel.runModal() == .OK, let url = panel.url {
appState.exportTo(format: format, url: url)
Expand All @@ -215,22 +220,22 @@ struct SettingsView: View {
TabView {
GeneralSettingsView()
.tabItem {
Label("General", systemImage: "gear")
Label(translate("settings.tab.general"), systemImage: "gear")
}

AppearanceSettingsView()
.tabItem {
Label("Appearance", systemImage: "paintbrush")
Label(translate("settings.tab.appearance"), systemImage: "paintbrush")
}

AnalysisSettingsView()
.tabItem {
Label("Analysis", systemImage: "cpu")
Label(translate("settings.tab.analysis"), systemImage: "cpu")
}

AISettingsTab()
.tabItem {
Label("AI", systemImage: "brain")
Label(translate("settings.tab.ai"), systemImage: "brain")
}
}
.frame(width: 500, height: 350)
Expand All @@ -240,13 +245,24 @@ struct SettingsView: View {
struct GeneralSettingsView: View {
@AppStorage("autoAnalyze") private var autoAnalyze = true
@AppStorage("showHexView") private var showHexView = true
@AppStorage("appLanguage") private var appLanguage = AppLanguage.systemCode

var body: some View {
Form {
Toggle("Auto-analyze on file open", isOn: $autoAnalyze)
Toggle("Show Hex View by default", isOn: $showHexView)
Picker(translate("settings.general.language"), selection: $appLanguage) {
ForEach(AppLanguage.available) { language in
Text(language.pickerLabel).tag(language.code)
}
}
Toggle(translate("settings.general.autoAnalyze"), isOn: $autoAnalyze)
Toggle(translate("settings.general.showHexView"), isOn: $showHexView)
}
.padding()
.onAppear {
if !AppLanguage.isValidStorageValue(appLanguage) {
appLanguage = AppLanguage.systemCode
}
}
}
}

Expand All @@ -256,15 +272,24 @@ struct AppearanceSettingsView: View {

var body: some View {
Form {
Picker("Font", selection: $fontName) {
Picker(translate("settings.appearance.font"), selection: $fontName) {
Text("SF Mono").tag("SF Mono")
Text("Menlo").tag("Menlo")
Text("Monaco").tag("Monaco")
Text("Courier New").tag("Courier New")
}

Slider(value: $fontSize, in: 10...20, step: 1) {
Text("Font Size: \(Int(fontSize))")
Text(translate("settings.appearance.fontSize"))
}

HStack {
Text(translate("settings.appearance.fontSize"))
.font(.caption)
.foregroundColor(.secondary)
Text("\(Int(fontSize))")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
Expand All @@ -278,9 +303,9 @@ struct AnalysisSettingsView: View {

var body: some View {
Form {
Toggle("Deep analysis (slower)", isOn: $deepAnalysis)
Toggle("Analyze strings", isOn: $analyzeStrings)
Toggle("Analyze cross-references", isOn: $analyzeXRefs)
Toggle(translate("settings.analysis.deepAnalysis"), isOn: $deepAnalysis)
Toggle(translate("settings.analysis.analyzeStrings"), isOn: $analyzeStrings)
Toggle(translate("settings.analysis.analyzeXrefs"), isOn: $analyzeXRefs)
}
.padding()
}
Expand Down
Loading