cleaning and adding updates
This commit is contained in:
@@ -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.
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.
@@ -1,2 +0,0 @@
|
||||
#Wed Mar 18 14:12:38 EDT 2026
|
||||
gradle.version=8.5
|
||||
+12
-2
@@ -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.
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
@@ -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
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
* EBCDIC→ASCII 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 EBCDIC→ASCII 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 ASC2FT→ebcdicToUnicode 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 EBCDIC→ASCII 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 EBCDIC→Unicode
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ rootProject.name = 'j3270-project'
|
||||
|
||||
include 'lib3270j'
|
||||
include 'j3270'
|
||||
include 'a3270'
|
||||
|
||||
Reference in New Issue
Block a user