From e94bb1e66e83f18f3219a69038b612270c6c8d46 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:44:25 +0000 Subject: [PATCH] GP-6191: Implement repeat_char CSI final in VT-100. --- .../core/terminal/TerminalLayoutModel.java | 36 ++++++++++++++++-- .../plugin/core/terminal/vt/VtHandler.java | 17 ++++++++- .../app/plugin/core/terminal/vt/VtParser.java | 13 ++++++- .../core/terminal/TerminalProviderTest.java | 37 +++++++++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) 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 index 60f076dcc3..d1d50edfd6 100644 --- 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 @@ -55,6 +55,9 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { protected final CharsetDecoder decoder; + // For "repeat char" + protected byte lastChar; + // States for handling VT-style charsets protected final Map vtCharsets = new HashMap<>(); protected VtCharset.G curVtCharsetG = VtCharset.G.G0; @@ -265,9 +268,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { return NumericUtilities.convertBytesToString(data, ":"); } - @Override - public void handleChar(byte b) throws Exception { - bb.put(b); + protected void doHandleCharBytes() { bb.flip(); CoderResult result = decoder.decode(bb, cb, false); if (result.isError()) { @@ -292,6 +293,13 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { cb.clear(); } + @Override + public void handleChar(byte b) throws Exception { + bb.put(b); + lastChar = b; + doHandleCharBytes(); + } + @Override public void handleBell() { DockingWindowManager.beep(); @@ -314,6 +322,28 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { } } + /** + * {@inheritDoc} + *

+ * It's unclear exactly what is repeated. Some documentation, including + * http://rheuh.free.fr/docpack/C/ansi/ansi.html says Repeat Character or Control. Here, I only + * handle characters. I tried repeating a Cursor Up control. I also tried repeating a new line. + * Neither did what I expected when tested against the reference. That said, those control also + * seemed to "forget" what the last character was. Nothing was repeated at all. I'll assume that + * is undefined behavior. I'll not worry about "forgetting," as I'll assume the character to + * repeat will always immediately precede this CSI. + */ + @Override + public void handleRepeatChar(int n) { + if (lastChar == 0) { + return; + } + for (int i = 0; i < n; i++) { + bb.put(lastChar); + } + doHandleCharBytes(); + } + @Override public void handleLineFeed() { buffer.moveCursorDown(1, true); 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 index 830b045b4e..d59a02af91 100644 --- 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 @@ -956,6 +956,12 @@ public interface VtHandler { handleBackwardTab(n); return; } + case 'b': { // Repeat last character + OfInt bits = parseCsiInts(csiParam); + int n = bits.hasNext() ? bits.nextInt() : 1; + handleRepeatChar(n); + return; + } case 'c': { // Send Device Attributes Msg.trace(this, "TODO: Send Device Attributes"); return; @@ -1331,6 +1337,15 @@ public interface VtHandler { */ void handleBackwardTab(int n); + /** + * Handle the repeat char sequence: repeat the last character n more times. + *

+ * If the terminal is not in wrap mode, truncate anything beyond the last column. + * + * @param n + */ + void handleRepeatChar(int n); + /** * Handle the line feed control code (0x0a), usually just move the cursor down one. */ @@ -1753,7 +1768,7 @@ public interface VtHandler { * * @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) + * @see #handleScrollViewportDown(int, boolean) */ default void handleScrollLinesUp(int n, boolean intoScrollBack) { handleScrollViewportDown(n, intoScrollBack); 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 index 3d77cb4b6c..ae246e02fa 100644 --- 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 @@ -4,9 +4,9 @@ * 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. @@ -139,6 +139,15 @@ public class VtParser { state = doProcess(state, buf); } + protected void debugChar(char c) { + if (!Character.isISOControl(c)) { + System.err.print("%c".formatted(c)); + } + else { + System.err.print("\\x%02x".formatted(c & 0xff)); + } + } + /** * Process a given byte by delegating to the current state machine node * 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 index 05e7e6d843..1a02120ffd 100644 --- 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 @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import java.io.File; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.HashMap; @@ -33,6 +34,7 @@ import docking.widgets.fieldpanel.support.*; import ghidra.app.plugin.core.clipboard.ClipboardPlugin; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.services.*; +import ghidra.framework.Application; import ghidra.framework.OperatingSystem; import ghidra.pty.*; import ghidra.util.SystemUtilities; @@ -88,6 +90,41 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { } } + @Test + public void testTermmines() throws Exception { + assumeFalse(SystemUtilities.isInTestingBatchMode()); + assumeFalse(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS); + + terminalService = addPlugin(tool, TerminalPlugin.class); + clipboardService = addPlugin(tool, ClipboardPlugin.class); + + env.showFrontEndTool(); + + PtyFactory factory = PtyFactory.local(); + File termmines = Application.getModuleDataFile("TestResources", "termmines").getFile(false); + try (Pty pty = factory.openpty()) { + Map env = new HashMap<>(System.getenv()); + env.put("TERM", "xterm-256color"); + PtySession session = + pty.getChild().session(new String[] { termmines.getAbsolutePath() }, env); + + PtyParent parent = pty.getParent(); + PtyChild child = pty.getChild(); + try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), + parent.getInputStream(), parent.getOutputStream())) { + term.addTerminalListener(new TerminalListener() { + @Override + public void resized(short cols, short rows) { + System.err.println("resized: " + cols + "x" + rows); + child.setWindowSize(cols, rows); + } + }); + session.waitExited(); + pty.close(); + } + } + } + @Test @SuppressWarnings("resource") public void testCmd() throws Exception {