IND testing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m5s

This commit is contained in:
2026-08-27 22:05:56 -04:00
parent 2e9b6d325a
commit 8654b72d8c
3 changed files with 442 additions and 36 deletions
@@ -42,6 +42,7 @@ public class FTDft {
private boolean dftEof = false; private boolean dftEof = false;
private boolean messageFlag = false; private boolean messageFlag = false;
private long bytesTransferred = 0; private long bytesTransferred = 0;
private int pendingByte = -1;
// Savebuf for Read Modified retransmit // Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null; private byte[] dftSaveBuf = null;
@@ -92,6 +93,7 @@ public class FTDft {
dftSaveBuf = null; dftSaveBuf = null;
dftSaveBufLen = 0; dftSaveBufLen = 0;
lastCr = false; lastCr = false;
pendingByte = -1;
} }
/** /**
@@ -403,7 +405,7 @@ public class FTDft {
try { try {
while (!dftEof && totalRead < numbytes) { while (!dftEof && totalRead < numbytes) {
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) { if (config.isAscii()) {
int b = dftAsciiRead(config); int b = dftAsciiRead(config);
if (b == -1) { if (b == -1) {
dftEof = true; dftEof = true;
@@ -490,6 +492,12 @@ public class FTDft {
* Matching x3270 dft_ascii_read logic. * Matching x3270 dft_ascii_read logic.
*/ */
private int dftAsciiRead(FTConfig config) throws IOException { private int dftAsciiRead(FTConfig config) throws IOException {
if (pendingByte != -1) {
int b = pendingByte;
pendingByte = -1;
return b;
}
if (inputStream == null) return -1; if (inputStream == null) return -1;
int c = inputStream.read(); int c = inputStream.read();
@@ -497,15 +505,18 @@ public class FTDft {
if (config.isCrFlag() && !lastCr && c == '\n') { if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false; lastCr = false;
// Expand \n to \r\n: return \r byte now // Expand \n to \r\n: buffer \n in pendingByte, return \r now
int rEbc = translator.unicodeToEbcdic('\r'); pendingByte = convertAsciiByte('\n', config);
if (rEbc < 0) rEbc = 0x0D; return convertAsciiByte('\r', config);
return config.isRemapFlag() ? FT2ASC[rEbc & 0xFF] : rEbc;
} }
lastCr = (c == '\r'); lastCr = (c == '\r');
return convertAsciiByte((char) c, config);
}
private int convertAsciiByte(char c, FTConfig config) {
if (!config.isRemapFlag()) { if (!config.isRemapFlag()) {
int ebc = translator.unicodeToEbcdic((char) c); int ebc = translator.unicodeToEbcdic(c);
return ebc >= 0 ? ebc : 0x40; return ebc >= 0 ? ebc : 0x40;
} }
@@ -519,7 +530,7 @@ public class FTDft {
} else if (c == 0x9F) { } else if (c == 0x9F) {
ebc = 0xFF; ebc = 0xFF;
} else { } else {
ebc = translator.unicodeToEbcdic((char) c); ebc = translator.unicodeToEbcdic(c);
} }
if (ebc < 0) ebc = 0x40; if (ebc < 0) ebc = 0x40;
@@ -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);
}
}
@@ -1,68 +1,85 @@
package haus.nightmare.lib3270j.ft; package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator; import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
public class FTDftTest { public class FTDftTest {
private EbcdicTranslator translator; private EbcdicTranslator translator;
private ScreenBuffer screen; private TestInputProcessor inputProcessor;
private InputProcessor input; private TestFTDftListener listener;
private TestListener listener; private FTDft ftDft;
private FTDft dft;
private FTConfig config;
private static class TestListener implements FTDft.FTDftListener { static class TestInputProcessor extends InputProcessor {
final List<byte[]> 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 dftRunningCalled = false;
boolean completeCalled = false; boolean completeCalled = false;
boolean abortCalled = false; boolean abortCalled = false;
String completeMsg = null; String completeMsg = null;
String abortMsg = null; String abortMsg = null;
long bytesTransferred = 0;
FTConstants.FTState state = FTConstants.FTState.RUNNING;
FTConfig config;
@Override @Override
public void onDftRunning() { public void onDftRunning() {
dftRunningCalled = true; dftRunningCalled = true;
state = FTState.RUNNING;
} }
@Override @Override
public void onTransferComplete(String errorMessage) { public void onTransferComplete(String error) {
state = FTState.NONE;
completeCalled = true; completeCalled = true;
completeMsg = errorMessage; completeMsg = error;
} }
@Override @Override
public void onTransferAborted(String errorMessage) { public void onTransferAborted(String error) {
state = FTState.NONE;
abortCalled = true; abortCalled = true;
abortMsg = errorMessage; abortMsg = error;
} }
@Override @Override
public void onBytesTransferred(long bytes) { public void onBytesTransferred(long bytes) {
bytesTransferred = bytes; this.bytesTransferred = bytes;
} }
@Override @Override
public FTConstants.FTState getCurrentState() { public FTState getCurrentState() {
return state; return state;
} }
@Override @Override
public void setState(FTConstants.FTState state) { public void setState(FTState state) {
this.state = state; this.state = state;
} }
@@ -80,21 +97,35 @@ public class FTDftTest {
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
translator = new EbcdicTranslator(); translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
input = new InputProcessor(screen, translator, null); inputProcessor = new TestInputProcessor(screen, translator);
listener = new TestListener(); listener = new TestFTDftListener();
config = new FTConfig(); FTConfig config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE); config.setDirection(FTConfig.Direction.RECEIVE);
config.setTransferMode(FTConfig.TransferMode.BINARY); config.setTransferMode(FTConfig.TransferMode.BINARY);
listener.config = config; 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 @Test
public void testOpenMessageStreamAsciiAndHandleTrans14Error() { public void testOpenMessageStreamAsciiAndHandleTrans14Error() {
ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream); ftDft.initTransfer(null, outStream);
// 1. Simulate TR_OPEN_REQ with ASCII "FT:MSG" matching j3270.log: // 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 // 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 " 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 // Should NOT trigger onDftRunning because it's a message stream
assertFalse(listener.dftRunningCalled); assertFalse(listener.dftRunningCalled);
@@ -132,7 +163,7 @@ public class FTDftTest {
insertData[0] = (byte) ((sfLen >> 8) & 0xFF); insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 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 // Verify that onTransferAborted was called with the error message
assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error"); assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error");
@@ -147,7 +178,7 @@ public class FTDftTest {
@Test @Test
public void testOpenDataStreamAndWriteData() { public void testOpenDataStreamAndWriteData() {
ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream); ftDft.initTransfer(null, outStream);
// 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA" // 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA"
byte[] openReq = new byte[] { byte[] openReq = new byte[] {
@@ -157,7 +188,7 @@ public class FTDftTest {
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // "FT:DATA" 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"); assertTrue(listener.dftRunningCalled, "onDftRunning should be called for file data stream");
// 2. Simulate TR_DATA_INSERT with binary file payload // 2. Simulate TR_DATA_INSERT with binary file payload
@@ -181,10 +212,258 @@ public class FTDftTest {
insertData[0] = (byte) ((sfLen >> 8) & 0xFF); insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 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 // Data must be written to file stream
assertArrayEquals(fileData, outStream.toByteArray()); assertArrayEquals(fileData, outStream.toByteArray());
assertEquals(8, listener.bytesTransferred); 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"));
}
} }