IND$FILE receiving

This commit is contained in:
2026-04-21 15:35:03 -04:00
parent bda8ea9530
commit 4ac3c6fa69
6 changed files with 2134 additions and 0 deletions
@@ -0,0 +1,310 @@
package org.lib3270j.ft;
/**
* Configuration for an IND$FILE file transfer session.
* Ported from x3270's ft_conf_t (ft_private.h).
*/
public class FTConfig {
/** Host operating system type */
public enum HostType {
TSO, CMS, CICS
}
/** Record format for datasets (TSO/CMS sends only) */
public enum RecordFormat {
DEFAULT, FIXED, VARIABLE, UNDEFINED
}
/** Space allocation units (TSO sends only) */
public enum AllocationUnit {
DEFAULT, TRACKS, CYLINDERS, AVBLOCK
}
/** Transfer direction */
public enum Direction {
RECEIVE, SEND
}
/** Transfer mode */
public enum TransferMode {
ASCII, BINARY
}
/** CR/LF handling */
public enum CrAction {
ADD, REMOVE, KEEP
}
/** Behavior when destination file already exists */
public enum ExistAction {
KEEP, REPLACE, APPEND
}
// ========== Transfer Parameters ==========
private String hostFilename;
private String localFilename;
private Direction direction = Direction.RECEIVE;
private HostType hostType = HostType.TSO;
private TransferMode transferMode = TransferMode.ASCII;
private CrAction crAction = CrAction.REMOVE;
private boolean remapFlag = true;
private ExistAction existAction = ExistAction.KEEP;
private RecordFormat recfm = RecordFormat.DEFAULT;
private AllocationUnit units = AllocationUnit.DEFAULT;
private int lrecl = 0;
private int blksize = 0;
private int primarySpace = 0;
private int secondarySpace = 0;
private int avblock = 0;
private int dftBufferSize = FTConstants.DFT_BUF;
private String otherOptions = null;
// ========== Derived convenience getters ==========
public boolean isReceive() { return direction == Direction.RECEIVE; }
public boolean isSend() { return direction == Direction.SEND; }
public boolean isAscii() { return transferMode == TransferMode.ASCII; }
public boolean isBinary() { return transferMode == TransferMode.BINARY; }
public boolean isCrFlag() {
// CR processing is only applicable for ASCII transfers
return isAscii() && crAction != CrAction.KEEP;
}
public boolean isAppend() { return existAction == ExistAction.APPEND; }
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
// ========== Standard getters and setters ==========
public String getHostFilename() { return hostFilename; }
public void setHostFilename(String hostFilename) { this.hostFilename = hostFilename; }
public String getLocalFilename() { return localFilename; }
public void setLocalFilename(String localFilename) { this.localFilename = localFilename; }
public Direction getDirection() { return direction; }
public void setDirection(Direction direction) { this.direction = direction; }
public HostType getHostType() { return hostType; }
public void setHostType(HostType hostType) { this.hostType = hostType; }
public TransferMode getTransferMode() { return transferMode; }
public void setTransferMode(TransferMode transferMode) { this.transferMode = transferMode; }
public CrAction getCrAction() { return crAction; }
public void setCrAction(CrAction crAction) { this.crAction = crAction; }
public boolean isRemapFlag() { return remapFlag; }
public void setRemapFlag(boolean remapFlag) { this.remapFlag = remapFlag; }
public ExistAction getExistAction() { return existAction; }
public void setExistAction(ExistAction existAction) { this.existAction = existAction; }
public RecordFormat getRecfm() { return recfm; }
public void setRecfm(RecordFormat recfm) { this.recfm = recfm; }
public AllocationUnit getUnits() { return units; }
public void setUnits(AllocationUnit units) { this.units = units; }
public int getLrecl() { return lrecl; }
public void setLrecl(int lrecl) { this.lrecl = lrecl; }
public int getBlksize() { return blksize; }
public void setBlksize(int blksize) { this.blksize = blksize; }
public int getPrimarySpace() { return primarySpace; }
public void setPrimarySpace(int primarySpace) { this.primarySpace = primarySpace; }
public int getSecondarySpace() { return secondarySpace; }
public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; }
public int getAvblock() { return avblock; }
public void setAvblock(int avblock) { this.avblock = avblock; }
public int getDftBufferSize() { return dftBufferSize; }
public void setDftBufferSize(int dftBufferSize) {
this.dftBufferSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, dftBufferSize));
}
public String getOtherOptions() { return otherOptions; }
public void setOtherOptions(String otherOptions) {
this.otherOptions = (otherOptions != null && !otherOptions.trim().isEmpty())
? otherOptions.trim() : null;
}
// ========== Convenience UI setters ==========
public void setAppend(boolean append) {
if (append) this.existAction = ExistAction.APPEND;
}
public void setOverwrite(boolean overwrite) {
if (overwrite) this.existAction = ExistAction.REPLACE;
}
public void setReceive(boolean receive) {
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
}
public void setAscii(boolean ascii) {
setTransferMode(ascii ? TransferMode.ASCII : TransferMode.BINARY);
}
public void setCrFlag(boolean cr) {
setCrAction(cr ? CrAction.REMOVE : CrAction.KEEP);
}
public void setRecfm(String r) {
if (r == null || r.trim().isEmpty()) {
this.recfm = RecordFormat.DEFAULT;
} else {
String u = r.trim().toUpperCase();
if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
else this.recfm = RecordFormat.DEFAULT;
}
}
public void setLrecl(String l) {
try { this.lrecl = Integer.parseInt(l.trim()); } catch (NumberFormatException e) { this.lrecl = 0; }
}
public void setBlksize(String b) {
try { this.blksize = Integer.parseInt(b.trim()); } catch (NumberFormatException e) { this.blksize = 0; }
}
public void setSpace(String s) {
// Space string like "10,5" for primary/secondary
if (s == null || s.trim().isEmpty()) return;
String[] parts = s.split(",");
try {
if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim());
if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
// ignore
}
}
public void setOptions(String opts) {
setOtherOptions(opts);
}
// ========== Validation ==========
/**
* Validate the configuration before starting a transfer.
* @return null if valid, or an error message string
*/
public String validate() {
if (hostFilename == null || hostFilename.trim().isEmpty()) {
return "Host file name is required";
}
if (localFilename == null || localFilename.trim().isEmpty()) {
return "Local file name is required";
}
if (hostType == HostType.TSO && isSend() &&
units != AllocationUnit.DEFAULT && primarySpace <= 0) {
return "Primary space is required when allocation is specified";
}
if (hostType == HostType.TSO && isSend() &&
units == AllocationUnit.AVBLOCK && avblock <= 0) {
return "Avblock value is required when allocation is AVBLOCK";
}
return null;
}
/**
* Build the IND$FILE command string to send to the host.
* Ported from x3270's ft_go() in ft.c.
*/
public String buildCommand() {
StringBuilder cmd = new StringBuilder();
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename);
// CMS/CICS options use ( prefix
if (hostType != HostType.TSO) {
cmd.append(" (");
} else {
cmd.append(" ");
}
// Mode
if (isAscii()) {
cmd.append("ASCII");
} else if (hostType == HostType.CICS) {
cmd.append("BINARY");
}
// CR/LF handling
if (isAscii() && isCrFlag()) {
cmd.append(" CRLF");
} else if (hostType == HostType.CICS) {
cmd.append(" NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
cmd.append(" APPEND");
}
// TSO-specific send options
if (isSend()) {
if (hostType == HostType.TSO) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM(");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
case UNDEFINED: cmd.append("U"); break;
default: break;
}
cmd.append(")");
if (lrecl > 0) {
cmd.append(" LRECL(").append(lrecl).append(")");
}
if (blksize > 0) {
cmd.append(" BLKSIZE(").append(blksize).append(")");
}
}
if (units != AllocationUnit.DEFAULT) {
cmd.append(" SPACE(").append(primarySpace);
if (secondarySpace > 0) {
cmd.append(",").append(secondarySpace);
}
cmd.append(")");
switch (units) {
case TRACKS: cmd.append(" TRACKS"); break;
case CYLINDERS: cmd.append(" CYLINDERS"); break;
case AVBLOCK: cmd.append(" AVBLOCK(").append(avblock).append(")"); break;
default: break;
}
}
} else if (hostType == HostType.CMS) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM ");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
default: break;
}
if (lrecl > 0) {
cmd.append(" LRECL ").append(lrecl);
}
}
}
}
// Additional options
if (otherOptions != null) {
cmd.append(" ").append(otherOptions);
}
return cmd.toString().trim();
}
}
@@ -0,0 +1,200 @@
package org.lib3270j.ft;
/**
* Constants for IND$FILE file transfer protocol.
* Ported from x3270: ft_cut_ds.h, ft_dft_ds.h, ft.c
*/
public final class FTConstants {
private FTConstants() {} // utility class
// ========== CUT Mode Frame Layout ==========
/** Offset to the CUT structured field at the end of the screen */
public static final int O_SF = 1919;
// Primary area offsets
public static final int O_FRAME_TYPE = 0;
// Control Code frame (host → terminal)
public static final int FT_CONTROL_CODE = 0xC3;
public static final int O_CC_FRAME_SEQ = 1;
public static final int O_CC_STATUS_CODE = 2;
public static final int O_CC_MESSAGE = 4;
// Control Code status codes
public static final int SC_HOST_ACK = 0x8181;
public static final int SC_XFER_COMPLETE = 0x8189;
public static final int SC_ABORT_FILE = 0x8194;
public static final int SC_ABORT_XMIT = 0x8198;
// Data Request frame (host → terminal, for uploads)
public static final int FT_DATA_REQUEST = 0xC2;
public static final int O_DR_SF = 1;
public static final int O_DR_DATA_CODE = 2;
public static final int O_DR_FRAME_SEQ = 3;
// Retransmit frame
public static final int FT_RETRANSMIT = 0x4C;
// Data frame (bidirectional)
public static final int FT_DATA = 0xC1;
public static final int O_DT_FRAME_SEQ = 1;
public static final int O_DT_CSUM = 2;
public static final int O_DT_LEN = 3;
public static final int O_DT_DATA = 5;
// Response Area (near end of screen)
public static final int O_RESPONSE = O_SF - 5;
public static final int RO_FRAME_TYPE = O_RESPONSE + 1;
public static final int RO_FRAME_SEQ = O_RESPONSE + 2;
public static final int RO_REASON_CODE = O_RESPONSE + 3;
// Response frame types
public static final int RFT_RETRANSMIT = 0x4C;
public static final int RFT_CONTROL_CODE = 0xC3;
// Special EOF data markers
public static final int EOF_DATA1 = 0x5C;
public static final int EOF_DATA2 = 0xA9;
// Upload data area offsets
public static final int O_UP_DATA_CODE = 2;
public static final int O_UP_FRAME_SEQ = 3;
public static final int O_UP_CSUM = 4;
public static final int O_UP_LEN = 5;
public static final int O_UP_DATA = 7;
public static final int O_UP_MAX = O_SF - O_UP_DATA;
// ========== CUT Mode AID codes ==========
public static final int ACK_OK = 0x7D; // AID_ENTER
public static final int ACK_RETRANSMIT = 0xF1; // AID_PF1
public static final int ACK_RESYNC_VM = 0x6D; // AID_CLEAR
public static final int ACK_RESYNC_TSO = 0x6E; // AID_PA2
public static final int ACK_ABORT = 0xF2; // AID_PF2
// ========== DFT Mode Structured Field Codes ==========
/** Structured field type for file transfer data */
public static final int SF_TRANSFER_DATA = 0xD0;
// Host requests
public static final int TR_OPEN_REQ = 0x0012;
public static final int TR_CLOSE_REQ = 0x4112;
public static final int TR_SET_CUR_REQ = 0x4511;
public static final int TR_GET_REQ = 0x4611;
public static final int TR_INSERT_REQ = 0x4711;
public static final int TR_DATA_INSERT = 0x4704;
// PC replies
public static final int TR_GET_REPLY = 0x4605;
public static final int TR_NORMAL_REPLY = 0x4705;
public static final int TR_ERROR_REPLY = 0x08; // low 8 bits
public static final int TR_CLOSE_REPLY = 0x4109;
// Other headers
public static final int TR_RECNUM_HDR = 0x6306;
public static final int TR_ERROR_HDR = 0x6904;
public static final int TR_NOT_COMPRESSED = 0xC080;
public static final int TR_BEGIN_DATA = 0x61;
// Error codes
public static final int TR_ERR_EOF = 0x2200;
public static final int TR_ERR_CMDFAIL = 0x0100;
// DFT buffer size limits
public static final int DFT_MIN_BUF = 256;
public static final int DFT_MAX_BUF = 32768;
public static final int DFT_BUF = 4096;
// AID for structured field response
public static final int AID_SF = 0x88;
// ========== Transfer State ==========
public enum FTState {
NONE, // No transfer in progress
AWAIT_ACK, // IND$FILE sent, awaiting acknowledgement
RUNNING, // Ack received, data flowing
ABORT_WAIT, // Awaiting chance to send an abort
ABORT_SENT // Abort sent; awaiting response
}
// ========== Encoding Tables ==========
/**
* Base-64-like encoding table used by CUT mode for lengths and checksums.
* 64 characters: a-z, &, -, ., :, +, A-Z, 0-5
*/
public static final String TABLE6 =
"abcdefghijklmnopqrstuvwxyz&-.,:+ABCDEFGHIJKLMNOPQRSTUVWXYZ012345";
/**
* IND$FILE's fixed ASCII-to-EBCDIC translation table (i_asc2ft).
* This is NOT the standard CP037 mapping — it's IND$FILE's own table.
* Used when remap=true to invert IND$FILE's built-in translation.
*/
public static final int[] ASC2FT = {
0x00,0x01,0x02,0x03,0x37,0x2d,0x2e,0x2f,0x16,0x05,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
0x10,0x11,0x12,0x13,0x3c,0x3d,0x32,0x26,0x18,0x19,0x3f,0x27,0x1c,0x1d,0x1e,0x1f,
0x40,0x5a,0x7f,0x7b,0x5b,0x6c,0x50,0x7d,0x4d,0x5d,0x5c,0x4e,0x6b,0x60,0x4b,0x61,
0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0x7a,0x5e,0x4c,0x7e,0x6e,0x6f,
0x7c,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,
0xd7,0xd8,0xd9,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0x4a,0xe0,0x4f,0x5f,0x6d,
0x79,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x91,0x92,0x93,0x94,0x95,0x96,
0x97,0x98,0x99,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xc0,0x6a,0xd0,0xa1,0x07,
0x20,0x21,0x22,0x23,0x24,0x15,0x06,0x17,0x28,0x29,0x2a,0x2b,0x2c,0x09,0x0a,0x1b,
0x30,0x31,0x1a,0x33,0x34,0x35,0x36,0x08,0x38,0x39,0x3a,0x3b,0x04,0x14,0x3e,0xe1,
0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x51,0x52,0x53,0x54,0x55,0x56,0x57,
0x58,0x59,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x70,0x71,0x72,0x73,0x74,0x75,
0x76,0x77,0x78,0x80,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,0x90,0x9a,0x9b,0x9c,0x9d,0x9e,
0x9f,0xa0,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,
0xb8,0xb9,0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xca,0xcb,0xcc,0xcd,0xce,0xcf,0xda,0xdb,
0xdc,0xdd,0xde,0xdf,0xea,0xeb,0xec,0xed,0xee,0xef,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
};
/**
* IND$FILE's fixed EBCDIC-to-ASCII translation table (i_ft2asc).
* The inverse of ASC2FT.
*/
public static final int[] FT2ASC = {
0x00,0x01,0x02,0x03,0x9c,0x09,0x86,0x7f,0x97,0x8d,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
0x10,0x11,0x12,0x13,0x9d,0x85,0x08,0x87,0x18,0x19,0x92,0x8f,0x1c,0x1d,0x1e,0x1f,
0x80,0x81,0x82,0x83,0x84,0x00,0x17,0x1b,0x88,0x89,0x8a,0x8b,0x8c,0x05,0x06,0x07,
0x90,0x91,0x16,0x93,0x94,0x95,0x96,0x04,0x98,0x99,0x9a,0x9b,0x14,0x15,0x9e,0x1a,
0x20,0xa0,0xa1,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0x5b,0x2e,0x3c,0x28,0x2b,0x5d,
0x26,0xa9,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0x21,0x24,0x2a,0x29,0x3b,0x5e,
0x2d,0x2f,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0x7c,0x2c,0x25,0x5f,0x3e,0x3f,
0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xc0,0xc1,0xc2,0x60,0x3a,0x23,0x40,0x27,0x3d,0x22,
0xc3,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,
0xca,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xcb,0xcc,0xcd,0xce,0xcf,0xd0,
0xd1,0x7e,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,
0xd8,0xd9,0xda,0xdb,0xdc,0xdd,0xde,0xdf,0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,
0x7b,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0xe8,0xe9,0xea,0xeb,0xec,0xed,
0x7d,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f,0x50,0x51,0x52,0xee,0xef,0xf0,0xf1,0xf2,0xf3,
0x5c,0x9f,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,
0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
};
// ========== Utility Methods ==========
/**
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
* Converts a table6-encoded EBCDIC character to its 6-bit value.
*/
public static int from6(int ebcdicByte) {
// First convert EBCDIC to ASCII via IND$FILE's table
int ascii = FT2ASC[ebcdicByte & 0xFF];
int idx = TABLE6.indexOf((char) ascii);
return idx >= 0 ? idx : 0;
}
/**
* Encode a 6-bit value into a table6-encoded EBCDIC character.
*/
public static int to6(int value) {
char ascii = TABLE6.charAt(value & 0x3F);
return ASC2FT[ascii & 0xFF];
}
}
@@ -0,0 +1,587 @@
package org.lib3270j.ft;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.input.InputProcessor;
import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.*;
import java.util.logging.Logger;
/**
* CUT (Character Unit Transfer) mode file transfer handler.
* Data flows through the screen buffer using a framed protocol.
* Ported from x3270's ft_cut.c.
*/
public class FTCut {
private static final Logger log = Logger.getLogger(FTCut.class.getName());
/** Callback for transfer events */
public interface FTCutListener {
void onTransferRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
FTConstants.FTState getCurrentState();
void setState(FTConstants.FTState state);
FTConfig getConfig();
File getLocalFile();
}
private final ScreenBuffer screen;
private final InputProcessor input;
private final EbcdicTranslator translator;
private final FTCutListener listener;
// CUT mode state
private boolean xferInProgress = false;
private long expandedLength = 0;
private int quadrant = -1;
private boolean cutEof = false;
// Upload translation buffer
private static final int XLATE_NBUF = 32;
private int xlateBuffered = 0;
private int xlateBufIx = 0;
private final int[] xlateBuf = new int[XLATE_NBUF];
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
private static final int SELECTOR_0 = 0x5E; // ';'
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[] XLATE_0 = {
0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,
0xd3,0xd4,0xd5,0xd6, 0xd7,0xd8,0xd9,0xe2, 0xe3,0xe4,0xe5,0xe6,
0xe7,0xe8,0xe9,0x81, 0x82,0x83,0x84,0x85, 0x86,0x87,0x88,0x89,
0x91,0x92,0x93,0x94, 0x95,0x96,0x97,0x98, 0x99,0xa2,0xa3,0xa4,
0xa5,0xa6,0xa7,0xa8, 0xa9,0xf0,0xf1,0xf2, 0xf3,0xf4,0xf5,0xf6,
0xf7,0xf8,0xf9,0x6c, 0x50,0x6d,0x4d,0x5d, 0x4c,0x4e,0x6b,0x60,
0x4b,0x61,0x7a,0x6e, 0x6f
};
private static final int[] XLATE_1 = {
0x20,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, 0x48,0x49,0x4a,0x4b,
0x4c,0x4d,0x4e,0x4f, 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57,
0x58,0x59,0x5a,0x61, 0x62,0x63,0x64,0x65, 0x66,0x67,0x68,0x69,
0x6a,0x6b,0x6c,0x6d, 0x6e,0x6f,0x70,0x71, 0x72,0x73,0x74,0x75,
0x76,0x77,0x78,0x79, 0x7a,0x30,0x31,0x32, 0x33,0x34,0x35,0x36,
0x37,0x38,0x39,0x25, 0x26,0x27,0x28,0x29, 0x2a,0x2b,0x2c,0x2d,
0x2e,0x2f,0x3a,0x3b, 0x3f
};
private static final int[] XLATE_2 = {
0x00,0x00,0x01,0x02, 0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0a,
0x0b,0x0c,0x0d,0x0e, 0x0f,0x10,0x11,0x12, 0x13,0x14,0x15,0x16,
0x17,0x18,0x19,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x3c,0x3d,0x3e, 0x00,0xfa,0xfb,0xfc,
0xfd,0xfe,0xff,0x7b, 0x7c,0x7d,0x7e,0x7f, 0x1a,0x1b,0x1c,0x1d,
0x1e,0x1f,0x00,0x00, 0x00
};
private static final int[] XLATE_3 = {
0x00,0xa0,0xa1,0xea, 0xeb,0xec,0xed,0xee, 0xef,0xe0,0xe1,0xaa,
0xab,0xac,0xad,0xae, 0xaf,0xb0,0xb1,0xb2, 0xb3,0xb4,0xb5,0xb6,
0xb7,0xb8,0xb9,0x80, 0x00,0xca,0xcb,0xcc, 0xcd,0xce,0xcf,0xc0,
0x00,0x8a,0x8b,0x8c, 0x8d,0x8e,0x8f,0x90, 0x00,0xda,0xdb,0xdc,
0xdd,0xde,0xdf,0xd0, 0x00,0x00,0x21,0x22, 0x23,0x24,0x5b,0x5c,
0x00,0x5e,0x5f,0x00, 0x9c,0x9d,0x9e,0x9f, 0xba,0xbb,0xbc,0xbd,
0xbe,0xbf,0x9a,0x9b, 0x00
};
private static final int[][] QUADS = { XLATE_0, XLATE_1, XLATE_2, XLATE_3 };
private static final int[] SELECTORS = { SELECTOR_0, SELECTOR_1, SELECTOR_2, SELECTOR_3 };
// File I/O
private InputStream inputStream;
private OutputStream outputStream;
private boolean lastCr = false;
public FTCut(ScreenBuffer screen, InputProcessor input,
EbcdicTranslator translator, FTCutListener listener) {
this.screen = screen;
this.input = input;
this.translator = translator;
this.listener = listener;
}
/**
* Process a CUT-mode screen update.
* Called by the transfer coordinator when the screen changes during a transfer.
*/
public void processScreenUpdate() {
if (listener.getCurrentState() == FTState.NONE) return;
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
// Not a CUT frame (likely local echo or intermediate screen), ignore
return;
}
int frameType = screen.getCellEC(O_FRAME_TYPE);
switch (frameType) {
case FT_CONTROL_CODE:
cutControlCode();
break;
case FT_DATA_REQUEST:
cutDataRequest();
break;
case FT_RETRANSMIT:
cutRetransmit();
break;
case FT_DATA:
cutData();
break;
default:
log.fine("Ignoring non-CUT frame type 0x" + Integer.toHexString(frameType));
return;
}
}
private boolean isSkip(byte attr) {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
}
/**
* 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() {
int code = (screen.getCellEC(O_CC_STATUS_CODE) << 8) |
screen.getCellEC(O_CC_STATUS_CODE + 1);
log.fine("CUT: CONTROL_CODE 0x" + Integer.toHexString(code));
switch (code) {
case SC_HOST_ACK:
log.info("CUT: HOST_ACK received — transfer running");
xferInProgress = true;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
cutAck();
listener.onTransferRunning();
break;
case SC_XFER_COMPLETE:
log.info("CUT: Transfer complete");
cutAck();
xferInProgress = false;
listener.onTransferComplete(null);
break;
case SC_ABORT_FILE:
case SC_ABORT_XMIT:
log.warning("CUT: ABORT received");
xferInProgress = false;
cutAck();
// Extract error message from the host (positions 4-83)
String msg = extractHostMessage();
listener.onTransferAborted(msg);
break;
default:
log.warning("CUT: Unknown control code 0x" + Integer.toHexString(code));
cutAbort("Unknown CUT control code", SC_ABORT_XMIT);
break;
}
}
/**
* Extract the error message from the host's control code frame.
* The message starts at O_CC_MESSAGE and is up to 80 EBCDIC characters.
*/
private String extractHostMessage() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 80; i++) {
int ebc = screen.getCellEC(O_CC_MESSAGE + i);
if (ebc == 0) continue;
char ch = translator.ebcdicToUnicode(ebc);
sb.append(ch);
}
// Trim trailing spaces and '$'
String msg = sb.toString().stripTrailing();
if (msg.endsWith("$")) {
msg = msg.substring(0, msg.length() - 1).stripTrailing();
}
return msg.isEmpty() ? "Host cancelled transfer" : msg;
}
// ========== Data Request (Upload) ==========
/**
* 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));
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
return;
}
FTConfig config = listener.getConfig();
// Read data from local file into screen buffer
int count = 0;
try {
while (count < O_UP_MAX && !cutEof) {
int c = xlateGetc(config);
if (c == -1) {
cutEof = true;
break;
}
screen.setCell(O_UP_DATA + count, c);
count++;
}
} catch (IOException e) {
log.warning("CUT: Read error: " + e.getMessage());
cutAbort("Read error: " + e.getMessage(), SC_ABORT_FILE);
return;
}
// 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);
// 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));
// Hide the data field (change SF attribute to zero intensity)
byte attr = screen.getCellFAByte(O_DR_SF);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL);
screen.setCellFA(O_DR_SF, attr);
// Send it
log.fine("CUT: > DATA seq=" + from6(seq) + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
}
// ========== Data (Download) ==========
/**
* Process data from the host (download: receive data from host).
*/
private void cutData() {
log.fine("CUT: DATA");
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
return;
}
FTConfig config = listener.getConfig();
// Extract raw data
int rawLength = (from6(screen.getCellEC(O_DT_LEN)) << 6) |
from6(screen.getCellEC(O_DT_LEN + 1));
if (rawLength > O_RESPONSE - O_DT_DATA) {
cutAbort("Oversized CUT data frame", SC_ABORT_XMIT);
return;
}
byte[] rawData = new byte[rawLength];
for (int i = 0; i < rawLength; i++) {
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
}
// Check for EOF marker
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
(rawData[1] & 0xFF) == EOF_DATA2) {
log.fine("CUT: EOF marker received");
cutAck();
return;
}
// Convert and write to local file
try {
byte[] converted = convertDownload(rawData, rawLength, config);
if (outputStream != null) {
outputStream.write(converted);
expandedLength += converted.length;
listener.onBytesTransferred(expandedLength);
}
cutAck();
} catch (IOException e) {
log.warning("CUT: Write error: " + e.getMessage());
cutAbort("Write error: " + e.getMessage(), SC_ABORT_FILE);
}
}
// ========== Retransmit ==========
private void cutRetransmit() {
log.warning("CUT: RETRANSMIT (not supported, aborting)");
cutAbort("Retransmit not supported", SC_ABORT_XMIT);
}
// ========== Acknowledge ==========
private void cutAck() {
log.fine("CUT: > ACK (Enter)");
input.sendAidForFT(AID_ENTER);
}
// ========== Abort ==========
private void cutAbort(String message, int reason) {
log.warning("CUT: ABORT — " + message);
// 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.
*/
private byte[] convertDownload(byte[] rawData, int length, FTConfig config)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(length * 2);
for (int i = 0; i < length; i++) {
int c = rawData[i] & 0xFF;
while (true) {
if (quadrant < 0) {
// 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
}
if (c < 0x40 || c > 0xF9) {
throw new IOException("CUT conversion error (data out of bounds)");
}
char asciiChar = translator.ebcdicToUnicode(c);
int ix = ALPHAS.indexOf(asciiChar);
if (ix < 0) {
// Try a different quadrant
quadrant = -1;
continue; // retry loop
}
if (quadrant != 2 && c != 0xC1 && QUADS[quadrant][ix] == 0) {
// Try a different quadrant
quadrant = -1;
continue; // retry
}
// 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
}
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));
} else if (decoded == 0xFF) {
// Special case: 0xFF → U+009F
out.write(String.valueOf((char) 0x9F).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
} else {
// Displayable character: invert IND$FILE's table
int ebc = FTConstants.ASC2FT[decoded & 0xFF];
char unicodeChar = translator.ebcdicToUnicode(ebc);
out.write(String.valueOf(unicodeChar).getBytes(
java.nio.charset.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--;
return r;
}
if (inputStream == null) return -1;
int c = inputStream.read();
if (c == -1) return -1;
int localByte = c & 0xFF;
int nc = 0;
int[] cbuf = new int[4]; // max 4 bytes (2 for \r + 2 for \n if quadrant encoded)
if (config.isAscii()) {
if (config.isCrFlag() && !lastCr && localByte == '\n') {
nc += uploadConvert('\r', cbuf, nc, config);
}
lastCr = (localByte == '\r');
}
nc += uploadConvert(localByte, cbuf, nc, config);
if (nc > 1) {
for (int i = 1; i < nc; i++) {
xlateBuf[xlateBuffered++] = cbuf[i];
}
xlateBufIx = 0;
}
return cbuf[0];
}
private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) {
int ebc;
if (localByte == 0) {
// Nulls are special in the 'OTHER_2' quadrant
if (quadrant != 2) {
quadrant = 2; // OTHER_2
cbuf[offset] = SELECTORS[quadrant];
cbuf[offset + 1] = 0xC1; // XLATE_NULL
return 2;
} else {
cbuf[offset] = 0xC1;
return 1;
}
}
if (!config.isAscii() || !config.isRemapFlag()) {
// 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
}
ebc = org.lib3270j.ft.FTConstants.FT2ASC[standardEbc & 0xFF];
}
return storeUpload(ebc, cbuf, offset);
}
private int storeUpload(int ebc, 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));
return 1;
}
}
}
int oq = quadrant;
for (quadrant = 0; quadrant < 4; quadrant++) {
if (quadrant == oq) continue;
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == ebc) {
obBuf[offset] = SELECTORS[quadrant];
obBuf[offset + 1] = translator.unicodeToEbcdic(ALPHAS.charAt(i));
return 2;
}
}
}
quadrant = -1;
// Fallback safety measure
obBuf[offset] = translator.unicodeToEbcdic('?');
return 1;
}
}
@@ -0,0 +1,496 @@
package org.lib3270j.ft;
import org.lib3270j.input.InputProcessor;
import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*;
import java.io.*;
import java.util.logging.Logger;
/**
* DFT (Distributed Function Terminal) mode file transfer handler.
* Uses Structured Fields (SF_TRANSFER_DATA = 0xD0) for data exchange.
* Offers better performance than CUT mode with configurable buffer sizes.
* Ported from x3270's ft_dft.c.
*/
public class FTDft {
private static final Logger log = Logger.getLogger(FTDft.class.getName());
/** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener {
void onTransferRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
FTConstants.FTState getCurrentState();
void setState(FTConstants.FTState state);
FTConfig getConfig();
File getLocalFile();
}
private final InputProcessor input;
private final EbcdicTranslator translator;
private final FTDftListener listener;
// DFT state
private long recnum = 0;
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
// Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null;
private int dftSaveBufLen = 0;
// File I/O
private InputStream inputStream;
private OutputStream outputStream;
private boolean lastCr = false;
public FTDft(InputProcessor input, EbcdicTranslator translator,
FTDftListener listener) {
this.input = input;
this.translator = translator;
this.listener = listener;
}
/**
* Initialize DFT mode for a new transfer.
*/
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;
if (config.isReceive()) {
outputStream = new FileOutputStream(localFile, config.isAppend());
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
/**
* Clean up DFT mode resources.
*/
public void cleanup() {
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
dftSaveBuf = null;
dftSaveBufLen = 0;
}
/**
* Process a DFT structured field from the host.
* Called by DataStreamProcessor when SF type is SF_TRANSFER_DATA (0xD0).
*
* @param data raw structured field data
* @param offset start of the SF (after length + type byte)
* @param length total SF length
*/
public void processStructuredField(byte[] data, int offset, int length) {
// The SF payload starts after the 2-byte length + 1-byte SF type
int payloadStart = offset + 3;
if (payloadStart + 2 > offset + length) {
log.warning("DFT: SF too short");
return;
}
// First 2 bytes of payload are the DFT request code
int requestCode = ((data[payloadStart] & 0xFF) << 8) |
(data[payloadStart + 1] & 0xFF);
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest();
break;
case TR_INSERT_REQ:
dftInsertRequest(data, payloadStart, offset + length - payloadStart);
break;
case TR_DATA_INSERT:
dftDataInsert(data, payloadStart, offset + length - payloadStart);
break;
case TR_SET_CUR_REQ:
// No-op, same as x3270
log.fine("DFT: SetCursor (ignored)");
break;
case TR_GET_REQ:
dftGetRequest();
break;
case TR_CLOSE_REQ:
dftCloseRequest();
break;
default:
log.warning("DFT: Unknown request code 0x" + Integer.toHexString(requestCode));
dftAbort("Unknown DFT request", TR_DATA_INSERT);
break;
}
}
// ========== Open Request ==========
private void dftOpenRequest() {
log.fine("DFT: Open");
listener.onTransferRunning();
// Send acknowledgement
dftDataAck();
}
// ========== Insert Request (host sending data for download) ==========
private void dftInsertRequest(byte[] data, int offset, int length) {
log.fine("DFT: Insert");
dftDataInsert(data, offset, length);
}
private void dftDataInsert(byte[] data, int offset, int length) {
FTConfig config = listener.getConfig();
if (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;
// Look for TR_BEGIN_DATA marker
while (pos < end) {
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
pos += 3;
if (actualDataLen > 0 && pos + actualDataLen <= end) {
try {
writeDownloadData(data, pos, actualDataLen, config);
bytesTransferred += actualDataLen;
listener.onBytesTransferred(bytesTransferred);
} catch (IOException e) {
dftAbort("Write error: " + e.getMessage(), TR_DATA_INSERT);
return;
}
}
pos += Math.max(0, actualDataLen);
} else if (pos + 1 < end) {
// 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
} else if (hdrCode16 == TR_NOT_COMPRESSED) {
pos += 2;
} else {
pos += 2; // skip unknown 2-byte header
}
} else {
pos++;
}
}
// Send acknowledgement
dftDataAck();
}
/**
* Write download data to the local file, handling ASCII conversion.
*/
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()) {
if (ascii == '\r') {
lastCr = true;
continue;
}
if (lastCr) {
lastCr = false;
if (ascii == '\n') {
outputStream.write('\n');
continue;
}
outputStream.write('\r');
}
}
outputStream.write(ascii);
} 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));
}
}
}
// ========== Get Request (host wants data for upload) ==========
private void dftGetRequest() {
FTConfig config = listener.getConfig();
log.fine("DFT: Get");
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
dftAbort("Transfer cancelled by user", TR_GET_REQ);
return;
}
int bufferSize = config.getDftBufferSize();
int numbytes = bufferSize - 27; // reserve space for headers
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
try {
while (!dftEof && totalRead < numbytes) {
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
// Binary read
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
}
} catch (IOException e) {
dftAbort("Read error: " + e.getMessage(), TR_GET_REQ);
return;
}
// Build SF response
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
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
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));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
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
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.
*/
private int dftAsciiRead(FTConfig config) throws IOException {
if (inputStream == null) return -1;
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');
int ebc = translator.unicodeToEbcdic((char) c);
return ebc >= 0 ? ebc : 0x40;
}
}
// ========== Close Request ==========
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);
input.sendStructuredFieldData(out.toByteArray());
}
// ========== Data Acknowledgement ==========
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);
input.sendStructuredFieldData(out.toByteArray());
}
// ========== Abort ==========
private void dftAbort(String message, int code) {
log.warning("DFT: ABORT — " + message);
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
// 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);
input.sendStructuredFieldData(out.toByteArray());
listener.onTransferAborted(message);
}
/**
* Handle a Read Modified command when upload data is pending.
* Retransmits the last saved buffer.
*/
public void readModified() {
if (dftSaveBuf != null && dftSaveBufLen > 0) {
log.fine("DFT: Retransmitting saved buffer");
input.sendStructuredFieldData(dftSaveBuf);
}
}
}