Refactor
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 44s

This commit is contained in:
2026-08-25 16:05:12 +00:00
parent 36b93df986
commit 7eebd2107c
56 changed files with 308 additions and 308 deletions
@@ -0,0 +1,171 @@
package haus.nightmare.lib3270j;
/**
* Configuration for a 3270 terminal connection.
*/
public class ConnectionConfig {
private String host;
private int port = 23;
private TerminalModel model = TerminalModel.IBM_3279_4;
private String luName = null;
private boolean extendedDataStream = true;
private boolean useTls = false;
private boolean tlsVerifyCert = true;
private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
private String sslProtocol = "TLS";
private int connectTimeoutMs = 15000;
private int nopIntervalSeconds = 0;
private String terminalName = null; // override terminal type string
private boolean tn3270eEnabled = true;
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
public ConnectionConfig() {}
public ConnectionConfig(String host, int port) {
this.host = host;
this.port = port;
}
public ConnectionConfig(String host, int port, boolean useTls) {
this.host = host;
this.port = port;
this.useTls = useTls;
}
public ConnectionConfig(String host, int port, TerminalModel model) {
this.host = host;
this.port = port;
this.model = model;
}
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
this.host = host;
this.port = port;
this.model = model;
this.useTls = useTls;
}
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public TerminalModel getModel() { return model; }
public void setModel(TerminalModel model) { this.model = model; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isExtendedDataStream() { return extendedDataStream; }
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
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 haus.nightmare.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
public void setCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
public String getSslProtocol() { return sslProtocol; }
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
public int getConnectTimeoutMs() { return connectTimeoutMs; }
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
public haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
public void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
}
public String getTerminalName() { return terminalName; }
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"),
* 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()) {
return new ConnectionConfig("localhost", defaultPort, defaultModel);
}
String s = hostStr.trim();
boolean tls = false;
boolean tn3270e = true;
// 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;
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
// Check for host:port (handle IPv6 [::1]:port)
if (s.startsWith("[") && s.contains("]")) {
int closeBracket = s.indexOf(']');
host = s.substring(1, closeBracket);
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
try {
port = Integer.parseInt(s.substring(closeBracket + 2));
} catch (NumberFormatException ignored) {}
}
} else {
int colon = s.lastIndexOf(':');
if (colon > 0 && colon < s.length() - 1) {
try {
port = Integer.parseInt(s.substring(colon + 1));
host = s.substring(0, colon);
} catch (NumberFormatException ignored) {}
}
}
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e);
return config;
}
/**
* Get the effective terminal type string to send during negotiation.
*/
public String getEffectiveTerminalType() {
if (terminalName != null) {
return terminalName;
}
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
}
}
@@ -0,0 +1,62 @@
package haus.nightmare.lib3270j;
/**
* Connection state machine states.
* Mirrors the cstate enum from globals.h in x3270.
*/
public enum ConnectionState {
NOT_CONNECTED, // No socket, unknown mode
RECONNECTING, // Delay before automatic reconnect
RESOLVING, // Resolving hostname
TCP_PENDING, // Socket connection pending
TLS_PENDING, // TLS negotiation pending
PROXY_PENDING, // Proxy negotiation pending
TELNET_PENDING, // Telnet negotiation pending
CONNECTED_NVT, // Connected in NVT line mode
CONNECTED_NVT_CHAR, // Connected in NVT character-at-a-time mode
CONNECTED_3270, // Connected in RFC 1576 TN3270 mode
CONNECTED_UNBOUND, // Connected in TN3270E mode, unbound
CONNECTED_E_NVT, // Connected in TN3270E NVT mode
CONNECTED_SSCP, // Connected in TN3270E SSCP-LU mode
CONNECTED_TN3270E; // Connected in TN3270E 3270 mode
/** True if any kind of connection exists (even half-connected). */
public boolean isConnected() {
return this.ordinal() > NOT_CONNECTED.ordinal();
}
/** True if in a half-connected state (resolving through telnet pending). */
public boolean isHalfConnected() {
return this.ordinal() >= RESOLVING.ordinal() && this.ordinal() < CONNECTED_NVT.ordinal();
}
/** True if fully connected (past TCP pending). */
public boolean isFullyConnected() {
return this.ordinal() > TCP_PENDING.ordinal();
}
/** True if in NVT mode (any flavor). */
public boolean isNvt() {
return this == CONNECTED_NVT || this == CONNECTED_NVT_CHAR || this == CONNECTED_E_NVT;
}
/** True if in 3270 mode (any flavor). */
public boolean is3270() {
return this == CONNECTED_3270 || this == CONNECTED_TN3270E || this == CONNECTED_SSCP;
}
/** True if in SSCP-LU mode. */
public boolean isSscp() {
return this == CONNECTED_SSCP;
}
/** True if in TN3270E mode (any submode). */
public boolean isTn3270e() {
return this.ordinal() >= CONNECTED_UNBOUND.ordinal();
}
/** True if in a full data session (NVT or 3270). */
public boolean isFullSession() {
return isNvt() || is3270();
}
}
@@ -0,0 +1,174 @@
package haus.nightmare.lib3270j;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.listener.*;
import java.util.logging.*;
/**
* Diagnostic: login as guest1/guest and trace the Welcome → MOTD transition.
*/
public class DiagnosticClient {
static volatile int screenUpdateCount = 0;
static Telnet3270Client client;
public static void main(String[] args) throws Exception {
Logger rootLogger = Logger.getLogger("haus.nightmare.lib3270j");
rootLogger.setLevel(Level.ALL);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
Logger.getLogger("").setLevel(Level.WARNING);
String host = args.length >= 1 ? args[0] : "192.168.0.30";
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4);
client = new Telnet3270Client(config);
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
System.out.println(">>> STATE: " + oldState + " -> " + newState);
}
@Override
public void onConnectionError(String message) {
System.out.println(">>> ERROR: " + message);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
System.out.println(">>> TN3270E: type=" + deviceType + " name=" + deviceName);
}
});
client.addScreenUpdateListener(new ScreenUpdateListener() {
@Override
public void onScreenUpdated() {
screenUpdateCount++;
System.out.println("\n>>> SCREEN UPDATE #" + screenUpdateCount);
dumpScreen();
client.getInputProcessor().setKeyboardLocked(false);
}
@Override
public void onSoundAlarm() {
System.out.println(">>> ALARM");
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
System.out.println(">>> SCREEN SIZE: " + rows + "x" + cols);
}
});
System.out.println("=== Connecting to " + host + ":" + port + " ===");
client.connect();
// Wait for login screen
waitForUpdates(5000);
// Type guest1 at login field
System.out.println("\n=== Typing 'guest1' ===");
typeAndWait("guest1", 2000);
// Press Enter to submit login
System.out.println("\n=== Pressing Enter (submit login) ===");
client.sendEnter();
waitForUpdates(3000);
// Check if we need to enter password
System.out.println("\n=== Typing password 'guest' ===");
typeAndWait("guest", 2000);
client.sendEnter();
waitForUpdates(5000);
// Now we should see Welcome/allocations screen
// Wait for *** prompt
Thread.sleep(3000);
System.out.println("\n=== Current screen (should be Welcome/allocations with ***) ===");
dumpScreen();
// Press Enter at *** to continue — THIS is where the screen should clear
System.out.println("\n=== Pressing Enter at *** (Welcome → MOTD transition) ===");
client.sendEnter();
waitForUpdates(5000);
// Wait for more updates
Thread.sleep(3000);
System.out.println("\n=== After *** Enter — screen should have been cleared for MOTD ===");
dumpScreen();
// If there's another *** prompt, press Enter again
System.out.println("\n=== Pressing Enter again ===");
client.sendEnter();
waitForUpdates(5000);
Thread.sleep(2000);
dumpScreen();
// One more Enter
System.out.println("\n=== Pressing Enter one more time ===");
client.sendEnter();
waitForUpdates(5000);
Thread.sleep(2000);
dumpScreen();
// Let it sit for any more data
Thread.sleep(3000);
System.out.println("\n=== Final screen ===");
dumpScreen();
client.disconnect();
System.out.println(">>> Disconnected");
}
static void typeAndWait(String text, int waitMs) throws InterruptedException {
for (char c : text.toCharArray()) {
client.typeCharacter(c);
Thread.sleep(30);
}
Thread.sleep(waitMs);
}
static void waitForUpdates(int timeoutMs) throws InterruptedException {
int start = screenUpdateCount;
long deadline = System.currentTimeMillis() + timeoutMs;
while (screenUpdateCount == start && System.currentTimeMillis() < deadline) {
Thread.sleep(100);
}
if (screenUpdateCount == start) {
System.out.println(">>> (no screen update within " + timeoutMs + "ms)");
}
Thread.sleep(500);
}
static void dumpScreen() {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getRows();
int cols = sb.getCols();
System.out.println("--- Screen " + rows + "x" + cols +
" cursor=" + sb.getCursorAddress() +
" formatted=" + sb.isFormatted() +
" state=" + client.getConnectionState() + " ---");
for (int r = 0; r < rows; r++) {
StringBuilder line = new StringBuilder();
boolean hasContent = false;
for (int c = 0; c < cols; c++) {
ExtendedAttribute ea = sb.getCell(r * cols + c);
if (ea.isFieldAttribute()) {
line.append('|');
} else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
line.append(ea.ucs4);
hasContent = true;
} else {
line.append(' ');
}
}
if (hasContent) {
System.out.println(String.format("%02d: %s", r, line.toString().stripTrailing()));
}
}
System.out.println("---");
}
}
@@ -0,0 +1,238 @@
package haus.nightmare.lib3270j;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetConnection;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import java.io.IOException;
import java.util.logging.Logger;
/**
* Main API entry point for lib3270j.
*
* Provides a high-level interface for connecting to a TN3270 host,
* managing the screen buffer, and handling user input.
*
* Usage:
* <pre>
* ConnectionConfig config = new ConnectionConfig("hostname", 23, TerminalModel.IBM_3279_4);
* Telnet3270Client client = new Telnet3270Client(config);
* client.addConnectionListener(myListener);
* client.addScreenUpdateListener(myScreenListener);
* client.connect();
* // ... interact with screen ...
* client.disconnect();
* </pre>
*/
public class Telnet3270Client {
private static final Logger log = Logger.getLogger(Telnet3270Client.class.getName());
private final ConnectionConfig config;
private final EbcdicTranslator translator;
private final ScreenBuffer screenBuffer;
private final DataStreamProcessor dsProcessor;
private final TelnetFSM fsm;
private final InputProcessor inputProcessor;
private TelnetConnection connection;
public Telnet3270Client(ConnectionConfig config) {
this.config = config;
this.translator = new EbcdicTranslator();
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor);
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
}
/**
* Connect to the configured host.
* This method blocks until the TCP connection is established,
* then returns while telnet/TN3270E negotiation continues asynchronously.
*/
public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
" model=" + config.getModel());
connection = new TelnetConnection(config, fsm);
fsm.setConnection(connection);
// Establish TCP connection
connection.connect();
// Notify FSM that TCP is connected — begins telnet negotiation
fsm.onConnected();
}
/**
* Disconnect from the host.
*/
public void disconnect() {
if (connection != null) {
connection.disconnect();
connection = null;
}
fsm.onDisconnect();
}
/**
* Check if connected (any state past TCP pending).
*/
public boolean isConnected() {
return connection != null && connection.isConnected();
}
// ========== Listener management ==========
public void addConnectionListener(ConnectionListener l) {
fsm.addConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.addScreenUpdateListener(l);
dsProcessor.addScreenUpdateListener(l);
}
// ========== Screen access ==========
/** Get the screen buffer for rendering. */
public ScreenBuffer getScreenBuffer() { return screenBuffer; }
/** Get the EBCDIC translator. */
public EbcdicTranslator getTranslator() { return translator; }
/** Get the current connection state. */
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
/** Get the input processor for keyboard operations. */
public InputProcessor getInputProcessor() { return inputProcessor; }
/** Get the data stream processor. */
public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
/** Get the connection config. */
public ConnectionConfig getConfig() { return config; }
/** Set a custom or interactive TLS certificate verifier callback. */
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
config.setCertificateVerifier(verifier);
}
/** Get the active SSLSession if connected over TLS, or null. */
public javax.net.ssl.SSLSession getSslSession() {
return connection.getSslSession();
}
// ========== Convenience input methods ==========
/** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
/** Type a string at the cursor position. */
public void typeString(String s) {
for (char ch : s.toCharArray()) {
inputProcessor.typeCharacter(ch);
}
}
/** Emulate input of a string, pressing Enter on newlines. */
public void emulateInput(String s) {
inputProcessor.emulateInput(s);
}
/** Send Enter key. */
public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
}
/** Send a PF key (1-24). */
public void sendPF(int number) {
int aid;
switch (number) {
case 1: aid = 0xf1; break; case 2: aid = 0xf2; break;
case 3: aid = 0xf3; break; case 4: aid = 0xf4; break;
case 5: aid = 0xf5; break; case 6: aid = 0xf6; break;
case 7: aid = 0xf7; break; case 8: aid = 0xf8; break;
case 9: aid = 0xf9; break; case 10: aid = 0x7a; break;
case 11: aid = 0x7b; break; case 12: aid = 0x7c; break;
case 13: aid = 0xc1; break; case 14: aid = 0xc2; break;
case 15: aid = 0xc3; break; case 16: aid = 0xc4; break;
case 17: aid = 0xc5; break; case 18: aid = 0xc6; break;
case 19: aid = 0xc7; break; case 20: aid = 0xc8; break;
case 21: aid = 0xc9; break; case 22: aid = 0x4a; break;
case 23: aid = 0x4b; break; case 24: aid = 0x4c; break;
default: return;
}
inputProcessor.sendAid(aid);
}
/** Send a PA key (1-3). */
public void sendPA(int number) {
int aid;
switch (number) {
case 1: aid = 0x6c; break;
case 2: aid = 0x6e; break;
case 3: aid = 0x6b; break;
default: return;
}
inputProcessor.sendAid(aid);
}
/** Send Clear key. */
public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
}
/** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); }
/** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); }
/** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); }
/** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); }
/** Move cursor to home position. */
public void cursorHome() { inputProcessor.cursorHome(); }
/** Tab to next unprotected field. */
public void tab() { inputProcessor.tab(); }
/** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); }
/** Move cursor to next line. */
public void newline() { inputProcessor.newline(); }
/** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
/** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); }
/** Attention key. */
public void attn() { inputProcessor.attn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager();
}
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
return dsProcessor.getGraphicsPlane();
}
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return dsProcessor.getGocaDecoder();
}
}
@@ -0,0 +1,75 @@
package haus.nightmare.lib3270j;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* Terminal model definitions for IBM 3278 and 3279 terminals.
* Models 2-5 are supported, each with default (24x80) and alternate screen sizes.
*/
public enum TerminalModel {
IBM_3278_2(2, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
IBM_3278_3(3, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
IBM_3278_4(4, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
IBM_3278_5(5, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS),
IBM_3279_2(2, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
IBM_3279_3(3, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
IBM_3279_4(4, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
IBM_3279_5(5, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS);
private final int modelNumber;
private final boolean color;
private final int defaultRows;
private final int defaultCols;
private final int alternateRows;
private final int alternateCols;
TerminalModel(int modelNumber, boolean color,
int defaultRows, int defaultCols,
int alternateRows, int alternateCols) {
this.modelNumber = modelNumber;
this.color = color;
this.defaultRows = defaultRows;
this.defaultCols = defaultCols;
this.alternateRows = alternateRows;
this.alternateCols = alternateCols;
}
public int getModelNumber() { return modelNumber; }
public boolean isColor() { return color; }
public int getDefaultRows() { return defaultRows; }
public int getDefaultCols() { return defaultCols; }
public int getAlternateRows() { return alternateRows; }
public int getAlternateCols() { return alternateCols; }
/**
* Returns the terminal type string for TN3270E negotiation.
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream.
*/
public String getTerminalType() {
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
}
/**
* Returns the base terminal type without "-E" suffix (for non-extended mode).
*/
public String getBaseTerminalType() {
return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber);
}
/**
* Look up a model by number and color mode.
*/
public static TerminalModel forModel(int number, boolean isColor) {
for (TerminalModel m : values()) {
if (m.modelNumber == number && m.color == isColor) {
return m;
}
}
throw new IllegalArgumentException("Unknown model: " + number + " color=" + isColor);
}
@Override
public String toString() {
return getTerminalType();
}
}
@@ -0,0 +1,142 @@
package haus.nightmare.lib3270j.charset;
/**
* EBCDIC ↔ Unicode translator.
* Default: Code Page 037 (US/Canada EBCDIC).
*/
public class EbcdicTranslator {
/**
* EBCDIC Code Page 037 to Unicode mapping.
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
*/
private static final int[] CP037_TO_UNICODE = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F (space, accent chars, punctuation)
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F (lowercase a-i)
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F (lowercase j-r)
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF (lowercase s-z)
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF (uppercase A-I)
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF (uppercase J-R)
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF (uppercase S-Z)
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF (digits 0-9)
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
/**
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
*/
private static final int[] UNICODE_TO_CP037 = new int[256];
static {
// Build reverse mapping
java.util.Arrays.fill(UNICODE_TO_CP037, -1);
for (int i = 0; i < 256; i++) {
int unicode = CP037_TO_UNICODE[i];
if (unicode < 256) {
UNICODE_TO_CP037[unicode] = i;
}
}
}
/**
* Translate EBCDIC byte to Unicode character.
*/
public char ebcdicToUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Static helper to translate EBCDIC byte to Unicode character.
*/
public static char toUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Static helper to translate EBCDIC byte to ASCII character.
*/
public static char ebcdicToAscii(int ebc) {
return toUnicode(ebc);
}
/**
* Translate Unicode character to EBCDIC byte.
* Returns -1 if the character cannot be mapped.
*/
public int unicodeToEbcdic(char unicode) {
if (unicode < 256) {
return UNICODE_TO_CP037[unicode];
}
return -1;
}
/**
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
public byte unicodeToEbcdicSafe(char unicode) {
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
}
/**
* Translate a byte array from EBCDIC to a Unicode string.
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
}
return sb.toString();
}
/**
* Translate a Unicode string to EBCDIC byte array.
*/
public byte[] stringToEbcdic(String s) {
byte[] result = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
result[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return result;
}
}
@@ -0,0 +1,515 @@
package haus.nightmare.lib3270j.datastream;
import java.io.ByteArrayOutputStream;
import java.util.logging.Logger;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
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.
*/
public class QueryReplyBuilder {
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
// Canned values from 3279-2 (matching sf.c)
private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c;
private static final int Xr_3279_2 = 0x000a02e5;
private static final int Yr_3279_2 = 0x0002006f;
private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
// Base query reply codes (text mode)
private static final int[] SUPPORTED_QR_BASE = {
QR_SUMMARY, // 0x80 — summary must list itself
QR_USABLE_AREA, // 0x81
QR_ALPHA_PART, // 0x84
QR_CHARSETS, // 0x85
QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_IMP_PART, // 0xa6
};
// Vector graphics query reply codes matching HOD DS3270.java line 1723
private static final int[] SUPPORTED_QR_VECTOR = {
QR_SUMMARY, // 0x80
QR_USABLE_AREA, // 0x81
QR_ALPHA_PART, // 0x84
QR_CHARSETS, // 0x85
QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88
QR_SAVE_RESTORE, // 0x8c
QR_DDM, // 0x95
QR_TRANSPARENCY, // 0x99
QR_IMP_PART, // 0xa6
QR_RPQ_NAMES, // 0xa8
QR_GRAPHICS, // 0xb0
QR_GIMAGE, // 0xb1
QR_AUX_DEV, // 0xb2
QR_OEM_FMT, // 0xb3
QR_GCOLOR, // 0xb4
QR_GSYMBOLS, // 0xb6
};
public QueryReplyBuilder(ScreenBuffer screen) {
this.screen = screen;
}
public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) {
this.screen = screen;
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
}
public GraphicsMode getGraphicsMode() {
return graphicsMode;
}
public void setGraphicsMode(GraphicsMode graphicsMode) {
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
}
/**
* Build all query replies as a single AID_SF + structured field response.
*/
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
// AID byte for structured field
out.write(AID_SF);
// Summary
appendQueryReply(out, QR_SUMMARY, buildSummary());
// Usable Area
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
// Alpha Partitions
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
// Character Sets
appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color
appendQueryReply(out, QR_COLOR, buildColor());
// Highlighting
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
// Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
if (graphicsMode.isVectorGraphicsEnabled()) {
// Save/Restore (0x8C)
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
}
// Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096));
if (graphicsMode.isVectorGraphicsEnabled()) {
// Transparency (0x99)
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
}
// Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
// Vector Graphics QRs if enabled
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
appendOemFmt(out); // 0xB3
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6
}
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray();
}
/**
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
* matching x3270 sf.c behavior.
*/
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
if (requestedCodes == null || requestedCodes.length == 0) {
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
}
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
for (byte codeByte : requestedCodes) {
int code = codeByte & 0xFF;
switch (code) {
case QR_SUMMARY:
appendQueryReply(out, QR_SUMMARY, buildSummary());
break;
case QR_USABLE_AREA:
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
break;
case QR_ALPHA_PART:
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
break;
case QR_CHARSETS:
appendQueryReply(out, QR_CHARSETS, buildCharsets());
break;
case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor());
break;
case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
break;
case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
break;
case QR_SAVE_RESTORE:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_DDM:
appendQueryReply(out, QR_DDM, buildDdm(4096));
break;
case QR_TRANSPARENCY:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_IMP_PART:
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_RPQ_NAMES:
case QR_RPQNAMES:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, code, buildRpqNames());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GRAPHICS:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GIMAGE:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_AUX_DEV:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_OEM_FMT:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendOemFmt(out);
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GCOLOR:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GCOLOR, buildGColor());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GSYMBOLS:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
default:
// Unsupported query reply code — emit QR_NULL
appendQueryReply(out, QR_NULL, new byte[0]);
break;
}
}
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
return out.toByteArray();
}
private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) {
// Length includes the 2-byte length field + SFID_QREPLY + code + data
int len = 4 + data.length;
out.write((len >> 8) & 0xFF);
out.write(len & 0xFF);
out.write(SFID_QREPLY);
out.write(code);
out.write(data, 0, data.length);
}
private byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
for (int code : codes) {
out.write(code);
}
return out.toByteArray();
}
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(0x01); // 12/14-bit addressing
out.write(0x00); // no special character features
out.write((maxCols >> 8) & 0xFF); // usable width high
out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low
out.write(0x01); // units (mm)
// Xr (4 bytes) - canned from 3279-2
out.write((Xr_3279_2 >> 24) & 0xFF);
out.write((Xr_3279_2 >> 16) & 0xFF);
out.write((Xr_3279_2 >> 8) & 0xFF);
out.write(Xr_3279_2 & 0xFF);
// Yr (4 bytes) - canned from 3279-2
out.write((Yr_3279_2 >> 24) & 0xFF);
out.write((Yr_3279_2 >> 16) & 0xFF);
out.write((Yr_3279_2 >> 8) & 0xFF);
out.write(Yr_3279_2 & 0xFF);
out.write(SW_3279_2); // AW
out.write(SH_3279_2); // AH
int buf = maxCols * maxRows;
out.write((buf >> 8) & 0xFF); // buffer size high
out.write(buf & 0xFF); // buffer size low
return out.toByteArray();
}
private byte[] buildAlphaPartitions(int maxRows) {
int bufSize = screen.getMaxCols() * screen.getMaxRows();
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high
out.write(bufSize & 0xFF); // total partition storage low
out.write(0x00); // flags
return out.toByteArray();
}
private byte[] buildCharsets() {
if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
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)
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
out.write(0x00); // Load PS device type (high)
out.write(0x00); // Load PS device type (low)
out.write(0x00); // reserved
out.write(0x07); // DL = 7 bytes per descriptor
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037)
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) - 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) - 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();
}
// Standard 3179G / Base character sets (matches sf.c / HOD)
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
out.write(0x82); // flags: GE, CGCSGID present
out.write(0x00); // more flags
out.write(SW_3279_2); // SDW - default char width (9)
out.write(SH_3279_2); // SDH - default char height (12)
out.write(0x00); // LoadPS format (0x00)
out.write(0x00);
out.write(0x00);
out.write(0x00);
out.write(0x07); // DL = 7
// Set 0 (Base EBCDIC - Non-loadable, single plane, CP037)
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
// Set 1 (APL/Text)
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
return out.toByteArray();
}
private 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
};
}
private 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
};
}
private byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
}
private byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved
out.write(0x00); // reserved
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
out.write(bufferSize & 0xFF);
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
out.write(bufferSize & 0xFF);
out.write(0x01); // NSS = 01
out.write(0x01); // DDMSS = 01
return out.toByteArray();
}
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters
// SDP 1: Default screen size (Model 2: 80x24)
out.write(0x00); // flags
out.write(0x00); // reserved
out.write(0x0b); // SDP length (11 bytes)
out.write(0x01); // SDP type: implicit partition sizes
out.write(0x00); // reserved
// Default size
out.write((MODEL_2_COLS >> 8) & 0xFF);
out.write(MODEL_2_COLS & 0xFF);
out.write((MODEL_2_ROWS >> 8) & 0xFF);
out.write(MODEL_2_ROWS & 0xFF);
// Alternate size (Model 4: 80x43, Model 5: 132x27, etc.)
out.write((maxCols >> 8) & 0xFF);
out.write(maxCols & 0xFF);
out.write((maxRows >> 8) & 0xFF);
out.write(maxRows & 0xFF);
return out.toByteArray();
}
private byte[] buildGraphics(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{
(byte) 0x80, 0x02,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
0x00
};
}
private byte[] buildGImage(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{
0x00, 0x01,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
}
private byte[] buildAuxDev() {
return new byte[]{
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
}
private byte[] buildSaveRestore() {
// HOD DS3270.java line 1768: 6 bytes payload
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
}
private byte[] buildTransparency() {
// HOD DS3270.java line 1782: 2 bytes payload
return new byte[]{ 0x00, 0x00 };
}
private byte[] buildRpqNames() {
// HOD DS3270.java line 1798: 5 bytes payload
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
}
private void appendOemFmt(ByteArrayOutputStream out) {
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
appendQueryReply(out, QR_OEM_FMT, new byte[]{
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_OEM_FMT, new byte[]{
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
});
appendQueryReply(out, QR_OEM_FMT, new byte[]{
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_OEM_FMT, new byte[]{
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
});
}
private byte[] buildGColor() {
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();
}
private byte[] buildGSymbols() {
return new byte[]{
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
}
}
@@ -0,0 +1,322 @@
package haus.nightmare.lib3270j.ft;
/**
* Configuration for an IND$FILE file transfer session.
* Ported from x3270's ft_conf_t (ft_private.h).
*/
public class FTConfig {
/** Host operating system type */
public enum HostType {
TSO, CMS, CICS
}
/** Record format for datasets (TSO/CMS sends only) */
public enum RecordFormat {
DEFAULT, FIXED, VARIABLE, UNDEFINED
}
/** Space allocation units (TSO sends only) */
public enum AllocationUnit {
DEFAULT, TRACKS, CYLINDERS, AVBLOCK
}
/** Transfer direction */
public enum Direction {
RECEIVE, SEND
}
/** Transfer mode */
public enum TransferMode {
ASCII, BINARY
}
/** CR/LF handling */
public enum CrAction {
ADD, REMOVE, KEEP
}
/** Behavior when destination file already exists */
public enum ExistAction {
KEEP, REPLACE, APPEND
}
// ========== Transfer Parameters ==========
private String hostFilename;
private String localFilename;
private Direction direction = Direction.RECEIVE;
private HostType hostType = HostType.TSO;
private TransferMode transferMode = TransferMode.ASCII;
private CrAction crAction = CrAction.REMOVE;
private boolean remapFlag = true;
private ExistAction existAction = ExistAction.KEEP;
private RecordFormat recfm = RecordFormat.DEFAULT;
private AllocationUnit units = AllocationUnit.DEFAULT;
private int lrecl = 0;
private int blksize = 0;
private int primarySpace = 0;
private int secondarySpace = 0;
private int avblock = 0;
private int dftBufferSize = FTConstants.DFT_BUF;
private String otherOptions = null;
// ========== Derived convenience getters ==========
public boolean isReceive() { return direction == Direction.RECEIVE; }
public boolean isSend() { return direction == Direction.SEND; }
public boolean isAscii() { return transferMode == TransferMode.ASCII; }
public boolean isBinary() { return transferMode == TransferMode.BINARY; }
public boolean isCrFlag() {
// CR processing is only applicable for ASCII transfers
return isAscii() && crAction != CrAction.KEEP;
}
public boolean isAppend() { return existAction == ExistAction.APPEND; }
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
// ========== Standard getters and setters ==========
public String getHostFilename() { return hostFilename; }
public void setHostFilename(String hostFilename) { this.hostFilename = hostFilename; }
public String getLocalFilename() { return localFilename; }
public void setLocalFilename(String localFilename) { this.localFilename = localFilename; }
public Direction getDirection() { return direction; }
public void setDirection(Direction direction) { this.direction = direction; }
public HostType getHostType() { return hostType; }
public void setHostType(HostType hostType) { this.hostType = hostType; }
public TransferMode getTransferMode() { return transferMode; }
public void setTransferMode(TransferMode transferMode) { this.transferMode = transferMode; }
public CrAction getCrAction() { return crAction; }
public void setCrAction(CrAction crAction) { this.crAction = crAction; }
public boolean isRemapFlag() { return remapFlag; }
public void setRemapFlag(boolean remapFlag) { this.remapFlag = remapFlag; }
public ExistAction getExistAction() { return existAction; }
public void setExistAction(ExistAction existAction) { this.existAction = existAction; }
public RecordFormat getRecfm() { return recfm; }
public void setRecfm(RecordFormat recfm) { this.recfm = recfm; }
public AllocationUnit getUnits() { return units; }
public void setUnits(AllocationUnit units) { this.units = units; }
public int getLrecl() { return lrecl; }
public void setLrecl(int lrecl) { this.lrecl = lrecl; }
public int getBlksize() { return blksize; }
public void setBlksize(int blksize) { this.blksize = blksize; }
public int getPrimarySpace() { return primarySpace; }
public void setPrimarySpace(int primarySpace) { this.primarySpace = primarySpace; }
public int getSecondarySpace() { return secondarySpace; }
public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; }
public int getAvblock() { return avblock; }
public void setAvblock(int avblock) { this.avblock = avblock; }
public int getDftBufferSize() { return dftBufferSize; }
public void setDftBufferSize(int dftBufferSize) {
this.dftBufferSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, dftBufferSize));
}
public String getOtherOptions() { return otherOptions; }
public void setOtherOptions(String otherOptions) {
this.otherOptions = (otherOptions != null && !otherOptions.trim().isEmpty())
? otherOptions.trim() : null;
}
// ========== Convenience UI setters ==========
public void setAppend(boolean append) {
if (append) this.existAction = ExistAction.APPEND;
}
public void setOverwrite(boolean overwrite) {
if (overwrite) this.existAction = ExistAction.REPLACE;
}
public void setReceive(boolean receive) {
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
}
public void setAscii(boolean ascii) {
setTransferMode(ascii ? TransferMode.ASCII : TransferMode.BINARY);
}
public void setCrFlag(boolean cr) {
setCrAction(cr ? CrAction.REMOVE : CrAction.KEEP);
}
public void setRecfm(String r) {
if (r == null || r.trim().isEmpty()) {
this.recfm = RecordFormat.DEFAULT;
} else {
String u = r.trim().toUpperCase();
if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
else this.recfm = RecordFormat.DEFAULT;
}
}
public void setLrecl(String l) {
try { this.lrecl = Integer.parseInt(l.trim()); } catch (NumberFormatException e) { this.lrecl = 0; }
}
public void setBlksize(String b) {
try { this.blksize = Integer.parseInt(b.trim()); } catch (NumberFormatException e) { this.blksize = 0; }
}
public void setSpace(String s) {
// Space string like "10,5" for primary/secondary
if (s == null || s.trim().isEmpty()) return;
String[] parts = s.split(",");
try {
if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim());
if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
// ignore
}
}
public void setOptions(String opts) {
setOtherOptions(opts);
}
// ========== Validation ==========
/**
* Validate the configuration before starting a transfer.
* @return null if valid, or an error message string
*/
public String validate() {
if (hostFilename == null || hostFilename.trim().isEmpty()) {
return "Host file name is required";
}
if (localFilename == null || localFilename.trim().isEmpty()) {
return "Local file name is required";
}
if (hostType == HostType.TSO && isSend() &&
units != AllocationUnit.DEFAULT && primarySpace <= 0) {
return "Primary space is required when allocation is specified";
}
if (hostType == HostType.TSO && isSend() &&
units == AllocationUnit.AVBLOCK && avblock <= 0) {
return "Avblock value is required when allocation is AVBLOCK";
}
return null;
}
/**
* Build the IND$FILE command string to send to the host.
* Ported from x3270's ft_go() in ft.c.
*/
public String buildCommand() {
StringBuilder cmd = new StringBuilder();
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename != null ? hostFilename.trim() : "");
// Collect options into a separate buffer
StringBuilder opts = new StringBuilder();
// Mode
if (isAscii()) {
opts.append("ASCII");
} else if (hostType == HostType.CICS) {
opts.append("BINARY");
}
// CR/LF handling
if (isAscii() && isCrFlag()) {
if (opts.length() > 0) opts.append(" ");
opts.append("CRLF");
} else if (hostType == HostType.CICS && !isAscii()) {
if (opts.length() > 0) opts.append(" ");
opts.append("NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
}
// Host-specific send options
if (isSend()) {
if (hostType == HostType.TSO) {
if (recfm != RecordFormat.DEFAULT) {
if (opts.length() > 0) opts.append(" ");
opts.append("RECFM(");
switch (recfm) {
case FIXED: opts.append("F"); break;
case VARIABLE: opts.append("V"); break;
case UNDEFINED: opts.append("U"); break;
default: break;
}
opts.append(")");
if (lrecl > 0) {
opts.append(" LRECL(").append(lrecl).append(")");
}
if (blksize > 0) {
opts.append(" BLKSIZE(").append(blksize).append(")");
}
}
if (units != AllocationUnit.DEFAULT) {
if (opts.length() > 0) opts.append(" ");
opts.append("SPACE(").append(primarySpace);
if (secondarySpace > 0) {
opts.append(",").append(secondarySpace);
}
opts.append(")");
switch (units) {
case TRACKS: opts.append(" TRACKS"); break;
case CYLINDERS: opts.append(" CYLINDERS"); break;
case AVBLOCK: opts.append(" AVBLOCK(").append(avblock).append(")"); break;
default: break;
}
}
} else if (hostType == HostType.CMS) {
if (recfm != RecordFormat.DEFAULT) {
if (opts.length() > 0) opts.append(" ");
opts.append("RECFM ");
switch (recfm) {
case FIXED: opts.append("F"); break;
case VARIABLE: opts.append("V"); break;
default: break;
}
if (lrecl > 0) {
opts.append(" LRECL ").append(lrecl);
}
}
}
}
// Additional options
if (otherOptions != null && !otherOptions.trim().isEmpty()) {
if (opts.length() > 0) opts.append(" ");
opts.append(otherOptions.trim());
}
// Apply options with host-specific prefix only if options are present
if (opts.length() > 0) {
if (hostType != HostType.TSO) {
cmd.append(" (").append(opts.toString());
} else {
cmd.append(" ").append(opts.toString());
}
}
return cmd.toString().trim();
}
}
@@ -0,0 +1,212 @@
package haus.nightmare.lib3270j.ft;
/**
* Constants for IND$FILE file transfer protocol.
* Ported from x3270: ft_cut_ds.h, ft_dft_ds.h, ft.c
*/
public final class FTConstants {
private FTConstants() {} // utility class
// ========== CUT Mode Frame Layout ==========
/** Offset to the CUT structured field at the end of the screen */
public static final int O_SF = 1919;
// Primary area offsets
public static final int O_FRAME_TYPE = 0;
// Control Code frame (host → terminal)
public static final int FT_CONTROL_CODE = 0xC3;
public static final int O_CC_FRAME_SEQ = 1;
public static final int O_CC_STATUS_CODE = 2;
public static final int O_CC_MESSAGE = 4;
// Control Code status codes
public static final int SC_HOST_ACK = 0x8181;
public static final int SC_XFER_COMPLETE = 0x8189;
public static final int SC_ABORT_FILE = 0x8194;
public static final int SC_ABORT_XMIT = 0x8198;
// Data Request frame (host → terminal, for uploads)
public static final int FT_DATA_REQUEST = 0xC2;
public static final int O_DR_SF = 1;
public static final int O_DR_DATA_CODE = 2;
public static final int O_DR_FRAME_SEQ = 3;
// Retransmit frame
public static final int FT_RETRANSMIT = 0x4C;
// Data frame (bidirectional)
public static final int FT_DATA = 0xC1;
public static final int O_DT_FRAME_SEQ = 1;
public static final int O_DT_CSUM = 2;
public static final int O_DT_LEN = 3;
public static final int O_DT_DATA = 5;
// Response Area (near end of screen)
public static final int O_RESPONSE = O_SF - 5;
public static final int RO_FRAME_TYPE = O_RESPONSE + 1;
public static final int RO_FRAME_SEQ = O_RESPONSE + 2;
public static final int RO_REASON_CODE = O_RESPONSE + 3;
// Response frame types
public static final int RFT_RETRANSMIT = 0x4C;
public static final int RFT_CONTROL_CODE = 0xC3;
// Special EOF data markers
public static final int EOF_DATA1 = 0x5C;
public static final int EOF_DATA2 = 0xA9;
// Upload data area offsets
public static final int O_UP_DATA_CODE = 2;
public static final int O_UP_FRAME_SEQ = 3;
public static final int O_UP_CSUM = 4;
public static final int O_UP_LEN = 5;
public static final int O_UP_DATA = 7;
public static final int O_UP_MAX = O_SF - O_UP_DATA;
// ========== CUT Mode AID codes ==========
public static final int ACK_OK = 0x7D; // AID_ENTER
public static final int ACK_RETRANSMIT = 0xF1; // AID_PF1
public static final int ACK_RESYNC_VM = 0x6D; // AID_CLEAR
public static final int ACK_RESYNC_TSO = 0x6E; // AID_PA2
public static final int ACK_ABORT = 0xF2; // AID_PF2
// ========== DFT Mode Structured Field Codes ==========
/** Structured field type for file transfer data */
public static final int SF_TRANSFER_DATA = 0xD0;
// Host requests
public static final int TR_OPEN_REQ = 0x0012;
public static final int TR_CLOSE_REQ = 0x4112;
public static final int TR_SET_CUR_REQ = 0x4511;
public static final int TR_GET_REQ = 0x4611;
public static final int TR_INSERT_REQ = 0x4711;
public static final int TR_DATA_INSERT = 0x4704;
// PC replies
public static final int TR_GET_REPLY = 0x4605;
public static final int TR_NORMAL_REPLY = 0x4705;
public static final int TR_ERROR_REPLY = 0x08; // low 8 bits
public static final int TR_CLOSE_REPLY = 0x4109;
// Other headers
public static final int TR_RECNUM_HDR = 0x6306;
public static final int TR_ERROR_HDR = 0x6904;
public static final int TR_NOT_COMPRESSED = 0xC080;
public static final int TR_BEGIN_DATA = 0x61;
// Error codes
public static final int TR_ERR_EOF = 0x2200;
public static final int TR_ERR_CMDFAIL = 0x0100;
// DFT buffer size limits
public static final int DFT_MIN_BUF = 256;
public static final int DFT_MAX_BUF = 32768;
public static final int DFT_BUF = 4096;
// AID for structured field response
public static final int AID_SF = 0x88;
// ========== Transfer State ==========
public enum FTState {
NONE, // No transfer in progress
AWAIT_ACK, // IND$FILE sent, awaiting acknowledgement
RUNNING, // Ack received, data flowing
ABORT_WAIT, // Awaiting chance to send an abort
ABORT_SENT // Abort sent; awaiting response
}
// ========== Encoding Tables ==========
/**
* Base-64-like encoding table used by CUT mode for lengths and checksums.
* 64 characters: a-z, &, -, ., :, +, A-Z, 0-5
*/
public static final String TABLE6 =
"abcdefghijklmnopqrstuvwxyz&-.,:+ABCDEFGHIJKLMNOPQRSTUVWXYZ012345";
/**
* IND$FILE's fixed ASCII-to-EBCDIC translation table (i_asc2ft).
* This is NOT the standard CP037 mapping — it's IND$FILE's own table.
* Used when remap=true to invert IND$FILE's built-in translation.
*/
public static final int[] ASC2FT = {
0x00,0x01,0x02,0x03,0x37,0x2d,0x2e,0x2f,0x16,0x05,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
0x10,0x11,0x12,0x13,0x3c,0x3d,0x32,0x26,0x18,0x19,0x3f,0x27,0x1c,0x1d,0x1e,0x1f,
0x40,0x5a,0x7f,0x7b,0x5b,0x6c,0x50,0x7d,0x4d,0x5d,0x5c,0x4e,0x6b,0x60,0x4b,0x61,
0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0x7a,0x5e,0x4c,0x7e,0x6e,0x6f,
0x7c,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,
0xd7,0xd8,0xd9,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0x4a,0xe0,0x4f,0x5f,0x6d,
0x79,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x91,0x92,0x93,0x94,0x95,0x96,
0x97,0x98,0x99,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xc0,0x6a,0xd0,0xa1,0x07,
0x20,0x21,0x22,0x23,0x24,0x15,0x06,0x17,0x28,0x29,0x2a,0x2b,0x2c,0x09,0x0a,0x1b,
0x30,0x31,0x1a,0x33,0x34,0x35,0x36,0x08,0x38,0x39,0x3a,0x3b,0x04,0x14,0x3e,0xe1,
0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x51,0x52,0x53,0x54,0x55,0x56,0x57,
0x58,0x59,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x70,0x71,0x72,0x73,0x74,0x75,
0x76,0x77,0x78,0x80,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,0x90,0x9a,0x9b,0x9c,0x9d,0x9e,
0x9f,0xa0,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,
0xb8,0xb9,0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xca,0xcb,0xcc,0xcd,0xce,0xcf,0xda,0xdb,
0xdc,0xdd,0xde,0xdf,0xea,0xeb,0xec,0xed,0xee,0xef,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
};
/**
* IND$FILE's fixed EBCDIC-to-ASCII translation table (i_ft2asc).
* The inverse of ASC2FT.
*/
public static final int[] FT2ASC = {
0x00,0x01,0x02,0x03,0x9c,0x09,0x86,0x7f,0x97,0x8d,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
0x10,0x11,0x12,0x13,0x9d,0x85,0x08,0x87,0x18,0x19,0x92,0x8f,0x1c,0x1d,0x1e,0x1f,
0x80,0x81,0x82,0x83,0x84,0x00,0x17,0x1b,0x88,0x89,0x8a,0x8b,0x8c,0x05,0x06,0x07,
0x90,0x91,0x16,0x93,0x94,0x95,0x96,0x04,0x98,0x99,0x9a,0x9b,0x14,0x15,0x9e,0x1a,
0x20,0xa0,0xa1,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0x5b,0x2e,0x3c,0x28,0x2b,0x5d,
0x26,0xa9,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0x21,0x24,0x2a,0x29,0x3b,0x5e,
0x2d,0x2f,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0x7c,0x2c,0x25,0x5f,0x3e,0x3f,
0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xc0,0xc1,0xc2,0x60,0x3a,0x23,0x40,0x27,0x3d,0x22,
0xc3,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,
0xca,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xcb,0xcc,0xcd,0xce,0xcf,0xd0,
0xd1,0x7e,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,
0xd8,0xd9,0xda,0xdb,0xdc,0xdd,0xde,0xdf,0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,
0x7b,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0xe8,0xe9,0xea,0xeb,0xec,0xed,
0x7d,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f,0x50,0x51,0x52,0xee,0xef,0xf0,0xf1,0xf2,0xf3,
0x5c,0x9f,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,
0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
};
// ========== Utility Methods ==========
/**
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
* Converts a table6-encoded EBCDIC character to its 6-bit value.
*/
public static int from6(int ebcdicByte, haus.nightmare.lib3270j.charset.EbcdicTranslator translator) {
char ascii = translator.ebcdicToUnicode(ebcdicByte & 0xFF);
int idx = TABLE6.indexOf(ascii);
return idx >= 0 ? idx : 0;
}
public static int from6(int ebcdicByte) {
// First convert EBCDIC to ASCII via IND$FILE's table
int ascii = FT2ASC[ebcdicByte & 0xFF];
int idx = TABLE6.indexOf((char) ascii);
return idx >= 0 ? idx : 0;
}
/**
* Encode a 6-bit value into a table6-encoded EBCDIC character.
*/
public static int to6(int value, haus.nightmare.lib3270j.charset.EbcdicTranslator translator) {
char ascii = TABLE6.charAt(value & 0x3F);
int ebc = translator.unicodeToEbcdic(ascii);
return ebc >= 0 ? ebc : ASC2FT[ascii & 0xFF];
}
public static int to6(int value) {
char ascii = TABLE6.charAt(value & 0x3F);
return ASC2FT[ascii & 0xFF];
}
}
@@ -0,0 +1,559 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import static haus.nightmare.lib3270j.ft.FTConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
* CUT (Character Unit Transfer) mode file transfer handler.
* Data flows through the screen buffer using a framed protocol.
* Ported from x3270's ft_cut.c.
*/
public class FTCut {
private static final Logger log = Logger.getLogger(FTCut.class.getName());
/** Callback for transfer events */
public interface FTCutListener {
void onCutRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
FTConstants.FTState getCurrentState();
void setState(FTConstants.FTState state);
FTConfig getConfig();
File getLocalFile();
}
private final ScreenBuffer screen;
private final InputProcessor input;
private final EbcdicTranslator translator;
private final FTCutListener listener;
// CUT mode state
private boolean xferInProgress = false;
private long expandedLength = 0;
private int quadrant = -1;
private boolean cutEof = false;
// Upload translation buffer
private static final int XLATE_NBUF = 32;
private int xlateBuffered = 0;
private int xlateBufIx = 0;
private final int[] xlateBuf = new int[XLATE_NBUF];
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
private static final int SELECTOR_0 = 0x5E; // ';' (EBCDIC)
private static final int SELECTOR_1 = 0x7E; // '=' (EBCDIC)
private static final int SELECTOR_2 = 0x5C; // '*' (EBCDIC)
private static final int SELECTOR_3 = 0x7D; // '\'' (EBCDIC)
private static final int[] XLATE_0 = {
0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,
0xd3,0xd4,0xd5,0xd6, 0xd7,0xd8,0xd9,0xe2, 0xe3,0xe4,0xe5,0xe6,
0xe7,0xe8,0xe9,0x81, 0x82,0x83,0x84,0x85, 0x86,0x87,0x88,0x89,
0x91,0x92,0x93,0x94, 0x95,0x96,0x97,0x98, 0x99,0xa2,0xa3,0xa4,
0xa5,0xa6,0xa7,0xa8, 0xa9,0xf0,0xf1,0xf2, 0xf3,0xf4,0xf5,0xf6,
0xf7,0xf8,0xf9,0x6c, 0x50,0x6d,0x4d,0x5d, 0x4c,0x4e,0x6b,0x60,
0x4b,0x61,0x7a,0x6e, 0x6f
};
private static final int[] XLATE_1 = {
0x20,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, 0x48,0x49,0x4a,0x4b,
0x4c,0x4d,0x4e,0x4f, 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57,
0x58,0x59,0x5a,0x61, 0x62,0x63,0x64,0x65, 0x66,0x67,0x68,0x69,
0x6a,0x6b,0x6c,0x6d, 0x6e,0x6f,0x70,0x71, 0x72,0x73,0x74,0x75,
0x76,0x77,0x78,0x79, 0x7a,0x30,0x31,0x32, 0x33,0x34,0x35,0x36,
0x37,0x38,0x39,0x25, 0x26,0x27,0x28,0x29, 0x2a,0x2b,0x2c,0x2d,
0x2e,0x2f,0x3a,0x3b, 0x3f
};
private static final int[] XLATE_2 = {
0x00,0x00,0x01,0x02, 0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0a,
0x0b,0x0c,0x0d,0x0e, 0x0f,0x10,0x11,0x12, 0x13,0x14,0x15,0x16,
0x17,0x18,0x19,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x3c,0x3d,0x3e, 0x00,0xfa,0xfb,0xfc,
0xfd,0xfe,0xff,0x7b, 0x7c,0x7d,0x7e,0x7f, 0x1a,0x1b,0x1c,0x1d,
0x1e,0x1f,0x00,0x00, 0x00
};
private static final int[] XLATE_3 = {
0x00,0xa0,0xa1,0xea, 0xeb,0xec,0xed,0xee, 0xef,0xe0,0xe1,0xaa,
0xab,0xac,0xad,0xae, 0xaf,0xb0,0xb1,0xb2, 0xb3,0xb4,0xb5,0xb6,
0xb7,0xb8,0xb9,0x80, 0x00,0xca,0xcb,0xcc, 0xcd,0xce,0xcf,0xc0,
0x00,0x8a,0x8b,0x8c, 0x8d,0x8e,0x8f,0x90, 0x00,0xda,0xdb,0xdc,
0xdd,0xde,0xdf,0xd0, 0x00,0x00,0x21,0x22, 0x23,0x24,0x5b,0x5c,
0x00,0x5e,0x5f,0x00, 0x9c,0x9d,0x9e,0x9f, 0xba,0xbb,0xbc,0xbd,
0xbe,0xbf,0x9a,0x9b, 0x00
};
private static final int[][] QUADS = { XLATE_0, XLATE_1, XLATE_2, XLATE_3 };
private static final int[] SELECTORS = { SELECTOR_0, SELECTOR_1, SELECTOR_2, SELECTOR_3 };
// File I/O
private InputStream inputStream;
private OutputStream outputStream;
private boolean lastCr = false;
public FTCut(ScreenBuffer screen, InputProcessor input,
EbcdicTranslator translator, FTCutListener listener) {
this.screen = screen;
this.input = input;
this.translator = translator;
this.listener = listener;
}
/**
* Initialize CUT mode with active streams.
*/
public void initTransfer(InputStream in, OutputStream out) {
this.inputStream = in;
this.outputStream = out;
resetState();
}
/**
* Initialize CUT mode for a new transfer.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
resetState();
if (config.isReceive()) {
boolean append = config.isAppend();
outputStream = new FileOutputStream(localFile, append);
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
private void resetState() {
xferInProgress = false;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
lastCr = false;
}
/**
* Clean up CUT mode resources.
*/
public void cleanup() {
xferInProgress = false;
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
}
/**
* Process a CUT-mode screen update.
* Called by the transfer coordinator when the screen changes during a transfer.
*/
public void processScreenUpdate() {
if (listener.getCurrentState() == FTState.NONE) return;
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
if (xferInProgress) {
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
xferInProgress = false;
listener.onTransferAborted("Host aborted transfer (returned to menu)");
}
return;
}
int frameType = screen.getCellEC(O_FRAME_TYPE);
switch (frameType) {
case FT_CONTROL_CODE:
cutControlCode();
break;
case FT_DATA_REQUEST:
cutDataRequest();
break;
case FT_RETRANSMIT:
cutRetransmit();
break;
case FT_DATA:
cutData();
break;
default:
log.fine("Ignoring non-CUT frame type 0x" + Integer.toHexString(frameType));
return;
}
}
private boolean isSkip(byte attr) {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
}
// ========== Control Code Processing ==========
private void cutControlCode() {
int code = (screen.getCellEC(O_CC_STATUS_CODE) << 8) |
screen.getCellEC(O_CC_STATUS_CODE + 1);
log.fine("CUT: CONTROL_CODE 0x" + Integer.toHexString(code));
switch (code) {
case SC_HOST_ACK:
log.info("CUT: HOST_ACK received — transfer running");
xferInProgress = true;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
cutAck();
listener.onCutRunning();
break;
case SC_XFER_COMPLETE:
log.info("CUT: Transfer complete");
cutAck();
xferInProgress = false;
listener.onTransferComplete(null);
break;
case SC_ABORT_FILE:
case SC_ABORT_XMIT:
log.warning("CUT: ABORT received");
xferInProgress = false;
cutAck();
String msg = extractHostMessage();
listener.onTransferAborted(msg);
break;
default:
log.warning("CUT: Unknown control code 0x" + Integer.toHexString(code));
cutAbort("Unknown CUT control code", SC_ABORT_XMIT);
break;
}
}
/**
* Extract the error message from the host's control code frame.
* The message starts at O_CC_MESSAGE and is up to 80 EBCDIC characters.
*/
private String extractHostMessage() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 80; i++) {
int ebc = screen.getCellEC(O_CC_MESSAGE + i);
if (ebc == 0) continue;
char ch = translator.ebcdicToUnicode(ebc);
sb.append(ch);
}
String msg = sb.toString().stripTrailing();
if (msg.endsWith("$")) {
msg = msg.substring(0, msg.length() - 1).stripTrailing();
}
return msg.isEmpty() ? "Host cancelled transfer" : msg;
}
// ========== Data Request (Upload) ==========
private void cutDataRequest() {
int seqEbc = screen.getCellEC(O_DR_FRAME_SEQ);
int seq = FTConstants.from6(seqEbc, translator);
log.fine("CUT: DATA_REQUEST seq=" + seq);
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
return;
}
FTConfig config = listener.getConfig();
int count = 0;
try {
while (count < O_UP_MAX && !cutEof) {
int c = xlateGetc(config);
if (c == -1) {
cutEof = true;
break;
}
screen.setCell(O_UP_DATA + count, c);
count++;
}
} catch (IOException e) {
log.warning("CUT: Read error: " + e.getMessage());
cutAbort("Read error: " + e.getMessage(), SC_ABORT_FILE);
return;
}
if (count == 0 && cutEof) {
screen.setCell(O_UP_DATA, EOF_DATA1);
screen.setCell(O_UP_DATA + 1, EOF_DATA2);
count = 2;
}
screen.setCell(O_UP_FRAME_SEQ, seqEbc);
int cs = 0;
for (int i = 0; i < count; i++) {
cs ^= screen.getCellEC(O_UP_DATA + i);
}
screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator));
screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator));
// Hide data field by setting zero intensity on field attribute
byte attr = screen.getCellFAByte(O_DR_SF);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL | FA_MODIFY);
screen.setCellFA(O_DR_SF, attr);
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
}
// ========== Data (Download) ==========
private void cutData() {
log.fine("CUT: DATA");
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
return;
}
FTConfig config = listener.getConfig();
int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
FTConstants.from6(screen.getCellEC(O_DT_LEN + 1), translator);
if (rawLength > O_RESPONSE - O_DT_DATA) {
cutAbort("Oversized CUT data frame", SC_ABORT_XMIT);
return;
}
byte[] rawData = new byte[rawLength];
for (int i = 0; i < rawLength; i++) {
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
}
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
(rawData[1] & 0xFF) == EOF_DATA2) {
log.fine("CUT: EOF marker received");
cutAck();
return;
}
try {
byte[] converted = convertDownload(rawData, rawLength, config);
if (outputStream != null) {
outputStream.write(converted);
expandedLength += converted.length;
listener.onBytesTransferred(expandedLength);
}
cutAck();
} catch (IOException e) {
log.warning("CUT: Write error: " + e.getMessage());
cutAbort("Write error: " + e.getMessage(), SC_ABORT_FILE);
}
}
// ========== Retransmit ==========
private void cutRetransmit() {
log.warning("CUT: RETRANSMIT (not supported, aborting)");
cutAbort("Retransmit not supported", SC_ABORT_XMIT);
}
// ========== Acknowledge ==========
private void cutAck() {
log.fine("CUT: > ACK (Enter)");
input.sendAidForFT(AID_ENTER);
}
// ========== Abort ==========
private void cutAbort(String message, int reason) {
log.warning("CUT: ABORT — " + message);
screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE);
screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ));
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
input.sendAidForFT(AID_PF2);
listener.onTransferAborted(message);
}
// ========== Character Translation ==========
/**
* Convert received CUT EBCDIC data to local format.
* Matching x3270 upload_convert logic.
*/
private byte[] convertDownload(byte[] rawData, int length, FTConfig config)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(length * 2);
for (int i = 0; i < length; i++) {
int c = rawData[i] & 0xFF;
while (true) {
if (quadrant < 0) {
for (quadrant = 0; quadrant < 4; quadrant++) {
if (c == SELECTORS[quadrant]) break;
}
if (quadrant >= 4) {
throw new IOException("CUT conversion error (quadrant selector not found)");
}
break;
}
if (c < 0x40 || c > 0xF9) {
throw new IOException("CUT conversion error (data out of bounds)");
}
char asciiChar = (char) FT2ASC[c & 0xFF];
int ix = ALPHAS.indexOf(asciiChar);
if (ix < 0) {
quadrant = -1;
continue;
}
if (!(quadrant == 2 && c == 0xC1) && QUADS[quadrant][ix] == 0) {
quadrant = -1;
continue;
}
int decoded = QUADS[quadrant][ix];
if (config.isAscii() && config.isCrFlag() && (decoded == 0x0D || decoded == 0x1A)) {
break;
}
if (!config.isAscii() || !config.isRemapFlag()) {
out.write(decoded);
break;
}
if (decoded < 0x20 || (decoded >= 0x80 && decoded < 0xA0 && decoded != 0x9F)) {
out.write(String.valueOf((char) decoded).getBytes(StandardCharsets.UTF_8));
} else if (decoded == 0xFF) {
out.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
} else {
int ebc = ASC2FT[decoded & 0xFF];
char unicodeChar = translator.ebcdicToUnicode(ebc);
out.write(String.valueOf(unicodeChar).getBytes(StandardCharsets.UTF_8));
}
break;
}
}
return out.toByteArray();
}
private int xlateGetc(FTConfig config) throws IOException {
if (xlateBuffered > 0) {
int r = xlateBuf[xlateBufIx++];
xlateBuffered--;
return r;
}
if (inputStream == null) return -1;
int c = inputStream.read();
if (c == -1) return -1;
int localByte = c & 0xFF;
int nc = 0;
int[] cbuf = new int[4];
if (config.isAscii()) {
if (config.isCrFlag() && !lastCr && localByte == '\n') {
nc += uploadConvert('\r', cbuf, nc, config);
}
lastCr = (localByte == '\r');
}
nc += uploadConvert(localByte, cbuf, nc, config);
if (nc > 1) {
for (int i = 1; i < nc; i++) {
xlateBuf[xlateBuffered++] = cbuf[i];
}
xlateBufIx = 0;
}
return cbuf[0];
}
private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) {
int ebc;
if (localByte == 0) {
if (quadrant != 2) {
quadrant = 2; // OTHER_2
cbuf[offset] = SELECTORS[quadrant];
cbuf[offset + 1] = 0xC1; // XLATE_NULL
return 2;
} else {
cbuf[offset] = 0xC1;
return 1;
}
}
if (!config.isAscii() || !config.isRemapFlag()) {
ebc = localByte & 0xFF;
} else {
int standardEbc = translator.unicodeToEbcdic((char) localByte);
if (standardEbc < 0) {
standardEbc = 0x40;
}
ebc = FTConstants.FT2ASC[standardEbc & 0xFF];
}
return storeUpload(ebc, cbuf, offset);
}
private int storeUpload(int pseudoAsciiByte, int[] obBuf, int offset) {
if (quadrant >= 0) {
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == pseudoAsciiByte) {
char ch = ALPHAS.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
obBuf[offset] = ebc >= 0 ? ebc : 0x40;
return 1;
}
}
}
int oq = quadrant;
for (quadrant = 0; quadrant < 4; quadrant++) {
if (quadrant == oq) continue;
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == pseudoAsciiByte) {
char ch = ALPHAS.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
obBuf[offset] = SELECTORS[quadrant];
obBuf[offset + 1] = ebc >= 0 ? ebc : 0x40;
return 2;
}
}
}
quadrant = -1;
int questionEbc = translator.unicodeToEbcdic('?');
obBuf[offset] = questionEbc >= 0 ? questionEbc : 0x6F;
return 1;
}
}
@@ -0,0 +1,547 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import static haus.nightmare.lib3270j.ft.FTConstants.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
* DFT (Distributed Function Terminal) mode file transfer handler.
* Uses Structured Fields (SF_TRANSFER_DATA = 0xD0) for data exchange.
* Offers better performance than CUT mode with configurable buffer sizes.
* Ported from x3270's ft_dft.c.
*/
public class FTDft {
private static final Logger log = Logger.getLogger(FTDft.class.getName());
private static final String OPEN_MSG = "FT:MSG";
private static final String END_TRANSFER = "TRANS03";
/** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener {
void onDftRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
FTConstants.FTState getCurrentState();
void setState(FTConstants.FTState state);
FTConfig getConfig();
File getLocalFile();
}
private final InputProcessor input;
private final EbcdicTranslator translator;
private final FTDftListener listener;
// DFT state
private long recnum = 1;
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
// Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null;
private int dftSaveBufLen = 0;
// File I/O
private InputStream inputStream;
private OutputStream outputStream;
private boolean lastCr = false;
public FTDft(InputProcessor input, EbcdicTranslator translator,
FTDftListener listener) {
this.input = input;
this.translator = translator;
this.listener = listener;
}
/**
* Initialize DFT mode with active streams.
*/
public void initTransfer(InputStream in, OutputStream out) {
this.inputStream = in;
this.outputStream = out;
resetState();
}
/**
* Initialize DFT mode for a new transfer from file.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
resetState();
if (config.isReceive()) {
outputStream = new FileOutputStream(localFile, config.isAppend());
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
private void resetState() {
recnum = 1;
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
}
/**
* Clean up DFT mode resources.
*/
public void cleanup() {
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
dftSaveBuf = null;
dftSaveBufLen = 0;
}
/**
* Process a DFT structured field from the host.
* Called by DataStreamProcessor when SF type is SF_TRANSFER_DATA (0xD0).
*
* @param data raw structured field data
* @param offset start of the SF (after length + type byte)
* @param length total SF length
*/
public void processStructuredField(byte[] data, int offset, int length) {
// The SF payload starts after the 2-byte length + 1-byte SF type
int payloadStart = offset + 3;
if (payloadStart + 2 > offset + length) {
log.warning("DFT: SF too short");
return;
}
// First 2 bytes of payload are the DFT request code
int requestCode = ((data[payloadStart] & 0xFF) << 8) |
(data[payloadStart + 1] & 0xFF);
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest(data, offset, length);
break;
case TR_INSERT_REQ:
dftInsertRequest(data, payloadStart, offset + length - payloadStart);
break;
case TR_DATA_INSERT:
dftDataInsert(data, payloadStart, offset + length - payloadStart);
break;
case TR_SET_CUR_REQ:
// No-op, same as x3270
log.fine("DFT: SetCursor (ignored)");
break;
case TR_GET_REQ:
dftGetRequest();
break;
case TR_CLOSE_REQ:
dftCloseRequest();
break;
default:
log.warning("DFT: Unknown request code 0x" + Integer.toHexString(requestCode));
dftAbort("Unknown DFT request", TR_DATA_INSERT);
break;
}
}
// ========== Open Request ==========
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
log.fine("DFT: Open request");
// Parse open request payload matching x3270
// sfLength is the 2-byte length value at sfOffset
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
String nameBuf = "";
if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) {
nameBuf = extractName(data, sfOffset + 3 + 25, 7);
} else if (sfLenVal == 0x29 && sfOffset + 3 + 31 <= data.length) {
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
}
if (OPEN_MSG.equalsIgnoreCase(nameBuf)) {
messageFlag = true;
log.info("DFT: Open request for message stream");
} else {
messageFlag = false;
listener.onDftRunning();
}
dftEof = false;
recnum = 1;
// Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009)
dftOpenAck();
}
private String extractName(byte[] data, int start, int maxLen) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < maxLen && (start + i) < data.length; i++) {
int b = data[start + i] & 0xFF;
if (b == 0) break;
char ch = translator.ebcdicToUnicode(b);
sb.append(ch);
}
return sb.toString().trim();
}
private void dftOpenAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5); // SF length
out.write(SF_TRANSFER_DATA);
out.write(0); out.write(9); // OpenAck response code 0x0009
input.sendStructuredFieldData(out.toByteArray());
}
// ========== Insert Request (host sending data for download) ==========
private void dftInsertRequest(byte[] data, int offset, int length) {
log.fine("DFT: Insert");
dftDataInsert(data, offset, length);
}
private void dftDataInsert(byte[] data, int offset, int length) {
FTConfig config = listener.getConfig();
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
dftAbort("Transfer cancelled by user", TR_DATA_INSERT);
return;
}
// Skip the 2-byte request code
int pos = offset + 2;
int end = offset + length;
// Look for TR_BEGIN_DATA marker
while (pos < end) {
int headerCode = data[pos] & 0xFF;
if (headerCode == TR_BEGIN_DATA) {
if (pos + 3 > end) break;
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
int actualDataLen = dataLen - 3;
pos += 3;
if (actualDataLen > 0 && pos + actualDataLen <= end) {
if (messageFlag) {
// Handle message payload from host
dftDataAck();
handleHostMessage(data, pos, actualDataLen);
return;
}
try {
writeDownloadData(data, pos, actualDataLen, config);
bytesTransferred += actualDataLen;
listener.onBytesTransferred(bytesTransferred);
} catch (IOException e) {
dftAbort("Write error: " + e.getMessage(), TR_DATA_INSERT);
return;
}
}
pos += Math.max(0, actualDataLen);
} else if (pos + 1 < end) {
int hdrCode16 = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
if (hdrCode16 == TR_RECNUM_HDR) {
pos += 6;
} else if (hdrCode16 == TR_NOT_COMPRESSED) {
pos += 2;
} else {
pos += 2;
}
} else {
pos++;
}
}
// Send acknowledgement for file data
dftDataAck();
}
private void handleHostMessage(byte[] data, int offset, int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (b == 0 || b == '$') break;
char ch = translator.ebcdicToUnicode(b);
sb.append(ch);
}
String msg = sb.toString().trim();
log.info("DFT message: " + msg);
String msgLower = msg.toLowerCase();
if (msg.startsWith(END_TRANSFER) || msgLower.contains("complete") || msgLower.contains("transferred") || msgLower.contains("success")) {
listener.onTransferComplete(null);
} else if (listener.getCurrentState() == FTState.ABORT_SENT || msgLower.contains("error") || msgLower.contains("failed") || msgLower.contains("abort")) {
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
} else {
// Informational message (default success)
listener.onTransferComplete(null);
}
}
/**
* Write download data to the local file, handling ASCII conversion.
* Matching x3270 upload_convert logic.
*/
private void writeDownloadData(byte[] data, int offset, int length,
FTConfig config) throws IOException {
if (outputStream == null) return;
if (!config.isAscii()) {
outputStream.write(data, offset, length);
return;
}
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (config.isCrFlag() && (b == '\r' || b == 0x1A)) {
continue; // Strip CR and EOF ^Z
}
if (!config.isRemapFlag()) {
outputStream.write(b);
continue;
}
/*
* ASCII mode with remap:
* Host IND$FILE sends pseudo-ASCII byte b.
* Map pseudo-ASCII b to EBCDIC byte via ASC2FT[b],
* then convert EBCDIC byte to Unicode UTF-8 character.
*/
if (b < 0x20 || (b >= 0x80 && b < 0xA0 && b != 0x9F)) {
// Control code — write as Unicode directly
outputStream.write(String.valueOf((char) b).getBytes(StandardCharsets.UTF_8));
} else if (b == 0xFF) {
// Special 0xFF -> U+009F
outputStream.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
} else {
int ebc = ASC2FT[b & 0xFF];
char ch = translator.ebcdicToUnicode(ebc);
outputStream.write(String.valueOf(ch).getBytes(StandardCharsets.UTF_8));
}
}
}
// ========== Get Request (host wants data for upload) ==========
private void dftGetRequest() {
FTConfig config = listener.getConfig();
log.fine("DFT: Get");
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
dftAbort("Transfer cancelled by user", TR_GET_REQ);
return;
}
int bufferSize = config.getDftBufferSize();
int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
try {
while (!dftEof && totalRead < numbytes) {
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
}
} catch (IOException e) {
dftAbort("Read error: " + e.getMessage(), TR_GET_REQ);
return;
}
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
if (totalRead > 0) {
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
out.write((int) (recnum & 0xFF));
recnum++;
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
out.write(TR_NOT_COMPRESSED & 0xFF);
out.write(TR_BEGIN_DATA);
int dataFieldLen = totalRead + 5;
out.write((dataFieldLen >> 8) & 0xFF);
out.write(dataFieldLen & 0xFF);
out.write(readBuf, 0, totalRead);
bytesTransferred += totalRead;
} else {
log.fine("DFT: > GetReply EOF");
out.write((TR_GET_REQ >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_EOF >> 8) & 0xFF);
out.write(TR_ERR_EOF & 0xFF);
dftEof = true;
}
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
}
/**
* Read a byte from local file for upload, handling ASCII conversion and remapping.
* Matching x3270 dft_ascii_read logic.
*/
private int dftAsciiRead(FTConfig config) throws IOException {
if (inputStream == null) return -1;
int c = inputStream.read();
if (c == -1) return -1;
if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false;
// Expand \n to \r\n: return \r byte now
int rEbc = translator.unicodeToEbcdic('\r');
if (rEbc < 0) rEbc = 0x0D;
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
}
lastCr = (c == '\r');
if (!config.isRemapFlag()) {
int ebc = translator.unicodeToEbcdic((char) c);
return ebc >= 0 ? ebc : 0x40;
}
/*
* ASCII mode with remap:
* Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc].
*/
int ebc;
if (c < 0x20 || (c >= 0x80 && c < 0x9F)) {
ebc = ASC2FT[c & 0xFF];
} else if (c == 0x9F) {
ebc = 0xFF;
} else {
ebc = translator.unicodeToEbcdic((char) c);
}
if (ebc < 0) ebc = 0x40;
return FT2ASC[ebc & 0xFF];
}
// ========== Close Request ==========
private void dftCloseRequest() {
log.fine("DFT: Close");
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
out.write(TR_CLOSE_REPLY & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
if (!messageFlag) {
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
listener.onTransferComplete(null);
}
}
// ========== Data Acknowledgement ==========
private void dftDataAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
out.write(TR_NORMAL_REPLY & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
}
// ========== Abort ==========
private void dftAbort(String message, int code) {
log.warning("DFT: ABORT — " + message);
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
listener.onTransferAborted(message);
}
/**
* Handle a Read Modified command when upload data is pending.
* Retransmits the last saved buffer.
*/
public void readModified() {
if (dftSaveBuf != null && dftSaveBufLen > 0) {
log.fine("DFT: Retransmitting saved buffer");
input.sendStructuredFieldData(dftSaveBuf);
}
}
}
@@ -0,0 +1,195 @@
package haus.nightmare.lib3270j.graphics;
/**
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
* vector graphics and Programmed Symbols (PS).
*/
public final class GocaConstants {
private GocaConstants() {}
// Structured Field IDs
public static final int SF_LOADPS = 0x0F; // 2-byte Structured Field prefix (or 0x06 for direct Load PS)
public static final int SF_LOADPS_DIRECT = 0x06; // Load Programmed Symbols direct SFID
public static final int SF_OBJCNTL = 0x24; // Object Control
public static final int SF_OBJDATA = 0x85; // Graphics Object Data
public static final int SF_3270_G = 0x20; // 3270 Graphics / Picture
// Structured Field Sub-IDs (for SF 0x0F)
public static final int SF_LOADPS_SUB = 0x06; // Load Programmed Symbols
public static final int SF_LOADLT_SUB = 0x07; // Load Line Type / Symbol Set
public static final int SF_OBJDATA_SUB = 0x0F; // Graphics Object Data (GOCA draw orders)
public static final int SF_OBJPICT_SUB = 0x10; // Graphics Object Picture (Segment draw orders)
public static final int SF_OBJCNTL_SUB = 0x11; // Graphics Object Control (Procedure orders)
// Procedure Orders (for SF_OBJCNTL_SUB 0x11)
public static final int P_NOP1 = 0x00; // Procedure NOOP
public static final int P_COMT = 0x01; // Procedure Comment
public static final int P_ATTCUR = 0x08; // Attach Graphic Cursor
public static final int P_DETCUR = 0x09; // Detach Graphic Cursor
public static final int P_ERASE = 0x0A; // Erase Graphics Presentation Space
public static final int P_STOPDR = 0x0F; // Stop Draw
public static final int P_SCUDEF = 0x21; // Set Current Defaults
public static final int P_BEGPROC = 0x30; // Begin Procedure
public static final int P_SETCUR = 0x31; // Set Graphic Cursor Position
// Coordinate space
public static final int VIRTUAL_COORD_MAX = 4096;
// GOCA Drawing / Segment Orders
public static final int G_NOP1 = 0x00; // NOOP 1-byte
public static final int G_COMT = 0x01; // Comment
public static final int G_GSMC = 0x07; // Set Marker Color
public static final int G_GSPS = 0x08; // Set Pattern Set
public static final int G_GSCOL = 0x0A; // Set Color
public static final int G_GSMX = 0x0C; // Set Foreground Mix
public static final int G_GSBMX = 0x0D; // Set Background Mix
public static final int G_GSFLW = 0x11; // Set Fractional Line Width
public static final int G_GSLT = 0x18; // Set Line Type
public static final int G_GSLW = 0x19; // Set Line Width
public static final int G_GSMS = 0x1B; // Set Marker Size
public static final int G_GSCP = 0x21; // Set Current Position
public static final int G_GSAP = 0x22; // Arc Parameters
public static final int G_GSECOL = 0x26; // Set Extended Color
public static final int G_GSVW = 0x27; // Set Viewing Window
public static final int G_GSPT = 0x28; // Set Pattern Symbol
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
public static final int G_GCALL = 0x2A; // Call Segment
public static final int G_GSCH = 0x33; // Set Character Cell
public static final int G_GSCA = 0x34; // Set Character Angle
public static final int G_GSCR = 0x35; // Set Character Shear
public static final int G_GSMCEL = 0x37; // Set Marker Cell
public static final int G_GSCS = 0x38; // Set Character Set
public static final int G_GSMP = 0x39; // Set Marker Precision
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
public static final int G_GSCD = 0x3A; // Set Character Direction
public static final int G_GSCC = 0x3B; // Set Character Precision
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
public static final int G_GPOP = 0x3F; // Pop Attribute
public static final int G_GEAR = 0x60; // End Area
public static final int G_GBAR = 0x68; // Begin Area
public static final int G_BEGSEGM = 0x70; // Begin Segment
public static final int G_ENDSEGM = 0x71; // End Segment
public static final int G_GERASE = 0x7E; // Erase Graphics Plane
public static final int G_GCLINE = 0x81; // Line at Current Position
public static final int G_GCMRK = 0x82; // Marker at Current Position
public static final int G_GCCHST = 0x83; // Character String at Current Position
public static final int G_GCFLT = 0x85; // Fillet at Current Position
public static final int G_GCARC = 0x86; // Partial Arc at Current Position
public static final int G_GCFARC = 0x87; // Full Arc at Current Position
public static final int G_GEIMG = 0x91; // End Image
public static final int G_GIMD = 0x92; // Image Data
public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position
public static final int G_GLINE = 0xC1; // Line (Absolute)
public static final int G_GMRK = 0xC2; // Marker (Absolute)
public static final int G_GCHST = 0xC3; // Character String (Absolute)
public static final int G_GFLT = 0xC5; // Fillet (Absolute)
public static final int G_GARC = 0xC6; // Partial Arc (Absolute)
public static final int G_GFARC = 0xC7; // Full Arc (Absolute)
public static final int G_GBIMG = 0xD1; // Begin Image
public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start)
// Line Types (GSLT)
public static final int LT_DEFAULT = 0;
public static final int LT_DOT = 1;
public static final int LT_SHORTDASH= 2;
public static final int LT_DASHDOT = 3;
public static final int LT_DOUBLEDOT= 4;
public static final int LT_LONGDASH = 5;
public static final int LT_DASHDOUBLEDOT = 6;
public static final int LT_SOLID = 7;
// Line Widths (GSLW)
public static final int LW_DEFAULT = 0;
public static final int LW_NORMAL = 1;
public static final int LW_THICK = 2;
// Character Precision (G_GSCC / 0x3B)
public static final int CP_DEFAULT = 0;
public static final int CP_STRING = 1;
public static final int CP_CHAR = 2;
public static final int CP_STROKE = 3;
// Fill Patterns (GSPT)
public static final int PT_DEFAULT = 0;
public static final int PT_D1 = 1;
public static final int PT_D2 = 2;
public static final int PT_D3 = 3;
public static final int PT_D4 = 4;
public static final int PT_D5 = 5;
public static final int PT_D6 = 6;
public static final int PT_D7 = 7;
public static final int PT_D8 = 8;
public static final int PT_VERT_LINE = 9;
public static final int PT_HORIZ_LINE = 10;
public static final int PT_DIAG_BLTR = 11;
public static final int PT_DIAG_BLTR2 = 12;
public static final int PT_DIAG_TLBR = 13;
public static final int PT_DIAG_TLBR2 = 14;
public static final int PT_EMPTY = 15;
public static final int PT_SOLID = 16;
// Marker Symbols (GSMT)
public static final int MK_DEFAULT = 0;
public static final int MK_CROSS = 1; // x
public static final int MK_PLUS = 2; // +
public static final int MK_DIAMOND = 3; // <>
public static final int MK_SQUARE = 4; // []
public static final int MK_6STAR = 5; // * 6-point
public static final int MK_8STAR = 6; // * 8-point
public static final int MK_SDIAMOND = 7; // solid diamond
public static final int MK_SSQUARE = 8; // solid square
public static final int MK_DOT = 9; // .
public static final int MK_CIRCLE = 10;// o
// Character Direction (GSCD)
public static final int CD_DEFAULT = 0;
public static final int CD_LR = 1; // Left to Right
public static final int CD_TB = 2; // Top to Bottom
public static final int CD_RL = 3; // Right to Left
public static final int CD_BT = 4; // Bottom to Top
// Foreground / Background Mix Modes (GSMX / GSBMX)
public static final int MIX_DEFAULT = 0;
public static final int MIX_OR = 1;
public static final int MIX_OVER = 2;
public static final int MIX_LEAVE = 3;
public static final int MIX_XOR = 4;
public static final int MIX_UNDER = 5;
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
public static final int[] GOCA_COLORS = new int[] {
0xFF00FF00, // 0: Default (Green)
0xFF7890F0, // 1: Blue (120, 144, 240)
0xFFFF0000, // 2: Red (255, 0, 0)
0xFFFF00FF, // 3: Pink / Magenta (255, 0, 255)
0xFF00FF00, // 4: Green (0, 255, 0)
0xFF00FFFF, // 5: Turquoise / Cyan (0, 255, 255)
0xFFFFFF00, // 6: Yellow (255, 255, 0)
0xFFFFFFFF, // 7: Neutral White (255, 255, 255)
0xFF000000, // 8: Black (0, 0, 0)
0xFF000080, // 9: Deep Blue (0, 0, 128)
0xFF800000, // 10: Orange / Dark Red (128, 0, 0)
0xFF800080, // 11: Purple (128, 0, 128)
0xFF008000, // 12: Pale Green (0, 128, 0)
0xFF008080, // 13: Pale Cyan (0, 128, 128)
0xFFD79700, // 14: Mustard (215, 151, 0)
0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192)
0xFF492400 // 16: Brown (73, 36, 0)
};
/**
* Retrieves the 32-bit ARGB color value for a given GOCA color index (0-16).
* Returns Green (0xFF00FF00) if the index is out of range.
*/
public static int getGocaColorArgb(int colorIndex) {
if (colorIndex == 0xFF) {
return GOCA_COLORS[7];
}
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
return GOCA_COLORS[colorIndex];
}
return GOCA_COLORS[0];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
package haus.nightmare.lib3270j.graphics;
/**
* Builds the 56-byte IBM 3179G / 3270G Graphic Input Structured Field.
* Used for light-pen and graphics cursor interactive input per IBM HOD / GDDM specifications.
*/
public class GraphicInputBuilder {
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
private static final byte[] MASK = new byte[] {
0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00
};
/**
* Builds the 56-byte Graphic Input Structured Field.
*
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
* @param isShift true if shift key was down
* @param isCtrl true if ctrl key was down
* @return 56-byte payload
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl) {
return buildGraphicInput(gocaX, gocaY, aidCode, isMouseAction, isShift, isCtrl, 0, 0);
}
/**
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
*
* IMPORTANT ARCHITECTURE NOTE:
* Per IBM GA23-0059 / GDDM specifications:
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
* the exact segment ID in bytes 28-31. If hardcoded or mismatched, GDDM rejects the click
* with a WCC 0xF7 alarm beep.
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
*
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
* @param isShift true if shift key was down
* @param isCtrl true if ctrl key was down
* @param pickedSegId Picked GOCA segment ID (0 if none)
* @param pickTag Pick correlation tag
* @return 56-byte payload
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) {
byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length);
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
sf[25] = (byte) (gocaX & 0xFF);
// Byte 26-27: GOCA Y coordinate (signed 16-bit big-endian)
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
sf[27] = (byte) (gocaY & 0xFF);
if (pickedSegId != 0) {
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
sf[31] = (byte) (pickedSegId & 0xFF);
}
if (isMouseAction) {
if (pickTag != 0) {
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
sf[33] = (byte) (pickTag & 0xFF);
}
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
} else {
// Keyboard AID (Enter, PF keys)
sf[31] = 0x07;
sf[33] = 0x07;
sf[34] = (byte) 0xFF;
sf[35] = (byte) (aidCode & 0xFF);
}
return sf;
}
}
@@ -0,0 +1,70 @@
package haus.nightmare.lib3270j.graphics;
/**
* Graphics modes supported by j3270 / lib3270j.
*/
public enum GraphicsMode {
/** Text only (no graphics query replies, classic 3279-4 terminal behavior). */
NONE("None (Text Only)"),
/** Programmed Symbols only (custom character matrices and APL, single & triple plane). */
PROGRAMMED_SYMBOLS("Programmed Symbols Only"),
/** Vector graphics only (GOCA / 3179G drawing orders). */
VECTOR_GRAPHICS("Vector Graphics Only"),
/** Full graphics support (both Programmed Symbols and Vector Graphics). */
BOTH("Both (Programmed Symbols & Vector Graphics)");
private final String description;
GraphicsMode(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public boolean isProgrammedSymbolsEnabled() {
return this == PROGRAMMED_SYMBOLS || this == BOTH;
}
public boolean isVectorGraphicsEnabled() {
return this == VECTOR_GRAPHICS || this == BOTH;
}
public static GraphicsMode fromString(String str) {
if (str == null || str.trim().isEmpty()) {
return NONE;
}
String s = str.trim().toUpperCase();
switch (s) {
case "BOTH":
case "ALL":
case "FULL":
case "ON":
case "TRUE":
return BOTH;
case "PS":
case "PROGRAMMED_SYMBOLS":
case "PROGRAMMEDSYMBOLS":
case "SYMBOLS":
case "APL":
return PROGRAMMED_SYMBOLS;
case "VECTOR":
case "VECTOR_GRAPHICS":
case "VECTORGRAPHICS":
case "GOCA":
case "GDDM":
return VECTOR_GRAPHICS;
case "NONE":
case "OFF":
case "FALSE":
case "DISABLED":
case "TEXT":
default:
return NONE;
}
}
}
@@ -0,0 +1,843 @@
package haus.nightmare.lib3270j.graphics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
/**
* Offscreen rendering surface for GOCA vector graphics.
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap).
*/
public class GraphicsPlane {
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
// 17 Standard GOCA 8x8 Fill Patterns
public static final byte[][] PATTERN_DATA = new byte[][]{
{-1, -1, -1, -1, -1, -1, -1, -1}, // 0: Solid (all 1s)
{-1, -1, -1, -18, -1, -1, -1, -18}, // 1: D1
{-1, -69, -1, -18, -1, -69, -1, -18}, // 2: D2
{119, -35, -69, -18, 119, -35, -69, -18}, // 3: D3
{-69, -52, 51, -18, -69, -52, 51, -18}, // 4: D4
{85, -86, 85, -86, 85, -86, 85, -86}, // 5: D5 (50% checker)
{68, 51, -52, 17, 68, 51, -52, 17}, // 6: D6
{-120, 34, 68, 17, -120, 34, 68, 17}, // 7: D7
{0, 68, 0, 17, 0, 68, 0, 17}, // 8: D8 (sparse dots)
{-128, -128, -128, -128, -128, -128, -128, -128}, // 9: Vertical line
{-1, 0, 0, 0, 0, 0, 0, 0}, // 10: Horizontal line
{1, 2, 4, 8, 16, 32, 64, -128}, // 11: Diagonal bottom-left to top-right
{3, 12, 48, -64, 3, 12, 48, -64}, // 12: Diagonal BL-TR dense
{-128, 64, 32, 16, 8, 4, 2, 1}, // 13: Diagonal top-left to bottom-right
{-64, 48, 12, 3, -64, 48, 12, 3}, // 14: Diagonal TL-BR dense
{0, 0, 0, 0, 0, 0, 0, 0}, // 15: Empty (transparent)
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
};
private int canvasWidth = 800;
private int canvasHeight = 600;
private int[] rgbBuffer;
private boolean hasContent = false;
private long updateCount = 0;
private int screenCols = 80;
private int screenRows = 24;
public GraphicsPlane(int width, int height) {
this.canvasWidth = Math.max(1, width);
this.canvasHeight = Math.max(1, height);
this.rgbBuffer = new int[canvasWidth * canvasHeight];
}
public synchronized void resize(int width, int height) {
int w = Math.max(1, width);
int h = Math.max(1, height);
if (w == canvasWidth && h == canvasHeight && rgbBuffer != null) {
return;
}
int[] newBuffer = new int[w * h];
if (rgbBuffer != null && hasContent) {
// Scale existing content to new dimensions using nearest-neighbor
for (int dy = 0; dy < h; dy++) {
int sy = (dy * canvasHeight) / h;
for (int dx = 0; dx < w; dx++) {
int sx = (dx * canvasWidth) / w;
newBuffer[dy * w + dx] = rgbBuffer[sy * canvasWidth + sx];
}
}
}
this.canvasWidth = w;
this.canvasHeight = h;
this.rgbBuffer = newBuffer;
}
public synchronized void clear() {
if (rgbBuffer != null) {
Arrays.fill(rgbBuffer, 0);
}
hasContent = false;
updateCount++;
}
public synchronized long getUpdateCount() {
return updateCount;
}
public synchronized boolean hasContent() {
return hasContent;
}
public synchronized int[] getRgbBuffer() {
return rgbBuffer;
}
public int getCanvasWidth() {
return canvasWidth;
}
public int getCanvasHeight() {
return canvasHeight;
}
public synchronized void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24;
int targetW = this.screenCols * 9;
int targetH = this.screenRows * 12;
if (this.canvasWidth != targetW || this.canvasHeight != targetH) {
resize(targetW, targetH);
}
}
public int getScreenCols() {
return screenCols;
}
public int getScreenRows() {
return screenRows;
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
*/
public double mapXDouble(double gocaX) {
int nominalWidth = screenCols * 9;
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
double nx = gocaX + xMax;
return (nx * canvasWidth) / (double) (nominalWidth > 0 ? nominalWidth : 1);
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
*/
public double mapYDouble(double gocaY) {
int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2;
double ny = yMax - gocaY;
return (ny * canvasHeight) / (double) (nominalHeight > 0 ? nominalHeight : 1);
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
*/
public int mapX(int gocaX) {
return (int) Math.round(mapXDouble((double) gocaX));
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
*/
public int mapY(int gocaY) {
return (int) Math.round(mapYDouble((double) gocaY));
}
/**
* Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax).
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
*/
public int unmapX(int px) {
int nominalWidth = screenCols * 9;
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1));
return nx - xMax;
}
/**
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up).
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
*/
public int unmapY(int py) {
int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2;
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1));
return yMax - ny;
}
/**
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/
public synchronized void setPixel(int x, int y, int colorArgb) {
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return;
int idx = y * canvasWidth + x;
if (srcA == 255) {
rgbBuffer[idx] = colorArgb;
} else {
int dst = rgbBuffer[idx];
int dstA = (dst >>> 24) & 0xFF;
if (dstA == 0) {
rgbBuffer[idx] = colorArgb;
} else {
int srcR = (colorArgb >>> 16) & 0xFF;
int srcG = (colorArgb >>> 8) & 0xFF;
int srcB = colorArgb & 0xFF;
int dstR = (dst >>> 16) & 0xFF;
int dstG = (dst >>> 8) & 0xFF;
int dstB = dst & 0xFF;
int invSrcA = 255 - srcA;
int outA = srcA + (dstA * invSrcA + 127) / 255;
int outR = (srcR * srcA + dstR * invSrcA + 127) / 255;
int outG = (srcG * srcA + dstG * invSrcA + 127) / 255;
int outB = (srcB * srcA + dstB * invSrcA + 127) / 255;
rgbBuffer[idx] = ((outA & 0xFF) << 24) | ((outR & 0xFF) << 16) | ((outG & 0xFF) << 8) | (outB & 0xFF);
}
}
hasContent = true;
updateCount++;
}
}
/**
* Plots a pixel with fractional alpha coverage (0.0 to 1.0) for anti-aliasing.
*/
public synchronized void setPixelCoverage(int x, int y, int colorRgb, double coverage) {
if (coverage <= 0.0) return;
int alpha = (int) Math.round(coverage * 255.0);
if (alpha > 255) alpha = 255;
if (alpha <= 0) return;
setPixel(x, y, (alpha << 24) | (colorRgb & 0x00FFFFFF));
}
/**
* Draws an anti-aliased line using Xiaolin Wu's algorithm with sub-pixel double coordinates.
*/
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : 0x00FFFFFF;
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
drawStyledLine((int) Math.round(x0), (int) Math.round(y0),
(int) Math.round(x1), (int) Math.round(y1),
(0xFF << 24) | color, lineType, lineWidth);
return;
}
// Special case: single point or zero-length line
if (Math.abs(x1 - x0) < 1e-5 && Math.abs(y1 - y0) < 1e-5) {
drawPixelWithThickness((int) Math.round(x0), (int) Math.round(y0), (0xFF << 24) | color, (lineWidth == GocaConstants.LW_THICK) ? 2 : 1);
return;
}
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
if (steep) {
double tmp = x0; x0 = y0; y0 = tmp;
tmp = x1; x1 = y1; y1 = tmp;
}
if (x0 > x1) {
double tmp = x0; x0 = x1; x1 = tmp;
tmp = y0; y0 = y1; y1 = tmp;
}
double dx = x1 - x0;
double dy = y1 - y0;
double gradient = (dx == 0.0) ? 1.0 : (dy / dx);
// First endpoint
double xend = Math.round(x0);
double yend = y0 + gradient * (xend - x0);
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
int xpxl1 = (int) xend;
int ypxl1 = (int) Math.floor(yend);
if (steep) {
plotPixelWu(ypxl1, xpxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(ypxl1 + 1, xpxl1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
} else {
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(xpxl1, ypxl1 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
}
double intery = yend + gradient;
// Second endpoint
xend = Math.round(x1);
yend = y1 + gradient * (xend - x1);
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
int xpxl2 = (int) xend;
int ypxl2 = (int) Math.floor(yend);
if (steep) {
plotPixelWu(ypxl2, xpxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(ypxl2 + 1, xpxl2, color, (yend - Math.floor(yend)) * xgap, lineWidth);
} else {
plotPixelWu(xpxl2, ypxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(xpxl2, ypxl2 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
}
// Main anti-aliased stepping loop
if (steep) {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int y = (int) Math.floor(intery);
double frac = intery - y;
plotPixelWu(y, x, color, 1.0 - frac, lineWidth);
plotPixelWu(y + 1, x, color, frac, lineWidth);
intery += gradient;
}
} else {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int y = (int) Math.floor(intery);
double frac = intery - y;
plotPixelWu(x, y, color, 1.0 - frac, lineWidth);
plotPixelWu(x, y + 1, color, frac, lineWidth);
intery += gradient;
}
}
hasContent = true;
updateCount++;
}
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return;
if (lineWidth == GocaConstants.LW_THICK) {
setPixelCoverage(x, y, colorRgb, 1.0);
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
} else {
// Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
setPixelCoverage(x, y, colorRgb, b);
}
}
/**
* Draws an absolute or relative line using anti-aliasing for smooth vectors.
*/
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
}
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
int dx = Math.abs(x2 - x1);
int dy = Math.abs(y2 - y1);
int sx = x1 < x2 ? 1 : -1;
int sy = y1 < y2 ? 1 : -1;
int err = dx - dy;
int curX = x1;
int curY = y1;
int stepIndex = 0;
while (true) {
if (shouldPlotLinePixel(stepIndex, lineType)) {
drawPixelWithThickness(curX, curY, color, thickness);
}
stepIndex++;
if (curX == x2 && curY == y2) {
break;
}
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
curX += sx;
}
if (e2 < dx) {
err += dx;
curY += sy;
}
}
hasContent = true;
updateCount++;
}
private boolean shouldPlotLinePixel(int step, int lineType) {
switch (lineType) {
case GocaConstants.LT_DOT:
return (step % 4) < 2;
case GocaConstants.LT_SHORTDASH:
return (step % 6) < 4;
case GocaConstants.LT_DASHDOT:
int m12 = step % 12;
return m12 < 6 || (m12 >= 8 && m12 < 10);
case GocaConstants.LT_DOUBLEDOT:
int m10 = step % 10;
return m10 < 2 || (m10 >= 4 && m10 < 6);
case GocaConstants.LT_LONGDASH:
return (step % 11) < 8;
case GocaConstants.LT_DASHDOUBLEDOT:
int m18 = step % 18;
return m18 < 8 || (m18 >= 10 && m18 < 12) || (m18 >= 14 && m18 < 16);
case GocaConstants.LT_SOLID:
case GocaConstants.LT_DEFAULT:
default:
return true;
}
}
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
if (thickness <= 1) {
setPixel(x, y, color);
} else {
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
setPixel(x + dx, y + dy, color);
}
}
}
}
/**
* Draws a full or partial arc / ellipse with sub-pixel double precision and anti-aliasing.
*/
public synchronized void drawArc(double cx, double cy, double rx, double ry, double startAngleDeg, double sweepAngleDeg,
int colorArgb, int lineType, int lineWidth, boolean isFull) {
if (rx <= 0) rx = 1.0;
if (ry <= 0) ry = 1.0;
int numSteps = (int) Math.max(36, Math.max(rx, ry) * 6);
double startRad = Math.toRadians(startAngleDeg);
double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
double stepRad = sweepRad / numSteps;
double prevX = cx + rx * Math.cos(startRad);
double prevY = cy - ry * Math.sin(startRad);
for (int i = 1; i <= numSteps; i++) {
double angle = startRad + i * stepRad;
double nextX = cx + rx * Math.cos(angle);
double nextY = cy - ry * Math.sin(angle);
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
prevX = nextX;
prevY = nextY;
}
hasContent = true;
updateCount++;
}
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
int colorArgb, int lineType, int lineWidth, boolean isFull) {
drawArc((double) cx, (double) cy, (double) rx, (double) ry, startAngleDeg, sweepAngleDeg, colorArgb, lineType, lineWidth, isFull);
}
/**
* Draws a Fillet (spline / curve approximation across control points) with sub-pixel precision.
*/
public synchronized void drawFillet(double[] px, double[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
if (px == null || py == null || numPoints < 2) return;
if (numPoints == 2) {
drawLine(px[0], py[0], px[1], py[1], colorArgb, lineType, lineWidth);
return;
}
double prevX = px[0];
double prevY = py[0];
for (int i = 0; i < numPoints - 1; i++) {
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
double p1x = px[i];
double p1y = py[i];
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
int steps = 30;
for (int s = 1; s <= steps; s++) {
double t = (double) s / steps;
double oneMinusT = 1.0 - t;
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
drawLine(prevX, prevY, bx, by, colorArgb, lineType, lineWidth);
prevX = bx;
prevY = by;
}
}
hasContent = true;
updateCount++;
}
public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
if (px == null || py == null || numPoints < 2) return;
double[] dpx = new double[numPoints];
double[] dpy = new double[numPoints];
for (int i = 0; i < numPoints; i++) {
dpx[i] = px[i];
dpy[i] = py[i];
}
drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth);
}
/**
* Fills a closed polygon area with a solid color or hatching pattern.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) {
fillArea(px, py, numPoints, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, 2, 0xFF000000);
}
/**
* Fills a closed polygon area with a solid color or hatching pattern and background mix.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, null, 1, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
}
/**
* Fills an area containing one or more closed polygon subpaths with a solid color or hatching pattern,
* using the even-odd fill rule across all subpath contours.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
int bg = bgColorArgb;
if (pattern != GocaConstants.PT_EMPTY) {
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
for (int i = 1; i < numPoints; i++) {
if (py[i] < minY) minY = py[i];
if (py[i] > maxY) maxY = py[i];
}
minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>();
byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0];
for (int y = minY; y <= maxY; y++) {
nodeX.clear();
int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) {
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
if (pLen >= 3) {
int j = pLen - 1;
for (int i = 0; i < pLen; i++) {
int yi = py[offset + i];
int yj = py[offset + j];
int xi = px[offset + i];
int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
nodeX.add(x);
}
j = i;
}
}
offset += pLen;
}
Collections.sort(nodeX);
for (int i = 0; i < nodeX.size(); i += 2) {
if (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, nodeX.get(i));
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) {
if (pattern == GocaConstants.PT_SOLID || pattern == 0 || pattern >= 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix != 0) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg);
}
}
}
}
}
}
if (drawBoundary && boundaryColorArgb != 0) {
int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) {
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
if (pLen >= 2) {
for (int i = 0; i < pLen - 1; i++) {
drawLine((double) px[offset + i], (double) py[offset + i],
(double) px[offset + i + 1], (double) py[offset + i + 1],
boundaryColorArgb, lineType, lineWidth);
}
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
(double) px[offset], (double) py[offset],
boundaryColorArgb, lineType, lineWidth);
}
}
offset += pLen;
}
}
hasContent = true;
updateCount++;
}
/**
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision.
*/
public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
double s = Math.max(3.0, size > 0 ? (double) size : 5.0);
switch (markerType) {
case GocaConstants.MK_CROSS: // x
drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_PLUS: // +
case GocaConstants.MK_DEFAULT:
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_DIAMOND: // <>
drawLine(x, y - s, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x + s, y, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x, y + s, x - s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y, x, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_SQUARE: // []
drawLine(x - s, y - s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x + s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x + s, y + s, x - s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y + s, x - s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_6STAR: // 6-point star
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s / 2.0, y - s, x + s / 2.0, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s / 2.0, y + s, x + s / 2.0, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_8STAR: // 8-point star
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_SDIAMOND: // solid diamond
fillArea(new int[]{(int) Math.round(x), (int) Math.round(x + s), (int) Math.round(x), (int) Math.round(x - s)},
new int[]{(int) Math.round(y - s), (int) Math.round(y), (int) Math.round(y + s), (int) Math.round(y)},
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
break;
case GocaConstants.MK_SSQUARE: // solid square
fillArea(new int[]{(int) Math.round(x - s), (int) Math.round(x + s), (int) Math.round(x + s), (int) Math.round(x - s)},
new int[]{(int) Math.round(y - s), (int) Math.round(y - s), (int) Math.round(y + s), (int) Math.round(y + s)},
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
break;
case GocaConstants.MK_DOT: // dot
drawArc(x, y, 2.0, 2.0, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
break;
case GocaConstants.MK_CIRCLE: // circle
default:
drawArc(x, y, s, s, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
break;
}
hasContent = true;
updateCount++;
}
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
drawMarker((double) x, (double) y, markerType, size, colorArgb);
}
private static final int[] VSS_OFFSETS = new int[256];
static {
Arrays.fill(VSS_OFFSETS, -1);
int sym = VectorSymbolData.VSS_SYMBOL_START; // 33
if (sym < 256) {
VSS_OFFSETS[sym] = 0;
}
for (int i = 0; i < VectorSymbolData.vss_data.length; i++) {
if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF
sym++;
if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) {
VSS_OFFSETS[sym] = i + 1;
}
}
}
}
@FunctionalInterface
public interface TextRenderer {
void drawText(GraphicsPlane plane, double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle);
}
private TextRenderer textRenderer;
public void setTextRenderer(TextRenderer renderer) {
this.textRenderer = renderer;
}
public TextRenderer getTextRenderer() {
return this.textRenderer;
}
/**
* Draws character text (using pluggable TextRenderer or fallback vector font).
*/
public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
if (text == null || text.isEmpty()) return;
if (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true;
updateCount++;
} else {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
}
}
public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
}
/**
* Draws stroked vector text using IBM Vector Symbol Set (VSS) with sub-pixel anti-aliasing.
*/
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
double curX = x;
double curY = y;
double cw = cellWidth > 0 ? cellWidth : 12.0;
double ch = cellHeight > 0 ? cellHeight : 20.0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch);
switch (dir) {
case GocaConstants.CD_TB: curY += ch; break;
case GocaConstants.CD_RL: curX -= cw; break;
case GocaConstants.CD_BT: curY -= ch; break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
}
hasContent = true;
updateCount++;
}
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
int code = (int) c;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return;
}
int offset = VSS_OFFSETS[code];
if (offset < 0) {
return;
}
int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
if (order == 0xC1) {
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
int numPoints = byteLen / 4;
int dataPtr = ptr + 2;
if (numPoints >= 2) {
double[] px = new double[numPoints];
double[] py = new double[numPoints];
int[] ipx = new int[numPoints];
int[] ipy = new int[numPoints];
for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch;
ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[p]);
}
// If contour is closed (e.g. bold character loop), fill with solid color
int firstVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
int firstVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
int lastVx = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 1] & 0xFF);
int lastVy = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 3] & 0xFF);
boolean isClosed = (numPoints >= 4) && (firstVx == lastVx) && (firstVy == lastVy);
if (isClosed) {
fillArea(ipx, ipy, numPoints, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
}
for (int p = 0; p < numPoints - 1; p++) {
drawLine(px[p], py[p], px[p + 1], py[p + 1], color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
}
}
ptr += 2 + byteLen;
} else {
ptr++;
}
}
}
/**
* Draws raw image pixel bitmap.
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / 8);
if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
}
}
}
}
hasContent = true;
updateCount++;
}
}
@@ -0,0 +1,303 @@
package haus.nightmare.lib3270j.graphics;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Manages IBM 3270 Programmed Symbols (PS / APL) character sets.
* Handles the Load Programmed Symbols (LOADPS structured field 0x0F).
*/
public class ProgramSymbolManager {
private static final Logger logger = Logger.getLogger(ProgramSymbolManager.class.getName());
public static final int NUMBER_SYMBOL_SETS = 10;
public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; // RWS 2..3 (Sets 0..1)
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);
}
}
/**
* Resets all symbol sets.
*/
public synchronized void clearAll() {
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 ProgramSymbolSet getSymbolSet(int lcid) {
if (lcid <= 0 || lcid >= 256) {
return null;
}
ProgramSymbolSet set = lcidMap[lcid];
if (set == null) {
for (ProgramSymbolSet s : sets) {
if (s != null && s.getLcid() == lcid) return s;
}
for (ProgramSymbolSet s : stagingSets) {
if (s != null && s.getLcid() == lcid) return s;
}
}
return set;
}
/**
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
*/
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
ProgramSymbolSet set = getSymbolSet(lcid);
if (set == null) {
return null;
}
int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint;
return set.getSlot(index);
}
/**
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
*/
public synchronized void loadps(byte[] data) {
if (data == null || data.length < 4) {
logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)");
return;
}
int flags = data[0] & 0xFF;
int loadFormat = flags & 0x1F;
boolean clearAll = (flags & 0x40) != 0;
boolean hasExtHeader = (flags & 0x80) != 0;
int lcid = data[1] & 0xFF;
int startCodePoint = data[2] & 0xFF;
int rws = data[3] & 0xFF;
int setIndex;
switch (rws) {
case 2: setIndex = 0; break;
case 3: setIndex = 1; break;
case 4: setIndex = 2; break;
case 5: setIndex = 3; break;
case 6: setIndex = 4; break;
case 7: setIndex = 5; break;
case 8: setIndex = 6; break;
case 9: setIndex = 7; break;
case 10: setIndex = 8; break;
case 11: setIndex = 9; break;
default:
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
return;
}
boolean isTriplePlane = (rws >= 4 && rws <= 7);
ProgramSymbolSet set = sets[setIndex];
int extHeaderLen = 0;
int cellWidth = defaultCellWidth;
int cellHeight = defaultCellHeight;
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
if (hasExtHeader && data.length > 4) {
extHeaderLen = data[4] & 0xFF;
if (extHeaderLen > 3 && data.length > 6) {
int lw = data[6] & 0xFF;
if (lw > 0) cellWidth = lw;
}
if (extHeaderLen > 4 && data.length > 7) {
int lh = data[7] & 0xFF;
if (lh > 0) cellHeight = lh;
}
if (extHeaderLen >= 6 && data.length > 9) {
colorPlane = data[9] & 0xFF;
}
}
set.setLcid(lcid);
if (lcid > 0 && lcid < 256) {
lcidMap[lcid] = set;
}
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
int remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int bytesPerSymbol;
if (loadFormat == 1) {
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
} else {
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
}
if (bytesPerSymbol <= 0) {
bytesPerSymbol = 18;
}
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
byte[] pixelData = new byte[cellWidth * cellHeight];
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
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) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
codeIndex++;
offset += bytesPerSymbol;
remaining -= bytesPerSymbol;
}
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.
*/
private void unpackFormat1(byte[] src, int srcOff, byte[] dst, int width, int height,
boolean isTriplePlane, int colorPlane) {
// Format 1 transmits 18 bytes:
// Byte 0-1: contains column 0 for each of the 16 rows
// Bytes 2-17: contains columns 1-8 for each of the 16 rows
int planeMask = (colorPlane != 0) ? colorPlane : (isTriplePlane ? 7 : 1);
for (int row = 0; row < 16 && row < height; row++) {
// Column 0 bit from byte 0 or byte 1
int b0 = (row < 8) ? (src[srcOff] & 0xFF) : (src[srcOff + 1] & 0xFF);
int bitShift0 = 7 - (row % 8);
boolean bit0 = ((b0 >> bitShift0) & 1) != 0;
int dstIdx0 = row * width;
if (dstIdx0 < dst.length) {
if (!isTriplePlane) {
dst[dstIdx0] = bit0 ? (byte) 1 : 0;
} else if (colorPlane == 0) {
dst[dstIdx0] = bit0 ? (byte) 7 : 0;
} else {
if (bit0) {
dst[dstIdx0] |= (byte) colorPlane;
} else {
dst[dstIdx0] &= (byte) ~colorPlane;
}
}
}
// Columns 1..8 from bytes 2..17
if (srcOff + 2 + row < src.length) {
int rowByte = src[srcOff + 2 + row] & 0xFF;
for (int col = 1; col < 9 && col < width; col++) {
int dstIdx = row * width + col;
if (dstIdx < dst.length) {
boolean bit = ((rowByte >> (8 - col)) & 1) != 0;
if (!isTriplePlane) {
dst[dstIdx] = bit ? (byte) 1 : 0;
} else if (colorPlane == 0) {
dst[dstIdx] = bit ? (byte) 7 : 0;
} else {
if (bit) {
dst[dstIdx] |= (byte) colorPlane;
} else {
dst[dstIdx] &= (byte) ~colorPlane;
}
}
}
}
}
}
}
/**
* Unpacks Format 3 variable dimension bit-pattern.
*/
private void unpackFormat3(byte[] src, int srcOff, byte[] dst, int width, int height,
boolean isTriplePlane, int colorPlane) {
int totalPixels = width * height;
for (int i = 0; i < totalPixels; i++) {
int byteIndex = srcOff + (i / 8);
if (byteIndex >= src.length) break;
int bitIndex = 7 - (i % 8);
boolean bit = ((src[byteIndex] >> bitIndex) & 1) != 0;
if (!isTriplePlane) {
dst[i] = bit ? (byte) 1 : 0;
} else if (colorPlane == 0) {
dst[i] = bit ? (byte) 7 : 0;
} else {
if (bit) {
dst[i] |= (byte) colorPlane;
} else {
dst[i] &= (byte) ~colorPlane;
}
}
}
}
}
@@ -0,0 +1,189 @@
package haus.nightmare.lib3270j.graphics;
/**
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
*/
public class ProgramSymbolSet {
public static final int NUM_SLOTS = 191; // Code points 0x40 - 0xFE (0..190)
private int lcid = 0;
private final boolean isTriplePlane;
private final SymbolSlot[] slots = new SymbolSlot[NUM_SLOTS];
public ProgramSymbolSet(boolean isTriplePlane) {
this.isTriplePlane = isTriplePlane;
}
public int getLcid() {
return lcid;
}
public void setLcid(int lcid) {
this.lcid = lcid;
}
public boolean isTriplePlane() {
return isTriplePlane;
}
public void clear() {
for (int i = 0; i < NUM_SLOTS; i++) {
slots[i] = null;
}
}
public void clearSlot(int index) {
if (index >= 0 && index < NUM_SLOTS) {
slots[index] = null;
}
}
public void setSlot(int index, SymbolSlot slot) {
if (index >= 0 && index < NUM_SLOTS) {
slots[index] = slot;
}
}
public SymbolSlot getSlot(int index) {
if (index >= 0 && index < NUM_SLOTS) {
return slots[index];
}
return null;
}
/**
* Represents a single custom symbol bitmap.
*/
public static class SymbolSlot {
private final int width;
private final int height;
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
private final boolean isTriplePlane;
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;
this.height = height > 0 ? height : 16;
this.pixelData = pixelData;
this.isTriplePlane = isTriplePlane;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public byte[] getPixelData() {
return pixelData;
}
public boolean isTriplePlane() {
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).
*/
public synchronized int[] getRgbPixels(int fgArgb, int bgArgb) {
if (cachedRgbArray != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
return cachedRgbArray;
}
int[] rgbArray = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = y * width + x;
int val = (pixelData != null && idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
if (val == 0) {
rgbArray[idx] = bgArgb;
} else if (!isTriplePlane) {
rgbArray[idx] = fgArgb;
} else {
// Triple-Plane RGB composite:
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
int r = (val & 0x01) != 0 ? 255 : 0;
int g = (val & 0x02) != 0 ? 255 : 0;
int b = (val & 0x04) != 0 ? 255 : 0;
rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
}
}
}
this.cachedRgbArray = rgbArray;
this.cachedFgRgb = fgArgb;
this.cachedBgRgb = bgArgb;
return rgbArray;
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,776 @@
package haus.nightmare.lib3270j.input;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
import java.util.logging.Logger;
/**
* Handles keyboard input and generates 3270 outbound data streams.
* Manages cursor positioning, character entry, and AID key handling.
*/
public class InputProcessor {
private static final Logger log = Logger.getLogger(InputProcessor.class.getName());
private final ScreenBuffer screen;
private final EbcdicTranslator translator;
private final TelnetFSM fsm;
private int lastAid = AID_NO;
private boolean keyboardLocked;
private boolean insertMode;
public InputProcessor(ScreenBuffer screen, EbcdicTranslator translator, TelnetFSM fsm) {
this.screen = screen;
this.translator = translator;
this.fsm = fsm;
}
private haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane;
private haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder;
public void setGraphicsPlane(haus.nightmare.lib3270j.graphics.GraphicsPlane gp) {
this.graphicsPlane = gp;
}
public void setGocaDecoder(haus.nightmare.lib3270j.graphics.GocaDecoder gd) {
this.gocaDecoder = gd;
}
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return gocaDecoder;
}
public enum OiaStatus {
NOT_CONNECTED("OFFLINE"),
X_SYSTEM("X SYSTEM"),
X_PROT("X PROT"),
READY("READY");
private final String label;
OiaStatus(String label) { this.label = label; }
public String getLabel() { return label; }
}
public OiaStatus getOiaStatus() {
if (fsm == null || fsm.getConnectionState() == null || !fsm.getConnectionState().isConnected()) {
return OiaStatus.NOT_CONNECTED;
}
if (keyboardLocked) {
return OiaStatus.X_SYSTEM;
}
return OiaStatus.READY;
}
public interface LockStateListener {
void onLockStateChanged(boolean locked);
}
private LockStateListener lockStateListener;
public void setLockStateListener(LockStateListener listener) {
this.lockStateListener = listener;
}
public boolean isKeyboardLocked() { return keyboardLocked; }
public void setKeyboardLocked(boolean locked) {
if (this.keyboardLocked != locked) {
this.keyboardLocked = locked;
if (lockStateListener != null) {
lockStateListener.onLockStateChanged(locked);
}
}
}
public boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insert) { this.insertMode = insert; }
/**
* Enter a character at the current cursor position.
*/
public void typeCharacter(char ch) {
if (keyboardLocked) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size;
// Check if cursor is at a field attribute or in a protected field
ExtendedAttribute ea = screen.getCell(baddr);
if (ea.isFieldAttribute()) {
// Move to next position
baddr = (baddr + 1) % size;
ea = screen.getCell(baddr);
}
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) {
// Protected field — can't type here
return;
}
// Translate character to EBCDIC
int ebc = translator.unicodeToEbcdic(ch);
if (ebc < 0) return;
if (insertMode) {
// Insert mode: shift characters right from cursor to end of field
// Find end of field
int endAddr = baddr;
int count = 0;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = screen.incrementAddress(endAddr);
count++;
if (endAddr == baddr) break; // wrapped around (unformatted)
}
// Check if last position is non-null (field overflow)
if (screen.getCell(endAddr).ec != 0) {
// Field overflow — can't insert
return;
}
// Shift right from endAddr-1 down to baddr
int dst = endAddr;
int shiftCount = 0;
while (dst != baddr && shiftCount < size) {
int src = screen.decrementAddress(dst);
screen.getCell(dst).ec = screen.getCell(src).ec;
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
dst = src;
shiftCount++;
}
}
// Write character — preserve existing fg/bg/gr/cs attributes
// so the character inherits the field's color scheme
ea = screen.getCell(baddr);
ea.ec = (byte) ebc;
ea.ucs4 = ch;
// Set MDT on field attribute
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
ExtendedAttribute faEa = screen.getCell(faAddr);
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
}
// Advance cursor
int startAdvance = baddr;
baddr = (baddr + 1) % size;
int advCount = 0;
// Skip over field attributes safely
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
baddr = (baddr + 1) % size;
advCount++;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/**
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
*/
public void sendAid(int aidCode) {
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
if (keyboardLocked && aidCode != AID_CLEAR) {
System.err.println("Keyboard locked, dropping AID");
return;
}
lastAid = aidCode;
setKeyboardLocked(true);
if (aidCode == AID_CLEAR) {
screen.clear();
screen.markAllChanged();
if (graphicsPlane != null) {
graphicsPlane.clear();
}
// Send just the AID
byte[] data = new byte[] { (byte) aidCode };
sendAidResponse(data);
return;
}
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
// SSCP-LU Mode (e.g. VM line mode / CP console before conmode 3270):
// Per RFC 2355: Send raw EBCDIC line data without AID or 3270 cursor address.
int cols = screen.getCols();
int curAddr = screen.getCursorAddress();
int row = curAddr / cols;
int rowStart = row * cols;
int rowEnd = rowStart + cols;
// Find last non-null, non-blank character in the current row
int lastChar = rowStart - 1;
for (int i = rowEnd - 1; i >= rowStart; i--) {
int ec = screen.getCell(i).ec & 0xFF;
if (ec != 0x00 && ec != 0x40) {
lastChar = i;
break;
}
}
ByteArrayOutputStream sscpData = new ByteArrayOutputStream();
for (int i = rowStart; i <= lastChar; i++) {
int ec = screen.getCell(i).ec & 0xFF;
sscpData.write(ec != 0x00 ? ec : 0x40);
}
fsm.sendSscpLuData(sscpData.toByteArray());
// Advance cursor to beginning of next row
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
screen.setCursorAddress(nextRowAddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
setKeyboardLocked(false);
return;
}
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
// PA keys: send AID + optional PID + cursor address only (no modified data)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
sendAidResponse(out.toByteArray());
return;
}
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (screen.isFormatted()) {
// Send modified fields with SBA
// Per 3270 Data Stream Architecture: suppress NULLs (0x00) from field data
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int fieldStart = (i + 1) % size;
// Always send SBA and address of first character in field
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
// Send all non-null characters in field (suppressing 0x00)
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart) break;
}
}
}
} else {
// Unformatted screen in 3270 mode:
// Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
// or we can just send everything up to the last non-null on the screen.
// IBM spec: "all alphanumeric characters... Nulls are suppressed."
// Actually, the simplest is to send everything, but suppress nulls.
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
}
}
sendAidResponse(out.toByteArray());
}
private void sendAidResponse(byte[] data) {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
fsm.sendSscpLuData(data);
} else if (fsm != null) {
fsm.send3270Data(data);
}
}
/**
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
*/
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
return;
}
if (isKeyboardLocked()) {
return;
}
setKeyboardLocked(true);
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, aidCode, true, isShift, isCtrl, pickedSeg, pickTag
);
System.err.println(String.format(
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl
));
out.write(AID_SF);
try {
out.write(sf);
} catch (java.io.IOException ignored) {}
} else {
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
}
sendAidResponse(out.toByteArray());
}
// ========== Cursor movement ==========
/**
* Simulate a Text Light Pen selection.
*/
public boolean lightPenSelect(int address) {
if (screen == null || !screen.isFormatted()) {
System.out.println("LP: screen null or unformatted");
return false;
}
int size = screen.getRows() * screen.getCols();
if (size <= 0) return false;
address = ((address % size) + size) % size;
int faPos = screen.findFieldAttribute(address);
if (faPos < 0) {
System.out.println("LP: no FA found for addr=" + address);
return false;
}
ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF;
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
+ " selectable=" + haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
return false;
}
int designatorPos = (faPos + 1) % size;
ExtendedAttribute desCell = screen.getCell(designatorPos);
int ebcdic = desCell.ec & 0xFF;
char ascii = (char) desCell.ucs4;
screen.setCursorAddress(designatorPos);
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
} else if (ebcdic == 0x50 || ascii == '&') {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
return true;
} else if (ebcdic == 0x6F || ascii == '?') {
desCell.ec = (byte) 0x6E;
desCell.ucs4 = '>';
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else if (ebcdic == 0x6E || ascii == '>') {
desCell.ec = (byte) 0x6F;
desCell.ucs4 = '?';
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
}
}
public void cursorUp() {
int addr = screen.getCursorAddress();
addr -= screen.getCols();
if (addr < 0) addr += screen.getRows() * screen.getCols();
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorDown() {
int addr = screen.getCursorAddress();
addr += screen.getCols();
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorLeft() {
int addr = screen.getCursorAddress();
addr = screen.decrementAddress(addr);
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorRight() {
int addr = screen.getCursorAddress();
addr = screen.incrementAddress(addr);
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorHome() {
if (screen.isFormatted()) {
screen.setCursorAddress(screen.findNextUnprotected(0));
} else {
screen.setCursorAddress(0);
}
screen.updateDisplaySnapshot();
}
public void tab() {
int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public int getLastAid() { return lastAid; }
public void setLastAid(int aid) { this.lastAid = aid; }
public void setCursorAddress(int baddr) {
screen.setCursorAddress(baddr);
screen.updateDisplaySnapshot();
}
public void backTab() {
if (!screen.isFormatted()) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int addr = screen.getCursorAddress();
addr = ((addr % size) + size) % size;
int start = screen.decrementAddress(addr);
addr = start;
int count = 0;
do {
addr = screen.decrementAddress(addr);
if (screen.getCell(addr).isFieldAttribute()) {
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
screen.setCursorAddress(screen.incrementAddress(addr));
screen.updateDisplaySnapshot();
return;
}
}
count++;
} while (addr != start && count < size);
}
public void eraseEof() {
if (!screen.isFormatted()) {
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
for (int i = addr; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
ea.ec = 0;
ea.ucs4 = 0;
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return;
}
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
addr = ((addr % size) + size) % size;
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// Erase from cursor to end of field
int count = 0;
while (!screen.getCell(addr).isFieldAttribute() && count < size) {
ExtendedAttribute ea = screen.getCell(addr);
ea.ec = 0;
ea.ucs4 = 0;
addr = screen.incrementAddress(addr);
count++;
}
// Set MDT
int faAddr = screen.findFieldAttribute(screen.getCursorAddress());
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
public void deleteChar() {
if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
addr = ((addr % size) + size) % size;
// Cannot delete a field attribute
if (screen.getCell(addr).isFieldAttribute()) return;
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// Shift characters left within the field
int shiftAddr = addr;
int count = 0;
while (count < size) {
int next = screen.incrementAddress(shiftAddr);
if (screen.getCell(next).isFieldAttribute()) {
screen.getCell(shiftAddr).ec = 0;
screen.getCell(shiftAddr).ucs4 = 0;
break;
}
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
shiftAddr = next;
count++;
}
int faAddr = screen.findFieldAttribute(addr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
public void backspace() {
int addr = screen.getCursorAddress();
int prev = screen.decrementAddress(addr);
if (screen.isFormatted()) {
// If previous position is a field attribute or in protected field, do not back up over it
if (screen.getCell(prev).isFieldAttribute()) {
return;
}
byte faVal = screen.getFieldAttributeAt(prev);
if (faIsProtected(faVal & 0xFF)) {
return;
}
}
screen.setCursorAddress(prev);
deleteChar();
}
/** Erase All Unprotected fields (Erase Input key in 3270). */
public void eraseInput() {
if (keyboardLocked) return;
screen.eraseAllUnprotected();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
public void newline() {
if (keyboardLocked) return;
int cur = screen.getCursorAddress();
int cols = screen.getCols();
int rows = screen.getRows();
int curRow = cur / cols;
int nextRow = (curRow + 1) % rows;
int target = nextRow * cols;
if (screen.isFormatted()) {
target = screen.findNextUnprotected(target == 0 ? (rows * cols - 1) : (target - 1));
}
screen.setCursorAddress(target);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Insert Duplicate (DUP) code and advance to next field. */
public void dup() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_DUP;
ea.ucs4 = '*';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
tab();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Insert Field Mark (FM) code. */
public void fieldMark() {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) return;
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) FCORDER_FM;
ea.ucs4 = ';';
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
int size = screen.getRows() * screen.getCols();
baddr = (baddr + 1) % size;
while (screen.getCell(baddr).isFieldAttribute()) {
baddr = (baddr + 1) % size;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Attention key (sends Telnet IP). */
public void attn() {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
fsm.sendRecord(ip);
}
}
/** SysReq key. */
public void sysReq() {
reset();
}
/** Reset (unlock keyboard, cancel insert mode). */
public void reset() {
setKeyboardLocked(false);
insertMode = false;
}
// ========== File Transfer Support ==========
/**
* Send an AID code, bypassing the keyboard lock.
* Used internally by the file transfer protocol to send Enter/PF keys
* during a transfer when the keyboard is normally locked.
*/
public void sendAidForFT(int aidCode) {
boolean wasLocked = keyboardLocked;
keyboardLocked = false;
sendAid(aidCode);
keyboardLocked = wasLocked;
}
/**
* Type a string into the current cursor position and press Enter.
* Used by IND$FILE to inject the transfer command.
* Equivalent to emulate_input() in x3270.
*/
public void emulateInput(String text) {
// Type each character
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '\n') {
// Newline means press Enter
sendAid(AID_ENTER);
return;
}
typeCharacter(ch);
}
}
/**
* Erase the current field and return its capacity.
* Used by IND$FILE to prepare the field for the transfer command.
* Equivalent to kybd_prime() in x3270.
*
* Returns:
* - The number of characters that can fit in the field, or
* - -1 if the keyboard is locked
* - -2 if not in 3270 mode
* - -3 if no input field found
*/
public int kybdPrime() {
if (keyboardLocked) return -1;
if (!screen.isFormatted()) {
// Unformatted screen — use entire buffer
int size = screen.getRows() * screen.getCols();
screen.setCursorAddress(0);
for (int i = 0; i < size; i++) {
screen.getCell(i).ec = 0;
screen.getCell(i).ucs4 = 0;
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return size;
}
// Find the field at cursor and erase it
int curAddr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(curAddr);
if (faIsProtected(faVal & 0xFF)) {
// Try to find first unprotected field
curAddr = screen.findNextUnprotected(curAddr);
faVal = screen.getFieldAttributeAt(curAddr);
if (faIsProtected(faVal & 0xFF)) {
return -3; // No unprotected field
}
}
// Move to start of field
int faAddr = screen.findFieldAttribute(curAddr);
int fieldStart = screen.incrementAddress(faAddr);
screen.setCursorAddress(fieldStart);
// Count field length and erase
int fieldLen = 0;
int pos = fieldStart;
int size = screen.getRows() * screen.getCols();
int count = 0;
while (!screen.getCell(pos).isFieldAttribute() && count < size) {
screen.getCell(pos).ec = 0;
screen.getCell(pos).ucs4 = 0;
fieldLen++;
pos = screen.incrementAddress(pos);
count++;
if (pos == fieldStart) break;
}
// Set MDT
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return fieldLen;
}
/**
* Send a structured field response directly.
* Used by DFT mode file transfer.
*/
public void sendStructuredFieldData(byte[] data) {
fsm.send3270Data(data);
}
/** Get a reference to the TelnetFSM for direct SF operations. */
public TelnetFSM getTelnetFSM() {
return fsm;
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.listener;
import haus.nightmare.lib3270j.ConnectionState;
/**
* Listener for connection state changes and errors.
*/
public interface ConnectionListener {
/** Called when the connection state changes. */
void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState);
/** Called when a connection error occurs. */
void onConnectionError(String message);
/** Called when TN3270E negotiation completes. */
default void onTN3270ENegotiated(String deviceType, String deviceName) {}
}
@@ -0,0 +1,18 @@
package haus.nightmare.lib3270j.listener;
/**
* Listener for screen buffer changes.
*/
public interface ScreenUpdateListener {
/** Called when the screen buffer has been updated. */
void onScreenUpdated();
/** Called when the cursor position changes. */
default void onCursorMoved(int oldAddress, int newAddress) {}
/** Called when the host sends a sound alarm (WCC bit). */
default void onSoundAlarm() {}
/** Called when the screen size changes (erase/write vs erase/write alternate). */
default void onScreenSizeChanged(int rows, int cols) {}
}
@@ -0,0 +1,385 @@
package haus.nightmare.lib3270j.protocol;
/**
* 3270 Data Stream protocol constants.
* Derived from 3270ds.h in x3270.
*/
public final class DS3270Constants {
private DS3270Constants() {}
// ========== 3270 Commands ==========
public static final int CMD_W = 0x01; // Write
public static final int CMD_RB = 0x02; // Read Buffer
public static final int CMD_NOP = 0x03; // No-Op
public static final int CMD_EW = 0x05; // Erase/Write
public static final int CMD_RM = 0x06; // Read Modified
public static final int CMD_EWA = 0x0d; // Erase/Write Alternate
public static final int CMD_RMA = 0x0e; // Read Modified All
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
public static final int CMD_WSF = 0x11; // Write Structured Field
// SNA 3270 Commands
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
public static final int SNA_CMD_EWA = 0x7e; // Erase/Write Alternate
public static final int SNA_CMD_W = 0xf1; // Write
public static final int SNA_CMD_RB = 0xf2; // Read Buffer
public static final int SNA_CMD_WSF = 0xf3; // Write Structured Field
public static final int SNA_CMD_EW = 0xf5; // Erase/Write
public static final int SNA_CMD_RM = 0xf6; // Read Modified
// ========== 3270 Orders ==========
public static final int ORDER_PT = 0x05; // Program Tab
public static final int ORDER_GE = 0x08; // Graphic Escape
public static final int ORDER_SBA = 0x11; // Set Buffer Address
public static final int ORDER_EUA = 0x12; // Erase Unprotected to Address
public static final int ORDER_IC = 0x13; // Insert Cursor
public static final int ORDER_SF = 0x1d; // Start Field
public static final int ORDER_SA = 0x28; // Set Attribute
public static final int ORDER_SFE = 0x29; // Start Field Extended
public static final int ORDER_YALE = 0x2b; // Yale sub command
public static final int ORDER_MF = 0x2c; // Modify Field
public static final int ORDER_RA = 0x3c; // Repeat to Address
// Format control orders
public static final int FCORDER_NULL = 0x00;
public static final int FCORDER_FF = 0x0c; // Form feed
public static final int FCORDER_CR = 0x0d; // Carriage return
public static final int FCORDER_SO = 0x0e; // Shift out (DBCS start)
public static final int FCORDER_SI = 0x0f; // Shift in (DBCS end)
public static final int FCORDER_NL = 0x15; // New line
public static final int FCORDER_EM = 0x19; // End of medium
public static final int FCORDER_LF = 0x25; // Line feed
public static final int FCORDER_DUP = 0x1c; // Duplicate
public static final int FCORDER_FM = 0x1e; // Field mark
public static final int FCORDER_SUB = 0x3f; // Substitute
public static final int FCORDER_EO = 0xff; // Eight ones
// ========== Field Attributes ==========
public static final int FA_PRINTABLE = 0xc0;
public static final int FA_PROTECT = 0x20; // Protected (1) / Unprotected (0)
public static final int FA_NUMERIC = 0x10; // Numeric (1) / Alphanumeric (0)
public static final int FA_INTENSITY = 0x0c; // Display/selector pen mask
public static final int FA_INT_NORM_NSEL = 0x00; // Normal, non-detect
public static final int FA_INT_NORM_SEL = 0x04; // Normal, detectable
public static final int FA_INT_HIGH_SEL = 0x08; // Intensified, detectable
public static final int FA_INT_ZERO_NSEL = 0x0c; // Non-display, non-detect
public static final int FA_RESERVED = 0x02;
public static final int FA_MODIFY = 0x01; // Modified
public static final int FA_MASK = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC | FA_INTENSITY | FA_MODIFY;
public static boolean faIsModified(int fa) { return (fa & FA_MODIFY) != 0; }
public static boolean faIsNumeric(int fa) { return (fa & FA_NUMERIC) != 0; }
public static boolean faIsProtected(int fa) { return (fa & FA_PROTECT) != 0; }
public static boolean faIsSkip(int fa) { return (fa & FA_PROTECT) != 0 && (fa & FA_NUMERIC) != 0; }
public static boolean faIsZero(int fa) { return (fa & FA_INTENSITY) == FA_INT_ZERO_NSEL; }
public static boolean faIsHigh(int fa) { return (fa & FA_INTENSITY) == FA_INT_HIGH_SEL; }
public static boolean faIsNormal(int fa) {
return (fa & FA_INTENSITY) == FA_INT_NORM_NSEL || (fa & FA_INTENSITY) == FA_INT_NORM_SEL;
}
public static boolean faIsSelectable(int fa) {
return (fa & FA_INTENSITY) == FA_INT_NORM_SEL || (fa & FA_INTENSITY) == FA_INT_HIGH_SEL;
}
// ========== Extended Attributes ==========
public static final int XA_ALL = 0x00;
public static final int XA_3270 = 0xc0;
public static final int XA_VALIDATION = 0xc1;
public static final int XA_OUTLINING = 0xc2;
public static final int XA_HIGHLIGHTING = 0x41;
public static final int XA_FOREGROUND = 0x42;
public static final int XA_CHARSET = 0x43;
public static final int XA_BACKGROUND = 0x45;
public static final int XA_TRANSPARENCY = 0x46;
public static final int XA_INPUT_CONTROL = 0xfe;
// Highlighting values
public static final int XAH_DEFAULT = 0x00;
public static final int XAH_NORMAL = 0xf0;
public static final int XAH_BLINK = 0xf1;
public static final int XAH_REVERSE = 0xf2;
public static final int XAH_UNDERSCORE = 0xf4;
public static final int XAH_INTENSIFY = 0xf8;
// Default color
public static final int XAC_DEFAULT = 0x00;
// Outlining values
public static final int XAO_UNDERLINE = 0x01;
public static final int XAO_RIGHT = 0x02;
public static final int XAO_OVERLINE = 0x04;
public static final int XAO_LEFT = 0x08;
// Validation values
public static final int XAV_FILL = 0x04;
public static final int XAV_ENTRY = 0x02;
public static final int XAV_TRIGGER = 0x01;
// Transparency values
public static final int XAT_DEFAULT = 0x00;
public static final int XAT_OR = 0xf0;
public static final int XAT_XOR = 0xf1;
public static final int XAT_OPAQUE = 0xff;
// Input control
public static final int XAI_DISABLED = 0x00;
public static final int XAI_ENABLED = 0x01;
// ========== WCC (Write Control Character) ==========
public static final int WCC_RESET_BIT = 0x40;
public static final int WCC_START_PRINTER_BIT = 0x08;
public static final int WCC_SOUND_ALARM_BIT = 0x04;
public static final int WCC_KEYBOARD_RESTORE_BIT = 0x02;
public static final int WCC_RESET_MDT_BIT = 0x01;
public static boolean wccReset(int wcc) { return (wcc & WCC_RESET_BIT) != 0; }
public static boolean wccStartPrinter(int wcc) { return (wcc & WCC_START_PRINTER_BIT) != 0; }
public static boolean wccSoundAlarm(int wcc) { return (wcc & WCC_SOUND_ALARM_BIT) != 0; }
public static boolean wccKeyboardRestore(int wcc) { return (wcc & WCC_KEYBOARD_RESTORE_BIT) != 0; }
public static boolean wccResetMDT(int wcc) { return (wcc & WCC_RESET_MDT_BIT) != 0; }
// ========== AID (Attention Identifier) ==========
public static final int AID_NO = 0x60;
public static final int AID_QREPLY = 0x61;
public static final int AID_ENTER = 0x7d;
public static final int AID_PF1 = 0xf1;
public static final int AID_PF2 = 0xf2;
public static final int AID_PF3 = 0xf3;
public static final int AID_PF4 = 0xf4;
public static final int AID_PF5 = 0xf5;
public static final int AID_PF6 = 0xf6;
public static final int AID_PF7 = 0xf7;
public static final int AID_PF8 = 0xf8;
public static final int AID_PF9 = 0xf9;
public static final int AID_PF10 = 0x7a;
public static final int AID_PF11 = 0x7b;
public static final int AID_PF12 = 0x7c;
public static final int AID_PF13 = 0xc1;
public static final int AID_PF14 = 0xc2;
public static final int AID_PF15 = 0xc3;
public static final int AID_PF16 = 0xc4;
public static final int AID_PF17 = 0xc5;
public static final int AID_PF18 = 0xc6;
public static final int AID_PF19 = 0xc7;
public static final int AID_PF20 = 0xc8;
public static final int AID_PF21 = 0xc9;
public static final int AID_PF22 = 0x4a;
public static final int AID_PF23 = 0x4b;
public static final int AID_PF24 = 0x4c;
public static final int AID_OICR = 0xe6;
public static final int AID_MSR_MHS = 0xe7;
public static final int AID_SELECT = 0x7e;
public static final int AID_PA1 = 0x6c;
public static final int AID_PA2 = 0x6e;
public static final int AID_PA3 = 0x6b;
public static final int AID_CLEAR = 0x6d;
public static final int AID_SYSREQ = 0xf0;
public static final int AID_SF = 0x88;
public static final int SFID_QREPLY = 0x81;
// ========== Structured Field IDs ==========
public static final int SF_READ_PART = 0x01;
public static final int SF_RP_QUERY = 0x02;
public static final int SF_RP_QLIST = 0x03;
public static final int SF_RPQ_LIST = 0x00;
public static final int SF_RPQ_EQUIV = 0x40;
public static final int SF_RPQ_ALL = 0x80;
public static final int SF_ERASE_RESET = 0x03;
public static final int SF_ER_DEFAULT = 0x00;
public static final int SF_ER_ALT = 0x80;
public static final int SF_SET_REPLY_MODE = 0x09;
public static final int SF_SRM_FIELD = 0x00;
public static final int SF_SRM_XFIELD = 0x01;
public static final int SF_SRM_CHAR = 0x02;
public static final int SF_CREATE_PART = 0x0c;
public static final int SF_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80;
public static final int QR_USABLE_AREA = 0x81;
public static final int QR_IMAGE = 0x82;
public static final int QR_TEXT_PART = 0x83;
public static final int QR_ALPHA_PART = 0x84;
public static final int QR_CHARSETS = 0x85;
public static final int QR_COLOR = 0x86;
public static final int QR_HIGHLIGHTING = 0x87;
public static final int QR_REPLY_MODES = 0x88;
public static final int QR_SAVE_RESTORE = 0x8c;
public static final int QR_DBCS_ASIA = 0x91;
public static final int QR_DDM = 0x95;
public static final int QR_TRANSPARENCY = 0x99;
public static final int QR_RPQNAMES = 0xa1;
public static final int QR_IMP_PART = 0xa6;
public static final int QR_RPQ_NAMES = 0xa8;
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device
public static final int QR_OEM_FMT = 0xb3; // OEM Format
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets
public static final int QR_NULL = 0xff;
// ========== Screen model sizes ==========
public static final int MODEL_2_ROWS = 24;
public static final int MODEL_2_COLS = 80;
public static final int MODEL_3_ROWS = 32;
public static final int MODEL_3_COLS = 80;
public static final int MODEL_4_ROWS = 43;
public static final int MODEL_4_COLS = 80;
public static final int MODEL_5_ROWS = 27;
public static final int MODEL_5_COLS = 132;
public static final int MAX_ROWS_COLS = 0x3fff;
// ========== Host colors ==========
public static final int HOST_COLOR_NEUTRAL_BLACK = 0;
public static final int HOST_COLOR_BLUE = 1;
public static final int HOST_COLOR_RED = 2;
public static final int HOST_COLOR_PINK = 3;
public static final int HOST_COLOR_GREEN = 4;
public static final int HOST_COLOR_TURQUOISE = 5;
public static final int HOST_COLOR_YELLOW = 6;
public static final int HOST_COLOR_NEUTRAL_WHITE = 7;
public static final int HOST_COLOR_BLACK = 8;
public static final int HOST_COLOR_DEEP_BLUE = 9;
public static final int HOST_COLOR_ORANGE = 10;
public static final int HOST_COLOR_PURPLE = 11;
public static final int HOST_COLOR_PALE_GREEN = 12;
public static final int HOST_COLOR_PALE_TURQUOISE = 13;
public static final int HOST_COLOR_GREY = 14;
public static final int HOST_COLOR_WHITE = 15;
// ========== Graphics rendition bits ==========
public static final int GR_BLINK = 0x01;
public static final int GR_REVERSE = 0x02;
public static final int GR_UNDERLINE = 0x04;
public static final int GR_INTENSIFY = 0x08;
// ========== Character set codes ==========
public static final int CS_MASK = 0x03;
public static final int CS_BASE = 0x00;
public static final int CS_APL = 0x01;
public static final int CS_LINEDRAW = 0x02;
public static final int CS_DBCS = 0x03;
public static final int CS_GE = 0x04;
// ========== BIND definitions ==========
public static final int BIND_RU = 0x31;
public static final int BIND_OFF_MAXRU_SEC = 10;
public static final int BIND_OFF_MAXRU_PRI = 11;
public static final int BIND_OFF_RD = 20;
public static final int BIND_OFF_CD = 21;
public static final int BIND_OFF_RA = 22;
public static final int BIND_OFF_CA = 23;
public static final int BIND_OFF_SSIZE = 24;
public static final int BIND_OFF_PLU_NAME_LEN = 27;
public static final int BIND_PLU_NAME_MAX = 8;
public static final int BIND_OFF_PLU_NAME = 28;
// BIND dimension flags
public static final int BIND_DIMS_PRESENT = 0x1;
public static final int BIND_DIMS_ALT = 0x2;
public static final int BIND_DIMS_VALID = 0x4;
// ========== EBCDIC common characters ==========
public static final int EBC_NULL = 0x00;
public static final int EBC_SPACE = 0x40;
public static final int EBC_AMPERSAND = 0x50;
public static final int EBC_MINUS = 0x60;
public static final int EBC_PERIOD = 0x4b;
public static final int EBC_COMMA = 0x6b;
public static final int EBC_DUP = 0x1c;
public static final int EBC_FM = 0x1e;
public static final int EBC_FF = 0x0c;
public static final int EBC_CR = 0x0d;
public static final int EBC_NL = 0x15;
public static final int EBC_EM = 0x19;
public static final int EBC_SUB = 0x3f;
public static final int EBC_EO = 0xff;
/**
* 6-bit code table for 12-bit buffer address encoding.
*/
public static final int[] CODE_TABLE = {
0x40, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xC8, 0xC9, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
0xD8, 0xD9, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
0xE8, 0xE9, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7,
0xF8, 0xF9, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
};
/**
* Decode a 2-byte buffer address.
* Handles both 12-bit and 14-bit addressing.
*/
public static int decodeAddress(int b1, int b2) {
if ((b1 & 0xC0) == 0x00) {
// 14-bit format
return ((b1 & 0x3F) << 8) | b2;
} else {
// 12-bit format
return ((b1 & 0x3F) << 6) | (b2 & 0x3F);
}
}
/**
* Encode a buffer address into a 2-byte array.
* Uses 14-bit format if screen > 4096 positions, otherwise 12-bit.
*/
public static byte[] encodeAddress(int addr, int rows, int cols) {
byte[] result = new byte[2];
if (rows * cols > 0x1000) {
// 14-bit format
result[0] = (byte) ((addr >> 8) & 0x3F);
result[1] = (byte) (addr & 0xFF);
} else {
// 12-bit format
result[0] = (byte) CODE_TABLE[(addr >> 6) & 0x3F];
result[1] = (byte) CODE_TABLE[addr & 0x3F];
}
return result;
}
/** Check if a byte is a 3270 order. */
public static boolean isOrder(int b) {
switch (b) {
case ORDER_PT:
case ORDER_GE:
case ORDER_SBA:
case ORDER_EUA:
case ORDER_IC:
case ORDER_SF:
case ORDER_SA:
case ORDER_SFE:
case ORDER_YALE:
case ORDER_MF:
case ORDER_RA:
return true;
default:
return false;
}
}
/** Get human-readable command name. */
public static String commandName(int cmd) {
switch (cmd) {
case CMD_W: case SNA_CMD_W: return "Write";
case CMD_EW: case SNA_CMD_EW: return "EraseWrite";
case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate";
case CMD_RB: case SNA_CMD_RB: return "ReadBuffer";
case CMD_RM: case SNA_CMD_RM: return "ReadModified";
case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll";
case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected";
case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField";
case CMD_NOP: return "NoOp";
default: return String.format("Unknown(0x%02x)", cmd);
}
}
}
@@ -0,0 +1,142 @@
package haus.nightmare.lib3270j.protocol;
/**
* TN3270E protocol constants per RFC 2355.
* Derived from tn3270e.h in x3270.
*/
public final class TN3270EConstants {
private TN3270EConstants() {}
// Negotiation operations
public static final int OP_ASSOCIATE = 0;
public static final int OP_CONNECT = 1;
public static final int OP_DEVICE_TYPE = 2;
public static final int OP_FUNCTIONS = 3;
public static final int OP_IS = 4;
public static final int OP_REASON = 5;
public static final int OP_REJECT = 6;
public static final int OP_REQUEST = 7;
public static final int OP_SEND = 8;
// Reason codes
public static final int REASON_CONN_PARTNER = 0;
public static final int REASON_DEVICE_IN_USE = 1;
public static final int REASON_INV_ASSOCIATE = 2;
public static final int REASON_INV_DEVICE_NAME = 3;
public static final int REASON_INV_DEVICE_TYPE = 4;
public static final int REASON_TYPE_NAME_ERROR = 5;
public static final int REASON_UNKNOWN_ERROR = 6;
public static final int REASON_UNSUPPORTED_REQ = 7;
// Function names
public static final int FUNC_BIND_IMAGE = 0;
public static final int FUNC_DATA_STREAM_CTL = 1;
public static final int FUNC_RESPONSES = 2;
public static final int FUNC_SCS_CTL_CODES = 3;
public static final int FUNC_SYSREQ = 4;
public static final int FUNC_CONTENTION_RESOLUTION = 5;
public static final int FUNC_SNA_SENSE = 6;
// Data type names
public static final int DT_3270_DATA = 0x00;
public static final int DT_SCS_DATA = 0x01;
public static final int DT_RESPONSE = 0x02;
public static final int DT_BIND_IMAGE = 0x03;
public static final int DT_UNBIND = 0x04;
public static final int DT_NVT_DATA = 0x05;
public static final int DT_REQUEST = 0x06;
public static final int DT_SSCP_LU_DATA = 0x07;
public static final int DT_PRINT_EOJ = 0x08;
public static final int DT_BID = 0x09;
// Request flags
public static final int RQF_ERR_COND_CLEARED = 0x00;
public static final int RQF_SEND_DATA = 0x01;
public static final int RQF_KEYBOARD_RESTORE = 0x02;
public static final int RQF_SIGNAL = 0x04;
// Response flags (header)
public static final int RSF_NO_RESPONSE = 0x00;
public static final int RSF_ERROR_RESPONSE = 0x01;
public static final int RSF_ALWAYS_RESPONSE = 0x02;
// Response flags (trailer)
public static final int RSF_POSITIVE_RESPONSE = 0x00;
public static final int RSF_NEGATIVE_RESPONSE = 0x01;
public static final int RSF_SNA_SENSE = 0x02;
// Positive response data
public static final int POS_DEVICE_END = 0x00;
// Negative response data
public static final int NEG_COMMAND_REJECT = 0x00;
public static final int NEG_INTERVENTION_REQUIRED = 0x01;
public static final int NEG_OPERATION_CHECK = 0x02;
public static final int NEG_COMPONENT_DISCONNECTED = 0x03;
// TN3270E header size
public static final int EH_SIZE = 5;
// UNBIND types
public static final int UNBIND_NORMAL = 0x01;
public static final int UNBIND_BIND_FORTHCOMING = 0x02;
public static final int UNBIND_VR_INOPERATIVE = 0x07;
public static final int UNBIND_RX_INOPERATIVE = 0x08;
public static final int UNBIND_HRESET = 0x09;
public static final int UNBIND_SSCP_GONE = 0x0a;
public static final int UNBIND_VR_DEACTIVATED = 0x0b;
public static final int UNBIND_LU_FAILURE_PERM = 0x0c;
public static final int UNBIND_LU_FAILURE_TEMP = 0x0e;
public static final int UNBIND_CLEANUP = 0x0f;
public static final int UNBIND_BAD_SENSE = 0xfe;
// Name lookups for tracing
private static final String[] REASON_NAMES = {
"CONN-PARTNER", "DEVICE-IN-USE", "INV-ASSOCIATE", "INV-NAME",
"INV-DEVICE-TYPE", "TYPE-NAME-ERROR", "UNKNOWN-ERROR", "UNSUPPORTED-REQ"
};
private static final String[] FUNCTION_NAMES = {
"BIND-IMAGE", "DATA-STREAM-CTL", "RESPONSES", "SCS-CTL-CODES",
"SYSREQ", "CONTENTION-RESOLUTION", "SNA-SENSE"
};
private static final String[] DATA_TYPE_NAMES = {
"3270-DATA", "SCS-DATA", "RESPONSE", "BIND-IMAGE", "UNBIND",
"NVT-DATA", "REQUEST", "SSCP-LU-DATA", "PRINT-EOJ", "BID"
};
private static final String[] HRSP_FLAG_NAMES = {
"NO-RESPONSE", "ERROR-RESPONSE", "ALWAYS-RESPONSE"
};
public static String reasonName(int code) {
return code >= 0 && code < REASON_NAMES.length ? REASON_NAMES[code] : "??";
}
public static String functionName(int code) {
return code >= 0 && code < FUNCTION_NAMES.length ? FUNCTION_NAMES[code] : "??";
}
public static String dataTypeName(int code) {
return code >= 0 && code < DATA_TYPE_NAMES.length ? DATA_TYPE_NAMES[code] : "??";
}
public static String responseHeaderFlagName(int code) {
return code >= 0 && code < HRSP_FLAG_NAMES.length ? HRSP_FLAG_NAMES[code] : "??";
}
/** Format a function set as a human-readable string. */
public static String functionNames(boolean[] funcs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < funcs.length && i <= FUNC_SNA_SENSE; i++) {
if (funcs[i]) {
if (sb.length() > 0) sb.append(", ");
sb.append(functionName(i));
}
}
return sb.toString();
}
}
@@ -0,0 +1,118 @@
package haus.nightmare.lib3270j.protocol;
/**
* Telnet protocol constants from RFC 854 and extensions.
* Derived from arpa_telnet.h in x3270.
*/
public final class TelnetConstants {
private TelnetConstants() {}
// Telnet commands
public static final int IAC = 255; // Interpret As Command
public static final int DONT = 254; // You are not to use option
public static final int DO = 253; // Please, you use option
public static final int WONT = 252; // I won't use option
public static final int WILL = 251; // I will use option
public static final int SB = 250; // Interpret as subnegotiation
public static final int GA = 249; // You may reverse the line
public static final int EL = 248; // Erase the current line
public static final int EC = 247; // Erase the current character
public static final int AYT = 246; // Are you there
public static final int AO = 245; // Abort output
public static final int IP = 244; // Interrupt process
public static final int BREAK = 243; // Break
public static final int DM = 242; // Data mark
public static final int NOP = 241; // No operation
public static final int SE = 240; // End sub negotiation
public static final int EOR = 239; // End of record
public static final int SUSP = 237; // Suspend process
public static final int xEOF = 236; // End of file
// Telnet options
public static final int TELOPT_BINARY = 0; // 8-bit data path
public static final int TELOPT_ECHO = 1; // Echo
public static final int TELOPT_RCP = 2; // Prepare to reconnect
public static final int TELOPT_SGA = 3; // Suppress go ahead
public static final int TELOPT_NAMS = 4; // Approximate message size
public static final int TELOPT_STATUS = 5; // Give status
public static final int TELOPT_TM = 6; // Timing mark
public static final int TELOPT_TTYPE = 24; // Terminal type
public static final int TELOPT_EOR = 25; // End of record
public static final int TELOPT_NAWS = 31; // Window size
public static final int TELOPT_TSPEED = 32; // Terminal speed
public static final int TELOPT_LFLOW = 33; // Remote flow control
public static final int TELOPT_LINEMODE = 34; // Linemode option
public static final int TELOPT_XDISPLOC = 35; // X Display Location
public static final int TELOPT_OLD_ENVIRON = 36; // Old environment variables
public static final int TELOPT_AUTHENTICATION = 37; // Authenticate
public static final int TELOPT_ENCRYPT = 38; // Encryption option
public static final int TELOPT_NEW_ENVIRON = 39; // New environment variables
public static final int TELOPT_TN3270E = 40; // Extended 3270 regime
public static final int TELOPT_STARTTLS = 46; // Start TLS
public static final int TELOPT_EXOPL = 255; // Extended options list
// Sub-option qualifiers
public static final int TELQUAL_IS = 0; // Option is...
public static final int TELQUAL_SEND = 1; // Send option
public static final int TELQUAL_INFO = 2; // Info
// New-environ sub-option objects
public static final int TELOBJ_VAR = 0;
public static final int TELOBJ_VALUE = 1;
public static final int TELOBJ_ESC = 2;
public static final int TELOBJ_USERVAR = 3;
// STARTTLS sub-option
public static final int TLS_FOLLOWS = 1;
// Standard ports
public static final int TELNET_PORT = 23;
public static final int TELNETS_PORT = 992;
/** Telnet option name lookup for tracing. */
public static String optionName(int opt) {
switch (opt) {
case TELOPT_BINARY: return "BINARY";
case TELOPT_ECHO: return "ECHO";
case TELOPT_SGA: return "SGA";
case TELOPT_TM: return "TIMING-MARK";
case TELOPT_TTYPE: return "TTYPE";
case TELOPT_EOR: return "EOR";
case TELOPT_NAWS: return "NAWS";
case TELOPT_NEW_ENVIRON: return "NEW-ENVIRON";
case TELOPT_TN3270E: return "TN3270E";
case TELOPT_STARTTLS: return "STARTTLS";
case TELOPT_TSPEED: return "TSPEED";
case TELOPT_LFLOW: return "LFLOW";
case TELOPT_LINEMODE: return "LINEMODE";
case TELOPT_XDISPLOC: return "XDISPLOC";
case TELOPT_OLD_ENVIRON: return "OLD-ENVIRON";
default: return "OPT-" + opt;
}
}
/** Telnet command name lookup for tracing. */
public static String commandName(int cmd) {
switch (cmd) {
case IAC: return "IAC";
case DONT: return "DONT";
case DO: return "DO";
case WONT: return "WONT";
case WILL: return "WILL";
case SB: return "SB";
case GA: return "GA";
case EL: return "EL";
case EC: return "EC";
case AYT: return "AYT";
case AO: return "AO";
case IP: return "IP";
case BREAK: return "BRK";
case DM: return "DMARK";
case NOP: return "NOP";
case SE: return "SE";
case EOR: return "EOR";
default: return "CMD-" + cmd;
}
}
}
@@ -0,0 +1,77 @@
package haus.nightmare.lib3270j.screen;
/**
* Extended attribute structure for a single screen buffer position.
* Mirrors struct ea from globals.h in x3270.
*/
public class ExtendedAttribute {
/** EBCDIC character code at this position. */
public byte ec;
/** Field attribute byte (non-zero if this position IS a field attribute). */
public byte fa;
/** Foreground color (0x00 for default, or 0xf0-0xff for explicit). */
public byte fg;
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
public byte bg;
/**
* Graphics rendition bits.
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
*/
public byte gr;
/** Character set (CS_BASE=0, CS_APL=1, CS_LINEDRAW=2, CS_DBCS=3; CS_GE=0x04 flag). */
public byte cs;
/** Input control (DBCS). */
public byte ic;
/** DBCS state. */
public byte db;
/**
* Unicode character for display (set by translation from ec, or directly in NVT mode).
*/
public char ucs4;
/** Clear all attributes. */
public void clear() {
ec = 0;
fa = 0;
fg = 0;
bg = 0;
gr = 0;
cs = 0;
ic = 0;
db = 0;
ucs4 = 0;
}
/** Copy all values from another ExtendedAttribute. */
public void copyFrom(ExtendedAttribute other) {
this.ec = other.ec;
this.fa = other.fa;
this.fg = other.fg;
this.bg = other.bg;
this.gr = other.gr;
this.cs = other.cs;
this.ic = other.ic;
this.db = other.db;
this.ucs4 = other.ucs4;
}
/** Check if this position is a field attribute. */
public boolean isFieldAttribute() {
return fa != 0;
}
@Override
public String toString() {
return String.format("EA[ec=%02x fa=%02x fg=%02x bg=%02x gr=%02x cs=%02x u=%04x]",
ec & 0xFF, fa & 0xFF, fg & 0xFF, bg & 0xFF, gr & 0xFF, cs & 0xFF, (int) ucs4);
}
}
@@ -0,0 +1,498 @@
package haus.nightmare.lib3270j.screen;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
* The 3270 screen buffer.
* Manages the display buffer, cursor, field attributes, and screen dimensions.
* Equivalent to ea_buf[] / aea_buf[] and related state in ctlr.c.
*/
public class ScreenBuffer {
private ExtendedAttribute[] buffer; // Main screen buffer
private ExtendedAttribute[] altBuffer; // Alternate screen buffer
private final ExtendedAttribute defaultFA; // Default field attribute (ea_buf[-1])
private int maxRows, maxCols; // Maximum (alternate) dimensions
private int defRows, defCols; // Default dimensions (24x80)
private int altRows, altCols; // Alternate dimensions
private int rows, cols; // Current dimensions
private int cursorAddress;
private int bufferAddress;
private boolean screenAlt; // Using alternate screen?
private boolean formatted; // Screen has at least one field attribute?
private byte replyMode = SF_SRM_FIELD;
private int activePartition = 0; // 0 = implicit partition
private boolean explicitPartitionActive = false;
// Change tracking
private boolean screenChanged;
private int firstChanged = -1;
private int lastChanged = -1;
// Default attribute values
private byte defaultFg = 0x00;
private byte defaultBg = 0x00;
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;
this.defRows = MODEL_2_ROWS;
this.defCols = MODEL_2_COLS;
this.altRows = model.getAlternateRows();
this.altCols = model.getAlternateCols();
this.maxRows = altRows;
this.maxCols = altCols;
this.rows = defRows;
this.cols = defCols;
// Default field attribute (like ea_buf[-1])
defaultFA = new ExtendedAttribute();
defaultFA.fa = (byte) (FA_PRINTABLE | FA_MODIFY);
defaultFA.ic = 1;
allocateBuffers();
updateDisplaySnapshot();
}
private void allocateBuffers() {
int size = maxRows * maxCols;
buffer = new ExtendedAttribute[size];
altBuffer = new ExtendedAttribute[size];
for (int i = 0; i < size; i++) {
buffer[i] = new ExtendedAttribute();
altBuffer[i] = new ExtendedAttribute();
}
cursorAddress = 0;
bufferAddress = 0;
}
/** Get the current screen buffer. */
public ExtendedAttribute[] getBuffer() { return buffer; }
/** Get a cell at the given buffer address. */
public ExtendedAttribute getCell(int addr) {
if (addr < 0 || addr >= rows * cols) return defaultFA;
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; }
public int getMaxRows() { return maxRows; }
public int getMaxCols() { return maxCols; }
public int getDefRows() { return defRows; }
public int getDefCols() { return defCols; }
public int getAltRows() { return altRows; }
public int getAltCols() { return altCols; }
public boolean isScreenAlt() { return screenAlt; }
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) {
if (newAltRows == altRows && newAltCols == altCols) return;
this.altRows = newAltRows;
this.altCols = newAltCols;
// maxRows/maxCols should be the larger of alt vs current max
if (newAltRows > maxRows || newAltCols > maxCols) {
this.maxRows = Math.max(maxRows, newAltRows);
this.maxCols = Math.max(maxCols, newAltCols);
allocateBuffers();
}
updateDisplaySnapshot();
}
// ========== Cursor ==========
public int getCursorAddress() { return cursorAddress; }
public synchronized void setCursorAddress(int addr) {
this.cursorAddress = addr;
this.displayCursorAddress = addr;
}
public int getCursorRow() { return cursorAddress / cols; }
public int getCursorCol() { return cursorAddress % cols; }
public int getBufferAddress() { return bufferAddress; }
public void setBufferAddress(int addr) { this.bufferAddress = addr; }
public byte getReplyMode() { return replyMode; }
public void setReplyMode(byte mode) { this.replyMode = mode; }
public int getActivePartition() { return activePartition; }
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; }
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
// ========== Screen erase ==========
/**
* Perform an erase, optionally using the alternate screen size.
*/
public synchronized void erase(boolean alt) {
clear();
int newRows = alt ? altRows : defRows;
int newCols = alt ? altCols : defCols;
if (alt == screenAlt && rows == newRows && cols == newCols) {
updateDisplaySnapshot();
return;
}
rows = newRows;
cols = newCols;
screenAlt = alt;
updateDisplaySnapshot();
}
public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos];
ea.clear();
ea.fa = fa;
if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
}
formatted = true;
screenChanged = true;
// The display logic needs to render this attribute character
ea.ec = 0x00; // Typically null character for the attribute space itself
}
/** Clear the entire buffer. */
public synchronized void clear() {
for (ExtendedAttribute ea : buffer) {
ea.clear();
}
cursorAddress = 0;
bufferAddress = 0;
formatted = false;
replyMode = SF_SRM_FIELD;
activePartition = 0;
explicitPartitionActive = false;
screenChanged = true;
defaultFg = 0x00;
defaultBg = 0x00;
defaultGr = 0x00;
defaultCs = 0x00;
defaultIc = 0x00;
replyMode = SF_SRM_FIELD;
updateDisplaySnapshot();
}
/**
* Erase all unprotected fields.
*/
public synchronized void eraseAllUnprotected() {
int size = rows * cols;
boolean inUnprotected = false;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
if (!faIsProtected(ea.fa & 0xFF)) {
inUnprotected = true;
// Clear modified bit
ea.fa = (byte) (ea.fa & ~FA_MODIFY);
} else {
inUnprotected = false;
}
} else if (inUnprotected) {
ea.ec = 0;
ea.ucs4 = 0;
ea.cs = 0;
ea.fg = 0;
ea.bg = 0;
ea.gr = 0;
ea.ic = 0;
}
}
// Move cursor to first unprotected field
cursorAddress = findNextUnprotected(0);
screenChanged = true;
updateDisplaySnapshot();
}
// ========== Field attribute navigation ==========
/**
* Find the field attribute for a given buffer address.
* Returns -1 if screen is not formatted.
*/
public int findFieldAttribute(int baddr) {
if (!formatted) return -1;
int size = rows * cols;
if (size <= 0) return -1;
baddr = ((baddr % size) + size) % size;
int start = baddr;
int count = 0;
do {
if (buffer[baddr].isFieldAttribute()) {
return baddr;
}
baddr = (baddr > 0) ? baddr - 1 : size - 1;
count++;
} while (baddr != start && count < size);
return -1;
}
/**
* Get the field attribute byte for a given position.
*/
public byte getFieldAttributeAt(int baddr) {
int fa_addr = findFieldAttribute(baddr);
if (fa_addr < 0) return defaultFA.fa;
return buffer[fa_addr].fa;
}
/**
* Find the next unprotected field after the given address.
* Returns 0 if none found.
*/
public int findNextUnprotected(int baddr) {
if (!formatted) return 0;
int size = rows * cols;
if (size <= 0) return 0;
baddr = ((baddr % size) + size) % size;
int start = baddr;
int count = 0;
do {
int next = (baddr + 1) % size;
if (buffer[baddr].isFieldAttribute()
&& !faIsProtected(buffer[baddr].fa & 0xFF)
&& !buffer[next].isFieldAttribute()) {
return next;
}
baddr = next;
count++;
} while (baddr != start && count < size);
return 0;
}
/** Refresh the formatted flag by scanning for any field attributes. */
public void updateFormatted() {
formatted = false;
int size = rows * cols;
for (int i = 0; i < size; i++) {
if (buffer[i].isFieldAttribute()) {
formatted = true;
return;
}
}
}
public boolean isFormatted() { return formatted; }
public void setFormatted(boolean f) { this.formatted = f; }
// ========== Buffer address arithmetic ==========
/** Increment buffer address (wrapping). */
public int incrementAddress(int addr) {
int size = rows * cols;
if (size <= 0) return 0;
return ((addr % size) + size + 1) % size;
}
/** Decrement buffer address (wrapping). */
public int decrementAddress(int addr) {
int size = rows * cols;
if (size <= 0) return 0;
int norm = ((addr % size) + size) % size;
return (norm > 0) ? norm - 1 : size - 1;
}
/** Convert buffer address to row. */
public int addressToRow(int addr) { return addr / cols; }
/** Convert buffer address to column. */
public int addressToCol(int addr) { return addr % cols; }
/** Convert row/col to buffer address. */
public int rowColToAddress(int row, int col) { return row * cols + col; }
// ========== Change tracking ==========
public boolean isScreenChanged() { return screenChanged; }
public void clearChanged() { screenChanged = false; firstChanged = -1; lastChanged = -1; }
public void markAllChanged() { screenChanged = true; }
// ========== Setters for model reconfiguration ==========
public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
int altRows, int altCols) {
this.maxRows = maxRows;
this.maxCols = maxCols;
this.defRows = defRows;
this.defCols = defCols;
this.altRows = altRows;
this.altCols = altCols;
this.rows = defRows;
this.cols = defCols;
allocateBuffers();
updateDisplaySnapshot();
}
public void translateToUnicode() {
int size = rows * cols;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (!ea.isFieldAttribute()) {
if (ea.ec == 0) {
ea.ucs4 = 0;
} else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE
ea.ucs4 = getAplGraphic(ea.ec & 0xFF);
} else {
ea.ucs4 = translator.ebcdicToUnicode(ea.ec & 0xFF);
}
}
}
}
private char getAplGraphic(int ec) {
switch (ec) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec);
}
}
/**
* Get the default field attribute.
*/
public ExtendedAttribute getDefaultFieldAttribute() {
return defaultFA;
}
// ========== Low-level cell access for file transfer (CUT mode) ==========
/**
* Write an EBCDIC byte to a cell at the given buffer address.
* Used by CUT mode file transfer to place data into the screen buffer.
*/
public void setCell(int baddr, int ebcdicByte) {
if (baddr < 0 || baddr >= maxRows * maxCols) return;
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
buffer[baddr].ucs4 = 0; // will be set on display refresh
screenChanged = true;
}
/**
* Write an EBCDIC byte with extended attribute flags (cs=0).
* Equivalent to ctlr_add() in x3270.
*/
public void setCellWithCS(int baddr, int ebcdicByte, int cs) {
if (baddr < 0 || baddr >= maxRows * maxCols) return;
buffer[baddr].ec = (byte) (ebcdicByte & 0xFF);
buffer[baddr].cs = (byte) (cs & 0xFF);
buffer[baddr].ucs4 = 0;
screenChanged = true;
}
/**
* Modify the field attribute byte at a given buffer address.
* Used by CUT mode to hide data fields during transfer.
*/
public void setCellFA(int baddr, byte fa) {
if (baddr < 0 || baddr >= maxRows * maxCols) return;
buffer[baddr].fa = fa;
buffer[baddr].ic = 1; // mark as field attribute
formatted = true;
screenChanged = true;
}
/**
* Get just the EBCDIC code byte from a cell.
* Convenience method for file transfer data extraction.
*/
public int getCellEC(int baddr) {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].ec & 0xFF;
}
/**
* Get the field attribute byte at a cell (the fa byte, not the ic field).
* For CUT mode checking of field attribute modifications.
*/
public byte getCellFAByte(int baddr) {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].fa;
}
}
@@ -0,0 +1,187 @@
package haus.nightmare.lib3270j.telnet;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import java.io.*;
import java.net.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
/**
* Manages the raw TCP socket connection to a TN3270 host.
* Handles connect/disconnect, raw byte I/O, and spawns a reader thread.
*/
public class TelnetConnection {
private static final Logger log = Logger.getLogger(TelnetConnection.class.getName());
private static final int READ_BUFFER_SIZE = 32768;
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private Thread readerThread;
private volatile boolean running;
private final TelnetFSM fsm;
private final ConnectionConfig config;
private javax.net.ssl.SSLSession sslSession;
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
this.config = config;
this.fsm = fsm;
}
public javax.net.ssl.SSLSession getSslSession() {
return sslSession;
}
/**
* Connect to the host. Blocks until connection is established or fails.
*/
public void connect() throws IOException {
if (config.isUseTls()) {
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
" (verifyCert=" + config.isTlsVerifyCert() + ")");
try {
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
sslSocket.setKeepAlive(true);
sslSocket.setTcpNoDelay(true);
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
sslSocket.startHandshake();
socket = sslSocket;
sslSession = sslSocket.getSession();
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
" cipher=" + sslSession.getCipherSuite());
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException("TLS setup failure: " + e.getMessage(), e);
}
} else {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
}
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
outputStream = new BufferedOutputStream(socket.getOutputStream());
log.info("Connected to " + socket.getRemoteSocketAddress());
running = true;
readerThread = new Thread(this::readLoop, "TN3270-Reader");
readerThread.setDaemon(true);
readerThread.start();
}
/**
* Send raw bytes to the host.
*/
public synchronized void sendRaw(byte[] data) throws IOException {
sendRaw(data, 0, data.length);
}
/**
* Send raw bytes to the host with offset and length.
*/
public synchronized void sendRaw(byte[] data, int offset, int length) throws IOException {
if (outputStream == null) return;
outputStream.write(data, offset, length);
outputStream.flush();
if (log.isLoggable(Level.FINE)) {
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
}
}
/**
* Disconnect from the host.
*/
public void disconnect() {
running = false;
try {
if (socket != null && !socket.isClosed()) {
socket.shutdownInput();
socket.shutdownOutput();
socket.close();
}
} catch (IOException e) {
log.log(Level.FINE, "Error during disconnect", e);
}
socket = null;
inputStream = null;
outputStream = null;
log.info("Disconnected");
}
/**
* Check if the socket is connected.
*/
public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed();
}
/**
* Main reader loop. Reads from socket and feeds bytes to the telnet FSM.
*/
private void readLoop() {
byte[] buf = new byte[READ_BUFFER_SIZE];
try {
while (running && isConnected()) {
int n = inputStream.read(buf);
if (n < 0) {
log.info("Host disconnected (EOF)");
fsm.onDisconnect();
break;
}
if (n > 0) {
if (log.isLoggable(Level.FINE)) {
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
}
try {
fsm.feedBytes(buf, 0, n);
fsm.endOfNetworkData();
} catch (Throwable t) {
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
}
}
}
} catch (SocketException e) {
if (running) {
log.info("Socket closed: " + e.getMessage());
fsm.onDisconnect();
}
} catch (IOException e) {
if (running) {
log.log(Level.WARNING, "Read error", e);
fsm.onError("Read error: " + e.getMessage());
}
} catch (Throwable t) {
if (running) {
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
fsm.onError("Network loop error: " + t.getMessage());
}
}
}
/** Format bytes as hex string for logging. */
static String formatHex(byte[] data, int offset, int length) {
StringBuilder sb = new StringBuilder(length * 3);
for (int i = 0; i < length && i < 128; i++) {
if (i > 0) sb.append(' ');
sb.append(String.format("%02x", data[offset + i] & 0xFF));
}
if (length > 128) sb.append("...");
return sb.toString();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
package haus.nightmare.lib3270j.tls;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* Callback interface for validating TLS server certificates.
* Used when standard certificate path validation fails or when custom verification is needed.
*/
@FunctionalInterface
public interface TlsCertificateVerifier {
/**
* Determine whether to trust an unverified server certificate chain.
*
* @param chain The peer certificate chain presented by the server.
* @param authType The key exchange algorithm (e.g., "RSA", "ECDHE_RSA").
* @param exception The CertificateException thrown by standard validation (or null if called proactively).
* @return true to trust the certificate and proceed with the connection; false to abort.
*/
boolean shouldTrust(X509Certificate[] chain, String authType, CertificateException exception);
}
@@ -0,0 +1,114 @@
package haus.nightmare.lib3270j.tls;
import haus.nightmare.lib3270j.ConnectionConfig;
import javax.net.ssl.*;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Custom X509TrustManager that supports:
* 1. Standard certificate verification using the JVM default TrustManager.
* 2. Unverified / trust-all mode when tlsVerifyCert is false.
* 3. Interactive/custom certificate verifier callbacks (e.g. GUI prompts for self-signed certificates).
*/
public class TlsTrustManager implements X509TrustManager {
private static final Logger log = Logger.getLogger(TlsTrustManager.class.getName());
private final ConnectionConfig config;
private X509TrustManager defaultTrustManager;
public TlsTrustManager(ConnectionConfig config) {
this.config = config;
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
this.defaultTrustManager = (X509TrustManager) tm;
break;
}
}
} catch (Exception e) {
log.log(Level.WARNING, "Failed to initialize default TrustManagerFactory", e);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (defaultTrustManager != null) {
defaultTrustManager.checkClientTrusted(chain, authType);
}
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (config != null && !config.isTlsVerifyCert()) {
log.fine("Certificate verification bypassed (tlsVerifyCert=false)");
return;
}
if (chain == null || chain.length == 0) {
CertificateException ex = new CertificateException("null or zero-length certificate chain");
if (config != null && config.getCertificateVerifier() != null) {
if (config.getCertificateVerifier().shouldTrust(chain, authType, ex)) {
log.info("Server certificate accepted via TlsCertificateVerifier callback");
return;
}
}
throw ex;
}
try {
if (defaultTrustManager != null) {
defaultTrustManager.checkServerTrusted(chain, authType);
} else {
throw new CertificateException("No default X509TrustManager available");
}
} catch (Exception ex) {
CertificateException certEx = (ex instanceof CertificateException)
? (CertificateException) ex
: new CertificateException("Certificate validation failed: " + ex.getMessage(), ex);
log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx);
if (config != null && config.getCertificateVerifier() != null) {
boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx);
if (accepted) {
log.info("Server certificate accepted via TlsCertificateVerifier callback");
return;
} else {
log.warning("Server certificate rejected by TlsCertificateVerifier callback");
throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx);
}
}
throw certEx;
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
if (defaultTrustManager != null) {
return defaultTrustManager.getAcceptedIssuers();
}
return new X509Certificate[0];
}
/**
* Create an initialized SSLContext configured for the given ConnectionConfig.
*/
public static SSLContext createSSLContext(ConnectionConfig config) throws Exception {
String protocol = (config != null && config.getSslProtocol() != null)
? config.getSslProtocol()
: "TLS";
SSLContext sslContext = SSLContext.getInstance(protocol);
TlsTrustManager trustManager = new TlsTrustManager(config);
sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
return sslContext;
}
}