From 2437b49fc10f11ae24062d83e74e1bd3818181a1 Mon Sep 17 00:00:00 2001 From: Rudi Date: Tue, 27 Oct 2026 13:53:28 +0000 Subject: [PATCH] Filetransfer fixes for VM --- .../j3270/ft/FileTransferDialog.java | 58 ++- .../datastream/DataStreamProcessor.java | 1 + .../haus/nightmare/lib3270j/ft/FTConfig.java | 31 +- .../haus/nightmare/lib3270j/ft/FTCut.java | 95 +++- .../haus/nightmare/lib3270j/ft/FTDft.java | 438 ++++++------------ .../lib3270j/input/InputProcessor.java | 17 +- .../nightmare/lib3270j/telnet/TelnetFSM.java | 3 + .../nightmare/lib3270j/ft/FTConfigTest.java | 29 ++ .../haus/nightmare/lib3270j/ft/FTDftTest.java | 92 ++++ .../nightmare/lib3270j/ft/FTPhase3Test.java | 11 +- .../ft/FTProtocolHardeningPhase16Test.java | 53 +-- 11 files changed, 444 insertions(+), 384 deletions(-) diff --git a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java index f35c458..69a4c6a 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java @@ -353,14 +353,14 @@ public class FileTransferDialog extends JDialog { boolean isTso = (hostType == FTConfig.HostType.TSO); boolean isCms = (hostType == FTConfig.HostType.CMS); - appendCheck.setEnabled(!isSend); - overwriteCheck.setEnabled(!isSend); + appendCheck.setEnabled(true); + overwriteCheck.setEnabled(true); crCheck.setEnabled(isAscii); remapCheck.setEnabled(isAscii); - recfmField.setEnabled(isSend && isTso); - lreclField.setEnabled(isSend && isTso); + recfmField.setEnabled(isSend && (isTso || isCms)); + lreclField.setEnabled(isSend && (isTso || isCms)); blksizeField.setEnabled(isSend && isTso); unitsCombo.setEnabled(isSend && isTso); spaceField.setEnabled(isSend && isTso); @@ -405,7 +405,7 @@ public class FileTransferDialog extends JDialog { if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) { hostFileField.setText(dialog.getSelectedHostFile()); HostDirectoryEntry entry = dialog.getSelectedEntry(); - if (entry != null) { + if (entry != null && hostType == FTConfig.HostType.TSO) { if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) { recfmField.setText(entry.getRecfm().trim()); } @@ -443,30 +443,54 @@ public class FileTransferDialog extends JDialog { return; } + FTConfig.HostType selectedHostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); + boolean isReceive = receiveRadio.isSelected(); + boolean isSend = sendRadio.isSelected(); + FTConfig config = new FTConfig(); - config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem()); + config.setHostType(selectedHostType); config.setLocalFilename(localFileField.getText().trim()); config.setHostFilename(hostFileField.getText().trim()); - config.setReceive(receiveRadio.isSelected()); + config.setReceive(isReceive); config.setAscii(asciiRadio.isSelected()); config.setCrFlag(crCheck.isSelected()); config.setRemapFlag(remapCheck.isSelected()); - config.setAppend(appendCheck.isSelected()); - config.setOverwrite(overwriteCheck.isSelected()); + if (isReceive) { + 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(); if (mtu != null) { config.setDftBufferSize(mtu); } - config.setRecfm(recfmField.getText().trim()); - config.setLrecl(lreclField.getText().trim()); - config.setBlksize(blksizeField.getText().trim()); - FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem(); - config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT); - config.setSpace(spaceField.getText().trim()); - config.setDirectoryBlocks(dirField.getText().trim()); - config.setOptions(optionsField.getText().trim()); + if (isSend) { + if (selectedHostType == FTConfig.HostType.TSO) { + config.setRecfm(recfmField.getText().trim()); + config.setLrecl(lreclField.getText().trim()); + config.setBlksize(blksizeField.getText().trim()); + String spaceText = spaceField.getText().trim(); + String dirText = dirField.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(); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 7820bb3..fbe4bcb 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -1190,6 +1190,7 @@ public class DataStreamProcessor { processOutbound3270DS(data, pos, fieldLen); break; case SF_TRANSFER_DATA: // 0xD0 + qreplyPending = false; if (ftDft != null) { ftDft.processStructuredField(data, pos, fieldLen); } else { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java index e5baf1b..8744381 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTConfig.java @@ -63,6 +63,7 @@ public class FTConfig { private CrAction crAction = CrAction.REMOVE; private boolean remapFlag = true; private ExistAction existAction = ExistAction.KEEP; + private boolean allowLocalOverwrite = true; private RecordFormat recfm = RecordFormat.DEFAULT; private AllocationUnit units = AllocationUnit.DEFAULT; private int lrecl = 0; @@ -97,7 +98,7 @@ public class FTConfig { return isAscii() && crAction != CrAction.KEEP; } 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 void setOptionNew(boolean optionNew) { this.optionNew = optionNew; } public boolean isOptionNoso() { return optionNoso; } @@ -210,7 +211,7 @@ public class FTConfig { } public void setOverwrite(boolean overwrite) { - if (overwrite) this.existAction = ExistAction.REPLACE; + this.allowLocalOverwrite = overwrite; } public String getCodePage() { return codePage; } @@ -588,7 +589,7 @@ public class FTConfig { } else if (isOptionNew()) { if (opts.length() > 0) opts.append(" "); opts.append("NEW"); - } else if (isOverwrite()) { + } else if (existAction == ExistAction.REPLACE && hostType != HostType.CMS) { if (opts.length() > 0) opts.append(" "); opts.append("REPLACE"); } @@ -607,12 +608,14 @@ public class FTConfig { default: break; } opts.append(")"); - if (lrecl > 0) { - opts.append(" LRECL(").append(lrecl).append(")"); - } - if (blksize > 0) { - opts.append(" BLKSIZE(").append(blksize).append(")"); - } + } + if (lrecl > 0) { + if (opts.length() > 0) opts.append(" "); + opts.append("LRECL(").append(lrecl).append(")"); + } + if (blksize > 0) { + if (opts.length() > 0) opts.append(" "); + opts.append("BLKSIZE(").append(blksize).append(")"); } if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) { AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS; @@ -641,12 +644,10 @@ public class FTConfig { case VARIABLE: opts.append("V"); break; default: break; } - if (lrecl > 0) { - opts.append(" LRECL ").append(lrecl); - } - if (blksize > 0) { - opts.append(" BLOCK ").append(blksize); - } + } + if (lrecl > 0) { + if (opts.length() > 0) opts.append(" "); + opts.append("LRECL ").append(lrecl); } } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java index c5bc2ac..17881ee 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTCut.java @@ -109,6 +109,7 @@ public class FTCut { private InputStream inputStream; private OutputStream outputStream; private boolean lastCr = false; + private int lookaheadByte = -1; public FTCut(ScreenBuffer screen, InputProcessor input, EbcdicTranslator translator, FTCutListener listener) { @@ -181,6 +182,7 @@ public class FTCut { xlateBufIx = 0; cutEof = false; lastCr = false; + lookaheadByte = -1; retransmitRetries = 0; lastUploadCount = 0; lastUploadSeq = 0; @@ -217,11 +219,17 @@ public class FTCut { if (sfAttr == 0 || !isSkip(sfAttr)) { // Not a CUT frame (likely local echo, menu return, or host error screen) if (xferInProgress) { - log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted."); xferInProgress = false; - String hostErr = scanScreenForErrors(); - String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)"; - listener.onTransferAborted(abortMsg); + String screenUpper = screen.getText().toUpperCase(); + if (screenUpper.contains("TRANS03") || screenUpper.contains("TRANS04") || screenUpper.contains("COMPLETE")) { + 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; } @@ -270,6 +278,9 @@ public class FTCut { String line = lineSb.toString().trim(); if (line.isEmpty()) continue; String lineUpper = line.toUpperCase(); + if (lineUpper.contains("TRANS03") || lineUpper.contains("TRANS04") || lineUpper.contains("COMPLETE") || lineUpper.contains("TERMINAL -> HOST COMPLETE")) { + continue; + } for (String prefix : errorPrefixes) { if (lineUpper.contains(prefix)) { return line; @@ -309,14 +320,18 @@ public class FTCut { switch (code) { case SC_HOST_ACK: log.info("CUT: HOST_ACK received — transfer running"); + listener.onCutRunning(); + if (listener.getCurrentState() == FTState.NONE) { + return; + } xferInProgress = true; expandedLength = 0; quadrant = -1; xlateBuffered = 0; xlateBufIx = 0; cutEof = false; + lastCr = false; cutAck(); - listener.onCutRunning(); break; case SC_XFER_COMPLETE: @@ -378,6 +393,14 @@ public class FTCut { return; } + if (listener.getCurrentState() == FTState.AWAIT_ACK) { + listener.onCutRunning(); + if (listener.getCurrentState() == FTState.NONE) { + return; + } + xferInProgress = true; + } + FTConfig config = listener.getConfig(); int count = 0; @@ -439,6 +462,14 @@ public class FTCut { return; } + if (listener.getCurrentState() == FTState.AWAIT_ACK) { + listener.onCutRunning(); + if (listener.getCurrentState() == FTState.NONE) { + return; + } + xferInProgress = true; + } + FTConfig config = listener.getConfig(); int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) | @@ -615,6 +646,16 @@ public class FTCut { 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 { if (xlateBuffered > 0) { int r = xlateBuf[xlateBufIx++]; @@ -622,24 +663,40 @@ public class FTCut { return r; } - if (inputStream == null) return -1; - - int c = inputStream.read(); + int c = readRawByte(); if (c == -1) return -1; - int localByte = c & 0xFF; + int[] cbuf = new int[32]; int nc = 0; - int[] cbuf = new int[4]; - if (config.isAscii()) { - if (config.isCrFlag() && !lastCr && localByte == '\n') { - nc += uploadConvert('\r', cbuf, nc, config); + if (config.isAscii() && config.isCrFlag()) { + if (c == '\r') { + 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) { for (int i = 1; i < nc; i++) { xlateBuf[xlateBuffered++] = cbuf[i]; @@ -665,10 +722,10 @@ public class FTCut { if (!config.isAscii() || !config.isRemapFlag()) { ebc = localByte & 0xFF; - } else if (localByte < 0x20 || (localByte >= 0x80 && localByte < 0x9F)) { - ebc = localByte & 0xFF; + } else if (localByte < 0x20 || localByte == 0x7F || (localByte >= 0x80 && localByte < 0x9F)) { + ebc = FTConstants.FT2ASC[FTConstants.ASC2FT[localByte & 0xFF]]; } else if (localByte == 0x9F) { - ebc = 0xFF; + ebc = FTConstants.FT2ASC[0xFF]; } else { int standardEbc = translator.unicodeToEbcdic((char) localByte); if (standardEbc < 0) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java index 391a705..16b7f83 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java @@ -41,13 +41,16 @@ public class FTDft { private long recnum = 1; private boolean dftEof = false; private boolean messageFlag = false; + private boolean dataStreamOpened = false; private long bytesTransferred = 0; private long estimatedTotalBytes = 0; private int hostLrecl = 0; private String hostRecfm = ""; private int hostBlksize = 0; + private int openRecsz = 0; private int customMtuSize = 0; private int pendingByte = -1; + private int lookaheadByte = -1; // Record formatting buffers and SO/SI tracking private static final int MAX_DFT_RECORD_BUFFER_SIZE = 131072; // 128KB sanity cap @@ -159,15 +162,18 @@ public class FTDft { recnum = 1; dftEof = false; messageFlag = false; + dataStreamOpened = false; bytesTransferred = 0; estimatedTotalBytes = 0; hostLrecl = 0; hostRecfm = ""; hostBlksize = 0; + openRecsz = 0; dftSaveBuf = null; dftSaveBufLen = 0; lastCr = false; pendingByte = -1; + lookaheadByte = -1; downloadRecordBuffer.reset(); dbcsMode = false; hasPendingByteInDBCS = false; @@ -216,8 +222,8 @@ public class FTDft { 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) - if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) { + // Check if this is a host error reply (TR_ERROR_REPLY 0x08) + if ((requestCode & 0xFF) == TR_ERROR_REPLY) { handleHostErrorReply(data, payloadStart, offset + length - payloadStart); 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) { int end = Math.min(payloadStart + length, data.length); int errCode = 0; @@ -320,20 +317,24 @@ public class FTDft { private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) { log.fine("DFT: Open request"); - // Parse open request payload matching x3270 / DDM open - // sfLength is the 2-byte length value at sfOffset + // Parse open request payload matching x3270 / HoD Xfer3270: + // sfLenVal is the 2-byte length value at sfOffset (or sfLength when 0x0000) int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF); + if (sfLenVal == 0) { + sfLenVal = sfLength; + } String nameBuf = ""; + int parsedRecsz = 0; if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) { nameBuf = extractName(data, sfOffset + 3 + 25, 7); } 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); + } 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); if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) || 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 + ")"); } else { messageFlag = false; + dataStreamOpened = true; listener.onDftRunning(); + if (listener.getCurrentState() == FTState.NONE) { + sendOpenFailed(); + return; + } + openRecsz = parsedRecsz; + if (parsedRecsz > 0) { + hostLrecl = parsedRecsz; + } } dftEof = false; @@ -352,28 +362,45 @@ public class FTDft { dftOpenAck(); } - private void parseDdmAttributes(byte[] data, int start, int length) { - int end = Math.min(start + length, data.length); - int pos = start + 2; // skip TR_OPEN_REQ + private void parseDdmAttributes(byte[] data, int offset, int length) { + int pos = offset + 2; // skip 2-byte request code (0x0012) + int end = Math.min(offset + length, data.length); 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); if (attrLen < 3 || pos + attrLen > end) { pos++; continue; } - if (attrType == DDM_HDR_LRECL && attrLen >= 5) { - hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF); - } else if (attrType == DDM_HDR_RECFM && attrLen >= 4) { - int r = data[pos + 3] & 0xFF; - hostRecfm = (r == 1) ? "F" : (r == 2 ? "V" : "U"); - } else if (attrType == DDM_HDR_BLKSIZE && attrLen >= 5) { - hostBlksize = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF); - } else if (attrType == DDM_HDR_FILESIZE && attrLen >= 7) { - estimatedTotalBytes = (((long)(data[pos + 3] & 0xFF)) << 24) | - (((long)(data[pos + 4] & 0xFF)) << 16) | - (((long)(data[pos + 5] & 0xFF)) << 8) | - ((long)(data[pos + 6] & 0xFF)); + switch (attrId) { + case DDM_HDR_LRECL: + if (attrLen >= 5) { + hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF); + } + break; + case DDM_HDR_RECFM: + if (attrLen >= 4) { + int r = data[pos + 3] & 0xFF; + if (r == 1 || r == 'F' || r == 0xC6) hostRecfm = "F"; + else if (r == 2 || r == 'V' || r == 0xE5) hostRecfm = "V"; + 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; } @@ -515,7 +542,7 @@ public class FTDft { log.info("DFT host message: " + msg); 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); } 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); @@ -526,8 +553,8 @@ public class FTDft { } /** - * Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping. - * Conforms to IBM Host On-Demand specification. + * Write download data to the local file, handling ASCII conversion and EOF stripping. + * Conforms to x3270 / IBM Host On-Demand specification. */ private void writeDownloadData(byte[] data, int offset, int length, FTConfig config) throws IOException { @@ -543,76 +570,7 @@ public class FTDft { return; } - boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) || - "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); - } - } + writeConvertedChunk(data, offset, length, config); } private void flushRemainingDownloadRecords() throws IOException { @@ -690,45 +648,36 @@ public class FTDft { int bufferSize = getMTUSize(); int numbytes = bufferSize - 27; + if (openRecsz > 0 && openRecsz < numbytes) { + numbytes = openRecsz; + } byte[] readBuf = new byte[numbytes]; 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 { - if (config != null && config.isAscii() && isRecfmF && lrecl > 0) { - totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config); - } else if (config != null && config.isAscii() && isRecfmV) { - totalRead = readVariableRecordUpload(readBuf, numbytes, config); - } else { - while (!dftEof && totalRead < numbytes) { - 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; + while (!dftEof && totalRead < numbytes) { + 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; } } } 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. * 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. * Matching x3270 dft_ascii_read logic. @@ -1007,26 +825,39 @@ public class FTDft { return b; } - if (inputStream == null) return -1; - - int c = inputStream.read(); + int c = readRawByte(); if (c == -1) return -1; - if (config.isCrFlag() && !lastCr && c == '\n') { - lastCr = false; - // Expand \n to \r\n: buffer \n in pendingByte, return \r now - pendingByte = convertAsciiByte('\n', config); - return convertAsciiByte('\r', config); + if (config.isCrFlag()) { + if (c == '\r') { + int next = readRawByte(); + while (next == '\r') { + 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); } private int convertAsciiByte(char c, FTConfig config) { if (!config.isRemapFlag()) { - int ebc = translator.unicodeToEbcdic(c); - return ebc >= 0 ? ebc : 0x40; + return c & 0xFF; } /* @@ -1034,7 +865,7 @@ public class FTDft { * Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc]. */ int ebc; - if (c < 0x20 || (c >= 0x80 && c < 0x9F)) { + if (c < 0x20 || c == 0x7F || (c >= 0x80 && c < 0x9F)) { ebc = ASC2FT[c & 0xFF]; } else if (c == 0x9F) { ebc = 0xFF; @@ -1059,9 +890,13 @@ public class FTDft { input.sendStructuredFieldData(RB_CLOSE_ACK.clone()); - if (listener.getCurrentState() != FTState.NONE) { - log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)"); - listener.onTransferComplete(null); + if (messageFlag || !dataStreamOpened) { + if (listener != null && listener.getCurrentState() != FTState.NONE) { + 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++)); } + 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 ========== private void dftAbort(String message, int code) { log.warning("DFT: ABORT — " + message); if (code == TR_OPEN_REQ) { - input.sendStructuredFieldData(RB_OPEN_FAILED.clone()); + sendOpenFailed(); } else { ByteArrayOutputStream out = new ByteArrayOutputStream(10); out.write(AID_SF); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index 6bd6964..37247e2 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -1977,8 +1977,15 @@ public class InputProcessor { public void sendAidForFT(int aidCode) { boolean wasLocked = keyboardLocked; keyboardLocked = false; - sendAid(aidCode); - keyboardLocked = wasLocked; + int prevInhibit = (oia != null) ? oia.getInputInhibited() : ECLConstants.INHIBIT_NOT_INHIBITED; + 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 int curAddr = screen.getCursorAddress(); byte faVal = screen.getFieldAttributeAt(curAddr); - if (faIsProtected(faVal & 0xFF)) { + if (screen.getCell(curAddr).isFieldAttribute() || faIsProtected(faVal & 0xFF)) { // Try to find first unprotected field curAddr = screen.findNextUnprotected(curAddr); faVal = screen.getFieldAttributeAt(curAddr); - if (faIsProtected(faVal & 0xFF)) { + if (screen.getCell(curAddr).isFieldAttribute() || faIsProtected(faVal & 0xFF)) { return -3; // No unprotected field } } @@ -2517,6 +2524,8 @@ public class InputProcessor { public void sendStructuredFieldData(byte[] data) { if (fsm != null) { fsm.send3270Data(data); + } else if (outputSender != null) { + outputSender.send3270Data(data); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java index 4f389cf..8811c7f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -582,6 +582,9 @@ public class TelnetFSM { private void processDont(int opt) { log.info("RCVD DONT " + TelnetConstants.optionName(opt)); + if (opt == TELOPT_TTYPE) { + ttypeIndex = 0; + } if (myOpts[opt]) { myOpts[opt] = false; sendCommand(WONT, opt); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java index db99de1..1d75893 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTConfigTest.java @@ -187,4 +187,33 @@ public class FTConfigTest { assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits()); 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()); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java index 6cfbdc0..b23702b 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java @@ -576,4 +576,96 @@ public class FTDftTest { assertTrue(ftDft.resendInboundDataBufferToHost()); 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"); + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTPhase3Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTPhase3Test.java index 3f3ea56..c69e48e 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTPhase3Test.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTPhase3Test.java @@ -88,10 +88,17 @@ public class FTPhase3Test { config.setLrecl(80); String cmd = config.buildCommand(); - assertTrue(cmd.contains("REPLACE")); - assertTrue(cmd.contains("BLOCK 4096")); + assertFalse(cmd.contains("REPLACE")); + assertFalse(cmd.contains("BLOCK 4096")); assertTrue(cmd.contains("RECFM F")); 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 diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTProtocolHardeningPhase16Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTProtocolHardeningPhase16Test.java index 950021d..ca1b83c 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTProtocolHardeningPhase16Test.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTProtocolHardeningPhase16Test.java @@ -293,12 +293,14 @@ public class FTProtocolHardeningPhase16Test { FTConfig config = new FTConfig(); config.setDirection(FTConfig.Direction.SEND); config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrFlag(true); + config.setRemapFlag(false); config.setRecfm(FTConfig.RecordFormat.FIXED); config.setLrecl("10"); listener.config = config; - // Local content has two lines: "SHORT" (5 chars) and "LONGER LINE THAN TEN" (20 chars) - String localContent = "SHORT\nLONGER LINE THAN TEN\n"; + // Local content has mixed newline styles (\n, \r\n, \n\r) + String localContent = "SHORT\nLONGER LINE\r\nTHIRD\n\r"; ByteArrayInputStream in = new ByteArrayInputStream(localContent.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); ftDft.initTransfer(in, out); @@ -311,16 +313,9 @@ public class FTProtocolHardeningPhase16Test { byte[] reply = input.sentStructuredFields.get(0); int headerLen = 17; 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); - assertEquals("SHORT ", rec1); - - 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); + String payload = new String(reply, headerLen, dataLen, StandardCharsets.UTF_8); + assertEquals("SHORT\r\nLONGER LINE\r\nTHIRD\r\n", payload); } @Test @@ -328,6 +323,8 @@ public class FTProtocolHardeningPhase16Test { FTConfig config = new FTConfig(); config.setDirection(FTConfig.Direction.RECEIVE); config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrFlag(true); + config.setRemapFlag(false); config.setRecfm(FTConfig.RecordFormat.FIXED); config.setLrecl("10"); listener.config = config; @@ -336,8 +333,8 @@ public class FTProtocolHardeningPhase16Test { ByteArrayOutputStream out = new ByteArrayOutputStream(); ftDft.initTransfer(in, out); - // Host sends 10-byte fixed record "HELLO " - byte[] hostRecord = "HELLO ".getBytes(StandardCharsets.UTF_8); + // Host sends CRLF-terminated record "HELLO\r\n" + EOF (0x1A) + byte[] hostRecord = "HELLO\r\n\u001a".getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream sf = new ByteArrayOutputStream(); sf.write(0x00); sf.write(hostRecord.length + 8); sf.write(SF_TRANSFER_DATA); @@ -353,17 +350,19 @@ public class FTProtocolHardeningPhase16Test { 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); - assertEquals("HELLO" + System.lineSeparator(), result); + assertEquals("HELLO\n", result); } @Test 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(); sendConfig.setDirection(FTConfig.Direction.SEND); sendConfig.setTransferMode(FTConfig.TransferMode.ASCII); + sendConfig.setCrFlag(true); + sendConfig.setRemapFlag(false); sendConfig.setRecfm(FTConfig.RecordFormat.VARIABLE); listener.config = sendConfig; @@ -378,37 +377,29 @@ public class FTProtocolHardeningPhase16Test { assertEquals(1, input.sentStructuredFields.size()); byte[] reply = input.sentStructuredFields.get(0); int headerLen = 17; - - // Verify 4-byte RDW header: length should be testLine.length() + 4 - 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); + String payload = new String(reply, headerLen, reply.length - headerLen, StandardCharsets.UTF_8); + assertEquals(testLine + "\r\n", payload); // 2. Download RECFM V FTConfig recvConfig = new FTConfig(); recvConfig.setDirection(FTConfig.Direction.RECEIVE); recvConfig.setTransferMode(FTConfig.TransferMode.ASCII); + recvConfig.setCrFlag(true); + recvConfig.setRemapFlag(false); recvConfig.setRecfm(FTConfig.RecordFormat.VARIABLE); listener.config = recvConfig; ByteArrayOutputStream downOut = new ByteArrayOutputStream(); ftDft.initTransfer(new ByteArrayInputStream(new byte[0]), downOut); - // Host sends RDW header + "TEST V DATA" - byte[] vData = "TEST V DATA".getBytes(StandardCharsets.UTF_8); - int vRdwLen = vData.length + 4; + byte[] vData = "TEST V DATA\r\n".getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream sfDown = new ByteArrayOutputStream(); sfDown.write(0x00); sfDown.write(0x00); sfDown.write(SF_TRANSFER_DATA); sfDown.write(0x47); sfDown.write(0x04); 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((vRdwLen >> 8) & 0xFF); sfDown.write(vRdwLen & 0xFF); - sfDown.write(0x00); sfDown.write(0x00); sfDown.write(vData); byte[] downPkt = sfDown.toByteArray(); @@ -418,7 +409,7 @@ public class FTProtocolHardeningPhase16Test { ftDft.processStructuredField(downPkt, 0, downPkt.length); String downResult = new String(downOut.toByteArray(), StandardCharsets.UTF_8); - assertEquals("TEST V DATA" + System.lineSeparator(), downResult); + assertEquals("TEST V DATA\n", downResult); } @Test