From 482341f6b1680fbdeda0b817261a64d4006158fb Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:56:38 -0400 Subject: [PATCH] GP-1977: Introduce Terminal Service and Plugin --- Ghidra/Debug/Debugger-agent-gdb/build.gradle | 2 +- .../gdb/GdbInJvmDebuggerModelFactory.java | 2 +- .../gdb/GdbOverSshDebuggerModelFactory.java | 2 +- .../gdb/gadp/impl/GdbGadpServerImpl.java | 2 +- .../java/agent/gdb/manager/GdbManager.java | 4 +- .../gdb/manager/impl/GdbManagerImpl.java | 6 +- .../agent/gdb/model/impl/GdbModelImpl.java | 2 +- .../manager/impl/AbstractGdbManagerTest.java | 4 +- .../manager/impl/JoinedGdbManagerTest.java | 4 +- .../impl/SpawnedWindowsMi2GdbManagerTest.java | 4 +- .../agent/gdb/model/ssh/SshGdbModelHost.java | 2 +- .../gdb/model/ssh/SshJoinGdbModelHost.java | 2 +- Ghidra/Debug/Debugger-rmi-trace/build.gradle | 1 + .../RunBashInTerminalScript.java | 39 + .../ghidra_scripts/TerminalGhidraScript.java | 77 + Ghidra/Debug/Framework-Debugging/build.gradle | 2 +- .../Features/Base/data/base.theme.properties | 25 +- .../core/clipboard/ClipboardPlugin.java | 23 +- .../plugin/core/interpreter/AnsiRenderer.java | 3 - .../plugin/core/terminal/DefaultTerminal.java | 67 + .../terminal/TerminalAwtEventEncoder.java | 314 +++ .../terminal/TerminalClipboardProvider.java | 193 ++ .../plugin/core/terminal/TerminalFinder.java | 250 +++ .../plugin/core/terminal/TerminalLayout.java | 43 + .../core/terminal/TerminalLayoutModel.java | 633 +++++++ .../core/terminal/TerminalListener.java | 41 + .../plugin/core/terminal/TerminalPanel.java | 712 +++++++ .../plugin/core/terminal/TerminalPlugin.java | 102 + .../core/terminal/TerminalProvider.java | 309 +++ .../core/terminal/TerminalTextField.java | 286 +++ .../terminal/TerminalTextFieldElement.java | 229 +++ .../core/terminal/ThreadedTerminal.java | 115 ++ .../core/terminal/vt/AnsiColorResolver.java | 49 + .../plugin/core/terminal/vt/VtAttributes.java | 175 ++ .../app/plugin/core/terminal/vt/VtBuffer.java | 753 ++++++++ .../plugin/core/terminal/vt/VtCharset.java | 111 ++ .../plugin/core/terminal/vt/VtHandler.java | 1682 +++++++++++++++++ .../app/plugin/core/terminal/vt/VtLine.java | 330 ++++ .../app/plugin/core/terminal/vt/VtOutput.java | 34 + .../app/plugin/core/terminal/vt/VtParser.java | 123 ++ .../core/terminal/vt/VtResponseEncoder.java | 64 + .../app/plugin/core/terminal/vt/VtState.java | 337 ++++ .../ClipboardContentProviderService.java | 32 + .../ghidra/app/services/ClipboardService.java | 6 +- .../java/ghidra/app/services/Terminal.java | 78 + .../ghidra/app/services/TerminalService.java | 119 ++ .../java/docking/DockingKeyBindingAction.java | 13 +- .../java/docking/DockingWindowManager.java | 16 +- .../docking/action/MultipleKeyAction.java | 1 + .../indexedscrollpane/IndexedScrollPane.java | 21 +- .../main/java/generic/theme/ThemeManager.java | 2 +- Ghidra/Framework/Pty/Module.manifest | 0 Ghidra/Framework/Pty/build.gradle | 30 + Ghidra/Framework/Pty/certification.manifest | 4 + .../Pty/src/main/java/ghidra}/pty/Pty.java | 2 +- .../src/main/java/ghidra}/pty/PtyChild.java | 2 +- .../main/java/ghidra}/pty/PtyEndpoint.java | 2 +- .../src/main/java/ghidra}/pty/PtyFactory.java | 8 +- .../src/main/java/ghidra}/pty/PtyParent.java | 3 +- .../src/main/java/ghidra}/pty/PtySession.java | 2 +- .../src/main/java/ghidra}/pty/linux/Err.java | 2 +- .../java/ghidra}/pty/linux/FdInputStream.java | 2 +- .../ghidra}/pty/linux/FdOutputStream.java | 2 +- .../main/java/ghidra}/pty/linux/LinuxPty.java | 4 +- .../java/ghidra}/pty/linux/LinuxPtyChild.java | 10 +- .../ghidra}/pty/linux/LinuxPtyEndpoint.java | 4 +- .../ghidra}/pty/linux/LinuxPtyFactory.java | 6 +- .../ghidra}/pty/linux/LinuxPtyParent.java | 18 +- .../pty/linux/LinuxPtySessionLeader.java | 2 +- .../main/java/ghidra}/pty/linux/PosixC.java | 22 +- .../src/main/java/ghidra}/pty/linux/Util.java | 2 +- .../pty/local/LocalProcessPtySession.java | 4 +- .../LocalWindowsNativeProcessPtySession.java | 6 +- .../ghidra}/pty/macos/MacosPtyFactory.java | 8 +- .../ghidra}/pty/ssh/GhidraSshPtyFactory.java | 4 +- .../src/main/java/ghidra}/pty/ssh/SshPty.java | 17 +- .../java/ghidra}/pty/ssh/SshPtyChild.java | 9 +- .../java/ghidra}/pty/ssh/SshPtyEndpoint.java | 10 +- .../java/ghidra}/pty/ssh/SshPtyParent.java | 15 +- .../java/ghidra}/pty/ssh/SshPtySession.java | 4 +- .../pty/windows/AnsiBufferedInputStream.java | 2 +- .../main/java/ghidra}/pty/windows/ConPty.java | 11 +- .../java/ghidra}/pty/windows/ConPtyChild.java | 10 +- .../ghidra}/pty/windows/ConPtyEndpoint.java | 4 +- .../ghidra}/pty/windows/ConPtyFactory.java | 6 +- .../ghidra}/pty/windows/ConPtyParent.java | 10 +- .../main/java/ghidra}/pty/windows/Handle.java | 2 +- .../pty/windows/HandleInputStream.java | 2 +- .../pty/windows/HandleOutputStream.java | 2 +- .../main/java/ghidra}/pty/windows/Pipe.java | 2 +- .../pty/windows/PseudoConsoleHandle.java | 4 +- .../pty/windows/jna/ConsoleApiNative.java | 2 +- .../java/ghidra}/pty/AbstractPtyTest.java | 2 +- .../java/ghidra}/pty/linux/LinuxPtyTest.java | 11 +- .../test/java/ghidra}/pty/ssh/SshPtyTest.java | 6 +- .../java/ghidra}/pty/windows/ConPtyTest.java | 4 +- .../ghidra}/pty/windows/NamedPipeTest.java | 2 +- .../core/terminal/TerminalProviderTest.java | 381 ++++ 98 files changed, 7972 insertions(+), 141 deletions(-) create mode 100644 Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/DefaultTerminal.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalAwtEventEncoder.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalClipboardProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalFinder.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayout.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalListener.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPlugin.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextField.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/ThreadedTerminal.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/AnsiColorResolver.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtAttributes.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtOutput.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/Terminal.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/TerminalService.java create mode 100644 Ghidra/Framework/Pty/Module.manifest create mode 100644 Ghidra/Framework/Pty/build.gradle create mode 100644 Ghidra/Framework/Pty/certification.manifest rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/Pty.java (99%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/PtyChild.java (99%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/PtyEndpoint.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/PtyFactory.java (89%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/PtyParent.java (91%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/PtySession.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/Err.java (96%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/FdInputStream.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/FdOutputStream.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/LinuxPty.java (97%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/LinuxPtyChild.java (96%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/LinuxPtyEndpoint.java (94%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/LinuxPtyFactory.java (90%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/LinuxPtyParent.java (57%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/LinuxPtySessionLeader.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/PosixC.java (83%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/linux/Util.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/local/LocalProcessPtySession.java (94%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/local/LocalWindowsNativeProcessPtySession.java (96%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/macos/MacosPtyFactory.java (86%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/ssh/GhidraSshPtyFactory.java (99%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/ssh/SshPty.java (77%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/ssh/SshPtyChild.java (95%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/ssh/SshPtyEndpoint.java (80%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/ssh/SshPtyParent.java (68%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/ssh/SshPtySession.java (94%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/AnsiBufferedInputStream.java (99%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/ConPty.java (94%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/ConPtyChild.java (94%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/ConPtyEndpoint.java (94%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/ConPtyFactory.java (90%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/ConPtyParent.java (78%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/Handle.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/HandleInputStream.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/HandleOutputStream.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/Pipe.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/PseudoConsoleHandle.java (92%) rename Ghidra/{Debug/Debugger-agent-gdb/src/main/java/agent/gdb => Framework/Pty/src/main/java/ghidra}/pty/windows/jna/ConsoleApiNative.java (99%) rename Ghidra/{Debug/Debugger-agent-gdb/src/test/java/agent/gdb => Framework/Pty/src/test/java/ghidra}/pty/AbstractPtyTest.java (98%) rename Ghidra/{Debug/Debugger-agent-gdb/src/test/java/agent/gdb => Framework/Pty/src/test/java/ghidra}/pty/linux/LinuxPtyTest.java (96%) rename Ghidra/{Debug/Debugger-agent-gdb/src/test/java/agent/gdb => Framework/Pty/src/test/java/ghidra}/pty/ssh/SshPtyTest.java (96%) rename Ghidra/{Debug/Debugger-agent-gdb/src/test/java/agent/gdb => Framework/Pty/src/test/java/ghidra}/pty/windows/ConPtyTest.java (99%) rename Ghidra/{Debug/Debugger-agent-gdb/src/test/java/agent/gdb => Framework/Pty/src/test/java/ghidra}/pty/windows/NamedPipeTest.java (98%) create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java diff --git a/Ghidra/Debug/Debugger-agent-gdb/build.gradle b/Ghidra/Debug/Debugger-agent-gdb/build.gradle index 91a01307c2..b0a44b6fbc 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/build.gradle +++ b/Ghidra/Debug/Debugger-agent-gdb/build.gradle @@ -29,7 +29,7 @@ dependencies { api project(':Framework-AsyncComm') api project(':Framework-Debugging') api project(':Debugger-gadp') - api 'com.jcraft:jsch:0.1.55' + api project(':Pty') testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts') diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java index 16a41d0c33..ad5fbd81e0 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbInJvmDebuggerModelFactory.java @@ -20,12 +20,12 @@ import java.util.concurrent.CompletableFuture; import agent.gdb.manager.GdbManager; import agent.gdb.model.impl.GdbModelImpl; -import agent.gdb.pty.PtyFactory; import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.util.ConfigurableFactory.FactoryDescription; import ghidra.dbg.util.ShellUtils; import ghidra.program.model.listing.Program; +import ghidra.pty.PtyFactory; @FactoryDescription( brief = "gdb", diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java index cec59fdc5d..1abea7487f 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java @@ -19,12 +19,12 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import agent.gdb.model.impl.GdbModelImpl; -import agent.gdb.pty.ssh.GhidraSshPtyFactory; import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.DebuggerObjectModel; import ghidra.dbg.util.ShellUtils; import ghidra.dbg.util.ConfigurableFactory.FactoryDescription; import ghidra.program.model.listing.Program; +import ghidra.pty.ssh.GhidraSshPtyFactory; @FactoryDescription( brief = "gdb via SSH", diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java index 595132d0bd..ba6ae22066 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/gadp/impl/GdbGadpServerImpl.java @@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture; import agent.gdb.gadp.GdbGadpServer; import agent.gdb.model.impl.GdbModelImpl; -import agent.gdb.pty.PtyFactory; import ghidra.dbg.gadp.server.AbstractGadpServer; +import ghidra.pty.PtyFactory; public class GdbGadpServerImpl implements GdbGadpServer { public class GadpSide extends AbstractGadpServer { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java index e6d653e858..21591c5c7c 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java @@ -24,8 +24,8 @@ import java.util.concurrent.ExecutionException; import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointInsertions; import agent.gdb.manager.impl.GdbManagerImpl; -import agent.gdb.pty.PtyFactory; -import agent.gdb.pty.linux.LinuxPty; +import ghidra.pty.PtyFactory; +import ghidra.pty.linux.LinuxPty; /** * The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java index 33299a9370..589454e0b5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/impl/GdbManagerImpl.java @@ -40,9 +40,6 @@ import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning; import agent.gdb.manager.parsing.GdbMiParser; import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList; import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError; -import agent.gdb.pty.*; -import agent.gdb.pty.PtyChild.Echo; -import agent.gdb.pty.windows.AnsiBufferedInputStream; import ghidra.GhidraApplicationLayout; import ghidra.async.*; import ghidra.async.AsyncLock.Hold; @@ -51,6 +48,9 @@ import ghidra.dbg.util.HandlerMap; import ghidra.dbg.util.PrefixMap; import ghidra.framework.OperatingSystem; import ghidra.lifecycle.Internal; +import ghidra.pty.*; +import ghidra.pty.PtyChild.Echo; +import ghidra.pty.windows.AnsiBufferedInputStream; import ghidra.util.Msg; import ghidra.util.SystemUtilities; import ghidra.util.datastruct.ListenerSet; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java index be288a2bdf..ff7ac03a73 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/model/impl/GdbModelImpl.java @@ -24,7 +24,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import agent.gdb.manager.*; import agent.gdb.manager.impl.cmd.GdbCommandError; -import agent.gdb.pty.PtyFactory; import ghidra.async.AsyncUtils; import ghidra.dbg.DebuggerModelClosedReason; import ghidra.dbg.agent.AbstractDebuggerObjectModel; @@ -34,6 +33,7 @@ import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.schema.AnnotatedSchemaContext; import ghidra.dbg.target.schema.TargetObjectSchema; import ghidra.program.model.address.*; +import ghidra.pty.PtyFactory; public class GdbModelImpl extends AbstractDebuggerObjectModel { // TODO: Need some minimal memory modeling per architecture on the model/agent side. diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java index 5f05295c77..0ac769b950 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/AbstractGdbManagerTest.java @@ -35,12 +35,12 @@ import org.junit.*; import agent.gdb.manager.*; import agent.gdb.manager.GdbManager.StepCmd; import agent.gdb.manager.breakpoint.GdbBreakpointInfo; -import agent.gdb.pty.PtyFactory; -import agent.gdb.pty.linux.LinuxPtyFactory; import generic.ULongSpan; import generic.ULongSpan.ULongSpanSet; import ghidra.async.AsyncReference; import ghidra.dbg.testutil.DummyProc; +import ghidra.pty.PtyFactory; +import ghidra.pty.linux.LinuxPtyFactory; import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.util.Msg; import ghidra.util.SystemUtilities; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java index afe8b05f27..0a3370a972 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java @@ -22,8 +22,8 @@ import java.util.concurrent.CompletableFuture; import org.junit.Ignore; import agent.gdb.manager.GdbManager; -import agent.gdb.pty.PtySession; -import agent.gdb.pty.linux.LinuxPty; +import ghidra.pty.PtySession; +import ghidra.pty.linux.LinuxPty; import ghidra.util.Msg; @Ignore("Need compatible GDB version for CI") diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedWindowsMi2GdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedWindowsMi2GdbManagerTest.java index ee34503067..6ebdff9919 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedWindowsMi2GdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/SpawnedWindowsMi2GdbManagerTest.java @@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture; import org.junit.Ignore; import agent.gdb.manager.GdbManager; -import agent.gdb.pty.PtyFactory; -import agent.gdb.pty.windows.ConPtyFactory; +import ghidra.pty.PtyFactory; +import ghidra.pty.windows.ConPtyFactory; @Ignore("Need compatible version on CI") public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java index 3091359e86..238fdf1fc6 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshGdbModelHost.java @@ -18,9 +18,9 @@ package agent.gdb.model.ssh; import java.util.Map; import agent.gdb.GdbOverSshDebuggerModelFactory; -import agent.gdb.pty.ssh.SshPtyTest; import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.test.AbstractModelHost; +import ghidra.pty.ssh.SshPtyTest; import ghidra.util.exception.CancelledException; public class SshGdbModelHost extends AbstractModelHost { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java index 4b6080ab97..f10cd6e1f1 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java @@ -18,9 +18,9 @@ package agent.gdb.model.ssh; import java.util.Map; import agent.gdb.GdbOverSshDebuggerModelFactory; -import agent.gdb.pty.ssh.SshPtyTest; import ghidra.dbg.DebuggerModelFactory; import ghidra.dbg.test.AbstractModelHost; +import ghidra.pty.ssh.SshPtyTest; import ghidra.util.exception.CancelledException; public class SshJoinGdbModelHost extends AbstractModelHost { diff --git a/Ghidra/Debug/Debugger-rmi-trace/build.gradle b/Ghidra/Debug/Debugger-rmi-trace/build.gradle index eec00786ef..960b5ce04c 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/build.gradle +++ b/Ghidra/Debug/Debugger-rmi-trace/build.gradle @@ -25,6 +25,7 @@ apply plugin: 'eclipse' eclipse.project.name = 'Debug Debugger-rmi-trace' dependencies { + api project(':Pty') api project(':Debugger') } diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java new file mode 100644 index 0000000000..41c1d37cc2 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java @@ -0,0 +1,39 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import ghidra.framework.plugintool.util.PluginException; +import ghidra.pty.Pty; +import ghidra.pty.PtySession; + +public class RunBashInTerminalScript extends TerminalGhidraScript { + @Override + protected void runSession(Pty pty) throws IOException, PluginException { + Map env = new HashMap<>(System.getenv()); + env.put("TERM", "xterm-256color"); + PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); + displayInTerminal(pty.getParent(), () -> { + try { + session.waitExited(); + } + catch (InterruptedException e) { + return; + } + }); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java new file mode 100644 index 0000000000..c2a1e9193d --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java @@ -0,0 +1,77 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import ghidra.app.plugin.core.terminal.TerminalListener; +import ghidra.app.plugin.core.terminal.TerminalPlugin; +import ghidra.app.script.GhidraScript; +import ghidra.app.services.Terminal; +import ghidra.app.services.TerminalService; +import ghidra.framework.plugintool.util.PluginException; +import ghidra.pty.*; + +public class TerminalGhidraScript extends GhidraScript { + + protected TerminalService ensureTerminalService() throws PluginException { + TerminalService termServ = state.getTool().getService(TerminalService.class); + if (termServ != null) { + return termServ; + } + state.getTool().addPlugin(TerminalPlugin.class.getName()); + return state.getTool().getService(TerminalService.class); + } + + protected void displayInTerminal(PtyParent parent, Runnable waiter) throws PluginException { + TerminalService terminalService = ensureTerminalService(); + try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"), + parent.getInputStream(), parent.getOutputStream())) { + term.addTerminalListener(new TerminalListener() { + @Override + public void resized(int cols, int rows) { + parent.setWindowSize(cols, rows); + } + }); + waiter.run(); + } + } + + protected void runSession(Pty pty) throws IOException, PluginException { + Map env = new HashMap<>(System.getenv()); + env.put("TERM", "xterm-256color"); + pty.getChild().nullSession(); + displayInTerminal(pty.getParent(), () -> { + while (true) { + try { + Thread.sleep(100000); + } + catch (InterruptedException e) { + return; + } + } + }); + } + + @Override + protected void run() throws Exception { + PtyFactory factory = PtyFactory.local(); + try (Pty pty = factory.openpty()) { + runSession(pty); + } + } +} diff --git a/Ghidra/Debug/Framework-Debugging/build.gradle b/Ghidra/Debug/Framework-Debugging/build.gradle index 43122e351a..39efb34953 100644 --- a/Ghidra/Debug/Framework-Debugging/build.gradle +++ b/Ghidra/Debug/Framework-Debugging/build.gradle @@ -27,7 +27,7 @@ dependencies { api project(':Generic') api project(':SoftwareModeling') api project(':ProposedUtils') - + api "net.java.dev.jna:jna:5.4.0" api "net.java.dev.jna:jna-platform:5.4.0" diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties index 9223843e8c..d9d338bcf3 100644 --- a/Ghidra/Features/Base/data/base.theme.properties +++ b/Ghidra/Features/Base/data/base.theme.properties @@ -106,6 +106,29 @@ color.fg.plugin.interpreter.renderer.color.intense.6 = color.palette.magenta color.fg.plugin.interpreter.renderer.color.intense.7 = color.palette.cyan color.fg.plugin.interpreter.renderer.color.intense.8 = color.palette.white +// Taken from Terminal.app as documented on Wikipedia +color.bg.plugin.terminal = rgb(0,0,0) +color.fg.plugin.terminal = rgb(203,204,205) +color.cursor.focused.terminal = color.cursor.focused +color.cursor.unfocused.terminal = color.cursor.unfocused +color.fg.plugin.terminal.normal.black = rgb(0,0,0) +color.fg.plugin.terminal.normal.red = rgb(194,54,33) +color.fg.plugin.terminal.normal.green = rgb(37,188,36) +color.fg.plugin.terminal.normal.yellow = rgb(173,173,39) +color.fg.plugin.terminal.normal.blue = rgb(73,46,255) +color.fg.plugin.terminal.normal.magenta = rgb(211,56,211) +color.fg.plugin.terminal.normal.cyan = rgb(51,187,200) +color.fg.plugin.terminal.normal.white = rgb(203,204,205) +color.fg.plugin.terminal.bright.black = rgb(129,131,131) +color.fg.plugin.terminal.bright.red = rgb(252,57,31) +color.fg.plugin.terminal.bright.green = rgb(49,231,34) +color.fg.plugin.terminal.bright.yellow = rgb(234,236,35) +color.fg.plugin.terminal.bright.blue = rgb(88,51,255) +color.fg.plugin.terminal.bright.magenta = rgb(249,53,248) +color.fg.plugin.terminal.bright.cyan = rgb(20,240,240) +color.fg.plugin.terminal.bright.white = rgb(233,235,235) + + color.bg.plugin.locationreferences.highlight = color.palette.lightcornflowerblue color.bg.plugin.myprogramchangesdisplay.markers.changes.unsaved = color.palette.darkgray @@ -155,7 +178,7 @@ font.plugin.tabs = SansSerif-PLAIN-11 font.plugin.tabs.list = SansSerif-BOLD-9 font.plugin.tips = Dialog-PLAIN-12 font.plugin.tips.label = font.plugin.tips[BOLD] - +font.plugin.terminal = font.monospaced diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java index 8f37c8a0b0..6ea28c2157 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/ClipboardPlugin.java @@ -173,7 +173,8 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl } /** - * @see java.awt.datatransfer.ClipboardOwner#lostOwnership(java.awt.datatransfer.Clipboard, java.awt.datatransfer.Transferable) + * @see java.awt.datatransfer.ClipboardOwner#lostOwnership(java.awt.datatransfer.Clipboard, + * java.awt.datatransfer.Transferable) */ @Override public void lostOwnership(Clipboard clipboard, Transferable contents) { @@ -353,11 +354,19 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl // maker interface } + private String getActionOwner(ClipboardContentProviderService clipboardService) { + String owner = clipboardService.getClipboardActionOwner(); + if (owner != null) { + return owner; + } + return getName(); + } + private class CopyAction extends DockingAction implements ICopy { private final ClipboardContentProviderService clipboardService; private CopyAction(ClipboardContentProviderService clipboardService) { - super("Copy", ClipboardPlugin.this.getName()); + super("Copy", getActionOwner(clipboardService)); this.clipboardService = clipboardService; setPopupMenuData(new MenuData(new String[] { "Copy" }, "Clipboard")); @@ -365,6 +374,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl "Clipboard")); setKeyBindingData(new KeyBindingData(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK)); setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy")); + clipboardService.customizeClipboardAction(this); } @Override @@ -390,7 +400,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl private final ClipboardContentProviderService clipboardService; private PasteAction(ClipboardContentProviderService clipboardService) { - super("Paste", ClipboardPlugin.this.getName()); + super("Paste", ClipboardPlugin.this.getActionOwner(clipboardService)); this.clipboardService = clipboardService; setPopupMenuData(new MenuData(new String[] { "Paste" }, "Clipboard")); @@ -398,6 +408,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl new ToolBarData(new GIcon("icon.plugin.clipboard.paste"), "Clipboard")); setKeyBindingData(new KeyBindingData(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK)); setHelpLocation(new HelpLocation("ClipboardPlugin", "Paste")); + clipboardService.customizeClipboardAction(this); } @Override @@ -426,12 +437,13 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl private final ClipboardContentProviderService clipboardService; private CopySpecialAction(ClipboardContentProviderService clipboardService) { - super("Copy Special", ClipboardPlugin.this.getName()); + super("Copy Special", ClipboardPlugin.this.getActionOwner(clipboardService)); this.clipboardService = clipboardService; setPopupMenuData(new MenuData(new String[] { "Copy Special..." }, "Clipboard")); setEnabled(false); setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special")); + clipboardService.customizeClipboardAction(this); } @Override @@ -457,12 +469,13 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl private final ClipboardContentProviderService clipboardService; private CopySpecialAgainAction(ClipboardContentProviderService clipboardService) { - super("Copy Special Again", ClipboardPlugin.this.getName()); + super("Copy Special Again", ClipboardPlugin.this.getActionOwner(clipboardService)); this.clipboardService = clipboardService; setPopupMenuData(new MenuData(new String[] { "Copy Special Again" }, "Clipboard")); setEnabled(false); setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special")); + clipboardService.customizeClipboardAction(this); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/AnsiRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/AnsiRenderer.java index a10b54c0c1..242ba544be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/AnsiRenderer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/interpreter/AnsiRenderer.java @@ -36,9 +36,6 @@ import ghidra.util.ColorUtils; public class AnsiRenderer { /** - * These colors are taken from Terminal.app as documented on Wikipedia as of 26 April 2022. - * - *

* See ANSI escape * code on Wikipedia. They appear here in ANSI order. */ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/DefaultTerminal.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/DefaultTerminal.java new file mode 100644 index 0000000000..441d212611 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/DefaultTerminal.java @@ -0,0 +1,67 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.nio.ByteBuffer; + +import ghidra.app.plugin.core.terminal.vt.VtOutput; +import ghidra.app.services.Terminal; +import ghidra.util.Swing; + +/** + * A terminal that does nothing on its own. + * + *

+ * Everything displayed happens via {@link #injectDisplayOutput(ByteBuffer)}, and everything typed + * into it is emitted via the {@link VtOutput}, which was given at construction. + */ +public class DefaultTerminal implements Terminal { + protected final TerminalProvider provider; + + public DefaultTerminal(TerminalProvider provider) { + this.provider = provider; + } + + @Override + public void close() { + Swing.runIfSwingOrRunLater(() -> provider.removeFromTool()); + } + + @Override + public void addTerminalListener(TerminalListener listener) { + provider.addTerminalListener(listener); + } + + @Override + public void removeTerminalListener(TerminalListener listener) { + provider.removeTerminalListener(listener); + } + + @Override + public void injectDisplayOutput(ByteBuffer bb) { + provider.processInput(bb); + } + + @Override + public void setFixedSize(int rows, int cols) { + provider.setFixedSize(rows, cols); + } + + @Override + public void setDynamicSize() { + provider.setDyanmicSize(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalAwtEventEncoder.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalAwtEventEncoder.java new file mode 100644 index 0000000000..ff6da13ccc --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalAwtEventEncoder.java @@ -0,0 +1,314 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.event.*; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; + +import ghidra.util.Msg; + +/** + * An encoder which can translate AWT/Swing events into ANSI input codes. + * + *

+ * The input system is not as well decoupled from Swing as the output system. For ease of use, the + * methods are named the same as their corresponding Swing event listener methods, though they may + * require additional arguments. These in turn invoke the {@link #generateBytes(ByteBuffer)} method, + * which the implementor must send to the appropriate recipient, usually a pty. + */ +public abstract class TerminalAwtEventEncoder { + public static final byte[] CODE_NONE = {}; + + public static byte[] vtseq(int number) { + try { + return ("\033[" + number + "~").getBytes("ASCII"); + } + catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + public static final byte ESC = (byte) 0x1b; + public static final byte FUNC = (byte) 0x4f; + + public static final byte[] CODE_INSERT = vtseq(2); + public static final byte[] CODE_DELETE = vtseq(3); + public static final byte[] CODE_HOME = { ESC, '[', 'H' }; + public static final byte[] CODE_END = { ESC, '[', 'F' }; + public static final byte[] CODE_PAGE_UP = vtseq(5); + public static final byte[] CODE_PAGE_DOWN = vtseq(6); + public static final byte[] CODE_NUMPAD5 = { ESC, '[', 'E' }; + + public static final byte[] CODE_UP = { ESC, FUNC, 'A' }; + public static final byte[] CODE_DOWN = { ESC, FUNC, 'B' }; + public static final byte[] CODE_RIGHT = { ESC, FUNC, 'C' }; + public static final byte[] CODE_LEFT = { ESC, FUNC, 'D' }; + public static final byte[] CODE_F1 = { ESC, FUNC, 'P' }; + public static final byte[] CODE_F2 = { ESC, FUNC, 'Q' }; + public static final byte[] CODE_F3 = { ESC, FUNC, 'R' }; + public static final byte[] CODE_F4 = { ESC, FUNC, 'S' }; + public static final byte[] CODE_F5 = vtseq(15); + public static final byte[] CODE_F6 = vtseq(17); + public static final byte[] CODE_F7 = vtseq(18); + public static final byte[] CODE_F8 = vtseq(19); + public static final byte[] CODE_F9 = vtseq(20); + public static final byte[] CODE_F10 = vtseq(21); + public static final byte[] CODE_F11 = vtseq(23); + public static final byte[] CODE_F12 = vtseq(24); + public static final byte[] CODE_F13 = vtseq(25); + public static final byte[] CODE_F14 = vtseq(26); + public static final byte[] CODE_F15 = vtseq(28); + public static final byte[] CODE_F16 = vtseq(29); + public static final byte[] CODE_F17 = vtseq(31); + public static final byte[] CODE_F18 = vtseq(32); + public static final byte[] CODE_F19 = vtseq(33); + public static final byte[] CODE_F20 = vtseq(34); + + public static final byte[] CODE_FOCUS_GAINED = { ESC, '[', 'I' }; + public static final byte[] CODE_FOCUS_LOST = { ESC, '[', 'O' }; + + protected final Charset charset; + protected final CharsetEncoder encoder; + + protected final ByteBuffer bb = ByteBuffer.allocate(16); + protected final CharBuffer cb = CharBuffer.allocate(16); + + public TerminalAwtEventEncoder(String charsetName) { + this(Charset.forName(charsetName)); + } + + public TerminalAwtEventEncoder(Charset charset) { + this.charset = charset; + this.encoder = charset.newEncoder(); + } + + protected abstract void generateBytes(ByteBuffer buf); + + protected byte[] getModifiedAnsiKeyCode(KeyEvent e) { + int modifier = 1; + if (e.isShiftDown()) { + modifier += 1; + } + if (e.isAltDown()) { + modifier += 2; + } + if (e.isControlDown()) { + modifier += 4; + } + if (e.isMetaDown()) { + modifier += 8; + } + int code = switch (e.getKeyCode()) { + case KeyEvent.VK_HOME -> 1; + case KeyEvent.VK_INSERT -> 2; + case KeyEvent.VK_DELETE -> 3; // TODO: Already handled? + case KeyEvent.VK_END -> 4; + case KeyEvent.VK_PAGE_UP -> 5; + case KeyEvent.VK_PAGE_DOWN -> 6; + case KeyEvent.VK_F1 -> 11; + case KeyEvent.VK_F2 -> 12; + case KeyEvent.VK_F3 -> 13; + case KeyEvent.VK_F4 -> 14; + case KeyEvent.VK_F5 -> 15; + case KeyEvent.VK_F6 -> 17; + case KeyEvent.VK_F7 -> 18; + case KeyEvent.VK_F8 -> 19; + case KeyEvent.VK_F9 -> 20; + case KeyEvent.VK_F10 -> 21; + case KeyEvent.VK_F11 -> 23; + case KeyEvent.VK_F12 -> 24; + case KeyEvent.VK_F13 -> 25; + case KeyEvent.VK_F14 -> 26; + case KeyEvent.VK_F15 -> 28; + case KeyEvent.VK_F16 -> 29; + case KeyEvent.VK_F17 -> 31; + case KeyEvent.VK_F18 -> 32; + case KeyEvent.VK_F19 -> 33; + case KeyEvent.VK_F20 -> 34; + default -> -1; + }; + if (code == -1) { + return CODE_NONE; + } + try { + // TODO: This doesn't seem to work right, but I'm lost trying to fix it. + return "\033[%d;%d~".formatted(code, modifier).getBytes("ASCII"); + } + catch (UnsupportedEncodingException ex) { + throw new AssertionError(ex); + } + } + + protected byte[] getAnsiKeyCode(KeyEvent e) { + if (e.getModifiersEx() != 0) { + return getModifiedAnsiKeyCode(e); + } + return switch (e.getKeyCode()) { + case KeyEvent.VK_INSERT -> CODE_INSERT; + // NB. CODE_DELETE is handled in keyTyped + case KeyEvent.VK_HOME -> CODE_HOME; + case KeyEvent.VK_END -> CODE_END; + case KeyEvent.VK_PAGE_UP -> CODE_PAGE_UP; + case KeyEvent.VK_PAGE_DOWN -> CODE_PAGE_DOWN; + case KeyEvent.VK_NUMPAD5 -> CODE_NUMPAD5; + case KeyEvent.VK_UP -> CODE_UP; + case KeyEvent.VK_DOWN -> CODE_DOWN; + case KeyEvent.VK_RIGHT -> CODE_RIGHT; + case KeyEvent.VK_LEFT -> CODE_LEFT; + case KeyEvent.VK_F1 -> CODE_F1; + case KeyEvent.VK_F2 -> CODE_F2; + case KeyEvent.VK_F3 -> CODE_F3; + case KeyEvent.VK_F4 -> CODE_F4; + case KeyEvent.VK_F5 -> CODE_F5; + case KeyEvent.VK_F6 -> CODE_F6; + case KeyEvent.VK_F7 -> CODE_F7; + case KeyEvent.VK_F8 -> CODE_F8; + case KeyEvent.VK_F9 -> CODE_F9; + case KeyEvent.VK_F10 -> CODE_F10; + case KeyEvent.VK_F11 -> CODE_F11; + case KeyEvent.VK_F12 -> CODE_F12; + case KeyEvent.VK_F13 -> CODE_F13; + case KeyEvent.VK_F14 -> CODE_F14; + case KeyEvent.VK_F15 -> CODE_F15; + case KeyEvent.VK_F16 -> CODE_F16; + case KeyEvent.VK_F17 -> CODE_F17; + case KeyEvent.VK_F18 -> CODE_F18; + case KeyEvent.VK_F19 -> CODE_F19; + case KeyEvent.VK_F20 -> CODE_F20; + // F21-F24 are not given on Wikipedia... + default -> CODE_NONE; + }; + } + + public void keyPressed(KeyEvent e) { + byte[] bytes = getAnsiKeyCode(e); + bb.put(bytes); + generateBytesExc(); + } + + public void keyTyped(KeyEvent e) { + sendChar(e.getKeyChar()); + } + + public void mousePressed(MouseEvent e, int row, int col) { + mouseEvent(e, row, col, true); + } + + public void mouseReleased(MouseEvent e, int row, int col) { + mouseEvent(e, row, col, false); + } + + protected int translateModifiers(InputEvent e) { + int mods = 0; + if (e.isShiftDown()) { + mods += 4; + } + if (e.isMetaDown()) { + mods += 8; + } + if (e.isControlDown()) { + mods += 16; + } + return mods; + } + + protected void sendMouseEvent(int buttonsAndModifiers, int row, int col) { + cb.clear(); + cb.put("\033[M"); + cb.put((char) (' ' + buttonsAndModifiers)); + cb.put((char) (' ' + col)); + cb.put((char) (' ' + row)); + sendCharBuffer(); + } + + protected void mouseEvent(MouseEvent e, int row, int col, boolean isPress) { + int buttonsAndModifiers = isPress ? switch (e.getButton()) { + case MouseEvent.BUTTON1 -> 0; + case MouseEvent.BUTTON2 -> 1; + case MouseEvent.BUTTON3 -> 2; + default -> throw new AssertionError(); + } : 3; + buttonsAndModifiers += translateModifiers(e); + sendMouseEvent(buttonsAndModifiers, row, col); + } + + public void mouseWheelMoved(MouseWheelEvent e, int row, int col) { + int buttonsAndModifiers = (e.getWheelRotation() < 0 ? 0 : 1) + 64; + buttonsAndModifiers += translateModifiers(e); + sendMouseEvent(buttonsAndModifiers, row, col); + } + + public void focusGained() { + bb.put(CODE_FOCUS_GAINED); + generateBytesExc(); + } + + public void focusLost() { + bb.put(CODE_FOCUS_LOST); + generateBytesExc(); + } + + protected void sendCharBuffer() { + cb.flip(); + CoderResult result = encoder.encode(cb, bb, true); + cb.compact(); + if (result.isError()) { + Msg.error(this, "Error while encoding"); + encoder.reset(); + cb.clear(); + } + generateBytesExc(); + } + + public void sendChar(char c) { + switch (c) { + case 0x7f: + bb.put(CODE_DELETE); + generateBytesExc(); + break; + default: + /** + * If I ever care to support Unicode, I may need to worry about surrogate pairs. + */ + cb.clear(); + cb.put(c); + sendCharBuffer(); + break; + } + } + + protected void generateBytesExc() { + bb.flip(); + try { + generateBytes(bb); + } + catch (Throwable t) { + Msg.error(this, "Error generating bytes: " + t, t); + } + finally { + bb.clear(); + } + } + + public void sendText(CharSequence text) { + for (int i = 0; i < text.length(); i++) { + sendChar(text.charAt(i)); + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalClipboardProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalClipboardProvider.java new file mode 100644 index 0000000000..aec5de7873 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalClipboardProvider.java @@ -0,0 +1,193 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.datatransfer.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import org.apache.commons.lang3.ArrayUtils; + +import docking.ActionContext; +import docking.ComponentProvider; +import docking.action.DockingAction; +import docking.action.KeyBindingData; +import docking.dnd.StringTransferable; +import docking.widgets.fieldpanel.support.FieldSelection; +import ghidra.app.services.ClipboardContentProviderService; +import ghidra.app.util.ClipboardType; +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +/** + * The clipboard provider for the terminal plugin. + * + *

+ * In addition to providing clipboard contents and paste functionality, this customizes the Copy and + * Paste actions. We change the "owner" to be this plugin, so that the action can be configured + * independently of the standard Copy and Paste actions. Then, we re-bind the keys to Ctrl+Shift+C + * and Shift+Shift+V, respectively. This ensures that Ctrl+C will still send an Interrupt (char 3). + * This is the convention followed by just about every XTerm clone. + */ +public class TerminalClipboardProvider implements ClipboardContentProviderService { + protected static final ClipboardType TEXT_TYPE = + new ClipboardType(DataFlavor.stringFlavor, "Text"); + protected static final List COPY_TYPES = List.of(TEXT_TYPE); + + protected final TerminalProvider provider; + protected FieldSelection selection; + + protected final Set listeners = new CopyOnWriteArraySet<>(); + + public TerminalClipboardProvider(TerminalProvider provider) { + this.provider = provider; + } + + @Override + public ComponentProvider getComponentProvider() { + return provider; + } + + @Override + public Transferable copy(TaskMonitor monitor) { + if (selection == null || selection.getNumRanges() != 1) { + return null; + } + String text = provider.panel.getSelectedText(selection.getFieldRange(0)); + if (text == null) { + return null; + } + return new StringTransferable(text); + } + + @Override + public Transferable copySpecial(ClipboardType copyType, TaskMonitor monitor) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean paste(Transferable pasteData) { + try { + String text = (String) pasteData.getTransferData(DataFlavor.stringFlavor); + provider.panel.paste(text); + return true; + } + catch (UnsupportedFlavorException | IOException e) { + return false; + } + } + + @Override + public List getCurrentCopyTypes() { + if (selection == null || selection.getNumRanges() == 0) { + return List.of(); + } + return COPY_TYPES; + } + + @Override + public boolean isValidContext(ActionContext context) { + return context.getComponentProvider() == provider; + } + + @Override + public boolean enableCopy() { + return true; + } + + @Override + public boolean enableCopySpecial() { + return false; + } + + @Override + public boolean enablePaste() { + return true; + } + + @Override + public void lostOwnership(Transferable transferable) { + // Nothing to do + } + + @Override + public void addChangeListener(ChangeListener listener) { + listeners.add(listener); + } + + @Override + public void removeChangeListener(ChangeListener listener) { + listeners.remove(listener); + } + + @Override + public boolean canPaste(DataFlavor[] availableFlavors) { + return -1 != ArrayUtils.indexOf(availableFlavors, DataFlavor.stringFlavor); + } + + @Override + public boolean canCopy() { + return selection != null && selection.getNumRanges() == 1; + } + + @Override + public boolean canCopySpecial() { + return false; + } + + private void notifyStateChanged() { + ChangeEvent event = new ChangeEvent(this); + for (ChangeListener listener : listeners) { + try { + listener.stateChanged(event); + } + catch (Throwable t) { + Msg.showError(this, null, "Error", t.getMessage(), t); + } + } + } + + public void selectionChanged(FieldSelection selection) { + this.selection = selection; + notifyStateChanged(); + } + + @Override + public String getClipboardActionOwner() { + return provider.plugin.getName(); + } + + @Override + public void customizeClipboardAction(DockingAction action) { + switch (action.getName()) { + case "Copy": + action.setKeyBindingData(new KeyBindingData(KeyEvent.VK_C, + InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + break; + case "Paste": + action.setKeyBindingData(new KeyBindingData(KeyEvent.VK_V, + InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + break; + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalFinder.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalFinder.java new file mode 100644 index 0000000000..e3cc65d7b2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalFinder.java @@ -0,0 +1,250 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.math.BigInteger; +import java.util.EnumSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import docking.widgets.fieldpanel.support.FieldLocation; +import docking.widgets.fieldpanel.support.FieldRange; +import ghidra.app.plugin.core.terminal.TerminalPanel.FindOptions; +import ghidra.app.plugin.core.terminal.vt.VtLine; + +/** + * The algorithm for finding text in the terminal buffer. + * + *

+ * This is an abstract class, so that text search and regex search are better separated, while the + * common parts need not be duplicated. + */ +public abstract class TerminalFinder { + protected final TerminalLayoutModel model; + protected final FieldLocation cur; + protected final boolean forward; + + protected final boolean caseSensitive; + protected final boolean wrap; + protected final boolean wholeWord; + + protected final StringBuilder sb = new StringBuilder(); + + /** + * Create a finder on the given model + * + * @see TerminalPanel#find(String, Set, FieldLocation, boolean) + * @param model the model + * @param cur the start of the current selection, or null + * @param forward true for forward, false for backward + * @param options a set of options, preferably an {@link EnumSet} + */ + protected TerminalFinder(TerminalLayoutModel model, FieldLocation cur, boolean forward, + Set options) { + this.model = model; + if (cur != null) { + this.cur = cur; + } + else if (forward) { + this.cur = new FieldLocation(); + } + else { + BigInteger maxIndex = model.getNumIndexes().subtract(BigInteger.ONE); + int maxChar = model.getLayout(maxIndex).line.length(); + this.cur = new FieldLocation(maxIndex, 0, 0, maxChar); + } + this.forward = forward; + this.caseSensitive = options.contains(FindOptions.CASE_SENSITIVE); + this.wrap = options.contains(FindOptions.WRAP); + this.wholeWord = options.contains(FindOptions.WHOLE_WORD); + } + + protected void lowerBuf(StringBuilder sb) { + for (int i = 0; i < sb.length(); i++) { + sb.setCharAt(i, Character.toLowerCase(sb.charAt(i))); + } + } + + protected boolean isWholeWord(int i, String match) { + if (i > 0 && VtLine.isWordChar(sb.charAt(i - 1))) { + return false; + } + int iAfter = i + match.length(); + if (iAfter < sb.length() && VtLine.isWordChar(sb.charAt(iAfter))) { + return false; + } + return true; + } + + protected abstract FieldRange findInLine(int start, BigInteger index); + + protected boolean continueIndex(BigInteger index, BigInteger end) { + if (forward) { + return index.compareTo(end) <= 0; + } + return index.compareTo(end) >= 0; + } + + /** + * Search within the layouts in the given range of indices, inclusive + * + * @param start the first index + * @param end the last index, inclusive + * @param step the step (1 or -1) + * @return the field range, if found, or null + */ + protected FieldRange findInIndices(BigInteger start, BigInteger end, BigInteger step) { + for (BigInteger index = start; continueIndex(index, end); index = index.add(step)) { + TerminalLayout layout = model.getLayout(index); + VtLine line = layout.line; + sb.delete(0, sb.length()); + line.gatherText(sb, 0, line.length()); + if (!caseSensitive) { + lowerBuf(sb); + } + int s; + if (index.equals(cur.getIndex())) { + s = cur.getCol(); + } + else { + s = forward ? 0 : line.length() - 1; + } + FieldRange found = findInLine(s, index); + if (found != null) { + return found; + } + } + return null; + } + + /** + * Execute the search + * + * @return the range covering the found term, or null if not found + */ + public FieldRange find() { + BigInteger step = forward ? BigInteger.ONE : BigInteger.ONE.negate(); + BigInteger maxIndex = model.getNumIndexes().subtract(BigInteger.ONE); + FieldRange found = findInIndices(cur.getIndex(), + forward ? maxIndex : BigInteger.ZERO, step); + if (found != null) { + return found; + } + if (!wrap) { + return null; + } + return findInIndices(forward ? BigInteger.ZERO : maxIndex, + cur.getIndex(), step); + } + + /** + * A finder that searches for exact text, case insensitive by default + */ + public static class TextTerminalFinder extends TerminalFinder { + protected final String text; + + /** + * @see TerminalPanel#find(String, Set, FieldLocation, boolean) + */ + public TextTerminalFinder(TerminalLayoutModel model, FieldLocation cur, boolean forward, + String text, Set options) { + super(model, cur, forward, options); + if (text.isEmpty()) { + throw new IllegalArgumentException("Empty text"); + } + this.text = text; + } + + @Override + protected FieldRange findInLine(int start, BigInteger index) { + int length = sb.length(); + int i = Math.min(start, length - 1); + int step = forward ? 1 : -1; + while (0 <= i && i < length) { + i = forward ? sb.indexOf(text, i) : sb.lastIndexOf(text, i); + if (i == -1) { + return null; + } + if (!wholeWord || isWholeWord(i, text)) { + return new FieldRange( + new FieldLocation(index, 0, 0, i), + new FieldLocation(index, 0, 0, i + text.length())); + } + i += step; + } + return null; + } + } + + /** + * A find that searches for regex patterns, case insensitive by default + */ + public static class RegexTerminalFinder extends TerminalFinder { + protected final Pattern pattern; + + /** + * @see TerminalPanel#find(String, Set, FieldLocation, boolean) + */ + public RegexTerminalFinder(TerminalLayoutModel model, FieldLocation cur, boolean forward, + String pattern, Set options) { + super(model, cur, forward, options); + if (pattern.isEmpty()) { + throw new IllegalArgumentException("Empty pattern"); + } + this.pattern = Pattern.compile(pattern); + } + + @Override + protected FieldRange findInLine(int start, BigInteger index) { + Matcher matcher = pattern.matcher(sb); + int length = sb.length(); + if (length == 0) { + return null; + } + start = Math.min(length - 1, start); + if (forward) { + for (int i = start; i < length && matcher.find(i);) { + if (!wholeWord || isWholeWord(i, matcher.group())) { + return new FieldRange( + new FieldLocation(index, 0, 0, matcher.start()), + new FieldLocation(index, 0, 0, matcher.end())); + } + i = matcher.start() + 1; + } + return null; + } + int lastStart = -1; + int lastEnd = -1; + for (int i = 0; i <= start && matcher.find(i);) { + if (!wholeWord || isWholeWord(i, matcher.group())) { + if (matcher.start() > start) { + break; + } + lastStart = matcher.start(); + lastEnd = matcher.end(); + } + i = matcher.start() + 1; + } + if (lastStart == -1) { + return null; + } + return new FieldRange( + new FieldLocation(index, 0, 0, lastStart), + new FieldLocation(index, 0, 0, lastEnd)); + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayout.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayout.java new file mode 100644 index 0000000000..7807cfcf16 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayout.java @@ -0,0 +1,43 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.FontMetrics; + +import docking.widgets.fieldpanel.support.SingleRowLayout; +import ghidra.app.plugin.core.terminal.vt.AnsiColorResolver; +import ghidra.app.plugin.core.terminal.vt.VtLine; + +/** + * A layout for a line of text in the terminal. + * + *

+ * The layout is not terribly complicated, but we must also provide the text field and text element. + * Instead of parceling out the attributed strings into different elements, we hand the entire line + * to a single element, which can then render the text, with its various attributes, straight from + * the model's character buffer. This spares us a good deal of object creation, and allows us to + * re-use the layouts more frequently. + */ +public class TerminalLayout extends SingleRowLayout { + protected final VtLine line; + protected final TerminalTextField field; + + public TerminalLayout(VtLine line, FontMetrics metrics, AnsiColorResolver colors) { + super(TerminalTextField.create(line, metrics, colors)); + this.line = line; + this.field = (TerminalTextField) getField(0); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java new file mode 100644 index 0000000000..8184d548a5 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java @@ -0,0 +1,633 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; +import java.util.*; + +import docking.DockingWindowManager; +import docking.widgets.fieldpanel.LayoutModel; +import docking.widgets.fieldpanel.listener.IndexMapper; +import docking.widgets.fieldpanel.listener.LayoutModelListener; +import docking.widgets.fieldpanel.support.FieldLocation; +import docking.widgets.fieldpanel.support.FieldRange; +import ghidra.app.plugin.core.terminal.vt.*; +import ghidra.app.plugin.core.terminal.vt.VtCharset.G; +import ghidra.util.*; + +/** + * The terminal layout model. + * + *

+ * This, the buffers, and the parser, comprise the core logic of the terminal emulator. This + * implements the Ghidra layout model, as well as the handler methods of the VT100 parser. Most of + * the commands it dispatches to the current buffer. A few others modify some flags, e.g., the + * handling of mouse events. Another swaps between buffers, etc. This layout model then maps each + * line to a {@link TerminalLayout}. Unlike some other layout models, this does not create a new + * layout whenever a line is mutated. Given the frequency with which the terminal contents change, + * that would generate a decent bit of garbage. The "layout" instead dynamically computes its + * properties from the mutable line object and paints straight from its buffers. + */ +public class TerminalLayoutModel implements LayoutModel, VtHandler { + + // Buffers for character decoding + protected final ByteBuffer bb = ByteBuffer.allocate(16); + protected final CharBuffer cb = CharBuffer.allocate(16); + + protected final CharsetDecoder decoder; + + // States for handling VT-style charsets + protected final Map vtCharsets = new HashMap<>(); + protected VtCharset.G curVtCharsetG = VtCharset.G.G0; + protected VtCharset curVtCharset = VtCharset.USASCII; + + // A handle to the panel, so that application commands can manipulate it, e.g., titles, + // cursor enablement + protected final TerminalPanel panel; + + // Rendering properties + protected FontMetrics metrics; + protected final AnsiColorResolver colors; + + protected final ArrayList listeners = new ArrayList<>(); + + // Layouts and cache for the model + protected ArrayList layouts = new ArrayList<>(); + protected BigInteger numIndexes = BigInteger.ZERO; + protected final Map layoutCache = new LinkedHashMap<>() { + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= bufPrimary.size() + bufAlternate.size(); + } + }; + + // The parser for the actual VT/ANSI control sequences + protected VtParser parser = new VtParser(this); + + // Screen buffers, primary, alternate, and current + protected final VtBuffer bufPrimary = new VtBuffer(); + protected final VtBuffer bufAlternate = new VtBuffer(); + protected VtBuffer buffer = bufPrimary; + + // Flags for what's been enabled + protected boolean bracketedPaste; + protected boolean reportMousePress; + protected boolean reportMouseRelease; + protected boolean reportFocus; + + private Object lock = new Object(); + + /** + * Create a model + * + * @param panel the panel to receive commands from the model's VT/ANSI parser + * @param charset the charset for decoding bytes to characters + * @param metrics font metrics for the monospaced terminal font + * @param colors a resolver for ANSI colors + */ + public TerminalLayoutModel(TerminalPanel panel, Charset charset, FontMetrics metrics, + AnsiColorResolver colors) { + this.panel = panel; + this.decoder = charset.newDecoder(); + this.metrics = metrics; + this.colors = colors; + + bufAlternate.setMaxScrollBack(0); + + buildLayouts(); + } + + @Override + public void handleFullReset() { + bb.clear(); + cb.clear(); + decoder.reset(); + vtCharsets.clear(); + curVtCharsetG = VtCharset.G.G0; + curVtCharset = VtCharset.USASCII; + + layouts.clear(); + layoutCache.clear(); + bufPrimary.reset(); + bufAlternate.reset(); + buffer = bufPrimary; + + bracketedPaste = false; + reportMousePress = false; + reportMouseRelease = false; + reportFocus = false; + } + + public void processInput(ByteBuffer buffer) { + synchronized (lock) { + parser.process(buffer); + // TODO: Do this less frequently? + buildLayouts(); + } + Swing.runIfSwingOrRunLater(() -> { + modelChanged(); + panel.placeCursor(true); + }); + } + + @Override + public Dimension getPreferredViewSize() { + // This assumes font is monospaced. + return new Dimension(buffer.getCols() * metrics.charWidth('M'), + buffer.getRows() * metrics.getHeight()); + } + + @Override + public BigInteger getNumIndexes() { + return numIndexes; + } + + @Override + public TerminalLayout getLayout(BigInteger index) { + synchronized (lock) { + if (BigInteger.ZERO.compareTo(index) <= 0 && index.compareTo(numIndexes) < 0) { + return layouts.get(index.intValue()); + } + } + return null; + } + + @Override + public BigInteger getIndexBefore(BigInteger index) { + if (BigInteger.ZERO.compareTo(index) < 0) { + return index.subtract(BigInteger.ONE); + } + return null; + } + + @Override + public BigInteger getIndexAfter(BigInteger index) { + BigInteger candidate = index.add(BigInteger.ONE); + if (candidate.compareTo(numIndexes) < 0) { + return candidate; + } + return null; + } + + protected void addOrSetLayout(int i, TerminalLayout l) { + if (i < layouts.size()) { + layouts.set(i, l); + } + else { + assert i == layouts.size(); + layouts.add(l); + } + } + + protected TerminalLayout newLayout(VtLine line) { + return new TerminalLayout(line, metrics, colors); + } + + protected void buildLayouts() { + int count = buffer.size(); + numIndexes = BigInteger.valueOf(count); + + buffer.forEachLine(true, (i, y, line) -> { + if (i < layouts.size()) { + TerminalLayout layout = layouts.get(i); + if (layout.line == line) { + return; // Already checked for line.clearDirty() + } + layout = layoutCache.computeIfAbsent(line, this::newLayout); + layouts.set(i, layout); + } + else { + TerminalLayout layout = layoutCache.computeIfAbsent(line, this::newLayout); + layouts.add(layout); + } + }); + } + + protected void modelChanged() { + for (LayoutModelListener listener : listeners) { + try { + listener.modelSizeChanged(IndexMapper.IDENTITY_MAPPER); + } + catch (Throwable e) { + Msg.showError(this, null, "Error in Listener", "Error in Listener", e); + } + } + } + + @Override + public boolean isUniform() { + return true; + } + + @Override + public void addLayoutModelListener(LayoutModelListener listener) { + listeners.add(listener); + } + + @Override + public void removeLayoutModelListener(LayoutModelListener listener) { + listeners.remove(listener); + } + + @Override + public void flushChanges() { + // Nothing to do + } + + private static String dumpBuf(ByteBuffer bb) { + byte[] data = new byte[bb.remaining()]; + bb.get(bb.position(), data); + return NumericUtilities.convertBytesToString(data, ":"); + } + + @Override + public void handleChar(byte b) throws Exception { + bb.put(b); + bb.flip(); + CoderResult result = decoder.decode(bb, cb, false); + if (result.isError()) { + Msg.error(this, "Error while decoding: " + dumpBuf(bb)); + decoder.reset(); + bb.clear(); + } + else { + bb.compact(); + } + cb.flip(); + while (cb.hasRemaining()) { + try { + // A little strange using both unicode and vt charsets.... + buffer.putChar(curVtCharset.mapChar(cb.get())); + buffer.moveCursorRight(1); + } + catch (Throwable t) { + Msg.error(this, "Error handling character: " + t, t); + } + } + cb.clear(); + } + + @Override + public void handleBell() { + DockingWindowManager.beep(); + } + + @Override + public void handleBackSpace() { + buffer.moveCursorLeft(1); + } + + @Override + public void handleTab() { + buffer.tab(); + } + + @Override + public void handleBackwardTab(int n) { + for (int i = 0; i < n; i++) { + buffer.tabBack(); + } + } + + @Override + public void handleLineFeed() { + buffer.moveCursorDown(1); + } + + @Override + public void handleCarriageReturn() { + buffer.carriageReturn(); + } + + @Override + public void handleSetCharset(G g, VtCharset cs) { + vtCharsets.put(g, cs); + if (curVtCharsetG == g) { + curVtCharset = cs; + } + } + + @Override + public void handleAltCharset(boolean alt) { + curVtCharsetG = alt ? VtCharset.G.G1 : VtCharset.G.G0; + curVtCharset = vtCharsets.getOrDefault(curVtCharsetG, VtCharset.USASCII); + } + + @Override + public void handleForegroundColor(AnsiColor fg) { + buffer.setAttributes(buffer.getAttributes().fg(fg)); + } + + @Override + public void handleBackgroundColor(AnsiColor bg) { + buffer.setAttributes(buffer.getAttributes().bg(bg)); + } + + @Override + public void handleResetAttributes() { + buffer.setAttributes(VtAttributes.DEFAULTS); + } + + @Override + public void handleIntensity(Intensity intensity) { + buffer.setAttributes(buffer.getAttributes().intensity(intensity)); + } + + @Override + public void handleFont(AnsiFont font) { + buffer.setAttributes(buffer.getAttributes().font(font)); + } + + @Override + public void handleUnderline(Underline underline) { + buffer.setAttributes(buffer.getAttributes().underline(underline)); + } + + @Override + public void handleBlink(Blink blink) { + buffer.setAttributes(buffer.getAttributes().blink(blink)); + } + + @Override + public void handleReverseVideo(boolean reverse) { + buffer.setAttributes(buffer.getAttributes().reverseVideo(reverse)); + } + + @Override + public void handleHidden(boolean hidden) { + buffer.setAttributes(buffer.getAttributes().hidden(hidden)); + } + + @Override + public void handleStrikeThrough(boolean strikeThrough) { + buffer.setAttributes(buffer.getAttributes().strikeThrough(strikeThrough)); + } + + @Override + public void handleProportionalSpacing(boolean spacing) { + buffer.setAttributes(buffer.getAttributes().proportionalSpacing(spacing)); + } + + @Override + public void handleInsertMode(boolean en) { + // Not seen any use this, but it'll probably need doing later. + Msg.trace(this, "TODO: handleInsertMode: " + en); + } + + @Override + public void handleApplicationCursorKeys(boolean en) { + // Not sure what this means. Ignore for now. + Msg.trace(this, "TODO: handleApplicationCursorKeys: " + en); + } + + @Override + public void handleApplicationKeypad(boolean en) { + // Not sure what this means. Ignore for now. + Msg.trace(this, "TODO: handleApplicationKeypad: " + en); + } + + @Override + public void handleAutoWrapMode(boolean en) { + System.err.println("TODO: handleAutoWrapMode: " + en); + } + + @Override + public void handleBlinkCursor(boolean blink) { + // Ignore this. FieldPanel seems to support it, but it's inconsistent. + // It's not a necessary feature, anyway. + Msg.trace(this, "TODO: handleBlinkCursor: " + blink); + } + + @Override + public void handleShowCursor(boolean show) { + panel.fieldPanel.setCursorOn(show); + } + + @Override + public void handleReportMouseEvents(boolean press, boolean release) { + reportMousePress = press; + reportMouseRelease = release; + } + + @Override + public void handleReportFocus(boolean report) { + reportFocus = report; + } + + @Override + public void handleMetaKey(boolean en) { + Msg.trace(this, "TODO: handleMetaKey: " + en); // Not sure I care + } + + @Override + public void handleAltScreenBuffer(boolean alt, boolean clearAlt) { + VtBuffer newBuffer = alt ? bufAlternate : bufPrimary; + if (buffer == newBuffer) { + return; + } + if (clearAlt) { + bufAlternate.erase(Erasure.FULL_DISPLAY); + } + buffer = newBuffer; + } + + @Override + public void handleBracketedPasteMode(boolean en) { + this.bracketedPaste = en; + } + + @Override + public void handleSaveCursorPos() { + buffer.saveCursorPos(); + } + + @Override + public void handleRestoreCursorPos() { + buffer.restoreCursorPos(); + } + + @Override + public void handleMoveCursor(Direction direction, int n) { + switch (direction) { + case UP: + buffer.moveCursorUp(n); + return; + case DOWN: + buffer.moveCursorDown(n); + return; + case FORWARD: + buffer.moveCursorRight(n); + return; + case BACK: + buffer.moveCursorLeft(n); + return; + } + } + + @Override + public void handleMoveCursor(int row, int col) { + buffer.moveCursor(row, col); + } + + @Override + public void handleMoveCursorRow(int row) { + buffer.moveCursor(row, buffer.getCurX()); + } + + @Override + public void handleMoveCursorCol(int col) { + buffer.moveCursor(buffer.getCurY(), col); + } + + @Override + public void handleReportCursorPos() { + panel.reportCursorPos(buffer.getCurY(), buffer.getCurX()); + } + + @Override + public void handleErase(Erasure erasure) { + buffer.erase(erasure); + } + + @Override + public void handleInsertLines(int n) { + buffer.insertLines(n); + } + + @Override + public void handleDeleteLines(int n) { + buffer.deleteLines(n); + } + + @Override + public void handleDeleteCharacters(int n) { + buffer.deleteChars(n); + } + + @Override + public void handleEraseCharacters(int n) { + buffer.eraseChars(n); + } + + @Override + public void handleInsertCharacters(int n) { + buffer.insertChars(n); + } + + @Override + public void handleSetScrollRange(Integer start, Integer end) { + buffer.setScrollViewport(start, end); + } + + @Override + public void handleScrollViewportDown(int n, boolean intoScrollBack) { + for (int i = 0; i < n; i++) { + buffer.scrollViewportDown(intoScrollBack); + } + } + + @Override + public void handleScrollViewportUp(int n) { + for (int i = 0; i < n; i++) { + buffer.scrollViewportUp(); + } + } + + @Override + public void handleSaveIconTitle() { + // Don't care about "Icon" title + } + + @Override + public void handleRestoreIconTitle() { + // Don't care about "Icon" title + } + + @Override + public void handleSaveWindowTitle() { + panel.saveTitle(); + } + + @Override + public void handleRestoreWindowTitle() { + panel.restoreTitle(); + } + + @Override + public void handleWindowTitle(String title) { + panel.setTitle(title); + } + + protected boolean resizeTerminal(int cols, int rows) { + boolean affected; + synchronized (lock) { + affected = buffer.resize(cols, rows); + bufPrimary.resize(cols, rows); + bufAlternate.resize(cols, rows); + } + if (affected) { + Swing.runIfSwingOrRunLater(() -> { + modelChanged(); + panel.placeCursor(true); + }); + } + return affected; + } + + public int getScrollBackSize() { + return buffer.getScrollBackSize(); + } + + public int getCursorRow() { + return buffer.getCurY(); + } + + public int getCursorColumn() { + return buffer.getCurX(); + } + + public int getCols() { + return buffer.getCols(); + } + + public int getRows() { + return buffer.getRows(); + } + + public String getSelectedText(FieldRange range) { + synchronized (lock) { + FieldLocation start = range.getStart(); + int startRow = start.getIndex().intValueExact(); + int startCol = start.getCol(); + + FieldLocation end = range.getEnd(); + int endRow = end.getIndex().intValueExact(); + int endCol = end.getCol(); + + return buffer.getText(startRow, startCol, endRow, endCol, System.lineSeparator()); + } + } + + public void setFontMetrics(FontMetrics metrics2) { + layouts.clear(); + layoutCache.clear(); + buildLayouts(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalListener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalListener.java new file mode 100644 index 0000000000..a67a40f31d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalListener.java @@ -0,0 +1,41 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +/** + * A listener for various events on a terminal panel + */ +public interface TerminalListener { + /** + * The terminal was resized by the user + * + *

+ * If applicable and possible, this information should be communicated to the connection + * + * @param cols the number of columns + * @param rows the number of rows + */ + default void resized(int cols, int rows) { + } + + /** + * The application requested the window title changed + * + * @param title the requested title + */ + default void retitled(String title) { + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java new file mode 100644 index 0000000000..91722d98ce --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java @@ -0,0 +1,712 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.*; +import java.awt.event.*; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.*; +import java.util.List; + +import javax.swing.JPanel; +import javax.swing.ScrollPaneConstants; + +import docking.widgets.EventTrigger; +import docking.widgets.fieldpanel.FieldPanel; +import docking.widgets.fieldpanel.LayoutModel; +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.listener.*; +import docking.widgets.fieldpanel.support.*; +import docking.widgets.indexedscrollpane.IndexedScrollPane; +import generic.theme.GColor; +import generic.theme.Gui; +import ghidra.app.plugin.core.terminal.TerminalFinder.RegexTerminalFinder; +import ghidra.app.plugin.core.terminal.TerminalFinder.TextTerminalFinder; +import ghidra.app.plugin.core.terminal.vt.*; +import ghidra.app.plugin.core.terminal.vt.VtHandler.*; +import ghidra.app.services.ClipboardService; +import ghidra.util.ColorUtils; +import ghidra.util.Msg; + +/** + * A VT100 terminal emulator in a panel. + * + *

+ * This implementation uses Ghidra's {@link FieldPanel} for its rendering, highlighting, cursor + * positioning, etc. This one follows the same pattern as many other such panels in Ghidra with some + * exceptions. Namely, it removes all key listeners from the field panel to prevent any accidental + * local control of the cursor. A terminal emulator defers that entirely to the application. Key + * strokes are instead sent to the application directly, and it may respond with commands to move + * the actual cursor. This component also implements the {@link AnsiColorResolver}, as it makes the + * most sense to declare the various {@link GColor}s here. + */ +public class TerminalPanel extends JPanel implements FieldLocationListener, FieldSelectionListener, + LayoutListener, AnsiColorResolver { + protected static final int MAX_TITLE_STACK_SIZE = 20; + + protected static final String DEFAULT_FONT_ID = "font.plugin.terminal"; + protected static final GColor COLOR_BACKGROUND = new GColor("color.bg.plugin.terminal"); + protected static final GColor COLOR_FOREGROUND = new GColor("color.fg.plugin.terminal"); + protected static final GColor COLOR_CURSOR_FOCUSED = + new GColor("color.cursor.focused.terminal"); + protected static final GColor COLOR_CURSOR_UNFOCUSED = + new GColor("color.cursor.unfocused.terminal"); + + // basic colors + protected static final GColor COLOR_0_BLACK = + new GColor("color.fg.plugin.terminal.normal.black"); + protected static final GColor COLOR_1_RED = + new GColor("color.fg.plugin.terminal.normal.red"); + protected static final GColor COLOR_2_GREEN = + new GColor("color.fg.plugin.terminal.normal.green"); + protected static final GColor COLOR_3_YELLOW = + new GColor("color.fg.plugin.terminal.normal.yellow"); + protected static final GColor COLOR_4_BLUE = + new GColor("color.fg.plugin.terminal.normal.blue"); + protected static final GColor COLOR_5_MAGENTA = + new GColor("color.fg.plugin.terminal.normal.magenta"); + protected static final GColor COLOR_6_CYAN = + new GColor("color.fg.plugin.terminal.normal.cyan"); + protected static final GColor COLOR_7_WHITE = + new GColor("color.fg.plugin.terminal.normal.white"); + protected static final GColor COLOR_0_BRIGHT_BLACK = + new GColor("color.fg.plugin.terminal.bright.black"); + protected static final GColor COLOR_1_BRIGHT_RED = + new GColor("color.fg.plugin.terminal.bright.red"); + protected static final GColor COLOR_2_BRIGHT_GREEN = + new GColor("color.fg.plugin.terminal.bright.green"); + protected static final GColor COLOR_3_BRIGHT_YELLOW = + new GColor("color.fg.plugin.terminal.bright.yellow"); + protected static final GColor COLOR_4_BRIGHT_BLUE = + new GColor("color.fg.plugin.terminal.bright.blue"); + protected static final GColor COLOR_5_BRIGHT_MAGENTA = + new GColor("color.fg.plugin.terminal.bright.magenta"); + protected static final GColor COLOR_6_BRIGHT_CYAN = + new GColor("color.fg.plugin.terminal.bright.cyan"); + protected static final GColor COLOR_7_BRIGHT_WHITE = + new GColor("color.fg.plugin.terminal.bright.white"); + + protected static final int[] CUBE_STEPS = { + 0, 95, 135, 175, 215, 255 + }; + + protected class TerminalFieldPanel extends FieldPanel { + public TerminalFieldPanel(LayoutModel model) { + super(model, "Terminal"); + setFieldDescriptionProvider((l, f) -> { + if (f == null) { + return null; + } + // TODO: Adjust, because lines in the history should not be counted + return "line " + (l.getIndex().intValue() + 1) + ": " + f.getText(); + }); + paintContext.setFocusedCursorColor(COLOR_CURSOR_FOCUSED); + paintContext.setNotFocusedCursorColor(COLOR_CURSOR_UNFOCUSED); + paintContext.setCursorFocused(true); + } + + @Override + public void modelSizeChanged(IndexMapper indexMapper) { + // Avoid centering on cursor + setCursorOn(false); + super.modelSizeChanged(indexMapper); + setCursorOn(true); + } + } + + protected FontMetrics metrics; + protected final TerminalLayoutModel model; + protected final TerminalFieldPanel fieldPanel; + protected final IndexedScrollPane scroller; + + protected boolean fixedSize = false; + protected String title; + protected final Deque titleStack = new LinkedList<>(); + + protected final TerminalProvider provider; + protected ClipboardService clipboardService; + protected TerminalClipboardProvider clipboardProvider; + protected String selectedText; + + protected final ArrayList terminalListeners = new ArrayList<>(); + + protected VtOutput outputCb; + protected final TerminalAwtEventEncoder eventEncoder; + protected final VtResponseEncoder responseEncoder; + + protected TerminalPanel(Charset charset, TerminalProvider provider) { + this.provider = provider; + clipboardProvider = new TerminalClipboardProvider(provider); + Gui.registerFont(this, DEFAULT_FONT_ID); + this.metrics = getFontMetrics(getFont()); + this.model = new TerminalLayoutModel(this, charset, metrics, this); + this.fieldPanel = new TerminalFieldPanel(model); + fieldPanel.addFieldSelectionListener(this); + fieldPanel.addFieldLocationListener(this); + fieldPanel.addLayoutListener(this); + + setBackground(COLOR_BACKGROUND); + // Have to set background before creating scroller; + fieldPanel.setBackgroundColor(COLOR_BACKGROUND); + scroller = new IndexedScrollPane(fieldPanel); + scroller.setBackground(COLOR_BACKGROUND); + scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + + scroller.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + if (fixedSize) { + return; + } + resizeTerminalToWindow(); + } + }); + + setPreferredSize(new Dimension(600, 400)); + + setLayout(new BorderLayout()); + add(scroller); + + eventEncoder = new TerminalAwtEventEncoder(charset) { + @Override + public void generateBytes(ByteBuffer buf) { + if (outputCb != null) { + outputCb.out(buf); + } + } + }; + responseEncoder = new VtResponseEncoder(charset) { + @Override + protected void generateBytes(ByteBuffer buf) { + if (outputCb != null) { + outputCb.out(buf); + } + } + }; + + for (KeyListener r : fieldPanel.getKeyListeners()) { + fieldPanel.removeKeyListener(r); + } + fieldPanel.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (provider.isLocalActionKeyBinding(e)) { + return; // Do not consume, so action can take it + } + eventEncoder.keyPressed(e); + e.consume(); + } + + @Override + public void keyTyped(KeyEvent e) { + eventEncoder.keyTyped(e); + e.consume(); + } + }); + fieldPanel.addMouseListener(new MouseListener() { + @Override + public void mousePressed(MouseEvent e) { + /** + * NOTE: According to gdb's docs, it's common for terminals to use SHIFT to override + * application mouse tracking: + * + * https://sourceware.org/gdb/onlinedocs/gdb/TUI-Mouse-Support.html + */ + if (model.reportMousePress && !e.isShiftDown()) { + FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY()); + eventEncoder.mousePressed(e, location.getIndex().intValueExact(), + location.getCol()); + e.consume(); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (model.reportMousePress && !e.isShiftDown()) { + FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY()); + eventEncoder.mouseReleased(e, location.getIndex().intValueExact(), + location.getCol()); + e.consume(); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY()); + if (model.reportMousePress && !e.isShiftDown()) { + e.consume(); + return; + } + else if (e.getClickCount() == 2 && e.getButton() == 1) { + selectWordAt(location, EventTrigger.GUI_ACTION); + e.consume(); + } + else if (e.getButton() == 2) { + String text = getSelectedText(); + if (text == null) { + return; + } + paste(text); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + }); + fieldPanel.addMouseMotionListener(new MouseMotionListener() { + @Override + public void mouseDragged(MouseEvent e) { + if (model.reportMousePress && !e.isShiftDown()) { + // TODO: This is not stopping the field selection + e.consume(); + return; + } + } + + @Override + public void mouseMoved(MouseEvent e) { + } + }); + fieldPanel.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY()); + if (model.reportMousePress && !e.isShiftDown()) { + eventEncoder.mouseWheelMoved(e, location.getIndex().intValueExact(), + location.getCol()); + e.consume(); + } + } + }); + fieldPanel.addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + if (model.reportFocus) { + eventEncoder.focusGained(); + } + } + + @Override + public void focusLost(FocusEvent e) { + if (model.reportFocus) { + eventEncoder.focusLost(); + } + } + }); + } + + public void addTerminalListener(TerminalListener listener) { + terminalListeners.add(listener); + } + + public void removeTerminalListener(TerminalListener listener) { + terminalListeners.remove(listener); + } + + protected void notifyTerminalResized(int cols, int rows) { + for (TerminalListener l : terminalListeners) { + try { + l.resized(cols, rows); + } + catch (Throwable t) { + Msg.showError(this, null, "Error", t.getMessage(), t); + } + } + } + + protected void notifyTerminalRetitled(String title) { + for (TerminalListener l : terminalListeners) { + try { + l.retitled(title); + } + catch (Throwable t) { + Msg.showError(this, null, "Error", t.getMessage(), t); + } + } + } + + @Override + public void setFont(Font font) { + super.setFont(font); + this.metrics = getFontMetrics(font); + if (model != null) { + model.setFontMetrics(this.metrics); + } + } + + public TerminalFieldPanel getFieldPanel() { + return fieldPanel; + } + + @Override + public void layoutsChanged(List layouts) { + /** + * Don't just blow away the selection every key stroke; however, don't allow terminal + * changes to modify the selected text without the user knowing. That rule is directly + * implemented here. If the selected text changes, destroy the selection. + */ + if (!Objects.equals(selectedText, getSelectedText())) { + fieldPanel.clearSelection(); + } + } + + @Override + public void selectionChanged(FieldSelection selection, EventTrigger trigger) { + selectedText = getSelectedText(); + clipboardProvider.selectionChanged(selection); + } + + @Override + public void fieldLocationChanged(FieldLocation location, Field field, EventTrigger trigger) { + /** + * Prevent the user from doing this. Cursor location is controlled by pty. While we've + * prevented key strokes from causing this, we've not prevented mouse clicks from doing it. + * Next best thing is to just move it back. + */ + if (trigger == EventTrigger.GUI_ACTION) { + placeCursor(false); + } + } + + /** + * Select the whole word at the given location. + * + *

+ * This is used for double-click to select the whole word. + * + * @param location the cursor's location + * @param trigger the cause of the selection + */ + public void selectWordAt(FieldLocation location, EventTrigger trigger) { + BigInteger index = location.getIndex(); + TerminalLayout layout = model.getLayout(index); + if (layout == null) { + return; + } + int start = Math.min(location.col, layout.line.findWord(location.col, false)); + int end = Math.max(location.col + 1, layout.line.findWord(location.col, true)); + FieldSelection sel = new FieldSelection(); + sel.addRange(new FieldLocation(index, 0, 0, start), new FieldLocation(index, 0, 0, end)); + fieldPanel.setSelection(sel, trigger); + } + + /** + * Process the given bytes as application output. + * + *

+ * In most circumstances, there is a thread that just reads an output stream, usually from a + * pty, and feeds it into this method. + * + * @param buffer the buffer + */ + public void processInput(ByteBuffer buffer) { + model.processInput(buffer); + } + + protected Color resolveDefaultColor(WhichGround ground, boolean reverseVideo) { + if (ground == WhichGround.BACKGROUND) { + if (reverseVideo) { + return COLOR_FOREGROUND; + } + return null; // background is already drawn + } + if (reverseVideo) { + return COLOR_BACKGROUND; + } + return COLOR_FOREGROUND; + } + + protected Color resolveStandardColor(AnsiStandardColor standard) { + return switch (standard) { + case BLACK -> COLOR_0_BLACK; + case RED -> COLOR_1_RED; + case GREEN -> COLOR_2_GREEN; + case YELLOW -> COLOR_3_YELLOW; + case BLUE -> COLOR_4_BLUE; + case MAGENTA -> COLOR_5_MAGENTA; + case CYAN -> COLOR_6_CYAN; + case WHITE -> COLOR_7_WHITE; + }; + } + + protected Color resolveIntenseColor(AnsiIntenseColor intense) { + return switch (intense) { + case BLACK -> COLOR_0_BRIGHT_BLACK; + case RED -> COLOR_1_BRIGHT_RED; + case GREEN -> COLOR_2_BRIGHT_GREEN; + case YELLOW -> COLOR_3_BRIGHT_YELLOW; + case BLUE -> COLOR_4_BRIGHT_BLUE; + case MAGENTA -> COLOR_5_BRIGHT_MAGENTA; + case CYAN -> COLOR_6_BRIGHT_CYAN; + case WHITE -> COLOR_7_BRIGHT_WHITE; + }; + } + + protected Color resolve216Color(Ansi216Color cube) { + return ColorUtils.getColor(CUBE_STEPS[cube.r()], CUBE_STEPS[cube.g()], + CUBE_STEPS[cube.b()]); + } + + protected Color resolveGrayscaleColor(AnsiGrayscaleColor gray) { + return ColorUtils.getColor(gray.v() * 10 + 8); + } + + protected Color resolve24BitColor(Ansi24BitColor rgb) { + return ColorUtils.getColor(rgb.r(), rgb.g(), rgb.b()); + } + + @Override + public Color resolveColor(AnsiColor color, WhichGround ground, Intensity intensity, + boolean reverseVideo) { + if (color == AnsiDefaultColor.INSTANCE) { + return resolveDefaultColor(ground, reverseVideo); + } + if (color instanceof AnsiStandardColor standard) { + return resolveStandardColor(standard); + } + if (color instanceof AnsiIntenseColor intense) { + return resolveIntenseColor(intense); + } + if (color instanceof Ansi216Color cube) { + return resolve216Color(cube); + } + if (color instanceof AnsiGrayscaleColor gray) { + return resolveGrayscaleColor(gray); + } + if (color instanceof Ansi24BitColor rgb) { + return resolve24BitColor(rgb); + } + throw new AssertionError(); + } + + public void setClipboardService(ClipboardService clipboardService) { + if (this.clipboardService == clipboardService) { + return; + } + if (this.clipboardService != null) { + this.clipboardService.deRegisterClipboardContentProvider(clipboardProvider); + } + this.clipboardService = clipboardService; + if (this.clipboardService != null) { + this.clipboardService.registerClipboardContentProvider(clipboardProvider); + } + } + + /** + * Set the callback for application input, i.e., terminal output + * + *

+ * In most circumstances, the bytes are sent to an input stream, usually from a pty. + * + * @param outputCb the callback + */ + public void setOutputCallback(VtOutput outputCb) { + this.outputCb = outputCb; + } + + protected void placeCursor(boolean scroll) { + int scrollBack = model.getScrollBackSize(); + fieldPanel.setCursorPosition(BigInteger.valueOf(model.getCursorRow() + scrollBack), 0, 0, + model.getCursorColumn()); + if (scroll) { + fieldPanel.scrollToCursor(); + } + } + + protected void saveTitle() { + titleStack.push(title); + if (titleStack.size() > MAX_TITLE_STACK_SIZE) { + titleStack.pollLast(); + } + } + + protected void restoreTitle() { + notifyTerminalRetitled(title = titleStack.poll()); + } + + protected void setTitle(String title) { + notifyTerminalRetitled(this.title = title); + } + + /** + * Send the cursor's position to the application + * + * @param row the cursor's row + * @param col the cursor's column + */ + public void reportCursorPos(int row, int col) { + responseEncoder.reportCursorPos(row, col); + } + + public void dispose() { + if (this.clipboardService != null) { + clipboardService.deRegisterClipboardContentProvider(clipboardProvider); + } + } + + /** + * Send the given text to the application, as if typed on the keyboard + * + *

+ * Note the application may request a mode called "bracketed paste," in which case the text will + * be surrounded by special control sequences, allowing the application to distinguish pastes + * from manual typing. An application may do this so that an Undo could undo the whole paste, + * and not just the last keystroke simulated by the paste. + * + * @param text the text + */ + public void paste(String text) { + if (model.bracketedPaste) { + responseEncoder.reportPasteStart(); + } + try { + eventEncoder.sendText(text); + } + finally { + if (model.bracketedPaste) { + responseEncoder.reportPasteEnd(); + } + } + } + + /** + * Get the text selected by the user + * + *

+ * If the selection is disjoint, this returns null. + * + * @return the selected text, or null + */ + public String getSelectedText() { + FieldSelection sel = fieldPanel.getSelection(); + if (sel == null || sel.getNumRanges() != 1) { + return null; + } + return getSelectedText(sel.getFieldRange(0)); + } + + /** + * Get the text covered by the given range + * + * @param range the range + * @return the text + */ + public String getSelectedText(FieldRange range) { + return model.getSelectedText(range); + } + + /** + * Enumerated options available when searching the terminal's buffer + */ + public enum FindOptions { + /** + * Make the search case sensitive. If this flag is absent, the search defaults to case + * insensitive. + */ + CASE_SENSITIVE, + /** + * Allow the search to wrap. + */ + WRAP, + /** + * Require the result to be a whole word. + */ + WHOLE_WORD, + /** + * Treat the search term as a regular expression instead of literal text. + */ + REGEX + } + + /** + * Search the terminal's buffer for the given text. + * + *

+ * The start location should be given, so that the search can progress to each successive + * result. If no location is given, e.g., because this is the first time the user has searched, + * then a default location will be chosen based on the search direction: the start for forward + * or the end for backward. + * + * @param text the text (or pattern for {@link FindOptions#REGEX}) + * @param options the search options + * @param start the starting location, or null for a default + * @param forward true to search forward, false to search backward + * @return the range covering the found term, or null if not found + */ + public FieldRange find(String text, Set options, FieldLocation start, + boolean forward) { + TerminalFinder finder = options.contains(FindOptions.REGEX) + ? new RegexTerminalFinder(model, start, forward, text, options) + : new TextTerminalFinder(model, start, forward, text, options); + return finder.find(); + } + + protected void resizeTerminalToWindow() { + Rectangle bounds = scroller.getViewportBorderBounds(); + int rows = bounds.height / metrics.getHeight(); + int cols = bounds.width / metrics.charWidth('M'); + resizeTerminal(rows, cols); + } + + protected void resizeTerminal(int rows, int cols) { + if (model.resizeTerminal(cols, rows)) { + notifyTerminalResized(model.getCols(), model.getRows()); + } + } + + /** + * Set the terminal to a fixed size. + * + *

+ * The terminal will no longer respond to the window resizing, and scrollbars are displayed as + * needed. If the terminal size changes as a result of this call, + * {@link TerminalListener#resized(int, int)} is invoked. + * + * @param rows the number of rows + * @param cols the number of columns + */ + public void setFixedTerminalSize(int rows, int cols) { + this.fixedSize = true; + scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + resizeTerminal(rows, cols); + } + + /** + * Set the terminal to fit the window size. + * + *

+ * Immediately fit the terminal to the window. It will also respond to the window resizing by + * recalculating the rows and columns and adjusting the buffer's contents to fit. Whenever the + * terminal size changes {@link TerminalListener#resized(int, int)} is invoked. The bottom + * scrollbar is disabled, and the vertical scrollbar is always displayed, to avoid frenetic + * horizontal resizing. + */ + public void setDynamicTerminalSize() { + this.fixedSize = false; + scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + resizeTerminalToWindow(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPlugin.java new file mode 100644 index 0000000000..8858a03dac --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPlugin.java @@ -0,0 +1,102 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import ghidra.app.CorePluginPackage; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.terminal.vt.VtOutput; +import ghidra.app.services.*; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.util.Msg; + +/** + * The plugin that provides {@link TerminalService} + */ +@PluginInfo( + status = PluginStatus.UNSTABLE, + category = PluginCategoryNames.COMMON, + packageName = CorePluginPackage.NAME, + description = "Provides VT100 Terminal Emulation", + shortDescription = "VT100 Emulator", + servicesProvided = { TerminalService.class }) +public class TerminalPlugin extends Plugin implements TerminalService { + + protected ClipboardService clipboardService; + + protected List providers = new ArrayList<>(); + + public TerminalPlugin(PluginTool tool) { + super(tool); + clipboardService = tool.getService(ClipboardService.class); + } + + public TerminalProvider createProvider(Charset charset, VtOutput outputCb) { + TerminalProvider provider = new TerminalProvider(this, charset); + provider.setOutputCallback(outputCb); + provider.addToTool(); + provider.setVisible(true); + providers.add(provider); + provider.setClipboardService(clipboardService); + return provider; + } + + @Override + public Terminal createNullTerminal(Charset charset, VtOutput outputCb) { + return new DefaultTerminal(createProvider(charset, outputCb)); + } + + @Override + public Terminal createWithStreams(Charset charset, InputStream in, OutputStream out) { + WritableByteChannel channel = Channels.newChannel(out); + return new ThreadedTerminal(createProvider(charset, buf -> { + while (buf.hasRemaining()) { + try { + channel.write(buf); + } + catch (IOException e) { + Msg.error(this, "Could not write terminal output", e); + } + } + }), in); + } + + @Override + public void serviceAdded(Class interfaceClass, Object service) { + if (interfaceClass == ClipboardService.class) { + clipboardService = (ClipboardService) service; + for (TerminalProvider p : providers) { + p.setClipboardService(clipboardService); + } + } + } + + @Override + public void serviceRemoved(Class interfaceClass, Object service) { + if (interfaceClass == ClipboardService.class) { + for (TerminalProvider p : providers) { + p.setClipboardService(null); + } + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java new file mode 100644 index 0000000000..c941ac32dd --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalProvider.java @@ -0,0 +1,309 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.*; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.apache.commons.collections4.IteratorUtils; + +import docking.*; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.action.builder.ActionBuilder; +import docking.widgets.OkDialog; +import docking.widgets.fieldpanel.support.*; +import generic.theme.GIcon; +import ghidra.app.plugin.core.terminal.TerminalPanel.FindOptions; +import ghidra.app.plugin.core.terminal.vt.VtOutput; +import ghidra.app.services.ClipboardService; +import ghidra.framework.plugintool.ComponentProviderAdapter; + +/** + * A window holding a VT100 terminal emulator. + * + *

+ * This also provides UI actions for searching the terminal's contents. + */ +public class TerminalProvider extends ComponentProviderAdapter { + + protected class FindDialog extends DialogComponentProvider { + protected final JTextField txtFind = new JTextField(20); + protected final JCheckBox cbCaseSensitive = new JCheckBox("Case sensitive"); + protected final JCheckBox cbWrapSearch = new JCheckBox("Wrap search"); + protected final JCheckBox cbWholeWord = new JCheckBox("Whole word"); + protected final JCheckBox cbRegex = new JCheckBox("Regular expression"); + + protected final JButton btnFindNext = new JButton("Next"); + protected final JButton btnFindPrevious = new JButton("Previous"); + + protected FindDialog() { + super("Find", false, false, true, false); + + populateComponents(); + } + + protected GridBagConstraints cell(int row, int col, int width, boolean hFill) { + GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridx = col; + constraints.gridy = row; + constraints.gridwidth = width; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.insets = new Insets(row == 0 ? 0 : 5, col == 0 ? 0 : 3, 0, 0); + constraints.weightx = hFill ? 1.0 : 0.0; + return constraints; + } + + protected JLabel label(String text) { + JLabel label = new JLabel(text); + label.setHorizontalAlignment(SwingConstants.RIGHT); + return label; + } + + protected void populateComponents() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + panel.add(label("Find"), cell(0, 0, 1, false)); + panel.add(txtFind, cell(0, 1, 1, true)); + + panel.add(cbCaseSensitive, cell(2, 0, 2, true)); + panel.add(cbWrapSearch, cell(3, 0, 2, true)); + panel.add(cbWholeWord, cell(4, 0, 2, true)); + panel.add(cbRegex, cell(5, 0, 2, true)); + + addWorkPanel(panel); + + addButton(btnFindNext); + addButton(btnFindPrevious); + addDismissButton(); + setDefaultButton(btnFindNext); + + txtFind.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + contextChanged(); + btnFindNext.setEnabled(isEnabledFindStep(null)); + btnFindPrevious.setEnabled(isEnabledFindStep(null)); + } + + @Override + public void removeUpdate(DocumentEvent e) { + contextChanged(); + btnFindNext.setEnabled(isEnabledFindStep(null)); + btnFindPrevious.setEnabled(isEnabledFindStep(null)); + } + + @Override + public void changedUpdate(DocumentEvent e) { + contextChanged(); + btnFindNext.setEnabled(isEnabledFindStep(null)); + btnFindPrevious.setEnabled(isEnabledFindStep(null)); + } + }); + btnFindNext.addActionListener(evt -> { + activatedFindNext(null); + }); + btnFindPrevious.addActionListener(evt -> { + activatedFindPrevious(null); + }); + } + + public Set getOptions() { + EnumSet opts = EnumSet.noneOf(FindOptions.class); + if (cbCaseSensitive.isSelected()) { + opts.add(FindOptions.CASE_SENSITIVE); + } + if (cbWrapSearch.isSelected()) { + opts.add(FindOptions.WRAP); + } + if (cbWholeWord.isSelected()) { + opts.add(FindOptions.WHOLE_WORD); + } + if (cbRegex.isSelected()) { + opts.add(FindOptions.REGEX); + } + return opts; + } + } + + protected final TerminalPlugin plugin; + + protected final TerminalPanel panel; + protected final FindDialog findDialog = new FindDialog(); + + protected DockingAction actionFind; + protected DockingAction actionFindNext; + protected DockingAction actionFindPrevious; + + public TerminalProvider(TerminalPlugin plugin, Charset charset) { + super(plugin.getTool(), "Terminal", plugin.getName()); + this.plugin = plugin; + this.panel = new TerminalPanel(charset, this); + this.panel.addTerminalListener(new TerminalListener() { + @Override + public void retitled(String title) { + setSubTitle(title); + } + }); + createActions(); + } + + @Override + public JComponent getComponent() { + return panel; + } + + public void processInput(ByteBuffer buffer) { + panel.processInput(buffer); + } + + public TerminalPanel getTerminalPanel() { + return panel; + } + + @Override + public void removeFromTool() { + panel.dispose(); + plugin.providers.remove(this); + super.removeFromTool(); + } + + public void setOutputCallback(VtOutput outputCb) { + panel.setOutputCallback(outputCb); + } + + public void addTerminalListener(TerminalListener listener) { + panel.addTerminalListener(listener); + } + + public void removeTerminalListener(TerminalListener listener) { + panel.removeTerminalListener(listener); + } + + public void setClipboardService(ClipboardService clipboardService) { + panel.setClipboardService(clipboardService); + } + + protected void createActions() { + actionFind = new ActionBuilder("Find", plugin.getName()) + .menuIcon(new GIcon("icon.search")) + .menuPath(new String[] { "Find" }) + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F, + InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)) + .onAction(this::activatedFind) + .buildAndInstallLocal(this); + actionFindNext = new ActionBuilder("Find Next", plugin.getName()) + .menuPath(new String[] { "Find Next" }) + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_H, + InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)) + .enabledWhen(this::isEnabledFindStep) + .onAction(this::activatedFindNext) + .buildAndInstallLocal(this); + actionFindPrevious = new ActionBuilder("Find Previous", plugin.getName()) + .menuPath(new String[] { "Find Previous" }) + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_G, + InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)) + .enabledWhen(this::isEnabledFindStep) + .onAction(this::activatedFindPrevious) + .buildAndInstallLocal(this); + } + + protected void activatedFind(ActionContext ctx) { + tool.showDialog(findDialog); + } + + protected void doFind(boolean forward) { + FieldSelection sel = panel.getFieldPanel().getSelection(); + final FieldLocation start; + if (sel == null || sel.getNumRanges() == 0) { + start = null; + } + else { + FieldLocation s = sel.getFieldRange(0).getStart(); + if (forward) { + start = new FieldLocation(s.getIndex(), 0, 0, s.getCol() + 1); + } + else { + /** + * The search algorithm should work such that col == -1 works the same as the end of + * the previous line -- or no result if its the first line. + */ + start = new FieldLocation(s.getIndex(), 0, 0, s.getCol() - 1); + } + } + FieldRange found = + panel.find(findDialog.txtFind.getText(), findDialog.getOptions(), start, forward); + if (found == null) { + OkDialog.showInfo("Find", "String not found"); + return; + } + FieldSelection newSel = new FieldSelection(); + newSel.addRange(found); + panel.fieldPanel.setSelection(newSel); + panel.fieldPanel.scrollTo(found.getStart()); + } + + protected boolean isEnabledFindStep(ActionContext ctx) { + return !findDialog.txtFind.getText().isEmpty(); + } + + protected void activatedFindNext(ActionContext ctx) { + doFind(true); + } + + protected void activatedFindPrevious(ActionContext ctx) { + doFind(false); + } + + /** + * Check if the given keystroke would activate a local action. + * + *

+ * Because we usurp control of the keyboard, but we still want local actions accessible via + * keyboard shortcuts, we need a way to check if a local action could take the stroke. In this + * way, we allow local actions to override the terminal, but not tool/global actions. + * + * @param e the event + * @return true if a local action could be activated + */ + protected boolean isLocalActionKeyBinding(KeyEvent e) { + KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e); + DockingWindowManager wm = DockingWindowManager.getActiveInstance(); + for (DockingActionIf action : IteratorUtils.asIterable(wm.getComponentActions(this))) { + if (Objects.equals(stroke, action.getKeyBinding())) { + return true; + } + } + return false; + } + + public void setFixedSize(int rows, int cols) { + panel.setFixedTerminalSize(rows, cols); + } + + public void setDyanmicSize() { + panel.setDynamicTerminalSize(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextField.java new file mode 100644 index 0000000000..3164d746ad --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextField.java @@ -0,0 +1,286 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.*; +import java.util.List; + +import javax.swing.JComponent; + +import docking.widgets.fieldpanel.field.FieldElement; +import docking.widgets.fieldpanel.field.TextField; +import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager; +import docking.widgets.fieldpanel.internal.PaintContext; +import docking.widgets.fieldpanel.support.*; +import ghidra.app.plugin.core.terminal.vt.*; + +/** + * A text field (renderer) for the terminal panel. + * + *

+ * The purpose of this thing is to hold a single text field element. It is also responsible for + * rendering selections and the cursor. Because the cursor is also supposed to be controlled by the + * application, we do less "validation" and correction of it on our end. If it's past the end of a + * line, so be it. + */ +public class TerminalTextField implements TextField { + protected final int startX; + protected final TerminalTextFieldElement element; + protected final int em; + + protected boolean isPrimary; + + /** + * Create a text field for the given line. + * + *

+ * This method will create the sole text field element populating this field. + * + * @param line the line from the {@link VtBuffer} that will be rendered in this field + * @param metrics the font metrics + * @param colors the color resolver + * @return the field + */ + public static TerminalTextField create(VtLine line, FontMetrics metrics, + AnsiColorResolver colors) { + return new TerminalTextField(0, new TerminalTextFieldElement(line, metrics, colors), + metrics); + } + + protected TerminalTextField(int startX, TerminalTextFieldElement element, FontMetrics metrics) { + this.startX = startX; + this.element = element; + this.em = metrics.charWidth('M'); + } + + @Override + public void paint(JComponent c, Graphics g, PaintContext context, Rectangle clip, + FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, int rowHeight) { + if (context.isPrinting()) { + print(g, context); + } + else { + paintSelection(g, colorManager, 0, rowHeight); + paintText(c, g, context); + paintCursor(g, context.getCursorColor(), cursorLoc); + } + } + + protected void print(Graphics g, PaintContext context) { + element.paint(null, g, startX, 0); + } + + protected void paintText(JComponent c, Graphics g, PaintContext context) { + element.paint(c, g, startX, 0); + } + + protected void paintSelection(Graphics g, FieldBackgroundColorManager colorManager, int row, + int rowHeight) { + List selections = colorManager.getSelectionHighlights(row); + if (selections.isEmpty()) { + return; + } + int textLength = element.length(); + int endTextPos = findX(textLength); + for (Highlight highlight : selections) { + g.setColor(highlight.getColor()); + int startCol = highlight.getStart(); + int endCol = highlight.getEnd(); + int x1 = findX(startCol); + int x2 = endCol < element.length() ? findX(endCol) : endTextPos; + g.fillRect(startX + x1, -getHeightAbove(), x2 - x1, getHeight()); + } + + // Padding? + } + + /** + * Paint a big cursor, so people can actually see it. Also, don't check column number. The + * cursor is frequently past the end of the text, e.g., after pressing space in vim. + */ + protected void paintCursor(Graphics g, Color cursorColor, RowColLocation cursorLoc) { + if (cursorLoc != null) { + g.setColor(cursorColor); + int x = startX + findX(cursorLoc.col()); + g.drawRect(x, -getHeightAbove(), em - 1, getHeight() - 1); + g.drawRect(x + 1, -getHeightAbove() + 1, em - 3, getHeight() - 3); + } + } + + protected int findX(int col) { + return em * col; + } + + @Override + public int getWidth() { + return element.getStringWidth(); + } + + @Override + public int getPreferredWidth() { + return element.getStringWidth(); + } + + @Override + public int getHeight() { + return element.getHeightAbove() + element.getHeightBelow(); + } + + @Override + public int getHeightAbove() { + return element.getHeightAbove(); + } + + @Override + public int getHeightBelow() { + return element.getHeightBelow(); + } + + @Override + public int getStartX() { + return startX; + } + + @Override + public boolean contains(int x, int y) { + return (x >= startX) && (x < startX + getWidth()) && (y >= -element.getHeightAbove()) && + (y < element.getHeightBelow()); + } + + @Override + public int getNumDataRows() { + return 1; + } + + @Override + public int getNumRows() { + return 1; + } + + @Override + public int getNumCols(int row) { + return element.getNumCols(); + } + + @Override + public int getX(int row, int col) { + return startX + findX(col); + } + + @Override + public int getY(int row) { + return -getHeightAbove(); + } + + @Override + public int getRow(int y) { + return 0; + } + + @Override + public int getCol(int row, int x) { + int relX = Math.max(0, x - startX); + return element.getMaxCharactersForWidth(relX); + } + + @Override + public boolean isValid(int row, int col) { + return row == 0 && 0 <= col && col < getNumCols(0); + } + + @Override + public Rectangle getCursorBounds(int row, int col) { + if (row != 0) { + return null; + } + + int x = findX(col) + startX; + return new Rectangle(x, -getHeightAbove(), em, getHeight()); + } + + @Override + public int getScrollableUnitIncrement(int topOfScreen, int direction, int max) { + if ((topOfScreen < -getHeightAbove()) || (topOfScreen > getHeightBelow())) { + return max; + } + + if (direction > 0) { // if scrolling down + return getHeightBelow() - topOfScreen; + } + + return -getHeightAbove() - topOfScreen; + } + + @Override + public void setPrimary(boolean isPrimary) { + this.isPrimary = isPrimary; + } + + @Override + public boolean isPrimary() { + return isPrimary; + } + + @Override + public void rowHeightChanged(int heightAbove, int heightBelow) { + // Don't care + } + + @Override + public String getText() { + return element.getText(); + } + + @Override + public String getTextWithLineSeparators() { + return element.getText(); + } + + @Override + public RowColLocation textOffsetToScreenLocation(int textOffset) { + // allow the max position to be just after the last character + return new RowColLocation(0, Math.min(textOffset, element.length())); + } + + @Override + public int screenLocationToTextOffset(int row, int col) { + return Math.min(element.length(), col); + } + + @Override + public RowColLocation screenToDataLocation(int screenRow, int screenColumn) { + return element.getDataLocationForCharacterIndex(screenColumn); + } + + @Override + public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) { + int column = element.getCharacterIndexForDataLocation(dataRow, dataColumn); + if (column < 0) { + return new DefaultRowColLocation(0, element.length()); + } + return new RowColLocation(0, column); + } + + @Override + public boolean isClipped() { + return false; + } + + @Override + public FieldElement getFieldElement(int screenRow, int screenColumn) { + return element.getFieldElement(screenColumn); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java new file mode 100644 index 0000000000..2e2b0ae4b6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java @@ -0,0 +1,229 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.awt.*; +import java.awt.geom.AffineTransform; + +import javax.swing.JComponent; + +import docking.widgets.fieldpanel.field.FieldElement; +import docking.widgets.fieldpanel.support.RowColLocation; +import ghidra.app.plugin.core.terminal.vt.*; +import ghidra.app.plugin.core.terminal.vt.VtHandler.Intensity; + +/** + * A text field element for rendering a full line of terminal text + * + *

+ * {@link TerminalTextFields} are populated by a single element. The typical pattern seems to be to + * create a separate element for each bit of text having common attributes. This pattern would + * generate quite a bit of garbage, since the terminal contents change frequently. Every time a line + * content changed, we'd have to re-construct the elements. Instead, we use a single re-usable + * element that renders the {@link VtLine} directly, including the variety of attributes. When the + * line changes, we merely have to re-paint. + */ +public class TerminalTextFieldElement implements FieldElement { + public static final int UNDERLINE_HEIGHT = 1; + + protected final VtLine line; + protected final FontMetrics metrics; + protected final AnsiColorResolver colors; + + protected final int em; + + /** + * Create a text field element + * + * @param line the line of text from the {@link VtBuffer} + * @param metrics the font metrics + * @param colors the color resolver + */ + public TerminalTextFieldElement(VtLine line, FontMetrics metrics, AnsiColorResolver colors) { + this.line = line; + this.metrics = metrics; + this.colors = colors; + + this.em = metrics.charWidth('M'); + } + + @Override + public String getText() { + StringBuilder sb = new StringBuilder(); + line.gatherText(sb, 0, line.length()); + return sb.toString(); + } + + @Override + public int length() { + return line.length(); + } + + /** + * Get the number of columns (total width, not just the used by the line) + * + * @return the column count + */ + public int getNumCols() { + return line.cols(); + } + + @Override + public int getStringWidth() { + // Assumes monospaced. + return em * length(); + } + + @Override + public int getHeightAbove() { + return metrics.getMaxAscent() + metrics.getLeading(); + } + + @Override + public int getHeightBelow() { + return metrics.getMaxDescent(); + } + + @Override + public char charAt(int index) { + return line.getChar(index); + } + + @Override + public Color getColor(int charIndex) { + return line.getCellAttrs(charIndex).resolveForeground(colors); + } + + @Override + public FieldElement substring(int start) { + return this; // Used for clipping and wrapping. I don't care. + } + + @Override + public FieldElement substring(int start, int end) { + return this; // Used for clipping and wrapping. I don't care. + } + + @Override + public FieldElement replaceAll(char[] targets, char replacement) { + throw new UnsupportedOperationException("No wrapping"); + } + + @Override + public int getMaxCharactersForWidth(int width) { + // Assumes monospaced. + return width / em; + } + + @Override + public RowColLocation getDataLocationForCharacterIndex(int characterIndex) { + return new RowColLocation(0, characterIndex); + } + + @Override + public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) { + if (dataRow == 0 && dataColumn >= 0 && dataColumn < length()) { + return dataColumn; + } + return -1; + } + + protected static class SaveTransform implements AutoCloseable { + private final Graphics2D g; + private final AffineTransform saved; + + public SaveTransform(Graphics g) { + this.g = (Graphics2D) g; + this.saved = this.g.getTransform(); + } + + @Override + public void close() { + this.g.setTransform(saved); + } + } + + protected void paintChars(JComponent c, Graphics g, int x, int y, VtAttributes attrs, int start, + int end) { + char[] ch = line.getCharBuffer(); + int descent = metrics.getDescent(); + int height = metrics.getHeight(); + int left = x + start * em; + int width = em * (end - start); + Font font = metrics.getFont(); + Color bg = attrs.resolveBackground(colors); + if (bg != null) { + g.setColor(bg); + g.fillRect(left, descent - height, width, height); + } + g.setColor(attrs.resolveForeground(colors)); + // NB. I don't really intend to implement blinking. + // TODO: AnsiFont mapping? + if (attrs.intensity() == Intensity.DIM) { + g.setFont(font.deriveFont(Font.PLAIN)); + } + else { + // Normal will use bold font, but standard color + g.setFont(font.deriveFont(Font.BOLD)); + } + if (!attrs.hidden()) { + switch (attrs.underline()) { + case DOUBLE: + g.fillRect(left, descent - UNDERLINE_HEIGHT * 3, width, UNDERLINE_HEIGHT); + // Yes, fall through + case SINGLE: + g.fillRect(left, descent - UNDERLINE_HEIGHT, width, UNDERLINE_HEIGHT); + case NONE: + } + + for (int i = start; i < end; i++) { + /** + * HACK: The default monospaced font selected by Java may not have glyphs for the + * box-drawing characters, so it may choose glyphs from a different font. + * Alternatively, the default monospaced font's box-drawing glyphs are not, in fact, + * monospaced. This is not acceptable. To deal with that, when we find a glyph whose + * width does not match, we'll scale it horizontally so that it does. + */ + int chW = metrics.charWidth(ch[i]); + if (chW != em) { + try (SaveTransform st = new SaveTransform(g)) { + st.g.translate(x + em * i, 0); + st.g.scale((double) em / chW, 1.0); + st.g.drawChars(ch, i, 1, 0, 0); + } + } + else { + g.drawChars(ch, i, 1, x + em * i, 0); + } + } + if (attrs.strikeThrough()) { + g.fillRect(left, height * 2 / 3, width, UNDERLINE_HEIGHT); + } + } + // TODO: What is proportionalSpacing? + } + + @Override + public void paint(JComponent c, Graphics g, int x, int y) { + line.forEachRun( + (attrs, start, end) -> paintChars(c, g, x, y, attrs, start, end)); + } + + @Override + public FieldElement getFieldElement(int column) { + return this; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/ThreadedTerminal.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/ThreadedTerminal.java new file mode 100644 index 0000000000..071fdd1793 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/ThreadedTerminal.java @@ -0,0 +1,115 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; + +import ghidra.app.services.TerminalService; +import ghidra.util.Msg; + +/** + * A terminal with a background thread and input stream powering its display. + * + *

+ * The thread eagerly reads the given input stream and pumps it into the given provider. Be careful + * using {@link #injectDisplayOutput(ByteBuffer)}. While it is synchronized, there's no guarantee + * escape codes don't get mixed up. Note that this does not make any effort to connect the + * terminal's keyboard to any output stream. + * + * @see TerminalService#createWithStreams(java.nio.charset.Charset, InputStream, OutputStream) + */ +public class ThreadedTerminal extends DefaultTerminal { + + protected final ReadableByteChannel in; + protected final Thread pumpThread = new Thread(this::pump); + protected final ByteBuffer buffer = ByteBuffer.allocate(1024); + + protected boolean closed = false; + + /** + * Construct a terminal connected to the given input stream + * + * @param provider the provider + * @param in the input stream + */ + public ThreadedTerminal(TerminalProvider provider, InputStream in) { + super(provider); + this.in = Channels.newChannel(in); + this.pumpThread.start(); + } + + @Override + public void close() { + closed = true; + pumpThread.interrupt(); + super.close(); + } + + @SuppressWarnings("unused") // diagnostic + private void printBuffer() { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(buffer.position(), bytes); + //System.err.println("<< " + NumericUtilities.convertBytesToString(bytes, ":")); + try { + String str = new String(bytes, "US-ASCII"); + for (char c : str.toCharArray()) { + if (c == 0x1b) { + System.err.print("\n\\x1b"); + } + else if (c < ' ' || c > '\u007f') { + System.err.print("\\x%02x".formatted((int) c)); + } + else { + System.err.print(c); + } + } + System.err.println(); + } + catch (UnsupportedEncodingException e) { + System.err.println("Couldn't decode"); + } + } + + protected void pump() { + try { + while (!closed) { + if (-1 == in.read(buffer) || closed) { + return; + } + buffer.flip(); + //printBuffer(); + synchronized (buffer) { + provider.processInput(buffer); + } + buffer.clear(); + } + } + catch (IOException e) { + Msg.error(this, "Console input closed unexpectedly: " + e); + closed = true; + } + } + + @Override + public void injectDisplayOutput(ByteBuffer bb) { + synchronized (buffer) { + provider.processInput(bb); + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/AnsiColorResolver.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/AnsiColorResolver.java new file mode 100644 index 0000000000..e0c667ff6f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/AnsiColorResolver.java @@ -0,0 +1,49 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.awt.Color; + +import ghidra.app.plugin.core.terminal.vt.VtHandler.AnsiColor; +import ghidra.app.plugin.core.terminal.vt.VtHandler.Intensity; + +/** + * A mechanism for converting an ANSI color specification to an AWT color. + */ +public interface AnsiColorResolver { + + /** + * A stupid name for a thing that is either the foreground or the background. + */ + enum WhichGround { + FOREGROUND, BACKGROUND; + } + + /** + * Convert a color specification to an AWT color + * + * @param color the ANSI color specification + * @param ground identifies the colors use in the foreground or the background + * @param intensity gives the intensity of the color, really only used when a basic color is + * specified. + * @param reverseVideo identifies whether the foreground and background colors were swapped, + * really only used when the default color is specified. + * @return the AWT color, or null to not draw (usually in the case of the default background + * color) + */ + Color resolveColor(AnsiColor color, WhichGround ground, Intensity intensity, + boolean reverseVideo); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtAttributes.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtAttributes.java new file mode 100644 index 0000000000..784ad8c197 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtAttributes.java @@ -0,0 +1,175 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.awt.Color; + +import ghidra.app.plugin.core.terminal.vt.AnsiColorResolver.WhichGround; +import ghidra.app.plugin.core.terminal.vt.VtHandler.*; + +/** + * A tuple of attributes to apply when rendering terminal text. + * + *

+ * These are set and collected as the parser and handler deal with various ANSI VT escape codes. As + * characters are placed in the buffer, the current attributes are applied to the corresponding + * cells. The renderer then has to apply the attributes appropriately as it renders each character + * in the buffer. + */ +public record VtAttributes(AnsiColor fg, AnsiColor bg, Intensity intensity, + AnsiFont font, Underline underline, Blink blink, boolean reverseVideo, boolean hidden, + boolean strikeThrough, boolean proportionalSpacing) { + + /** + * The default attributes: plain white on black, usually. + */ + public static final VtAttributes DEFAULTS = + new VtAttributes(AnsiDefaultColor.INSTANCE, AnsiDefaultColor.INSTANCE, + Intensity.NORMAL, AnsiFont.NORMAL, Underline.NONE, Blink.NONE, false, false, false, + false); + + /** + * Create a copy of this record with the foreground color replaced + * + * @param fg the new foreground color + * @return the new record + */ + public VtAttributes fg(AnsiColor fg) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the background color replaced + * + * @param bg the new background color + * @return the new record + */ + public VtAttributes bg(AnsiColor bg) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the intensity replaced + * + * @param intensity the new intensity + * @return the new record + */ + public VtAttributes intensity(Intensity intensity) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the font replaced + * + * @param font the new font + * @return the new record + */ + public VtAttributes font(AnsiFont font) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the underline replaced + * + * @param underline the new underline + * @return the new record + */ + public VtAttributes underline(Underline underline) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the blink replaced + * + * @param blink the new blink + * @return the new record + */ + public VtAttributes blink(Blink blink) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the reverse-video replaced + * + * @param reverseVideo the new reverse-video + * @return the new record + */ + public VtAttributes reverseVideo(boolean reverseVideo) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the hidden replaced + * + * @param hidden the new hidden + * @return the new record + */ + public VtAttributes hidden(boolean hidden) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the strike-through replaced + * + * @param strikeThrough the new strike-through + * @return the new record + */ + public VtAttributes strikeThrough(boolean strikeThrough) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Create a copy of this record with the proportional-spacing replaced + * + * @param proportionalSpacing the new proportional-spacing + * @return the new record + */ + public VtAttributes proportionalSpacing(boolean proportionalSpacing) { + return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo, + hidden, strikeThrough, proportionalSpacing); + } + + /** + * Resolve the foreground color for these attributes + * + * @param colors the color resolver + * @return the color + */ + public Color resolveForeground(AnsiColorResolver colors) { + return colors.resolveColor(reverseVideo ? bg : fg, WhichGround.FOREGROUND, intensity, + reverseVideo); + } + + /** + * Resolve the background color for these attributes + * + * @param colors the color resolver + * @return the color, or null to not paint the background + */ + public Color resolveBackground(AnsiColorResolver colors) { + return colors.resolveColor(reverseVideo ? fg : bg, WhichGround.BACKGROUND, Intensity.NORMAL, + reverseVideo); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java new file mode 100644 index 0000000000..a111d68fd7 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java @@ -0,0 +1,753 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.util.ArrayDeque; +import java.util.ArrayList; + +import ghidra.app.plugin.core.terminal.vt.VtHandler.Erasure; + +/** + * A buffer for a terminal display and scroll-back + * + *

+ * This object implements all of the buffer, line, and character manipulations available in the + * terminal. It's likely more will need to be added in the future. While the ANSI VT parser + * determines what commands to execute, this buffer provides the actual implementation of those + * commands. + */ +public class VtBuffer { + public static final int DEFAULT_ROWS = 25; + public static final int DEFAULT_COLS = 80; + + protected static final int TAB_WIDTH = 8; + + protected int rows; + protected int cols; + protected int curX; + protected int curY; + protected int savedX; + protected int savedY; + protected int scrollStart; + protected int scrollEnd; // exclusive + + protected int maxScrollBack = 10_000; + + protected VtAttributes curAttrs = VtAttributes.DEFAULTS; + + protected ArrayDeque scrollBack = new ArrayDeque<>(); + protected ArrayList lines = new ArrayList<>(); + + /** + * Create a new buffer of the default size (25 lines, 80 columns) + */ + public VtBuffer() { + this(DEFAULT_ROWS, DEFAULT_COLS); + } + + /** + * Create a new buffer of the given size + * + * @param rows the number of rows + * @param cols the number of columns + */ + public VtBuffer(int rows, int cols) { + this.rows = Math.max(1, rows); + this.cols = Math.max(1, cols); + this.scrollStart = 0; + this.scrollEnd = rows; + + while (lines.size() < rows) { + lines.add(new VtLine(cols)); + } + } + + /** + * Clear the buffer and all state, as if it has just been created + */ + public void reset() { + lines.clear(); + while (lines.size() < rows) { + lines.add(new VtLine(cols)); + } + curX = 0; + curY = 0; + scrollBack.clear(); + } + + /** + * Get the number of rows in the display + * + *

+ * This is not just the number of rows currently being used. This is the "rows" dimension of the + * display, i.e., the maximum number of rows it can display before scrolling. + * + * @return the number of rows + */ + public int getRows() { + return rows; + } + + /** + * Get the number of columns in the display + * + *

+ * This is not just the number of columns currently being used. This is the "columns" dimension + * of the display, i.e., the maximum number of characters in a rows before wrapping. + * + * @return the number of columns + */ + public int getCols() { + return cols; + } + + /** + * Put the given character at the cursor, and move the cursor forward + * + *

+ * The cursor's current attributes are applied to the character. + * + * @param c the character to put into the buffer + * @see #setAttributes(VtAttributes) + * @see #getAttributes() + */ + public void putChar(char c) { + if (c == 0) { + return; + } + lines.get(curY).putChar(curX, c, curAttrs); + } + + /** + * More the cursor forward to the next tab stop + */ + public void tab() { + int n = TAB_WIDTH + (-curX % TAB_WIDTH); + moveCursorRight(n); + } + + /** + * Move the cursor backward to the previous tab stop + */ + public void tabBack() { + if (curX == 0) { + return; + } + int n = (curX - 1) % TAB_WIDTH + 1; + moveCursorLeft(n); + } + + /** + * Move the cursor back to the beginning of the line + * + *

+ * This does not move the cursor down. + */ + public void carriageReturn() { + curX = 0; + } + + /** + * Scroll the viewport down a line + * + *

+ * The lines are shifted upward. The line at the top of the viewport is removed, and a blank + * line is inserted at the bottom of the viewport. If the viewport includes the display's top + * line and intoScrollBack is specified, the line is shifted into the scroll-back buffer. + */ + public void scrollViewportDown(boolean intoScrollBack) { + if (scrollStart == scrollEnd) { + return; + } + VtLine temp; + if (intoScrollBack && scrollStart == 0 && maxScrollBack > 0) { + temp = scrollBack.size() >= maxScrollBack ? scrollBack.remove() : null; + scrollBack.add(lines.remove(0)); + } + else { + temp = lines.remove(scrollStart); + } + + if (temp == null) { + temp = new VtLine(cols); + } + else { + temp.reset(cols); + } + lines.add(scrollEnd - 1, temp); // Account for removed line + } + + /** + * Scroll the viewport up a line + * + *

+ * The lines are shifted downward. The line at the bottom of the viewport is removed, and a + * blank line is inserted at the top of the viewport. + */ + public void scrollViewportUp() { + VtLine temp = lines.remove(scrollEnd - 1); + temp.reset(cols); + lines.add(scrollStart, temp); + } + + /** + * If the cursor is beyond the bottom of the display, scroll the viewport down and move the + * cursor up until the cursor is at the bottom of the display. If applicable, lines at the top + * of the display is shifted into the scroll-back buffer. + */ + public void checkVerticalScroll() { + while (curY >= scrollEnd) { + scrollViewportDown(true); + curY = Math.max(0, curY - 1); + } + } + + /** + * Move the cursor up n rows + * + *

+ * The cursor cannot move above the top of the display. The value of n must be positive, + * otherwise behavior is undefined. To move the cursor down, use {@link #moveCursorDown(int)}. + * + * @param n the number of rows to move the cursor up + */ + public void moveCursorUp(int n) { + curY = Math.max(0, curY - n); + } + + /** + * Move the cursor down n rows + * + *

+ * If the cursor would move below the bottom of the display, the viewport will be scrolled so + * that the cursor remains in the display. The value of n must be positive, otherwise behavior + * is undefined. To move the cursor up, use {@link #moveCursorUp(int)}. + * + * @param n + */ + public void moveCursorDown(int n) { + curY += n; + checkVerticalScroll(); + } + + /** + * Move the cursor left (backward) n columns + * + *

+ * If the cursor would move left of the display, it is instead moved to the far right of the + * previous row, unless the cursor is already on the top row, in which case, it will be placed + * in the top-left corner of the display. NOTE: If the cursor is moved to the previous row, no + * heed is given to "leftovers." It doesn't matter how far to the left the cursor would have + * been; it is moved to the far right column and exactly one row up. The value of n must be + * positive, otherwise behavior is undefined. To move the cursor right, use + * {@link #moveCursorRight(int)}. + * + * @param n the number of columns + */ + public void moveCursorLeft(int n) { + if (curX - n >= 0) { + curX -= n; + } + else if (curY > 0) { + curX = cols - 1; + curY--; + } + } + + /** + * Move the cursor right (forward) n columns + * + *

+ * If the cursor would move right of the display, it is instead moved to the far left of the + * next row. If the cursor is already on the bottom row, the viewport is scrolled down a line. + * NOTE: If the cursor is moved to the next row, no heed is given to "leftovers." It doesn't + * matter how far to the right the cursor would have been; it is moved to the far left column + * and exactly one row down. The value of n must be positive, otherwise behavior is undefined. + * To move the cursor left, use {@link #moveCursorLeft(int)}. + * + * @param n the number of columns + */ + public void moveCursorRight(int n) { + curX += n; + if (curX >= cols) { + curX = 0; + curY++; + } + checkVerticalScroll(); + } + + /** + * Save the current cursor position + * + *

+ * There is only one slot for the saved cursor. It is not a stack or anything fancy. To restore + * the cursor, use {@link #restoreCursorPos()}. The advantage to using this vice + * {@link #getCurX()} and {@link #getCurY()} to save it externally, is that the buffer will + * adjust the saved position if the buffer is resized via {@link #resize(int, int)}. + */ + public void saveCursorPos() { + savedX = curX; + savedY = curY; + } + + /** + * Restore a saved cursor position + * + *

+ * If there was no previous call to {@link #saveCursorPos()}, the cursor is placed at the + * top-left of the display. + */ + public void restoreCursorPos() { + curX = savedX; + curY = savedY; + } + + /** + * Move the cursor to the given row and column + * + *

+ * The position is clamped to the dimensions of the display. No scrolling will take place if + * {@code col} exceeds the number of rows. + * + * @param row the desired row, 0 up, top to bottom + * @param col the desired column, 0 up, left to right + */ + public void moveCursor(int row, int col) { + this.curX = Math.max(0, Math.min(cols - 1, col)); + this.curY = Math.max(0, Math.min(rows - 1, row)); + } + + /** + * Get the cursor's current attributes + * + *

+ * Characters put into the buffer via {@link #putChar(char)} are assigned the cursor's current + * attributes at the time they are inserted. + * + * @see #setAttributes(VtAttributes) + * @return the current attributes + */ + public VtAttributes getAttributes() { + return curAttrs; + } + + /** + * Set the cursor's current attributes + * + *

+ * These are usually the attributes given by the ANSI SGR control sequences. They may not affect + * the display of the cursor itself, but rather of the characters placed at the cursor via + * {@link #putChar(char)}. NOTE: Not all attributes are necessarily supported by the renderer. + * + * @param attributes the desired attributes + */ + public void setAttributes(VtAttributes attributes) { + this.curAttrs = attributes == null ? VtAttributes.DEFAULTS : attributes; + } + + /** + * Erase (clear) some portion of the display buffer + * + *

+ * If the current line is erased from start to the cursor, the cursor's attributes are applied + * to the cleared columns. + * + * @param erasure specifies what, relative to the cursor, to erase. + */ + public void erase(Erasure erasure) { + switch (erasure) { + case TO_DISPLAY_END: + for (int y = curY; y < rows; y++) { + VtLine line = lines.get(y); + if (y == curY) { + line.clearToEnd(curX); + } + else { + line.clear(); + } + } + return; + case TO_DISPLAY_START: + for (int y = 0; y <= curY; y++) { + VtLine line = lines.get(y); + if (y == curY) { + line.clearToStart(curX, curAttrs); + } + else { + line.clear(); + } + } + return; + case FULL_DISPLAY: + for (VtLine line : lines) { + line.clear(); + } + return; + case FULL_DISPLAY_AND_SCROLLBACK: + for (VtLine line : lines) { + line.clear(); + } + scrollBack.clear(); + return; + case TO_LINE_END: + lines.get(curY).clearToEnd(curX); + return; + case TO_LINE_START: + lines.get(curY).clearToStart(curX, curAttrs); + return; + case FULL_LINE: + lines.get(curY).clear(); + return; + } + } + + /** + * Insert n blank lines at the cursor + * + *

+ * Lines at the bottom of the viewport are removed and all the lines between the cursor and the + * bottom of the viewport are shifted down, to make room for n blank lines. None of the lines + * above the cursor are affected, including those in the scroll-back buffer. + * + * @param n the number of lines to insert + */ + public void insertLines(int n) { + for (int i = 0; i < n; i++) { + VtLine temp = lines.remove(scrollEnd - 1); + temp.reset(cols); + lines.add(curY, temp); + } + } + + /** + * Delete n lines at the cursor + * + *

+ * Lines at (and immediately below) the cursor are removed and all lines between the cursor and + * the bottom of the viewport are shifted up to make room for n blank lines inserted at (and + * above) the bottom of the viewport. None of the lines above the cursor are affected. + * + * @param n the number of lines to delete + */ + public void deleteLines(int n) { + for (int i = 0; i < n; i++) { + VtLine temp = lines.remove(curY); + temp.reset(cols); + lines.add(scrollEnd - 1, temp); // account for removed index + } + } + + /** + * Insert n blank characters at the cursor + * + *

+ * Any characters right the cursor on the same line are shifted right to make room and n blanks + * are inserted at (and to the right) of the cursor. No wrapping occurs. Characters that would + * be moved or inserted right of the display buffer are effectively deleted. The cursor is + * not moved after this operation. + * + * @param n the number of blanks to insert. + */ + public void insertChars(int n) { + lines.get(curY).insert(curX, n); + } + + /** + * Delete n characters at the cursor + * + *

+ * Characters at (and {@code n-1} to the right) of the cursor are deleted. The remaining + * characters to the right are shifted left {@code n} columns. + * + * @param n the number of characters to delete + */ + public void deleteChars(int n) { + lines.get(curY).delete(curX, curX + n); + } + + /** + * Erase n characters at the cursor + * + *

+ * Characters at (and {@code n-1} to the right) of the cursor are erased, i.e., replaced with + * blanks. No shifting takes place. + * + * @param n the number of characters to erase + */ + public void eraseChars(int n) { + lines.get(curY).erase(curX, curX + n, curAttrs); + } + + /** + * Specify the scrolling viewport of the buffer + * + *

+ * By default, the viewport is the entire display, and scrolling the viewport downward may cause + * lines to enter the scroll-back buffer. The buffer manages these boundaries so that they can + * be updated on calls to {@link #resize(int, int)}. Both parameters are optional, though + * {@code end} should likely only be given if {@code start} is also given. The parameters are + * silently adjusted to ensure that both are within the bounds of the display and so that the + * end is at or below the start. Once set, the cursor should remain within the viewport, or + * otherwise cause the viewport to scroll. Operations that would cause the display to scroll, + * instead cause just the viewport to scroll. Additionally, cursor movement operations are + * clamped to the viewport. + * + * @param start the first line in the viewport, 0 up, top to bottom, inclusive. If omitted, this + * is the top line of the display. + * @param end the last line in the viewport, 0 up, top to bottom, inclusive. If omitted, this is + * the bottom line of the display. + */ + public void setScrollViewport(Integer start, Integer end) { + if (start != null) { + scrollStart = Math.max(0, start); + } + else { + scrollStart = 0; + } + if (end != null) { + // scrollEnd is exclusive + scrollEnd = Math.max(scrollStart + 1, Math.min(rows, end + 1)); + } + else { + scrollEnd = rows; + } + } + + /** + * Resize the buffer to the given number of rows and columns + * + *

+ * The viewport is reset to include the full display. Each line, including those in the + * scroll-back buffer are resized to match the requested number of columns. If the row count is + * decreasing, lines at the top of the display are be shifted into the scroll-back buffer. If + * the row count is increasing, lines at the bottom of the scroll-back buffer are shifted into + * the display buffer. The scroll-back buffer may be culled if the resulting number of lines + * exceeds that scroll-back maximum. The cursor position is adjusted so that, if possible, it + * remains on the same line. (The cursor cannot enter the scroll-back region.) Finally, the + * cursor is clamped into the display region. The saved cursor, if applicable, is similarly + * treated. + * + * @param cols the number of columns + * @param rows the number of rows + * @return true if the buffer was actually resized + */ + public boolean resize(int cols, int rows) { + cols = Math.max(1, cols); + rows = Math.max(1, rows); + + if (this.rows == rows && this.cols == cols) { + return false; + } + + for (VtLine line : scrollBack) { + line.resize(cols); + } + for (VtLine line : lines) { + line.resize(cols); + } + this.rows = rows; + this.cols = cols; + this.scrollStart = 0; + this.scrollEnd = rows; + + while (lines.size() < rows) { + lines.add(0, scrollBack.isEmpty() ? new VtLine(cols) : scrollBack.pollLast()); + curY++; + savedY++; + } + while (lines.size() > rows) { + scrollBack.addLast(lines.remove(0)); + curY--; + savedY--; + } + while (scrollBack.size() > maxScrollBack) { + scrollBack.pollFirst(); + } + + curX = Math.min(curX, cols - 1); + savedX = Math.min(savedX, cols - 1); + + curY = Math.max(0, Math.min(curY, rows - 1)); + savedY = Math.max(0, Math.min(savedY, rows - 1)); + + return true; + } + + /** + * Adjust the maximum number of lines in the scroll-back buffer + * + *

+ * If the scroll-back buffer exceeds the given maximum, it is immediately culled. + * + * @param maxScrollBack the maximum number of scroll-back lines + */ + public void setMaxScrollBack(int maxScrollBack) { + this.maxScrollBack = maxScrollBack; + while (scrollBack.size() > maxScrollBack) { + scrollBack.pollFirst(); + } + } + + /** + * A callback for iterating over the lines of the buffer + */ + public interface LineConsumer { + /** + * Process a line of terminal text + * + * @param i the index of the line, optionally including scroll-back, 0 up, top to bottom + * @param y the vertical position of the line. For a scroll-back line, this is -1. + * Otherwise, this counts 0 up, top to bottom. + * @param t the line + * @see VtBuffer#forEachLine(boolean, LineConsumer) + */ + void accept(int i, int y, VtLine t); + } + + /** + * Perform an action on each line of terminal text, optionally including the scroll-back buffer. + * + * @param includeScrollBack true to include the scroll-back buffer + * @param action the action + */ + public void forEachLine(boolean includeScrollBack, LineConsumer action) { + int i = 0; + if (includeScrollBack) { + for (VtLine line : scrollBack) { + action.accept(i, -1, line); + i++; + } + } + int y = 0; + for (VtLine line : lines) { + action.accept(i, y, line); + i++; + y++; + } + } + + /** + * Get the total number of lines, including scroll-back lines, in the buffer + * + *

+ * This is equal to {@link #getScrollBackSize()}{@code +}{@link #getRows()}. + * + * @return the number of lines + */ + public int size() { + return scrollBack.size() + rows; + } + + /** + * Get the number of lines in the scroll-back buffer + * + * @return the number of lines + */ + public int getScrollBackSize() { + return scrollBack.size(); + } + + /** + * Get the cursor's column, 0 up, left to right + * + * @return the column + */ + public int getCurX() { + return curX; + } + + /** + * Get the cursor's row, 0 up, top to bottom + * + * @return the row + */ + public int getCurY() { + return curY; + } + + /** + * This is essentially the loop body for {@link #getText(int, int, int, int, CharSequence)}. It + * is factored into a separate method, because we need to loop over the scroll-back buffer as + * well as the display buffer, and we want the same body. + */ + protected boolean gatherLineText(StringBuilder sb, int startRow, int startCol, int endRow, + int endCol, int i, VtLine line, CharSequence lineSep) { + if (i < startRow) { + return false; + } + if (i == startRow && startRow == endRow) { + line.gatherText(sb, startCol, endCol); + return true; + } + if (i == startRow) { + line.gatherText(sb, startCol, cols); + sb.append(lineSep); + return false; + } + if (i == endRow) { + line.gatherText(sb, 0, endCol); + return true; + } + if (i > endRow) { + return true; + } + line.gatherText(sb, 0, cols); + sb.append(lineSep); + return false; + } + + /** + * Get the text between two locations in the buffer + * + *

+ * The buffer attempts to avoid extraneous space at the end of each line. This isn't always + * perfect and depends on how lines are cleared. If they are cleared using + * {@link #erase(Erasure)}, then the buffer will cull the trailing spaces resulting from the + * clear. If they are cleared using {@link #putChar(char)} passing a space {@code ' '}, then the + * inserted spaces will be included. In practice, this depends on the application controlling + * the terminal. + * + *

+ * Like the other methods, locations are specified 0 up, top to bottom, and left to right. + * Unlike the other methods, the ending character is excluded from the result. + * + * @param startRow the row for the starting location, inclusive + * @param startCol the column for the starting location, inclusive + * @param endRow the row for the ending location, inclusive + * @param endCol the column for the ending location, exclusive + * @param lineSep the line separator + * @return the text + */ + public String getText(int startRow, int startCol, int endRow, int endCol, + CharSequence lineSep) { + StringBuilder buf = new StringBuilder(); + int sbSize = scrollBack.size(); + if (startRow < sbSize) { + int i = 0; + for (VtLine line : scrollBack) { + if (gatherLineText(buf, startRow, startCol, endRow, endCol, i, line, lineSep)) { + break; + } + i++; + } + } + for (int i = Math.max(sbSize, startRow); i <= endRow; i++) { + VtLine line = lines.get(i - sbSize); + gatherLineText(buf, startRow, startCol, endRow, endCol, i, line, lineSep); + } + return buf.toString(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java new file mode 100644 index 0000000000..5ec2145ed6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java @@ -0,0 +1,111 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +/** + * A legacy style charset + * + *

+ * Finding the particulars for these online has not been fun, so these are implemented on an + * as-needed basis. There's probably a simple translation to some unicode code pages, since those + * seem to be ordered by some of these legacy character sets. The default implementation for each + * charset will just be equivalent to US-ASCII. There's a lot of plumbing missing around these, two. + * For example, I'm assuming that switching to "the alternate charset" means using G1 instead of G0. + * I've not read carefully enough to know how G2 or G3 are used. + * + *

+ * It'd be nice to just use UTF-8, but the application would have to agree. + */ +public enum VtCharset { + UK, + USASCII, + FINNISH, + SWEDISH, + GERMAN, + FRENCH_CANADIAN, + FRENCH, + ITALIAN, + SPANISH, + DUTCH, + GREEK, + TURKISH, + PORTUGESE, + HEBREW, + SWISS, + NORWEGIAN_DANISH, + + DEC_SPECIAL_LINES { + @Override + public char mapChar(char c) { + switch (c) { + case 'j': + return '\u2518'; // 1pt lower-right corner + case 'k': + return '\u2510'; // 1pt upper-right corner + case 'l': + return '\u250C'; // 1pt upper-left corner + case 'm': + return '\u2514'; // 1pt lower-left corner + case 'q': + return '\u2500'; // 1pt horizontal line + case 'x': + return '\u2502'; // 1pt vertical line + } + return super.mapChar(c); + } + }, + DEC_SUPPLEMENTAL, + DEC_TECHNICAL, + + DEC_HEBREW, + DEC_GREEK, + DEC_TURKISH, + DEC_SUPPLEMENTAL_GRAPHICS, + DEC_CYRILLIC, + ; + + /** + * The designation for a charset slot + * + *

+ * It seems the terminal allows for the selection of 4 alternative charsets, the first of which + * G0 is the default or primary. + */ + public enum G { + G0('('), G1(')'), G2('*'), G3('-'); + + public final byte b; + + /** + * Construct a charset slot designator + * + * @param b the byte in the control sequence that identifies this slot + */ + private G(char b) { + this.b = (byte) b; + } + } + + /** + * Map a character, as decoded using US-ASCII, into the actual character for the character set. + * + * @param c the character from US-ASCII. + * @return the mapped character + */ + public char mapChar(char c) { + return c; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java new file mode 100644 index 0000000000..01bbd11be2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java @@ -0,0 +1,1682 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.PrimitiveIterator.OfInt; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.util.Msg; + +/** + * The handler of parsed ANSI VT control sequences + * + *

+ * Here are some of the resources where I found useful documentation: + * + *

+ * + *

+ * They were incredibly useful, even when experimentation was required to fill in details, because + * they at least described the sort of behavior I should be looking for. Throughout the referenced + * documents and within this documentation, the following abbreviations are used for escape + * sequences: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
AbbreviationSequenceJava String
{@code ESC}byte 0x1b{@code "\033"}
{@code CSI}{@code ESC [}{@code "\033["}
{@code OSC} + * {@code ESC ]}{@code "\033]"}
{@code ST} + * {@code ESC \}{@code "\033\\"}
{@code BEL} + * byte 0x07{@code "\007"}
+ * + *

+ * The separation between the parser and the handler deals in state management. The parser manages + * state only of the control sequence parser itself, i.e., the current node in the token parsing + * state machine. The state of the terminal, e.g., the current attributes, cursor position, etc., + * are managed by the handler and its delegates. + * + *

+ * For example, the Cursor Position sequence is documented as: + *

+ * CSI n ; m H + *

+ * Supposing {@code n} is 13 and {@code m} is 40, this sequence would be encoded as the string + * {@code "\033[13;40H"}. The parser will handle decoding the CSI, parameters, and final byte + * {@code 'H'}. It will then invoke {@link #handleCsi(ByteBuffer, ByteBuffer, byte)}. The default + * implementation provided by this interface handles many of the final bytes, including {@code 'H'}. + * It will thus invoke the abstract {@link #handleMoveCursor(int, int)} method passing 12 and 39. + * Note that 1 is subtracted from both parameters, because ANSI specifies 1-up indexing while Java + * lends itself to 0-up indexing. + * + *

+ * The XTerm documentation, which is arguably the most thorough, presents the CSI commands + * alphabetically by the final byte, in ASCII order. For sanity and consistency, we adopt the same + * ordering in our switch cases. + */ +public interface VtHandler { + /** + * Use for initializing static final byte array fields from an ASCII-encoded string + * + * @param str the string + * @return the encoded bytes + */ + static byte[] ascii(String str) { + try { + return str.getBytes("ASCII"); + } + catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + // Various parameters for the 'h' and 'l' final CSI bytes + + public static final byte[] _4 = ascii("4"); + public static final byte[] Q1 = ascii("?1"); + public static final byte[] Q7 = ascii("?7"); + public static final byte[] Q12 = ascii("?12"); + public static final byte[] Q25 = ascii("?25"); + public static final byte[] Q1000 = ascii("?1000"); + public static final byte[] Q1004 = ascii("?1004"); + public static final byte[] Q1034 = ascii("?1034"); + public static final byte[] Q1047 = ascii("?1047"); + public static final byte[] Q1048 = ascii("?1048"); + public static final byte[] Q1049 = ascii("?1049"); + public static final byte[] Q2004 = ascii("?2004"); + + /** + * An ANSI color specification + * + *

+ * We avoid going straight to AWT colors, 1) Because it provides better separation between the + * terminal logic and the rendering framework, and 2) Because some specifications, e.g., default + * background, are better delayed until the renderer has gathered the necessary context to + * resolve it. Various enums and records implement this interface to provide the specifcations. + */ + public interface AnsiColor { + } + + /** + * A singleton representing the default color + * + *

+ * The actual color selected will depend on context and use. Most notably, the default color + * used for foreground should greatly contrast the default color used for the background. + */ + public enum AnsiDefaultColor implements AnsiColor { + INSTANCE; + } + + /** + * One of the eight standard ANSI colors + * + *

+ * The actual color may be modified by other SGR attributes, notably {@link Intensity}. For + * colors that are described by hue, some thought should be given to how the standard and + * intense versions differ. Some palettes may choose a darker color, reserving the brightest for + * the intense version. Others may use the brightest, choosing to whiten the intense version. + */ + public enum AnsiStandardColor implements AnsiColor { + /** + * Usually the darkest black available. Implementations may select a color softer on the + * eyes, depending on use. For foreground, this should likely be true black (0,0,0). + */ + BLACK, + /** + * A color whose hue is clearly red. + */ + RED, + /** + * A color whose hue is clearly green. + */ + GREEN, + /** + * A color whose hue is clearly yellow. + */ + YELLOW, + /** + * A color whose hue is clearly blue. For palettes made to display on a dark (but not black) + * background, a hue tinted toward cyan is recommended. + */ + BLUE, + /** + * A color whose hue is clearly magenta or purple. For palettes made to display on a dark + * (but not black) background, a hue tinted toward red is recommended. + */ + MAGENTA, + /** + * A color whose hue is clearly cyan. + */ + CYAN, + /** + * A relatively bright white, sparing the brightest for intense white. + */ + WHITE; + + /** + * An unmodifiable list giving all the standard colors + */ + public static final List ALL = List.of(AnsiStandardColor.values()); + + /** + * Get the standard color for the given numerical code + * + *

+ * For example, the sequence {@code CSI [ 34 m} would use code 4 (blue). + * + * @param code the code + * @return the color + */ + public static AnsiStandardColor get(int code) { + return ALL.get(code); + } + } + + /** + * One of the eight ANSI intense colors + * + *

+ * Note that intense colors may also be specified using the standard color with the + * {@link Intensity#BOLD} attribute, depending on the command sequence. + */ + public enum AnsiIntenseColor implements AnsiColor { + /** + * A relatively dark grey, but not true black. + */ + BLACK, + /** + * See {@link AnsiStandardColor#RED}, but brighter and/or whiter. + */ + RED, + /** + * See {@link AnsiStandardColor#GREEN}, but brighter and/or whiter. + */ + GREEN, + /** + * See {@link AnsiStandardColor#YELLOW}, but brighter and/or whiter. + */ + YELLOW, + /** + * See {@link AnsiStandardColor#BLUE}, but brighter and/or whiter. + */ + BLUE, + /** + * See {@link AnsiStandardColor#MAGENTA}, but brighter and/or whiter. + */ + MAGENTA, + /** + * See {@link AnsiStandardColor#CYAN}, but brighter and/or whiter. + */ + CYAN, + /** + * Usually the brightest white available. + */ + WHITE; + + /** + * An unmodifiable list giving all the intense colors + */ + public static final List ALL = List.of(AnsiIntenseColor.values()); + + /** + * Get the intense color for the given numerical code + * + *

+ * For example, the sequence {@code CSI [ 94 m} would use code 4 (blue). + * + * @param code the code + * @return the color + */ + public static AnsiIntenseColor get(int code) { + return ALL.get(code); + } + } + + /** + * For 8-bit colors, one of the 216 colors from the RGB cube + * + *

+ * The r, g, and b fields give the "step" number from 0 to 5, dimmest to brightest. + */ + public record Ansi216Color(int r, int g, int b) implements AnsiColor { + } + + /** + * For 8-bit colors, one of the 24 grays + * + *

+ * The v field is a value from 0 to 23, 0 being the dimmest, but not true black, and 23 being + * the brightest, but not true white. + */ + public record AnsiGrayscaleColor(int v) implements AnsiColor { + } + + /** + * A 24-bit color + * + *

+ * The r, g, and b fields are values from 0 to 255 dimmest to brightest. + */ + public record Ansi24BitColor(int r, int g, int b) implements AnsiColor { + } + + /** + * Modifies the intensity of the character either by color or by font weight. + * + *

+ * The renderer may choose a combination of strategies. For example, {@link #NORMAL} may be + * rendered using the standard color and bold type. Then {@link #BOLD} would use the intense + * color, keeping the bold type; whereas {@link #DIM} would use normal type, keeping the + * standard color. Some user configuration may be desired here. + */ + public enum Intensity { + /** + * The default intensity + */ + NORMAL, + /** + * More intense than {@link #NORMAL} + */ + BOLD, + /** + * Less intense than {@link #NORMAL} + */ + DIM; + } + + /** + * Modifies the shape of the font + */ + public enum AnsiFont { + /** + * The default font + */ + NORMAL, + /** + * Slanted or Italic font + */ + ITALIC, + /** + * Black letter or Fraktur font (hardly ever used) + */ + BLACK_LETTER; + } + + /** + * Places lines under the text + */ + public enum Underline { + /** + * The default, no underlines + */ + NONE, + /** + * A single underline + */ + SINGLE, + /** + * Double underlines + */ + DOUBLE; + } + + /** + * Causes text to blink + * + *

+ * If implemented, renderers should take care not to irritate the user. One option is to make + * {@link #FAST} actually slow, and {@link #SLOW} even slower. Another option is to only blink + * for a relatively short period after displaying the text, or perhaps only when the terminal + * has focus. + */ + public enum Blink { + /** + * The default, no blinking + */ + NONE, + /** + * Slow blinking + */ + SLOW, + /** + * Fast blinking + */ + FAST; + } + + /** + * A direction for relative cursor movement + */ + public enum Direction { + /** + * Up a line or row + */ + UP, + /** + * Down a line or row + */ + DOWN, + /** + * Forward or right a character or column + */ + FORWARD, + /** + * Backward or left a character or column + */ + BACK; + + /** + * Derive the direction from the final byte of the CSI sequence + * + * @param b the final byte + * @return the direction + */ + public static Direction forCsiFinal(byte b) { + return switch (b) { + case 'A' -> UP; + case 'B' -> DOWN; + case 'C' -> FORWARD; + case 'D' -> BACK; + default -> throw new AssertionError(); + }; + } + } + + /** + * An enumeration of erasure specifications + */ + public enum Erasure { + /** + * Erase the current line from the cursor to the end, including the cursor's current column + */ + TO_LINE_END, + /** + * Erase the current line from the start to the cursor, including the cursor's current + * column + */ + TO_LINE_START, + /** + * Erase the current line, entirely + */ + FULL_LINE, + /** + * Erase the current line from the cursor to the end, including the cursor's current column, + * as well as all lines after the current line. + */ + TO_DISPLAY_END, + /** + * Erase the current line from the start to the cursor, including the cursor's current + * column, as well as all lines before the current line. This excludes the scroll-back + * buffer. + */ + TO_DISPLAY_START, + /** + * Erase the entire display, except the scroll-back buffer. + */ + FULL_DISPLAY, + /** + * Erase the entire display, including the scroll-back buffer. + */ + FULL_DISPLAY_AND_SCROLLBACK; + + /** + * Derive the erasure specification from the parameter to the Erase Display (ED) control + * sequence + * + * @param n the parameter + * @return the erasure specification + */ + public static Erasure fromED(int n) { + return switch (n) { + case 0 -> TO_DISPLAY_END; + case 1 -> TO_DISPLAY_START; + case 2 -> FULL_DISPLAY; + case 3 -> FULL_DISPLAY_AND_SCROLLBACK; + default -> TO_DISPLAY_END; + }; + } + + /** + * Derive the erasure specification from the parameter to the Erase Line (EL) control + * sequence + * + * @param n the parameter + * @return the erasure specification + */ + public static Erasure fromEL(int n) { + return switch (n) { + case 0 -> TO_LINE_END; + case 1 -> TO_LINE_START; + case 2 -> FULL_LINE; + default -> TO_LINE_END; + }; + } + } + + /** + * Check if the given buffer's contents are equal to that of the given array + * + * @param buf the buffer + * @param arr the array + * @return true if equal, false otherwise + */ + static boolean bufEq(ByteBuffer buf, byte[] arr) { + return Arrays.equals(buf.array(), buf.position(), buf.limit(), arr, 0, arr.length); + } + + /** + * Render a character and its byte value as a string, used for diagnostics + * + * @param b the byte/character to examine + * @return the string + */ + static String charInfo(byte b) { + return Character.toString(b) + " (" + Integer.toHexString(b & 0xff) + ")"; + } + + /** + * Decode the byte buffer's contents to an ASCII string, used for diagnostics. + * + * @param buf the buffer to examine + * @return the string + */ + static String strBuf(ByteBuffer buf) { + byte[] arr = new byte[buf.remaining()]; + buf.get(buf.position(), arr); + try { + return new String(arr, "ASCII"); + } + catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + /** + * Handle normal character output, i.e., place the character on the display + * + *

+ * This excludes control sequences and control characters, e.g., tab, line feed. While we've not + * tested, in theory, this can instead buffer the byte for decoding from UTF-8. Still, the + * implementation should eagerly decode, rendering characters as soon as they are available. + * + * @param b the byte/character + * @throws Exception if anything goes wrong + */ + void handleChar(byte b) throws Exception; + + /** + * Handle a character not part of an escape sequence. + * + *

+ * This may include control characters, which are displatched appropriately by this method. + * Additionally, this handles any exception thrown by {@link #handleChar(byte)}. + * + * @param b the byte/character + */ + default void handleCharExc(byte b) { + try { + switch (b) { + case 7: + handleBell(); + return; + case 8: + handleBackSpace(); + return; + case 9: + handleTab(); + return; + case 10: + handleLineFeed(); + return; + case 13: + handleCarriageReturn(); + return; + case 14: + handleAltCharset(true); + return; + case 15: + handleAltCharset(false); + return; + } + handleChar(b); + } + catch (Exception e) { + Msg.error(this, "Exception handling terminal character output " + charInfo(b) + ":" + e, + e); + } + } + + /** + * Parse a sequence of integers in the form n ; m ; .... + * + *

+ * This is designed to replace the {@link String#split(String)} and + * {@link Integer#parseInt(String)} pattern, which should avoid some unnecessary object + * creation. Unfortunately, the iterator itself is still an object.... Each parameter is parsed + * on demand. + * + * @param csiParam the buffer of characters containing the parameters to parse + * @return an iterator of integers + */ + static OfInt parseCsiInts(ByteBuffer csiParam) { + ByteBuffer buf = csiParam.duplicate(); + return new OfInt() { + int next = prepareNext(); + + private int prepareNext() { + if (!buf.hasRemaining()) { + return -1; + } + int value = 0; + while (buf.hasRemaining()) { + byte b = buf.get(); + if ('0' <= b && b <= '9') { + value = value * 10 + (b - '0'); + } + else if (b == ';' || b == ':') { + return value; + } + else { + throw new UnknownCsiException(); + } + } + return value; + } + + @Override + public boolean hasNext() { + return next != -1; + } + + @Override + public int nextInt() { + int ret = next; + next = prepareNext(); + return ret; + } + }; + } + + /** + * An exception for when a CSI sequence is not implemented or recognized + */ + class UnknownCsiException extends RuntimeException { + } + + /** + * Handle the parameters for a 'h' or 'l' final byte CSI sequence + * + * @param csiParam the parameter buffer + * @param en true for 'h', which generally enables things, and false for 'l' + */ + default void handleHOrLStuff(ByteBuffer csiParam, boolean en) { + if (bufEq(csiParam, _4)) { + handleInsertMode(en); + } + else if (bufEq(csiParam, Q1)) { + handleApplicationCursorKeys(en); + } + else if (bufEq(csiParam, Q7)) { + handleAutoWrapMode(en); + } + else if (bufEq(csiParam, Q12)) { + handleBlinkCursor(en); + } + else if (bufEq(csiParam, Q25)) { + handleShowCursor(en); + } + else if (bufEq(csiParam, Q1000)) { + handleReportMouseEvents(en, en); + } + else if (bufEq(csiParam, Q1004)) { + handleReportFocus(en); + } + else if (bufEq(csiParam, Q1034)) { + handleMetaKey(en); + } + else if (bufEq(csiParam, Q1047)) { + handleAltScreenBuffer(en, false); + } + else if (bufEq(csiParam, Q1048)) { + if (en) { + handleSaveCursorPos(); + } + else { + handleRestoreCursorPos(); + } + } + else if (bufEq(csiParam, Q1049)) { + // TODO: I'm already using a separate cursor per buffer.... + if (en) { + handleSaveCursorPos(); + handleAltScreenBuffer(en, true); + } + else { + handleAltScreenBuffer(en, true); + handleRestoreCursorPos(); + } + } + else if (bufEq(csiParam, Q2004)) { + handleBracketedPasteMode(en); + } + else { + throw new UnknownCsiException(); + } + } + + /** + * Handle XTerm CSI commands that manipulate the window titles + * + * @param csiParam the buffer of parameters + */ + default void handleWindowManipulation(ByteBuffer csiParam) { + OfInt bits = parseCsiInts(csiParam); + if (!bits.hasNext()) { + throw new UnknownCsiException(); + } + switch (bits.nextInt()) { + case 22: { + switch (bits.nextInt()) { + case 0: { + handleSaveIconTitle(); + handleSaveWindowTitle(); + return; + } + case 1: { + handleSaveIconTitle(); + return; + } + case 2: { + handleSaveWindowTitle(); + return; + } + default: { + throw new UnknownCsiException(); + } + } + } + case 23: { + switch (bits.nextInt()) { + case 0: { + handleRestoreIconTitle(); + handleRestoreWindowTitle(); + return; + } + case 1: { + handleRestoreIconTitle(); + return; + } + case 2: { + handleRestoreWindowTitle(); + return; + } + default: { + throw new UnknownCsiException(); + } + } + } + default: { + throw new UnknownCsiException(); + } + } + } + + /** + * Handle a CSI sequence + * + * @param csiParam the parameter buffer + * @param csiInter the intermediate buffer + * @param csiFinal the final byte + * @throws Exception if anything goes wrong + */ + default void handleCsi(ByteBuffer csiParam, ByteBuffer csiInter, byte csiFinal) + throws Exception { + try { + switch (csiFinal) { + case '@': { // Insert characters + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleInsertCharacters(n); + return; + } + case 'A': // Cursor up + case 'B': // Cursor down + case 'C': // Cursor forward + case 'D': /* Cursor back */ { + Direction dir = Direction.forCsiFinal(csiFinal); + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleMoveCursor(dir, n); + return; + } + case 'G': { // Cursor character absolute + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleMoveCursorCol(n - 1); + return; + } + case 'H': { // Cursor position + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + int m = bits.hasNext() ? bits.nextInt() : 1; + handleMoveCursor(n - 1, m - 1); + return; + } + case 'J': { // Erase in display + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 0; + handleErase(Erasure.fromED(n)); + return; + } + case 'K': { // Erase in line + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 0; + handleErase(Erasure.fromEL(n)); + return; + } + case 'L': { // Insert lines + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleInsertLines(n); + return; + } + case 'M': { // Delete lines + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleDeleteLines(n); + return; + } + case 'P': { // Delete characters + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleDeleteCharacters(n); + return; + } + case 'S': { // Scroll up lines + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleScrollLinesUp(n, false); + return; + } + case 'T': { // Scroll down lines + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleScrollLinesDown(n); + return; + } + case 'X': { // Erase characters + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleEraseCharacters(n); + return; + } + case 'Z': { // Cursor backward tabulation + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleBackwardTab(n); + return; + } + case 'c': { // Send Device Attributes + Msg.trace(this, "TODO: Send Device Attributes"); + return; + } + case 'd': { // Line position absolute + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleMoveCursorRow(n - 1); + return; + } + case 'h': { + handleHOrLStuff(csiParam, true); + return; + } + case 'l': { + handleHOrLStuff(csiParam, false); + return; + } + case 'm': { // Select Graphic Rendition (SGR) + if (csiParam.hasRemaining()) { + switch (csiParam.get(csiParam.position())) { + case '>': // Set key modifier options + Msg.trace(this, "TODO: Set key modifier options"); + return; + case '?': // Query key modifier options + Msg.trace(this, "TODO: Query key modifier options"); + return; + } + } + OfInt bits = parseCsiInts(csiParam); + if (!bits.hasNext()) { + handleResetAttributes(); + } + while (bits.hasNext()) { + handleSgrAttribute(bits); + } + return; + } + case 'n': { // Device Status Report + OfInt bits = parseCsiInts(csiParam); + if (!bits.hasNext()) { + throw new UnknownCsiException(); + } + switch (bits.nextInt()) { + case 6: // Report Cursor Position + handleReportCursorPos(); + return; + case 5: // Status Report (Not implemented) + default: + throw new UnknownCsiException(); + } + } + case 'p': { // Soft terminal reset + // TODO: Not sure how/if this should differ from "full" reset + handleFullReset(); + return; + } + case 'r': { // Scroll screen + OfInt bits = parseCsiInts(csiParam); + Integer start = bits.hasNext() ? bits.nextInt() - 1 : null; + Integer end = bits.hasNext() ? bits.nextInt() - 1 : null; + handleSetScrollRange(start, end); + return; + } + case 's': { + handleSaveCursorPos(); + return; + } + case 't': { // Window manipulation + handleWindowManipulation(csiParam); + return; + } + case 'u': { + handleRestoreCursorPos(); + return; + } + default: { + throw new UnknownCsiException(); + } + } + } + catch (UnknownCsiException e) { + Msg.error(this, "Unknown CSI sequence: param:'" + + strBuf(csiParam) + "' inter:'" + strBuf(csiInter) + "' final:" + + charInfo(csiFinal)); + } + return; + } + + /** + * Handle a CSI sequence, printing any exception + * + * @see #handleCsi(ByteBuffer, ByteBuffer, byte) + */ + default void handleCsiExc(ByteBuffer csiParam, ByteBuffer csiInter, byte csiFinal) { + try { + handleCsi(csiParam, csiInter, csiFinal); + } + catch (Exception e) { + Msg.error(this, "Exception handling terminal CSI sequence", e); + } + } + + /** + * An exception for when an OSC sequence is not implemented or recognized + */ + class UnknownOscException extends RuntimeException { + } + + /** Pattern for the OSC set window title sequence */ + Pattern PAT_OSC_WINDOW_TITLE = Pattern.compile("0;(?.*)"); + /** Pattern for the OSC color query sequence */ + Pattern PAT_OSC_COLOR_QUERY = Pattern.compile("1[0-9];\\?"); + // TODO: 104;c;c;c... is color reset. I've not implemented setting them, though. + // No c given = reset all + + /** + * Handle an OSC sequence + * + * @param oscParam the parameter buffer + * @throws Exception if anything goes wrong + */ + default void handleOsc(ByteBuffer oscParam) throws Exception { + try { + String paramStr = strBuf(oscParam); + Matcher matcher; + + matcher = PAT_OSC_WINDOW_TITLE.matcher(paramStr); + if (matcher.matches()) { + handleWindowTitle(matcher.group("title")); + return; + } + + matcher = PAT_OSC_COLOR_QUERY.matcher(paramStr); + if (matcher.matches()) { + Msg.trace(this, "TODO: OSC Color Query"); + return; + } + throw new UnknownOscException(); + } + catch (UnknownOscException e) { + Msg.error(this, "Unknown OSC sequence: param:'" + strBuf(oscParam) + "'"); + } + } + + /** + * Handle a OSC sequence, printing any exception + * + * @see #handleOsc(ByteBuffer) + */ + default void handleOscExc(ByteBuffer oscParam) { + try { + handleOsc(oscParam); + } + catch (Exception e) { + Msg.error(this, "Exception handling terminal OSC sequence", e); + } + } + + /** + * Decode an ANSI color specification + * + * @param colorCode the color code (0-7 for standard, 8 for extended, 9 for default + * @param bits the parameters for extended colors + * @param intensity the current intensity, if applicable + * @return the color specification + */ + default AnsiColor decodeColor(int colorCode, OfInt bits, Intensity intensity) { + if (colorCode < 8) { + return intensity == Intensity.BOLD + ? AnsiIntenseColor.get(colorCode) + : AnsiStandardColor.get(colorCode); + } + if (colorCode == 8) { + return decodeExtendedColor(bits); + } + if (colorCode == 9) { + return AnsiDefaultColor.INSTANCE; + } + Msg.error(this, "Unrecognized color code: " + colorCode); + return null; + } + + /** + * Decode an extended ANSI color specification + * + * @param bits the parameters + * @return the color specification + */ + default AnsiColor decodeExtendedColor(OfInt bits) { + if (!bits.hasNext()) { + Msg.error(this, "Missing color type in extended color code"); + return null; + } + int type = bits.nextInt(); + if (type == 5) { + if (!bits.hasNext()) { + return null; + } + return decode8BitColor(bits.nextInt()); + } + if (type == 2) { + if (!bits.hasNext()) { + return null; + } + int r = bits.nextInt(); + if (!bits.hasNext()) { + return null; + } + int g = bits.nextInt(); + if (!bits.hasNext()) { + return null; + } + int b = bits.nextInt(); + return new Ansi24BitColor(r, g, b); + } + Msg.error(this, "Unrecognized extended color type: " + type); + return null; + } + + /** + * Decode the 8-bit ANSI color. + * + * <p> + * Colors 0-15 are the standard + high-intensity. Colors 16-231 come from a 6x6x6 RGB cube. + * Finally, colors 232-255 are 24 steps of gray scale. + * + * @param code an 8-bit number + * @return the ANSI color + */ + default AnsiColor decode8BitColor(int code) { + if (code < 8) { + return AnsiStandardColor.get(code); + } + if (code < 16) { + return AnsiIntenseColor.get(code - 8); + } + if (code < 232) { + code -= 16; + int b = code % 6; + int g = (code / 6) % 6; + int r = (code / 36) % 6; + return new Ansi216Color(r, g, b); + } + if (code < 256) { + return new AnsiGrayscaleColor(code); + } + Msg.warn(this, "Invalid 8-bit color code: " + code); + return null; + } + + /** + * Handle an Select Graphics Rendition attribute (final byte 'm') + * + * @param bits the parameters + */ + default void handleSgrAttribute(OfInt bits) { + int code = bits.nextInt(); + if (30 <= code && code < 50) { + int colorCode = code % 10; + AnsiColor color = decodeColor(colorCode, bits, Intensity.NORMAL); + if (code < 40) { + handleForegroundColor(color); + } + else { + handleBackgroundColor(color); + } + return; + } + if (90 <= code && code < 110) { + int colorCode = code % 10; + AnsiColor color = decodeColor(colorCode, bits, Intensity.BOLD); + if (code < 100) { + handleForegroundColor(color); + } + else { + handleBackgroundColor(color); + } + return; + } + switch (code) { + case 0: + handleResetAttributes(); + return; + case 1: + handleIntensity(Intensity.BOLD); + return; + case 2: + handleIntensity(Intensity.DIM); + return; + case 3: + handleFont(AnsiFont.ITALIC); + return; + case 4: + handleUnderline(Underline.SINGLE); + return; + case 5: + handleBlink(Blink.SLOW); + return; + case 6: + handleBlink(Blink.FAST); + return; + case 7: + handleReverseVideo(true); + return; + case 8: + handleHidden(true); + return; + case 9: + handleStrikeThrough(true); + return; + case 20: + handleFont(AnsiFont.BLACK_LETTER); + return; + case 21: + handleUnderline(Underline.DOUBLE); + return; + case 22: + handleIntensity(Intensity.NORMAL); + return; + case 23: + handleFont(AnsiFont.NORMAL); + return; + case 24: + handleUnderline(Underline.NONE); + return; + case 25: + handleBlink(Blink.NONE); + return; + case 26: + handleProportionalSpacing(true); + return; + case 27: + handleReverseVideo(false); + return; + case 28: + handleHidden(false); + return; + case 29: + handleStrikeThrough(false); + return; + default: + Msg.warn(this, "Unrecognized SGR attribute: " + code); + return; + } + } + + /** + * Alert the user, typically with an audible "ding" or "beep." Alternatively, a gentle visual + * alert may be used. + */ + void handleBell(); + + /** + * Handle the backspace control code (0x08), usually just move the cursor left one. + */ + void handleBackSpace(); + + /** + * Handle the tab control code (0x09), usually just move the cursor to the next tab stop. + */ + void handleTab(); + + /** + * Handle the backward tab sequence: move the cursor backward n tab stops. + * + * @param n + */ + void handleBackwardTab(int n); + + /** + * Handle the line feed control code (0x0a), usually just move the cursor down one. + */ + void handleLineFeed(); + + /** + * Handle the carriage return control code (0x0d), usually just move the cursor to the start of + * the line. + */ + void handleCarriageReturn(); + + /** + * Handle toggling of the alternate character set. + * + * @param alt true for G1, false for G0 + */ + void handleAltCharset(boolean alt); + + /** + * Handle setting of the foreground color + * + * @param color the color specification + */ + void handleForegroundColor(AnsiColor color); + + /** + * Handle setting of the background color + * + * @param color the color specification + */ + void handleBackgroundColor(AnsiColor color); + + /** + * Handle resetting the SGR attributes + */ + void handleResetAttributes(); + + /** + * Handle setting the intensity + * + * @param intensity the intensity + */ + void handleIntensity(Intensity intensity); + + /** + * Handle setting the font + * + * @param font the font + */ + void handleFont(AnsiFont font); + + /** + * Handle setting the underline + * + * @param underline the underline + */ + void handleUnderline(Underline underline); + + /** + * Handle setting the blink + * + * @param blink the blink + */ + void handleBlink(Blink blink); + + /** + * Handle toggling of reverse video + * + * <p> + * This can be a bit confusing with default colors. In general, this means swapping the + * foreground and background color specifications (not inverting the colors or mirroring or some + * such). In the case of the default colors, the implementor must be sure to swap the meaning or + * "default background" and "default foreground." Furthermore, if "do not paint" is used for + * "default background," care must be taken to ensure the foreground is still painted in + * reversed mode. + * + * @param reverse true to reverse, false otherwise + */ + void handleReverseVideo(boolean reverse); + + /** + * Handle toggling of the hidden attribute + * + * @param hidden true to hide, false to show + */ + void handleHidden(boolean hidden); + + /** + * Handle setting strike-through + * + * @param strikeThrough true to strike, false for no strike + */ + void handleStrikeThrough(boolean strikeThrough); + + /** + * Handle setting proportional spacing + * + * @param spacing true for space proportionally, false otherwise + */ + void handleProportionalSpacing(boolean spacing); + + /** + * Handle toggling insert mode + * + * <p> + * In insert mode, characters at and to the right of the cursor are shifted right to make room + * for each new character. In replace mode (default), the character at the cursor is replaced + * with each new character. + * + * @param en true for insert, false for replace (default) + */ + void handleInsertMode(boolean en); + + /** + * Toggle application handling of the cursor keys + * + * @param en true (default) for application control, false for local control + */ + void handleApplicationCursorKeys(boolean en); + + /** + * Toggle application handling of the keypad + * + * @param en true for application control, false for local control + */ + void handleApplicationKeypad(boolean en); + + /** + * Toggle auto-wrap mode + * + * @param en true for auto-wrap, false otherwise + */ + void handleAutoWrapMode(boolean en); + + /** + * Toggle blinking of the cursor + * + * <p> + * Renderers should take care not to irritate the user. Some possibilities are to blink slowly, + * blink only for a short period of time after it moves, and/or blink only when the terminal has + * focus. + * + * @param blink true to blink, false to leave solid + */ + void handleBlinkCursor(boolean blink); + + /** + * Toggle display of the cursor + * + * @param show true to show the cursor, false to hide it. + */ + void handleShowCursor(boolean show); + + /** + * Toggle reporting of select mouse events + * + * @param press true to report mouse press events, false to not report them + * @param release true to report mouse release events, false to not report them + */ + void handleReportMouseEvents(boolean press, boolean release); + + /** + * Toggle reporting of terminal focus + * + * @param report true to report focus gain and loss events, false to not report them + */ + void handleReportFocus(boolean report); + + /** + * Toggle handling of the meta key + * + * @param en true to report the meta modifier in key/mouse events, false to exclude it + */ + void handleMetaKey(boolean en); + + /** + * Switch to and from the alternate screen buffer, optionally clearing it + * + * <p> + * This will never clear the normal buffer. If the buffer does not change as a result of this + * call, then the alternate buffer is not cleared, even if clearAlt is specified. + * + * @param alt true for alternate, false for normal + * @param clearAlt if switching, whether to clear the alternate buffer + */ + void handleAltScreenBuffer(boolean alt, boolean clearAlt); + + /** + * Toggle bracketed paste mode + * + * <p> + * See the XTerm documentation for motivation, but one example could be applications that have + * an undo stack. Without bracketed paste, the application could not recognize the pasted text + * as one undoable operation. + * + * @param en true to bracket pasted text is special control sequences + */ + void handleBracketedPasteMode(boolean en); + + /** + * Handle a request to save the cursor position + */ + void handleSaveCursorPos(); + + /** + * Handle a request to restore the previously-saved cursor position + */ + void handleRestoreCursorPos(); + + /** + * Handle a relative cursor movement command + * + * @param direction the direction + * @param n the number of rows or columns to move + */ + void handleMoveCursor(Direction direction, int n); + + /** + * Handle an absolute cursor movement command + * + * @param row the row (0-up) + * @param col the column (0-up) + */ + void handleMoveCursor(int row, int col); + + /** + * Handle an absolute cursor row movement command + * + * <p> + * The column should remain the same, i.e., do <em>not</em> reset the column to 0. + * + * @param row the row (0-up) + */ + void handleMoveCursorRow(int row); + + /** + * Handle an absolute cursor column movement command + * + * @param col the column (0-up) + */ + void handleMoveCursorCol(int col); + + /** + * Handle a request to report the cursor position + */ + void handleReportCursorPos(); + + /** + * Handle a request to save the terminal window's icon title + * + * <p> + * "Icon titles" are a concept from the X Windows system. Do the closest equivalent, if anything + * applies at all. The current title is pushed to a stack of limited size. + */ + void handleSaveIconTitle(); + + /** + * Handle a request to save the terminal window's title + * + * <p> + * Window titles are fairly applicable to all desktop windowing systems. The current title is + * pushed to a stack of limited size. + */ + void handleSaveWindowTitle(); + + /** + * Handle a request to restore the terminal window's icon title + * + * <p> + * The title is set to the one popped from the stack of saved window icon titles. + * + * @see #handleSaveIconTitle() + */ + void handleRestoreIconTitle(); + + /** + * Handle a request to restore the terminal window's title + * + * <p> + * The title is set to the one popped from the stack of saved window titles. + * + * @see #handleSaveWindowTitle() + */ + void handleRestoreWindowTitle(); + + /** + * Handle a request to set the terminal window's title + * + * @param title the titled + */ + void handleWindowTitle(String title); + + /** + * Handle a request to erase part of the display + * + * @param erasure what, relative to the cursor, to erase + */ + void handleErase(Erasure erasure); + + /** + * Insert n lines at and below the cursor + * + * <p> + * Lines within the viewport are shifted down or deleted to make room for the new lines. + * + * @param n the number of lines to insert + */ + void handleInsertLines(int n); + + /** + * Delete n lines at and below the cursor + * + * <p> + * Lines within the viewport are shifted up, and new lines inserted at the bottom. + * + * @param n the number of lines to delete + */ + void handleDeleteLines(int n); + + /** + * Delete n characters from the current cursor position, and shift the remaining characters + * back. If n is one, only the character at the cursor position is deleted. If n is greater, + * then additional characters are deleted after (to the right) of the cursor. Consider the + * current line contents and cursor position: + * + * <pre> + * 123456789 + * ^ + * </pre> + * + * Deleting 2 characters should result in {@code 1234789}. The character at the cursor (5) and + * the following character (6) are deleted. The remaining (789) are all shifted back (left). + * + * @param n the number of characters to delete. + */ + void handleDeleteCharacters(int n); + + /** + * Erase n characters from the current cursor position. In essence, replace the erased + * characters with spaces. If n is one, only the character at the cursor position is erased. If + * n is greater, then additional characters are erased after (to the right) of the cursor. + * + * @param n the number of characters to erase. + */ + void handleEraseCharacters(int n); + + /** + * Insert n blank characters at the current cursor position, shifting characters right to make + * room. + * + * @param n the number of characters to insert. + */ + void handleInsertCharacters(int n); + + /** + * Set the range of rows (viewport) involved in scrolling. + * + * <p> + * This applies not only to {@link #handleScrollUp()} and {@link #handleScrollDown()}, but also + * to when the cursor moves far enough down that the display must scroll. Normally, start is 0 + * and end is rows-1 (The parser will adjust the 1-up indices to 0-up) so that the entire + * display is scrolled. If the cursor moves past end (not just the end of the device, but the + * end given here) then the scrolling region must be scrolled. The top line is removed, the + * interior lines are moved up, and the bottom line is cleared. If the terminal is resized, the + * scroll range is reset to the whole display. + * + * @param start the first row (0-up) in the scrolling region. If omitted, the first row of the + * display. + * @param end the last row (0-up, inclusive) in the scrolling region. If omitted, the last row + * of the display. + */ + void handleSetScrollRange(Integer start, Integer end); + + /** + * Scroll the display n lines down, considering only those lines in the scrolling range. + * + * <p> + * To be unambiguous, this of movement of the viewport. The viewport scrolls down, so the lines + * themselves scroll up. The default range is the whole display. The cursor is not moved. + * + * @param n the number of lines to scroll + * @param intoScrollBack specifies whether the top line may flow into the scroll-back buffer + * @see #handleSetScrollRange(Integer, Integer) + */ + void handleScrollViewportDown(int n, boolean intoScrollBack); + + /** + * Scroll the display n lines up, considering only those lines in the scrolling range. + * + * @param n the number of lines to scroll + * @see #handleScrollDown() + * @see #handleSetScrollRange(Integer, Integer) + */ + void handleScrollViewportUp(int n); + + /** + * Scroll the lines n slots down, considering only those lines in the scrolling range. + * + * <p> + * This is equivalent to scrolling the <em>viewport</em> n lines <em>up</em>. This method exists + * in attempt to reflect "up" and "down" correctly in the documentation. Unfortunately, the + * documentation is not always clear whether we're scrolling the viewport or the lines + * themselves. + * + * @param n the number of lines to scroll + * @see #handleScrollViewportUp(int) + */ + default void handleScrollLinesDown(int n) { + handleScrollViewportUp(n); + } + + /** + * Scroll the lines n slots up, considering only those lines in the scrolling range. + * + * <p> + * The is equivalent to scrolling the <em>viewport</em> n lines <em>down</em>. This method + * exists in attempt to reflect "up" and "down" correctly in the documentation. Unfortunately, + * the documentation is not always clear whether we're scrolling the viewport or the lines + * themselves. + * + * @param n the number of lines to scroll + * @param intoScrollBack specifies whether the top line may flow into the scroll-back buffer + * @see #handleScrollViewportDown(int) + */ + default void handleScrollLinesUp(int n, boolean intoScrollBack) { + handleScrollViewportDown(n, intoScrollBack); + } + + /** + * Set the charset for a given slot + * + * @param g the slot + * @param cs the charset + */ + void handleSetCharset(VtCharset.G g, VtCharset cs); + + /** + * Handle a request to fully reset the terminal + * + * <p> + * All buffers should be cleared and all state variables, positions, attributes, etc., should be + * reset to their defaults. + */ + void handleFullReset(); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java new file mode 100644 index 0000000000..a3689ca1c6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java @@ -0,0 +1,330 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +/** + * A line of text in the {@link VtBuffer} + */ +public class VtLine { + protected int cols; + protected int len; + protected char[] chars; + private VtAttributes[] cellAttrs; + + /** + * Create a line with the given maximum number of characters + * + * @param cols the maximum number of characters + */ + public VtLine(int cols) { + reset(cols); + } + + /** + * Get the character in the given column + * + * @param x the column, 0 up + * @return the character + */ + public char getChar(int x) { + return chars[x]; + } + + /** + * Get the full character buffer + * + * <p> + * This is a reference to the buffer, which is very useful when rendering. Modifying this buffer + * externally is not recommended. + * + * @return the buffer + */ + public char[] getCharBuffer() { + return chars; + } + + /** + * Get the attributes for the character in the given column + * + * @param x the column, 0 up + * @return the attributes + */ + public VtAttributes getCellAttrs(int x) { + VtAttributes attrs = cellAttrs[x]; + if (attrs == null) { + return VtAttributes.DEFAULTS; + } + return attrs; + } + + /** + * Place the given character with attributes into the given column + * + * @param x the column, 0 up + * @param c the character + * @param attrs the attributes + */ + public void putChar(int x, char c, VtAttributes attrs) { + int oldLen = len; + len = Math.max(len, x + 1); + for (int i = oldLen; i < x; i++) { + chars[i] = ' '; + cellAttrs[i] = VtAttributes.DEFAULTS; + } + chars[x] = c; + if (attrs != null) { + cellAttrs[x] = attrs; + } + } + + /** + * Resize the line to the given maximum character count + * + * @param cols the maximum number of characters + */ + public void resize(int cols) { + this.cols = cols; + // NB. Don't forget the characters in the buffer. User may resize back again. + // TODO: Could/should we re-wrap? Would need to record wraps vs returns, though. + if (cols <= chars.length) { + return; + } + char[] newChars = new char[cols]; + VtAttributes[] newCellAttrs = new VtAttributes[cols]; + System.arraycopy(chars, 0, newChars, 0, Math.min(cols, chars.length)); + System.arraycopy(cellAttrs, 0, newCellAttrs, 0, Math.min(cols, cellAttrs.length)); + this.chars = newChars; + this.cellAttrs = newCellAttrs; + } + + /** + * Reset the line + * + * @param cols + */ + public void reset(int cols) { + this.cols = cols; + this.len = 0; + if (this.cols != cols || this.chars == null) { + this.chars = new char[cols]; + this.cellAttrs = new VtAttributes[cols]; + } + } + + /** + * Get the length of the line, excluding trailing cleared characters + * + * @return the length + */ + public int length() { + return Math.min(len, cols); + } + + /** + * Get the number of columns in the line + * + * @return the column count + */ + public int cols() { + return cols; + } + + /** + * Clear the full line + */ + public void clear() { + len = 0; + } + + /** + * Clear characters at and after the given column + * + * @param x the column, 0 up + */ + public void clearToEnd(int x) { + len = Math.min(len, x); + } + + /** + * Clear characters before and at the given column + * + * @param x the column, 0 up + * @param attrs attributes to apply to the cleared (space) characters + */ + public void clearToStart(int x, VtAttributes attrs) { + if (len <= x) { + len = 0; + return; + } + for (int i = 0; i <= x; i++) { + chars[i] = ' '; + cellAttrs[i] = attrs; + } + } + + /** + * Delete characters in the given range, shifting remaining characters to the left + * + * @param start the first column, 0 up + * @param end the last column, exclusive, 0 up + */ + public void delete(int start, int end) { + if (len <= end) { + len = start; + return; + } + int shift = end - start; + len -= shift; + for (int x = start; x < end; x++) { + chars[x] = chars[x + shift]; + cellAttrs[x] = cellAttrs[x + shift]; + } + } + + /** + * Replace characters in the given range with spaces + * + * <p> + * If the last column is erased, this instead clears from the start to the end. The difference + * is subtle, but deals in how the line reports its text contents. The trailing spaces will not + * be included if this call results in the last column being erased. + * + * @param start the first column, 0 up + * @param end the last column, exclusive, 0 up + * @param attrs the attributes to assign the space characters + */ + public void erase(int start, int end, VtAttributes attrs) { + if (len <= end) { + len = start; + return; + } + for (int x = start; x < end; x++) { + chars[x] = ' '; + cellAttrs[x] = attrs; + } + } + + /** + * Insert n (space) characters at and after the given column + * + * @param start the column, 0 up + * @param n the number of characters to insert + */ + public void insert(int start, int n) { + // Via experimentation, there is no wrapping. + // Neither of the shifted, nor the inserted characters. + // Additionally, the cursor does not move. + + // TODO: What about colors/attributes? + int end = Math.min(cols, start + n); + for (int x = cols - 1; x >= end; x--) { + chars[x] = chars[x - n]; + cellAttrs[x] = cellAttrs[x - n]; + } + for (int x = start; x < end; x++) { + chars[x] = ' '; + } + len = Math.min(cols, len + n); + } + + /** + * A callback for a run of contiguous characters having the same attributes + */ + public interface RunConsumer { + /** + * Execute an action on a run + * + * @param attrs the attributes shared by all in the run + * @param start the first column of the run, 0 up + * @param end the last column of the run, exclusive, 0 up + */ + void accept(VtAttributes attrs, int start, int end); + } + + /** + * Execute an action on each run of contiguous characters having the same attributes, from left + * to right. + * + * @param action the callback action + */ + public void forEachRun(RunConsumer action) { + int length = length(); + if (length == 0) { + action.accept(VtAttributes.DEFAULTS, 0, 0); + } + int first = 0; + VtAttributes attrs = getCellAttrs(0); + for (int x = 1; x < length; x++) { + if (!attrs.equals(getCellAttrs(x))) { + action.accept(attrs, first, x); + first = x; + attrs = getCellAttrs(x); + } + } + action.accept(attrs, first, length); + } + + /** + * Append a portion of this line's text to the given string builder + * + * @param sb the destination builder + * @param start the first column, 0 up + * @param end the last column, exclusive, 0 up + */ + public void gatherText(StringBuilder sb, int start, int end) { + start = Math.max(0, Math.min(start, len)); + end = Math.max(0, Math.min(end, len)); + sb.append(chars, start, end - start); + } + + /** + * Check if the given character is considered part of a word + * + * <p> + * This is used both when selecting words, and when requiring search to find whole words. + * + * @param ch the character + * @return true if the character is part of a word + */ + public static boolean isWordChar(char ch) { + return Character.isLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '@'; + } + + /** + * Find the boundaries for the word at the given column + * + * @param x the column, 0 up + * @param forward true to find the end, false to find the beginning + * @return the first column, 0 up, or the last column, exclusive, 0 up + */ + public int findWord(int x, boolean forward) { + int step = forward ? 1 : -1; + for (int i = x; i < len && i >= 0; i += step) { + char ch = chars[i]; + if (isWordChar(ch)) { + continue; + } + if (forward) { + return i; + } + return i + 1; + } + if (forward) { + return len; + } + return 0; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtOutput.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtOutput.java new file mode 100644 index 0000000000..59f3bdda52 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtOutput.java @@ -0,0 +1,34 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.nio.ByteBuffer; + +/** + * A callback for bytes generated by the terminal, e.g., to report a key press or reply to a + * request. + */ +public interface VtOutput { + /** + * Handle output from the terminal + * + * <p> + * Most likely these bytes should be sent down an output stream, usually to a pty. + * + * @param buf the buffer of bytes generated + */ + void out(ByteBuffer buf); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java new file mode 100644 index 0000000000..7e14ba8fba --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java @@ -0,0 +1,123 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.nio.ByteBuffer; + +/** + * The parser for a terminal emulator + * + * <p> + * The only real concern of this parser is to separate escape sequences from normal character + * output. All state not related to parsing is handled by a {@link VtHandler}. Most of the logic is + * implemented in the machine state nodes: {@link VtState}. + */ +public class VtParser { + protected final VtHandler handler; + protected VtState state = VtState.CHAR; + + protected VtCharset.G csG; + protected ByteBuffer csiParam = ByteBuffer.allocate(100); + protected ByteBuffer csiInter = ByteBuffer.allocate(100); + protected ByteBuffer oscParam = ByteBuffer.allocate(100); + + /** + * Construct a parser with the given handler + * + * @param handler the handler + */ + public VtParser(VtHandler handler) { + this.handler = handler; + } + + /** + * Create a copy of the CSI buffers, reconstructed as they were in the original stream. + * + * <p> + * This is used to re-process parsed bytes after broken CSI sequence + * + * @param b the character currently being parsed + * @return the copy + */ + protected ByteBuffer copyCsiBuffer(byte b) { + csiParam.flip(); + csiInter.flip(); + ByteBuffer buf = ByteBuffer.allocate(2 + csiParam.remaining() + csiInter.remaining()); + buf.put((byte) '['); + buf.put(csiParam); + buf.put(csiInter); + buf.put(b); + csiParam.clear(); + csiInter.clear(); + return buf; + } + + /** + * Create a copy of the OSC buffers, reconstructed as they were in the original stream. + * + * <p> + * This is used to re-process parsed bytes after a broken OSC sequence + * + * @param b the character currently being parsed + * @return the copy + */ + protected ByteBuffer copyOscBuffer(byte b) { + oscParam.flip(); + ByteBuffer buf = ByteBuffer.allocate(2 + oscParam.remaining()); + buf.put((byte) ']'); + buf.put(oscParam); + buf.put(b); + oscParam.clear(); + return buf; + } + + /** + * Process the bytes from the given buffer + * + * <p> + * This is likely fed from an input stream, usually of a pty. + * + * @param buf the buffer + */ + public void process(ByteBuffer buf) { + state = doProcess(state, buf); + } + + /** + * Process a given byte by delegating to the current state machine node + * + * @param state the node + * @param b the byte + * @return the new state node + */ + protected VtState doProcessByte(VtState state, byte b) { + return state.handleNext(b, this, handler); + } + + /** + * Process a given byte buffer, one byte at a time + * + * @param state the initial machine state node + * @param buf the buffer + * @return the resulting machine state node + */ + protected VtState doProcess(VtState state, ByteBuffer buf) { + while (buf.hasRemaining()) { + state = doProcessByte(state, buf.get()); + } + return state; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java new file mode 100644 index 0000000000..c6c655feef --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java @@ -0,0 +1,64 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import ghidra.util.Msg; + +public abstract class VtResponseEncoder { + protected static final byte[] PASTE_START = VtHandler.ascii("\033[200~"); + protected static final byte[] PASTE_END = VtHandler.ascii("\033[201~"); + + protected final ByteBuffer bb = ByteBuffer.allocate(16); + + protected final Charset charset; + + public VtResponseEncoder(Charset charset) { + this.charset = charset; + } + + protected abstract void generateBytes(ByteBuffer buf); + + public void reportCursorPos(int row, int col) { + bb.put(("\033[" + row + ";" + col + "R").getBytes(charset)); + generateBytesExc(); + } + + protected void generateBytesExc() { + bb.flip(); + try { + generateBytes(bb); + } + catch (Throwable t) { + Msg.error(this, "Error generating bytes: " + t, t); + } + finally { + bb.clear(); + } + } + + public void reportPasteStart() { + bb.put(PASTE_START); + generateBytesExc(); + } + + public void reportPasteEnd() { + bb.put(PASTE_END); + generateBytesExc(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java new file mode 100644 index 0000000000..f4c2771924 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java @@ -0,0 +1,337 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal.vt; + +import ghidra.app.plugin.core.terminal.vt.VtCharset.G; + +public enum VtState { + /** + * The initial state, just process output characters until we encounter an {@code ESC}. + */ + CHAR { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + if (b == 0x1b) { + return ESC; + } + handler.handleCharExc(b); + return CHAR; + } + }, + /** + * We have just encountered an {@code ESC}. + */ + ESC { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + switch (b) { + case '7': + handler.handleSaveCursorPos(); + return CHAR; + case '8': + handler.handleRestoreCursorPos(); + return CHAR; + case '(': + parser.csG = G.G0; + return CHARSET; + case ')': + parser.csG = G.G1; + return CHARSET; + case '*': + parser.csG = G.G2; + return CHARSET; + case '+': + parser.csG = G.G3; + return CHARSET; + case '[': + return CSI_PARAM; + case ']': + return OSC_PARAM; + case '=': + handler.handleApplicationKeypad(true); + return CHAR; + case '>': + // Normal keypad + handler.handleApplicationKeypad(false); + return CHAR; + case 'D': + handler.handleScrollViewportDown(1, true); + return CHAR; + case 'M': + handler.handleScrollViewportUp(1); + return CHAR; + case 'c': + handler.handleFullReset(); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + handler.handleCharExc(b); + return CHAR; + } + }, + /** + * We have encountered {@code ESC} and a charset-selection byte. Now we just need to know the + * charset. Most are one byte, but there are some two-byte codes. + */ + CHARSET { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + switch (b) { + case '"': + return CHARSET_QUOTE; + case '%': + return CHARSET_PERCENT; + case '&': + return CHARSET_AMPERSAND; + case 'A': + handler.handleSetCharset(parser.csG, VtCharset.UK); + return CHAR; + case 'B': + handler.handleSetCharset(parser.csG, VtCharset.USASCII); + return CHAR; + case 'C': + case '5': + handler.handleSetCharset(parser.csG, VtCharset.FINNISH); + return CHAR; + case 'H': + case '7': + handler.handleSetCharset(parser.csG, VtCharset.SWEDISH); + return CHAR; + case 'K': + handler.handleSetCharset(parser.csG, VtCharset.GERMAN); + return CHAR; + case 'Q': + case '9': + handler.handleSetCharset(parser.csG, VtCharset.FRENCH_CANADIAN); + return CHAR; + case 'R': + case 'f': + handler.handleSetCharset(parser.csG, VtCharset.FRENCH); + return CHAR; + case 'Y': + handler.handleSetCharset(parser.csG, VtCharset.ITALIAN); + return CHAR; + case 'Z': + handler.handleSetCharset(parser.csG, VtCharset.SPANISH); + return CHAR; + case '4': + handler.handleSetCharset(parser.csG, VtCharset.DUTCH); + return CHAR; + case '=': + handler.handleSetCharset(parser.csG, VtCharset.SWISS); + return CHAR; + case '`': + case 'E': + case '6': + handler.handleSetCharset(parser.csG, VtCharset.NORWEGIAN_DANISH); + return CHAR; + case '0': + handler.handleSetCharset(parser.csG, VtCharset.DEC_SPECIAL_LINES); + return CHAR; + case '<': + handler.handleSetCharset(parser.csG, VtCharset.DEC_SUPPLEMENTAL); + return CHAR; + case '>': + handler.handleSetCharset(parser.csG, VtCharset.DEC_TECHNICAL); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), b); + } + }, + /** + * We're selecting a two-byte charset, and we just encountered {@code "}. + */ + CHARSET_QUOTE { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + switch (b) { + case '>': + handler.handleSetCharset(parser.csG, VtCharset.GREEK); + return CHAR; + case '4': + handler.handleSetCharset(parser.csG, VtCharset.DEC_HEBREW); + return CHAR; + case '?': + handler.handleSetCharset(parser.csG, VtCharset.DEC_GREEK); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcessByte( + parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), (byte) '"'), b); + } + }, + /** + * We're selecting a two-byte charset, and we just encountered {@code %}. + */ + CHARSET_PERCENT { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + switch (b) { + case '2': + handler.handleSetCharset(parser.csG, VtCharset.TURKISH); + return CHAR; + case '6': + handler.handleSetCharset(parser.csG, VtCharset.PORTUGESE); + return CHAR; + case '=': + handler.handleSetCharset(parser.csG, VtCharset.HEBREW); + return CHAR; + case '0': + handler.handleSetCharset(parser.csG, VtCharset.DEC_TURKISH); + return CHAR; + case '5': + handler.handleSetCharset(parser.csG, VtCharset.DEC_SUPPLEMENTAL_GRAPHICS); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcessByte( + parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), (byte) '%'), b); + } + }, + /** + * We're selecting a two-byte charset, and we just encountered {@code &}. + */ + CHARSET_AMPERSAND { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + switch (b) { + case '4': + handler.handleSetCharset(parser.csG, VtCharset.DEC_CYRILLIC); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcessByte( + parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), (byte) '&'), b); + } + }, + /** + * We've encountered {@code CSI}, so now we're parsing parameters, intermediates, or the final + * character. + */ + CSI_PARAM { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + if (0x30 <= b && b <= 0x3f) { + parser.csiParam.put(b); + return CSI_PARAM; + } + if (0x20 <= b && b <= 0x2f) { + parser.csiInter.put(b); + return CSI_INTER; + } + if (0x40 <= b && b <= 0x7e) { + handleCsi(b, parser, handler); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcess(CHAR, parser.copyCsiBuffer(b)); + } + }, + /** + * We've finished (or skipped) parsing CSI parameters, so now we're parsing intermediates or the + * final character. + */ + CSI_INTER { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + if (0x20 <= b && b <= 0x2f) { + parser.csiInter.put(b); + return CSI_INTER; + } + if (0x40 <= b && b <= 0x7e) { + handleCsi(b, parser, handler); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcess(CHAR, parser.copyCsiBuffer(b)); + } + }, + /** + * We've encountered {@code OSC}, so now we're parsing parameters until we encounter {@code BEL} + * or {@code ST}. + */ + OSC_PARAM { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + if (0x20 <= b && b <= 0x7f) { + parser.oscParam.put(b); + return OSC_PARAM; + } + if (b == 0x07) { + handleOsc(parser, handler); + return CHAR; + } + if (b == 0x1b) { + return OSC_ESC; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcess(CHAR, parser.copyOscBuffer(b)); + } + }, + /** + * We've encountered {@code ESC} part of , so now we're parsing parameters until we encounter + * {@code BEL} or {@code ST}. + */ + OSC_ESC { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + if (b == '\\') { + handleOsc(parser, handler); + return CHAR; + } + handler.handleCharExc((byte) 0x1b); + return parser.doProcessByte(OSC_PARAM, b); + } + }; + + /** + * Handle the given character + * + * @param b the character currently being parsed + * @param parser the parser + * @param handler the handler + * @return the resulting machine state + */ + protected abstract VtState handleNext(byte b, VtParser parser, VtHandler handler); + + /** + * Handle a CSI sequence + * + * @param csiFinal the final byte + * @param parser the parser + * @param handler the handler + */ + protected void handleCsi(byte csiFinal, VtParser parser, VtHandler handler) { + parser.csiParam.flip(); + parser.csiInter.flip(); + handler.handleCsiExc(parser.csiParam, parser.csiInter, csiFinal); + parser.csiParam.clear(); + parser.csiInter.clear(); + } + + /** + * Handle an OSC sequence + * + * @param parser the parser + * @param handler the handler + */ + protected void handleOsc(VtParser parser, VtHandler handler) { + parser.oscParam.flip(); + handler.handleOscExc(parser.oscParam); + parser.oscParam.clear(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java index 5c55a2c314..608e80cebd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardContentProviderService.java @@ -23,6 +23,7 @@ import javax.swing.event.ChangeListener; import docking.ActionContext; import docking.ComponentProvider; +import docking.action.DockingAction; import ghidra.app.util.ClipboardType; import ghidra.util.task.TaskMonitor; @@ -140,4 +141,35 @@ public interface ClipboardContentProviderService { * @return true if copy special is enabled */ public boolean canCopySpecial(); + + /** + * Provide an alternative action owner. + * + * <p> + * This may be necessary if the key bindings or other user-customizable attributes need to be + * separated from the standard clipboard actions. By default, the clipboard service will create + * actions with a shared owner so that one keybinding, e.g., Ctrl-C, is shared across all Copy + * actions. + * + * @return the alternative owner, or null for the standard owner + * @see #customizeClipboardAction(DockingAction) + */ + default public String getClipboardActionOwner() { + return null; + } + + /** + * Customize the given action. + * + * <p> + * This method is called at the end of the action's constructor, which takes placed + * <em>before</em> the action is added to the provider. By default, this method does nothing. + * Likely, you will need to know which action you are customizing. Inspect the action name. + * + * @param action the action + * @see #getClipboardActionOwner() + */ + default void customizeClipboardAction(DockingAction action) { + // Default is don't customize + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardService.java index f8a111e583..9792ede8ea 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/ClipboardService.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +16,7 @@ package ghidra.app.services; public interface ClipboardService { - public void registerClipboardContentProvider( ClipboardContentProviderService service ); - public void deRegisterClipboardContentProvider( ClipboardContentProviderService service ); + public void registerClipboardContentProvider(ClipboardContentProviderService service); + + public void deRegisterClipboardContentProvider(ClipboardContentProviderService service); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/Terminal.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/Terminal.java new file mode 100644 index 0000000000..865670b03b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/Terminal.java @@ -0,0 +1,78 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.services; + +import java.nio.ByteBuffer; + +import ghidra.app.plugin.core.terminal.TerminalListener; + +/** + * A handle to a terminal window in the UI. + */ +public interface Terminal extends AutoCloseable { + /** + * Add a listener for terminal events + * + * @param listener the listener + */ + void addTerminalListener(TerminalListener listener); + + /** + * Remove a listener for terminal events + * + * @param listener the listener + */ + void removeTerminalListener(TerminalListener listener); + + /** + * Process the given buffer as if it were output by the terminal's application. + * + * <p> + * <b>Warning:</b> While implementations may synchronize to ensure the additional buffer is not + * processed at the same time as actual application input, there may not be any effort to ensure + * that the buffer is not injected in the middle of an escape sequence. Even if the injection is + * outside an escape sequence, this may still lead to unexpected behavior, since the injected + * output may be affected by or otherwise interfere with the application's control of the + * terminal's state. Generally, this should only be used for testing, or other cases when the + * caller knows it has exclusive control of the terminal. + * + * @param bb the buffer of bytes to inject + */ + void injectDisplayOutput(ByteBuffer bb); + + /** + * @see #injectDisplayOutput(ByteBuffer) + */ + default void injectDisplayOutput(byte[] arr) { + injectDisplayOutput(ByteBuffer.wrap(arr)); + } + + /** + * Set the terminal size to the given dimensions, as do <em>not</em> resize it to the window. + * + * @param rows the number of rows + * @param cols the number of columns + */ + void setFixedSize(int rows, int cols); + + /** + * Fit the terminals dimensions to the containing window. + */ + void setDynamicSize(); + + @Override + void close(); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/TerminalService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/TerminalService.java new file mode 100644 index 0000000000..1cb2565441 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/TerminalService.java @@ -0,0 +1,119 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.services; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import ghidra.app.plugin.core.terminal.TerminalPlugin; +import ghidra.app.plugin.core.terminal.vt.VtOutput; +import ghidra.framework.plugintool.ServiceInfo; + +/** + * A service that provides for the creation and management of DEC VT100 terminal emulators. + * + * <p> + * These are perhaps better described as XTerm clones. It seems the term "VT100" is applied to any + * text display that interprets some number of ANSI escape codes. While the XTerm documentation does + * a decent job of listing which VT version (or Tektronix, or whatever terminal) that introduced or + * specified each code/sequence in the last 6 or so decades, applications don't really seem to care + * about the details. You set {@code TERM=xterm}, and they just use whatever codes the feel like. + * Some make more conservative assumptions than others. For example, there is an escape sequence to + * insert a blank character, shifting the remaining characters in the line to the right. Despite + * using this, Bash (or perhaps Readline) will still re-send the remaining characters, just in case. + * It seems over the years, in an effort to be compatible with as many applications as possible, + * terminal emulators have implemented more and more escape codes, many of which were invented by + * XTerm, and some of which result from mis-reading documentation and/or replicating erroneous + * implementations. + * + * <p> + * Perhaps our interpretation of the history is jaded, and as we learn more, our implementation can + * become more disciplined, but as it stands, our {@link TerminalPlugin} takes the <em>ad hoc</em> + * approach: We've implemented the sequences we need to make it compatible with the applications we + * intend to run, hoping that the resulting feature set will work with many others. It will likely + * need patching to add missing features over its lifetime. We make extensive use of the + * <a href="https://invisible-island.net/xterm/ctlseqs/ctlseqs.html">XTerm control sequence + * documentation</a>, as well as the + * <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia article on ANSI escape + * codes</a>. Where the documentation lacks specificity or otherwise seems incorrect, we experiment + * with a reference implementation to discern and replicate its behavior. The clearest way we know + * to do this is to run the {@code tty} command from the reference terminal to get its + * pseudo-terminal (pty) file name. Then, we use Python from a separate terminal to write test + * sequences to it and/or read sequences from it. We use the {@code sleep} command to prevent Bash + * from reading its own terminal. This same process is applied to test our implementation. + * + * <p> + * The applications we've tested with include, without regard to version: + * <ul> + * <li>{@code bash}</li> + * <li>{@code less}</li> + * <li>{@code vim}</li> + * <li>{@code gdb -tui}</li> + * <li>{@code termmines} (from our Debugger training exercises)</li> + * </ul> + * + * <p> + * Some known issues: + * <ul> + * <li>It seems Java does not provide all the key modifier information, esp., the meta key. Either + * that or Ghidra's intercepting them. Thus, we can't encode those modifiers.</li> + * <li>Many control sequences are not implemented. They're intentionally left to be implemented on + * an as-needed basis.</li> + * <li>We inherit many of the erroneous key encodings, e.g., for F1-F4, present in the reference + * implementation.</li> + * <li>Character sets are incomplete. The box/line drawing set is most important to us as it's used + * by {@code gdb -tui}. Historically, these charsets are used to encode international characters. + * Modern systems (and terminal emulators) support Unicode (though perhaps only UTF-8), but it's not + * obvious how that interacts with the legacy charset switching. It's also likely many applications, + * despite UTF-8 being available, will still use the legacy charset switching, esp., for box + * drawing. Furthermore, because it's tedious work to figure the mapping for every character in a + * charset, we've only cared to implement a portion of the box-drawing charset, and it's sorely + * incomplete.</li> + * </ul> + */ +@ServiceInfo(defaultProvider = TerminalPlugin.class) +public interface TerminalService { + + /** + * Create a terminal not connected to any particular application. + * + * <p> + * To display application output, use {@link Terminal#injectDisplayOutput(java.nio.ByteBuffer)}. + * Application input is delivered to the given terminal output callback. If the application is + * connected via streams, esp., those from a pty, consider using + * {@link #createWithStreams(Charset, InputStream, OutputStream)}, instead. + * + * @param charset the character set for the terminal. See note in + * {@link #createWithStreams(Charset, InputStream, OutputStream)}. + * @param outputCb callback for output from the terminal, i.e., the application's input. + * @return the terminal + */ + Terminal createNullTerminal(Charset charset, VtOutput outputCb); + + /** + * Create a terminal connected to the application (or pty session) via the given streams. + * + * @param charset the character set for the terminal. <b>NOTE:</b> Only US-ASCII and UTF-8 have + * been tested. So long as the bytes 0x00-0x7f map one-to-one with characters with + * the same code point, it'll probably work. Charsets that require more than one byte + * to decode those characters will almost certainly break things. + * @param in the application's output, i.e., input for the terminal to display. + * @param out the application's input, i.e., output from the terminal's keyboard and mouse. + * @return the terminal + */ + Terminal createWithStreams(Charset charset, InputStream in, OutputStream out); +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java index f4b7f6c691..c65e3fc039 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingKeyBindingAction.java @@ -16,6 +16,7 @@ package docking; import java.awt.event.ActionEvent; +import java.util.List; import javax.swing.AbstractAction; import javax.swing.KeyStroke; @@ -24,12 +25,12 @@ import docking.action.DockingActionIf; import docking.actions.KeyBindingUtils; /** - * A class that can be used as an interface for using actions associated with keybindings. This + * A class that can be used as an interface for using actions associated with keybindings. This * class is meant to only by used by internal Ghidra key event processing. */ public abstract class DockingKeyBindingAction extends AbstractAction { - private DockingActionIf docakbleAction; + private DockingActionIf dockingAction; protected final KeyStroke keyStroke; protected final Tool tool; @@ -37,7 +38,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction { public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) { super(KeyBindingUtils.parseKeyStroke(keyStroke)); this.tool = tool; - this.docakbleAction = action; + this.dockingAction = action; this.keyStroke = keyStroke; } @@ -63,7 +64,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction { ComponentProvider provider = tool.getActiveComponentProvider(); ActionContext context = getLocalContext(provider); context.setSourceObject(e.getSource()); - docakbleAction.actionPerformed(context); + dockingAction.actionPerformed(context); } protected ActionContext getLocalContext(ComponentProvider localProvider) { @@ -78,4 +79,8 @@ public abstract class DockingKeyBindingAction extends AbstractAction { return new DefaultActionContext(localProvider, null); } + + public List<DockingActionIf> getActions() { + return List.of(dockingAction); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java index f21b529cd0..262b55aaed 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java @@ -705,11 +705,13 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder placeholderManager.removeComponent(provider); } -//================================================================================================== -// Package-level Action Methods -//================================================================================================== - - Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) { + /** + * Get the local actions installed on the given provider + * + * @param provider the provider + * @return an iterator over the actions + */ + public Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) { ComponentPlaceholder placeholder = getActivePlaceholder(provider); if (placeholder != null) { return placeholder.getActions(); @@ -719,6 +721,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder return emptyList.iterator(); } +//================================================================================================== +// Package-level Action Methods +//================================================================================================== + void removeProviderAction(ComponentProvider provider, DockingActionIf action) { ComponentPlaceholder placeholder = getActivePlaceholder(provider); if (placeholder != null) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java index d987f931eb..dba5a19f70 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/MultipleKeyAction.java @@ -318,6 +318,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction { return dwm.getActiveWindow(); } + @Override public List<DockingActionIf> getActions() { List<DockingActionIf> list = new ArrayList<>(actions.size()); for (ActionData actionData : actions) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollPane.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollPane.java index 424277a336..e185fd5b73 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollPane.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollPane.java @@ -57,7 +57,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener { } /** - * Sets this scroll pane to never show scroll bars. This is useful when you want a container + * Sets this scroll pane to never show scroll bars. This is useful when you want a container * whose view is always as big as the component in this scroll pane. */ public void setNeverScroll(boolean b) { @@ -67,6 +67,20 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener { useViewSizeAsPreferredSize = b; } + /** + * @see JScrollPane#setVerticalScrollBarPolicy(int) + */ + public void setVerticalScrollBarPolicy(int policy) { + scrollPane.setVerticalScrollBarPolicy(policy); + } + + /** + * @see JScrollPane#setHorizontalScrollBarPolicy(int) + */ + public void setHorizontalScrollBarPolicy(int policy) { + scrollPane.setHorizontalScrollBarPolicy(policy); + } + private ViewToIndexMapper createIndexMapper() { if (neverScroll) { return new PreMappedViewToIndexMapper(scrollable); @@ -215,6 +229,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener { return false; } + @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { @@ -296,8 +311,8 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener { /** * Sets whether the scroll wheel triggers scrolling <b>when over the scroll pane</b> of this - * class. When disabled, scrolling will still work when over the component inside of - * this class, but not when over the scroll bar. + * class. When disabled, scrolling will still work when over the component inside of this class, + * but not when over the scroll bar. * * @param enabled true to enable */ diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java index 72810bb0bc..a1a429550f 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java @@ -322,7 +322,7 @@ public abstract class ThemeManager { FontValue font = currentValues.getFont(id); if (font == null) { - error("No color value registered for: '" + id + "'"); + error("No font value registered for: '" + id + "'"); return DEFAULT_FONT; } return font.get(currentValues); diff --git a/Ghidra/Framework/Pty/Module.manifest b/Ghidra/Framework/Pty/Module.manifest new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Ghidra/Framework/Pty/build.gradle b/Ghidra/Framework/Pty/build.gradle new file mode 100644 index 0000000000..81c938a362 --- /dev/null +++ b/Ghidra/Framework/Pty/build.gradle @@ -0,0 +1,30 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.gradle.plugins.ide.eclipse.model.Container; + +apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle" +apply from: "$rootProject.projectDir/gradle/javaProject.gradle" +apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle" +apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle" +apply from: "$rootProject.projectDir/gradle/javadoc.gradle" + +apply plugin: 'eclipse' +eclipse.project.name = 'Framework Pty' + +dependencies { + api project(':Framework-Debugging') + api "com.jcraft:jsch:0.1.55" +} diff --git a/Ghidra/Framework/Pty/certification.manifest b/Ghidra/Framework/Pty/certification.manifest new file mode 100644 index 0000000000..433d104c25 --- /dev/null +++ b/Ghidra/Framework/Pty/certification.manifest @@ -0,0 +1,4 @@ +##VERSION: 2.0 +##MODULE IP: Apache License 2.0 +Module.manifest||GHIDRA||||END| +data/gui.palette.theme.properties||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/Pty.java similarity index 99% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/Pty.java index a1f1c1e1f9..d25044660c 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/Pty.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/Pty.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; import java.io.IOException; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java similarity index 99% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java index 72dafe6d29..9c98fc69d2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; import java.io.IOException; import java.util.*; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyEndpoint.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyEndpoint.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyEndpoint.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyEndpoint.java index db782c5b0d..2c188e9022 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyEndpoint.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyEndpoint.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; import java.io.InputStream; import java.io.OutputStream; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyFactory.java similarity index 89% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyFactory.java index 9daa3fbf7a..2464f17bbf 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyFactory.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; import java.io.IOException; -import agent.gdb.pty.linux.LinuxPtyFactory; -import agent.gdb.pty.macos.MacosPtyFactory; -import agent.gdb.pty.windows.ConPtyFactory; import ghidra.framework.OperatingSystem; +import ghidra.pty.linux.LinuxPtyFactory; +import ghidra.pty.macos.MacosPtyFactory; +import ghidra.pty.windows.ConPtyFactory; /** * A mechanism for opening pseudo-terminals diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java similarity index 91% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyParent.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java index 58fee345d5..73a695923b 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; /** * The parent (UNIX "master") end of a pseudo-terminal */ public interface PtyParent extends PtyEndpoint { + void setWindowSize(int cols, int rows); } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java index 631147092b..8402856d32 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/PtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; /** * A session led by the child pty diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Err.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java similarity index 96% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Err.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java index a52d24f1e5..46dc1595e2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Err.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import com.sun.jna.LastErrorException; import com.sun.jna.Native; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdInputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdInputStream.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdInputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdInputStream.java index ede700181c..3bc948b66d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdInputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdInputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.io.IOException; import java.io.InputStream; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdOutputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdOutputStream.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdOutputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdOutputStream.java index b3df529686..e873f4f955 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/FdOutputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdOutputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.io.IOException; import java.io.OutputStream; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPty.java similarity index 97% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPty.java index 45b701e597..3c635ca8f2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPty.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPty.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.io.IOException; import com.sun.jna.*; import com.sun.jna.ptr.IntByReference; -import agent.gdb.pty.Pty; +import ghidra.pty.Pty; import ghidra.util.Msg; public class LinuxPty implements Pty { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java similarity index 96% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyChild.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java index e59ad64ecc..164ddb7f6f 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.io.*; import java.net.URL; @@ -21,10 +21,10 @@ import java.net.URLDecoder; import java.nio.file.Paths; import java.util.*; -import agent.gdb.pty.PtyChild; -import agent.gdb.pty.PtySession; -import agent.gdb.pty.linux.PosixC.Termios; -import agent.gdb.pty.local.LocalProcessPtySession; +import ghidra.pty.PtyChild; +import ghidra.pty.PtySession; +import ghidra.pty.linux.PosixC.Termios; +import ghidra.pty.local.LocalProcessPtySession; import ghidra.util.Msg; public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyEndpoint.java similarity index 94% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyEndpoint.java index 2c611f7bf0..12b1b989ca 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyEndpoint.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyEndpoint.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.io.InputStream; import java.io.OutputStream; -import agent.gdb.pty.PtyEndpoint; +import ghidra.pty.PtyEndpoint; public class LinuxPtyEndpoint implements PtyEndpoint { protected final int fd; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java similarity index 90% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java index 28f24be2ca..4c67748eb5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.io.IOException; -import agent.gdb.pty.Pty; -import agent.gdb.pty.PtyFactory; +import ghidra.pty.Pty; +import ghidra.pty.PtyFactory; public class LinuxPtyFactory implements PtyFactory { @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyParent.java similarity index 57% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyParent.java index 86604373c4..9da8220008 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyParent.java @@ -13,12 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; -import agent.gdb.pty.PtyParent; +import ghidra.pty.PtyParent; +import ghidra.pty.linux.PosixC.Winsize; public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent { LinuxPtyParent(int fd) { super(fd); } + + @Override + public void setWindowSize(int cols, int rows) { + if (cols > 0xffff || rows > 0xffff) { + throw new IllegalArgumentException( + "Dimensions limited to unsigned shorts. Got cols=" + cols + ",rows=" + rows); + } + Winsize.ByReference ws = new Winsize.ByReference(); + ws.ws_col = (short) cols; + ws.ws_row = (short) rows; + ws.write(); + PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer()); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtySessionLeader.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtySessionLeader.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java index 1548d91605..6c555bc37d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/LinuxPtySessionLeader.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import java.util.List; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/PosixC.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java similarity index 83% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/PosixC.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java index f31ad44de7..4cc789e0aa 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/PosixC.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import com.sun.jna.*; import com.sun.jna.Structure.FieldOrder; @@ -26,6 +26,19 @@ import com.sun.jna.Structure.FieldOrder; */ public interface PosixC extends Library { + @FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" }) + class Winsize extends Structure { + public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific + + public short ws_row; + public short ws_col; + public short ws_xpixel; // Unused + public short ws_ypixel; // Unused + + public static class ByReference extends Winsize implements Structure.ByReference { + } + } + @FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed", "c_ospeed" }) class Termios extends Structure { @@ -96,6 +109,11 @@ public interface PosixC extends Library { return Err.checkLt0(BARE.execv(path, argv)); } + @Override + public int ioctl(int fd, int cmd, Pointer... args) { + return Err.checkLt0(BARE.ioctl(fd, cmd, args)); + } + @Override public int tcgetattr(int fd, Termios.ByReference termios_p) { return Err.checkLt0(BARE.tcgetattr(fd, termios_p)); @@ -123,6 +141,8 @@ public interface PosixC extends Library { int execv(String path, String[] argv); + int ioctl(int fd, int cmd, Pointer... args); + int tcgetattr(int fd, Termios.ByReference termios_p); int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Util.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Util.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Util.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Util.java index ef2bf9f290..6578ea77ef 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/linux/Util.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Util.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; import com.sun.jna.*; import com.sun.jna.ptr.IntByReference; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java similarity index 94% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java index ddf26b2757..dc7772f5e2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalProcessPtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.local; +package ghidra.pty.local; -import agent.gdb.pty.PtySession; +import ghidra.pty.PtySession; import ghidra.util.Msg; /** diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalWindowsNativeProcessPtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java similarity index 96% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalWindowsNativeProcessPtySession.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java index 07ba1c745e..2393fe1928 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/local/LocalWindowsNativeProcessPtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.local; +package ghidra.pty.local; import com.sun.jna.LastErrorException; import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.WinBase; import com.sun.jna.ptr.IntByReference; -import agent.gdb.pty.PtySession; -import agent.gdb.pty.windows.Handle; +import ghidra.pty.PtySession; +import ghidra.pty.windows.Handle; import ghidra.util.Msg; public class LocalWindowsNativeProcessPtySession implements PtySession { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/macos/MacosPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java similarity index 86% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/macos/MacosPtyFactory.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java index 64c4c5362f..7a9514c368 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/macos/MacosPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.macos; +package ghidra.pty.macos; import java.io.IOException; -import agent.gdb.pty.Pty; -import agent.gdb.pty.PtyFactory; -import agent.gdb.pty.linux.LinuxPty; +import ghidra.pty.Pty; +import ghidra.pty.PtyFactory; +import ghidra.pty.linux.LinuxPty; public class MacosPtyFactory implements PtyFactory { @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java similarity index 99% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java index b833af2c0e..145440dd74 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import java.io.IOException; import java.util.Objects; @@ -26,9 +26,9 @@ import org.apache.commons.text.StringEscapeUtils; import com.jcraft.jsch.*; import com.jcraft.jsch.ConfigRepository.Config; -import agent.gdb.pty.PtyFactory; import docking.DockingWindowManager; import docking.widgets.PasswordDialog; +import ghidra.pty.PtyFactory; import ghidra.util.Msg; import ghidra.util.StringUtilities; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPty.java similarity index 77% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPty.java index 53c5f0e5a4..d221b2f2ea 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPty.java @@ -13,34 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import java.io.*; -import com.jcraft.jsch.*; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.JSchException; -import agent.gdb.pty.*; +import ghidra.pty.*; public class SshPty implements Pty { private final ChannelExec channel; private final OutputStream out; private final InputStream in; + private final SshPtyParent parent; + private final SshPtyChild child; + public SshPty(ChannelExec channel) throws JSchException, IOException { this.channel = channel; out = channel.getOutputStream(); in = channel.getInputStream(); + + parent = new SshPtyParent(channel, out, in); + child = new SshPtyChild(channel, out, in); } @Override public PtyParent getParent() { - return new SshPtyParent(out, in); + return parent; } @Override public PtyChild getChild() { - return new SshPtyChild(channel, out, in); + return child; } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java similarity index 95% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java index 35196dc832..b3421fa2c5 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import java.io.*; import java.util.*; @@ -22,18 +22,15 @@ import java.util.stream.Collectors; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSchException; -import agent.gdb.pty.PtyChild; import ghidra.dbg.util.ShellUtils; +import ghidra.pty.PtyChild; import ghidra.util.Msg; public class SshPtyChild extends SshPtyEndpoint implements PtyChild { - private final ChannelExec channel; - private String name; public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { - super(outputStream, inputStream); - this.channel = channel; + super(channel, outputStream, inputStream); } private String sttyString(Collection<TermMode> mode) { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyEndpoint.java similarity index 80% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyEndpoint.java index 6614c4fd2d..d5521b1991 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyEndpoint.java @@ -13,18 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import java.io.InputStream; import java.io.OutputStream; -import agent.gdb.pty.PtyEndpoint; +import com.jcraft.jsch.ChannelExec; + +import ghidra.pty.PtyEndpoint; public class SshPtyEndpoint implements PtyEndpoint { + protected final ChannelExec channel; protected final OutputStream outputStream; protected final InputStream inputStream; - public SshPtyEndpoint(OutputStream outputStream, InputStream inputStream) { + public SshPtyEndpoint(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { + this.channel = channel; this.outputStream = outputStream; this.inputStream = inputStream; } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java similarity index 68% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java index ef44f169b0..24f7f96151 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java @@ -13,15 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import java.io.InputStream; import java.io.OutputStream; -import agent.gdb.pty.PtyParent; +import com.jcraft.jsch.ChannelExec; + +import ghidra.pty.PtyParent; public class SshPtyParent extends SshPtyEndpoint implements PtyParent { - public SshPtyParent(OutputStream outputStream, InputStream inputStream) { - super(outputStream, inputStream); + public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { + super(channel, outputStream, inputStream); + } + + @Override + public void setWindowSize(int cols, int rows) { + channel.setPtySize(cols, rows, 0, 0); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java similarity index 94% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java index 0cbce41e3b..7d40087c33 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import com.jcraft.jsch.Channel; -import agent.gdb.pty.PtySession; +import ghidra.pty.PtySession; public class SshPtySession implements PtySession { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/AnsiBufferedInputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/AnsiBufferedInputStream.java similarity index 99% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/AnsiBufferedInputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/AnsiBufferedInputStream.java index a0bb1a2d27..047489a664 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/AnsiBufferedInputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/AnsiBufferedInputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.*; import java.nio.ByteBuffer; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPty.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPty.java similarity index 94% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPty.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPty.java index bc1614b468..03b6be9130 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPty.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPty.java @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.IOException; import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.WinDef.DWORD; import com.sun.jna.platform.win32.WinNT.HANDLEByReference; -import com.sun.jna.platform.win32.COM.COMUtils; -import agent.gdb.pty.*; -import agent.gdb.pty.windows.jna.ConsoleApiNative; -import agent.gdb.pty.windows.jna.ConsoleApiNative.COORD; +import ghidra.pty.*; +import ghidra.pty.windows.jna.ConsoleApiNative; +import ghidra.pty.windows.jna.ConsoleApiNative.COORD; + +import com.sun.jna.platform.win32.COM.COMUtils; public class ConPty implements Pty { static final DWORD DW_ZERO = new DWORD(0); diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java similarity index 94% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyChild.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java index 2c5d2e90d6..bb1117b9f8 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.IOException; import java.util.*; @@ -25,11 +25,11 @@ import com.sun.jna.platform.win32.WinBase.PROCESS_INFORMATION; import com.sun.jna.platform.win32.WinDef.*; import com.sun.jna.platform.win32.WinNT.HANDLE; -import agent.gdb.pty.PtyChild; -import agent.gdb.pty.local.LocalWindowsNativeProcessPtySession; -import agent.gdb.pty.windows.jna.ConsoleApiNative; -import agent.gdb.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX; import ghidra.dbg.util.ShellUtils; +import ghidra.pty.PtyChild; +import ghidra.pty.local.LocalWindowsNativeProcessPtySession; +import ghidra.pty.windows.jna.ConsoleApiNative; +import ghidra.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX; public class ConPtyChild extends ConPtyEndpoint implements PtyChild { private final Handle pseudoConsoleHandle; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyEndpoint.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyEndpoint.java similarity index 94% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyEndpoint.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyEndpoint.java index 1fd3a133d8..8a43ef5e0e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyEndpoint.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyEndpoint.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.InputStream; import java.io.OutputStream; -import agent.gdb.pty.PtyEndpoint; +import ghidra.pty.PtyEndpoint; public class ConPtyEndpoint implements PtyEndpoint { protected InputStream inputStream; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyFactory.java similarity index 90% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyFactory.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyFactory.java index 85b3a732c4..08b8bce39e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyFactory.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.IOException; -import agent.gdb.pty.Pty; -import agent.gdb.pty.PtyFactory; +import ghidra.pty.Pty; +import ghidra.pty.PtyFactory; public class ConPtyFactory implements PtyFactory { @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java similarity index 78% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyParent.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java index ed1a10eea1..eff3740bd9 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/ConPtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java @@ -13,12 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; -import agent.gdb.pty.PtyParent; +import ghidra.pty.PtyParent; +import ghidra.util.Msg; public class ConPtyParent extends ConPtyEndpoint implements PtyParent { public ConPtyParent(Handle writeHandle, Handle readHandle) { super(writeHandle, readHandle); } + + @Override + public void setWindowSize(int rows, int cols) { + Msg.error(this, "Pty window size not implemented on Windows"); + } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/Handle.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/Handle.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/Handle.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/Handle.java index 7a2994ea9d..d04ced2938 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/Handle.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/Handle.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.lang.ref.Cleaner; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/HandleInputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/HandleInputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java index f66572e7c7..1f027ef57d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/HandleInputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.IOException; import java.io.InputStream; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/HandleOutputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleOutputStream.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/HandleOutputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleOutputStream.java index 7ae91dabc8..bfe073c72e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/HandleOutputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleOutputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.IOException; import java.io.OutputStream; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/Pipe.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/Pipe.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/Pipe.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/Pipe.java index 771c0323e4..50d7f48d9a 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/Pipe.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/Pipe.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import com.sun.jna.LastErrorException; import com.sun.jna.platform.win32.Kernel32; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/PseudoConsoleHandle.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/PseudoConsoleHandle.java similarity index 92% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/PseudoConsoleHandle.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/PseudoConsoleHandle.java index de6b45f34c..29bff32479 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/PseudoConsoleHandle.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/PseudoConsoleHandle.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import com.sun.jna.platform.win32.WinNT.HANDLE; -import agent.gdb.pty.windows.jna.ConsoleApiNative; +import ghidra.pty.windows.jna.ConsoleApiNative; public class PseudoConsoleHandle extends Handle { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/jna/ConsoleApiNative.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java similarity index 99% rename from Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/jna/ConsoleApiNative.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java index 90a194e731..4f426da9ff 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/windows/jna/ConsoleApiNative.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows.jna; +package ghidra.pty.windows.jna; import java.util.List; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/AbstractPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/AbstractPtyTest.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/AbstractPtyTest.java rename to Ghidra/Framework/Pty/src/test/java/ghidra/pty/AbstractPtyTest.java index 6b15d3818b..8ab969587d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/AbstractPtyTest.java +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/AbstractPtyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty; +package ghidra.pty; import static org.junit.Assert.assertEquals; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/linux/LinuxPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java similarity index 96% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/linux/LinuxPtyTest.java rename to Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java index c6f3fec9f0..f438164809 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/linux/LinuxPtyTest.java +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.linux; +package ghidra.pty.linux; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import java.io.*; @@ -24,11 +25,11 @@ import java.util.*; import org.junit.Before; import org.junit.Test; -import agent.gdb.pty.AbstractPtyTest; -import agent.gdb.pty.PtyChild.Echo; -import agent.gdb.pty.PtySession; import ghidra.dbg.testutil.DummyProc; import ghidra.framework.OperatingSystem; +import ghidra.pty.AbstractPtyTest; +import ghidra.pty.PtyChild.Echo; +import ghidra.pty.PtySession; public class LinuxPtyTest extends AbstractPtyTest { @Before diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/ssh/SshPtyTest.java similarity index 96% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java rename to Ghidra/Framework/Pty/src/test/java/ghidra/pty/ssh/SshPtyTest.java index 51191d89bb..32ebc1b693 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/ssh/SshPtyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.ssh; +package ghidra.pty.ssh; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeFalse; @@ -23,9 +23,9 @@ import java.io.*; import org.junit.Before; import org.junit.Test; -import agent.gdb.pty.PtyChild.Echo; -import agent.gdb.pty.PtySession; import ghidra.app.script.AskDialog; +import ghidra.pty.PtyChild.Echo; +import ghidra.pty.PtySession; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.util.SystemUtilities; import ghidra.util.exception.CancelledException; diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/windows/ConPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/windows/ConPtyTest.java similarity index 99% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/windows/ConPtyTest.java rename to Ghidra/Framework/Pty/src/test/java/ghidra/pty/windows/ConPtyTest.java index 1fc98e6942..3b81e97224 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/windows/ConPtyTest.java +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/windows/ConPtyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -27,9 +27,9 @@ import org.junit.Test; import com.sun.jna.LastErrorException; -import agent.gdb.pty.*; import ghidra.dbg.testutil.DummyProc; import ghidra.framework.OperatingSystem; +import ghidra.pty.*; public class ConPtyTest extends AbstractPtyTest { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/windows/NamedPipeTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/windows/NamedPipeTest.java similarity index 98% rename from Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/windows/NamedPipeTest.java rename to Ghidra/Framework/Pty/src/test/java/ghidra/pty/windows/NamedPipeTest.java index 5b2afb55cb..1557496cd4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/windows/NamedPipeTest.java +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/windows/NamedPipeTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package agent.gdb.pty.windows; +package ghidra.pty.windows; import java.io.*; diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java new file mode 100644 index 0000000000..d78da416b3 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java @@ -0,0 +1,381 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.terminal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import docking.widgets.OkDialog; +import docking.widgets.fieldpanel.support.*; +import ghidra.app.plugin.core.clipboard.ClipboardPlugin; +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.services.*; +import ghidra.framework.OperatingSystem; +import ghidra.pty.*; +import ghidra.util.SystemUtilities; + +public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest { + protected static byte[] ascii(String str) { + try { + return str.getBytes("US-ASCII"); + } + catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + protected static final byte[] TEST_CONTENTS = ascii(""" + term Term\r + noterm\r + """); + TerminalService terminalService; + ClipboardService clipboardService; + + @Test + @SuppressWarnings("resource") + public void testBash() throws Exception { + assumeFalse(SystemUtilities.isInTestingBatchMode()); + assumeFalse(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS); + + terminalService = addPlugin(tool, TerminalPlugin.class); + clipboardService = addPlugin(tool, ClipboardPlugin.class); + + PtyFactory factory = PtyFactory.local(); + try (Pty pty = factory.openpty()) { + Map<String, String> env = new HashMap<>(System.getenv()); + env.put("TERM", "xterm-256color"); + PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); + + PtyParent parent = pty.getParent(); + try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"), + parent.getInputStream(), parent.getOutputStream())) { + term.addTerminalListener(new TerminalListener() { + @Override + public void resized(int cols, int rows) { + parent.setWindowSize(cols, rows); + } + }); + session.waitExited(); + } + } + } + + @Test + @SuppressWarnings("resource") + public void testCmd() throws Exception { + assumeFalse(SystemUtilities.isInTestingBatchMode()); + assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS); + + terminalService = addPlugin(tool, TerminalPlugin.class); + clipboardService = addPlugin(tool, ClipboardPlugin.class); + + PtyFactory factory = PtyFactory.local(); + try (Pty pty = factory.openpty()) { + Map<String, String> env = new HashMap<>(System.getenv()); + PtySession session = + pty.getChild().session(new String[] { "C:\\Windows\\cmd.exe" }, env); + + PtyParent parent = pty.getParent(); + try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"), + parent.getInputStream(), parent.getOutputStream())) { + term.addTerminalListener(new TerminalListener() { + @Override + public void resized(int cols, int rows) { + parent.setWindowSize(cols, rows); + } + }); + session.waitExited(); + } + } + } + + protected void assertSingleSelection(int row, int colStart, int colEnd, FieldSelection sel) { + assertEquals(1, sel.getNumRanges()); + FieldRange range = sel.getFieldRange(0); + assertEquals(new FieldLocation(row, 0, 0, colStart), range.getStart()); + assertEquals(new FieldLocation(row, 0, 0, colEnd), range.getEnd()); + } + + @Test + @SuppressWarnings("resource") + public void testFindSimple() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("term"); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + OkDialog dialog = waitForInfoDialog(); + assertEquals("String not found", dialog.getMessage()); + dialog.close(); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindCaseSensitive() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("term"); + term.provider.findDialog.cbCaseSensitive.setSelected(true); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + OkDialog dialog = waitForInfoDialog(); + assertEquals("String not found", dialog.getMessage()); + dialog.close(); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindWrap() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("term"); + term.provider.findDialog.cbWrapSearch.setSelected(true); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindWholeWord() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("term"); + term.provider.findDialog.cbWholeWord.setSelected(true); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + OkDialog dialog = waitForInfoDialog(); + assertEquals("String not found", dialog.getMessage()); + dialog.close(); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindRegex() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("o?term"); + term.provider.findDialog.cbRegex.setSelected(true); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(1, 1, 6, + term.provider.panel.fieldPanel.getSelection())); + + // NB. the o is optional, so it finds a subrange of the previous result + performAction(term.provider.actionFindNext, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindNext, false); + OkDialog dialog = waitForInfoDialog(); + assertEquals("String not found", dialog.getMessage()); + dialog.close(); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindPrevious() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("term"); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + OkDialog dialog = waitForInfoDialog(); + assertEquals("String not found", dialog.getMessage()); + dialog.close(); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindPreviousWrap() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("term"); + term.provider.findDialog.cbWrapSearch.setSelected(true); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + } + } + + @Test + @SuppressWarnings("resource") + public void testFindPreviousRegex() throws Exception { + terminalService = addPlugin(tool, TerminalPlugin.class); + + try (DefaultTerminal term = (DefaultTerminal) terminalService + .createNullTerminal(Charset.forName("US-ASCII"), buf -> { + })) { + term.setFixedSize(25, 80); + term.injectDisplayOutput(TEST_CONTENTS); + + term.provider.findDialog.txtFind.setText("o?term"); + term.provider.findDialog.cbRegex.setSelected(true); + + // NB. the o is optional, so it finds a subrange of the next result + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(1, 2, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(1, 1, 6, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(0, 5, 9, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + waitForPass(() -> assertSingleSelection(0, 0, 4, + term.provider.panel.fieldPanel.getSelection())); + + performAction(term.provider.actionFindPrevious, false); + OkDialog dialog = waitForInfoDialog(); + assertEquals("String not found", dialog.getMessage()); + dialog.close(); + } + } +}