1 Commits

Author SHA1 Message Date
rudi 1212b2cceb Add tn3270(no e) and fix animations
Release j3270 / Build & Publish Release (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m10s
2026-08-21 16:32:48 -04:00
31 changed files with 698 additions and 198 deletions
+7 -2
View File
@@ -29,6 +29,11 @@ java -jar j3270.jar -s mainframe.example.com 992
# Or using standard x3270 L: prefix
java -jar j3270.jar L:mainframe.example.com:992
# Connect via Plain TN3270 (Non-E)
java -jar j3270.jar -P mainframe.example.com 23
# Or using standard x3270 P: prefix
java -jar j3270.jar P:mainframe.example.com:23
# Connect with unverified/self-signed certificate verification bypass
java -jar j3270.jar --tls --insecure mainframe.example.com 992
@@ -43,7 +48,7 @@ java -jar j3270.jar -c config.ini
## ✨ Features
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption.
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption.
- **SSL/TLS Security**:
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
@@ -71,7 +76,7 @@ The project includes self-contained, portable build scripts:
# Build executable JAR in 2 seconds:
sh ./build_all.sh
# Run all 48 automated unit tests in ~500ms:
# Run all 56 automated unit tests in ~500ms:
sh ./test_all.sh
```
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private int lastPort = 23;
private FileTransfer fileTransfer;
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator");
@@ -363,7 +364,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
@Override
public void onScreenUpdated() {
if (screenUpdatePending.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
screenUpdatePending.set(false);
// During an active file transfer, let the CUT/DFT handler drive
// keyboard state. In x3270, ft_cut_data() runs before WCC
// keyboard-restore is applied — the keyboard stays locked for the
@@ -378,6 +381,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
statusBar.updateStatus();
});
}
}
@Override
public void onSoundAlarm() {
@@ -442,6 +446,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
boolean debug = false;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
@@ -452,6 +457,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliTls = true;
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) {
cliTn3270e = false;
} else if ("--tn3270e".equals(args[i])) {
cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) {
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -529,6 +538,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert;
final Boolean finalTn3270e = cliTn3270e;
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> {
@@ -560,6 +570,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalNoVerify) {
config.setTlsVerifyCert(false);
}
if (finalTn3270e != null) {
config.setTn3270eEnabled(finalTn3270e);
}
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
@@ -578,9 +591,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
boolean tn3270e = org.pubvm.j3270.config.Settings.getAutoConnectTn3270e();
if (host != null && !host.isEmpty()) {
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
@@ -78,13 +78,21 @@ public class Settings {
prefs.putBoolean("autoConnectVerifyCert", verify);
}
public static boolean getAutoConnectTn3270e() {
return prefs.getBoolean("autoConnectTn3270e", true);
}
public static void setAutoConnectTn3270e(boolean tn3270e) {
prefs.putBoolean("autoConnectTn3270e", tn3270e);
}
public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name());
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.BOTH.name());
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
}
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name());
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.BOTH).name());
}
public static Color getColorOverride(int index, Color defaultColor) {
@@ -243,6 +251,11 @@ public class Settings {
case "tlsVerifyCert":
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
break;
case "autoConnectTn3270e":
case "tn3270e":
case "enableTn3270e":
setAutoConnectTn3270e(Boolean.parseBoolean(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior/connection key: " + key);
@@ -309,6 +322,7 @@ public class Settings {
w.println("autoConnectPort = " + getAutoConnectPort());
w.println("autoConnectTls = " + getAutoConnectTls());
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
}
w.println("blockSelectMode = " + getBlockSelectMode());
w.println();
@@ -18,6 +18,7 @@ public class ConnectDialog extends JDialog {
private JTextField luField;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox;
private boolean confirmed;
private ConnectionConfig result;
@@ -146,6 +147,17 @@ public class ConnectDialog extends JDialog {
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
tn3270eCheckBox.setForeground(fg);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(org.pubvm.j3270.config.Settings.getAutoConnectTn3270e());
tn3270eCheckBox.setFocusPainted(false);
mainPanel.add(tn3270eCheckBox, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
@@ -169,7 +181,7 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 7;
gbc.gridy = 8;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
@@ -214,6 +226,7 @@ public class ConnectDialog extends JDialog {
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
confirmed = true;
dispose();
}
@@ -243,4 +256,8 @@ public class ConnectDialog extends JDialog {
public void setInitialVerifyCert(boolean verify) {
verifyCertCheckBox.setSelected(verify);
}
public void setInitialTn3270e(boolean tn3270e) {
tn3270eCheckBox.setSelected(tn3270e);
}
}
@@ -22,6 +22,7 @@ public class TerminalPanel extends JPanel {
// Font and cell dimensions
private Font terminalFont;
private Font boldTerminalFont;
private int cellWidth;
private int cellHeight;
private int fontAscent;
@@ -47,6 +48,14 @@ public class TerminalPanel extends JPanel {
// ========== Selection / Copy-Paste state ==========
private boolean blockSelectMode = false;
private static final String[] CHAR_STRINGS = new String[128];
static {
for (int i = 0; i < 128; i++) {
CHAR_STRINGS[i] = String.valueOf((char) i);
}
}
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false;
@@ -779,6 +788,7 @@ public class TerminalPanel extends JPanel {
cellHeight = fm.getHeight();
fontAscent = fm.getAscent();
fontDescent = fm.getDescent();
boldTerminalFont = terminalFont.deriveFont(Font.BOLD);
}
private void setupCursorBlink() {
@@ -818,6 +828,8 @@ public class TerminalPanel extends JPanel {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
// Clear entire panel with background color
g2.setColor(bgColor);
@@ -828,8 +840,8 @@ public class TerminalPanel extends JPanel {
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getRows();
int cols = sb.getCols();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
boolean isColorModel = client.getConfig().getModel().isColor();
// Track current field attribute for monochrome color decisions
@@ -839,7 +851,7 @@ public class TerminalPanel extends JPanel {
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int baddr = row * cols + col;
ExtendedAttribute ea = sb.getCell(baddr);
ExtendedAttribute ea = sb.getDisplayCell(baddr);
int x = ox + col * cellWidth;
int y = oy + row * cellHeight;
@@ -854,10 +866,7 @@ public class TerminalPanel extends JPanel {
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
// Field attributes display as blanks
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Selection highlight on field attribute cells too
// Selection highlight on field attribute cells
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -892,12 +901,9 @@ public class TerminalPanel extends JPanel {
// Handle invisible fields (zero intensity / password fields)
// Modern UX: render '*' for typed characters so user sees length/digit count
if (faIsZero(currentFA & 0xFF)) {
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString("*", x, y + fontAscent);
@@ -917,9 +923,11 @@ public class TerminalPanel extends JPanel {
bgColor = tmp;
}
// Draw background
// Draw background only if different from default panel bgColor or if inverted
if (!bgColor.equals(this.bgColor) || reverse) {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
}
// Draw character or Programmed Symbol
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
@@ -927,23 +935,27 @@ public class TerminalPanel extends JPanel {
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) {
int[] rgb = slot.getRgbPixels(fgColor.getRGB(), bgColor.getRGB());
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(slot.getWidth(), slot.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);
img.setRGB(0, 0, slot.getWidth(), slot.getHeight(), rgb, 0, slot.getWidth());
g2.drawImage(img, x, y, cellWidth, cellHeight, null);
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), bgColor.getRGB());
if (img != null) {
g2.drawImage(img, x, y, null);
drawnAsPs = true;
}
}
}
if (!drawnAsPs) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
if (ch < 128) {
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
} else {
g2.drawString(String.valueOf(ch), x, y + fontAscent);
}
}
}
// Draw underline
if (underline) {
@@ -975,13 +987,13 @@ public class TerminalPanel extends JPanel {
// Draw cursor
if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getCursorAddress();
int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
g2.setColor(new Color(255, 255, 255, 180));
g2.setColor(CURSOR_COLOR);
g2.setXORMode(bgColor);
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode();
@@ -17,7 +17,8 @@ public class ConnectionConfig {
private int connectTimeoutMs = 15000;
private int nopIntervalSeconds = 0;
private String terminalName = null; // override terminal type string
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
private boolean tn3270eEnabled = true;
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.BOTH;
public ConnectionConfig() {}
@@ -66,6 +67,9 @@ public class ConnectionConfig {
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
@@ -87,8 +91,8 @@ public class ConnectionConfig {
public void setTerminalName(String name) { this.terminalName = name; }
/**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port")
* or standard "host:port" formats.
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
*/
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
if (hostStr == null || hostStr.trim().isEmpty()) {
@@ -96,17 +100,35 @@ public class ConnectionConfig {
}
String s = hostStr.trim();
boolean tls = false;
boolean tn3270e = true;
// Check TLS prefixes
if (s.startsWith("L:") || s.startsWith("l:")) {
tls = true;
s = s.substring(2);
} else if (s.startsWith("Y:") || s.startsWith("y:")) {
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
boolean prefixFound = true;
while (prefixFound) {
prefixFound = false;
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
tls = true;
s = s.substring(2);
prefixFound = true;
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
tls = true;
s = s.substring(4);
prefixFound = true;
} else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) {
tls = false;
int colon = s.indexOf(':');
s = s.substring(colon + 1);
prefixFound = true;
} else if (s.startsWith("P:") || s.startsWith("p:")) {
tn3270e = false;
s = s.substring(2);
prefixFound = true;
} else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) {
tn3270e = false;
int colon = s.indexOf(':');
s = s.substring(colon + 1);
prefixFound = true;
}
}
String host = s;
@@ -133,6 +155,7 @@ public class ConnectionConfig {
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e);
return config;
}
@@ -9,6 +9,7 @@ import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
@@ -105,16 +106,24 @@ public class DataStreamProcessor {
return;
int cmd = data[offset] & 0xFF;
log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
synchronized (screen.getRenderLock()) {
if (log.isLoggable(Level.FINE)) {
log.fine(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
}
switch (cmd) {
case CMD_W:
case SNA_CMD_W:
programSymbolManager.commitStagedSymbols();
processWrite(data, offset, length, false);
break;
case CMD_EW:
case SNA_CMD_EW:
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE: clearing screen (default size)");
}
{
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
@@ -127,7 +136,11 @@ public class DataStreamProcessor {
break;
case CMD_EWA:
case SNA_CMD_EWA:
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
}
{
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
@@ -140,18 +153,22 @@ public class DataStreamProcessor {
break;
case CMD_RB:
case SNA_CMD_RB:
programSymbolManager.commitStagedSymbols();
processReadBuffer();
break;
case CMD_RM:
case SNA_CMD_RM:
programSymbolManager.commitStagedSymbols();
processReadModified(false);
break;
case CMD_RMA:
case SNA_CMD_RMA:
programSymbolManager.commitStagedSymbols();
processReadModified(true);
break;
case CMD_EAU:
case SNA_CMD_EAU:
programSymbolManager.commitStagedSymbols();
log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected();
break;
@@ -170,9 +187,11 @@ public class DataStreamProcessor {
// Translate EBCDIC to Unicode for display
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
// Debug: dump non-empty screen lines
if (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW) {
if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) {
int r = screen.getRows();
int c = screen.getCols();
StringBuilder dump = new StringBuilder();
@@ -205,7 +224,7 @@ public class DataStreamProcessor {
}
}
if (dump.length() > 0) {
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
log.fine("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
}
}
}
@@ -798,7 +817,6 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 3];
System.arraycopy(data, pos + 3, psData, 0, psData.length);
programSymbolManager.loadps(psData);
notifyScreenUpdated();
}
break;
case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
@@ -810,7 +828,6 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, pos + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
notifyScreenUpdated();
}
break;
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
@@ -22,7 +22,7 @@ public class QueryReplyBuilder {
private static final int Yr_3279_2 = 0x0002006f;
private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.NONE;
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
// Base query reply codes (text mode)
private static final int[] SUPPORTED_QR_BASE = {
@@ -313,9 +313,9 @@ public class QueryReplyBuilder {
private byte[] buildCharsets() {
if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols mode (3279 PS with LoadPS 0x0A)
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0x82); // flags: GE, CGCSGID present
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags
out.write(SW_3279_2); // SDW (9)
out.write(SH_3279_2); // SDH (12)
@@ -328,14 +328,14 @@ public class QueryReplyBuilder {
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
// Descriptor 2 (SET 1): APL/GE character set
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3)
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7)
out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane)
out.write(0x04); out.write(0xc0); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x05); out.write(0xc0); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x06); out.write(0xc0); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x07); out.write(0xc0); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
return out.toByteArray();
}
@@ -17,12 +17,31 @@ public class ProgramSymbolManager {
public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5)
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
private int defaultCellWidth = 9;
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
public void setDefaultCellDimensions(int width, int height) {
this.defaultCellWidth = (width > 0) ? width : 9;
this.defaultCellHeight = (height > 0) ? height : 12;
}
public int getDefaultCellWidth() {
return defaultCellWidth;
}
public int getDefaultCellHeight() {
return defaultCellHeight;
}
private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
private final ProgramSymbolSet[] stagingSets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
private final ProgramSymbolSet[] lcidMap = new ProgramSymbolSet[256];
public ProgramSymbolManager() {
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS);
sets[i] = new ProgramSymbolSet(isTriple);
stagingSets[i] = new ProgramSymbolSet(isTriple);
}
}
@@ -30,32 +49,67 @@ public class ProgramSymbolManager {
* Resets all symbol sets.
*/
public synchronized void clearAll() {
for (ProgramSymbolSet set : sets) {
set.clear();
set.setLcid(0);
Arrays.fill(lcidMap, null);
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
sets[i].clear();
sets[i].setLcid(0);
stagingSets[i].clear();
stagingSets[i].setLcid(0);
}
}
/**
* Commits all staged multi-plane symbol sets to the active presentation sets atomically.
*/
public synchronized void commitStagedSymbols() {
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
int lcid = stagingSets[i].getLcid();
if (lcid > 0) {
ProgramSymbolSet staged = stagingSets[i];
ProgramSymbolSet active = sets[i];
active.setLcid(lcid);
for (int slot = 0; slot < ProgramSymbolSet.NUM_SLOTS; slot++) {
active.setSlot(slot, staged.getSlot(slot));
}
if (lcid < 256) {
lcidMap[lcid] = active;
}
}
}
}
/**
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
*/
public synchronized ProgramSymbolSet getSymbolSet(int lcid) {
if (lcid <= 0) {
public ProgramSymbolSet getSymbolSet(int lcid) {
if (lcid <= 0 || lcid >= 256) {
return null;
}
for (ProgramSymbolSet set : sets) {
if (set.getLcid() == lcid) {
ProgramSymbolSet set = lcidMap[lcid];
if (set == null) {
for (ProgramSymbolSet s : stagingSets) {
if (s.getLcid() == lcid) return s;
}
}
return set;
}
}
return null;
}
/**
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
*/
public synchronized ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
ProgramSymbolSet set = getSymbolSet(lcid);
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
if (lcid <= 0 || lcid >= 256) {
return null;
}
ProgramSymbolSet set = lcidMap[lcid];
if (set == null) {
for (ProgramSymbolSet s : stagingSets) {
if (s.getLcid() == lcid) {
set = s;
break;
}
}
}
if (set == null) {
return null;
}
@@ -98,12 +152,12 @@ public class ProgramSymbolManager {
return;
}
ProgramSymbolSet set = sets[setIndex];
boolean isTriplePlane = (rws >= 4 && rws <= 7);
ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex];
int extHeaderLen = 0;
int cellWidth = 9;
int cellHeight = 16;
int cellWidth = defaultCellWidth;
int cellHeight = defaultCellHeight;
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
if (hasExtHeader && data.length > 4) {
@@ -121,10 +175,10 @@ public class ProgramSymbolManager {
}
}
if (clearAll) {
set.clear();
}
set.setLcid(lcid);
if (!isTriplePlane && lcid > 0 && lcid < 256) {
lcidMap[lcid] = set;
}
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
int remaining = data.length - offset;
@@ -142,12 +196,10 @@ public class ProgramSymbolManager {
}
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
byte[] pixelData;
byte[] pixelData = new byte[cellWidth * cellHeight];
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
if (existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
pixelData = existing.getPixelData();
} else {
pixelData = new byte[cellWidth * cellHeight];
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
}
if (loadFormat == 1) {
@@ -163,10 +215,18 @@ public class ProgramSymbolManager {
remaining -= bytesPerSymbol;
}
logger.info(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
if (clearAll) {
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
set.clearSlot(i);
}
}
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
}
}
/**
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
@@ -64,6 +64,12 @@ public class ProgramSymbolSet {
private int[] cachedRgbArray;
private int cachedFgRgb = -1;
private int cachedBgRgb = -1;
private java.awt.image.BufferedImage cachedImage;
private java.awt.image.BufferedImage cachedScaledImage;
private int cachedTargetW = 0;
private int cachedTargetH = 0;
private int cachedScaledFgRgb = -1;
private int cachedScaledBgRgb = -1;
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
this.width = width > 0 ? width : 9;
@@ -88,6 +94,61 @@ public class ProgramSymbolSet {
return isTriplePlane;
}
/**
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
* Enables unscaled 1:1 hardware blitting in Java2D.
*/
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
if (targetW <= 0 || targetH <= 0) {
return getImage(fgArgb, bgArgb);
}
if (targetW == width && targetH == height) {
return getImage(fgArgb, bgArgb);
}
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
return cachedScaledImage;
}
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
for (int dy = 0; dy < targetH; dy++) {
int sy = dy * height / targetH;
int srcRowOffset = sy * width;
int dstRowOffset = dy * targetW;
for (int dx = 0; dx < targetW; dx++) {
int sx = dx * width / targetW;
dstRgb[dstRowOffset + dx] = srcRgb[srcRowOffset + sx];
}
}
this.cachedScaledImage = scaled;
this.cachedTargetW = targetW;
this.cachedTargetH = targetH;
this.cachedScaledFgRgb = fgArgb;
this.cachedScaledBgRgb = bgArgb;
return scaled;
}
/**
* Computes and returns the cached BufferedImage for this symbol glyph.
* Eliminates per-cell heap allocations during high frame rate rendering.
*/
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
return cachedImage;
}
int[] rgb = getRgbPixels(fgArgb, bgArgb);
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
this.cachedImage = img;
this.cachedFgRgb = fgArgb;
this.cachedBgRgb = bgArgb;
return img;
}
/**
* Computes and returns the 32-bit ARGB pixel array for this symbol.
* The returned array has length (width * height).
@@ -36,8 +36,12 @@ public class ScreenBuffer {
private byte defaultGr = 0x00;
private byte defaultCs = 0x00;
private byte defaultIc = 0x00;
private final EbcdicTranslator translator;
private final Object renderLock = new Object();
public Object getRenderLock() {
return renderLock;
}
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
this.translator = translator;
@@ -79,6 +83,50 @@ public class ScreenBuffer {
return buffer[addr];
}
private ExtendedAttribute[] displaySnapshot;
private int displayRows;
private int displayCols;
private int displayCursorAddress;
/**
* Atomically creates a snapshot of the current presentation buffer for tear-free rendering.
* Takes ~2 microseconds and eliminates mutual thread contention with the UI thread.
*/
public synchronized void updateDisplaySnapshot() {
int size = rows * cols;
if (displaySnapshot == null || displaySnapshot.length < size) {
displaySnapshot = new ExtendedAttribute[size];
for (int i = 0; i < size; i++) {
displaySnapshot[i] = new ExtendedAttribute();
}
}
for (int i = 0; i < size; i++) {
displaySnapshot[i].copyFrom(buffer[i]);
}
this.displayRows = rows;
this.displayCols = cols;
this.displayCursorAddress = cursorAddress;
}
public synchronized ExtendedAttribute getDisplayCell(int addr) {
if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) {
return getCell(addr);
}
return displaySnapshot[addr];
}
public synchronized int getDisplayRows() {
return displayRows > 0 ? displayRows : rows;
}
public synchronized int getDisplayCols() {
return displayCols > 0 ? displayCols : cols;
}
public synchronized int getDisplayCursorAddress() {
return displayRows > 0 ? displayCursorAddress : cursorAddress;
}
// ========== Dimension accessors ==========
public int getRows() { return rows; }
public int getCols() { return cols; }
@@ -149,9 +149,7 @@ public class TelnetConnection {
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
}
try {
for (int i = 0; i < n; i++) {
fsm.feedByte(buf[i] & 0xFF);
}
fsm.feedBytes(buf, 0, n);
fsm.endOfNetworkData();
} catch (Throwable t) {
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
@@ -58,6 +58,7 @@ public class TelnetFSM {
private int eXmitSeq;
private int responseRequired = RSF_NO_RESPONSE;
private boolean deferredWillTtype;
private boolean tn3270eDeviceTypeSent;
// Connection references
private TelnetConnection connection;
@@ -103,6 +104,7 @@ public class TelnetFSM {
java.util.Arrays.fill(hisOpts, false);
java.util.Arrays.fill(eFuncs, false);
tn3270eNegotiated = false;
tn3270eDeviceTypeSent = false;
tn3270eSubmode = TN3270ESubmode.UNBOUND;
tn3270eBound = false;
eXmitSeq = 0;
@@ -118,6 +120,37 @@ public class TelnetFSM {
changeState(ConnectionState.TELNET_PENDING);
}
/**
* Feed a bulk buffer of bytes from the network into the FSM.
*/
public void feedBytes(byte[] buf, int offset, int len) {
int end = offset + len;
int i = offset;
while (i < end) {
if (state == TNS_DATA) {
int start = i;
while (i < end && (buf[i] & 0xFF) != IAC) {
i++;
}
if (i > start) {
if (connectionState == ConnectionState.TELNET_PENDING) {
changeState(ConnectionState.CONNECTED_NVT);
}
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
ibuf.write(buf, start, i - start);
}
}
if (i < end) {
state = TNS_IAC;
i++;
}
} else {
feedByte(buf[i] & 0xFF);
i++;
}
}
}
/**
* Feed a single byte from the network into the FSM.
*/
@@ -172,8 +205,8 @@ public class TelnetFSM {
changeState(ConnectionState.CONNECTED_NVT);
}
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states)
if (connectionState.is3270() || connectionState.isTn3270e()) {
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
ibuf.write(c);
}
// NVT data would go to NVT processor (not implemented in initial version)
@@ -189,7 +222,7 @@ public class TelnetFSM {
break;
case EOR: // End of record process accumulated 3270 data
log.fine("RCVD EOR");
if (connectionState.is3270() || connectionState.isTn3270e()) {
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
processEndOfRecord();
}
ibuf.reset();
@@ -235,7 +268,9 @@ public class TelnetFSM {
break;
case TELOPT_TN3270E:
if (!hisOpts[opt]) {
if (!config.isTn3270eEnabled()) {
sendCommand(DONT, opt);
} else if (!hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
@@ -278,7 +313,7 @@ public class TelnetFSM {
case TELOPT_TTYPE:
if (!myOpts[opt]) {
myOpts[opt] = true;
if (hisOpts[TELOPT_TN3270E]) {
if (config.isTn3270eEnabled() && hisOpts[TELOPT_TN3270E]) {
// Defer TTYPE response until TN3270E negotiation completes
deferredWillTtype = true;
} else {
@@ -288,11 +323,16 @@ public class TelnetFSM {
break;
case TELOPT_TN3270E:
if (!myOpts[opt]) {
if (!config.isTn3270eEnabled()) {
sendCommand(WONT, opt);
} else if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
// Start TN3270E sub-negotiation: send device type request
if (!tn3270eDeviceTypeSent) {
sendTN3270EDeviceTypeRequest();
tn3270eDeviceTypeSent = true;
}
}
break;
@@ -428,7 +468,10 @@ public class TelnetFSM {
switch (op) {
case OP_SEND:
// Host asks us to send device-type request
if (!tn3270eDeviceTypeSent) {
sendTN3270EDeviceTypeRequest();
tn3270eDeviceTypeSent = true;
}
break;
case OP_DEVICE_TYPE:
@@ -488,14 +531,19 @@ public class TelnetFSM {
pos++;
}
// Check if REJECT
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
// Rejection
pos++;
int reason = -1;
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
pos++;
if (pos < data.length) {
reason = data[pos] & 0xFF;
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
// Try fallback model 2 if we were requesting something else
if (config.getModel() != TerminalModel.IBM_3278_2 &&
config.getModel() != TerminalModel.IBM_3279_2) {
log.warning("TN3270E device-type rejected (" +
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
config.setModel(TerminalModel.IBM_3278_2);
sendTN3270EDeviceTypeRequest();
return;
}
}
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
@@ -563,33 +611,86 @@ public class TelnetFSM {
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
}
private void handleTN3270EFunctions(byte[] data) {
// Parse: TN3270E FUNCTIONS IS [func...]
int pos = 2; // Skip TN3270E, FUNCTIONS
private void sendTN3270EFunctionsIs() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TN3270E);
out.write(OP_FUNCTIONS);
out.write(OP_IS);
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
pos++; // Skip IS
StringBuilder funcNames = new StringBuilder();
for (int i = 0; i < eFuncs.length; i++) {
if (eFuncs[i]) {
out.write(i);
if (funcNames.length() > 0) funcNames.append(" ");
funcNames.append(TN3270EConstants.functionName(i));
}
}
// The remaining bytes are the agreed-upon functions
java.util.Arrays.fill(eFuncs, false);
StringBuilder funcNames = new StringBuilder();
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB TN3270E FUNCTIONS IS " + funcNames + " SE");
}
private void handleTN3270EFunctions(byte[] data) {
// Parse: TN3270E FUNCTIONS REQUEST [func...] or TN3270E FUNCTIONS IS [func...]
int pos = 1;
boolean isRequest = false;
while (pos < data.length) {
int b = data[pos] & 0xFF;
if (b == OP_FUNCTIONS) {
pos++;
} else if (b == OP_REQUEST) {
isRequest = true;
pos++;
break;
} else if (b == OP_IS) {
isRequest = false;
pos++;
break;
} else {
pos++;
}
}
// The remaining bytes are the proposed/agreed functions
boolean[] hostFuncs = new boolean[8];
while (pos < data.length) {
int func = data[pos] & 0xFF;
if (func <= FUNC_SNA_SENSE) {
eFuncs[func] = true;
if (funcNames.length() > 0) funcNames.append(" ");
funcNames.append(TN3270EConstants.functionName(func));
hostFuncs[func] = true;
}
pos++;
}
if (isRequest) {
// Host sent FUNCTIONS REQUEST -> We reply with FUNCTIONS IS (intersection of functions)
for (int i = 0; i < eFuncs.length; i++) {
eFuncs[i] = eFuncs[i] && hostFuncs[i];
}
sendTN3270EFunctionsIs();
} else {
// Host sent FUNCTIONS IS -> Accept host's agreed function list
for (int i = 0; i < eFuncs.length; i++) {
eFuncs[i] = hostFuncs[i];
}
}
tn3270eNegotiated = true;
log.info("TN3270E functions IS: " + funcNames);
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
log.info("TN3270E negotiation complete");
// Move to CONNECTED_UNBOUND or CONNECTED_SSCP
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
// to be bound immediately upon completion of the FUNCTIONS negotiation.
if (eFuncs[FUNC_BIND_IMAGE]) {
changeState(ConnectionState.CONNECTED_UNBOUND);
} else {
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
}
// Notify listeners
for (ConnectionListener l : connectionListeners) {
@@ -597,12 +698,29 @@ public class TelnetFSM {
}
}
private String getNegotiatedFunctionNames() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < eFuncs.length; i++) {
if (eFuncs[i]) {
if (sb.length() > 0) sb.append(" ");
sb.append(TN3270EConstants.functionName(i));
}
}
return sb.length() > 0 ? sb.toString() : "<none>";
}
// ========== End of Record processing ==========
private void processEndOfRecord() {
byte[] data = ibuf.toByteArray();
ibuf.reset();
if (data.length == 0) return;
if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) {
log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode");
changeState(ConnectionState.CONNECTED_3270);
}
if (tn3270eNegotiated) {
// TN3270E mode: data starts with 5-byte header
processTN3270ERecord(data);
@@ -701,11 +819,11 @@ public class TelnetFSM {
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
case 0x7e:
case 0x7E:
// Both default and alternate = specified values
bindRa = bindRd; bindCa = bindCd;
break;
case 0x7f:
case 0x7F:
// Default and alternate are both specified separately
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
@@ -767,7 +885,20 @@ public class TelnetFSM {
break;
default:
// Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.)
// 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 == 0x05 ||
dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 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);
notifyScreenUpdate();
} else {
log.info("Unhandled TN3270E data type: " + dataType);
}
break;
}
}
@@ -790,7 +921,7 @@ public class TelnetFSM {
if (connectionState != ConnectionState.TELNET_PENDING) return;
// For TN3270E, we wait for TN3270E negotiation to complete
if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
return; // TN3270E in progress
}
@@ -15,8 +15,27 @@ public class QueryReplyBuilderTest {
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
@Test
public void testBuildAllQueryRepliesDefaultNone() {
assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode());
public void testBuildAllQueryRepliesDefaultBoth() {
assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode());
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
assertTrue(replies.length > 0);
assertEquals((byte) AID_SF, replies[0]);
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present
boolean hasB0 = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
hasB0 = true;
break;
}
}
assertTrue(hasB0);
}
@Test
public void testBuildAllQueryRepliesExplicitNone() {
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
assertTrue(replies.length > 0);
@@ -156,4 +175,61 @@ public class QueryReplyBuilderTest {
assertEquals(80, altCols);
assertEquals(43, altRows);
}
@Test
public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() {
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
byte[] requested = new byte[] { (byte) QR_CHARSETS };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
int offset = -1;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
offset = i + 2;
break;
}
}
assertTrue(offset >= 0, "QR_CHARSETS must be present");
// QR_CHARSETS payload structure:
// Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte)
// DL is at offset + 8, and is 7 bytes per descriptor.
int dl = replies[offset + 8] & 0xFF;
assertEquals(7, dl);
int descOffset = offset + 9;
// Descriptor 1: SET 0 (Base) -> flags 0x10
assertEquals(0x00, replies[descOffset] & 0xFF);
assertEquals(0x10, replies[descOffset + 1] & 0xFF);
// Descriptor 2: SET 1 (APL) -> flags 0x00
assertEquals(0x01, replies[descOffset + 7] & 0xFF);
assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF);
// Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane)
assertEquals(0x02, replies[descOffset + 14] & 0xFF);
assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF);
// Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane)
assertEquals(0x03, replies[descOffset + 21] & 0xFF);
assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF);
// Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x04, replies[descOffset + 28] & 0xFF);
assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF);
// Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x05, replies[descOffset + 35] & 0xFF);
assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF);
// Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x06, replies[descOffset + 42] & 0xFF);
assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF);
// Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF);
}
}
@@ -46,11 +46,11 @@ public class GraphicsModeTest {
@Test
public void testConnectionConfigDefault() {
ConnectionConfig config = new ConnectionConfig();
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
config.setGraphicsMode(GraphicsMode.BOTH);
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
config.setGraphicsMode(GraphicsMode.NONE);
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
config.setGraphicsMode(null);
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
}
@@ -51,7 +51,7 @@ public class ProgramSymbolManagerTest {
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
assertNotNull(slot);
assertEquals(9, slot.getWidth());
assertEquals(16, slot.getHeight());
assertEquals(12, slot.getHeight());
assertFalse(slot.isTriplePlane());
// Verify pixel data (Row 0 Col 0 is 1)
@@ -64,10 +64,33 @@ public class ProgramSymbolManagerTest {
int bg = 0xFF000000; // Black
int[] rgb = slot.getRgbPixels(fg, bg);
assertNotNull(rgb);
assertEquals(9 * 16, rgb.length);
assertEquals(9 * 12, rgb.length);
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
}
@Test
public void testExplicitCellDimensions() {
ProgramSymbolManager manager = new ProgramSymbolManager();
manager.setDefaultCellDimensions(9, 16);
byte[] payload = new byte[4 + 18];
payload[0] = 0x01; // Format 1
payload[1] = 0x40; // LCID 0x40
payload[2] = 0x41; // Code point 0x41
payload[3] = 0x02; // RWS 2
payload[4] = (byte) 0x80;
payload[5] = (byte) 0x00;
for (int r = 0; r < 16; r++) {
payload[6 + r] = (byte) 0xFF;
}
manager.loadps(payload);
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
assertNotNull(slot);
assertEquals(9, slot.getWidth());
assertEquals(16, slot.getHeight());
}
@Test
public void testTriplePlaneMultiColorComposite() {
ProgramSymbolManager manager = new ProgramSymbolManager();