cleaning and adding updates

This commit is contained in:
2026-08-20 17:24:55 -04:00
parent 4ac3c6fa69
commit 80166c477b
70 changed files with 471 additions and 284 deletions
+1
View File
@@ -1,3 +1,4 @@
.DS_Store
*.jar
build
.gradle
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
@@ -1,2 +0,0 @@
#Wed Mar 18 14:12:38 EDT 2026
gradle.version=8.5
View File
+1
View File
@@ -0,0 +1 @@
IND$FILE no longer works in CMS or TSO
+12 -2
View File
@@ -1,5 +1,12 @@
plugins {
id 'java'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22'
}
}
allprojects {
@@ -7,11 +14,13 @@ allprojects {
version = '0.1.0'
repositories {
google()
mavenCentral()
}
}
subprojects {
if (name != 'a3270') {
apply plugin: 'java'
java {
@@ -27,3 +36,4 @@ subprojects {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
set -e
echo "=== Building j3270 (Desktop JAR) and a3270 (Android APK) ==="
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem"
export ANDROID_HOME="/Users/rudi/Library/Android/sdk"
GRADLE_BIN="/Users/rudi/.gradle/wrapper/dists/gradle-8.5-bin/5t9huq95ubn472n8rpzujfbqh/gradle-8.5/bin/gradle"
BUILD_DIR="$(pwd)/build"
mkdir -p "$BUILD_DIR"
echo "Building Android APK (a3270)..."
"$GRADLE_BIN" :a3270:assembleDebug
echo "Building Desktop App JAR (j3270)..."
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270"
"$JAVA_HOME/bin/javac" -d "$BUILD_DIR/lib3270j" $(find lib3270j/src -name "*.java")
"$JAVA_HOME/bin/javac" -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270" $(find j3270/src -name "*.java")
echo "Main-Class: org.pubvm.j3270.J3270App" > "$BUILD_DIR/MANIFEST.MF"
echo "" >> "$BUILD_DIR/MANIFEST.MF"
"$JAVA_HOME/bin/jar" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
-C "$BUILD_DIR/j3270" .
# Copy APK to build/
if [ -f "a3270/build/outputs/apk/debug/a3270-debug.apk" ]; then
cp "a3270/build/outputs/apk/debug/a3270-debug.apk" "$BUILD_DIR/a3270.apk"
echo "Copied a3270-debug.apk to $BUILD_DIR/a3270.apk"
fi
echo "=== Build Complete ==="
echo "Artifacts in $BUILD_DIR:"
ls -lh "$BUILD_DIR"/*.jar "$BUILD_DIR"/*.apk 2>/dev/null || true
+2
View File
@@ -0,0 +1,2 @@
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
@@ -350,12 +350,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
@Override
public void onScreenUpdated() {
SwingUtilities.invokeLater(() -> {
if (client != null) {
client.getInputProcessor().setKeyboardLocked(false);
}
// During an active file transfer, let the CUT/DFT handler drive
// keyboard state. In x3270, ft_cut_data() runs before WCC
// keyboard-restore is applied — the keyboard stays locked for the
// entire CUT transfer.
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint();
statusBar.updateStatus();
});
@@ -22,6 +22,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private static final Logger log = Logger.getLogger(FileTransfer.class.getName());
public enum FTMode {
UNKNOWN, CUT, DFT
}
public interface FileTransferCallback {
void onTransferStarted();
void onTransferRunning();
@@ -36,6 +40,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private FTConfig currentConfig;
private File localFile;
private FTState state = FTState.NONE;
private FTMode activeMode = FTMode.UNKNOWN;
private FTCut cutHandler;
private FTDft dftHandler;
@@ -64,6 +69,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.currentConfig = config;
this.localFile = new File(config.getLocalFilename());
this.activeMode = FTMode.UNKNOWN;
// Check overwrite
if (config.isReceive() && !config.isAppend() && !config.isOverwrite()) {
@@ -83,14 +89,6 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
client.getDataStreamProcessor().setFTDft(dftHandler);
}
try {
cutHandler.initTransfer(localFile);
dftHandler.initTransfer(localFile);
} catch (IOException e) {
cleanupHandlers(false);
return "Failed to open local file: " + e.getMessage();
}
// Build and type the IND$FILE command
String command = config.buildCommand();
log.info("Starting IND$FILE transfer with command: " + command);
@@ -130,9 +128,19 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
}
}
/** Check whether a file transfer is currently active. */
public boolean isTransferActive() {
return state != FTState.NONE;
}
public FTMode getActiveMode() {
return activeMode;
}
/** Must be called after every screen update to drive CUT mode. */
public void onScreenUpdated() {
if (state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT) {
if ((activeMode == FTMode.CUT || activeMode == FTMode.UNKNOWN) &&
(state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT)) {
cutHandler.processScreenUpdate();
}
}
@@ -179,6 +187,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
boolean success = (errorMessage == null);
cleanupHandlers(success);
setState(FTState.NONE);
activeMode = FTMode.UNKNOWN;
currentConfig = null;
}
@@ -186,6 +195,32 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
@Override
public void onTransferRunning() {
// Mode detection check
StackTraceElement[] st = Thread.currentThread().getStackTrace();
boolean isDft = false;
for (StackTraceElement elem : st) {
if (elem.getClassName().contains("FTDft")) {
isDft = true;
break;
}
}
if (activeMode == FTMode.UNKNOWN) {
activeMode = isDft ? FTMode.DFT : FTMode.CUT;
log.info("FT mode established: " + activeMode);
try {
if (activeMode == FTMode.DFT) {
dftHandler.initTransfer(localFile);
} else {
cutHandler.initTransfer(localFile);
}
} catch (IOException e) {
log.warning("Failed to open local file for " + activeMode + ": " + e.getMessage());
onTransferAborted("Failed to open local file: " + e.getMessage());
return;
}
}
cancelTimeout();
setState(FTState.RUNNING);
SwingUtilities.invokeLater(callback::onTransferRunning);
@@ -193,7 +228,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
@Override
public void onTransferComplete(String errorMessage) {
completeTransfer(null);
completeTransfer(errorMessage);
SwingUtilities.invokeLater(() -> {
if (errorMessage == null) {
callback.onTransferComplete("Transfer complete.");
@@ -51,6 +51,7 @@ public class Telnet3270Client {
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor);
}
/**
@@ -34,6 +34,9 @@ public class DataStreamProcessor {
// File Transfer DFT handler
private org.lib3270j.ft.FTDft ftDft;
// Input processor reference to manage keyboard locking state
private org.lib3270j.input.InputProcessor inputProcessor;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
public interface OutputSender {
@@ -56,6 +59,10 @@ public class DataStreamProcessor {
this.ftDft = ftDft;
}
public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) {
this.inputProcessor = inputProcessor;
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
}
@@ -231,6 +238,12 @@ public class DataStreamProcessor {
log.fine("WCC: " + String.format("0x%02x", wcc) +
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
if (kbdRestore || inputProcessor != null) {
if (inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
}
if (resetMdt) {
resetAllMDT();
}
@@ -471,6 +484,10 @@ public class DataStreamProcessor {
if (!faIsProtected(faVal & 0xFF)) {
ea.ec = 0;
ea.ucs4 = 0;
ea.fg = 0;
ea.bg = 0;
ea.gr = 0;
ea.cs = 0;
}
}
baddr = (baddr + 1) % size;
@@ -614,6 +631,10 @@ public class DataStreamProcessor {
// ========== Read Modified ==========
private void processReadModified(boolean all) {
if (ftDft != null) {
ftDft.readModified();
}
outputPos = 0;
int aid = AID_NO; // Last AID
@@ -183,6 +183,12 @@ public final class FTConstants {
* 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, org.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];
@@ -193,6 +199,12 @@ public final class FTConstants {
/**
* Encode a 6-bit value into a table6-encoded EBCDIC character.
*/
public static int to6(int value, org.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];
+99 -127
View File
@@ -7,6 +7,7 @@ import static org.lib3270j.ft.FTConstants.*;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
@@ -49,10 +50,10 @@ public class FTCut {
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
private static final int SELECTOR_0 = 0x5E; // ';'
private static final int SELECTOR_1 = 0x7E; // '='
private static final int SELECTOR_2 = 0x5C; // '*'
private static final int SELECTOR_3 = 0x7D; // '\''
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,
@@ -110,6 +111,55 @@ public class FTCut {
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.
@@ -120,7 +170,12 @@ public class FTCut {
// 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 or intermediate screen), ignore
// 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;
}
@@ -149,42 +204,6 @@ public class FTCut {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
}
/**
* Initialize CUT mode for a new transfer.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
xferInProgress = false;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
lastCr = false;
if (config.isReceive()) {
boolean append = config.isAppend();
outputStream = new FileOutputStream(localFile, append);
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
/**
* 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());
}
}
// ========== Control Code Processing ==========
private void cutControlCode() {
@@ -218,7 +237,6 @@ public class FTCut {
xferInProgress = false;
cutAck();
// Extract error message from the host (positions 4-83)
String msg = extractHostMessage();
listener.onTransferAborted(msg);
break;
@@ -242,7 +260,6 @@ public class FTCut {
char ch = translator.ebcdicToUnicode(ebc);
sb.append(ch);
}
// Trim trailing spaces and '$'
String msg = sb.toString().stripTrailing();
if (msg.endsWith("$")) {
msg = msg.substring(0, msg.length() - 1).stripTrailing();
@@ -252,12 +269,10 @@ public class FTCut {
// ========== Data Request (Upload) ==========
/**
* Process a data request from the host (upload: send data to host).
*/
private void cutDataRequest() {
int seq = screen.getCellEC(O_DR_FRAME_SEQ);
log.fine("CUT: DATA_REQUEST seq=" + from6(seq));
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);
@@ -266,7 +281,6 @@ public class FTCut {
FTConfig config = listener.getConfig();
// Read data from local file into screen buffer
int count = 0;
try {
while (count < O_UP_MAX && !cutEof) {
@@ -284,32 +298,28 @@ public class FTCut {
return;
}
// EOF with no data send EOF marker
if (count == 0 && cutEof) {
screen.setCell(O_UP_DATA, EOF_DATA1);
screen.setCell(O_UP_DATA + 1, EOF_DATA2);
count = 2;
}
// Compute frame fields
screen.setCell(O_UP_FRAME_SEQ, seq);
screen.setCell(O_UP_FRAME_SEQ, seqEbc);
// Checksum
int cs = 0;
for (int i = 0; i < count; i++) {
cs ^= screen.getCellEC(O_UP_DATA + i);
}
screen.setCell(O_UP_CSUM, to6(cs));
screen.setCell(O_UP_LEN, to6((count >> 6) & 0x3F));
screen.setCell(O_UP_LEN + 1, to6(count & 0x3F));
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 the data field (change SF attribute to zero intensity)
// 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);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL | FA_MODIFY);
screen.setCellFA(O_DR_SF, attr);
// Send it
log.fine("CUT: > DATA seq=" + from6(seq) + " len=" + count);
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
@@ -317,9 +327,6 @@ public class FTCut {
// ========== Data (Download) ==========
/**
* Process data from the host (download: receive data from host).
*/
private void cutData() {
log.fine("CUT: DATA");
@@ -330,9 +337,8 @@ public class FTCut {
FTConfig config = listener.getConfig();
// Extract raw data
int rawLength = (from6(screen.getCellEC(O_DT_LEN)) << 6) |
from6(screen.getCellEC(O_DT_LEN + 1));
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);
@@ -344,7 +350,6 @@ public class FTCut {
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
}
// Check for EOF marker
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
(rawData[1] & 0xFF) == EOF_DATA2) {
log.fine("CUT: EOF marker received");
@@ -352,7 +357,6 @@ public class FTCut {
return;
}
// Convert and write to local file
try {
byte[] converted = convertDownload(rawData, rawLength, config);
if (outputStream != null) {
@@ -386,31 +390,20 @@ public class FTCut {
private void cutAbort(String message, int reason) {
log.warning("CUT: ABORT — " + message);
// Write abort frame to the response area
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);
// Send PF2 to signal abort
input.sendAidForFT(AID_PF2);
listener.onTransferAborted(message);
}
// ========== Character Translation ==========
/**
* Convert received EBCDIC data to local format.
* Handles ASCII mode (with optional CR processing and remapping)
* and binary mode (raw passthrough).
*
* The quadrant tables decode wire bytes into IND$FILE's "pseudo-ASCII"
* representation. For the remap path, control codes (< 0x20 and
* 0x80-0x9E) are treated as direct Unicode codepoints, while printable
* characters are re-mapped through ASC2FT (inverting IND$FILE's built-in
* EBCDICASCII table) back to real EBCDIC, then to Unicode.
* This matches x3270's upload_convert() logic.
* Convert received CUT EBCDIC data to local format.
* Matching x3270 upload_convert logic.
*/
private byte[] convertDownload(byte[] rawData, int length, FTConfig config)
throws IOException {
@@ -421,81 +414,59 @@ public class FTCut {
while (true) {
if (quadrant < 0) {
// Find the quadrant
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; // continue outer loop
break;
}
if (c < 0x40 || c > 0xF9) {
throw new IOException("CUT conversion error (data out of bounds)");
}
char asciiChar = translator.ebcdicToUnicode(c);
char asciiChar = (char) FT2ASC[c & 0xFF];
int ix = ALPHAS.indexOf(asciiChar);
if (ix < 0) {
// Try a different quadrant
quadrant = -1;
continue; // retry loop
continue;
}
if (quadrant != 2 && c != 0xC1 && QUADS[quadrant][ix] == 0) {
// Try a different quadrant
if (!(quadrant == 2 && c == 0xC1) && QUADS[quadrant][ix] == 0) {
quadrant = -1;
continue; // retry
continue;
}
// Map the character this produces IND$FILE's pseudo-ASCII byte
int decoded = QUADS[quadrant][ix];
if (config.isAscii() && config.isCrFlag() && (decoded == 0x0D || decoded == 0x1A)) {
break; // Ignore bare CR and EOF markers in ASCII stream
break;
}
if (!config.isAscii() || !config.isRemapFlag()) {
// Binary or ASCII-no-remap: emit the decoded byte as-is
out.write(decoded);
break;
}
/*
* ASCII with remap: invert IND$FILE's EBCDICASCII translation.
*
* Control codes (< 0x20) and high control range (0x80-0x9E,
* except 0x9F) are treated as direct Unicode codepoints.
* IND$FILE maps EBCDIC 0xE1 to pseudo-ASCII 0x9F, so
* 0xFF is the special marker that maps back to U+009F.
* Everything else is a printable character that needs
* the ASC2FTebcdicToUnicode remap chain.
*/
if (decoded < 0x20 || (decoded >= 0x80 && decoded < 0xA0 && decoded != 0x9F)) {
// Control code emit as its Unicode codepoint directly
out.write(String.valueOf((char) decoded).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
out.write(String.valueOf((char) decoded).getBytes(StandardCharsets.UTF_8));
} else if (decoded == 0xFF) {
// Special case: 0xFF U+009F
out.write(String.valueOf((char) 0x9F).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
out.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
} else {
// Displayable character: invert IND$FILE's table
int ebc = FTConstants.ASC2FT[decoded & 0xFF];
int ebc = ASC2FT[decoded & 0xFF];
char unicodeChar = translator.ebcdicToUnicode(ebc);
out.write(String.valueOf(unicodeChar).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
out.write(String.valueOf(unicodeChar).getBytes(StandardCharsets.UTF_8));
}
break;
} // end while(true) chunk
}
}
return out.toByteArray();
}
private int xlateGetc(FTConfig config) throws IOException {
// Return buffered data first
if (xlateBuffered > 0) {
int r = xlateBuf[xlateBufIx++];
xlateBuffered--;
@@ -509,7 +480,7 @@ public class FTCut {
int localByte = c & 0xFF;
int nc = 0;
int[] cbuf = new int[4]; // max 4 bytes (2 for \r + 2 for \n if quadrant encoded)
int[] cbuf = new int[4];
if (config.isAscii()) {
if (config.isCrFlag() && !lastCr && localByte == '\n') {
@@ -532,7 +503,6 @@ public class FTCut {
private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) {
int ebc;
if (localByte == 0) {
// Nulls are special in the 'OTHER_2' quadrant
if (quadrant != 2) {
quadrant = 2; // OTHER_2
cbuf[offset] = SELECTORS[quadrant];
@@ -545,25 +515,25 @@ public class FTCut {
}
if (!config.isAscii() || !config.isRemapFlag()) {
// Binary or Ascii without remap: treat byte directly
ebc = localByte & 0xFF;
} else {
// Ascii with remap: locally read ASCII translates to actual EBCDIC, then maps to IND$FILE's pseudo-ASCII
int standardEbc = translator.unicodeToEbcdic((char) localByte);
if (standardEbc < 0) {
standardEbc = 0x40; // Space as fallback
standardEbc = 0x40;
}
ebc = org.lib3270j.ft.FTConstants.FT2ASC[standardEbc & 0xFF];
ebc = FTConstants.FT2ASC[standardEbc & 0xFF];
}
return storeUpload(ebc, cbuf, offset);
}
private int storeUpload(int ebc, int[] obBuf, int offset) {
private int storeUpload(int pseudoAsciiByte, int[] obBuf, int offset) {
if (quadrant >= 0) {
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == ebc) {
obBuf[offset] = translator.unicodeToEbcdic(ALPHAS.charAt(i));
if (QUADS[quadrant][i] == pseudoAsciiByte) {
char ch = ALPHAS.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
obBuf[offset] = ebc >= 0 ? ebc : 0x40;
return 1;
}
}
@@ -572,16 +542,18 @@ public class FTCut {
for (quadrant = 0; quadrant < 4; quadrant++) {
if (quadrant == oq) continue;
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == ebc) {
if (QUADS[quadrant][i] == pseudoAsciiByte) {
char ch = ALPHAS.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
obBuf[offset] = SELECTORS[quadrant];
obBuf[offset + 1] = translator.unicodeToEbcdic(ALPHAS.charAt(i));
obBuf[offset + 1] = ebc >= 0 ? ebc : 0x40;
return 2;
}
}
}
quadrant = -1;
// Fallback safety measure
obBuf[offset] = translator.unicodeToEbcdic('?');
int questionEbc = translator.unicodeToEbcdic('?');
obBuf[offset] = questionEbc >= 0 ? questionEbc : 0x6F;
return 1;
}
}
+155 -111
View File
@@ -5,6 +5,7 @@ import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
@@ -17,6 +18,9 @@ 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 onTransferRunning();
@@ -34,7 +38,7 @@ public class FTDft {
private final FTDftListener listener;
// DFT state
private long recnum = 0;
private long recnum = 1;
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
@@ -56,17 +60,20 @@ public class FTDft {
}
/**
* Initialize DFT mode for a new transfer.
* 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();
recnum = 0;
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
resetState();
if (config.isReceive()) {
outputStream = new FileOutputStream(localFile, config.isAppend());
@@ -77,6 +84,16 @@ public class FTDft {
}
}
private void resetState() {
recnum = 1;
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
}
/**
* Clean up DFT mode resources.
*/
@@ -115,7 +132,7 @@ public class FTDft {
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest();
dftOpenRequest(data, offset, length);
break;
case TR_INSERT_REQ:
dftInsertRequest(data, payloadStart, offset + length - payloadStart);
@@ -142,11 +159,53 @@ public class FTDft {
// ========== Open Request ==========
private void dftOpenRequest() {
log.fine("DFT: Open");
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.onTransferRunning();
// Send acknowledgement
dftDataAck();
}
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) ==========
@@ -159,12 +218,11 @@ public class FTDft {
private void dftDataInsert(byte[] data, int offset, int length) {
FTConfig config = listener.getConfig();
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
dftAbort("Transfer cancelled by user", TR_DATA_INSERT);
return;
}
// Parse the SF payload to find data
// Skip the 2-byte request code
int pos = offset + 2;
int end = offset + length;
@@ -174,13 +232,19 @@ public class FTDft {
int headerCode = data[pos] & 0xFF;
if (headerCode == TR_BEGIN_DATA) {
// Next 2 bytes are data length (including the 3-byte header)
if (pos + 3 > end) break;
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
int actualDataLen = dataLen - 3; // subtract header
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;
@@ -192,74 +256,84 @@ public class FTDft {
}
pos += Math.max(0, actualDataLen);
} else if (pos + 1 < end) {
// Skip other headers (2-byte code + length-based skip)
// Most headers are 4-6 bytes, but we just skip based on known patterns
int hdrCode16 = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
if (hdrCode16 == TR_RECNUM_HDR) {
pos += 6; // 2-byte code + 4-byte record number
pos += 6;
} else if (hdrCode16 == TR_NOT_COMPRESSED) {
pos += 2;
} else {
pos += 2; // skip unknown 2-byte header
pos += 2;
}
} else {
pos++;
}
}
// Send acknowledgement
// 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);
if (msg.startsWith(END_TRANSFER)) {
listener.onTransferComplete(null);
} else if (listener.getCurrentState() == FTState.ABORT_SENT) {
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
} else {
listener.onTransferComplete(msg);
}
}
/**
* 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()) {
// Binary: write raw
outputStream.write(data, offset, length);
return;
}
// ASCII mode with optional CR stripping and remapping
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (config.isRemapFlag()) {
// Use IND$FILE's EBCDICASCII table
int ascii = FT2ASC[b];
if (config.isCrFlag() && (b == '\r' || b == 0x1A)) {
continue; // Strip CR and EOF ^Z
}
if (config.isCrFlag()) {
if (ascii == '\r') {
lastCr = true;
if (!config.isRemapFlag()) {
outputStream.write(b);
continue;
}
if (lastCr) {
lastCr = false;
if (ascii == '\n') {
outputStream.write('\n');
continue;
}
outputStream.write('\r');
}
}
outputStream.write(ascii);
/*
* 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 {
// Standard EBCDICUnicode
char ch = translator.ebcdicToUnicode(b);
if (config.isCrFlag()) {
if (ch == '\r') { lastCr = true; continue; }
if (lastCr) {
lastCr = false;
if (ch == '\n') { outputStream.write('\n'); continue; }
outputStream.write('\r');
}
}
outputStream.write(String.valueOf(ch).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
int ebc = ASC2FT[b & 0xFF];
char ch = translator.ebcdicToUnicode(ebc);
outputStream.write(String.valueOf(ch).getBytes(StandardCharsets.UTF_8));
}
}
}
@@ -276,7 +350,7 @@ public class FTDft {
}
int bufferSize = config.getDftBufferSize();
int numbytes = bufferSize - 27; // reserve space for headers
int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
@@ -290,7 +364,6 @@ public class FTDft {
}
readBuf[totalRead++] = (byte) b;
} else {
// Binary read
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
@@ -305,25 +378,20 @@ public class FTDft {
return;
}
// Build SF response
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF);
out.write(AID_SF); // AID byte
// Placeholder for SF length (2 bytes, filled in later)
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA); // SF type
out.write(SF_TRANSFER_DATA);
if (totalRead > 0) {
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
// TR_GET_REPLY
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
// Record number header
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
@@ -332,56 +400,47 @@ public class FTDft {
out.write((int) (recnum & 0xFF));
recnum++;
// Not compressed
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
out.write(TR_NOT_COMPRESSED & 0xFF);
// Begin data
out.write(TR_BEGIN_DATA);
int dataFieldLen = totalRead + 5;
out.write((dataFieldLen >> 8) & 0xFF);
out.write(dataFieldLen & 0xFF);
// The actual data
out.write(readBuf, 0, totalRead);
bytesTransferred += totalRead;
} else {
log.fine("DFT: > GetReply EOF");
// EOF reply
out.write((TR_GET_REQ >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
// Error header
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
// EOF error code
out.write((TR_ERR_EOF >> 8) & 0xFF);
out.write(TR_ERR_EOF & 0xFF);
dftEof = true;
}
// Set the SF length
byte[] result = out.toByteArray();
int sfLen = result.length - 1; // exclude AID byte
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
// Save for potential Read Modified retransmit
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
// Send it
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
}
/**
* Read a byte from the local file in ASCII mode with CR expansion and remapping.
* Returns -1 for EOF.
* 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;
@@ -389,30 +448,35 @@ public class FTDft {
int c = inputStream.read();
if (c == -1) return -1;
if (config.isRemapFlag()) {
// CR expansion: insert \r before \n
if (config.isCrFlag() && !lastCr && c == '\n') {
// We need to return \r now and buffer \n
lastCr = false;
// Can't easily buffer here, so return \r and push \n back
// Actually, let's handle this inline
// Return the CR equivalent, then handle NL next call
return ASC2FT['\r' & 0xFF];
// Note: the \n will be read on next call naturally
}
lastCr = (c == '\r');
return ASC2FT[c & 0xFF];
} else {
// No remap convert to standard EBCDIC
if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false;
int ebc = translator.unicodeToEbcdic('\r');
return ebc >= 0 ? ebc : 0x0D;
// 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 ==========
@@ -420,16 +484,10 @@ public class FTDft {
private void dftCloseRequest() {
log.fine("DFT: Close");
// Send close acknowledgement
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
// SF length
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
// TR_CLOSE_REPLY
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
out.write(TR_CLOSE_REPLY & 0xFF);
@@ -441,13 +499,8 @@ public class FTDft {
private void dftDataAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
// SF length
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
// TR_NORMAL_REPLY
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
out.write(TR_NORMAL_REPLY & 0xFF);
@@ -461,21 +514,12 @@ public class FTDft {
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
// SF length
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
// Error reply code
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
// Error header
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
// Command failed
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
@@ -30,8 +30,46 @@ public class InputProcessor {
this.fsm = fsm;
}
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) { this.keyboardLocked = locked; }
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; }
@@ -115,7 +153,7 @@ public class InputProcessor {
if (keyboardLocked && aidCode != AID_CLEAR) return;
lastAid = aidCode;
keyboardLocked = true;
setKeyboardLocked(true);
if (aidCode == AID_CLEAR) {
screen.clear();
@@ -149,8 +187,6 @@ public class InputProcessor {
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
if (faIsProtected(ea.fa & 0xFF)) continue;
int fieldStart = (i + 1) % size;
// First, collect field data and find last non-null byte
@@ -169,14 +205,14 @@ public class InputProcessor {
if (pos == fieldStart) break;
}
// Only send if there's actual data (strip trailing nulls)
if (lastNonNull >= 0) {
// Always send SBA and address
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
// Write only up to the last non-null byte
// Only send data if there is any (strip trailing nulls)
if (lastNonNull >= 0) {
byte[] allData = fieldData.toByteArray();
out.write(allData, 0, lastNonNull + 1);
}
@@ -260,6 +296,17 @@ public class InputProcessor {
}
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();
return;
}
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
byte faVal = screen.getFieldAttributeAt(addr);
@@ -282,6 +329,7 @@ public class InputProcessor {
}
public void deleteChar() {
if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
@@ -316,7 +364,7 @@ public class InputProcessor {
/** Reset (unlock keyboard, cancel insert mode). */
public void reset() {
keyboardLocked = false;
setKeyboardLocked(false);
insertMode = false;
}
+1
View File
@@ -2,3 +2,4 @@ rootProject.name = 'j3270-project'
include 'lib3270j'
include 'j3270'
include 'a3270'