4 Commits

Author SHA1 Message Date
rudi 14bf7ba4b1 Add bugfixes from testing
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 37s
Release j3270 / Build & Publish Release (push) Successful in 43s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m14s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m12s
2026-09-12 17:21:21 -04:00
rudi bdfe6eec2a IMON GRAPHIC!
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 31s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 42s
2026-09-12 15:33:12 +00:00
rudi c28c097e25 Fix bugs reported in Discord
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-12 14:58:48 +00:00
rudi 46a022d86b Add bugfixes from discord
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
2026-09-10 09:24:39 -04:00
31 changed files with 2654 additions and 318 deletions
@@ -712,6 +712,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return terminalPanel; return terminalPanel;
} }
public Telnet3270Client getClient() {
return client;
}
void connect(ConnectionConfig config) { void connect(ConnectionConfig config) {
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -998,8 +1002,72 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"About j3270", JOptionPane.INFORMATION_MESSAGE); "About j3270", JOptionPane.INFORMATION_MESSAGE);
} }
/**
* Configure application logging. When debug is false, disables all logging
* to console and file, preventing creation of j3270.log.
*/
public static void configureLogging(boolean debug) {
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
}
}
if (debug) {
Level logLevel = Level.ALL;
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler);
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
@Override
public synchronized void publish(java.util.logging.LogRecord record) {
super.publish(record);
flush();
}
};
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(new SimpleFormatter());
fileHandler.setFilter(appFilter);
globalRoot.addHandler(fileHandler);
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
} catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage());
}
} else {
globalRoot.setLevel(Level.OFF);
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
Logger l = Logger.getLogger(pkg);
l.setLevel(Level.OFF);
for (java.util.logging.Handler h : l.getHandlers()) {
l.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
}
}
}
}
}
// ========== Main ========== // ========== Main ==========
public static void main(String[] args) { public static void main(String[] args) {
boolean debug = false; boolean debug = false;
boolean cliTls = false; boolean cliTls = false;
@@ -1041,42 +1109,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
Level logLevel = debug ? Level.FINE : Level.INFO; configureLogging(debug);
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler);
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
@Override
public synchronized void publish(java.util.logging.LogRecord record) {
super.publish(record);
flush();
}
};
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(new SimpleFormatter());
fileHandler.setFilter(appFilter);
globalRoot.addHandler(fileHandler);
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
} catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage());
}
if (configFile != null) { if (configFile != null) {
try { try {
@@ -1167,6 +1201,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify); config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock()); config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) { if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode); config.setGraphicsMode(finalGraphicsMode);
@@ -141,6 +141,14 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static boolean getAutoReconnect() {
return getAutoConnectAutoReconnect();
}
public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect);
}
public static int getAutoConnectReconnectMaxRetries() { public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5); return prefs.getInt("autoConnectReconnectMaxRetries", 5);
} }
@@ -150,6 +158,32 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static boolean getInputMask() {
return prefs.getBoolean("inputMask", true);
}
public static void setInputMask(boolean mask) {
prefs.putBoolean("inputMask", mask);
flushPrefs();
}
public static boolean getInputMaskEnabled() {
return getInputMask();
}
public static void setInputMaskEnabled(boolean enabled) {
setInputMask(enabled);
}
public static String getInputMaskChar() {
return prefs.get("inputMaskChar", "*");
}
public static void setInputMaskChar(String ch) {
prefs.put("inputMaskChar", (ch != null && !ch.trim().isEmpty()) ? ch.trim().substring(0, 1) : "*");
flushPrefs();
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name()); String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr); return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
@@ -574,6 +608,26 @@ public class Settings {
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "autoReconnect":
case "auto_reconnect":
case "autoConnectAutoReconnect":
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
break;
case "reconnectMaxRetries":
case "autoConnectReconnectMaxRetries":
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
break;
case "inputMask":
case "input_mask":
case "inputMaskEnabled":
case "maskInput":
setInputMask(Boolean.parseBoolean(value));
break;
case "inputMaskChar":
case "input_mask_char":
case "maskChar":
setInputMaskChar(value);
break;
case "autoSysUnlock": case "autoSysUnlock":
case "auto_sys_unlock": case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value)); setAutoSysUnlock(Boolean.parseBoolean(value));
@@ -729,6 +783,10 @@ public class Settings {
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask());
w.println("inputMaskChar = " + getInputMaskChar());
w.println("autoSysUnlock = " + getAutoSysUnlock()); w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel()); w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine()); w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
@@ -31,6 +31,9 @@ public class SettingsDialog extends JDialog {
private JPanel autoConnectPanel; private JPanel autoConnectPanel;
private JTextField hostField; private JTextField hostField;
private JTextField portField; private JTextField portField;
private JCheckBox autoReconnectCheck;
private JCheckBox inputMaskCheck;
private JTextField inputMaskCharField;
private JCheckBox blockSelectCheck; private JCheckBox blockSelectCheck;
private JSpinner dynamicRowsSpinner; private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner; private JSpinner dynamicColsSpinner;
@@ -268,14 +271,45 @@ public class SettingsDialog extends JDialog {
}); });
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT); autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
// Block select mode checkbox // Auto-Reconnect checkbox
gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = 2;
gbc.gridwidth = 2; gbc.gridwidth = 2;
autoReconnectCheck = new JCheckBox("Auto-Reconnect on Disconnect", Settings.getAutoConnectAutoReconnect());
ThemeManager.styleCheckBox(autoReconnectCheck);
panel.add(autoReconnectCheck, gbc);
// Input Mask feature control
gbc.gridy = 3;
gbc.gridwidth = 2;
JPanel inputMaskPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
inputMaskPanel.setOpaque(false);
inputMaskCheck = new JCheckBox("Enable Input Mask (Password Masking)", Settings.getInputMask());
ThemeManager.styleCheckBox(inputMaskCheck);
inputMaskPanel.add(inputMaskCheck);
JLabel maskCharLabel = new JLabel("Mask Character:");
inputMaskCharField = new JTextField(Settings.getInputMaskChar(), 2);
ThemeManager.styleTextField(inputMaskCharField);
inputMaskCharField.setEnabled(inputMaskCheck.isSelected());
maskCharLabel.setEnabled(inputMaskCheck.isSelected());
inputMaskCheck.addActionListener(e -> {
boolean sel = inputMaskCheck.isSelected();
inputMaskCharField.setEnabled(sel);
maskCharLabel.setEnabled(sel);
});
inputMaskPanel.add(maskCharLabel);
inputMaskPanel.add(inputMaskCharField);
panel.add(inputMaskPanel, gbc);
// Block select mode checkbox
gbc.gridy = 4;
gbc.gridwidth = 2;
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode()); blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
panel.add(blockSelectCheck, gbc); panel.add(blockSelectCheck, gbc);
// Default Dynamic Screen Size // Default Dynamic Screen Size
gbc.gridy = 3; gbc.gridy = 5;
gbc.gridwidth = 1; gbc.gridwidth = 1;
gbc.gridx = 0; gbc.gridx = 0;
panel.add(new JLabel("Default Dynamic Screen:"), gbc); panel.add(new JLabel("Default Dynamic Screen:"), gbc);
@@ -296,16 +330,16 @@ public class SettingsDialog extends JDialog {
// Clipboard & Tabular Paste options // Clipboard & Tabular Paste options
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 4; gbc.gridy = 6;
gbc.gridwidth = 2; gbc.gridwidth = 2;
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel()); enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
panel.add(enablePasteFromExcelCheck, gbc); panel.add(enablePasteFromExcelCheck, gbc);
gbc.gridy = 5; gbc.gridy = 7;
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine()); pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
panel.add(pasteStopAtProtectedCheck, gbc); panel.add(pasteStopAtProtectedCheck, gbc);
gbc.gridy = 6; gbc.gridy = 8;
gbc.weighty = 1.0; gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc); panel.add(Box.createGlue(), gbc);
@@ -752,6 +786,24 @@ public class SettingsDialog extends JDialog {
} }
} }
// Auto-Reconnect
if (autoReconnectCheck != null) {
boolean ar = autoReconnectCheck.isSelected();
Settings.setAutoConnectAutoReconnect(ar);
if (parentApp != null && parentApp.getClient() != null && parentApp.getClient().getConfig() != null) {
parentApp.getClient().getConfig().setAutoReconnect(ar);
}
}
// Input Mask
if (inputMaskCheck != null) {
Settings.setInputMask(inputMaskCheck.isSelected());
}
if (inputMaskCharField != null) {
String charText = inputMaskCharField.getText().trim();
Settings.setInputMaskChar(charText.isEmpty() ? "*" : charText.substring(0, 1));
}
// Block select mode // Block select mode
Settings.setBlockSelectMode(blockSelectCheck.isSelected()); Settings.setBlockSelectMode(blockSelectCheck.isSelected());
@@ -65,6 +65,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180); private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private boolean crosshairRulerEnabled = false; private boolean crosshairRulerEnabled = false;
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC) private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
private boolean inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
private String inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
private boolean textBlinkVisible = true; private boolean textBlinkVisible = true;
private Image wallpaperImage = null; private Image wallpaperImage = null;
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null; private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
@@ -1239,12 +1241,32 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
applyModeSettings(); applyModeSettings();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler(); crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
revalidate(); revalidate();
repaint(); repaint();
} }
public boolean isInputMaskEnabled() {
return inputMaskEnabled;
}
public void setInputMaskEnabled(boolean enabled) {
this.inputMaskEnabled = enabled;
repaint();
}
public String getInputMaskChar() {
return inputMaskChar;
}
public void setInputMaskChar(String maskChar) {
this.inputMaskChar = maskChar;
repaint();
}
public void setFontSize(int size) { public void setFontSize(int size) {
currentFontSize = size; currentFontSize = size;
haus.nightmare.j3270.config.Settings.setFontSize(size); haus.nightmare.j3270.config.Settings.setFontSize(size);
@@ -1527,12 +1549,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Password fields // Password fields
if (faIsZero(currentFA & 0xFF)) { if (faIsZero(currentFA & 0xFF)) {
char ch = ea.ucs4; if (inputMaskEnabled) {
if (ch > 0x20 && ch != 0xFF) { char ch = ea.ucs4;
Font f = bold ? boldTerminalFont : terminalFont; if (ch > 0x20 && ch != 0xFF) {
g2.setFont(f); Font f = bold ? boldTerminalFont : terminalFont;
g2.setColor(fgColor); g2.setFont(f);
g2.drawString("*", x, y + fontAscent); g2.setColor(fgColor);
String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*";
g2.drawString(mask, x, y + fontAscent);
}
} }
if (isCellSelected(row, col)) { if (isCellSelected(row, col)) {
@@ -0,0 +1,93 @@
package haus.nightmare.j3270;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
public class LoggingConfigurationTest {
private PrintStream originalOut;
private PrintStream originalErr;
@BeforeEach
public void setUp() {
originalOut = System.out;
originalErr = System.err;
}
@AfterEach
public void tearDown() {
System.setOut(originalOut);
System.setErr(originalErr);
// Ensure all handlers are closed and logging reset
J3270App.configureLogging(false);
}
@Test
public void testLoggingDisabledWhenDebugFalse() {
File logFile = new File("j3270.log");
if (logFile.exists()) {
logFile.delete();
}
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
System.setErr(new PrintStream(errContent));
J3270App.configureLogging(false);
// Root logger should have no handlers attached
Logger rootLogger = Logger.getLogger("");
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
// Application loggers should be OFF
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
// Emit log records at all levels
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
appLogger.severe("Test SEVERE message");
appLogger.warning("Test WARNING message");
appLogger.info("Test INFO message");
appLogger.fine("Test FINE message");
// Verify nothing was written to stdout or stderr
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
// Verify j3270.log was NOT created
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
}
@Test
public void testLoggingEnabledWhenDebugTrue() {
J3270App.configureLogging(true);
Logger rootLogger = Logger.getLogger("");
Handler[] handlers = rootLogger.getHandlers();
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
assertEquals(Level.ALL, rootLogger.getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
File logFile = new File("j3270.log");
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
// Clean up
J3270App.configureLogging(false);
}
}
@@ -0,0 +1,176 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
public class BehaviorSettingsTest {
private boolean origAutoReconnect;
private int origReconnectMaxRetries;
private boolean origInputMask;
private String origInputMaskChar;
@BeforeEach
public void setUp() {
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar();
}
@AfterEach
public void tearDown() {
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar);
}
@Test
@DisplayName("Auto-reconnect settings get/set and alias consistency")
public void testAutoReconnectSettingsPersistence() {
Settings.setAutoConnectAutoReconnect(true);
assertTrue(Settings.getAutoConnectAutoReconnect());
assertTrue(Settings.getAutoReconnect());
Settings.setAutoReconnect(false);
assertFalse(Settings.getAutoConnectAutoReconnect());
assertFalse(Settings.getAutoReconnect());
Settings.setAutoConnectReconnectMaxRetries(8);
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
}
@Test
@DisplayName("Input mask settings get/set and defaults")
public void testInputMaskSettingsPersistence() {
Settings.setInputMask(false);
assertFalse(Settings.getInputMask());
assertFalse(Settings.getInputMaskEnabled());
Settings.setInputMaskEnabled(true);
assertTrue(Settings.getInputMask());
assertTrue(Settings.getInputMaskEnabled());
Settings.setInputMaskChar("#");
assertEquals("#", Settings.getInputMaskChar());
Settings.setInputMaskChar("*");
assertEquals("*", Settings.getInputMaskChar());
// Empty string should fall back to '*'
Settings.setInputMaskChar("");
assertEquals("*", Settings.getInputMaskChar());
}
@Test
@DisplayName("INI export and load preserves autoReconnect and inputMask")
public void testIniExportAndLoad() throws Exception {
Settings.setAutoConnectAutoReconnect(true);
Settings.setAutoConnectReconnectMaxRetries(12);
Settings.setInputMask(false);
Settings.setInputMaskChar("@");
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
tempFile.deleteOnExit();
Settings.exportToIniFile(tempFile.getAbsolutePath());
// Reset to different values
Settings.setAutoConnectAutoReconnect(false);
Settings.setAutoConnectReconnectMaxRetries(3);
Settings.setInputMask(true);
Settings.setInputMaskChar("*");
// Load back from INI
Settings.loadFromIniFile(tempFile.getAbsolutePath());
assertTrue(Settings.getAutoConnectAutoReconnect());
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
assertFalse(Settings.getInputMask());
assertEquals("@", Settings.getInputMaskChar());
tempFile.delete();
}
@Test
@DisplayName("TerminalPanel reloads input mask settings correctly")
public void testTerminalPanelInputMaskReload() {
try {
TerminalPanel panel = new TerminalPanel();
Settings.setInputMask(false);
Settings.setInputMaskChar("$");
panel.reloadSettings();
assertFalse(panel.isInputMaskEnabled());
assertEquals("$", panel.getInputMaskChar());
panel.setInputMaskEnabled(true);
assertTrue(panel.isInputMaskEnabled());
panel.setInputMaskChar("#");
assertEquals("#", panel.getInputMaskChar());
} catch (HeadlessException ignored) {
// Safe fallback for headless runner
}
}
@Test
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
public void testSettingsDialogBehaviorControls() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Access private fields in SettingsDialog to verify component bindings
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
autoRecField.setAccessible(true);
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
inputMaskCheckField.setAccessible(true);
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
inputMaskCharField.setAccessible(true);
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
// Test interaction: unchecking inputMask disables character field
inputMaskCheck.setSelected(false);
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
}
assertFalse(maskCharField.isEnabled());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -435,6 +435,14 @@ public class ConnectionConfig {
} }
} }
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
String parsedLu = null;
int atIdx = s.indexOf('@');
if (atIdx > 0 && atIdx < s.length() - 1) {
parsedLu = s.substring(0, atIdx).trim();
s = s.substring(atIdx + 1).trim();
}
String host = s; String host = s;
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23); int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
@@ -458,6 +466,9 @@ public class ConnectionConfig {
} }
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
if (parsedLu != null && !parsedLu.isEmpty()) {
config.setLuName(parsedLu);
}
config.setUseTls(tls); config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e); config.setTn3270eEnabled(tn3270e);
config.setKeepAliveEnabled(keepAlive); config.setKeepAliveEnabled(keepAlive);
@@ -4,10 +4,10 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14). * EBCDIC ↔ Unicode character translator conforming to Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS). * Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Supports custom per-instance character translation override tables and complete * Supports custom per-instance character translation override tables and complete
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings. * 3270 APL / Graphic Escape (GA23-0059) character mappings.
* Default: Code Page 037 (US/Canada EBCDIC). * Default: Code Page 037 (US/Canada EBCDIC).
*/ */
public class EbcdicTranslator { public class EbcdicTranslator {
@@ -210,9 +210,11 @@ public class EbcdicTranslator {
*/ */
public char ebcdicToUnicode(int ebc) { public char ebcdicToUnicode(int ebc) {
int b = ebc & 0xFF; int b = ebc & 0xFF;
Character custom = customEbcdicToUnicode.get(b); if (!customEbcdicToUnicode.isEmpty()) {
if (custom != null) { Character custom = customEbcdicToUnicode.get(b);
return custom; if (custom != null) {
return custom;
}
} }
return activeCodePage.ebcdicToUnicode(b); return activeCodePage.ebcdicToUnicode(b);
} }
@@ -236,9 +238,11 @@ public class EbcdicTranslator {
* Returns -1 if the character cannot be mapped. * Returns -1 if the character cannot be mapped.
*/ */
public int unicodeToEbcdic(char unicode) { public int unicodeToEbcdic(char unicode) {
Integer custom = customUnicodeToEbcdic.get(unicode); if (!customUnicodeToEbcdic.isEmpty()) {
if (custom != null) { Integer custom = customUnicodeToEbcdic.get(unicode);
return custom; if (custom != null) {
return custom;
}
} }
return activeCodePage.unicodeToEbcdic(unicode); return activeCodePage.unicodeToEbcdic(unicode);
} }
@@ -296,8 +300,8 @@ public class EbcdicTranslator {
} }
/** /**
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph. * Map a 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification. * Conforms to 3270 APL / Text character set and GA23-0059 specification.
*/ */
public char mapAPL(int ebcdicCodePoint) { public char mapAPL(int ebcdicCodePoint) {
switch (ebcdicCodePoint & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
@@ -331,7 +335,7 @@ public class EbcdicTranslator {
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' case 0x5F: return '\u00AC'; // Not sign '¬'
// IBM 3270 APL Operational & Structural Glyphs // 3270 APL Operational & Structural Glyphs
case 0x80: return '\u22C4'; // Diamond '⋄' case 0x80: return '\u22C4'; // Diamond '⋄'
case 0x81: return '\u237A'; // APL Alpha '' case 0x81: return '\u237A'; // APL Alpha ''
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥' case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
@@ -383,7 +387,7 @@ public class EbcdicTranslator {
} }
/** /**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode. * Translate a 3270 Graphic Escape (GE) / APL character code to Unicode.
*/ */
public char getAplGraphic(int ec) { public char getAplGraphic(int ec) {
return mapAPL(ec); return mapAPL(ec);
@@ -54,6 +54,12 @@ public class DataStreamProcessor {
private boolean unlockSysPending = false; private boolean unlockSysPending = false;
private boolean rcvdRead = false; private boolean rcvdRead = false;
// Modal SA (set attribute) character attributes
private byte currentFg = 0;
private byte currentBg = 0;
private byte currentGr = 0;
private byte currentCs = 0;
/** Functional interface for sending output back through the telnet stack. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
@@ -240,6 +246,10 @@ public class DataStreamProcessor {
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
log.info(">>> EAU: erasing all unprotected fields"); log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected(); screen.eraseAllUnprotected();
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
break; break;
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
@@ -378,11 +388,19 @@ public class DataStreamProcessor {
if (wccReset(wcc)) { if (wccReset(wcc)) {
// Reset all character attributes to defaults // Reset all character attributes to defaults
log.fine("WCC reset: clearing default attributes"); log.fine("WCC reset: clearing default attributes");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
} }
if (eraseFirst) { if (eraseFirst) {
screen.clear(); screen.clear();
log.fine("Cleared screen for Erase/Write"); log.fine("Cleared screen for Erase/Write");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
} }
// Process orders and data starting at byte 2 // Process orders and data starting at byte 2
@@ -390,9 +408,6 @@ public class DataStreamProcessor {
int end = offset + length; int end = offset + length;
int baddr = screen.getBufferAddress(); int baddr = screen.getBufferAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
// Current SA (set attribute) values for character-mode
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
boolean lastWasOrder = false; boolean lastWasOrder = false;
while (pos < end) { while (pos < end) {
@@ -427,10 +442,6 @@ public class DataStreamProcessor {
// FA position is a display position that shows as blank // FA position is a display position that shows as blank
ea.ec = 0; ea.ec = 0;
ea.ucs4 = ' '; ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
screen.setFormatted(true); screen.setFormatted(true);
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -455,10 +466,6 @@ public class DataStreamProcessor {
ea.clear(); ea.clear();
ea.ec = 0; ea.ec = 0;
ea.ucs4 = ' '; ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
for (int i = 0; i < nPairs; i++) { for (int i = 0; i < nPairs; i++) {
int attrType = data[pos + 2 + i * 2] & 0xFF; int attrType = data[pos + 2 + i * 2] & 0xFF;
@@ -485,6 +492,12 @@ public class DataStreamProcessor {
int attrType = data[pos + 1] & 0xFF; int attrType = data[pos + 1] & 0xFF;
int attrValue = data[pos + 2] & 0xFF; int attrValue = data[pos + 2] & 0xFF;
switch (attrType) { switch (attrType) {
case XA_ALL:
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
break;
case XA_FOREGROUND: case XA_FOREGROUND:
currentFg = (byte) attrValue; currentFg = (byte) attrValue;
break; break;
@@ -984,7 +997,13 @@ public class DataStreamProcessor {
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
if (fieldLen > 3) { if (fieldLen > 3) {
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * qrBuilder.getCharWidth();
int targetH = screen.getRows() * qrBuilder.getCharHeight();
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
notifyScreenUpdated(); notifyScreenUpdated();
} }
@@ -1298,9 +1317,10 @@ public class DataStreamProcessor {
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4); int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - offset)); int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9; int targetW = screen.getCols() * qrBuilder.getCharWidth();
int targetH = screen.getRows() * 16; int targetH = screen.getRows() * qrBuilder.getCharHeight();
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) { if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH); graphicsPlane.resize(targetW, targetH);
} }
@@ -0,0 +1,86 @@
package haus.nightmare.lib3270j.datastream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Reusable, resizable byte buffer that provides zero-copy access to its internal
* array and supports ByteBuffer slicing to minimize heap allocations during
* network stream processing.
*/
public class FastByteBuffer extends OutputStream {
private byte[] buf;
private int count;
public FastByteBuffer() {
this(32768);
}
public FastByteBuffer(int initialCapacity) {
this.buf = new byte[Math.max(32, initialCapacity)];
this.count = 0;
}
private void ensureCapacity(int minCapacity) {
if (minCapacity > buf.length) {
int newCap = Math.max(buf.length << 1, minCapacity);
buf = Arrays.copyOf(buf, newCap);
}
}
@Override
public synchronized void write(int b) {
ensureCapacity(count + 1);
buf[count++] = (byte) b;
}
@Override
public synchronized void write(byte[] b, int off, int len) {
if (b == null || len <= 0) return;
ensureCapacity(count + len);
System.arraycopy(b, off, buf, count, len);
count += len;
}
/**
* Direct reference to internal buffer array.
* Use {@link #size()} to determine active length.
*/
public synchronized byte[] buffer() {
return buf;
}
public synchronized int size() {
return count;
}
public synchronized void reset() {
count = 0;
}
/**
* Creates a read-only ByteBuffer view wrapping active bytes without copying.
*/
public synchronized ByteBuffer asByteBuffer() {
return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
}
/**
* Creates a read-only ByteBuffer slice for a sub-range without copying.
*/
public synchronized ByteBuffer slice(int offset, int length) {
if (offset < 0 || length < 0 || offset + length > count) {
throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count);
}
return ByteBuffer.wrap(buf, offset, length).slice().asReadOnlyBuffer();
}
/**
* Produces a copied byte array if an isolated copy is explicitly required.
*/
public synchronized byte[] toByteArray() {
return Arrays.copyOf(buf, count);
}
}
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
* Builds Query Reply structured fields in response to host Read Partition queries. * Builds Query Reply structured fields in response to host Read Partition queries.
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout. * Matches 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
*/ */
public class QueryReplyBuilder { public class QueryReplyBuilder {
@@ -18,10 +18,102 @@ public class QueryReplyBuilder {
private static final int SW_3279_2 = 0x09; private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c; private static final int SH_3279_2 = 0x0c;
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060) // Usable Area physical dimensions matching HOD DS3270.java (Inches, 96 dpi: 0x00010060)
private static final int Xr_HOD = 0x00010060; private static final int Xr_HOD = 0x00010060;
private static final int Yr_HOD = 0x00010060; private static final int Yr_HOD = 0x00010060;
// Pre-computed static query reply payloads to eliminate allocation churn
private static final byte[] STATIC_QR_COLOR = new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
private static final byte[] STATIC_QR_HIGHLIGHTING = new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
private static final byte[] STATIC_QR_REPLY_MODES = new byte[] {
SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR
};
private static final byte[] STATIC_QR_OUTLINING = new byte[] {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final byte[] STATIC_QR_DBCS_ASIA = new byte[] {
0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01
};
private static final byte[] STATIC_QR_AUXDA = new byte[] {
0x00, 0x00
};
private static final byte[] STATIC_QR_TRANSPARENCY = new byte[] {
0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF
};
private static final byte[] STATIC_QR_SEGMENT = new byte[] {
(byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00
};
private static final byte[] STATIC_QR_PROCEDURE = new byte[] {
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
private static final byte[] STATIC_QR_LINETYPE = new byte[] {
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
private static final byte[] STATIC_PORT_BLOCKS;
static {
ByteArrayOutputStream pOut = new ByteArrayOutputStream(64);
byte[][] data = {
{ 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
{ 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 },
{ 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
{ 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C }
};
for (byte[] d : data) {
int len = 4 + d.length;
pOut.write((len >> 8) & 0xFF);
pOut.write(len & 0xFF);
pOut.write(SFID_QREPLY);
pOut.write(QR_PORT);
pOut.write(d, 0, d.length);
}
STATIC_PORT_BLOCKS = pOut.toByteArray();
}
private static final byte[] STATIC_QR_GRCOLOR;
static {
ByteArrayOutputStream gOut = new ByteArrayOutputStream(110);
gOut.write(0x00); gOut.write(0x04); gOut.write(0x00); gOut.write(0xFF); gOut.write(0xFF);
gOut.write(0x00); gOut.write(0x10); gOut.write(0x00); gOut.write(0x10);
for (int i = 0; i < 16; i++) {
gOut.write(0x00);
gOut.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
gOut.write((argb >> 16) & 0xFF);
gOut.write((argb >> 8) & 0xFF);
gOut.write(argb & 0xFF);
gOut.write(0x00);
}
STATIC_QR_GRCOLOR = gOut.toByteArray();
}
private final ScreenBuffer screen; private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.BOTH; private GraphicsMode graphicsMode = GraphicsMode.BOTH;
@@ -101,27 +193,27 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color (0x86) // Color (0x86)
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
// Highlighting (0x87) // Highlighting (0x87)
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
// Reply Modes (0x88) // Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) { if (isDbcs) {
// Outlining (0x8C) // Outlining (0x8C)
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
// DBCS Asia (0x91) // DBCS Asia (0x91)
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} }
// Distributed Data Management (0x95) // Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
// Auxiliary Devices (0x99) // Auxiliary Devices (0x99)
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
// Implicit Partition (0xA6) // Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
@@ -141,27 +233,27 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) { if (isDbcs) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} }
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8 appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0 appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1 appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2 appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
appendPort(out); // 0xB3 appendPort(out); // 0xB3
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4 appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); // 0xB4
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6 appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
} }
log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")"); log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")");
@@ -197,20 +289,20 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
break; break;
case QR_COLOR: case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
break; break;
case QR_HIGHLIGHTING: case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
break; break;
case QR_REPLY_MODES: case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
break; break;
case QR_OUTLINING: // 0x8C case QR_OUTLINING: // 0x8C
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
break; break;
case QR_DBCS_ASIA: // 0x91 case QR_DBCS_ASIA: // 0x91
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -219,35 +311,35 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
break; break;
case QR_AUXDA: // 0x99 case QR_AUXDA: // 0x99
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
break; break;
case QR_IMP_PART: // 0xA6 case QR_IMP_PART: // 0xA6
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break; break;
case QR_TRANSPARENCY: // 0xA8 case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_SEGMENT: // 0xB0 case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_PROCEDURE: // 0xB1 case QR_PROCEDURE: // 0xB1
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_LINETYPE: // 0xB2 case QR_LINETYPE: // 0xB2
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_LINETYPE, buildLineType()); appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -261,7 +353,7 @@ public class QueryReplyBuilder {
break; break;
case QR_GRCOLOR: // 0xB4 case QR_GRCOLOR: // 0xB4
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -321,19 +413,19 @@ public class QueryReplyBuilder {
out.write(maxCols & 0xFF); // usable width low out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) out.write(0x00); // units (0x00 = inches, matching HOD QR_USEAREA_STRING)
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) // Xr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
out.write((Xr_HOD >> 24) & 0xFF); out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF); out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 0xFF); out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) // Yr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
out.write((Yr_HOD >> 24) & 0xFF); out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF); out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF);
out.write(Yr_HOD & 0xFF); out.write(Yr_HOD & 0xFF);
int charW = getCharWidth(); int charW = getCharWidth(maxRows);
int charH = getCharHeight(); int charH = getCharHeight(maxRows);
out.write(charW); // AW out.write(charW); // AW
out.write(charH); // AH out.write(charH); // AH
int buf = maxCols * maxRows; int buf = maxCols * maxRows;
@@ -343,6 +435,11 @@ public class QueryReplyBuilder {
} }
public int getCharWidth() { public int getCharWidth() {
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return getCharWidth(rows);
}
public int getCharWidth(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12; return 12;
} }
@@ -350,18 +447,11 @@ public class QueryReplyBuilder {
} }
public int getCharHeight() { public int getCharHeight() {
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES: int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations: return getCharHeight(rows);
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes }
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
// GDDM then places the top menu bar at Row 1 (gy = 187..200). public int getCharHeight(int rows) {
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
//
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
@@ -442,31 +532,15 @@ public class QueryReplyBuilder {
} }
public byte[] buildColor() { public byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) return STATIC_QR_COLOR.clone();
return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
} }
public byte[] buildHighlighting() { public byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) return STATIC_QR_HIGHLIGHTING.clone();
return new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
} }
public byte[] buildReplyModes() { public byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; return STATIC_QR_REPLY_MODES.clone();
} }
public byte[] buildDdm() { public byte[] buildDdm() {
@@ -517,39 +591,27 @@ public class QueryReplyBuilder {
} }
public byte[] buildOutlining() { public byte[] buildOutlining() {
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000") return STATIC_QR_OUTLINING.clone();
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
} }
public byte[] buildDbcsAsia() { public byte[] buildDbcsAsia() {
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001") return STATIC_QR_DBCS_ASIA.clone();
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
} }
public byte[] buildAuxDa() { public byte[] buildAuxDa() {
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000") return STATIC_QR_AUXDA.clone();
return new byte[]{ 0x00, 0x00 };
} }
public byte[] buildTransparency() { public byte[] buildTransparency() {
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff") return STATIC_QR_TRANSPARENCY.clone();
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
} }
public byte[] buildSegment() { public byte[] buildSegment() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; return STATIC_QR_SEGMENT.clone();
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildSegment(maxCols, maxRows);
} }
public byte[] buildSegment(int maxCols, int maxRows) { public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000") return STATIC_QR_SEGMENT.clone();
return new byte[]{
(byte) 0x80, 0x02,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00
};
} }
public byte[] buildGraphics() { public byte[] buildGraphics() {
@@ -561,21 +623,11 @@ public class QueryReplyBuilder {
} }
public byte[] buildProcedure() { public byte[] buildProcedure() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; return STATIC_QR_PROCEDURE.clone();
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildProcedure(maxCols, maxRows);
} }
public byte[] buildProcedure(int maxCols, int maxRows) { public byte[] buildProcedure(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0") return STATIC_QR_PROCEDURE.clone();
return new byte[]{
0x00, 0x01,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
} }
public byte[] buildGImage() { public byte[] buildGImage() {
@@ -587,12 +639,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildLineType() { public byte[] buildLineType() {
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b") return STATIC_QR_LINETYPE.clone();
return new byte[]{
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
} }
public byte[] buildAuxDev() { public byte[] buildAuxDev() {
@@ -604,19 +651,7 @@ public class QueryReplyBuilder {
} }
public void appendPort(ByteArrayOutputStream out) { public void appendPort(ByteArrayOutputStream out) {
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total) out.write(STATIC_PORT_BLOCKS, 0, STATIC_PORT_BLOCKS.length);
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
});
} }
public void appendOemFmt(ByteArrayOutputStream out) { public void appendOemFmt(ByteArrayOutputStream out) {
@@ -624,9 +659,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildPort() { public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70); return STATIC_PORT_BLOCKS.clone();
appendPort(out);
return out.toByteArray();
} }
public byte[] buildOemFormat() { public byte[] buildOemFormat() {
@@ -634,24 +667,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildGrColor() { public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) return STATIC_QR_GRCOLOR.clone();
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
for (int i = 0; i < 16; i++) {
out.write(0x00);
out.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
out.write(r);
out.write(g);
out.write(b);
out.write(0x00); // 6th byte in HOD color table
}
return out.toByteArray();
} }
public byte[] buildGraphicColor() { public byte[] buildGraphicColor() {
@@ -0,0 +1,72 @@
package haus.nightmare.lib3270j.datastream;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Thread-safe memory pool providing reusable byte buffers for high-throughput
* 3270 stream operations, minimizing garbage collector pressure.
*/
public final class ReusableByteBufferPool {
public static final int SIZE_SMALL = 512;
public static final int SIZE_MEDIUM = 4096;
public static final int SIZE_LARGE = 32768;
private static final int MAX_POOLED_PER_TIER = 32;
private static final Queue<byte[]> smallPool = new ConcurrentLinkedQueue<>();
private static final Queue<byte[]> mediumPool = new ConcurrentLinkedQueue<>();
private static final Queue<byte[]> largePool = new ConcurrentLinkedQueue<>();
private ReusableByteBufferPool() {}
/**
* Acquires a pooled byte array with at least the specified capacity.
*/
public static byte[] acquire(int minCapacity) {
if (minCapacity <= SIZE_SMALL) {
byte[] b = smallPool.poll();
return (b != null) ? b : new byte[SIZE_SMALL];
} else if (minCapacity <= SIZE_MEDIUM) {
byte[] b = mediumPool.poll();
return (b != null) ? b : new byte[SIZE_MEDIUM];
} else if (minCapacity <= SIZE_LARGE) {
byte[] b = largePool.poll();
return (b != null) ? b : new byte[SIZE_LARGE];
}
return new byte[minCapacity];
}
/**
* Acquires a ByteBuffer wrapping a pooled array up to minCapacity.
*/
public static ByteBuffer acquireByteBuffer(int minCapacity) {
byte[] b = acquire(minCapacity);
return ByteBuffer.wrap(b);
}
/**
* Returns a buffer to the pool for reuse if it matches a standard tier.
*/
public static void release(byte[] buffer) {
if (buffer == null) return;
if (buffer.length == SIZE_SMALL && smallPool.size() < MAX_POOLED_PER_TIER) {
smallPool.offer(buffer);
} else if (buffer.length == SIZE_MEDIUM && mediumPool.size() < MAX_POOLED_PER_TIER) {
mediumPool.offer(buffer);
} else if (buffer.length == SIZE_LARGE && largePool.size() < MAX_POOLED_PER_TIER) {
largePool.offer(buffer);
}
}
/**
* Clears all pools to release held memory.
*/
public static void clear() {
smallPool.clear();
mediumPool.clear();
largePool.clear();
}
}
@@ -0,0 +1,77 @@
package haus.nightmare.lib3270j.epi;
import java.io.Serializable;
import java.util.Objects;
/**
* Attention Identifier (AID) representation for 3270 presentation space.
*/
public final class AID implements Serializable {
private static final long serialVersionUID = 1L;
private final byte code;
private final String name;
public static final AID clear = new AID((byte) 0x6D, "CLEAR");
public static final AID enter = new AID((byte) 0x7D, "ENTER");
public static final AID PA1 = new AID((byte) 0x6C, "PA1");
public static final AID PA2 = new AID((byte) 0x6E, "PA2");
public static final AID PA3 = new AID((byte) 0x6B, "PA3");
public static final AID PF1 = new AID((byte) 0xF1, "PF1");
public static final AID PF2 = new AID((byte) 0xF2, "PF2");
public static final AID PF3 = new AID((byte) 0xF3, "PF3");
public static final AID PF4 = new AID((byte) 0xF4, "PF4");
public static final AID PF5 = new AID((byte) 0xF5, "PF5");
public static final AID PF6 = new AID((byte) 0xF6, "PF6");
public static final AID PF7 = new AID((byte) 0xF7, "PF7");
public static final AID PF8 = new AID((byte) 0xF8, "PF8");
public static final AID PF9 = new AID((byte) 0xF9, "PF9");
public static final AID PF10 = new AID((byte) 0x7A, "PF10");
public static final AID PF11 = new AID((byte) 0x7B, "PF11");
public static final AID PF12 = new AID((byte) 0x7C, "PF12");
public static final AID PF13 = new AID((byte) 0xC1, "PF13");
public static final AID PF14 = new AID((byte) 0xC2, "PF14");
public static final AID PF15 = new AID((byte) 0xC3, "PF15");
public static final AID PF16 = new AID((byte) 0xC4, "PF16");
public static final AID PF17 = new AID((byte) 0xC5, "PF17");
public static final AID PF18 = new AID((byte) 0xC6, "PF18");
public static final AID PF19 = new AID((byte) 0xC7, "PF19");
public static final AID PF20 = new AID((byte) 0xC8, "PF20");
public static final AID PF21 = new AID((byte) 0xC9, "PF21");
public static final AID PF22 = new AID((byte) 0x4A, "PF22");
public static final AID PF23 = new AID((byte) 0x4B, "PF23");
public static final AID PF24 = new AID((byte) 0x4C, "PF24");
public AID(byte code, String name) {
this.code = code;
this.name = name;
}
public byte translate() {
return code;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AID aid = (AID) o;
return code == aid.code;
}
@Override
public int hashCode() {
return Objects.hash(code);
}
@Override
public String toString() {
return name != null ? name : String.format("AID(0x%02X)", code & 0xFF);
}
}
@@ -0,0 +1,34 @@
package haus.nightmare.lib3270j.epi;
/**
* Standard External Presentation Interface (EPI) DataStream processor interface.
*/
public interface DataStream {
/**
* Analyzes an inbound 3270 data stream buffer and updates the screen model.
*
* @param buffer Byte array containing the inbound 3270 record
* @param length Length of active bytes in the buffer
* @throws EPIException If a data stream format or command error is encountered
*/
void analyze(byte[] buffer, int length) throws EPIException;
/**
* Formats modified screen fields into an outbound 3270 data stream.
*
* @param buffer Target byte array to receive formatted outbound record
* @return Number of bytes written into the buffer
* @throws EPIException If encoding or formatting fails
*/
int format(byte[] buffer) throws EPIException;
/**
* Serializes the entire screen buffer into an outbound 3270 data stream.
*
* @param buffer Target byte array to receive the full screen dump
* @return Number of bytes written into the buffer
* @throws EPIException If encoding fails
*/
int readBuffer(byte[] buffer) throws EPIException;
}
@@ -0,0 +1,499 @@
package haus.nightmare.lib3270j.epi;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 3270 stream processor implementing the External Presentation Interface (EPI).
* Provides high-level stream analysis, buffer formatting, 12/14-bit buffer address
* encoding/decoding, and stream-level character translation conforming to GA23-0059.
*/
public class DataStream3270 implements DataStream, Serializable {
private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(DataStream3270.class.getName());
public static final byte[] ENCODE_TABLE = new byte[]{
32, 65, 66, 67, 68, 69, 70, 71, 72, 73, 91, 46, 60, 40, 43, 33,
38, 74, 75, 76, 77, 78, 79, 80, 81, 82, 93, 36, 42, 41, 59, 94,
45, 47, 83, 84, 85, 86, 87, 88, 89, 90, 124, 44, 37, 95, 62, 63,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 35, 64, 39, 61, 34
};
public static final int[] DECODE_TABLE = new int[]{
0, 15, 63, 59, 27, 44, 16, 61, 13, 29, 28, 14, 43, 32, 11, 33,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 30, 12, 62, 46, 47,
60, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 19, 20, 21, 22,
23, 24, 25, 34, 35, 36, 37, 38, 39, 40, 41, 10, -1, 26, 31, 45,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 42, -1, -1, -1
};
public static final char[] EBCDIC_TABLE = new char[]{
'@', 'O', '\u007f', '{', '[', 'l', 'P', '}', 'M', ']', '\\', 'N', 'k', '`', 'K', 'a',
'\u00f0', '\u00f1', '\u00f2', '\u00f3', '\u00f4', '\u00f5', '\u00f6', '\u00f7', '\u00f8', '\u00f9', 'z', '^', 'L', '~', 'n', 'o',
'|', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', '\u00c6', '\u00c7', '\u00c8', '\u00c9', '\u00d1', '\u00d2', '\u00d3', '\u00d4', '\u00d5', '\u00d6',
'\u00d7', '\u00d8', '\u00d9', '\u00e2', '\u00e3', '\u00e4', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9', 'J', '\u0000', 'Z', '_', 'm',
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000',
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', 'j', '\u0000', '\u0000', '\u0000'
};
private static final char[] blankChars = new char[]{'\u0000', '\n', '\f', '\r', '\u000e', '\u000f', '\u0019'};
private static final String blanks = new String(blankChars);
private Screen screen;
private boolean formatted = false;
public DataStream3270(Screen screen) {
this.screen = screen;
}
public Screen getScreen() {
return screen;
}
public void setScreen(Screen screen) {
this.screen = screen;
}
public boolean isFormatted() {
return formatted;
}
public void setFormatted(boolean formatted) {
this.formatted = formatted;
}
@Override
public void analyze(byte[] buffer, int length) throws EPIException {
if (buffer == null || length == 0) {
return;
}
int index = 0;
int fieldCount = 0;
int bufCount = 0;
Field currentField = null;
int curPos = 0;
int cursorTarget = 0;
int spanTarget = 0;
byte[] lineBuffer = new byte[screen.getWidth()];
boolean isNewField = true;
int wcc = 3;
int screenWidth = screen.getWidth();
if (length > 2) {
wcc = toEbcdic(buffer[1]);
}
switch (buffer[index]) {
case 49: // Write (0x31)
case (byte) 0xF1:
curPos = cursorTarget = (screen.getCursorRow() - 1) * screenWidth + (screen.getCursorColumn() - 1);
if ((wcc & 1) != 0) {
int totalFields = screen.fieldCount();
for (int i = 1; i <= totalFields; i++) {
Field f = screen.field(i);
if (f != null && f.dataTag() == 1) {
f.resetDataTag();
}
}
}
break;
case 50: // Read Buffer (0x32)
case (byte) 0xF2:
screen.readMode = true;
return;
case 53: // Erase / Write (0x35)
case (byte) 0xF5:
screen.reset();
curPos = 0;
cursorTarget = 0;
this.formatted = false;
break;
default:
throw new EPI3270Exception(96, buffer[index], 4608);
}
try {
index = 2;
while (index < length) {
int op = buffer[index] & 0xFF;
switch (op) {
case 9: { // PT (Program Tab)
log.finer("PT");
curPos++;
break;
}
case 16: { // SFE (Start Field Extended)
log.finer("SFE");
this.formatted = true;
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
}
currentField = screen.getField(curPos);
if (currentField == null) {
currentField = new Field(screen, curPos);
isNewField = true;
} else {
currentField.setAttribute(true);
currentField.setBaseAttribute('\u0000');
currentField.setExtAttribute('A', '\u0000');
currentField.setExtAttribute('B', '\u0000');
currentField.setExtAttribute('E', '\u0000');
currentField.setExtAttribute('F', '\u0000');
isNewField = false;
}
int numPairs = buffer[++index] & 0xFF;
for (int p = 1; p <= numPairs; p++) {
char attrType = (char) buffer[index + 1];
char attrVal = toEbcdic(buffer[index + 2]);
currentField.setExtAttribute(attrType, attrVal);
index += 2;
}
curPos++;
break;
}
case 17: { // SBA (Set Buffer Address)
log.finer("SBA");
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
currentField = null;
}
curPos = decodeAddress(buffer[index + 1], buffer[index + 2]);
index += 2;
break;
}
case 18: { // EUA (Erase Unprotected to Address)
log.finer("EUA");
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
screen.resetFields(curPos, spanTarget);
index += 2;
curPos = spanTarget;
break;
}
case 19: { // IC (Insert Cursor)
log.finer("IC");
cursorTarget = curPos;
break;
}
case 20: { // RA (Repeat to Address)
log.finer("RA");
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
index += 3;
byte repeatByte = buffer[index];
if (currentField == null) {
currentField = new Field(screen, curPos);
currentField.setAttribute(false);
isNewField = true;
}
if (spanTarget <= curPos) {
int totalSize = screenWidth * screen.getDepth();
while (curPos < totalSize) {
if (bufCount >= screenWidth) {
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
bufCount = 0;
}
lineBuffer[bufCount++] = repeatByte;
curPos++;
}
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
currentField = null;
curPos = 0;
if (spanTarget > 0) {
currentField = new Field(screen, curPos);
currentField.setAttribute(false);
isNewField = true;
}
}
while (curPos < spanTarget) {
if (bufCount >= screenWidth) {
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
bufCount = 0;
}
lineBuffer[bufCount++] = repeatByte;
curPos++;
}
break;
}
case 29: { // SF (Start Field)
log.finer("SF");
this.formatted = true;
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
}
currentField = screen.getField(curPos);
if (currentField == null) {
currentField = new Field(screen, curPos);
isNewField = true;
} else {
currentField.setAttribute(true);
currentField.setExtAttribute('A', '\u0000');
currentField.setExtAttribute('B', '\u0000');
currentField.setExtAttribute('E', '\u0000');
currentField.setExtAttribute('F', '\u0000');
isNewField = false;
}
currentField.setBaseAttribute(toEbcdic(buffer[++index]));
curPos++;
break;
}
case 26:
case 30: { // MF (Modify Field)
log.finer("MF");
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
bufCount = 0;
if (isNewField) {
screen.insertField(currentField);
}
currentField = null;
}
int numPairs = buffer[++index] & 0xFF;
currentField = screen.getField(curPos);
if (currentField != null) {
currentField.setAttribute(true);
for (int p = 1; p <= numPairs; p++) {
currentField.setExtAttribute((char) buffer[index + 1], toEbcdic(buffer[index + 2]));
index += 2;
}
isNewField = false;
curPos++;
break;
}
for (int p = 1; p <= numPairs; p++) {
index += 2;
}
break;
}
case 31:
case 40: { // SA (Set Attribute)
log.finer("SA");
index += 2;
break;
}
default: {
if (currentField == null) {
currentField = screen.getField(curPos - 1);
if (currentField == null) {
currentField = new Field(screen, curPos);
isNewField = true;
currentField.setAttribute(false);
} else {
isNewField = false;
}
}
if (bufCount >= screenWidth) {
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
bufCount = 0;
}
byte b = buffer[index];
lineBuffer[bufCount++] = (blanks.indexOf(b) != -1) ? (byte) 32 : b;
curPos++;
break;
}
}
index++;
}
if (currentField != null) {
if (bufCount > 0) {
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
}
if (isNewField) {
screen.insertField(currentField);
}
}
} catch (Exception e) {
log.log(Level.WARNING, "Error during analyze", e);
throw new EPI3270Exception(90, e, 4609);
}
int maxCell = screenWidth * screen.getDepth();
if (cursorTarget >= 0 && cursorTarget < maxCell) {
screen.setCursor(cursorTarget / screenWidth + 1, cursorTarget % screenWidth + 1);
} else {
log.fine("Cursor address out of range: " + cursorTarget);
}
}
@Override
public int format(byte[] buffer) throws EPIException {
if (buffer == null || buffer.length == 0) {
return 0;
}
int pos = 0;
AID aid = screen.getAID();
buffer[pos++] = aid.translate();
if (aid.equals(AID.clear)) {
screen.initList();
screen.setCursor(1, 1);
this.formatted = false;
return pos;
}
if (aid.equals(AID.PA1) || aid.equals(AID.PA2) || aid.equals(AID.PA3)) {
return pos;
}
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
encodeAddress(buffer, pos, cursorAddr);
pos += 2;
int totalFields = screen.fieldCount();
try {
if (this.formatted) {
for (int i = 1; i <= totalFields; i++) {
Field field = screen.field(i);
if (field != null && field.dataTag() == 1) {
buffer[pos++] = 17; // SBA order
encodeAddress(buffer, pos, field.getPosition() + 1);
pos += 2;
byte[] bytes = field.getBytes();
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
pos += bytes.length;
}
}
}
} else if (totalFields > 0) {
Field f1 = screen.field(1);
byte[] bytes = (f1 != null) ? f1.getBytes() : null;
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
pos += bytes.length;
}
}
} catch (UnsupportedEncodingException uee) {
log.log(Level.WARNING, "Unsupported encoding during format", uee);
throw new EPI3270Exception(90, uee, 4609);
}
return pos;
}
@Override
public int readBuffer(byte[] buffer) throws EPIException {
if (buffer == null || buffer.length == 0) {
return 0;
}
int pos = 0;
buffer[pos++] = screen.getAID().translate();
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
encodeAddress(buffer, pos, cursorAddr);
pos += 2;
int totalFields = screen.fieldCount();
try {
for (int i = 1; i <= totalFields; i++) {
Field field = screen.field(i);
if (field == null) continue;
buffer[pos++] = 17; // SBA
encodeAddress(buffer, pos, field.getPosition());
pos += 2;
if (field.hasAttribute()) {
buffer[pos++] = 29; // SF
buffer[pos++] = toAscii(field.baseAttribute());
}
byte[] bytes = field.getBytes();
if (bytes != null && bytes.length > 0) {
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
pos += bytes.length;
}
}
} catch (UnsupportedEncodingException uee) {
log.log(Level.WARNING, "Unsupported encoding during readBuffer", uee);
throw new EPI3270Exception(90, uee, 4609);
}
return pos;
}
/**
* Encodes 12-bit / 14-bit presentation space address into 2 bytes.
*/
public void encodeAddress(byte[] target, int offset, int address) {
if (address < 0 || address > 4096) {
target[offset] = 32;
target[offset + 1] = 32;
return;
}
int hi = address / 64;
int lo = address % 64;
target[offset] = ENCODE_TABLE[hi];
target[offset + 1] = ENCODE_TABLE[lo];
}
/**
* Decodes 2-byte presentation space address into linear buffer position.
*/
public int decodeAddress(int b1, int b2) {
int v1 = b1 & 0xFF;
int v2 = b2 & 0xFF;
if (v1 < 32 || v1 > 127 || v2 < 32 || v2 > 127) {
return -1;
}
int d1 = DECODE_TABLE[v1 - 32];
int d2 = DECODE_TABLE[v2 - 32];
if (d1 < 0 || d2 < 0) {
return -1;
}
return d1 * 64 + d2;
}
/**
* Translates single byte to EBCDIC presentation character.
*/
public char toEbcdic(int b) {
int v = b & 0xFF;
if (v < 32 || v > 127) {
return (char) v;
}
return EBCDIC_TABLE[v - 32];
}
/**
* Translates character back to ASCII byte representation.
*/
public byte toAscii(char c) {
if (c < '@' || c > '\u00f9') {
return (byte) c;
}
for (int i = 0; i < EBCDIC_TABLE.length; i++) {
if (EBCDIC_TABLE[i] == c) {
return (byte) (i + 32);
}
}
return 0;
}
}
@@ -0,0 +1,28 @@
package haus.nightmare.lib3270j.epi;
/**
* 3270 protocol specific External Presentation Interface (EPI) exception.
*/
public class EPI3270Exception extends EPIException {
private static final long serialVersionUID = 1L;
private int commandOrOrder = 0;
public EPI3270Exception(int errorCode, int commandOrOrder, int reasonCode) {
super(errorCode, reasonCode);
this.commandOrOrder = commandOrOrder;
}
public EPI3270Exception(int errorCode, Throwable cause, int reasonCode) {
super(errorCode, cause, reasonCode);
}
public EPI3270Exception(String message) {
super(message);
}
public int getCommandOrOrder() {
return commandOrOrder;
}
}
@@ -0,0 +1,50 @@
package haus.nightmare.lib3270j.epi;
/**
* Base exception for External Presentation Interface (EPI) stream operations.
*/
public class EPIException extends Exception {
private static final long serialVersionUID = 1L;
private int errorCode = 0;
private int reasonCode = 0;
public EPIException() {
super();
}
public EPIException(String message) {
super(message);
}
public EPIException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public EPIException(int errorCode, Throwable cause) {
super(cause);
this.errorCode = errorCode;
}
public EPIException(int errorCode, Throwable cause, int reasonCode) {
super(cause);
this.errorCode = errorCode;
this.reasonCode = reasonCode;
}
public EPIException(int errorCode, int reasonCode) {
super("EPI Exception error=" + errorCode + " reason=" + reasonCode);
this.errorCode = errorCode;
this.reasonCode = reasonCode;
}
public int getErrorCode() {
return errorCode;
}
public int getReasonCode() {
return reasonCode;
}
}
@@ -0,0 +1,105 @@
package haus.nightmare.lib3270j.epi;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import java.io.UnsupportedEncodingException;
/**
* Bidirectional bridge between EPI Screen model and lib3270j ScreenBuffer.
*/
public class EpiScreenBufferBridge {
/**
* Copies contents from an EPI Screen into a ScreenBuffer presentation space.
*/
public static void copyToScreenBuffer(Screen epiScreen, ScreenBuffer target) {
if (epiScreen == null || target == null) return;
synchronized (target.getRenderLock()) {
int w = epiScreen.getWidth();
int h = epiScreen.getDepth();
if (target.getCols() != w || target.getRows() != h) {
target.setDimensions(h, w);
}
target.clear();
int count = epiScreen.fieldCount();
for (int i = 1; i <= count; i++) {
Field f = epiScreen.field(i);
if (f == null) continue;
int pos = f.getPosition();
if (pos >= 0 && pos < target.getSize()) {
if (f.hasAttribute()) {
byte fa = (byte) (f.baseAttribute() & 0xFF);
target.setFieldAttribute(pos, fa);
}
try {
byte[] bytes = f.getBytes();
if (bytes != null) {
int textPos = f.hasAttribute() ? pos + 1 : pos;
for (int bIdx = 0; bIdx < bytes.length && (textPos + bIdx) < target.getSize(); bIdx++) {
ExtendedAttribute ea = target.getCell(textPos + bIdx);
ea.ec = bytes[bIdx];
}
}
} catch (UnsupportedEncodingException ignored) {}
}
}
int r = Math.max(1, Math.min(target.getRows(), epiScreen.getCursorRow()));
int c = Math.max(1, Math.min(target.getCols(), epiScreen.getCursorColumn()));
target.setCursorAddress((r - 1) * target.getCols() + (c - 1));
target.translateToUnicode();
target.markAllChanged();
}
}
/**
* Extracts fields from a ScreenBuffer presentation space into an EPI Screen.
*/
public static void copyFromScreenBuffer(ScreenBuffer source, Screen epiScreen) {
if (source == null || epiScreen == null) return;
synchronized (source.getRenderLock()) {
epiScreen.setWidth(source.getCols());
epiScreen.setDepth(source.getRows());
epiScreen.initList();
int size = source.getSize();
Field currentField = null;
byte[] buf = new byte[source.getCols()];
int bufLen = 0;
for (int i = 0; i < size; i++) {
byte fa = source.getFieldAttributeAt(i);
if (fa != 0) {
if (currentField != null && bufLen > 0) {
currentField.setBytes(0, buf, bufLen);
epiScreen.insertField(currentField);
bufLen = 0;
}
currentField = new Field(epiScreen, i);
currentField.setAttribute(true);
currentField.setBaseAttribute((char) (fa & 0xFF));
} else if (currentField != null) {
ExtendedAttribute ea = source.getCell(i);
if (bufLen >= buf.length) {
currentField.setBytes(0, buf, bufLen);
bufLen = 0;
}
buf[bufLen++] = ea.ec;
}
}
if (currentField != null && bufLen > 0) {
currentField.setBytes(0, buf, bufLen);
epiScreen.insertField(currentField);
}
int cursorAddr = source.getCursorAddress();
int cols = source.getCols();
epiScreen.setCursor((cursorAddr / cols) + 1, (cursorAddr % cols) + 1);
}
}
}
@@ -0,0 +1,127 @@
package haus.nightmare.lib3270j.epi;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
/**
* Field representation within an EPI 3270 Screen.
*/
public class Field implements Serializable {
private static final long serialVersionUID = 1L;
private Screen screen;
private int position;
private int length;
private boolean hasAttribute;
private char baseAttribute;
private final Map<Character, Character> extAttributes = new HashMap<>();
private int dataTag; // 1 = modified, 0 = unmodified
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
public Field(Screen screen, int position) {
this.screen = screen;
this.position = position;
this.hasAttribute = false;
this.baseAttribute = '\0';
this.dataTag = 0;
}
public Screen getScreen() {
return screen;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public int getLength() {
return length > 0 ? length : content.size();
}
public void setLength(int length) {
this.length = length;
}
public boolean hasAttribute() {
return hasAttribute;
}
public void setAttribute(boolean hasAttribute) {
this.hasAttribute = hasAttribute;
}
public char baseAttribute() {
return baseAttribute;
}
public void setBaseAttribute(char baseAttribute) {
this.hasAttribute = true;
this.baseAttribute = baseAttribute;
// Bit 0x01 in 3270 attribute indicates Modified Data Tag (MDT)
if ((baseAttribute & 0x01) != 0) {
this.dataTag = 1;
}
}
public void setExtAttribute(char type, char value) {
this.hasAttribute = true;
extAttributes.put(type, value);
}
public char getExtAttribute(char type) {
Character val = extAttributes.get(type);
return val != null ? val : '\0';
}
public int dataTag() {
return dataTag;
}
public void resetDataTag() {
this.dataTag = 0;
}
public void setDataTag(int dataTag) {
this.dataTag = dataTag;
}
public void setBytes(int offset, byte[] data, int length) {
if (data != null && length > 0) {
content.write(data, 0, length);
this.length = content.size();
}
}
public byte[] getBytes() throws UnsupportedEncodingException {
return content.toByteArray();
}
public String getText() {
byte[] bytes = content.toByteArray();
return new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1);
}
public void setText(String text) {
content.reset();
if (text != null) {
byte[] b = text.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
content.write(b, 0, b.length);
this.length = b.length;
this.dataTag = 1;
}
}
public void clear() {
content.reset();
this.length = 0;
this.dataTag = 0;
}
}
@@ -0,0 +1,142 @@
package haus.nightmare.lib3270j.epi;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Screen representation within an EPI 3270 session.
*/
public class Screen implements Serializable {
private static final long serialVersionUID = 1L;
private int width = 80;
private int depth = 24;
private int cursorRow = 1;
private int cursorColumn = 1;
private AID aid = AID.enter;
public boolean readMode = false;
private final List<Field> fields = new ArrayList<>();
public Screen() {
this(80, 24);
}
public Screen(int width, int depth) {
this.width = width;
this.depth = depth;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getDepth() {
return depth;
}
public void setDepth(int depth) {
this.depth = depth;
}
public int getCursorRow() {
return cursorRow;
}
public int getCursorColumn() {
return cursorColumn;
}
public void setCursor(int row, int col) {
this.cursorRow = row;
this.cursorColumn = col;
}
public AID getAID() {
return aid;
}
public void setAID(AID aid) {
this.aid = aid != null ? aid : AID.enter;
}
public int fieldCount() {
return fields.size();
}
/**
* Retrieves field by 1-based index matching EPI convention.
*/
public Field field(int oneBasedIndex) {
if (oneBasedIndex >= 1 && oneBasedIndex <= fields.size()) {
return fields.get(oneBasedIndex - 1);
}
return null;
}
/**
* Finds the field starting at or covering the given linear buffer position.
*/
public Field getField(int position) {
for (Field f : fields) {
if (f.getPosition() == position) {
return f;
}
}
return null;
}
public void insertField(Field field) {
if (field == null) return;
// Keep fields ordered by position
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getPosition() == field.getPosition()) {
fields.set(i, field);
return;
} else if (fields.get(i).getPosition() > field.getPosition()) {
fields.add(i, field);
return;
}
}
fields.add(field);
}
public void initList() {
fields.clear();
}
public void reset() {
fields.clear();
cursorRow = 1;
cursorColumn = 1;
readMode = false;
}
/**
* Resets fields within the range [start, end) by erasing unprotected content.
*/
public void resetFields(int start, int end) {
int maxPos = width * depth;
for (Field f : fields) {
int pos = f.getPosition();
boolean inRange;
if (start <= end) {
inRange = (pos >= start && pos < end);
} else {
inRange = (pos >= start || pos < end);
}
if (inRange) {
// If unprotected, clear content
if (f.hasAttribute() && (f.baseAttribute() & 0x20) == 0) {
f.clear();
}
}
}
}
}
@@ -11,18 +11,18 @@ import java.util.regex.Pattern;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand * VM/CMS Spool and Print File Transfer facility matching Host On-Demand specifications.
* (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer).
* *
* Provides: * Provides:
* 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT). * 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT).
* 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+'). * 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+').
* 3. IBM 1403/3211 Machine carriage control channel command byte translation. * 3. Machine carriage control channel command byte translation.
* 4. High-level print spool stream extraction and transfer helpers. * 4. High-level print spool stream extraction and transfer helpers.
*/ */
public class CMSPrintXfer { public class CMSPrintXfer {
private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName()); private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName());
private static final Pattern HEADER_PATTERN = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
private final ECLXfer xfer; private final ECLXfer xfer;
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
@@ -126,13 +126,12 @@ public class CMSPrintXfer {
if (text == null || text.trim().isEmpty()) return entries; if (text == null || text.trim().isEmpty()) return entries;
String[] lines = text.split("\r?\n"); String[] lines = text.split("\r?\n");
Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
for (String line : lines) { for (String line : lines) {
String trimmed = line.trim(); String trimmed = line.trim();
if (trimmed.isEmpty()) continue; if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue; if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue;
if (headerPattern.matcher(trimmed).find()) continue; if (HEADER_PATTERN.matcher(trimmed).find()) continue;
SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice); SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice);
if (entry != null) { if (entry != null) {
@@ -303,11 +302,11 @@ public class CMSPrintXfer {
} }
// ========================================================================= // =========================================================================
// IBM 1403/3211 Machine Carriage Control Translation // Machine Carriage Control Translation
// ========================================================================= // =========================================================================
/** /**
* Translates IBM Machine Carriage Control Channel Command bytes into formatted text bytes. * Translates Machine Carriage Control Channel Command bytes into formatted text bytes.
* *
* Command codes: * Command codes:
* 0x01: Write without line advance * 0x01: Write without line advance
@@ -1,11 +1,22 @@
package haus.nightmare.lib3270j.ft; package haus.nightmare.lib3270j.ft;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Configuration for an IND$FILE file transfer session. * Configuration for an IND$FILE file transfer session.
* Ported from x3270's ft_conf_t (ft_private.h). * Ported from x3270's ft_conf_t (ft_private.h).
*/ */
public class FTConfig { public class FTConfig {
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
/** Host operating system type */ /** Host operating system type */
public enum HostType { public enum HostType {
TSO, CMS, CICS TSO, CMS, CICS
@@ -210,26 +221,22 @@ public class FTConfig {
String trimmed = opts.trim(); String trimmed = opts.trim();
// Extract and process parenthesized or space-separated tokens // Extract and process parenthesized or space-separated tokens
java.util.regex.Pattern recfmPattern = java.util.regex.Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?"); Matcher recfmMatcher = RECFM_PATTERN.matcher(trimmed);
java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed);
if (recfmMatcher.find()) { if (recfmMatcher.find()) {
setRecfm(recfmMatcher.group(1)); setRecfm(recfmMatcher.group(1));
} }
java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?"); Matcher lreclMatcher = LRECL_PATTERN.matcher(trimmed);
java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed);
if (lreclMatcher.find()) { if (lreclMatcher.find()) {
setLrecl(lreclMatcher.group(1)); setLrecl(lreclMatcher.group(1));
} }
java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?"); Matcher blkMatcher = BLK_PATTERN.matcher(trimmed);
java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed);
if (blkMatcher.find()) { if (blkMatcher.find()) {
setBlksize(blkMatcher.group(1)); setBlksize(blkMatcher.group(1));
} }
java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?"); Matcher spaceMatcher = SPACE_PATTERN.matcher(trimmed);
java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed);
if (spaceMatcher.find()) { if (spaceMatcher.find()) {
try { try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
@@ -239,8 +246,7 @@ public class FTConfig {
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?"); Matcher avbMatcher = AVB_PATTERN.matcher(trimmed);
java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed);
if (avbMatcher.find()) { if (avbMatcher.find()) {
try { try {
this.avblock = Integer.parseInt(avbMatcher.group(1)); this.avblock = Integer.parseInt(avbMatcher.group(1));
@@ -248,14 +254,12 @@ public class FTConfig {
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?"); Matcher cpMatcher = CP_PATTERN.matcher(trimmed);
java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed);
if (cpMatcher.find()) { if (cpMatcher.find()) {
this.codePage = cpMatcher.group(1); this.codePage = cpMatcher.group(1);
} }
java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?"); Matcher mtuMatcher = MTU_PATTERN.matcher(trimmed);
java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed);
if (mtuMatcher.find()) { if (mtuMatcher.find()) {
try { try {
setDftBufferSize(Integer.parseInt(mtuMatcher.group(1))); setDftBufferSize(Integer.parseInt(mtuMatcher.group(1)));
@@ -395,13 +395,16 @@ public class GocaDecoder {
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) { if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
return 1; return 1;
} }
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00) // Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00)
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM || if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE || order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
(inImage && order == GocaConstants.G_GEIMG_ALT)) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1; return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
} }
// G_GEIMG (0x93 End Image): self-defining draw order (e.g. 93 02 00 00 or 93 00 or 93)
if (order == GocaConstants.G_GEIMG) {
return (idx + 1 < end) ? ((data[idx + 1] & 0xFF) + 2) : 1;
}
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
@@ -552,31 +555,45 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GEIMG: // 0x93 case GocaConstants.G_GEIMG: { // 0x93 End Image
case GocaConstants.G_GEIMG_ALT: { // 0x91 endImage();
if (inImage || order == GocaConstants.G_GEIMG) { idx += orderLen;
break;
}
case GocaConstants.G_GBIMGC: { // 0x91: Begin Image at Current Position (G_GBIMGC)
if (inImage) {
endImage(); endImage();
idx += orderLen;
} else {
// 0x91: Begin Image at Current Position (G_GBIMGC)
if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
int w = readCoord(inputData, idx + 2);
int h = readCoord(inputData, idx + 4);
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 5) {
int fmt = inputData[idx + 6] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 6) {
compression = inputData[idx + 7] & 0xFF;
}
beginImage(curX, curY, w, h, bitDepth, compression);
}
idx += orderLen;
} }
int w = 0, h = 0;
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
w = readCoord(inputData, idx + 4);
h = readCoord(inputData, idx + 6);
if (payloadLen >= 7) {
int fmt = inputData[idx + 8] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 8) {
compression = inputData[idx + 9] & 0xFF;
}
} else if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
w = readCoord(inputData, idx + 2);
h = readCoord(inputData, idx + 4);
if (payloadLen >= 5) {
int fmt = inputData[idx + 6] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 6) {
compression = inputData[idx + 7] & 0xFF;
}
}
beginImage(curX, curY, w, h, bitDepth, compression);
idx += orderLen;
break; break;
} }
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
@@ -986,21 +1003,39 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GBIMG: { // Begin Image (0xD1) case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
if (inImage) {
endImage();
}
if (payloadLen >= 8 && idx + 2 + payloadLen <= end) { if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
int x = readCoord(inputData, idx + 2); int x = readCoord(inputData, idx + 2);
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w, h;
int h = readCoord(inputData, idx + 8);
int bitDepth = GocaConstants.BPP_1; int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED; int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 9) {
int fmt = inputData[idx + 10] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 10) { if (payloadLen >= 10) {
compression = inputData[idx + 11] & 0xFF; w = readCoord(inputData, idx + 8);
h = readCoord(inputData, idx + 10);
if (payloadLen >= 11) {
int fmt = inputData[idx + 12] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 12) {
compression = inputData[idx + 13] & 0xFF;
}
} else {
w = readCoord(inputData, idx + 6);
h = readCoord(inputData, idx + 8);
if (payloadLen >= 9) {
int fmt = inputData[idx + 10] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 10) {
compression = inputData[idx + 11] & 0xFF;
}
} }
beginImage(x, y, w, h, bitDepth, compression); beginImage(x, y, w, h, bitDepth, compression);
} }
@@ -418,9 +418,27 @@ public class GraphicsPlane implements PixelBuffer {
return transform; return transform;
} }
// Character cell dimensions matching Query Reply presentation space
private int charWidth = 9;
private int charHeight = 16;
public synchronized void setCharDimensions(int width, int height) {
if (width > 0) this.charWidth = width;
if (height > 0) this.charHeight = height;
this.transform.setDefaultCharMetrics(this.charWidth, this.charHeight);
}
public int getCharWidth() {
return charWidth;
}
public int getCharHeight() {
return charHeight;
}
public int getTotalWidth() { public int getTotalWidth() {
int cols = screenCols > 0 ? screenCols : 80; int cols = screenCols > 0 ? screenCols : 80;
return cols * 9; return cols * charWidth;
} }
/** /**
@@ -457,7 +475,7 @@ public class GraphicsPlane implements PixelBuffer {
*/ */
public int getTotalHeight() { public int getTotalHeight() {
int rows = screenRows > 0 ? screenRows : 24; int rows = screenRows > 0 ? screenRows : 24;
return rows * 16; return rows * charHeight;
} }
public int getXMax() { public int getXMax() {
@@ -47,7 +47,7 @@ public class TelnetFSM {
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
// 3270 input buffer (accumulated between telnet framing) // 3270 input buffer (accumulated between telnet framing)
private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768); private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
// Sub-negotiation buffer // Sub-negotiation buffer
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096); private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
@@ -70,8 +70,22 @@ public class TelnetFSM {
private List<String> getCandidateTerminalTypes() { private List<String> getCandidateTerminalTypes() {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
String currentLu = null;
List<String> lus = config.getLuNames();
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
currentLu = lus.get(luIndex);
} else if (config.getLuName() != null && !config.getLuName().trim().isEmpty()) {
currentLu = config.getLuName().trim();
}
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) { if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
list.add(config.getTerminalName().trim()); String tName = config.getTerminalName().trim();
if (currentLu != null && !currentLu.isEmpty() && !tName.contains("@")) {
list.add(tName + "@" + currentLu);
list.add(tName);
} else {
list.add(tName);
}
return list; return list;
} }
if (config.isDynamicModel()) { if (config.isDynamicModel()) {
@@ -82,24 +96,41 @@ public class TelnetFSM {
list.add("IBM-3279-2"); list.add("IBM-3279-2");
list.add("IBM-3278-2"); list.add("IBM-3278-2");
list.add("UNKNOWN"); list.add("UNKNOWN");
return list; } else {
TerminalModel model = config.getModel();
list.add(model.getTerminalType());
list.add(model.getBaseTerminalType());
if (model.isColor()) {
try {
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
list.add(mono.getTerminalType());
list.add(mono.getBaseTerminalType());
} catch (Exception ignored) {}
}
if (model.getModelNumber() != 2) {
list.add("IBM-3279-2-E");
list.add("IBM-3279-2");
list.add("IBM-3278-2");
}
list.add("UNKNOWN");
} }
TerminalModel model = config.getModel();
list.add(model.getTerminalType()); if (currentLu != null && !currentLu.isEmpty()) {
list.add(model.getBaseTerminalType()); List<String> luCandidates = new ArrayList<>();
if (model.isColor()) { for (String item : list) {
try { if (!"UNKNOWN".equalsIgnoreCase(item) && !item.contains("@")) {
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false); luCandidates.add(item + "@" + currentLu);
list.add(mono.getTerminalType()); }
list.add(mono.getBaseTerminalType()); }
} catch (Exception ignored) {} for (String item : list) {
if (!"UNKNOWN".equalsIgnoreCase(item)) {
luCandidates.add(item);
}
}
luCandidates.add("UNKNOWN");
return luCandidates;
} }
if (model.getModelNumber() != 2) {
list.add("IBM-3279-2-E");
list.add("IBM-3279-2");
list.add("IBM-3278-2");
}
list.add("UNKNOWN");
return list; return list;
} }
@@ -589,6 +620,9 @@ public class TelnetFSM {
out.write(IAC); out.write(IAC);
out.write(SE); out.write(SE);
sendBytes(out.toByteArray()); sendBytes(out.toByteArray());
if (termType.contains("@")) {
connectedLu = termType.substring(termType.indexOf('@') + 1).trim();
}
log.warning(">>> SENT SB TTYPE IS " + termType + " SE"); log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
} }
} }
@@ -990,9 +1024,9 @@ public class TelnetFSM {
// ========== End of Record processing ========== // ========== End of Record processing ==========
private void processEndOfRecord() { private void processEndOfRecord() {
byte[] data = ibuf.toByteArray(); int dataLen = ibuf.size();
ibuf.reset(); if (dataLen == 0) return;
if (data.length == 0) return; byte[] data = ibuf.buffer();
if ((connectionState == ConnectionState.TELNET_PENDING || if ((connectionState == ConnectionState.TELNET_PENDING ||
connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT ||
@@ -1003,14 +1037,15 @@ public class TelnetFSM {
if (tn3270eNegotiated) { if (tn3270eNegotiated) {
// TN3270E mode: data starts with 5-byte header // TN3270E mode: data starts with 5-byte header
processTN3270ERecord(data); processTN3270ERecord(data, 0, dataLen);
} else { } else {
// Plain TN3270 mode: data is raw 3270 data stream // Plain TN3270 mode: data is raw 3270 data stream
if (dsProcessor != null) { if (dsProcessor != null) {
dsProcessor.processRecord(data, 0, data.length, false); dsProcessor.processRecord(data, 0, dataLen, false);
} }
notifyScreenUpdate(); notifyScreenUpdate();
} }
ibuf.reset();
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR // Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
if (dsProcessor != null) { if (dsProcessor != null) {
@@ -1062,19 +1097,23 @@ public class TelnetFSM {
} }
public void processTn3270eHeader(byte[] data) { public void processTn3270eHeader(byte[] data) {
processTN3270ERecord(data); processTN3270ERecord(data, 0, data != null ? data.length : 0);
} }
private void processTN3270ERecord(byte[] data) { private void processTN3270ERecord(byte[] data) {
if (data.length < EH_SIZE) { processTN3270ERecord(data, 0, data != null ? data.length : 0);
log.warning("TN3270E record too short: " + data.length); }
private void processTN3270ERecord(byte[] data, int offset, int length) {
if (data == null || length < EH_SIZE) {
log.warning("TN3270E record too short: " + length);
return; return;
} }
int dataType = data[0] & 0xFF; int dataType = data[offset] & 0xFF;
int requestFlag = data[1] & 0xFF; int requestFlag = data[offset + 1] & 0xFF;
int responseFlag = data[2] & 0xFF; int responseFlag = data[offset + 2] & 0xFF;
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF); int seqNumber = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
this.sdi_flag = (requestFlag & 0x01) != 0; this.sdi_flag = (requestFlag & 0x01) != 0;
this.kri_flag = (requestFlag & 0x02) != 0; this.kri_flag = (requestFlag & 0x02) != 0;
@@ -1089,7 +1128,7 @@ public class TelnetFSM {
switch (dataType) { switch (dataType) {
case DT_3270_DATA: case DT_3270_DATA:
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
// Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP) // Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
if (connectionState != ConnectionState.CONNECTED_TN3270E) { if (connectionState != ConnectionState.CONNECTED_TN3270E) {
// Clear screen on transition to 3270 mode from unbound/SSCP/NVT // Clear screen on transition to 3270 mode from unbound/SSCP/NVT
@@ -1099,7 +1138,7 @@ public class TelnetFSM {
tn3270eSubmode = TN3270ESubmode.E_3270; tn3270eSubmode = TN3270ESubmode.E_3270;
} }
try { try {
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false); dsProcessor.processRecord(data, offset + EH_SIZE, length - EH_SIZE, false);
notifyScreenUpdate(); notifyScreenUpdate();
// Send positive response if required // Send positive response if required
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
@@ -1119,9 +1158,9 @@ public class TelnetFSM {
break; break;
case DT_SCS_DATA: case DT_SCS_DATA:
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
try { try {
processSCSInbound(data, EH_SIZE, data.length - EH_SIZE); processSCSInbound(data, offset + EH_SIZE, length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
} }
@@ -1148,9 +1187,9 @@ public class TelnetFSM {
changeState(ConnectionState.CONNECTED_SSCP); changeState(ConnectionState.CONNECTED_SSCP);
tn3270eSubmode = TN3270ESubmode.E_SSCP; tn3270eSubmode = TN3270ESubmode.E_SSCP;
} }
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
try { try {
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE); dsProcessor.processSscpLuData(data, offset + EH_SIZE, length - EH_SIZE);
notifyScreenUpdate(); notifyScreenUpdate();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
@@ -1169,11 +1208,15 @@ public class TelnetFSM {
break; break;
case DT_BIND_IMAGE: case DT_BIND_IMAGE:
process_bind(data, responseFlag, seqNumber); {
byte[] bindData = new byte[length];
System.arraycopy(data, offset, bindData, 0, length);
process_bind(bindData, responseFlag, seqNumber);
}
break; break;
case DT_UNBIND: case DT_UNBIND:
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL; int unbindReason = (length > EH_SIZE) ? (data[offset + EH_SIZE] & 0xFF) : UNBIND_NORMAL;
process_unbind(unbindReason, responseFlag, seqNumber); process_unbind(unbindReason, responseFlag, seqNumber);
break; break;
@@ -1186,9 +1229,9 @@ public class TelnetFSM {
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) { if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false); dsProcessor.getInputProcessor().setKeyboardLocked(false);
} }
if (data.length > EH_SIZE) { if (length > EH_SIZE) {
try { try {
processNVTData(data, EH_SIZE, data.length - EH_SIZE); processNVTData(data, offset + EH_SIZE, length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
} }
@@ -1243,12 +1286,12 @@ public class TelnetFSM {
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream. // This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F || if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 || dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) { dataType == 0x0D || (length >= 2 && (data[offset] & 0xFF) == 0x11)) {
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) + log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
") in TN3270E mode — automatically switching to plain TN3270 mode"); ") in TN3270E mode — automatically switching to plain TN3270 mode");
tn3270eNegotiated = false; tn3270eNegotiated = false;
changeState(ConnectionState.CONNECTED_3270); changeState(ConnectionState.CONNECTED_3270);
dsProcessor.processRecord(data, 0, data.length, true); dsProcessor.processRecord(data, offset, length, true);
notifyScreenUpdate(); notifyScreenUpdate();
} else { } else {
log.info("Unhandled TN3270E data type: " + dataType); log.info("Unhandled TN3270E data type: " + dataType);
@@ -1257,6 +1300,7 @@ public class TelnetFSM {
} }
} }
public void processSCSInbound(byte[] data) { public void processSCSInbound(byte[] data) {
if (data == null) return; if (data == null) return;
processSCSInbound(data, 0, data.length); processSCSInbound(data, 0, data.length);
@@ -1289,7 +1333,7 @@ public class TelnetFSM {
} }
/** /**
* HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E). * HoD 5-byte send_response compatible signature.
*/ */
public void send_response(short s, short s2, int n) { public void send_response(short s, short s2, int n) {
byte[] byArray = new byte[5]; byte[] byArray = new byte[5];
@@ -26,8 +26,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* Core 3270 File Transfer Controller conforming to IBM Host On-Demand * Core 3270 File Transfer Controller conforming to Host On-Demand specifications.
* (com.ibm.eNetwork.ECL.xfer3270.Xfer3270).
* *
* Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options, * Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options,
* dynamic MTU buffering, host/local dataset name mappings, directory queries, * dynamic MTU buffering, host/local dataset name mappings, directory queries,
@@ -37,6 +36,13 @@ public class Xfer3270 implements FileTransferInterface {
private static final Logger log = Logger.getLogger(Xfer3270.class.getName()); private static final Logger log = Logger.getLogger(Xfer3270.class.getName());
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
public static final String UNICODE_UCS2_STR = "UCS2"; public static final String UNICODE_UCS2_STR = "UCS2";
public static final String UNICODE_UTF8_STR = "UTF8"; public static final String UNICODE_UTF8_STR = "UTF8";
public static final String UNICODE_UTF_8_STR = "UTF-8"; public static final String UNICODE_UTF_8_STR = "UTF-8";
@@ -175,20 +181,20 @@ public class Xfer3270 implements FileTransferInterface {
} }
// Mainframe dataset parameters // Mainframe dataset parameters
Matcher recfmMatcher = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?").matcher(options); Matcher recfmMatcher = RECFM_PATTERN.matcher(options);
if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase(); if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase();
Matcher lreclMatcher = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher lreclMatcher = LRECL_PATTERN.matcher(options);
if (lreclMatcher.find()) { if (lreclMatcher.find()) {
try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {} try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {}
} }
Matcher blkMatcher = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher blkMatcher = BLK_PATTERN.matcher(options);
if (blkMatcher.find()) { if (blkMatcher.find()) {
try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {} try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {}
} }
Matcher spaceMatcher = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?").matcher(options); Matcher spaceMatcher = SPACE_PATTERN.matcher(options);
if (spaceMatcher.find()) { if (spaceMatcher.find()) {
try { try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
@@ -198,7 +204,7 @@ public class Xfer3270 implements FileTransferInterface {
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
Matcher avbMatcher = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher avbMatcher = AVB_PATTERN.matcher(options);
if (avbMatcher.find()) { if (avbMatcher.find()) {
try { try {
this.avblock = Integer.parseInt(avbMatcher.group(1)); this.avblock = Integer.parseInt(avbMatcher.group(1));
@@ -212,7 +218,7 @@ public class Xfer3270 implements FileTransferInterface {
this.spaceUnits = "CYLINDERS"; this.spaceUnits = "CYLINDERS";
} }
Matcher mtuMatcher = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?").matcher(options); Matcher mtuMatcher = MTU_PATTERN.matcher(options);
if (mtuMatcher.find()) { if (mtuMatcher.find()) {
try { try {
SetMTUSize(Integer.parseInt(mtuMatcher.group(1))); SetMTUSize(Integer.parseInt(mtuMatcher.group(1)));
@@ -82,4 +82,120 @@ public class DataStreamProcessorTest {
assertNotNull(sentData.get()); assertNotNull(sentData.get());
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID"); assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
} }
@Test
public void testCharacterAttributesPersistAcrossStartField() throws java.io.IOException {
// User's exact ISPF Option 0 sequence:
// SBA(7, 1) -> SF(prot,skip) -> SA(yellow) -> ' 4 ' -> SBA(7, 7) -> SF(prot,skip) -> 'DISPLAY'
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3); // WCC
// SBA(7, 1) -> address 7*80 + 1 = 561
byte[] addr1 = encodeAddress(561, 24, 80);
stream.write(ORDER_SBA);
stream.write(addr1);
// SF(prot, skip)
stream.write(ORDER_SF);
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
// SA(yellow) -> XA_FOREGROUND, COLOR_YELLOW (0xF6)
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
// Data: ' 4 '
stream.write(translator.stringToEbcdic(" 4 "));
// SBA(7, 7) -> address 7*80 + 7 = 567
byte[] addr2 = encodeAddress(567, 24, 80);
stream.write(ORDER_SBA);
stream.write(addr2);
// SF(prot, skip)
stream.write(ORDER_SF);
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
// Data: 'DISPLAY'
stream.write(translator.stringToEbcdic("DISPLAY"));
byte[] record = stream.toByteArray();
processor.processRecord(record, 0, record.length, true);
// Positions 562..566 (' 4 ') must be yellow (0xF6)
for (int i = 562; i <= 566; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should have yellow foreground (0xF6)");
}
// Positions 568..574 ('DISPLAY') across the second SF must also retain yellow (0xF6)
for (int i = 568; i <= 574; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " ('DISPLAY') should retain yellow foreground (0xF6)");
}
// Now test that SA with XA_ALL resets character attributes to default (0)
java.io.ByteArrayOutputStream resetStream = new java.io.ByteArrayOutputStream();
resetStream.write(CMD_W);
resetStream.write(0xC3);
resetStream.write(ORDER_SA);
resetStream.write(XA_ALL);
resetStream.write(0x00);
resetStream.write(translator.stringToEbcdic("TEST"));
byte[] resetRecord = resetStream.toByteArray();
processor.processRecord(resetRecord, 0, resetRecord.length, true);
for (int i = 575; i < 579; i++) {
assertEquals((byte) 0, screen.getCell(i).fg, "Cell at " + i + " should have default foreground (0) after SA(XA_ALL)");
}
}
@Test
public void testCharacterAttributesPersistAcrossStartFieldExtended() throws java.io.IOException {
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3);
// SA(yellow)
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
// SFE with 1 pair (3270 FA)
stream.write(ORDER_SFE);
stream.write(0x01); // 1 pair
stream.write(XA_3270);
stream.write(FA_PRINTABLE);
// Data 'HELLO'
stream.write(translator.stringToEbcdic("HELLO"));
byte[] record = stream.toByteArray();
processor.processRecord(record, 0, record.length, true);
// Check cells of HELLO (positions 1..5) have fg == 0xF6
for (int i = 1; i <= 5; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should retain yellow foreground across SFE");
}
}
@Test
public void testCharacterAttributesResetOnErase() throws java.io.IOException {
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3);
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
stream.write(translator.stringToEbcdic("A"));
processor.processRecord(stream.toByteArray(), 0, stream.size(), true);
assertEquals((byte) 0xF6, screen.getCell(0).fg);
// New EW command resets attributes
java.io.ByteArrayOutputStream ewStream = new java.io.ByteArrayOutputStream();
ewStream.write(CMD_EW);
ewStream.write(0xC3);
ewStream.write(translator.stringToEbcdic("B"));
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
assertEquals((byte) 0, screen.getCell(0).fg);
}
} }
@@ -0,0 +1,154 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for high-throughput zero-allocation buffer primitives.
*/
public class FastByteBufferTest {
@BeforeEach
public void setUp() {
ReusableByteBufferPool.clear();
}
@Test
@DisplayName("FastByteBuffer operations: write, grow, reset, slice, and direct array access")
public void testFastByteBufferBasicOperations() {
FastByteBuffer buf = new FastByteBuffer(16);
assertEquals(0, buf.size());
assertTrue(buf.buffer().length >= 16);
buf.write(0x11);
buf.write(0x22);
assertEquals(2, buf.size());
assertEquals((byte) 0x11, buf.buffer()[0]);
assertEquals((byte) 0x22, buf.buffer()[1]);
byte[] payload = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05};
buf.write(payload, 0, payload.length);
assertEquals(7, buf.size());
buf.write(payload, 1, 3); // write 0x02, 0x03, 0x04
assertEquals(10, buf.size());
// Test auto-growth
byte[] largeData = new byte[100];
for (int i = 0; i < largeData.length; i++) {
largeData[i] = (byte) (i & 0xFF);
}
buf.write(largeData, 0, largeData.length);
assertEquals(110, buf.size());
assertTrue(buf.buffer().length >= 110);
// Verify direct array access
byte[] raw = buf.buffer();
assertNotNull(raw);
assertEquals((byte) 0x11, raw[0]);
assertEquals((byte) 0x22, raw[1]);
// Verify ByteBuffer view
ByteBuffer readOnly = buf.asByteBuffer();
assertEquals(110, readOnly.remaining());
assertEquals((byte) 0x11, readOnly.get());
// Verify ByteBuffer slice
ByteBuffer slice = buf.slice(2, 5);
assertEquals(5, slice.remaining());
assertEquals((byte) 0x01, slice.get(0));
assertEquals((byte) 0x05, slice.get(4));
// Verify copied array
byte[] copied = buf.toByteArray();
assertEquals(110, copied.length);
assertEquals((byte) 0x11, copied[0]);
// Test reset
buf.reset();
assertEquals(0, buf.size());
// Buffer retained for zero-allocation reuse
assertTrue(buf.buffer().length >= 110);
}
@Test
@DisplayName("ReusableByteBufferPool acquires, releases, and recycles tiered buffers")
public void testByteBufferPoolRecycling() {
byte[] small1 = ReusableByteBufferPool.acquire(256);
assertEquals(ReusableByteBufferPool.SIZE_SMALL, small1.length);
byte[] medium1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_MEDIUM);
assertEquals(ReusableByteBufferPool.SIZE_MEDIUM, medium1.length);
byte[] large1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_LARGE);
assertEquals(ReusableByteBufferPool.SIZE_LARGE, large1.length);
// Non-standard oversized buffer
byte[] huge = ReusableByteBufferPool.acquire(65536);
assertEquals(65536, huge.length);
// Release back to pool
ReusableByteBufferPool.release(small1);
ReusableByteBufferPool.release(medium1);
ReusableByteBufferPool.release(large1);
ReusableByteBufferPool.release(huge);
// Next acquire should reuse the released instances
byte[] small2 = ReusableByteBufferPool.acquire(128);
assertSame(small1, small2, "Small buffer should be recycled from pool");
byte[] medium2 = ReusableByteBufferPool.acquire(2048);
assertSame(medium1, medium2, "Medium buffer should be recycled from pool");
byte[] large2 = ReusableByteBufferPool.acquire(30000);
assertSame(large1, large2, "Large buffer should be recycled from pool");
ByteBuffer bb = ReusableByteBufferPool.acquireByteBuffer(ReusableByteBufferPool.SIZE_SMALL);
assertNotNull(bb);
assertEquals(ReusableByteBufferPool.SIZE_SMALL, bb.capacity());
}
@Test
@DisplayName("ReusableByteBufferPool is safe under concurrent acquisition and release")
public void testConcurrentPoolAccess() throws InterruptedException {
int threads = 8;
int iterations = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threads);
CountDownLatch latch = new CountDownLatch(threads);
AtomicInteger failures = new AtomicInteger(0);
for (int t = 0; t < threads; t++) {
executor.submit(() -> {
try {
for (int i = 0; i < iterations; i++) {
byte[] buf = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_SMALL);
if (buf == null || buf.length != ReusableByteBufferPool.SIZE_SMALL) {
failures.incrementAndGet();
}
buf[0] = (byte) 0xAA;
buf[1] = (byte) 0xBB;
ReusableByteBufferPool.release(buf);
}
} catch (Exception e) {
failures.incrementAndGet();
} finally {
latch.countDown();
}
});
}
assertTrue(latch.await(5, TimeUnit.SECONDS));
executor.shutdown();
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
}
}
@@ -0,0 +1,141 @@
package haus.nightmare.lib3270j.epi;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EpiDataStream3270Test {
private Screen screen;
private DataStream3270 dataStream;
@BeforeEach
public void setUp() {
screen = new Screen(80, 24);
dataStream = new DataStream3270(screen);
}
@Test
public void testEncodeAndDecodeAddress() {
int[] testAddresses = {0, 1, 79, 80, 1919, 2000, 4095};
byte[] target = new byte[2];
for (int addr : testAddresses) {
dataStream.encodeAddress(target, 0, addr);
int decoded = dataStream.decodeAddress(target[0], target[1]);
assertEquals(addr, decoded, "Address round-trip mismatch for " + addr);
}
// Test invalid decode
assertEquals(-1, dataStream.decodeAddress((byte) 0x00, (byte) 0x00));
assertEquals(-1, dataStream.decodeAddress((byte) 0xFF, (byte) 0xFF));
}
@Test
public void testCharacterTranslations() {
char ebcdicChar = dataStream.toEbcdic('A'); // ASCII 65 -> EBCDIC 0xC1
assertEquals('\u00c1', ebcdicChar);
byte asciiByte = dataStream.toAscii(ebcdicChar); // EBCDIC 0xC1 -> ASCII 65
assertEquals('A', (char) asciiByte);
// Boundary cases
char low = dataStream.toEbcdic(0x10);
assertEquals((char) 0x10, low);
}
@Test
public void testAnalyzeWriteAndFormat() throws Exception {
// Build 3270 Write buffer:
// CMD_W (49 / 0x31), WCC (0xC3), SBA (17), Addr(0, 0), SF (29), Attr (0xC1 = MDT set), Text "TEST"
byte[] stream = new byte[11];
stream[0] = 49; // Write
stream[1] = (byte) 0xC3; // WCC
stream[2] = 17; // SBA
dataStream.encodeAddress(stream, 3, 0);
stream[5] = 29; // SF
stream[6] = (byte) 0xC1; // Attribute (MDT=1)
stream[7] = (byte) 'T';
stream[8] = (byte) 'E';
stream[9] = (byte) 'S';
stream[10] = (byte) 'T';
dataStream.analyze(stream, stream.length);
assertEquals(1, screen.fieldCount());
Field f = screen.field(1);
assertNotNull(f);
assertTrue(f.hasAttribute());
assertEquals(1, f.dataTag());
// Now format outbound buffer
screen.setAID(AID.enter);
screen.setCursor(1, 1);
byte[] outBuf = new byte[100];
int outLen = dataStream.format(outBuf);
assertTrue(outLen > 0);
assertEquals(AID.enter.translate(), outBuf[0]);
// Cursor addr at [1, 2]
// SBA (17) at [3]
assertEquals(17, outBuf[3]);
// Data text starting at [6]
assertEquals((byte) 'T', outBuf[6]);
assertEquals((byte) 'E', outBuf[7]);
assertEquals((byte) 'S', outBuf[8]);
assertEquals((byte) 'T', outBuf[9]);
}
@Test
public void testReadBufferSerialization() throws Exception {
// Erase/Write (53), WCC (0), SBA (17), Addr(0), SF (29), Attr(0xC0), Text "OK"
byte[] stream = new byte[9];
stream[0] = 53; // Erase / Write
stream[1] = 0; // WCC
stream[2] = 17; // SBA
dataStream.encodeAddress(stream, 3, 0);
stream[5] = 29; // SF
stream[6] = (byte) 0xC0; // Unmodified attribute
stream[7] = (byte) 'O';
stream[8] = (byte) 'K';
dataStream.analyze(stream, stream.length);
assertEquals(1, screen.fieldCount());
byte[] outBuf = new byte[100];
int len = dataStream.readBuffer(outBuf);
assertTrue(len >= 8);
assertEquals(screen.getAID().translate(), outBuf[0]);
assertEquals(17, outBuf[3]); // SBA
assertEquals(29, outBuf[6]); // SF
}
@Test
public void testEpiScreenBufferBridge() {
screen.setWidth(80);
screen.setDepth(24);
screen.setCursor(2, 5);
Field f = new Field(screen, 80);
f.setAttribute(true);
f.setBaseAttribute((char) 0xC8);
f.setBytes(0, new byte[]{(byte) 0xC1, (byte) 0xC2}, 2); // 'A', 'B'
screen.insertField(f);
ScreenBuffer sb = new ScreenBuffer();
EpiScreenBufferBridge.copyToScreenBuffer(screen, sb);
assertEquals(80, sb.getCols());
assertEquals(24, sb.getRows());
assertEquals(84, sb.getCursorAddress()); // row 2 (index 1) * 80 + col 5 (index 4) = 84
// Reverse copy
Screen backScreen = new Screen();
EpiScreenBufferBridge.copyFromScreenBuffer(sb, backScreen);
assertEquals(80, backScreen.getWidth());
assertEquals(24, backScreen.getDepth());
assertEquals(2, backScreen.getCursorRow());
assertEquals(5, backScreen.getCursorColumn());
}
}
@@ -285,6 +285,79 @@ public class GocaDecoderTest {
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding"); assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
} }
@Test
public void testImonGddmImageRendering() throws Exception {
GraphicsPlane plane = new GraphicsPlane(1040, 1118);
plane.setCharDimensions(13, 26);
plane.setScreenDimensions(80, 43);
GocaDecoder decoder = new GocaDecoder(plane);
assertEquals(1040, plane.getTotalWidth());
assertEquals(1118, plane.getTotalHeight());
assertEquals(520, plane.getXMax());
assertEquals(558, plane.getYMax());
ByteArrayOutputStream out = new ByteArrayOutputStream();
// GSCP: Set Current Position (377, 494)
out.write(GocaConstants.G_GSCP);
out.write(0x04);
out.write(0x01); out.write(0x79); // X = 377
out.write(0x01); out.write(0xEE); // Y = 494
// GSCOL: Color 1 = Blue
out.write(GocaConstants.G_GSCOL);
out.write(0x01);
// G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80
out.write(GocaConstants.G_GBIMGC);
out.write(0x06);
out.write(0x00); out.write(0x00); // flags/format
out.write(0x00); out.write(0x50); // w = 80
out.write(0x00); out.write(0x50); // h = 80
// G_GIMD (0x92): 10 bytes scanline
out.write(GocaConstants.G_GIMD);
out.write(0x0A);
out.write(new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF });
// G_GEIMG (0x93): End Image with 2-byte operand (02 00 00 as sent by IMON)
out.write(GocaConstants.G_GEIMG);
out.write(0x02);
out.write(0x00); out.write(0x00);
// Subsequent order to verify stream synchronization: GSCOL Color 2 = Red
out.write(GocaConstants.G_GSCOL);
out.write(0x02);
// G_GBIMGC (0x91): Second image tile
out.write(GocaConstants.G_GBIMGC);
out.write(0x06);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x50);
out.write(0x00); out.write(0x50);
out.write(GocaConstants.G_GIMD);
out.write(0x0A);
out.write(new byte[] { (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA });
out.write(GocaConstants.G_GEIMG);
out.write(0x02);
out.write(0x00); out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected plane to have content after IMON GOCA decoding");
int mappedX = plane.mapX(377);
int mappedY = plane.mapY(494);
assertEquals(897, mappedX);
assertEquals(64, mappedY);
int[] rgb = plane.getRgbBuffer();
int pixelAtImg = rgb[mappedY * 1040 + mappedX];
assertNotEquals(0, pixelAtImg, "Pixel at image location should be rendered");
}
@Test @Test
public void testDirectObjectControlSf24() { public void testDirectObjectControlSf24() {
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer( haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
@@ -84,6 +84,41 @@ public class TelnetFSMTest {
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4); ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c4.isTn3270eEnabled()); assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.ibm.com", c4.getHost()); assertEquals("vm.ibm.com", c4.getHost());
ConnectionConfig c5 = ConnectionConfig.parseHostString("00C2@mvs.hugfreevikings.wtf:1023", 23, TerminalModel.IBM_3279_4);
assertEquals("mvs.hugfreevikings.wtf", c5.getHost());
assertEquals(1023, c5.getPort());
assertEquals("00C2", c5.getLuName());
ConnectionConfig c6 = ConnectionConfig.parseHostString("L:TSO01@secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
assertTrue(c6.isUseTls());
assertEquals("secure.mvs.com", c6.getHost());
assertEquals(992, c6.getPort());
assertEquals("TSO01", c6.getLuName());
}
@Test
public void testPlainTn3270TTypeWithLuName() {
config.setTn3270eEnabled(false);
config.setLuName("00C2");
fsm.onConnected();
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// Host sends SB TTYPE SEND
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND,
TelnetConstants.IAC, TelnetConstants.SE);
boolean foundTtypeLu = false;
for (byte[] pkt : connection.sentData) {
String s = new String(pkt, java.nio.charset.StandardCharsets.ISO_8859_1);
if (s.contains("IBM-3279-4-E@00C2")) {
foundTtypeLu = true;
break;
}
}
assertTrue(foundTtypeLu, "Plain TN3270 TTYPE IS must append @<luName> when configured");
assertEquals("00C2", fsm.getConnectedLu());
} }
@Test @Test