Filetransfer fixes for VM
This commit is contained in:
@@ -353,14 +353,14 @@ public class FileTransferDialog extends JDialog {
|
|||||||
boolean isTso = (hostType == FTConfig.HostType.TSO);
|
boolean isTso = (hostType == FTConfig.HostType.TSO);
|
||||||
boolean isCms = (hostType == FTConfig.HostType.CMS);
|
boolean isCms = (hostType == FTConfig.HostType.CMS);
|
||||||
|
|
||||||
appendCheck.setEnabled(!isSend);
|
appendCheck.setEnabled(true);
|
||||||
overwriteCheck.setEnabled(!isSend);
|
overwriteCheck.setEnabled(true);
|
||||||
|
|
||||||
crCheck.setEnabled(isAscii);
|
crCheck.setEnabled(isAscii);
|
||||||
remapCheck.setEnabled(isAscii);
|
remapCheck.setEnabled(isAscii);
|
||||||
|
|
||||||
recfmField.setEnabled(isSend && isTso);
|
recfmField.setEnabled(isSend && (isTso || isCms));
|
||||||
lreclField.setEnabled(isSend && isTso);
|
lreclField.setEnabled(isSend && (isTso || isCms));
|
||||||
blksizeField.setEnabled(isSend && isTso);
|
blksizeField.setEnabled(isSend && isTso);
|
||||||
unitsCombo.setEnabled(isSend && isTso);
|
unitsCombo.setEnabled(isSend && isTso);
|
||||||
spaceField.setEnabled(isSend && isTso);
|
spaceField.setEnabled(isSend && isTso);
|
||||||
@@ -405,7 +405,7 @@ public class FileTransferDialog extends JDialog {
|
|||||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||||
hostFileField.setText(dialog.getSelectedHostFile());
|
hostFileField.setText(dialog.getSelectedHostFile());
|
||||||
HostDirectoryEntry entry = dialog.getSelectedEntry();
|
HostDirectoryEntry entry = dialog.getSelectedEntry();
|
||||||
if (entry != null) {
|
if (entry != null && hostType == FTConfig.HostType.TSO) {
|
||||||
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
|
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
|
||||||
recfmField.setText(entry.getRecfm().trim());
|
recfmField.setText(entry.getRecfm().trim());
|
||||||
}
|
}
|
||||||
@@ -443,30 +443,54 @@ public class FileTransferDialog extends JDialog {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FTConfig.HostType selectedHostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||||
|
boolean isReceive = receiveRadio.isSelected();
|
||||||
|
boolean isSend = sendRadio.isSelected();
|
||||||
|
|
||||||
FTConfig config = new FTConfig();
|
FTConfig config = new FTConfig();
|
||||||
config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem());
|
config.setHostType(selectedHostType);
|
||||||
config.setLocalFilename(localFileField.getText().trim());
|
config.setLocalFilename(localFileField.getText().trim());
|
||||||
config.setHostFilename(hostFileField.getText().trim());
|
config.setHostFilename(hostFileField.getText().trim());
|
||||||
config.setReceive(receiveRadio.isSelected());
|
config.setReceive(isReceive);
|
||||||
config.setAscii(asciiRadio.isSelected());
|
config.setAscii(asciiRadio.isSelected());
|
||||||
config.setCrFlag(crCheck.isSelected());
|
config.setCrFlag(crCheck.isSelected());
|
||||||
config.setRemapFlag(remapCheck.isSelected());
|
config.setRemapFlag(remapCheck.isSelected());
|
||||||
config.setAppend(appendCheck.isSelected());
|
if (isReceive) {
|
||||||
config.setOverwrite(overwriteCheck.isSelected());
|
config.setAppend(appendCheck.isSelected());
|
||||||
|
config.setOverwrite(overwriteCheck.isSelected());
|
||||||
|
} else {
|
||||||
|
if (overwriteCheck.isSelected()) {
|
||||||
|
config.setExistAction(FTConfig.ExistAction.REPLACE);
|
||||||
|
}
|
||||||
|
if (appendCheck.isSelected()) {
|
||||||
|
config.setAppend(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
||||||
if (mtu != null) {
|
if (mtu != null) {
|
||||||
config.setDftBufferSize(mtu);
|
config.setDftBufferSize(mtu);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setRecfm(recfmField.getText().trim());
|
if (isSend) {
|
||||||
config.setLrecl(lreclField.getText().trim());
|
if (selectedHostType == FTConfig.HostType.TSO) {
|
||||||
config.setBlksize(blksizeField.getText().trim());
|
config.setRecfm(recfmField.getText().trim());
|
||||||
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
config.setLrecl(lreclField.getText().trim());
|
||||||
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
config.setBlksize(blksizeField.getText().trim());
|
||||||
config.setSpace(spaceField.getText().trim());
|
String spaceText = spaceField.getText().trim();
|
||||||
config.setDirectoryBlocks(dirField.getText().trim());
|
String dirText = dirField.getText().trim();
|
||||||
config.setOptions(optionsField.getText().trim());
|
if (!spaceText.isEmpty() || !dirText.isEmpty()) {
|
||||||
|
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||||
|
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
||||||
|
config.setSpace(spaceText);
|
||||||
|
config.setDirectoryBlocks(dirText);
|
||||||
|
}
|
||||||
|
} else if (selectedHostType == FTConfig.HostType.CMS) {
|
||||||
|
config.setRecfm(recfmField.getText().trim());
|
||||||
|
config.setLrecl(lreclField.getText().trim());
|
||||||
|
config.setOptions(optionsField.getText().trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recordSessionTransfer();
|
recordSessionTransfer();
|
||||||
|
|
||||||
|
|||||||
@@ -1190,6 +1190,7 @@ public class DataStreamProcessor {
|
|||||||
processOutbound3270DS(data, pos, fieldLen);
|
processOutbound3270DS(data, pos, fieldLen);
|
||||||
break;
|
break;
|
||||||
case SF_TRANSFER_DATA: // 0xD0
|
case SF_TRANSFER_DATA: // 0xD0
|
||||||
|
qreplyPending = false;
|
||||||
if (ftDft != null) {
|
if (ftDft != null) {
|
||||||
ftDft.processStructuredField(data, pos, fieldLen);
|
ftDft.processStructuredField(data, pos, fieldLen);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public class FTConfig {
|
|||||||
private CrAction crAction = CrAction.REMOVE;
|
private CrAction crAction = CrAction.REMOVE;
|
||||||
private boolean remapFlag = true;
|
private boolean remapFlag = true;
|
||||||
private ExistAction existAction = ExistAction.KEEP;
|
private ExistAction existAction = ExistAction.KEEP;
|
||||||
|
private boolean allowLocalOverwrite = true;
|
||||||
private RecordFormat recfm = RecordFormat.DEFAULT;
|
private RecordFormat recfm = RecordFormat.DEFAULT;
|
||||||
private AllocationUnit units = AllocationUnit.DEFAULT;
|
private AllocationUnit units = AllocationUnit.DEFAULT;
|
||||||
private int lrecl = 0;
|
private int lrecl = 0;
|
||||||
@@ -97,7 +98,7 @@ public class FTConfig {
|
|||||||
return isAscii() && crAction != CrAction.KEEP;
|
return isAscii() && crAction != CrAction.KEEP;
|
||||||
}
|
}
|
||||||
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
||||||
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
|
public boolean isOverwrite() { return allowLocalOverwrite || existAction == ExistAction.REPLACE; }
|
||||||
public boolean isOptionNew() { return optionNew; }
|
public boolean isOptionNew() { return optionNew; }
|
||||||
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
|
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
|
||||||
public boolean isOptionNoso() { return optionNoso; }
|
public boolean isOptionNoso() { return optionNoso; }
|
||||||
@@ -210,7 +211,7 @@ public class FTConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setOverwrite(boolean overwrite) {
|
public void setOverwrite(boolean overwrite) {
|
||||||
if (overwrite) this.existAction = ExistAction.REPLACE;
|
this.allowLocalOverwrite = overwrite;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCodePage() { return codePage; }
|
public String getCodePage() { return codePage; }
|
||||||
@@ -588,7 +589,7 @@ public class FTConfig {
|
|||||||
} else if (isOptionNew()) {
|
} else if (isOptionNew()) {
|
||||||
if (opts.length() > 0) opts.append(" ");
|
if (opts.length() > 0) opts.append(" ");
|
||||||
opts.append("NEW");
|
opts.append("NEW");
|
||||||
} else if (isOverwrite()) {
|
} else if (existAction == ExistAction.REPLACE && hostType != HostType.CMS) {
|
||||||
if (opts.length() > 0) opts.append(" ");
|
if (opts.length() > 0) opts.append(" ");
|
||||||
opts.append("REPLACE");
|
opts.append("REPLACE");
|
||||||
}
|
}
|
||||||
@@ -607,12 +608,14 @@ public class FTConfig {
|
|||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
opts.append(")");
|
opts.append(")");
|
||||||
if (lrecl > 0) {
|
}
|
||||||
opts.append(" LRECL(").append(lrecl).append(")");
|
if (lrecl > 0) {
|
||||||
}
|
if (opts.length() > 0) opts.append(" ");
|
||||||
if (blksize > 0) {
|
opts.append("LRECL(").append(lrecl).append(")");
|
||||||
opts.append(" BLKSIZE(").append(blksize).append(")");
|
}
|
||||||
}
|
if (blksize > 0) {
|
||||||
|
if (opts.length() > 0) opts.append(" ");
|
||||||
|
opts.append("BLKSIZE(").append(blksize).append(")");
|
||||||
}
|
}
|
||||||
if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) {
|
if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) {
|
||||||
AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS;
|
AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS;
|
||||||
@@ -641,12 +644,10 @@ public class FTConfig {
|
|||||||
case VARIABLE: opts.append("V"); break;
|
case VARIABLE: opts.append("V"); break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
if (lrecl > 0) {
|
}
|
||||||
opts.append(" LRECL ").append(lrecl);
|
if (lrecl > 0) {
|
||||||
}
|
if (opts.length() > 0) opts.append(" ");
|
||||||
if (blksize > 0) {
|
opts.append("LRECL ").append(lrecl);
|
||||||
opts.append(" BLOCK ").append(blksize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ public class FTCut {
|
|||||||
private InputStream inputStream;
|
private InputStream inputStream;
|
||||||
private OutputStream outputStream;
|
private OutputStream outputStream;
|
||||||
private boolean lastCr = false;
|
private boolean lastCr = false;
|
||||||
|
private int lookaheadByte = -1;
|
||||||
|
|
||||||
public FTCut(ScreenBuffer screen, InputProcessor input,
|
public FTCut(ScreenBuffer screen, InputProcessor input,
|
||||||
EbcdicTranslator translator, FTCutListener listener) {
|
EbcdicTranslator translator, FTCutListener listener) {
|
||||||
@@ -181,6 +182,7 @@ public class FTCut {
|
|||||||
xlateBufIx = 0;
|
xlateBufIx = 0;
|
||||||
cutEof = false;
|
cutEof = false;
|
||||||
lastCr = false;
|
lastCr = false;
|
||||||
|
lookaheadByte = -1;
|
||||||
retransmitRetries = 0;
|
retransmitRetries = 0;
|
||||||
lastUploadCount = 0;
|
lastUploadCount = 0;
|
||||||
lastUploadSeq = 0;
|
lastUploadSeq = 0;
|
||||||
@@ -217,11 +219,17 @@ public class FTCut {
|
|||||||
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
||||||
// Not a CUT frame (likely local echo, menu return, or host error screen)
|
// Not a CUT frame (likely local echo, menu return, or host error screen)
|
||||||
if (xferInProgress) {
|
if (xferInProgress) {
|
||||||
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
|
||||||
xferInProgress = false;
|
xferInProgress = false;
|
||||||
String hostErr = scanScreenForErrors();
|
String screenUpper = screen.getText().toUpperCase();
|
||||||
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
|
if (screenUpper.contains("TRANS03") || screenUpper.contains("TRANS04") || screenUpper.contains("COMPLETE")) {
|
||||||
listener.onTransferAborted(abortMsg);
|
log.info("CUT: Host screen indicated transfer complete");
|
||||||
|
listener.onTransferComplete(null);
|
||||||
|
} else {
|
||||||
|
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
||||||
|
String hostErr = scanScreenForErrors();
|
||||||
|
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
|
||||||
|
listener.onTransferAborted(abortMsg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -270,6 +278,9 @@ public class FTCut {
|
|||||||
String line = lineSb.toString().trim();
|
String line = lineSb.toString().trim();
|
||||||
if (line.isEmpty()) continue;
|
if (line.isEmpty()) continue;
|
||||||
String lineUpper = line.toUpperCase();
|
String lineUpper = line.toUpperCase();
|
||||||
|
if (lineUpper.contains("TRANS03") || lineUpper.contains("TRANS04") || lineUpper.contains("COMPLETE") || lineUpper.contains("TERMINAL -> HOST COMPLETE")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (String prefix : errorPrefixes) {
|
for (String prefix : errorPrefixes) {
|
||||||
if (lineUpper.contains(prefix)) {
|
if (lineUpper.contains(prefix)) {
|
||||||
return line;
|
return line;
|
||||||
@@ -309,14 +320,18 @@ public class FTCut {
|
|||||||
switch (code) {
|
switch (code) {
|
||||||
case SC_HOST_ACK:
|
case SC_HOST_ACK:
|
||||||
log.info("CUT: HOST_ACK received — transfer running");
|
log.info("CUT: HOST_ACK received — transfer running");
|
||||||
|
listener.onCutRunning();
|
||||||
|
if (listener.getCurrentState() == FTState.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
xferInProgress = true;
|
xferInProgress = true;
|
||||||
expandedLength = 0;
|
expandedLength = 0;
|
||||||
quadrant = -1;
|
quadrant = -1;
|
||||||
xlateBuffered = 0;
|
xlateBuffered = 0;
|
||||||
xlateBufIx = 0;
|
xlateBufIx = 0;
|
||||||
cutEof = false;
|
cutEof = false;
|
||||||
|
lastCr = false;
|
||||||
cutAck();
|
cutAck();
|
||||||
listener.onCutRunning();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SC_XFER_COMPLETE:
|
case SC_XFER_COMPLETE:
|
||||||
@@ -378,6 +393,14 @@ public class FTCut {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (listener.getCurrentState() == FTState.AWAIT_ACK) {
|
||||||
|
listener.onCutRunning();
|
||||||
|
if (listener.getCurrentState() == FTState.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xferInProgress = true;
|
||||||
|
}
|
||||||
|
|
||||||
FTConfig config = listener.getConfig();
|
FTConfig config = listener.getConfig();
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -439,6 +462,14 @@ public class FTCut {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (listener.getCurrentState() == FTState.AWAIT_ACK) {
|
||||||
|
listener.onCutRunning();
|
||||||
|
if (listener.getCurrentState() == FTState.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xferInProgress = true;
|
||||||
|
}
|
||||||
|
|
||||||
FTConfig config = listener.getConfig();
|
FTConfig config = listener.getConfig();
|
||||||
|
|
||||||
int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
|
int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
|
||||||
@@ -615,6 +646,16 @@ public class FTCut {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int readRawByte() throws IOException {
|
||||||
|
if (lookaheadByte != -1) {
|
||||||
|
int b = lookaheadByte;
|
||||||
|
lookaheadByte = -1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (inputStream == null) return -1;
|
||||||
|
return inputStream.read();
|
||||||
|
}
|
||||||
|
|
||||||
private int xlateGetc(FTConfig config) throws IOException {
|
private int xlateGetc(FTConfig config) throws IOException {
|
||||||
if (xlateBuffered > 0) {
|
if (xlateBuffered > 0) {
|
||||||
int r = xlateBuf[xlateBufIx++];
|
int r = xlateBuf[xlateBufIx++];
|
||||||
@@ -622,24 +663,40 @@ public class FTCut {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputStream == null) return -1;
|
int c = readRawByte();
|
||||||
|
|
||||||
int c = inputStream.read();
|
|
||||||
if (c == -1) return -1;
|
if (c == -1) return -1;
|
||||||
|
|
||||||
int localByte = c & 0xFF;
|
int[] cbuf = new int[32];
|
||||||
int nc = 0;
|
int nc = 0;
|
||||||
int[] cbuf = new int[4];
|
|
||||||
|
|
||||||
if (config.isAscii()) {
|
if (config.isAscii() && config.isCrFlag()) {
|
||||||
if (config.isCrFlag() && !lastCr && localByte == '\n') {
|
if (c == '\r') {
|
||||||
nc += uploadConvert('\r', cbuf, nc, config);
|
int next = readRawByte();
|
||||||
|
while (next == '\r') {
|
||||||
|
next = readRawByte();
|
||||||
|
}
|
||||||
|
if (next != '\n' && next != -1) {
|
||||||
|
lookaheadByte = next;
|
||||||
|
}
|
||||||
|
nc = uploadConvert('\r', cbuf, 0, config);
|
||||||
|
nc += uploadConvert('\n', cbuf, nc, config);
|
||||||
|
} else if (c == '\n') {
|
||||||
|
int next = readRawByte();
|
||||||
|
while (next == '\r') {
|
||||||
|
next = readRawByte();
|
||||||
|
}
|
||||||
|
if (next != -1) {
|
||||||
|
lookaheadByte = next;
|
||||||
|
}
|
||||||
|
nc = uploadConvert('\r', cbuf, 0, config);
|
||||||
|
nc += uploadConvert('\n', cbuf, nc, config);
|
||||||
|
} else {
|
||||||
|
nc = uploadConvert(c & 0xFF, cbuf, 0, config);
|
||||||
}
|
}
|
||||||
lastCr = (localByte == '\r');
|
} else {
|
||||||
|
nc = uploadConvert(c & 0xFF, cbuf, 0, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
nc += uploadConvert(localByte, cbuf, nc, config);
|
|
||||||
|
|
||||||
if (nc > 1) {
|
if (nc > 1) {
|
||||||
for (int i = 1; i < nc; i++) {
|
for (int i = 1; i < nc; i++) {
|
||||||
xlateBuf[xlateBuffered++] = cbuf[i];
|
xlateBuf[xlateBuffered++] = cbuf[i];
|
||||||
@@ -665,10 +722,10 @@ public class FTCut {
|
|||||||
|
|
||||||
if (!config.isAscii() || !config.isRemapFlag()) {
|
if (!config.isAscii() || !config.isRemapFlag()) {
|
||||||
ebc = localByte & 0xFF;
|
ebc = localByte & 0xFF;
|
||||||
} else if (localByte < 0x20 || (localByte >= 0x80 && localByte < 0x9F)) {
|
} else if (localByte < 0x20 || localByte == 0x7F || (localByte >= 0x80 && localByte < 0x9F)) {
|
||||||
ebc = localByte & 0xFF;
|
ebc = FTConstants.FT2ASC[FTConstants.ASC2FT[localByte & 0xFF]];
|
||||||
} else if (localByte == 0x9F) {
|
} else if (localByte == 0x9F) {
|
||||||
ebc = 0xFF;
|
ebc = FTConstants.FT2ASC[0xFF];
|
||||||
} else {
|
} else {
|
||||||
int standardEbc = translator.unicodeToEbcdic((char) localByte);
|
int standardEbc = translator.unicodeToEbcdic((char) localByte);
|
||||||
if (standardEbc < 0) {
|
if (standardEbc < 0) {
|
||||||
|
|||||||
@@ -41,13 +41,16 @@ public class FTDft {
|
|||||||
private long recnum = 1;
|
private long recnum = 1;
|
||||||
private boolean dftEof = false;
|
private boolean dftEof = false;
|
||||||
private boolean messageFlag = false;
|
private boolean messageFlag = false;
|
||||||
|
private boolean dataStreamOpened = false;
|
||||||
private long bytesTransferred = 0;
|
private long bytesTransferred = 0;
|
||||||
private long estimatedTotalBytes = 0;
|
private long estimatedTotalBytes = 0;
|
||||||
private int hostLrecl = 0;
|
private int hostLrecl = 0;
|
||||||
private String hostRecfm = "";
|
private String hostRecfm = "";
|
||||||
private int hostBlksize = 0;
|
private int hostBlksize = 0;
|
||||||
|
private int openRecsz = 0;
|
||||||
private int customMtuSize = 0;
|
private int customMtuSize = 0;
|
||||||
private int pendingByte = -1;
|
private int pendingByte = -1;
|
||||||
|
private int lookaheadByte = -1;
|
||||||
|
|
||||||
// Record formatting buffers and SO/SI tracking
|
// Record formatting buffers and SO/SI tracking
|
||||||
private static final int MAX_DFT_RECORD_BUFFER_SIZE = 131072; // 128KB sanity cap
|
private static final int MAX_DFT_RECORD_BUFFER_SIZE = 131072; // 128KB sanity cap
|
||||||
@@ -159,15 +162,18 @@ public class FTDft {
|
|||||||
recnum = 1;
|
recnum = 1;
|
||||||
dftEof = false;
|
dftEof = false;
|
||||||
messageFlag = false;
|
messageFlag = false;
|
||||||
|
dataStreamOpened = false;
|
||||||
bytesTransferred = 0;
|
bytesTransferred = 0;
|
||||||
estimatedTotalBytes = 0;
|
estimatedTotalBytes = 0;
|
||||||
hostLrecl = 0;
|
hostLrecl = 0;
|
||||||
hostRecfm = "";
|
hostRecfm = "";
|
||||||
hostBlksize = 0;
|
hostBlksize = 0;
|
||||||
|
openRecsz = 0;
|
||||||
dftSaveBuf = null;
|
dftSaveBuf = null;
|
||||||
dftSaveBufLen = 0;
|
dftSaveBufLen = 0;
|
||||||
lastCr = false;
|
lastCr = false;
|
||||||
pendingByte = -1;
|
pendingByte = -1;
|
||||||
|
lookaheadByte = -1;
|
||||||
downloadRecordBuffer.reset();
|
downloadRecordBuffer.reset();
|
||||||
dbcsMode = false;
|
dbcsMode = false;
|
||||||
hasPendingByteInDBCS = false;
|
hasPendingByteInDBCS = false;
|
||||||
@@ -216,8 +222,8 @@ public class FTDft {
|
|||||||
|
|
||||||
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
||||||
|
|
||||||
// Check if this is a host error reply (e.g. TR_ERROR_REPLY 0x08, or contains TR_ERROR_HDR 0x6904)
|
// Check if this is a host error reply (TR_ERROR_REPLY 0x08)
|
||||||
if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) {
|
if ((requestCode & 0xFF) == TR_ERROR_REPLY) {
|
||||||
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
|
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -249,15 +255,6 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isErrorField(byte[] data, int start, int length) {
|
|
||||||
int end = Math.min(start + length, data.length);
|
|
||||||
for (int i = start; i + 3 < end; i++) {
|
|
||||||
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
|
|
||||||
if (hdr == TR_ERROR_HDR) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
|
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
|
||||||
int end = Math.min(payloadStart + length, data.length);
|
int end = Math.min(payloadStart + length, data.length);
|
||||||
int errCode = 0;
|
int errCode = 0;
|
||||||
@@ -320,20 +317,24 @@ public class FTDft {
|
|||||||
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
||||||
log.fine("DFT: Open request");
|
log.fine("DFT: Open request");
|
||||||
|
|
||||||
// Parse open request payload matching x3270 / DDM open
|
// Parse open request payload matching x3270 / HoD Xfer3270:
|
||||||
// sfLength is the 2-byte length value at sfOffset
|
// sfLenVal is the 2-byte length value at sfOffset (or sfLength when 0x0000)
|
||||||
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
|
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
|
||||||
|
if (sfLenVal == 0) {
|
||||||
|
sfLenVal = sfLength;
|
||||||
|
}
|
||||||
String nameBuf = "";
|
String nameBuf = "";
|
||||||
|
int parsedRecsz = 0;
|
||||||
|
|
||||||
if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) {
|
if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) {
|
||||||
nameBuf = extractName(data, sfOffset + 3 + 25, 7);
|
nameBuf = extractName(data, sfOffset + 3 + 25, 7);
|
||||||
} else if (sfLenVal == 0x29 && sfOffset + 3 + 31 <= data.length) {
|
} else if (sfLenVal == 0x29 && sfOffset + 3 + 31 <= data.length) {
|
||||||
|
parsedRecsz = ((data[sfOffset + 3 + 27] & 0xFF) << 8) | (data[sfOffset + 3 + 28] & 0xFF);
|
||||||
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
||||||
|
} else {
|
||||||
|
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for host DDM file attributes embedded in open request
|
|
||||||
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
|
||||||
|
|
||||||
int sfEnd = Math.min(sfOffset + sfLength, data.length);
|
int sfEnd = Math.min(sfOffset + sfLength, data.length);
|
||||||
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
|
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
|
||||||
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
|
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
|
||||||
@@ -342,7 +343,16 @@ public class FTDft {
|
|||||||
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
||||||
} else {
|
} else {
|
||||||
messageFlag = false;
|
messageFlag = false;
|
||||||
|
dataStreamOpened = true;
|
||||||
listener.onDftRunning();
|
listener.onDftRunning();
|
||||||
|
if (listener.getCurrentState() == FTState.NONE) {
|
||||||
|
sendOpenFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openRecsz = parsedRecsz;
|
||||||
|
if (parsedRecsz > 0) {
|
||||||
|
hostLrecl = parsedRecsz;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dftEof = false;
|
dftEof = false;
|
||||||
@@ -352,28 +362,45 @@ public class FTDft {
|
|||||||
dftOpenAck();
|
dftOpenAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseDdmAttributes(byte[] data, int start, int length) {
|
private void parseDdmAttributes(byte[] data, int offset, int length) {
|
||||||
int end = Math.min(start + length, data.length);
|
int pos = offset + 2; // skip 2-byte request code (0x0012)
|
||||||
int pos = start + 2; // skip TR_OPEN_REQ
|
int end = Math.min(offset + length, data.length);
|
||||||
while (pos + 3 <= end) {
|
while (pos + 3 <= end) {
|
||||||
int attrType = data[pos] & 0xFF;
|
int attrId = data[pos] & 0xFF;
|
||||||
int attrLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
int attrLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
||||||
if (attrLen < 3 || pos + attrLen > end) {
|
if (attrLen < 3 || pos + attrLen > end) {
|
||||||
pos++;
|
pos++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (attrType == DDM_HDR_LRECL && attrLen >= 5) {
|
switch (attrId) {
|
||||||
hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
case DDM_HDR_LRECL:
|
||||||
} else if (attrType == DDM_HDR_RECFM && attrLen >= 4) {
|
if (attrLen >= 5) {
|
||||||
int r = data[pos + 3] & 0xFF;
|
hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
||||||
hostRecfm = (r == 1) ? "F" : (r == 2 ? "V" : "U");
|
}
|
||||||
} else if (attrType == DDM_HDR_BLKSIZE && attrLen >= 5) {
|
break;
|
||||||
hostBlksize = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
case DDM_HDR_RECFM:
|
||||||
} else if (attrType == DDM_HDR_FILESIZE && attrLen >= 7) {
|
if (attrLen >= 4) {
|
||||||
estimatedTotalBytes = (((long)(data[pos + 3] & 0xFF)) << 24) |
|
int r = data[pos + 3] & 0xFF;
|
||||||
(((long)(data[pos + 4] & 0xFF)) << 16) |
|
if (r == 1 || r == 'F' || r == 0xC6) hostRecfm = "F";
|
||||||
(((long)(data[pos + 5] & 0xFF)) << 8) |
|
else if (r == 2 || r == 'V' || r == 0xE5) hostRecfm = "V";
|
||||||
((long)(data[pos + 6] & 0xFF));
|
else if (r == 3 || r == 'U' || r == 0xE4) hostRecfm = "U";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DDM_HDR_BLKSIZE:
|
||||||
|
if (attrLen >= 5) {
|
||||||
|
hostBlksize = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DDM_HDR_FILESIZE:
|
||||||
|
if (attrLen >= 7) {
|
||||||
|
estimatedTotalBytes = ((long) (data[pos + 3] & 0xFF) << 24) |
|
||||||
|
((long) (data[pos + 4] & 0xFF) << 16) |
|
||||||
|
((long) (data[pos + 5] & 0xFF) << 8) |
|
||||||
|
(data[pos + 6] & 0xFF);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
pos += attrLen;
|
pos += attrLen;
|
||||||
}
|
}
|
||||||
@@ -515,7 +542,7 @@ public class FTDft {
|
|||||||
log.info("DFT host message: " + msg);
|
log.info("DFT host message: " + msg);
|
||||||
|
|
||||||
String msgUpper = msg.toUpperCase();
|
String msgUpper = msg.toUpperCase();
|
||||||
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS") || msgUpper.contains("DFH0500") || msgUpper.contains("DFH0501")) {
|
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.startsWith("TRANS04") || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS") || msgUpper.contains("DFH0500") || msgUpper.contains("DFH0501")) {
|
||||||
listener.onTransferComplete(null);
|
listener.onTransferComplete(null);
|
||||||
} else if (msgUpper.startsWith("TRANS") || msgUpper.startsWith("DFH") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || (listener != null && listener.getCurrentState() == FTState.ABORT_SENT)) {
|
} else if (msgUpper.startsWith("TRANS") || msgUpper.startsWith("DFH") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || (listener != null && listener.getCurrentState() == FTState.ABORT_SENT)) {
|
||||||
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
||||||
@@ -526,8 +553,8 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping.
|
* Write download data to the local file, handling ASCII conversion and EOF stripping.
|
||||||
* Conforms to IBM Host On-Demand specification.
|
* Conforms to x3270 / IBM Host On-Demand specification.
|
||||||
*/
|
*/
|
||||||
private void writeDownloadData(byte[] data, int offset, int length,
|
private void writeDownloadData(byte[] data, int offset, int length,
|
||||||
FTConfig config) throws IOException {
|
FTConfig config) throws IOException {
|
||||||
@@ -543,76 +570,7 @@ public class FTDft {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
writeConvertedChunk(data, offset, length, config);
|
||||||
"V".equalsIgnoreCase(hostRecfm);
|
|
||||||
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
|
||||||
"F".equalsIgnoreCase(hostRecfm);
|
|
||||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
|
||||||
|
|
||||||
if (isRecfmV) {
|
|
||||||
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
|
|
||||||
downloadRecordBuffer.reset();
|
|
||||||
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
|
|
||||||
}
|
|
||||||
downloadRecordBuffer.write(data, offset, length);
|
|
||||||
processVariableDownloadRecords(config);
|
|
||||||
} else if (isRecfmF && lrecl > 0) {
|
|
||||||
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
|
|
||||||
downloadRecordBuffer.reset();
|
|
||||||
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
|
|
||||||
}
|
|
||||||
downloadRecordBuffer.write(data, offset, length);
|
|
||||||
processFixedDownloadRecords(lrecl, config);
|
|
||||||
} else {
|
|
||||||
writeConvertedChunk(data, offset, length, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processVariableDownloadRecords(FTConfig config) throws IOException {
|
|
||||||
while (downloadRecordBuffer.size() >= 4) {
|
|
||||||
byte[] buf = downloadRecordBuffer.toByteArray();
|
|
||||||
int rdwLen = ((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF);
|
|
||||||
if (rdwLen < 4) {
|
|
||||||
downloadRecordBuffer.reset();
|
|
||||||
if (buf.length > 1) {
|
|
||||||
downloadRecordBuffer.write(buf, 1, buf.length - 1);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (buf.length < rdwLen) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int recDataLen = rdwLen - 4;
|
|
||||||
writeConvertedChunk(buf, 4, recDataLen, config);
|
|
||||||
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
downloadRecordBuffer.reset();
|
|
||||||
if (buf.length > rdwLen) {
|
|
||||||
downloadRecordBuffer.write(buf, rdwLen, buf.length - rdwLen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processFixedDownloadRecords(int lrecl, FTConfig config) throws IOException {
|
|
||||||
while (downloadRecordBuffer.size() >= lrecl) {
|
|
||||||
byte[] buf = downloadRecordBuffer.toByteArray();
|
|
||||||
int end = lrecl;
|
|
||||||
while (end > 0) {
|
|
||||||
int b = buf[end - 1] & 0xFF;
|
|
||||||
if (b == 0x20 || b == 0x40 || (config != null && config.isRemapFlag() && b == FT2ASC[0x40])) {
|
|
||||||
end--;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeConvertedChunk(buf, 0, end, config);
|
|
||||||
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
downloadRecordBuffer.reset();
|
|
||||||
if (buf.length > lrecl) {
|
|
||||||
downloadRecordBuffer.write(buf, lrecl, buf.length - lrecl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void flushRemainingDownloadRecords() throws IOException {
|
private void flushRemainingDownloadRecords() throws IOException {
|
||||||
@@ -690,45 +648,36 @@ public class FTDft {
|
|||||||
|
|
||||||
int bufferSize = getMTUSize();
|
int bufferSize = getMTUSize();
|
||||||
int numbytes = bufferSize - 27;
|
int numbytes = bufferSize - 27;
|
||||||
|
if (openRecsz > 0 && openRecsz < numbytes) {
|
||||||
|
numbytes = openRecsz;
|
||||||
|
}
|
||||||
byte[] readBuf = new byte[numbytes];
|
byte[] readBuf = new byte[numbytes];
|
||||||
int totalRead = 0;
|
int totalRead = 0;
|
||||||
|
|
||||||
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
|
||||||
"F".equalsIgnoreCase(hostRecfm);
|
|
||||||
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
|
||||||
"V".equalsIgnoreCase(hostRecfm);
|
|
||||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (config != null && config.isAscii() && isRecfmF && lrecl > 0) {
|
while (!dftEof && totalRead < numbytes) {
|
||||||
totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config);
|
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
||||||
} else if (config != null && config.isAscii() && isRecfmV) {
|
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
||||||
totalRead = readVariableRecordUpload(readBuf, numbytes, config);
|
if (n <= 0) {
|
||||||
} else {
|
dftEof = true;
|
||||||
while (!dftEof && totalRead < numbytes) {
|
break;
|
||||||
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
|
||||||
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
|
||||||
if (n <= 0) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
totalRead += n;
|
|
||||||
} else if (config != null && config.isAscii()) {
|
|
||||||
int b = dftAsciiRead(config);
|
|
||||||
if (b == -1) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
readBuf[totalRead++] = (byte) b;
|
|
||||||
} else {
|
|
||||||
if (inputStream == null) { dftEof = true; break; }
|
|
||||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
|
||||||
if (n <= 0) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
totalRead += n;
|
|
||||||
}
|
}
|
||||||
|
totalRead += n;
|
||||||
|
} else if (config != null && config.isAscii()) {
|
||||||
|
int b = dftAsciiRead(config);
|
||||||
|
if (b == -1) {
|
||||||
|
dftEof = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
readBuf[totalRead++] = (byte) b;
|
||||||
|
} else {
|
||||||
|
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) {
|
} catch (IOException e) {
|
||||||
@@ -790,147 +739,6 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] readNextUploadLineConverted(FTConfig config) throws IOException {
|
|
||||||
if (inputStream == null || dftEof) return null;
|
|
||||||
ByteArrayOutputStream lineBytes = new ByteArrayOutputStream();
|
|
||||||
int c;
|
|
||||||
boolean hasData = false;
|
|
||||||
while ((c = inputStream.read()) != -1) {
|
|
||||||
hasData = true;
|
|
||||||
if (c == '\r') {
|
|
||||||
continue; // ignore CR
|
|
||||||
}
|
|
||||||
if (c == '\n') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (c == EOF_CTRL_Z || c == EOF_CTRL_D) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
lineBytes.write(c);
|
|
||||||
}
|
|
||||||
if (!hasData && c == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] raw = lineBytes.toByteArray();
|
|
||||||
return convertLineForUpload(raw, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] convertLineForUpload(byte[] raw, FTConfig config) {
|
|
||||||
String lineStr = new String(raw, StandardCharsets.UTF_8);
|
|
||||||
ByteArrayOutputStream converted = new ByteArrayOutputStream(raw.length + 8);
|
|
||||||
boolean isDbcs = (config != null && config.isDbcs()) || (translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS());
|
|
||||||
boolean noSo = config != null && config.isOptionNoso();
|
|
||||||
boolean inDbcsState = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < lineStr.length(); i++) {
|
|
||||||
char ch = lineStr.charAt(i);
|
|
||||||
int dbcsVal = (isDbcs && translator != null) ? translator.unicodeToDbcs(ch) : -1;
|
|
||||||
if (dbcsVal != -1) {
|
|
||||||
if (!inDbcsState && !noSo) {
|
|
||||||
converted.write(0x0E); // Shift-Out
|
|
||||||
inDbcsState = true;
|
|
||||||
}
|
|
||||||
converted.write((dbcsVal >> 8) & 0xFF);
|
|
||||||
converted.write(dbcsVal & 0xFF);
|
|
||||||
} else {
|
|
||||||
if (inDbcsState && !noSo) {
|
|
||||||
converted.write(0x0F); // Shift-In
|
|
||||||
inDbcsState = false;
|
|
||||||
}
|
|
||||||
converted.write(convertAsciiByte(ch, config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inDbcsState && !noSo) {
|
|
||||||
converted.write(0x0F); // Shift-In at end of record
|
|
||||||
}
|
|
||||||
return converted.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readFixedRecordUpload(byte[] outBuf, int maxBytes, int lrecl, FTConfig config) throws IOException {
|
|
||||||
int totalRead = 0;
|
|
||||||
int padByte = (config != null && config.isRemapFlag()) ? FT2ASC[0x40] : 0x20;
|
|
||||||
|
|
||||||
while (totalRead + lrecl <= maxBytes && !dftEof) {
|
|
||||||
byte[] rec;
|
|
||||||
if (pendingUploadRecord != null) {
|
|
||||||
rec = pendingUploadRecord;
|
|
||||||
pendingUploadRecord = null;
|
|
||||||
} else {
|
|
||||||
rec = readNextUploadLineConverted(config);
|
|
||||||
if (rec == null) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rec.length <= lrecl) {
|
|
||||||
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
|
||||||
totalRead += rec.length;
|
|
||||||
int padCount = lrecl - rec.length;
|
|
||||||
for (int p = 0; p < padCount; p++) {
|
|
||||||
outBuf[totalRead++] = (byte) padByte;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Longer than lrecl: split
|
|
||||||
System.arraycopy(rec, 0, outBuf, totalRead, lrecl);
|
|
||||||
totalRead += lrecl;
|
|
||||||
int rem = rec.length - lrecl;
|
|
||||||
pendingUploadRecord = new byte[rem];
|
|
||||||
System.arraycopy(rec, lrecl, pendingUploadRecord, 0, rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readVariableRecordUpload(byte[] outBuf, int maxBytes, FTConfig config) throws IOException {
|
|
||||||
int totalRead = 0;
|
|
||||||
while (totalRead + 4 < maxBytes && !dftEof) {
|
|
||||||
byte[] rec;
|
|
||||||
if (pendingUploadRecord != null) {
|
|
||||||
rec = pendingUploadRecord;
|
|
||||||
pendingUploadRecord = null;
|
|
||||||
} else {
|
|
||||||
rec = readNextUploadLineConverted(config);
|
|
||||||
if (rec == null) {
|
|
||||||
dftEof = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int needed = rec.length + 4;
|
|
||||||
if (totalRead + needed <= maxBytes) {
|
|
||||||
outBuf[totalRead++] = (byte) ((needed >> 8) & 0xFF);
|
|
||||||
outBuf[totalRead++] = (byte) (needed & 0xFF);
|
|
||||||
outBuf[totalRead++] = 0;
|
|
||||||
outBuf[totalRead++] = 0;
|
|
||||||
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
|
||||||
totalRead += rec.length;
|
|
||||||
} else {
|
|
||||||
if (totalRead > 0) {
|
|
||||||
// Save for next MTU buffer
|
|
||||||
pendingUploadRecord = rec;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// Single record exceeds entire MTU: split
|
|
||||||
int chunk = maxBytes - 4;
|
|
||||||
int rdwLen = chunk + 4;
|
|
||||||
outBuf[totalRead++] = (byte) ((rdwLen >> 8) & 0xFF);
|
|
||||||
outBuf[totalRead++] = (byte) (rdwLen & 0xFF);
|
|
||||||
outBuf[totalRead++] = 0;
|
|
||||||
outBuf[totalRead++] = 0;
|
|
||||||
System.arraycopy(rec, 0, outBuf, totalRead, chunk);
|
|
||||||
totalRead += chunk;
|
|
||||||
int rem = rec.length - chunk;
|
|
||||||
pendingUploadRecord = new byte[rem];
|
|
||||||
System.arraycopy(rec, chunk, pendingUploadRecord, 0, rem);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Explicitly send a framed data packet buffer to the host using DFT structured fields.
|
* Explicitly send a framed data packet buffer to the host using DFT structured fields.
|
||||||
* Supports CICS, TSO, and VM/CMS data packaging.
|
* Supports CICS, TSO, and VM/CMS data packaging.
|
||||||
@@ -996,6 +804,16 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int readRawByte() throws IOException {
|
||||||
|
if (lookaheadByte != -1) {
|
||||||
|
int b = lookaheadByte;
|
||||||
|
lookaheadByte = -1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (inputStream == null) return -1;
|
||||||
|
return inputStream.read();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a byte from local file for upload, handling ASCII conversion and remapping.
|
* Read a byte from local file for upload, handling ASCII conversion and remapping.
|
||||||
* Matching x3270 dft_ascii_read logic.
|
* Matching x3270 dft_ascii_read logic.
|
||||||
@@ -1007,26 +825,39 @@ public class FTDft {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputStream == null) return -1;
|
int c = readRawByte();
|
||||||
|
|
||||||
int c = inputStream.read();
|
|
||||||
if (c == -1) return -1;
|
if (c == -1) return -1;
|
||||||
|
|
||||||
if (config.isCrFlag() && !lastCr && c == '\n') {
|
if (config.isCrFlag()) {
|
||||||
lastCr = false;
|
if (c == '\r') {
|
||||||
// Expand \n to \r\n: buffer \n in pendingByte, return \r now
|
int next = readRawByte();
|
||||||
pendingByte = convertAsciiByte('\n', config);
|
while (next == '\r') {
|
||||||
return convertAsciiByte('\r', config);
|
next = readRawByte();
|
||||||
|
}
|
||||||
|
if (next != '\n' && next != -1) {
|
||||||
|
lookaheadByte = next;
|
||||||
|
}
|
||||||
|
pendingByte = convertAsciiByte('\n', config);
|
||||||
|
return convertAsciiByte('\r', config);
|
||||||
|
} else if (c == '\n') {
|
||||||
|
int next = readRawByte();
|
||||||
|
while (next == '\r') {
|
||||||
|
next = readRawByte();
|
||||||
|
}
|
||||||
|
if (next != -1) {
|
||||||
|
lookaheadByte = next;
|
||||||
|
}
|
||||||
|
pendingByte = convertAsciiByte('\n', config);
|
||||||
|
return convertAsciiByte('\r', config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastCr = (c == '\r');
|
|
||||||
|
|
||||||
return convertAsciiByte((char) c, config);
|
return convertAsciiByte((char) c, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int convertAsciiByte(char c, FTConfig config) {
|
private int convertAsciiByte(char c, FTConfig config) {
|
||||||
if (!config.isRemapFlag()) {
|
if (!config.isRemapFlag()) {
|
||||||
int ebc = translator.unicodeToEbcdic(c);
|
return c & 0xFF;
|
||||||
return ebc >= 0 ? ebc : 0x40;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1034,7 +865,7 @@ public class FTDft {
|
|||||||
* Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc].
|
* Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc].
|
||||||
*/
|
*/
|
||||||
int ebc;
|
int ebc;
|
||||||
if (c < 0x20 || (c >= 0x80 && c < 0x9F)) {
|
if (c < 0x20 || c == 0x7F || (c >= 0x80 && c < 0x9F)) {
|
||||||
ebc = ASC2FT[c & 0xFF];
|
ebc = ASC2FT[c & 0xFF];
|
||||||
} else if (c == 0x9F) {
|
} else if (c == 0x9F) {
|
||||||
ebc = 0xFF;
|
ebc = 0xFF;
|
||||||
@@ -1059,9 +890,13 @@ public class FTDft {
|
|||||||
|
|
||||||
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
|
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
|
||||||
|
|
||||||
if (listener.getCurrentState() != FTState.NONE) {
|
if (messageFlag || !dataStreamOpened) {
|
||||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
if (listener != null && listener.getCurrentState() != FTState.NONE) {
|
||||||
listener.onTransferComplete(null);
|
log.info("DFT: File transfer completed on close (" + bytesTransferred + " bytes)");
|
||||||
|
listener.onTransferComplete(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataStreamOpened = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,13 +906,24 @@ public class FTDft {
|
|||||||
input.sendStructuredFieldData(createDataAck(recnum++));
|
input.sendStructuredFieldData(createDataAck(recnum++));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendOpenFailed() {
|
||||||
|
if (RB_OPEN_FAILED.length > 0 && (RB_OPEN_FAILED[0] & 0xFF) == AID_SF) {
|
||||||
|
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
|
||||||
|
} else {
|
||||||
|
byte[] pkt = new byte[RB_OPEN_FAILED.length + 1];
|
||||||
|
pkt[0] = (byte) AID_SF;
|
||||||
|
System.arraycopy(RB_OPEN_FAILED, 0, pkt, 1, RB_OPEN_FAILED.length);
|
||||||
|
input.sendStructuredFieldData(pkt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Abort ==========
|
// ========== Abort ==========
|
||||||
|
|
||||||
private void dftAbort(String message, int code) {
|
private void dftAbort(String message, int code) {
|
||||||
log.warning("DFT: ABORT — " + message);
|
log.warning("DFT: ABORT — " + message);
|
||||||
|
|
||||||
if (code == TR_OPEN_REQ) {
|
if (code == TR_OPEN_REQ) {
|
||||||
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
|
sendOpenFailed();
|
||||||
} else {
|
} else {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
||||||
out.write(AID_SF);
|
out.write(AID_SF);
|
||||||
|
|||||||
@@ -1977,8 +1977,15 @@ public class InputProcessor {
|
|||||||
public void sendAidForFT(int aidCode) {
|
public void sendAidForFT(int aidCode) {
|
||||||
boolean wasLocked = keyboardLocked;
|
boolean wasLocked = keyboardLocked;
|
||||||
keyboardLocked = false;
|
keyboardLocked = false;
|
||||||
sendAid(aidCode);
|
int prevInhibit = (oia != null) ? oia.getInputInhibited() : ECLConstants.INHIBIT_NOT_INHIBITED;
|
||||||
keyboardLocked = wasLocked;
|
if (oia != null) {
|
||||||
|
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sendAid(aidCode);
|
||||||
|
} finally {
|
||||||
|
keyboardLocked = wasLocked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2039,11 +2046,11 @@ public class InputProcessor {
|
|||||||
// Find the field at cursor and erase it
|
// Find the field at cursor and erase it
|
||||||
int curAddr = screen.getCursorAddress();
|
int curAddr = screen.getCursorAddress();
|
||||||
byte faVal = screen.getFieldAttributeAt(curAddr);
|
byte faVal = screen.getFieldAttributeAt(curAddr);
|
||||||
if (faIsProtected(faVal & 0xFF)) {
|
if (screen.getCell(curAddr).isFieldAttribute() || faIsProtected(faVal & 0xFF)) {
|
||||||
// Try to find first unprotected field
|
// Try to find first unprotected field
|
||||||
curAddr = screen.findNextUnprotected(curAddr);
|
curAddr = screen.findNextUnprotected(curAddr);
|
||||||
faVal = screen.getFieldAttributeAt(curAddr);
|
faVal = screen.getFieldAttributeAt(curAddr);
|
||||||
if (faIsProtected(faVal & 0xFF)) {
|
if (screen.getCell(curAddr).isFieldAttribute() || faIsProtected(faVal & 0xFF)) {
|
||||||
return -3; // No unprotected field
|
return -3; // No unprotected field
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2517,6 +2524,8 @@ public class InputProcessor {
|
|||||||
public void sendStructuredFieldData(byte[] data) {
|
public void sendStructuredFieldData(byte[] data) {
|
||||||
if (fsm != null) {
|
if (fsm != null) {
|
||||||
fsm.send3270Data(data);
|
fsm.send3270Data(data);
|
||||||
|
} else if (outputSender != null) {
|
||||||
|
outputSender.send3270Data(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,9 @@ public class TelnetFSM {
|
|||||||
|
|
||||||
private void processDont(int opt) {
|
private void processDont(int opt) {
|
||||||
log.info("RCVD DONT " + TelnetConstants.optionName(opt));
|
log.info("RCVD DONT " + TelnetConstants.optionName(opt));
|
||||||
|
if (opt == TELOPT_TTYPE) {
|
||||||
|
ttypeIndex = 0;
|
||||||
|
}
|
||||||
if (myOpts[opt]) {
|
if (myOpts[opt]) {
|
||||||
myOpts[opt] = false;
|
myOpts[opt] = false;
|
||||||
sendCommand(WONT, opt);
|
sendCommand(WONT, opt);
|
||||||
|
|||||||
@@ -187,4 +187,33 @@ public class FTConfigTest {
|
|||||||
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
||||||
assertTrue(config.buildCommand().contains("SPACE(12,4,8) TRACKS"));
|
assertTrue(config.buildCommand().contains("SPACE(12,4,8) TRACKS"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetOverwriteDoesNotEmitReplaceOnSend() {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setHostType(FTConfig.HostType.CMS);
|
||||||
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
|
config.setHostFilename("TEST FILE A");
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||||
|
config.setOverwrite(true);
|
||||||
|
|
||||||
|
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF", config.buildCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCmsSendIgnoresBlksizeAndReplace() {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setHostType(FTConfig.HostType.CMS);
|
||||||
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
|
config.setHostFilename("TEST FILE A");
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||||
|
config.setRecfm("F");
|
||||||
|
config.setLrecl("80");
|
||||||
|
config.setBlksize("800");
|
||||||
|
config.setExistAction(FTConfig.ExistAction.REPLACE);
|
||||||
|
|
||||||
|
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM F LRECL 80", config.buildCommand());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,4 +576,96 @@ public class FTDftTest {
|
|||||||
assertTrue(ftDft.resendInboundDataBufferToHost());
|
assertTrue(ftDft.resendInboundDataBufferToHost());
|
||||||
assertEquals(2, inputProcessor.sentStructuredFields.size());
|
assertEquals(2, inputProcessor.sentStructuredFields.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testZosUploadRecfmVAndFNormalizesAllNewlineVariants() {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
|
config.setHostType(FTConfig.HostType.TSO);
|
||||||
|
config.setHostFilename("'USER.TEST.DATA'");
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||||
|
config.setRemapFlag(true);
|
||||||
|
config.setRecfm("V");
|
||||||
|
config.setLrecl("255");
|
||||||
|
listener.config = config;
|
||||||
|
|
||||||
|
// Mix \n, \r\n, \n\r (\c\r), bare \r, and \r\r\n
|
||||||
|
String originalText = "LINE1\nLINE2\r\nLINE3\n\rLINE4\rLINE5\r\r\n";
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.ISO_8859_1));
|
||||||
|
ftDft.initTransfer(in, null);
|
||||||
|
|
||||||
|
byte[] getReq = new byte[] {
|
||||||
|
(byte) 0x00, (byte) 0x05, (byte) SF_TRANSFER_DATA,
|
||||||
|
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
|
||||||
|
};
|
||||||
|
ftDft.processStructuredField(getReq, 0, getReq.length);
|
||||||
|
|
||||||
|
assertEquals(1, inputProcessor.sentStructuredFields.size());
|
||||||
|
byte[] payload = extractDataFromGetReply(inputProcessor.sentStructuredFields.get(0));
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : payload) {
|
||||||
|
int ub = b & 0xFF;
|
||||||
|
if (ub == 0x0D) {
|
||||||
|
sb.append("\r");
|
||||||
|
} else if (ub == 0x0A) {
|
||||||
|
sb.append("\n");
|
||||||
|
} else {
|
||||||
|
int ebc = ASC2FT[ub];
|
||||||
|
sb.append(translator.ebcdicToUnicode(ebc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("LINE1\r\nLINE2\r\nLINE3\r\nLINE4\r\nLINE5\r\n", sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testZvmUploadOpenWithSizeAndCloseLifecycle() {
|
||||||
|
FTConfig config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
|
config.setHostType(FTConfig.HostType.CMS);
|
||||||
|
config.setHostFilename("TEST EXEC A");
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||||
|
config.setRemapFlag(true);
|
||||||
|
listener.config = config;
|
||||||
|
|
||||||
|
String originalText = "/* REXX */\nSAY 'HELLO FROM Z/VM'\nEXIT 0\n";
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
|
||||||
|
ftDft.initTransfer(in, null);
|
||||||
|
|
||||||
|
// Simulate z/VM 41-byte (0x0029) OpenForUploadWithSize with recsz = 16 (0x0010) at offset 30..31
|
||||||
|
byte[] openWithSize = new byte[41];
|
||||||
|
openWithSize[0] = 0x00;
|
||||||
|
openWithSize[1] = 0x29;
|
||||||
|
openWithSize[2] = (byte) SF_TRANSFER_DATA;
|
||||||
|
openWithSize[3] = (byte) (TR_OPEN_REQ >> 8);
|
||||||
|
openWithSize[4] = (byte) (TR_OPEN_REQ & 0xFF);
|
||||||
|
openWithSize[30] = 0x00;
|
||||||
|
openWithSize[31] = 0x10; // recsz = 16
|
||||||
|
byte[] ftDataName = "FT:DATA".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
System.arraycopy(ftDataName, 0, openWithSize, 34, ftDataName.length);
|
||||||
|
|
||||||
|
ftDft.processStructuredField(openWithSize, 0, openWithSize.length);
|
||||||
|
assertTrue(listener.dftRunningCalled);
|
||||||
|
assertEquals(16, ftDft.getHostLrecl());
|
||||||
|
|
||||||
|
// Request 1st chunk — must be capped at recsz = 16 bytes
|
||||||
|
byte[] getReq = new byte[] {
|
||||||
|
0x00, 0x05, (byte) SF_TRANSFER_DATA,
|
||||||
|
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
|
||||||
|
};
|
||||||
|
ftDft.processStructuredField(getReq, 0, getReq.length);
|
||||||
|
byte[] chunk1 = extractDataFromGetReply(inputProcessor.sentStructuredFields.get(1));
|
||||||
|
assertEquals(16, chunk1.length);
|
||||||
|
|
||||||
|
// Close FT:DATA stream — should NOT mark transfer complete before FT:MSG
|
||||||
|
byte[] closeReq = new byte[] {
|
||||||
|
0x00, 0x05, (byte) SF_TRANSFER_DATA,
|
||||||
|
(byte) (TR_CLOSE_REQ >> 8), (byte) (TR_CLOSE_REQ & 0xFF)
|
||||||
|
};
|
||||||
|
ftDft.processStructuredField(closeReq, 0, closeReq.length);
|
||||||
|
assertFalse(listener.completeCalled, "Closing FT:DATA should wait for FT:MSG before completing");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,10 +88,17 @@ public class FTPhase3Test {
|
|||||||
config.setLrecl(80);
|
config.setLrecl(80);
|
||||||
|
|
||||||
String cmd = config.buildCommand();
|
String cmd = config.buildCommand();
|
||||||
assertTrue(cmd.contains("REPLACE"));
|
assertFalse(cmd.contains("REPLACE"));
|
||||||
assertTrue(cmd.contains("BLOCK 4096"));
|
assertFalse(cmd.contains("BLOCK 4096"));
|
||||||
assertTrue(cmd.contains("RECFM F"));
|
assertTrue(cmd.contains("RECFM F"));
|
||||||
assertTrue(cmd.contains("LRECL 80"));
|
assertTrue(cmd.contains("LRECL 80"));
|
||||||
|
|
||||||
|
config.setHostType(FTConfig.HostType.TSO);
|
||||||
|
config.setHostFilename("'USER.TEST.DATA'");
|
||||||
|
String tsoCmd = config.buildCommand();
|
||||||
|
assertTrue(tsoCmd.contains("BLKSIZE(4096)"));
|
||||||
|
assertTrue(tsoCmd.contains("RECFM(F)"));
|
||||||
|
assertTrue(tsoCmd.contains("LRECL(80)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+22
-31
@@ -293,12 +293,14 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
FTConfig config = new FTConfig();
|
FTConfig config = new FTConfig();
|
||||||
config.setDirection(FTConfig.Direction.SEND);
|
config.setDirection(FTConfig.Direction.SEND);
|
||||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setCrFlag(true);
|
||||||
|
config.setRemapFlag(false);
|
||||||
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||||
config.setLrecl("10");
|
config.setLrecl("10");
|
||||||
listener.config = config;
|
listener.config = config;
|
||||||
|
|
||||||
// Local content has two lines: "SHORT" (5 chars) and "LONGER LINE THAN TEN" (20 chars)
|
// Local content has mixed newline styles (\n, \r\n, \n\r)
|
||||||
String localContent = "SHORT\nLONGER LINE THAN TEN\n";
|
String localContent = "SHORT\nLONGER LINE\r\nTHIRD\n\r";
|
||||||
ByteArrayInputStream in = new ByteArrayInputStream(localContent.getBytes(StandardCharsets.UTF_8));
|
ByteArrayInputStream in = new ByteArrayInputStream(localContent.getBytes(StandardCharsets.UTF_8));
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
ftDft.initTransfer(in, out);
|
ftDft.initTransfer(in, out);
|
||||||
@@ -311,16 +313,9 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
byte[] reply = input.sentStructuredFields.get(0);
|
byte[] reply = input.sentStructuredFields.get(0);
|
||||||
int headerLen = 17;
|
int headerLen = 17;
|
||||||
int dataLen = reply.length - headerLen;
|
int dataLen = reply.length - headerLen;
|
||||||
assertTrue(dataLen >= 30, "Should have packed at least 3 fixed records of 10 bytes each");
|
|
||||||
|
|
||||||
String rec1 = new String(reply, headerLen, 10, StandardCharsets.UTF_8);
|
String payload = new String(reply, headerLen, dataLen, StandardCharsets.UTF_8);
|
||||||
assertEquals("SHORT ", rec1);
|
assertEquals("SHORT\r\nLONGER LINE\r\nTHIRD\r\n", payload);
|
||||||
|
|
||||||
String rec2 = new String(reply, headerLen + 10, 10, StandardCharsets.UTF_8);
|
|
||||||
assertEquals("LONGER LIN", rec2);
|
|
||||||
|
|
||||||
String rec3 = new String(reply, headerLen + 20, 10, StandardCharsets.UTF_8);
|
|
||||||
assertEquals("E THAN TEN", rec3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -328,6 +323,8 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
FTConfig config = new FTConfig();
|
FTConfig config = new FTConfig();
|
||||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
config.setCrFlag(true);
|
||||||
|
config.setRemapFlag(false);
|
||||||
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||||
config.setLrecl("10");
|
config.setLrecl("10");
|
||||||
listener.config = config;
|
listener.config = config;
|
||||||
@@ -336,8 +333,8 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
ftDft.initTransfer(in, out);
|
ftDft.initTransfer(in, out);
|
||||||
|
|
||||||
// Host sends 10-byte fixed record "HELLO "
|
// Host sends CRLF-terminated record "HELLO\r\n" + EOF (0x1A)
|
||||||
byte[] hostRecord = "HELLO ".getBytes(StandardCharsets.UTF_8);
|
byte[] hostRecord = "HELLO\r\n\u001a".getBytes(StandardCharsets.UTF_8);
|
||||||
ByteArrayOutputStream sf = new ByteArrayOutputStream();
|
ByteArrayOutputStream sf = new ByteArrayOutputStream();
|
||||||
sf.write(0x00); sf.write(hostRecord.length + 8);
|
sf.write(0x00); sf.write(hostRecord.length + 8);
|
||||||
sf.write(SF_TRANSFER_DATA);
|
sf.write(SF_TRANSFER_DATA);
|
||||||
@@ -353,17 +350,19 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
|
|
||||||
ftDft.processStructuredField(pkt, 0, pkt.length);
|
ftDft.processStructuredField(pkt, 0, pkt.length);
|
||||||
|
|
||||||
// Verify output has "HELLO" stripped of trailing spaces plus local line separator
|
// Verify output has CR and ^Z stripped
|
||||||
String result = new String(out.toByteArray(), StandardCharsets.UTF_8);
|
String result = new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||||
assertEquals("HELLO" + System.lineSeparator(), result);
|
assertEquals("HELLO\n", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRecfmVRdwUploadAndDownload() throws Exception {
|
public void testRecfmVRdwUploadAndDownload() throws Exception {
|
||||||
// 1. Upload RECFM V
|
// 1. Upload RECFM V (stream CRLF-normalized, no binary RDW injection in ASCII mode)
|
||||||
FTConfig sendConfig = new FTConfig();
|
FTConfig sendConfig = new FTConfig();
|
||||||
sendConfig.setDirection(FTConfig.Direction.SEND);
|
sendConfig.setDirection(FTConfig.Direction.SEND);
|
||||||
sendConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
sendConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
sendConfig.setCrFlag(true);
|
||||||
|
sendConfig.setRemapFlag(false);
|
||||||
sendConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
sendConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||||
listener.config = sendConfig;
|
listener.config = sendConfig;
|
||||||
|
|
||||||
@@ -378,37 +377,29 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
assertEquals(1, input.sentStructuredFields.size());
|
assertEquals(1, input.sentStructuredFields.size());
|
||||||
byte[] reply = input.sentStructuredFields.get(0);
|
byte[] reply = input.sentStructuredFields.get(0);
|
||||||
int headerLen = 17;
|
int headerLen = 17;
|
||||||
|
String payload = new String(reply, headerLen, reply.length - headerLen, StandardCharsets.UTF_8);
|
||||||
// Verify 4-byte RDW header: length should be testLine.length() + 4
|
assertEquals(testLine + "\r\n", payload);
|
||||||
int rdwLen = ((reply[headerLen] & 0xFF) << 8) | (reply[headerLen + 1] & 0xFF);
|
|
||||||
assertEquals(testLine.length() + 4, rdwLen);
|
|
||||||
assertEquals(0, reply[headerLen + 2]);
|
|
||||||
assertEquals(0, reply[headerLen + 3]);
|
|
||||||
String payload = new String(reply, headerLen + 4, testLine.length(), StandardCharsets.UTF_8);
|
|
||||||
assertEquals(testLine, payload);
|
|
||||||
|
|
||||||
// 2. Download RECFM V
|
// 2. Download RECFM V
|
||||||
FTConfig recvConfig = new FTConfig();
|
FTConfig recvConfig = new FTConfig();
|
||||||
recvConfig.setDirection(FTConfig.Direction.RECEIVE);
|
recvConfig.setDirection(FTConfig.Direction.RECEIVE);
|
||||||
recvConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
recvConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||||
|
recvConfig.setCrFlag(true);
|
||||||
|
recvConfig.setRemapFlag(false);
|
||||||
recvConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
recvConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||||
listener.config = recvConfig;
|
listener.config = recvConfig;
|
||||||
|
|
||||||
ByteArrayOutputStream downOut = new ByteArrayOutputStream();
|
ByteArrayOutputStream downOut = new ByteArrayOutputStream();
|
||||||
ftDft.initTransfer(new ByteArrayInputStream(new byte[0]), downOut);
|
ftDft.initTransfer(new ByteArrayInputStream(new byte[0]), downOut);
|
||||||
|
|
||||||
// Host sends RDW header + "TEST V DATA"
|
byte[] vData = "TEST V DATA\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] vData = "TEST V DATA".getBytes(StandardCharsets.UTF_8);
|
|
||||||
int vRdwLen = vData.length + 4;
|
|
||||||
ByteArrayOutputStream sfDown = new ByteArrayOutputStream();
|
ByteArrayOutputStream sfDown = new ByteArrayOutputStream();
|
||||||
sfDown.write(0x00); sfDown.write(0x00);
|
sfDown.write(0x00); sfDown.write(0x00);
|
||||||
sfDown.write(SF_TRANSFER_DATA);
|
sfDown.write(SF_TRANSFER_DATA);
|
||||||
sfDown.write(0x47); sfDown.write(0x04);
|
sfDown.write(0x47); sfDown.write(0x04);
|
||||||
sfDown.write(TR_BEGIN_DATA);
|
sfDown.write(TR_BEGIN_DATA);
|
||||||
int fLen = vRdwLen + 5;
|
int fLen = vData.length + 5;
|
||||||
sfDown.write((fLen >> 8) & 0xFF); sfDown.write(fLen & 0xFF);
|
sfDown.write((fLen >> 8) & 0xFF); sfDown.write(fLen & 0xFF);
|
||||||
sfDown.write((vRdwLen >> 8) & 0xFF); sfDown.write(vRdwLen & 0xFF);
|
|
||||||
sfDown.write(0x00); sfDown.write(0x00);
|
|
||||||
sfDown.write(vData);
|
sfDown.write(vData);
|
||||||
|
|
||||||
byte[] downPkt = sfDown.toByteArray();
|
byte[] downPkt = sfDown.toByteArray();
|
||||||
@@ -418,7 +409,7 @@ public class FTProtocolHardeningPhase16Test {
|
|||||||
ftDft.processStructuredField(downPkt, 0, downPkt.length);
|
ftDft.processStructuredField(downPkt, 0, downPkt.length);
|
||||||
|
|
||||||
String downResult = new String(downOut.toByteArray(), StandardCharsets.UTF_8);
|
String downResult = new String(downOut.toByteArray(), StandardCharsets.UTF_8);
|
||||||
assertEquals("TEST V DATA" + System.lineSeparator(), downResult);
|
assertEquals("TEST V DATA\n", downResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user