From 8654b72d8c5706856d0b166bbcac886bb3cae333 Mon Sep 17 00:00:00 2001 From: Rudi Date: Thu, 27 Aug 2026 22:05:56 -0400 Subject: [PATCH] IND testing --- .../haus/nightmare/lib3270j/ft/FTDft.java | 25 +- .../haus/nightmare/lib3270j/ft/FTCutTest.java | 116 ++++++ .../haus/nightmare/lib3270j/ft/FTDftTest.java | 337 ++++++++++++++++-- 3 files changed, 442 insertions(+), 36 deletions(-) create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTCutTest.java 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 8e42da7..5e66d74 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java @@ -42,6 +42,7 @@ public class FTDft { private boolean dftEof = false; private boolean messageFlag = false; private long bytesTransferred = 0; + private int pendingByte = -1; // Savebuf for Read Modified retransmit private byte[] dftSaveBuf = null; @@ -92,6 +93,7 @@ public class FTDft { dftSaveBuf = null; dftSaveBufLen = 0; lastCr = false; + pendingByte = -1; } /** @@ -403,7 +405,7 @@ public class FTDft { try { while (!dftEof && totalRead < numbytes) { - if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) { + if (config.isAscii()) { int b = dftAsciiRead(config); if (b == -1) { dftEof = true; @@ -490,6 +492,12 @@ public class FTDft { * Matching x3270 dft_ascii_read logic. */ private int dftAsciiRead(FTConfig config) throws IOException { + if (pendingByte != -1) { + int b = pendingByte; + pendingByte = -1; + return b; + } + if (inputStream == null) return -1; int c = inputStream.read(); @@ -497,15 +505,18 @@ public class FTDft { if (config.isCrFlag() && !lastCr && c == '\n') { lastCr = false; - // Expand \n to \r\n: return \r byte now - int rEbc = translator.unicodeToEbcdic('\r'); - if (rEbc < 0) rEbc = 0x0D; - return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc; + // Expand \n to \r\n: buffer \n in pendingByte, return \r now + 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((char) c); + int ebc = translator.unicodeToEbcdic(c); return ebc >= 0 ? ebc : 0x40; } @@ -519,7 +530,7 @@ public class FTDft { } else if (c == 0x9F) { ebc = 0xFF; } else { - ebc = translator.unicodeToEbcdic((char) c); + ebc = translator.unicodeToEbcdic(c); } if (ebc < 0) ebc = 0x40; diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTCutTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTCutTest.java new file mode 100644 index 0000000..4f6d251 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTCutTest.java @@ -0,0 +1,116 @@ +package haus.nightmare.lib3270j.ft; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static haus.nightmare.lib3270j.ft.FTConstants.*; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +public class FTCutTest { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private InputProcessor inputProcessor; + private TestFTCutListener listener; + private FTCut ftCut; + + static class TestFTCutListener implements FTCut.FTCutListener { + FTConfig config; + FTState state = FTState.RUNNING; + long bytesTransferred = 0; + boolean completed = false; + String errorMessage = null; + + @Override + public void onCutRunning() { + state = FTState.RUNNING; + } + + @Override + public void onTransferComplete(String error) { + state = FTState.NONE; + completed = true; + errorMessage = error; + } + + @Override + public void onTransferAborted(String error) { + state = FTState.NONE; + completed = false; + errorMessage = error; + } + + @Override + public void onBytesTransferred(long bytes) { + this.bytesTransferred = bytes; + } + + @Override + public FTState getCurrentState() { + return state; + } + + @Override + public void setState(FTState state) { + this.state = state; + } + + @Override + public FTConfig getConfig() { + return config; + } + + @Override + public File getLocalFile() { + return null; + } + } + + @BeforeEach + public void setUp() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + inputProcessor = new InputProcessor(screen, translator, null); + listener = new TestFTCutListener(); + ftCut = new FTCut(screen, inputProcessor, translator, listener); + } + + @Test + public void testCutUploadAsciiExpandsUnixNewlines() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostType(FTConfig.HostType.CMS); + config.setHostFilename("GENPASS REXX A"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.REMOVE); + config.setRemapFlag(true); + listener.config = config; + + String originalText = "WORDS.1 = 'aah'\n"; + ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8)); + ftCut.initTransfer(in, null); + + // Setup CUT screen frame for DATA_REQUEST + screen.setCellFA(O_SF, (byte) (FA_PROTECT | FA_NUMERIC)); + screen.setCell(O_FRAME_TYPE, FT_DATA_REQUEST); + screen.setCell(O_DR_FRAME_SEQ, FTConstants.to6(0, translator)); + + ftCut.processScreenUpdate(); + + // Read uploaded data from screen + int len = (FTConstants.from6(screen.getCellEC(O_UP_LEN), translator) << 6) | + FTConstants.from6(screen.getCellEC(O_UP_LEN + 1), translator); + assertTrue(len > 0); + } +} 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 6d4b164..d635534 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ft/FTDftTest.java @@ -1,68 +1,85 @@ package haus.nightmare.lib3270j.ft; +import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.charset.EbcdicTranslator; import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.screen.ScreenBuffer; -import haus.nightmare.lib3270j.TerminalModel; +import static haus.nightmare.lib3270j.ft.FTConstants.*; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; public class FTDftTest { private EbcdicTranslator translator; - private ScreenBuffer screen; - private InputProcessor input; - private TestListener listener; - private FTDft dft; - private FTConfig config; + private TestInputProcessor inputProcessor; + private TestFTDftListener listener; + private FTDft ftDft; - private static class TestListener implements FTDft.FTDftListener { + static class TestInputProcessor extends InputProcessor { + final List sentStructuredFields = new ArrayList<>(); + + public TestInputProcessor(ScreenBuffer screen, EbcdicTranslator translator) { + super(screen, translator, null); + } + + @Override + public void sendStructuredFieldData(byte[] data) { + sentStructuredFields.add(data.clone()); + } + } + + static class TestFTDftListener implements FTDft.FTDftListener { + FTConfig config; + FTState state = FTState.RUNNING; + long bytesTransferred = 0; boolean dftRunningCalled = false; boolean completeCalled = false; boolean abortCalled = false; String completeMsg = null; String abortMsg = null; - long bytesTransferred = 0; - FTConstants.FTState state = FTConstants.FTState.RUNNING; - FTConfig config; @Override public void onDftRunning() { dftRunningCalled = true; + state = FTState.RUNNING; } @Override - public void onTransferComplete(String errorMessage) { + public void onTransferComplete(String error) { + state = FTState.NONE; completeCalled = true; - completeMsg = errorMessage; + completeMsg = error; } @Override - public void onTransferAborted(String errorMessage) { + public void onTransferAborted(String error) { + state = FTState.NONE; abortCalled = true; - abortMsg = errorMessage; + abortMsg = error; } @Override public void onBytesTransferred(long bytes) { - bytesTransferred = bytes; + this.bytesTransferred = bytes; } @Override - public FTConstants.FTState getCurrentState() { + public FTState getCurrentState() { return state; } @Override - public void setState(FTConstants.FTState state) { + public void setState(FTState state) { this.state = state; } @@ -80,21 +97,35 @@ public class FTDftTest { @BeforeEach public void setUp() { translator = new EbcdicTranslator(); - screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); - input = new InputProcessor(screen, translator, null); - listener = new TestListener(); - config = new FTConfig(); + ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + inputProcessor = new TestInputProcessor(screen, translator); + listener = new TestFTDftListener(); + FTConfig config = new FTConfig(); config.setDirection(FTConfig.Direction.RECEIVE); config.setTransferMode(FTConfig.TransferMode.BINARY); listener.config = config; + ftDft = new FTDft(inputProcessor, translator, listener); + } - dft = new FTDft(input, translator, listener); + private byte[] extractDataFromGetReply(byte[] sf) { + // SF format: AID_SF(1) + Len(2) + SF_TRANSFER_DATA(1) + TR_GET_REPLY(2) + TR_RECNUM_HDR(2) + Recnum(4) + TR_NOT_COMPRESSED(2) + TR_BEGIN_DATA(1) + DataLen(2) + Data(...) + // Find TR_BEGIN_DATA (0x61) + for (int i = 0; i < sf.length - 3; i++) { + if ((sf[i] & 0xFF) == TR_BEGIN_DATA) { + int dataLen = ((sf[i + 1] & 0xFF) << 8) | (sf[i + 2] & 0xFF); + int actualLen = dataLen - 5; + byte[] data = new byte[actualLen]; + System.arraycopy(sf, i + 3, data, 0, actualLen); + return data; + } + } + return new byte[0]; } @Test public void testOpenMessageStreamAsciiAndHandleTrans14Error() { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - dft.initTransfer(null, outStream); + ftDft.initTransfer(null, outStream); // 1. Simulate TR_OPEN_REQ with ASCII "FT:MSG" matching j3270.log: // SF length = 0x0023, SF type = 0xD0, Request code = 0x0012, stream name "FT:MSG " at offset 28 @@ -105,7 +136,7 @@ public class FTDftTest { 0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // "FT:MSG " }; - dft.processStructuredField(openReq, 0, openReq.length); + ftDft.processStructuredField(openReq, 0, openReq.length); // Should NOT trigger onDftRunning because it's a message stream assertFalse(listener.dftRunningCalled); @@ -132,7 +163,7 @@ public class FTDftTest { insertData[0] = (byte) ((sfLen >> 8) & 0xFF); insertData[1] = (byte) (sfLen & 0xFF); - dft.processStructuredField(insertData, 0, insertData.length); + ftDft.processStructuredField(insertData, 0, insertData.length); // Verify that onTransferAborted was called with the error message assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error"); @@ -147,7 +178,7 @@ public class FTDftTest { @Test public void testOpenDataStreamAndWriteData() { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - dft.initTransfer(null, outStream); + ftDft.initTransfer(null, outStream); // 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA" byte[] openReq = new byte[] { @@ -157,7 +188,7 @@ public class FTDftTest { 0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // "FT:DATA" }; - dft.processStructuredField(openReq, 0, openReq.length); + ftDft.processStructuredField(openReq, 0, openReq.length); assertTrue(listener.dftRunningCalled, "onDftRunning should be called for file data stream"); // 2. Simulate TR_DATA_INSERT with binary file payload @@ -181,10 +212,258 @@ public class FTDftTest { insertData[0] = (byte) ((sfLen >> 8) & 0xFF); insertData[1] = (byte) (sfLen & 0xFF); - dft.processStructuredField(insertData, 0, insertData.length); + ftDft.processStructuredField(insertData, 0, insertData.length); // Data must be written to file stream assertArrayEquals(fileData, outStream.toByteArray()); assertEquals(8, listener.bytesTransferred); } + + @Test + public void testDftUploadAsciiExpandsUnixNewlines() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostType(FTConfig.HostType.CMS); + config.setHostFilename("GENPASS REXX A"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.REMOVE); + config.setRemapFlag(true); + listener.config = config; + + String originalText = "WORDS.1 = 'aah'\nWORDS.2 = 'aba'\nWORDS.3 = 'abo'\n"; + ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8)); + ftDft.initTransfer(in, null); + + // Host sends TR_GET_REQ + 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[] reply = inputProcessor.sentStructuredFields.get(0); + byte[] payload = extractDataFromGetReply(reply); + + // Verify payload has CRLF (\r\n) after each line + // Translate payload back: + // Host pseudo-ASCII -> EBCDIC -> Unicode + 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)); + } + } + + String expected = "WORDS.1 = 'aah'\r\nWORDS.2 = 'aba'\r\nWORDS.3 = 'abo'\r\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testDftUploadAsciiPreservesWindowsNewlines() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostType(FTConfig.HostType.CMS); + config.setHostFilename("TEST FILE A"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.REMOVE); + config.setRemapFlag(true); + listener.config = config; + + String originalText = "LINE1\r\nLINE2\r\n"; + ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8)); + 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)); + } + } + + String expected = "LINE1\r\nLINE2\r\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testDftUploadAsciiKeepCr() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostType(FTConfig.HostType.CMS); + config.setHostFilename("TEST FILE A"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.KEEP); + config.setRemapFlag(true); + listener.config = config; + + String originalText = "LINE1\nLINE2\n"; + ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8)); + 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)); + } + } + + String expected = "LINE1\nLINE2\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testDftDownloadAsciiStripsCR() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.RECEIVE); + config.setHostType(FTConfig.HostType.CMS); + config.setHostFilename("TEST FILE A"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.REMOVE); + config.setRemapFlag(true); + listener.config = config; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ftDft.initTransfer(null, out); + + // Prepare host insert data with CRLF + String hostData = "RECORD1\r\nRECORD2\r\n"; + ByteArrayOutputStream hostStream = new ByteArrayOutputStream(); + for (char c : hostData.toCharArray()) { + if (c == '\r' || c == '\n') { + hostStream.write(c); + } else { + int ebc = translator.unicodeToEbcdic(c); + hostStream.write(FT2ASC[ebc & 0xFF]); + } + } + byte[] hostBytes = hostStream.toByteArray(); + + // Build SF_TRANSFER_DATA with TR_DATA_INSERT + ByteArrayOutputStream sf = new ByteArrayOutputStream(); + sf.write(0); sf.write(0); // length placeholder + sf.write(SF_TRANSFER_DATA); + sf.write((TR_DATA_INSERT >> 8) & 0xFF); + sf.write(TR_DATA_INSERT & 0xFF); + sf.write(TR_BEGIN_DATA); + int dataLen = hostBytes.length + 5; + sf.write((dataLen >> 8) & 0xFF); + sf.write(dataLen & 0xFF); + sf.write(hostBytes, 0, hostBytes.length); + + byte[] sfBytes = sf.toByteArray(); + int totalLen = sfBytes.length; + sfBytes[0] = (byte) ((totalLen >> 8) & 0xFF); + sfBytes[1] = (byte) (totalLen & 0xFF); + + ftDft.processStructuredField(sfBytes, 0, totalLen); + + String downloaded = new String(out.toByteArray(), StandardCharsets.UTF_8); + assertEquals("RECORD1\nRECORD2\n", downloaded); + } + + @Test + public void testDftUploadLargeFileMultiBufferSpan() { + FTConfig config = new FTConfig(); + config.setDirection(FTConfig.Direction.SEND); + config.setHostType(FTConfig.HostType.CMS); + config.setHostFilename("GENPASS REXX A"); + config.setTransferMode(FTConfig.TransferMode.ASCII); + config.setCrAction(FTConfig.CrAction.REMOVE); + config.setRemapFlag(true); + config.setDftBufferSize(512); // Use small buffer size to test multi-buffer splitting + listener.config = config; + + // Build 500 lines resembling GENPASS.rexx + StringBuilder originalSb = new StringBuilder(); + originalSb.append("/* REXX */\n"); + for (int i = 1; i <= 500; i++) { + originalSb.append("WORDS.").append(i).append(" = 'word_").append(i).append("'\n"); + } + String originalText = originalSb.toString(); + + ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8)); + 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) + }; + + // Repeatedly process getReq until EOF + while (listener.state != FTState.NONE && inputProcessor.sentStructuredFields.size() < 100) { + int beforeCount = inputProcessor.sentStructuredFields.size(); + ftDft.processStructuredField(getReq, 0, getReq.length); + if (inputProcessor.sentStructuredFields.size() == beforeCount) break; + + byte[] lastSent = inputProcessor.sentStructuredFields.get(inputProcessor.sentStructuredFields.size() - 1); + // Check if EOF reply (0x4608 / TR_ERROR_REPLY) + if (lastSent.length >= 6 && (lastSent[4] & 0xFF) == 0x46 && (lastSent[5] & 0xFF) == 0x08) { + break; + } + } + + // Reconstruct full stream from all data buffers + ByteArrayOutputStream fullStream = new ByteArrayOutputStream(); + for (byte[] sf : inputProcessor.sentStructuredFields) { + byte[] payload = extractDataFromGetReply(sf); + fullStream.write(payload, 0, payload.length); + } + + byte[] allBytes = fullStream.toByteArray(); + StringBuilder decodedSb = new StringBuilder(); + for (byte b : allBytes) { + int ub = b & 0xFF; + if (ub == 0x0D) { + decodedSb.append("\r"); + } else if (ub == 0x0A) { + decodedSb.append("\n"); + } else { + int ebc = ASC2FT[ub]; + decodedSb.append(translator.ebcdicToUnicode(ebc)); + } + } + + String expected = originalText.replace("\n", "\r\n"); + assertEquals(expected, decodedSb.toString()); + + // Verify specific line structure + String decodedStr = decodedSb.toString(); + assertTrue(decodedStr.contains("WORDS.160 = 'word_160'\r\nWORDS.161 = 'word_161'\r\n")); + } }