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 .DS_Store
*.jar *.jar
build 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 { buildscript {
id 'java' repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22'
}
} }
allprojects { allprojects {
@@ -7,11 +14,13 @@ allprojects {
version = '0.1.0' version = '0.1.0'
repositories { repositories {
google()
mavenCentral() mavenCentral()
} }
} }
subprojects { subprojects {
if (name != 'a3270') {
apply plugin: 'java' apply plugin: 'java'
java { java {
@@ -27,3 +36,4 @@ subprojects {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' 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 @Override
public void onScreenUpdated() { public void onScreenUpdated() {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
if (client != null) { // During an active file transfer, let the CUT/DFT handler drive
client.getInputProcessor().setKeyboardLocked(false); // 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) { if (fileTransfer != null) {
fileTransfer.onScreenUpdated(); fileTransfer.onScreenUpdated();
} }
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint(); terminalPanel.repaint();
statusBar.updateStatus(); statusBar.updateStatus();
}); });
@@ -22,6 +22,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private static final Logger log = Logger.getLogger(FileTransfer.class.getName()); private static final Logger log = Logger.getLogger(FileTransfer.class.getName());
public enum FTMode {
UNKNOWN, CUT, DFT
}
public interface FileTransferCallback { public interface FileTransferCallback {
void onTransferStarted(); void onTransferStarted();
void onTransferRunning(); void onTransferRunning();
@@ -36,6 +40,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private FTConfig currentConfig; private FTConfig currentConfig;
private File localFile; private File localFile;
private FTState state = FTState.NONE; private FTState state = FTState.NONE;
private FTMode activeMode = FTMode.UNKNOWN;
private FTCut cutHandler; private FTCut cutHandler;
private FTDft dftHandler; private FTDft dftHandler;
@@ -64,6 +69,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.currentConfig = config; this.currentConfig = config;
this.localFile = new File(config.getLocalFilename()); this.localFile = new File(config.getLocalFilename());
this.activeMode = FTMode.UNKNOWN;
// Check overwrite // Check overwrite
if (config.isReceive() && !config.isAppend() && !config.isOverwrite()) { if (config.isReceive() && !config.isAppend() && !config.isOverwrite()) {
@@ -83,14 +89,6 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
client.getDataStreamProcessor().setFTDft(dftHandler); 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 // Build and type the IND$FILE command
String command = config.buildCommand(); String command = config.buildCommand();
log.info("Starting IND$FILE transfer with command: " + command); 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. */ /** Must be called after every screen update to drive CUT mode. */
public void onScreenUpdated() { 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(); cutHandler.processScreenUpdate();
} }
} }
@@ -179,6 +187,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
boolean success = (errorMessage == null); boolean success = (errorMessage == null);
cleanupHandlers(success); cleanupHandlers(success);
setState(FTState.NONE); setState(FTState.NONE);
activeMode = FTMode.UNKNOWN;
currentConfig = null; currentConfig = null;
} }
@@ -186,6 +195,32 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
@Override @Override
public void onTransferRunning() { 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(); cancelTimeout();
setState(FTState.RUNNING); setState(FTState.RUNNING);
SwingUtilities.invokeLater(callback::onTransferRunning); SwingUtilities.invokeLater(callback::onTransferRunning);
@@ -193,7 +228,7 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
@Override @Override
public void onTransferComplete(String errorMessage) { public void onTransferComplete(String errorMessage) {
completeTransfer(null); completeTransfer(errorMessage);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
if (errorMessage == null) { if (errorMessage == null) {
callback.onTransferComplete("Transfer complete."); callback.onTransferComplete("Transfer complete.");
@@ -51,6 +51,7 @@ public class Telnet3270Client {
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data); dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor);
} }
/** /**
@@ -34,6 +34,9 @@ public class DataStreamProcessor {
// File Transfer DFT handler // File Transfer DFT handler
private org.lib3270j.ft.FTDft ftDft; 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. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
@@ -56,6 +59,10 @@ public class DataStreamProcessor {
this.ftDft = ftDft; this.ftDft = ftDft;
} }
public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) {
this.inputProcessor = inputProcessor;
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l); screenListeners.add(l);
} }
@@ -231,6 +238,12 @@ public class DataStreamProcessor {
log.fine("WCC: " + String.format("0x%02x", wcc) + log.fine("WCC: " + String.format("0x%02x", wcc) +
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt); " reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
if (kbdRestore || inputProcessor != null) {
if (inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
}
if (resetMdt) { if (resetMdt) {
resetAllMDT(); resetAllMDT();
} }
@@ -471,6 +484,10 @@ public class DataStreamProcessor {
if (!faIsProtected(faVal & 0xFF)) { if (!faIsProtected(faVal & 0xFF)) {
ea.ec = 0; ea.ec = 0;
ea.ucs4 = 0; ea.ucs4 = 0;
ea.fg = 0;
ea.bg = 0;
ea.gr = 0;
ea.cs = 0;
} }
} }
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
@@ -614,6 +631,10 @@ public class DataStreamProcessor {
// ========== Read Modified ========== // ========== Read Modified ==========
private void processReadModified(boolean all) { private void processReadModified(boolean all) {
if (ftDft != null) {
ftDft.readModified();
}
outputPos = 0; outputPos = 0;
int aid = AID_NO; // Last AID int aid = AID_NO; // Last AID
@@ -183,6 +183,12 @@ public final class FTConstants {
* Decode a CUT-mode base-64 encoded integer from EBCDIC. * Decode a CUT-mode base-64 encoded integer from EBCDIC.
* Converts a table6-encoded EBCDIC character to its 6-bit value. * 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) { public static int from6(int ebcdicByte) {
// First convert EBCDIC to ASCII via IND$FILE's table // First convert EBCDIC to ASCII via IND$FILE's table
int ascii = FT2ASC[ebcdicByte & 0xFF]; int ascii = FT2ASC[ebcdicByte & 0xFF];
@@ -193,6 +199,12 @@ public final class FTConstants {
/** /**
* Encode a 6-bit value into a table6-encoded EBCDIC character. * 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) { public static int to6(int value) {
char ascii = TABLE6.charAt(value & 0x3F); char ascii = TABLE6.charAt(value & 0x3F);
return ASC2FT[ascii & 0xFF]; 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 static org.lib3270j.protocol.DS3270Constants.*;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
@@ -49,10 +50,10 @@ public class FTCut {
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?"; private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
private static final int SELECTOR_0 = 0x5E; // ';' private static final int SELECTOR_0 = 0x5E; // ';' (EBCDIC)
private static final int SELECTOR_1 = 0x7E; // '=' private static final int SELECTOR_1 = 0x7E; // '=' (EBCDIC)
private static final int SELECTOR_2 = 0x5C; // '*' private static final int SELECTOR_2 = 0x5C; // '*' (EBCDIC)
private static final int SELECTOR_3 = 0x7D; // '\'' private static final int SELECTOR_3 = 0x7D; // '\'' (EBCDIC)
private static final int[] XLATE_0 = { private static final int[] XLATE_0 = {
0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2, 0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,
@@ -110,6 +111,55 @@ public class FTCut {
this.listener = listener; 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. * Process a CUT-mode screen update.
* Called by the transfer coordinator when the screen changes during a transfer. * 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) // CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF); byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) { 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; return;
} }
@@ -149,42 +204,6 @@ public class FTCut {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0; 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 ========== // ========== Control Code Processing ==========
private void cutControlCode() { private void cutControlCode() {
@@ -218,7 +237,6 @@ public class FTCut {
xferInProgress = false; xferInProgress = false;
cutAck(); cutAck();
// Extract error message from the host (positions 4-83)
String msg = extractHostMessage(); String msg = extractHostMessage();
listener.onTransferAborted(msg); listener.onTransferAborted(msg);
break; break;
@@ -242,7 +260,6 @@ public class FTCut {
char ch = translator.ebcdicToUnicode(ebc); char ch = translator.ebcdicToUnicode(ebc);
sb.append(ch); sb.append(ch);
} }
// Trim trailing spaces and '$'
String msg = sb.toString().stripTrailing(); String msg = sb.toString().stripTrailing();
if (msg.endsWith("$")) { if (msg.endsWith("$")) {
msg = msg.substring(0, msg.length() - 1).stripTrailing(); msg = msg.substring(0, msg.length() - 1).stripTrailing();
@@ -252,12 +269,10 @@ public class FTCut {
// ========== Data Request (Upload) ========== // ========== Data Request (Upload) ==========
/**
* Process a data request from the host (upload: send data to host).
*/
private void cutDataRequest() { private void cutDataRequest() {
int seq = screen.getCellEC(O_DR_FRAME_SEQ); int seqEbc = screen.getCellEC(O_DR_FRAME_SEQ);
log.fine("CUT: DATA_REQUEST seq=" + from6(seq)); int seq = FTConstants.from6(seqEbc, translator);
log.fine("CUT: DATA_REQUEST seq=" + seq);
if (listener.getCurrentState() == FTState.ABORT_WAIT) { if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE); cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
@@ -266,7 +281,6 @@ public class FTCut {
FTConfig config = listener.getConfig(); FTConfig config = listener.getConfig();
// Read data from local file into screen buffer
int count = 0; int count = 0;
try { try {
while (count < O_UP_MAX && !cutEof) { while (count < O_UP_MAX && !cutEof) {
@@ -284,32 +298,28 @@ public class FTCut {
return; return;
} }
// EOF with no data send EOF marker
if (count == 0 && cutEof) { if (count == 0 && cutEof) {
screen.setCell(O_UP_DATA, EOF_DATA1); screen.setCell(O_UP_DATA, EOF_DATA1);
screen.setCell(O_UP_DATA + 1, EOF_DATA2); screen.setCell(O_UP_DATA + 1, EOF_DATA2);
count = 2; count = 2;
} }
// Compute frame fields screen.setCell(O_UP_FRAME_SEQ, seqEbc);
screen.setCell(O_UP_FRAME_SEQ, seq);
// Checksum
int cs = 0; int cs = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
cs ^= screen.getCellEC(O_UP_DATA + i); cs ^= screen.getCellEC(O_UP_DATA + i);
} }
screen.setCell(O_UP_CSUM, to6(cs)); screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator));
screen.setCell(O_UP_LEN, to6((count >> 6) & 0x3F)); screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
screen.setCell(O_UP_LEN + 1, to6(count & 0x3F)); 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); 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); screen.setCellFA(O_DR_SF, attr);
// Send it log.fine("CUT: > DATA seq=" + seq + " len=" + count);
log.fine("CUT: > DATA seq=" + from6(seq) + " len=" + count);
expandedLength += count; expandedLength += count;
listener.onBytesTransferred(expandedLength); listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER); input.sendAidForFT(AID_ENTER);
@@ -317,9 +327,6 @@ public class FTCut {
// ========== Data (Download) ========== // ========== Data (Download) ==========
/**
* Process data from the host (download: receive data from host).
*/
private void cutData() { private void cutData() {
log.fine("CUT: DATA"); log.fine("CUT: DATA");
@@ -330,9 +337,8 @@ public class FTCut {
FTConfig config = listener.getConfig(); FTConfig config = listener.getConfig();
// Extract raw data int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
int rawLength = (from6(screen.getCellEC(O_DT_LEN)) << 6) | FTConstants.from6(screen.getCellEC(O_DT_LEN + 1), translator);
from6(screen.getCellEC(O_DT_LEN + 1));
if (rawLength > O_RESPONSE - O_DT_DATA) { if (rawLength > O_RESPONSE - O_DT_DATA) {
cutAbort("Oversized CUT data frame", SC_ABORT_XMIT); cutAbort("Oversized CUT data frame", SC_ABORT_XMIT);
@@ -344,7 +350,6 @@ public class FTCut {
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i); rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
} }
// Check for EOF marker
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 && if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
(rawData[1] & 0xFF) == EOF_DATA2) { (rawData[1] & 0xFF) == EOF_DATA2) {
log.fine("CUT: EOF marker received"); log.fine("CUT: EOF marker received");
@@ -352,7 +357,6 @@ public class FTCut {
return; return;
} }
// Convert and write to local file
try { try {
byte[] converted = convertDownload(rawData, rawLength, config); byte[] converted = convertDownload(rawData, rawLength, config);
if (outputStream != null) { if (outputStream != null) {
@@ -386,31 +390,20 @@ public class FTCut {
private void cutAbort(String message, int reason) { private void cutAbort(String message, int reason) {
log.warning("CUT: ABORT — " + message); log.warning("CUT: ABORT — " + message);
// Write abort frame to the response area
screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE); screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE);
screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ)); screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ));
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF); screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF); screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
// Send PF2 to signal abort
input.sendAidForFT(AID_PF2); input.sendAidForFT(AID_PF2);
listener.onTransferAborted(message); listener.onTransferAborted(message);
} }
// ========== Character Translation ========== // ========== Character Translation ==========
/** /**
* Convert received EBCDIC data to local format. * Convert received CUT EBCDIC data to local format.
* Handles ASCII mode (with optional CR processing and remapping) * Matching x3270 upload_convert logic.
* 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.
*/ */
private byte[] convertDownload(byte[] rawData, int length, FTConfig config) private byte[] convertDownload(byte[] rawData, int length, FTConfig config)
throws IOException { throws IOException {
@@ -421,81 +414,59 @@ public class FTCut {
while (true) { while (true) {
if (quadrant < 0) { if (quadrant < 0) {
// Find the quadrant
for (quadrant = 0; quadrant < 4; quadrant++) { for (quadrant = 0; quadrant < 4; quadrant++) {
if (c == SELECTORS[quadrant]) break; if (c == SELECTORS[quadrant]) break;
} }
if (quadrant >= 4) { if (quadrant >= 4) {
throw new IOException("CUT conversion error (quadrant selector not found)"); throw new IOException("CUT conversion error (quadrant selector not found)");
} }
break; // continue outer loop break;
} }
if (c < 0x40 || c > 0xF9) { if (c < 0x40 || c > 0xF9) {
throw new IOException("CUT conversion error (data out of bounds)"); 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); int ix = ALPHAS.indexOf(asciiChar);
if (ix < 0) { if (ix < 0) {
// Try a different quadrant
quadrant = -1; quadrant = -1;
continue; // retry loop continue;
} }
if (quadrant != 2 && c != 0xC1 && QUADS[quadrant][ix] == 0) { if (!(quadrant == 2 && c == 0xC1) && QUADS[quadrant][ix] == 0) {
// Try a different quadrant
quadrant = -1; quadrant = -1;
continue; // retry continue;
} }
// Map the character this produces IND$FILE's pseudo-ASCII byte
int decoded = QUADS[quadrant][ix]; int decoded = QUADS[quadrant][ix];
if (config.isAscii() && config.isCrFlag() && (decoded == 0x0D || decoded == 0x1A)) { 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()) { if (!config.isAscii() || !config.isRemapFlag()) {
// Binary or ASCII-no-remap: emit the decoded byte as-is
out.write(decoded); out.write(decoded);
break; 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)) { if (decoded < 0x20 || (decoded >= 0x80 && decoded < 0xA0 && decoded != 0x9F)) {
// Control code emit as its Unicode codepoint directly out.write(String.valueOf((char) decoded).getBytes(StandardCharsets.UTF_8));
out.write(String.valueOf((char) decoded).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
} else if (decoded == 0xFF) { } else if (decoded == 0xFF) {
// Special case: 0xFF U+009F out.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
out.write(String.valueOf((char) 0x9F).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
} else { } else {
// Displayable character: invert IND$FILE's table int ebc = ASC2FT[decoded & 0xFF];
int ebc = FTConstants.ASC2FT[decoded & 0xFF];
char unicodeChar = translator.ebcdicToUnicode(ebc); char unicodeChar = translator.ebcdicToUnicode(ebc);
out.write(String.valueOf(unicodeChar).getBytes( out.write(String.valueOf(unicodeChar).getBytes(StandardCharsets.UTF_8));
java.nio.charset.StandardCharsets.UTF_8));
} }
break; break;
} // end while(true) chunk }
} }
return out.toByteArray(); return out.toByteArray();
} }
private int xlateGetc(FTConfig config) throws IOException { private int xlateGetc(FTConfig config) throws IOException {
// Return buffered data first
if (xlateBuffered > 0) { if (xlateBuffered > 0) {
int r = xlateBuf[xlateBufIx++]; int r = xlateBuf[xlateBufIx++];
xlateBuffered--; xlateBuffered--;
@@ -509,7 +480,7 @@ public class FTCut {
int localByte = c & 0xFF; int localByte = c & 0xFF;
int nc = 0; 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.isAscii()) {
if (config.isCrFlag() && !lastCr && localByte == '\n') { if (config.isCrFlag() && !lastCr && localByte == '\n') {
@@ -532,7 +503,6 @@ public class FTCut {
private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) { private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) {
int ebc; int ebc;
if (localByte == 0) { if (localByte == 0) {
// Nulls are special in the 'OTHER_2' quadrant
if (quadrant != 2) { if (quadrant != 2) {
quadrant = 2; // OTHER_2 quadrant = 2; // OTHER_2
cbuf[offset] = SELECTORS[quadrant]; cbuf[offset] = SELECTORS[quadrant];
@@ -545,25 +515,25 @@ public class FTCut {
} }
if (!config.isAscii() || !config.isRemapFlag()) { if (!config.isAscii() || !config.isRemapFlag()) {
// Binary or Ascii without remap: treat byte directly
ebc = localByte & 0xFF; ebc = localByte & 0xFF;
} else { } 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); int standardEbc = translator.unicodeToEbcdic((char) localByte);
if (standardEbc < 0) { 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); 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) { if (quadrant >= 0) {
for (int i = 0; i < 77; i++) { for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == ebc) { if (QUADS[quadrant][i] == pseudoAsciiByte) {
obBuf[offset] = translator.unicodeToEbcdic(ALPHAS.charAt(i)); char ch = ALPHAS.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
obBuf[offset] = ebc >= 0 ? ebc : 0x40;
return 1; return 1;
} }
} }
@@ -572,16 +542,18 @@ public class FTCut {
for (quadrant = 0; quadrant < 4; quadrant++) { for (quadrant = 0; quadrant < 4; quadrant++) {
if (quadrant == oq) continue; if (quadrant == oq) continue;
for (int i = 0; i < 77; i++) { 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] = SELECTORS[quadrant];
obBuf[offset + 1] = translator.unicodeToEbcdic(ALPHAS.charAt(i)); obBuf[offset + 1] = ebc >= 0 ? ebc : 0x40;
return 2; return 2;
} }
} }
} }
quadrant = -1; quadrant = -1;
// Fallback safety measure int questionEbc = translator.unicodeToEbcdic('?');
obBuf[offset] = translator.unicodeToEbcdic('?'); obBuf[offset] = questionEbc >= 0 ? questionEbc : 0x6F;
return 1; return 1;
} }
} }
+155 -111
View File
@@ -5,6 +5,7 @@ import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*; import static org.lib3270j.ft.FTConstants.*;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger; 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 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) */ /** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener { public interface FTDftListener {
void onTransferRunning(); void onTransferRunning();
@@ -34,7 +38,7 @@ public class FTDft {
private final FTDftListener listener; private final FTDftListener listener;
// DFT state // DFT state
private long recnum = 0; private long recnum = 1;
private boolean dftEof = false; private boolean dftEof = false;
private boolean messageFlag = false; private boolean messageFlag = false;
private long bytesTransferred = 0; 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 { public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig(); FTConfig config = listener.getConfig();
recnum = 0; resetState();
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
if (config.isReceive()) { if (config.isReceive()) {
outputStream = new FileOutputStream(localFile, config.isAppend()); 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. * Clean up DFT mode resources.
*/ */
@@ -115,7 +132,7 @@ public class FTDft {
switch (requestCode) { switch (requestCode) {
case TR_OPEN_REQ: case TR_OPEN_REQ:
dftOpenRequest(); dftOpenRequest(data, offset, length);
break; break;
case TR_INSERT_REQ: case TR_INSERT_REQ:
dftInsertRequest(data, payloadStart, offset + length - payloadStart); dftInsertRequest(data, payloadStart, offset + length - payloadStart);
@@ -142,11 +159,53 @@ public class FTDft {
// ========== Open Request ========== // ========== Open Request ==========
private void dftOpenRequest() { private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
log.fine("DFT: Open"); 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(); 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) ========== // ========== Insert Request (host sending data for download) ==========
@@ -159,12 +218,11 @@ public class FTDft {
private void dftDataInsert(byte[] data, int offset, int length) { private void dftDataInsert(byte[] data, int offset, int length) {
FTConfig config = listener.getConfig(); 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); dftAbort("Transfer cancelled by user", TR_DATA_INSERT);
return; return;
} }
// Parse the SF payload to find data
// Skip the 2-byte request code // Skip the 2-byte request code
int pos = offset + 2; int pos = offset + 2;
int end = offset + length; int end = offset + length;
@@ -174,13 +232,19 @@ public class FTDft {
int headerCode = data[pos] & 0xFF; int headerCode = data[pos] & 0xFF;
if (headerCode == TR_BEGIN_DATA) { if (headerCode == TR_BEGIN_DATA) {
// Next 2 bytes are data length (including the 3-byte header)
if (pos + 3 > end) break; if (pos + 3 > end) break;
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF); int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
int actualDataLen = dataLen - 3; // subtract header int actualDataLen = dataLen - 3;
pos += 3; pos += 3;
if (actualDataLen > 0 && pos + actualDataLen <= end) { if (actualDataLen > 0 && pos + actualDataLen <= end) {
if (messageFlag) {
// Handle message payload from host
dftDataAck();
handleHostMessage(data, pos, actualDataLen);
return;
}
try { try {
writeDownloadData(data, pos, actualDataLen, config); writeDownloadData(data, pos, actualDataLen, config);
bytesTransferred += actualDataLen; bytesTransferred += actualDataLen;
@@ -192,74 +256,84 @@ public class FTDft {
} }
pos += Math.max(0, actualDataLen); pos += Math.max(0, actualDataLen);
} else if (pos + 1 < end) { } 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); int hdrCode16 = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
if (hdrCode16 == TR_RECNUM_HDR) { if (hdrCode16 == TR_RECNUM_HDR) {
pos += 6; // 2-byte code + 4-byte record number pos += 6;
} else if (hdrCode16 == TR_NOT_COMPRESSED) { } else if (hdrCode16 == TR_NOT_COMPRESSED) {
pos += 2; pos += 2;
} else { } else {
pos += 2; // skip unknown 2-byte header pos += 2;
} }
} else { } else {
pos++; pos++;
} }
} }
// Send acknowledgement // Send acknowledgement for file data
dftDataAck(); 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. * Write download data to the local file, handling ASCII conversion.
* Matching x3270 upload_convert logic.
*/ */
private void writeDownloadData(byte[] data, int offset, int length, private void writeDownloadData(byte[] data, int offset, int length,
FTConfig config) throws IOException { FTConfig config) throws IOException {
if (outputStream == null) return; if (outputStream == null) return;
if (!config.isAscii()) { if (!config.isAscii()) {
// Binary: write raw
outputStream.write(data, offset, length); outputStream.write(data, offset, length);
return; return;
} }
// ASCII mode with optional CR stripping and remapping
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF; int b = data[offset + i] & 0xFF;
if (config.isRemapFlag()) { if (config.isCrFlag() && (b == '\r' || b == 0x1A)) {
// Use IND$FILE's EBCDICASCII table continue; // Strip CR and EOF ^Z
int ascii = FT2ASC[b]; }
if (config.isCrFlag()) { if (!config.isRemapFlag()) {
if (ascii == '\r') { outputStream.write(b);
lastCr = true;
continue; continue;
} }
if (lastCr) {
lastCr = false; /*
if (ascii == '\n') { * ASCII mode with remap:
outputStream.write('\n'); * Host IND$FILE sends pseudo-ASCII byte b.
continue; * Map pseudo-ASCII b to EBCDIC byte via ASC2FT[b],
} * then convert EBCDIC byte to Unicode UTF-8 character.
outputStream.write('\r'); */
} if (b < 0x20 || (b >= 0x80 && b < 0xA0 && b != 0x9F)) {
} // Control code write as Unicode directly
outputStream.write(ascii); 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 { } else {
// Standard EBCDICUnicode int ebc = ASC2FT[b & 0xFF];
char ch = translator.ebcdicToUnicode(b); char ch = translator.ebcdicToUnicode(ebc);
if (config.isCrFlag()) { outputStream.write(String.valueOf(ch).getBytes(StandardCharsets.UTF_8));
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));
} }
} }
} }
@@ -276,7 +350,7 @@ public class FTDft {
} }
int bufferSize = config.getDftBufferSize(); int bufferSize = config.getDftBufferSize();
int numbytes = bufferSize - 27; // reserve space for headers int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes]; byte[] readBuf = new byte[numbytes];
int totalRead = 0; int totalRead = 0;
@@ -290,7 +364,6 @@ public class FTDft {
} }
readBuf[totalRead++] = (byte) b; readBuf[totalRead++] = (byte) b;
} else { } else {
// Binary read
if (inputStream == null) { dftEof = true; break; } if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead); int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) { if (n <= 0) {
@@ -305,25 +378,20 @@ public class FTDft {
return; return;
} }
// Build SF response
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize); 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(); int sfLenPos = out.size();
out.write(0); out.write(0); out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA); // SF type out.write(SF_TRANSFER_DATA);
if (totalRead > 0) { if (totalRead > 0) {
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes"); log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
// TR_GET_REPLY
out.write((TR_GET_REPLY >> 8) & 0xFF); out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF); out.write(TR_GET_REPLY & 0xFF);
// Record number header
out.write((TR_RECNUM_HDR >> 8) & 0xFF); out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF); out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF)); out.write((int) ((recnum >> 24) & 0xFF));
@@ -332,56 +400,47 @@ public class FTDft {
out.write((int) (recnum & 0xFF)); out.write((int) (recnum & 0xFF));
recnum++; recnum++;
// Not compressed
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF); out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
out.write(TR_NOT_COMPRESSED & 0xFF); out.write(TR_NOT_COMPRESSED & 0xFF);
// Begin data
out.write(TR_BEGIN_DATA); out.write(TR_BEGIN_DATA);
int dataFieldLen = totalRead + 5; int dataFieldLen = totalRead + 5;
out.write((dataFieldLen >> 8) & 0xFF); out.write((dataFieldLen >> 8) & 0xFF);
out.write(dataFieldLen & 0xFF); out.write(dataFieldLen & 0xFF);
// The actual data
out.write(readBuf, 0, totalRead); out.write(readBuf, 0, totalRead);
bytesTransferred += totalRead; bytesTransferred += totalRead;
} else { } else {
log.fine("DFT: > GetReply EOF"); log.fine("DFT: > GetReply EOF");
// EOF reply
out.write((TR_GET_REQ >> 8) & 0xFF); out.write((TR_GET_REQ >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF); out.write(TR_ERROR_REPLY & 0xFF);
// Error header
out.write((TR_ERROR_HDR >> 8) & 0xFF); out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF); out.write(TR_ERROR_HDR & 0xFF);
// EOF error code
out.write((TR_ERR_EOF >> 8) & 0xFF); out.write((TR_ERR_EOF >> 8) & 0xFF);
out.write(TR_ERR_EOF & 0xFF); out.write(TR_ERR_EOF & 0xFF);
dftEof = true; dftEof = true;
} }
// Set the SF length
byte[] result = out.toByteArray(); 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] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF); result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
// Save for potential Read Modified retransmit
dftSaveBuf = result.clone(); dftSaveBuf = result.clone();
dftSaveBufLen = result.length; dftSaveBufLen = result.length;
// Send it
input.sendStructuredFieldData(result); input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred); listener.onBytesTransferred(bytesTransferred);
} }
/** /**
* Read a byte from the local file in ASCII mode with CR expansion and remapping. * Read a byte from local file for upload, handling ASCII conversion and remapping.
* Returns -1 for EOF. * Matching x3270 dft_ascii_read logic.
*/ */
private int dftAsciiRead(FTConfig config) throws IOException { private int dftAsciiRead(FTConfig config) throws IOException {
if (inputStream == null) return -1; if (inputStream == null) return -1;
@@ -389,30 +448,35 @@ public class FTDft {
int c = inputStream.read(); int c = inputStream.read();
if (c == -1) return -1; 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') { if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false; lastCr = false;
int ebc = translator.unicodeToEbcdic('\r'); // Expand \n to \r\n: return \r byte now
return ebc >= 0 ? ebc : 0x0D; int rEbc = translator.unicodeToEbcdic('\r');
if (rEbc < 0) rEbc = 0x0D;
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
} }
lastCr = (c == '\r'); lastCr = (c == '\r');
if (!config.isRemapFlag()) {
int ebc = translator.unicodeToEbcdic((char) c); int ebc = translator.unicodeToEbcdic((char) c);
return ebc >= 0 ? ebc : 0x40; 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 ========== // ========== Close Request ==========
@@ -420,16 +484,10 @@ public class FTDft {
private void dftCloseRequest() { private void dftCloseRequest() {
log.fine("DFT: Close"); log.fine("DFT: Close");
// Send close acknowledgement
ByteArrayOutputStream out = new ByteArrayOutputStream(6); ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF); out.write(AID_SF);
// SF length
out.write(0); out.write(5); out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA); out.write(SF_TRANSFER_DATA);
// TR_CLOSE_REPLY
out.write((TR_CLOSE_REPLY >> 8) & 0xFF); out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
out.write(TR_CLOSE_REPLY & 0xFF); out.write(TR_CLOSE_REPLY & 0xFF);
@@ -441,13 +499,8 @@ public class FTDft {
private void dftDataAck() { private void dftDataAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6); ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF); out.write(AID_SF);
// SF length
out.write(0); out.write(5); out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA); out.write(SF_TRANSFER_DATA);
// TR_NORMAL_REPLY
out.write((TR_NORMAL_REPLY >> 8) & 0xFF); out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
out.write(TR_NORMAL_REPLY & 0xFF); out.write(TR_NORMAL_REPLY & 0xFF);
@@ -461,21 +514,12 @@ public class FTDft {
ByteArrayOutputStream out = new ByteArrayOutputStream(10); ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF); out.write(AID_SF);
// SF length
out.write(0); out.write(9); out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA); out.write(SF_TRANSFER_DATA);
// Error reply code
out.write((code >> 8) & 0xFF); out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF); out.write(TR_ERROR_REPLY & 0xFF);
// Error header
out.write((TR_ERROR_HDR >> 8) & 0xFF); out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF); out.write(TR_ERROR_HDR & 0xFF);
// Command failed
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF); out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF); out.write(TR_ERR_CMDFAIL & 0xFF);
@@ -30,8 +30,46 @@ public class InputProcessor {
this.fsm = fsm; 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 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 boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insert) { this.insertMode = insert; } public void setInsertMode(boolean insert) { this.insertMode = insert; }
@@ -115,7 +153,7 @@ public class InputProcessor {
if (keyboardLocked && aidCode != AID_CLEAR) return; if (keyboardLocked && aidCode != AID_CLEAR) return;
lastAid = aidCode; lastAid = aidCode;
keyboardLocked = true; setKeyboardLocked(true);
if (aidCode == AID_CLEAR) { if (aidCode == AID_CLEAR) {
screen.clear(); screen.clear();
@@ -149,8 +187,6 @@ public class InputProcessor {
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i); ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) { if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
if (faIsProtected(ea.fa & 0xFF)) continue;
int fieldStart = (i + 1) % size; int fieldStart = (i + 1) % size;
// First, collect field data and find last non-null byte // First, collect field data and find last non-null byte
@@ -169,14 +205,14 @@ public class InputProcessor {
if (pos == fieldStart) break; if (pos == fieldStart) break;
} }
// Only send if there's actual data (strip trailing nulls) // Always send SBA and address
if (lastNonNull >= 0) {
out.write(ORDER_SBA); out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF); out.write(addr[0] & 0xFF);
out.write(addr[1] & 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(); byte[] allData = fieldData.toByteArray();
out.write(allData, 0, lastNonNull + 1); out.write(allData, 0, lastNonNull + 1);
} }
@@ -260,6 +296,17 @@ public class InputProcessor {
} }
public void eraseEof() { 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 addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
byte faVal = screen.getFieldAttributeAt(addr); byte faVal = screen.getFieldAttributeAt(addr);
@@ -282,6 +329,7 @@ public class InputProcessor {
} }
public void deleteChar() { public void deleteChar() {
if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(addr); byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return; if (faIsProtected(faVal & 0xFF)) return;
@@ -316,7 +364,7 @@ public class InputProcessor {
/** Reset (unlock keyboard, cancel insert mode). */ /** Reset (unlock keyboard, cancel insert mode). */
public void reset() { public void reset() {
keyboardLocked = false; setKeyboardLocked(false);
insertMode = false; insertMode = false;
} }
+1
View File
@@ -2,3 +2,4 @@ rootProject.name = 'j3270-project'
include 'lib3270j' include 'lib3270j'
include 'j3270' include 'j3270'
include 'a3270'