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
@@ -51,6 +51,7 @@ public class Telnet3270Client {
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor);
}
/**
@@ -34,6 +34,9 @@ public class DataStreamProcessor {
// File Transfer DFT handler
private org.lib3270j.ft.FTDft ftDft;
// Input processor reference to manage keyboard locking state
private org.lib3270j.input.InputProcessor inputProcessor;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
public interface OutputSender {
@@ -56,6 +59,10 @@ public class DataStreamProcessor {
this.ftDft = ftDft;
}
public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) {
this.inputProcessor = inputProcessor;
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
}
@@ -231,6 +238,12 @@ public class DataStreamProcessor {
log.fine("WCC: " + String.format("0x%02x", wcc) +
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
if (kbdRestore || inputProcessor != null) {
if (inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
}
if (resetMdt) {
resetAllMDT();
}
@@ -471,6 +484,10 @@ public class DataStreamProcessor {
if (!faIsProtected(faVal & 0xFF)) {
ea.ec = 0;
ea.ucs4 = 0;
ea.fg = 0;
ea.bg = 0;
ea.gr = 0;
ea.cs = 0;
}
}
baddr = (baddr + 1) % size;
@@ -614,6 +631,10 @@ public class DataStreamProcessor {
// ========== Read Modified ==========
private void processReadModified(boolean all) {
if (ftDft != null) {
ftDft.readModified();
}
outputPos = 0;
int aid = AID_NO; // Last AID
@@ -183,6 +183,12 @@ public final class FTConstants {
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
* Converts a table6-encoded EBCDIC character to its 6-bit value.
*/
public static int from6(int ebcdicByte, org.lib3270j.charset.EbcdicTranslator translator) {
char ascii = translator.ebcdicToUnicode(ebcdicByte & 0xFF);
int idx = TABLE6.indexOf(ascii);
return idx >= 0 ? idx : 0;
}
public static int from6(int ebcdicByte) {
// First convert EBCDIC to ASCII via IND$FILE's table
int ascii = FT2ASC[ebcdicByte & 0xFF];
@@ -193,6 +199,12 @@ public final class FTConstants {
/**
* Encode a 6-bit value into a table6-encoded EBCDIC character.
*/
public static int to6(int value, org.lib3270j.charset.EbcdicTranslator translator) {
char ascii = TABLE6.charAt(value & 0x3F);
int ebc = translator.unicodeToEbcdic(ascii);
return ebc >= 0 ? ebc : ASC2FT[ascii & 0xFF];
}
public static int to6(int value) {
char ascii = TABLE6.charAt(value & 0x3F);
return ASC2FT[ascii & 0xFF];
+99 -127
View File
@@ -7,6 +7,7 @@ import static org.lib3270j.ft.FTConstants.*;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
@@ -49,10 +50,10 @@ public class FTCut {
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
private static final int SELECTOR_0 = 0x5E; // ';'
private static final int SELECTOR_1 = 0x7E; // '='
private static final int SELECTOR_2 = 0x5C; // '*'
private static final int SELECTOR_3 = 0x7D; // '\''
private static final int SELECTOR_0 = 0x5E; // ';' (EBCDIC)
private static final int SELECTOR_1 = 0x7E; // '=' (EBCDIC)
private static final int SELECTOR_2 = 0x5C; // '*' (EBCDIC)
private static final int SELECTOR_3 = 0x7D; // '\'' (EBCDIC)
private static final int[] XLATE_0 = {
0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,
@@ -110,6 +111,55 @@ public class FTCut {
this.listener = listener;
}
/**
* Initialize CUT mode with active streams.
*/
public void initTransfer(InputStream in, OutputStream out) {
this.inputStream = in;
this.outputStream = out;
resetState();
}
/**
* Initialize CUT mode for a new transfer.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
resetState();
if (config.isReceive()) {
boolean append = config.isAppend();
outputStream = new FileOutputStream(localFile, append);
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
private void resetState() {
xferInProgress = false;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
lastCr = false;
}
/**
* Clean up CUT mode resources.
*/
public void cleanup() {
xferInProgress = false;
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
}
/**
* Process a CUT-mode screen update.
* Called by the transfer coordinator when the screen changes during a transfer.
@@ -120,7 +170,12 @@ public class FTCut {
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
// Not a CUT frame (likely local echo or intermediate screen), ignore
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
if (xferInProgress) {
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
xferInProgress = false;
listener.onTransferAborted("Host aborted transfer (returned to menu)");
}
return;
}
@@ -149,42 +204,6 @@ public class FTCut {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
}
/**
* Initialize CUT mode for a new transfer.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
xferInProgress = false;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
lastCr = false;
if (config.isReceive()) {
boolean append = config.isAppend();
outputStream = new FileOutputStream(localFile, append);
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
/**
* Clean up CUT mode resources.
*/
public void cleanup() {
xferInProgress = false;
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
}
// ========== Control Code Processing ==========
private void cutControlCode() {
@@ -218,7 +237,6 @@ public class FTCut {
xferInProgress = false;
cutAck();
// Extract error message from the host (positions 4-83)
String msg = extractHostMessage();
listener.onTransferAborted(msg);
break;
@@ -242,7 +260,6 @@ public class FTCut {
char ch = translator.ebcdicToUnicode(ebc);
sb.append(ch);
}
// Trim trailing spaces and '$'
String msg = sb.toString().stripTrailing();
if (msg.endsWith("$")) {
msg = msg.substring(0, msg.length() - 1).stripTrailing();
@@ -252,12 +269,10 @@ public class FTCut {
// ========== Data Request (Upload) ==========
/**
* Process a data request from the host (upload: send data to host).
*/
private void cutDataRequest() {
int seq = screen.getCellEC(O_DR_FRAME_SEQ);
log.fine("CUT: DATA_REQUEST seq=" + from6(seq));
int seqEbc = screen.getCellEC(O_DR_FRAME_SEQ);
int seq = FTConstants.from6(seqEbc, translator);
log.fine("CUT: DATA_REQUEST seq=" + seq);
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
@@ -266,7 +281,6 @@ public class FTCut {
FTConfig config = listener.getConfig();
// Read data from local file into screen buffer
int count = 0;
try {
while (count < O_UP_MAX && !cutEof) {
@@ -284,32 +298,28 @@ public class FTCut {
return;
}
// EOF with no data → send EOF marker
if (count == 0 && cutEof) {
screen.setCell(O_UP_DATA, EOF_DATA1);
screen.setCell(O_UP_DATA + 1, EOF_DATA2);
count = 2;
}
// Compute frame fields
screen.setCell(O_UP_FRAME_SEQ, seq);
screen.setCell(O_UP_FRAME_SEQ, seqEbc);
// Checksum
int cs = 0;
for (int i = 0; i < count; i++) {
cs ^= screen.getCellEC(O_UP_DATA + i);
}
screen.setCell(O_UP_CSUM, to6(cs));
screen.setCell(O_UP_LEN, to6((count >> 6) & 0x3F));
screen.setCell(O_UP_LEN + 1, to6(count & 0x3F));
screen.setCell(O_UP_CSUM, FTConstants.to6(cs & 0x3F, translator));
screen.setCell(O_UP_LEN, FTConstants.to6((count >> 6) & 0x3F, translator));
screen.setCell(O_UP_LEN + 1, FTConstants.to6(count & 0x3F, translator));
// Hide the data field (change SF attribute to zero intensity)
// Hide data field by setting zero intensity on field attribute
byte attr = screen.getCellFAByte(O_DR_SF);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL | FA_MODIFY);
screen.setCellFA(O_DR_SF, attr);
// Send it
log.fine("CUT: > DATA seq=" + from6(seq) + " len=" + count);
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
@@ -317,9 +327,6 @@ public class FTCut {
// ========== Data (Download) ==========
/**
* Process data from the host (download: receive data from host).
*/
private void cutData() {
log.fine("CUT: DATA");
@@ -330,9 +337,8 @@ public class FTCut {
FTConfig config = listener.getConfig();
// Extract raw data
int rawLength = (from6(screen.getCellEC(O_DT_LEN)) << 6) |
from6(screen.getCellEC(O_DT_LEN + 1));
int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
FTConstants.from6(screen.getCellEC(O_DT_LEN + 1), translator);
if (rawLength > O_RESPONSE - O_DT_DATA) {
cutAbort("Oversized CUT data frame", SC_ABORT_XMIT);
@@ -344,7 +350,6 @@ public class FTCut {
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
}
// Check for EOF marker
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
(rawData[1] & 0xFF) == EOF_DATA2) {
log.fine("CUT: EOF marker received");
@@ -352,7 +357,6 @@ public class FTCut {
return;
}
// Convert and write to local file
try {
byte[] converted = convertDownload(rawData, rawLength, config);
if (outputStream != null) {
@@ -386,31 +390,20 @@ public class FTCut {
private void cutAbort(String message, int reason) {
log.warning("CUT: ABORT — " + message);
// Write abort frame to the response area
screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE);
screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ));
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
// Send PF2 to signal abort
input.sendAidForFT(AID_PF2);
listener.onTransferAborted(message);
}
// ========== Character Translation ==========
/**
* Convert received EBCDIC data to local format.
* Handles ASCII mode (with optional CR processing and remapping)
* and binary mode (raw passthrough).
*
* The quadrant tables decode wire bytes into IND$FILE's "pseudo-ASCII"
* representation. For the remap path, control codes (< 0x20 and
* 0x80-0x9E) are treated as direct Unicode codepoints, while printable
* characters are re-mapped through ASC2FT (inverting IND$FILE's built-in
* 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;
}
}
+162 -118
View File
@@ -5,6 +5,7 @@ import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
/**
@@ -17,6 +18,9 @@ public class FTDft {
private static final Logger log = Logger.getLogger(FTDft.class.getName());
private static final String OPEN_MSG = "FT:MSG";
private static final String END_TRANSFER = "TRANS03";
/** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener {
void onTransferRunning();
@@ -34,7 +38,7 @@ public class FTDft {
private final FTDftListener listener;
// DFT state
private long recnum = 0;
private long recnum = 1;
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
@@ -56,17 +60,20 @@ public class FTDft {
}
/**
* Initialize DFT mode for a new transfer.
* Initialize DFT mode with active streams.
*/
public void initTransfer(InputStream in, OutputStream out) {
this.inputStream = in;
this.outputStream = out;
resetState();
}
/**
* Initialize DFT mode for a new transfer from file.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
recnum = 0;
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
resetState();
if (config.isReceive()) {
outputStream = new FileOutputStream(localFile, config.isAppend());
@@ -77,6 +84,16 @@ public class FTDft {
}
}
private void resetState() {
recnum = 1;
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
}
/**
* Clean up DFT mode resources.
*/
@@ -115,7 +132,7 @@ public class FTDft {
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest();
dftOpenRequest(data, offset, length);
break;
case TR_INSERT_REQ:
dftInsertRequest(data, payloadStart, offset + length - payloadStart);
@@ -142,11 +159,53 @@ public class FTDft {
// ========== Open Request ==========
private void dftOpenRequest() {
log.fine("DFT: Open");
listener.onTransferRunning();
// Send acknowledgement
dftDataAck();
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();
}
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;
continue;
}
if (lastCr) {
lastCr = false;
if (ascii == '\n') {
outputStream.write('\n');
continue;
}
outputStream.write('\r');
}
}
outputStream.write(ascii);
if (!config.isRemapFlag()) {
outputStream.write(b);
continue;
}
/*
* ASCII mode with remap:
* Host IND$FILE sends pseudo-ASCII byte b.
* Map pseudo-ASCII b to EBCDIC byte via ASC2FT[b],
* then convert EBCDIC byte to Unicode UTF-8 character.
*/
if (b < 0x20 || (b >= 0x80 && b < 0xA0 && b != 0x9F)) {
// Control code — write as Unicode directly
outputStream.write(String.valueOf((char) b).getBytes(StandardCharsets.UTF_8));
} else if (b == 0xFF) {
// Special 0xFF -> U+009F
outputStream.write(String.valueOf((char) 0x9F).getBytes(StandardCharsets.UTF_8));
} else {
// 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;
}
lastCr = (c == '\r');
if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false;
// Expand \n to \r\n: return \r byte now
int rEbc = translator.unicodeToEbcdic('\r');
if (rEbc < 0) rEbc = 0x0D;
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
}
lastCr = (c == '\r');
if (!config.isRemapFlag()) {
int ebc = translator.unicodeToEbcdic((char) c);
return ebc >= 0 ? ebc : 0x40;
}
/*
* ASCII mode with remap:
* Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc].
*/
int ebc;
if (c < 0x20 || (c >= 0x80 && c < 0x9F)) {
ebc = ASC2FT[c & 0xFF];
} else if (c == 0x9F) {
ebc = 0xFF;
} else {
ebc = translator.unicodeToEbcdic((char) c);
}
if (ebc < 0) ebc = 0x40;
return FT2ASC[ebc & 0xFF];
}
// ========== Close Request ==========
@@ -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) {
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
// 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;
}