diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index bd7fa9a..d718066 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -712,6 +712,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate return terminalPanel; } + public Telnet3270Client getClient() { + return client; + } + void connect(ConnectionConfig config) { lastHost = config.getHost(); lastPort = config.getPort(); @@ -1167,6 +1171,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); config.setTlsVerifyCert(verify); 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()); if (finalGraphicsMode != null) { config.setGraphicsMode(finalGraphicsMode); diff --git a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java index b866282..a55fef6 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java +++ b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java @@ -141,6 +141,14 @@ public class Settings { flushPrefs(); } + public static boolean getAutoReconnect() { + return getAutoConnectAutoReconnect(); + } + + public static void setAutoReconnect(boolean autoReconnect) { + setAutoConnectAutoReconnect(autoReconnect); + } + public static int getAutoConnectReconnectMaxRetries() { return prefs.getInt("autoConnectReconnectMaxRetries", 5); } @@ -150,6 +158,32 @@ public class Settings { 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() { String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name()); return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr); @@ -574,6 +608,26 @@ public class Settings { setDynamicCols(Integer.parseInt(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 "auto_sys_unlock": setAutoSysUnlock(Boolean.parseBoolean(value)); @@ -729,6 +783,10 @@ public class Settings { w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicCols = " + getDynamicCols()); 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("enablePasteFromExcel = " + getEnablePasteFromExcel()); w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine()); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java index 9bcfb40..852acfd 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java @@ -31,6 +31,9 @@ public class SettingsDialog extends JDialog { private JPanel autoConnectPanel; private JTextField hostField; private JTextField portField; + private JCheckBox autoReconnectCheck; + private JCheckBox inputMaskCheck; + private JTextField inputMaskCharField; private JCheckBox blockSelectCheck; private JSpinner dynamicRowsSpinner; private JSpinner dynamicColsSpinner; @@ -268,14 +271,45 @@ public class SettingsDialog extends JDialog { }); autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT); - // Block select mode checkbox + // Auto-Reconnect checkbox + gbc.gridx = 0; gbc.gridy = 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()); panel.add(blockSelectCheck, gbc); // Default Dynamic Screen Size - gbc.gridy = 3; + gbc.gridy = 5; gbc.gridwidth = 1; gbc.gridx = 0; panel.add(new JLabel("Default Dynamic Screen:"), gbc); @@ -296,16 +330,16 @@ public class SettingsDialog extends JDialog { // Clipboard & Tabular Paste options gbc.gridx = 0; - gbc.gridy = 4; + gbc.gridy = 6; gbc.gridwidth = 2; enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel()); panel.add(enablePasteFromExcelCheck, gbc); - gbc.gridy = 5; + gbc.gridy = 7; pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine()); panel.add(pasteStopAtProtectedCheck, gbc); - gbc.gridy = 6; + gbc.gridy = 8; gbc.weighty = 1.0; 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 Settings.setBlockSelectMode(blockSelectCheck.isSelected()); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index 15663f9..052d29a 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -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 boolean crosshairRulerEnabled = false; 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 Image wallpaperImage = null; private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null; @@ -1239,12 +1241,32 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { applyModeSettings(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); 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(); cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; revalidate(); 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) { currentFontSize = size; haus.nightmare.j3270.config.Settings.setFontSize(size); @@ -1527,12 +1549,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { // Password fields if (faIsZero(currentFA & 0xFF)) { - char ch = ea.ucs4; - if (ch > 0x20 && ch != 0xFF) { - Font f = bold ? boldTerminalFont : terminalFont; - g2.setFont(f); - g2.setColor(fgColor); - g2.drawString("*", x, y + fontAscent); + if (inputMaskEnabled) { + char ch = ea.ucs4; + if (ch > 0x20 && ch != 0xFF) { + Font f = bold ? boldTerminalFont : terminalFont; + g2.setFont(f); + g2.setColor(fgColor); + String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*"; + g2.drawString(mask, x, y + fontAscent); + } } if (isCellSelected(row, col)) { diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java new file mode 100644 index 0000000..025d23e --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java @@ -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) { + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java index b222048..4a08155 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java @@ -4,10 +4,10 @@ import java.util.*; 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). * 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). */ public class EbcdicTranslator { @@ -210,9 +210,11 @@ public class EbcdicTranslator { */ public char ebcdicToUnicode(int ebc) { int b = ebc & 0xFF; - Character custom = customEbcdicToUnicode.get(b); - if (custom != null) { - return custom; + if (!customEbcdicToUnicode.isEmpty()) { + Character custom = customEbcdicToUnicode.get(b); + if (custom != null) { + return custom; + } } return activeCodePage.ebcdicToUnicode(b); } @@ -236,9 +238,11 @@ public class EbcdicTranslator { * Returns -1 if the character cannot be mapped. */ public int unicodeToEbcdic(char unicode) { - Integer custom = customUnicodeToEbcdic.get(unicode); - if (custom != null) { - return custom; + if (!customUnicodeToEbcdic.isEmpty()) { + Integer custom = customUnicodeToEbcdic.get(unicode); + if (custom != null) { + return custom; + } } 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. - * Conforms to IBM 3270 APL / Text character set and GA23-0059 specification. + * Map a 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph. + * Conforms to 3270 APL / Text character set and GA23-0059 specification. */ public char mapAPL(int ebcdicCodePoint) { switch (ebcdicCodePoint & 0xFF) { @@ -331,7 +335,7 @@ public class EbcdicTranslator { case 0xBF: return '\u00B5'; // Micro 'µ' case 0x5F: return '\u00AC'; // Not sign '¬' - // IBM 3270 APL Operational & Structural Glyphs + // 3270 APL Operational & Structural Glyphs case 0x80: return '\u22C4'; // Diamond '⋄' case 0x81: return '\u237A'; // APL Alpha '⍺' 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) { return mapAPL(ec); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java new file mode 100644 index 0000000..cfad999 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java index e00d969..385c902 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java @@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; /** * 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 { @@ -18,10 +18,102 @@ public class QueryReplyBuilder { private static final int SW_3279_2 = 0x09; 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 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 GraphicsMode graphicsMode = GraphicsMode.BOTH; @@ -101,27 +193,27 @@ public class QueryReplyBuilder { appendQueryReply(out, QR_CHARSETS, buildCharsets()); // Color (0x86) - appendQueryReply(out, QR_COLOR, buildColor()); + appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR); // Highlighting (0x87) - appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); + appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING); // 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()); if (isDbcs) { // Outlining (0x8C) - appendQueryReply(out, QR_OUTLINING, buildOutlining()); + appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING); // DBCS Asia (0x91) - appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); + appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA); } // Distributed Data Management (0x95) appendQueryReply(out, QR_DDM, buildDdm(4096)); // Auxiliary Devices (0x99) - appendQueryReply(out, QR_AUXDA, buildAuxDa()); + appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA); // Implicit Partition (0xA6) 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_ALPHA_PART, buildAlphaPartitions(maxRows)); appendQueryReply(out, QR_CHARSETS, buildCharsets()); - appendQueryReply(out, QR_COLOR, buildColor()); - appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); - appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); + appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR); + appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING); + 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()); 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_AUXDA, buildAuxDa()); + appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8 - appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0 - appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1 - appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2 - appendPort(out); // 0xB3 - appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4 - appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6 + appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8 + appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0 + appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1 + appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2 + appendPort(out); // 0xB3 + appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); // 0xB4 + appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6 } log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")"); @@ -197,20 +289,20 @@ public class QueryReplyBuilder { appendQueryReply(out, QR_CHARSETS, buildCharsets()); break; case QR_COLOR: - appendQueryReply(out, QR_COLOR, buildColor()); + appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR); break; case QR_HIGHLIGHTING: - appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); + appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING); break; case QR_REPLY_MODES: - appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); + appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES); break; case QR_OUTLINING: // 0x8C - appendQueryReply(out, QR_OUTLINING, buildOutlining()); + appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING); break; case QR_DBCS_ASIA: // 0x91 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 { appendQueryReply(out, QR_NULL, new byte[0]); } @@ -219,35 +311,35 @@ public class QueryReplyBuilder { appendQueryReply(out, QR_DDM, buildDdm(4096)); break; case QR_AUXDA: // 0x99 - appendQueryReply(out, QR_AUXDA, buildAuxDa()); + appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA); break; case QR_IMP_PART: // 0xA6 appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); break; case QR_TRANSPARENCY: // 0xA8 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); + appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; case QR_SEGMENT: // 0xB0 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); + appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; case QR_PROCEDURE: // 0xB1 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); + appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; case QR_LINETYPE: // 0xB2 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_LINETYPE, buildLineType()); + appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); } else { appendQueryReply(out, QR_NULL, new byte[0]); } @@ -261,7 +353,7 @@ public class QueryReplyBuilder { break; case QR_GRCOLOR: // 0xB4 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_GRCOLOR, buildGrColor()); + appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); } else { appendQueryReply(out, QR_NULL, new byte[0]); } @@ -321,13 +413,13 @@ public class QueryReplyBuilder { out.write(maxCols & 0xFF); // usable width low out.write((maxRows >> 8) & 0xFF); // usable height high out.write(maxRows & 0xFF); // usable height low - out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) - // Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) + out.write(0x00); // units (0x00 = inches, matching HOD QR_USEAREA_STRING) + // Xr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI) out.write((Xr_HOD >> 24) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF); out.write((Xr_HOD >> 8) & 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 >> 16) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF); @@ -442,31 +534,15 @@ public class QueryReplyBuilder { } public byte[] buildColor() { - // Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) - 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 - }; + return STATIC_QR_COLOR.clone(); } public byte[] buildHighlighting() { - // Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) - return new byte[] { - 0x04, 0x00, (byte) 0xF0, - (byte) 0xF1, (byte) 0xF1, - (byte) 0xF2, (byte) 0xF2, - (byte) 0xF4, (byte) 0xF4 - }; + return STATIC_QR_HIGHLIGHTING.clone(); } public byte[] buildReplyModes() { - return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; + return STATIC_QR_REPLY_MODES.clone(); } public byte[] buildDdm() { @@ -517,39 +593,27 @@ public class QueryReplyBuilder { } public byte[] buildOutlining() { - // HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000") - return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + return STATIC_QR_OUTLINING.clone(); } public byte[] buildDbcsAsia() { - // HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001") - return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 }; + return STATIC_QR_DBCS_ASIA.clone(); } public byte[] buildAuxDa() { - // HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000") - return new byte[]{ 0x00, 0x00 }; + return STATIC_QR_AUXDA.clone(); } public byte[] buildTransparency() { - // HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff") - return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }; + return STATIC_QR_TRANSPARENCY.clone(); } public byte[] buildSegment() { - int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; - int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS; - return buildSegment(maxCols, maxRows); + return STATIC_QR_SEGMENT.clone(); } 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 new byte[]{ - (byte) 0x80, 0x02, - 0x00, 0x00, - 0x00, (byte) 0xFC, - 0x00 - }; + return STATIC_QR_SEGMENT.clone(); } public byte[] buildGraphics() { @@ -561,21 +625,11 @@ public class QueryReplyBuilder { } public byte[] buildProcedure() { - int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; - int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS; - return buildProcedure(maxCols, maxRows); + return STATIC_QR_PROCEDURE.clone(); } 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 new byte[]{ - 0x00, 0x01, - 0x00, 0x00, - 0x00, (byte) 0xFC, - 0x00, - 0x06, 0x40, 0x06, 0x40, 0x06, 0x01, - (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0 - }; + return STATIC_QR_PROCEDURE.clone(); } public byte[] buildGImage() { @@ -587,12 +641,7 @@ public class QueryReplyBuilder { } 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 new byte[]{ - 0x00, 0x09, 0x00, 0x07, - 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, - 0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08 - }; + return STATIC_QR_LINETYPE.clone(); } public byte[] buildAuxDev() { @@ -604,19 +653,7 @@ public class QueryReplyBuilder { } public void appendPort(ByteArrayOutputStream out) { - // HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total) - 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 - }); + out.write(STATIC_PORT_BLOCKS, 0, STATIC_PORT_BLOCKS.length); } public void appendOemFmt(ByteArrayOutputStream out) { @@ -624,9 +661,7 @@ public class QueryReplyBuilder { } public byte[] buildPort() { - ByteArrayOutputStream out = new ByteArrayOutputStream(70); - appendPort(out); - return out.toByteArray(); + return STATIC_PORT_BLOCKS.clone(); } public byte[] buildOemFormat() { @@ -634,24 +669,7 @@ public class QueryReplyBuilder { } public byte[] buildGrColor() { - // HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) - 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(); + return STATIC_QR_GRCOLOR.clone(); } public byte[] buildGraphicColor() { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/ReusableByteBufferPool.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/ReusableByteBufferPool.java new file mode 100644 index 0000000..bd29415 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/ReusableByteBufferPool.java @@ -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 smallPool = new ConcurrentLinkedQueue<>(); + private static final Queue mediumPool = new ConcurrentLinkedQueue<>(); + private static final Queue 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(); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/AID.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/AID.java new file mode 100644 index 0000000..bc4e816 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/AID.java @@ -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); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/DataStream.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/DataStream.java new file mode 100644 index 0000000..00c61d8 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/DataStream.java @@ -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; +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/DataStream3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/DataStream3270.java new file mode 100644 index 0000000..9370950 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/DataStream3270.java @@ -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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EPI3270Exception.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EPI3270Exception.java new file mode 100644 index 0000000..077bef6 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EPI3270Exception.java @@ -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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EPIException.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EPIException.java new file mode 100644 index 0000000..a9a2403 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EPIException.java @@ -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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EpiScreenBufferBridge.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EpiScreenBufferBridge.java new file mode 100644 index 0000000..daa0e43 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/EpiScreenBufferBridge.java @@ -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); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/Field.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/Field.java new file mode 100644 index 0000000..3af7ea3 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/Field.java @@ -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 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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/Screen.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/Screen.java new file mode 100644 index 0000000..11fa17c --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/epi/Screen.java @@ -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 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(); + } + } + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java index 1e7ddad..b27c547 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/CMSPrintXfer.java @@ -11,18 +11,18 @@ import java.util.regex.Pattern; import java.util.logging.Logger; /** - * VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand - * (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer). + * VM/CMS Spool and Print File Transfer facility matching Host On-Demand specifications. * * Provides: * 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', '+'). - * 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. */ public class CMSPrintXfer { 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 EbcdicTranslator translator; @@ -126,13 +126,12 @@ public class CMSPrintXfer { if (text == null || text.trim().isEmpty()) return entries; String[] lines = text.split("\r?\n"); - Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE"); for (String line : lines) { String trimmed = line.trim(); if (trimmed.isEmpty()) 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); 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: * 0x01: Write without line advance diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java index b337b52..569b8a6 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java @@ -1,11 +1,22 @@ package haus.nightmare.lib3270j.ft; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Configuration for an IND$FILE file transfer session. * Ported from x3270's ft_conf_t (ft_private.h). */ 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 */ public enum HostType { TSO, CMS, CICS @@ -210,26 +221,22 @@ public class FTConfig { String trimmed = opts.trim(); // 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)\\)?"); - java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed); + Matcher recfmMatcher = RECFM_PATTERN.matcher(trimmed); if (recfmMatcher.find()) { setRecfm(recfmMatcher.group(1)); } - java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?"); - java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed); + Matcher lreclMatcher = LRECL_PATTERN.matcher(trimmed); if (lreclMatcher.find()) { setLrecl(lreclMatcher.group(1)); } - java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?"); - java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed); + Matcher blkMatcher = BLK_PATTERN.matcher(trimmed); if (blkMatcher.find()) { setBlksize(blkMatcher.group(1)); } - java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?"); - java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed); + Matcher spaceMatcher = SPACE_PATTERN.matcher(trimmed); if (spaceMatcher.find()) { try { this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); @@ -239,8 +246,7 @@ public class FTConfig { } catch (NumberFormatException ignored) {} } - java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?"); - java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed); + Matcher avbMatcher = AVB_PATTERN.matcher(trimmed); if (avbMatcher.find()) { try { this.avblock = Integer.parseInt(avbMatcher.group(1)); @@ -248,14 +254,12 @@ public class FTConfig { } catch (NumberFormatException ignored) {} } - java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?"); - java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed); + Matcher cpMatcher = CP_PATTERN.matcher(trimmed); if (cpMatcher.find()) { this.codePage = cpMatcher.group(1); } - java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?"); - java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed); + Matcher mtuMatcher = MTU_PATTERN.matcher(trimmed); if (mtuMatcher.find()) { try { setDftBufferSize(Integer.parseInt(mtuMatcher.group(1))); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java index 81abb04..f8c3f4c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -47,7 +47,7 @@ public class TelnetFSM { private final boolean[] hisOpts = new boolean[256]; // options the host has enabled // 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 private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096); @@ -990,9 +990,9 @@ public class TelnetFSM { // ========== End of Record processing ========== private void processEndOfRecord() { - byte[] data = ibuf.toByteArray(); - ibuf.reset(); - if (data.length == 0) return; + int dataLen = ibuf.size(); + if (dataLen == 0) return; + byte[] data = ibuf.buffer(); if ((connectionState == ConnectionState.TELNET_PENDING || connectionState == ConnectionState.CONNECTED_NVT || @@ -1003,14 +1003,15 @@ public class TelnetFSM { if (tn3270eNegotiated) { // TN3270E mode: data starts with 5-byte header - processTN3270ERecord(data); + processTN3270ERecord(data, 0, dataLen); } else { // Plain TN3270 mode: data is raw 3270 data stream if (dsProcessor != null) { - dsProcessor.processRecord(data, 0, data.length, false); + dsProcessor.processRecord(data, 0, dataLen, false); } notifyScreenUpdate(); } + ibuf.reset(); // Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR if (dsProcessor != null) { @@ -1062,19 +1063,23 @@ public class TelnetFSM { } public void processTn3270eHeader(byte[] data) { - processTN3270ERecord(data); + processTN3270ERecord(data, 0, data != null ? data.length : 0); } private void processTN3270ERecord(byte[] data) { - if (data.length < EH_SIZE) { - log.warning("TN3270E record too short: " + data.length); + processTN3270ERecord(data, 0, data != null ? data.length : 0); + } + + private void processTN3270ERecord(byte[] data, int offset, int length) { + if (data == null || length < EH_SIZE) { + log.warning("TN3270E record too short: " + length); return; } - int dataType = data[0] & 0xFF; - int requestFlag = data[1] & 0xFF; - int responseFlag = data[2] & 0xFF; - int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF); + int dataType = data[offset] & 0xFF; + int requestFlag = data[offset + 1] & 0xFF; + int responseFlag = data[offset + 2] & 0xFF; + int seqNumber = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF); this.sdi_flag = (requestFlag & 0x01) != 0; this.kri_flag = (requestFlag & 0x02) != 0; @@ -1089,7 +1094,7 @@ public class TelnetFSM { switch (dataType) { 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) if (connectionState != ConnectionState.CONNECTED_TN3270E) { // Clear screen on transition to 3270 mode from unbound/SSCP/NVT @@ -1099,7 +1104,7 @@ public class TelnetFSM { tn3270eSubmode = TN3270ESubmode.E_3270; } try { - dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false); + dsProcessor.processRecord(data, offset + EH_SIZE, length - EH_SIZE, false); notifyScreenUpdate(); // Send positive response if required if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { @@ -1119,9 +1124,9 @@ public class TelnetFSM { break; case DT_SCS_DATA: - if (data.length > EH_SIZE) { + if (length > EH_SIZE) { 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) { sendTN3270EPositiveResponse(seqNumber); } @@ -1148,9 +1153,9 @@ public class TelnetFSM { changeState(ConnectionState.CONNECTED_SSCP); tn3270eSubmode = TN3270ESubmode.E_SSCP; } - if (data.length > EH_SIZE) { + if (length > EH_SIZE) { try { - dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE); + dsProcessor.processSscpLuData(data, offset + EH_SIZE, length - EH_SIZE); notifyScreenUpdate(); if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { sendTN3270EPositiveResponse(seqNumber); @@ -1169,11 +1174,15 @@ public class TelnetFSM { break; 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; 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); break; @@ -1186,9 +1195,9 @@ public class TelnetFSM { if (dsProcessor != null && dsProcessor.getInputProcessor() != null) { dsProcessor.getInputProcessor().setKeyboardLocked(false); } - if (data.length > EH_SIZE) { + if (length > EH_SIZE) { 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) { sendTN3270EPositiveResponse(seqNumber); } @@ -1243,12 +1252,12 @@ public class TelnetFSM { // 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 || 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) + ") in TN3270E mode — automatically switching to plain TN3270 mode"); tn3270eNegotiated = false; changeState(ConnectionState.CONNECTED_3270); - dsProcessor.processRecord(data, 0, data.length, true); + dsProcessor.processRecord(data, offset, length, true); notifyScreenUpdate(); } else { log.info("Unhandled TN3270E data type: " + dataType); @@ -1257,6 +1266,7 @@ public class TelnetFSM { } } + public void processSCSInbound(byte[] data) { if (data == null) return; processSCSInbound(data, 0, data.length); @@ -1289,7 +1299,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) { byte[] byArray = new byte[5]; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java index 215fa52..a9aaaa8 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/xfer3270/Xfer3270.java @@ -26,8 +26,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Core 3270 File Transfer Controller conforming to IBM Host On-Demand - * (com.ibm.eNetwork.ECL.xfer3270.Xfer3270). + * Core 3270 File Transfer Controller conforming to Host On-Demand specifications. * * Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options, * 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 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_UTF8_STR = "UTF8"; public static final String UNICODE_UTF_8_STR = "UTF-8"; @@ -175,20 +181,20 @@ public class Xfer3270 implements FileTransferInterface { } // 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(); - Matcher lreclMatcher = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?").matcher(options); + Matcher lreclMatcher = LRECL_PATTERN.matcher(options); if (lreclMatcher.find()) { 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()) { 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()) { try { this.primarySpace = Integer.parseInt(spaceMatcher.group(1)); @@ -198,7 +204,7 @@ public class Xfer3270 implements FileTransferInterface { } catch (NumberFormatException ignored) {} } - Matcher avbMatcher = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?").matcher(options); + Matcher avbMatcher = AVB_PATTERN.matcher(options); if (avbMatcher.find()) { try { this.avblock = Integer.parseInt(avbMatcher.group(1)); @@ -212,7 +218,7 @@ public class Xfer3270 implements FileTransferInterface { this.spaceUnits = "CYLINDERS"; } - Matcher mtuMatcher = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?").matcher(options); + Matcher mtuMatcher = MTU_PATTERN.matcher(options); if (mtuMatcher.find()) { try { SetMTUSize(Integer.parseInt(mtuMatcher.group(1))); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/FastByteBufferTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/FastByteBufferTest.java new file mode 100644 index 0000000..25a08c5 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/FastByteBufferTest.java @@ -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"); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/epi/EpiDataStream3270Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/epi/EpiDataStream3270Test.java new file mode 100644 index 0000000..0906020 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/epi/EpiDataStream3270Test.java @@ -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()); + } +}