IND$FILE receiving
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user