Fix ADMDRAW targetting

This commit is contained in:
2026-08-30 23:33:33 +00:00
parent 3a576b79b4
commit a3c4b95379
15 changed files with 1381 additions and 212 deletions
@@ -22,10 +22,16 @@ public class QueryReplyBuilderTest {
assertTrue(replies.length > 0);
assertEquals((byte) AID_SF, replies[0]);
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present
// In base query reply (buildAllQueryReplies), Summary (0x80) advertises QR_SEGMENT (0xB0)
// while the base reply itself contains base SFs (Usable Area, Charsets, Color, Highlighting, Reply Modes, DDM, AuxDA, ImpPart)
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
assertEquals(QR_SUMMARY, replies[4] & 0xFF); // 0x80
// In buildCompleteQueryReplies, Vector Graphics QR 0xB0 structured field payload is present
byte[] completeReplies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
boolean hasB0 = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
for (int i = 0; i < completeReplies.length - 3; i++) {
if ((completeReplies[i] & 0xFF) == 0x81 && (completeReplies[i + 1] & 0xFF) == QR_SEGMENT) {
hasB0 = true;
break;
}
@@ -41,31 +47,27 @@ public class QueryReplyBuilderTest {
assertTrue(replies.length > 0);
assertEquals((byte) AID_SF, replies[0]);
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be present
boolean hasB0 = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
hasB0 = true;
break;
}
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary
int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
for (int i = 5; i < 1 + sumLen; i++) {
assertNotEquals(QR_SEGMENT, replies[i] & 0xFF);
}
assertFalse(hasB0);
}
@Test
public void testBuildAllQueryRepliesWithVectorGraphics() {
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
// Vector Graphics QR 0xB0 and 0xB4 must be present
// In complete replies, Vector Graphics QR 0xB0 and 0xB4 must be present
boolean hasB0 = false;
boolean hasB4 = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRCOLOR) {
hasB4 = true;
}
}
@@ -76,17 +78,17 @@ public class QueryReplyBuilderTest {
@Test
public void testBuildAllQueryRepliesWithBoth() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
// Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A)
boolean hasB0 = false;
boolean hasCharsetsWithLoadPs = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
if ((replies[i] & 0xFF) == 0x81 && (completeReplyIsCharsets(replies, i))) {
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
hasCharsetsWithLoadPs = true;
}
@@ -96,17 +98,21 @@ public class QueryReplyBuilderTest {
assertTrue(hasCharsetsWithLoadPs);
}
private boolean completeReplyIsCharsets(byte[] replies, int i) {
return (replies[i + 1] & 0xFF) == QR_CHARSETS;
}
@Test
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
// Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present
// Charsets with LoadPS (0x0A) must be present, and QR_SEGMENT must NOT be present
boolean hasB0 = false;
boolean hasCharsetsWithLoadPs = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
@@ -240,7 +246,7 @@ public class QueryReplyBuilderTest {
@Test
public void testQueryReplyImageAndRpqNames() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE };
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_TRANSPARENCY, (byte) QR_PROCEDURE };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
@@ -251,15 +257,56 @@ public class QueryReplyBuilderTest {
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
// Second SF should be QR_RPQNAMES (0xA1) matching requested code
// Second SF should be QR_TRANSPARENCY (0xA8) matching requested code
int pos2 = 1 + len1;
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
assertEquals(QR_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1
assertEquals(QR_TRANSPARENCY, replies[pos2 + 3] & 0xFF); // 0xA8
// Third SF should be QR_GIMAGE (0xB1)
// Third SF should be QR_PROCEDURE (0xB1)
int pos3 = pos2 + len2;
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1
assertEquals(QR_PROCEDURE, replies[pos3 + 3] & 0xFF); // 0xB1
}
@Test
public void testUsableAreaMetricsMatchHOD() {
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
byte[] requested = new byte[] { (byte) QR_USABLE_AREA };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// Length (2 bytes), SFID (0x81), QR_USABLE_AREA (0x81)
assertEquals(23, ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF));
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_USABLE_AREA, replies[4] & 0xFF);
// Flags: 12/14 bit addressing (0x01), 0x00
assertEquals(0x01, replies[5] & 0xFF);
assertEquals(0x00, replies[6] & 0xFF);
// Usable width and height: 80, 43
assertEquals(80, ((replies[7] & 0xFF) << 8) | (replies[8] & 0xFF));
assertEquals(43, ((replies[9] & 0xFF) << 8) | (replies[10] & 0xFF));
// Units: 0x00 (Inches, matching IBM Host On-Demand QR_USEAREA_STRING)
assertEquals(0x00, replies[11] & 0xFF);
// Xr (4 bytes): 0x00020089 (matching HOD)
int xr = ((replies[12] & 0xFF) << 24) | ((replies[13] & 0xFF) << 16) | ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
assertEquals(0x00020089, xr);
// Yr (4 bytes): 0x00020085 (matching HOD)
int yr = ((replies[16] & 0xFF) << 24) | ((replies[17] & 0xFF) << 16) | ((replies[18] & 0xFF) << 8) | (replies[19] & 0xFF);
assertEquals(0x00020085, yr);
// AW and AH: 9 and 16
assertEquals(9, replies[20] & 0xFF);
assertEquals(16, replies[21] & 0xFF);
// Buffer size: 80 * 43 = 3440
assertEquals(80 * 43, ((replies[22] & 0xFF) << 8) | (replies[23] & 0xFF));
}
}
@@ -0,0 +1,271 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class QueryReplyPhase2Test {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
/**
* Helper to parse structured fields out of a query reply stream.
*/
private static class ParsedSF {
int length;
int sfid;
int qcode;
byte[] payload;
ParsedSF(int length, int sfid, int qcode, byte[] payload) {
this.length = length;
this.sfid = sfid;
this.qcode = qcode;
this.payload = payload;
}
}
private List<ParsedSF> parseQueryReplies(byte[] data) {
List<ParsedSF> list = new ArrayList<>();
assertNotNull(data);
assertTrue(data.length > 0);
assertEquals((byte) AID_SF, data[0]);
int pos = 1;
while (pos < data.length) {
int len = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
assertTrue(len >= 4, "Structured field length must be >= 4 at pos " + pos);
int sfid = data[pos + 2] & 0xFF;
int qcode = data[pos + 3] & 0xFF;
byte[] payload = new byte[len - 4];
System.arraycopy(data, pos + 4, payload, 0, payload.length);
list.add(new ParsedSF(len, sfid, qcode, payload));
pos += len;
}
assertEquals(data.length, pos, "All bytes in query reply must be accounted for");
return list;
}
@Test
public void testQCodeConstantsValuesMatchHod() {
assertEquals(0x80, QR_SUMMARY);
assertEquals(0x81, QR_USABLE_AREA);
assertEquals(0x84, QR_ALPHA_PART);
assertEquals(0x85, QR_CHARSETS);
assertEquals(0x86, QR_COLOR);
assertEquals(0x87, QR_HIGHLIGHTING);
assertEquals(0x88, QR_REPLY_MODES);
assertEquals(0x8C, QR_OUTLINING);
assertEquals(0x91, QR_DBCS_ASIA);
assertEquals(0x95, QR_DDM);
assertEquals(0x99, QR_AUXDA);
assertEquals(0xA6, QR_IMP_PART);
assertEquals(0xA8, QR_TRANSPARENCY);
assertEquals(0xB0, QR_SEGMENT);
assertEquals(0xB1, QR_PROCEDURE);
assertEquals(0xB2, QR_LINETYPE);
assertEquals(0xB3, QR_PORT);
assertEquals(0xB4, QR_GRCOLOR);
assertEquals(0xB6, QR_GRSYMBOLSET);
assertEquals(0xFF, QR_NULL);
}
@Test
public void testBaseGenericQueryReplySizeAndStructure() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] reply = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(reply);
List<ParsedSF> sfs = parseQueryReplies(reply);
// Base query reply must contain exactly 10 structured fields for SBCS:
// 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets),
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part)
assertEquals(10, sfs.size());
assertEquals(QR_SUMMARY, sfs.get(0).qcode);
assertEquals(QR_USABLE_AREA, sfs.get(1).qcode);
assertEquals(QR_ALPHA_PART, sfs.get(2).qcode);
assertEquals(QR_CHARSETS, sfs.get(3).qcode);
assertEquals(QR_COLOR, sfs.get(4).qcode);
assertEquals(QR_HIGHLIGHTING, sfs.get(5).qcode);
assertEquals(QR_REPLY_MODES, sfs.get(6).qcode);
assertEquals(QR_DDM, sfs.get(7).qcode);
assertEquals(QR_AUXDA, sfs.get(8).qcode);
assertEquals(QR_IMP_PART, sfs.get(9).qcode);
// Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply
for (ParsedSF sf : sfs) {
assertNotEquals(QR_TRANSPARENCY, sf.qcode);
assertNotEquals(QR_SEGMENT, sf.qcode);
assertNotEquals(QR_PROCEDURE, sf.qcode);
assertNotEquals(QR_LINETYPE, sf.qcode);
assertNotEquals(QR_PORT, sf.qcode);
assertNotEquals(QR_GRCOLOR, sf.qcode);
assertNotEquals(QR_GRSYMBOLSET, sf.qcode);
}
// Verify summary structured field advertises full capabilities
byte[] summaryPayload = sfs.get(0).payload;
assertTrue(summaryPayload.length >= 17);
assertEquals(QR_SUMMARY, summaryPayload[0] & 0xFF);
assertEquals(QR_USABLE_AREA, summaryPayload[1] & 0xFF);
}
@Test
public void testTargetedQueryListAdmDrawResponse() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
// ADMDRAW vector graphics startup query list: 0x8C, 0xA8, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB6
byte[] requested = new byte[] {
(byte) QR_OUTLINING, // 0x8C
(byte) QR_TRANSPARENCY, // 0xA8
(byte) QR_SEGMENT, // 0xB0
(byte) QR_PROCEDURE, // 0xB1
(byte) QR_LINETYPE, // 0xB2
(byte) QR_PORT, // 0xB3
(byte) QR_GRCOLOR, // 0xB4
(byte) QR_GRSYMBOLSET // 0xB6
};
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
List<ParsedSF> sfs = parseQueryReplies(replies);
// 8 requested QCODEs produce 11 SFs because QR_PORT emits 4 OEM format sub-fields
assertEquals(11, sfs.size());
// 1. QR_OUTLINING (0x8C): 10 bytes
assertEquals(QR_OUTLINING, sfs.get(0).qcode);
assertEquals(10, sfs.get(0).length);
assertArrayEquals(new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, sfs.get(0).payload);
// 2. QR_TRANSPARENCY (0xA8): 9 bytes
assertEquals(QR_TRANSPARENCY, sfs.get(1).qcode);
assertEquals(9, sfs.get(1).length);
assertArrayEquals(new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }, sfs.get(1).payload);
// 3. QR_SEGMENT (0xB0): 11 bytes
assertEquals(QR_SEGMENT, sfs.get(2).qcode);
assertEquals(11, sfs.get(2).length);
assertArrayEquals(new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 }, sfs.get(2).payload);
// 4. QR_PROCEDURE (0xB1): 21 bytes
assertEquals(QR_PROCEDURE, sfs.get(3).qcode);
assertEquals(21, sfs.get(3).length);
assertArrayEquals(new byte[]{
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00, 0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
}, sfs.get(3).payload);
// 5. QR_LINETYPE (0xB2): 24 bytes
assertEquals(QR_LINETYPE, sfs.get(4).qcode);
assertEquals(24, sfs.get(4).length);
assertArrayEquals(new byte[]{
0x00, 0x09, 0x00, 0x07, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
}, sfs.get(4).payload);
// 6-9. QR_PORT (0xB3): 4 subfields (17, 15, 17, 15 bytes)
assertEquals(QR_PORT, sfs.get(5).qcode);
assertEquals(17, sfs.get(5).length);
assertEquals(QR_PORT, sfs.get(6).qcode);
assertEquals(15, sfs.get(6).length);
assertEquals(QR_PORT, sfs.get(7).qcode);
assertEquals(17, sfs.get(7).length);
assertEquals(QR_PORT, sfs.get(8).qcode);
assertEquals(15, sfs.get(8).length);
// 10. QR_GRCOLOR (0xB4): 109 bytes
assertEquals(QR_GRCOLOR, sfs.get(9).qcode);
assertEquals(109, sfs.get(9).length);
// 11. QR_GRSYMBOLSET (0xB6): 33 bytes
assertEquals(QR_GRSYMBOLSET, sfs.get(10).qcode);
assertEquals(33, sfs.get(10).length);
}
@Test
public void testUnsupportedQCodesEmitQrNull() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] requested = new byte[] { (byte) 0x12, (byte) 0x7E, (byte) 0xEE };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
List<ParsedSF> sfs = parseQueryReplies(replies);
assertEquals(3, sfs.size());
for (ParsedSF sf : sfs) {
assertEquals(QR_NULL, sf.qcode);
assertEquals(4, sf.length);
assertEquals(0, sf.payload.length);
}
}
@Test
public void testDynamicCellDimensions() {
// In Vector Graphics mode, SDH must be 16 (0x10) for 3179G
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
assertEquals(9, qrBuilder.getCharWidth());
assertEquals(16, qrBuilder.getCharHeight());
// In Text-only mode (GraphicsMode.NONE), SDH must be 12 (0x0C) for 3279-2
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
assertEquals(9, qrBuilder.getCharWidth());
assertEquals(12, qrBuilder.getCharHeight());
}
@Test
public void testDataStreamProcessorQueryListDispatch() {
ByteArrayOutputStream captured = new ByteArrayOutputStream();
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
processor.setOutputSender(data -> {
try {
captured.write(data);
} catch (Exception e) {
fail(e);
}
});
// Test SF_RP_QUERY (0x02): generic query -> base query reply
byte[] genericQuery = new byte[] {
(byte) CMD_WSF,
0x00, 0x05, // Length = 5
(byte) SF_READ_PART, 0x00, // Partition = 0
(byte) SF_RP_QUERY // Type = 0x02
};
captured.reset();
processor.processRecord(genericQuery, 0, genericQuery.length, false);
byte[] sent = captured.toByteArray();
assertTrue(sent.length > 0);
assertEquals((byte) AID_SF, sent[0]);
List<ParsedSF> sfs = parseQueryReplies(sent);
assertEquals(10, sfs.size());
// Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4
byte[] queryList = new byte[] {
(byte) CMD_WSF,
0x00, 0x08, // Length = 8
(byte) SF_READ_PART, 0x00,
(byte) SF_RP_QLIST,
(byte) SF_RPQ_LIST, // 0x00
(byte) QR_SEGMENT, // 0xB0
(byte) QR_GRCOLOR // 0xB4
};
captured.reset();
processor.processRecord(queryList, 0, queryList.length, false);
sent = captured.toByteArray();
assertTrue(sent.length > 0);
List<ParsedSF> listSfs = parseQueryReplies(sent);
assertEquals(2, listSfs.size());
assertEquals(QR_SEGMENT, listSfs.get(0).qcode);
assertEquals(QR_GRCOLOR, listSfs.get(1).qcode);
}
}
@@ -0,0 +1,272 @@
package haus.nightmare.lib3270j.input;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.graphics.GddmCoordinateTransform;
import haus.nightmare.lib3270j.graphics.GocaDecoder;
import haus.nightmare.lib3270j.graphics.GraphicsPlane;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.Point;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 3: Interactive Selection, Cursor Addressing & ADMDRAW Alignment.
*/
public class Phase3InteractiveSelectionTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
}
@Test
public void testScreenBufferSetCursorPosition() {
screen.setCursorPosition(5, 12);
assertEquals(5, screen.getCursorRow());
assertEquals(12, screen.getCursorCol());
assertEquals(5 * 80 + 12, screen.getCursorAddress());
// Test clamping
screen.setCursorPosition(-2, -5);
assertEquals(0, screen.getCursorRow());
assertEquals(0, screen.getCursorCol());
assertEquals(0, screen.getCursorAddress());
screen.setCursorPosition(100, 200);
assertEquals(23, screen.getCursorRow());
assertEquals(79, screen.getCursorCol());
assertEquals(23 * 80 + 79, screen.getCursorAddress());
}
@Test
public void testImmediateLightPenSelectionSpaceDesignator() {
// Unprotected selectable field at pos 0 with space designator (0x40)
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
screen.getCell(1).ec = 0x40; // ' '
screen.getCell(1).ucs4 = ' ';
screen.getCell(2).ec = (byte) 0xC1; // 'A'
screen.getCell(2).ucs4 = 'A';
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
AtomicReference<byte[]> sentData = new AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sentData.set(data);
}
};
// Click at address 5 inside the field
boolean handled = input.lightPenSelect(5);
assertTrue(handled);
assertEquals(5, screen.getCursorAddress(), "Cursor should be positioned at clicked address");
assertTrue(faIsModified(screen.getCell(0).fa & 0xFF), "MDT should be set");
byte[] sent = sentData.get();
assertNotNull(sent);
assertEquals((byte) AID_SELECT, sent[0], "Immediate space designator must send AID_SELECT (0x7E)");
// Format: AID(1) + CursorAddr(2) + SBA(1) + DesignatorAddr(2) = 6 bytes (NO text content!)
assertEquals(6, sent.length, "AID_SELECT must NOT include field text content");
assertEquals((byte) ORDER_SBA, sent[3]);
}
@Test
public void testImmediateLightPenSelectionNullDesignator() {
// Unprotected selectable field at pos 0 with null designator (0x00)
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
screen.getCell(1).ec = 0x00; // null
screen.getCell(1).ucs4 = 0;
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
AtomicReference<byte[]> sentData = new AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sentData.set(data);
}
};
boolean handled = input.lightPenSelect(2);
assertTrue(handled);
assertEquals(2, screen.getCursorAddress());
assertTrue(faIsModified(screen.getCell(0).fa & 0xFF));
byte[] sent = sentData.get();
assertNotNull(sent);
assertEquals((byte) AID_SELECT, sent[0]);
}
@Test
public void testImmediateEnterLightPenSelectionAmpersandDesignator() {
// Selectable field at pos 0 with '&' designator (0x50)
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
screen.getCell(1).ec = 0x50; // '&'
screen.getCell(1).ucs4 = '&';
screen.getCell(2).ec = (byte) 0xC1; // 'A'
screen.getCell(2).ucs4 = 'A';
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
AtomicReference<byte[]> sentData = new AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sentData.set(data);
}
};
boolean handled = input.lightPenSelect(1);
assertTrue(handled);
assertEquals(1, screen.getCursorAddress());
assertTrue(faIsModified(screen.getCell(0).fa & 0xFF));
byte[] sent = sentData.get();
assertNotNull(sent);
assertEquals((byte) AID_ENTER, sent[0], "Immediate '&' designator must send AID_ENTER (0x7D)");
}
@Test
public void testDeferredLightPenSelectionToggle() {
// Protected selectable field at pos 0 with '?' designator (0x6F)
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_NORM_SEL));
screen.getCell(1).ec = 0x6F; // '?'
screen.getCell(1).ucs4 = '?';
screen.getCell(2).ec = (byte) 0xC3; // 'C'
screen.getCell(2).ucs4 = 'C';
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
AtomicReference<byte[]> sentData = new AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sentData.set(data);
}
};
// First click: toggle '?' -> '>' and set MDT (no transmission)
boolean handled = input.lightPenSelect(1);
assertTrue(handled);
assertEquals((byte) 0x6E, screen.getCell(1).ec, "Designator must change to '>' (0x6E)");
assertEquals('>', (char) screen.getCell(1).ucs4);
assertTrue(faIsModified(screen.getCell(0).fa & 0xFF), "MDT must be set");
assertNull(sentData.get(), "Deferred selection must NOT transmit immediately");
// Now user presses Enter: transmits AID_ENTER with '>' and modified field
input.sendAid(AID_ENTER);
byte[] sent = sentData.get();
assertNotNull(sent);
assertEquals((byte) AID_ENTER, sent[0]);
assertEquals((byte) ORDER_SBA, sent[3]);
assertEquals((byte) 0x6E, sent[6], "Sent field content must contain '>' (0x6E)");
assertEquals((byte) 0xC3, sent[7], "Sent field content must contain 'C'");
sentData.set(null);
// Second click: toggle '>' -> '?' and clear MDT
handled = input.lightPenSelect(1);
assertTrue(handled);
assertEquals((byte) 0x6F, screen.getCell(1).ec, "Designator must change back to '?' (0x6F)");
assertEquals('?', (char) screen.getCell(1).ucs4);
assertFalse(faIsModified(screen.getCell(0).fa & 0xFF), "MDT must be cleared");
assertNull(sentData.get(), "Deferred deselection must NOT transmit immediately");
}
@Test
public void testSendGraphicMouseAidCoordinatesAndCursorSynchronization() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xD6; // 'O'
screen.getCell(2).ec = (byte) 0xD2; // 'K'
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
// Click at row 3, col 15 -> address = 3 * 80 + 15 = 255
screen.setCursorPosition(3, 15);
GraphicsPlane plane = new GraphicsPlane(720, 384);
plane.setScreenDimensions(80, 24);
GocaDecoder goca = new GocaDecoder(plane);
goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(200, 100);
AtomicReference<byte[]> sentData = new AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sentData.set(data);
}
};
input.setGocaDecoder(goca);
input.sendGraphicMouseAid(AID_ENTER, 1, false, false);
byte[] sent = sentData.get();
assertNotNull(sent);
// AID_SF (0x88) + 56 SF bytes + AID_ENTER (0x7D) + 2 cursor bytes + SBA + ...
assertEquals((byte) AID_SF, sent[0]);
assertEquals(0x00, sent[1]);
assertEquals(0x34, sent[2]); // 52-byte SF length mask
// Coordinate verification in SF
int gx = (sent[25] << 8) | (sent[26] & 0xFF);
int gy = (sent[27] << 8) | (sent[28] & 0xFF);
assertEquals(200, (short) gx);
assertEquals(100, (short) gy);
// Trailing AID and cursor SBA at index 57-59
assertEquals((byte) AID_ENTER, sent[57]);
byte[] expectedCursor = encodeAddress(3 * 80 + 15, 24, 80);
assertEquals(expectedCursor[0], sent[58], "Trailing cursor address byte 0 must match clicked row/col");
assertEquals(expectedCursor[1], sent[59], "Trailing cursor address byte 1 must match clicked row/col");
}
@Test
public void testGddmCoordinateTransformModel2AndModel4() {
// Model 2 (80x24)
GddmCoordinateTransform t2 = new GddmCoordinateTransform(80, 24, 9, 16);
assertEquals(720, t2.getTotalWidth());
assertEquals(384, t2.getTotalHeight());
assertEquals(360, t2.getXMax());
assertEquals(191, t2.getYMax());
// Row 0 click (y in [0..15] out of 384)
Point gocaTop = t2.screenPixelToGoca(0, 0, 0, 0, 9, 16);
assertEquals(-360, gocaTop.x);
assertEquals(191, gocaTop.y, "Top row in Model 2 must start at gy = 191");
// Model 4 (80x43)
GddmCoordinateTransform t4 = new GddmCoordinateTransform(80, 43, 9, 16);
assertEquals(720, t4.getTotalWidth());
assertEquals(688, t4.getTotalHeight());
assertEquals(360, t4.getXMax());
assertEquals(343, t4.getYMax());
Point gocaTop4 = t4.screenPixelToGoca(0, 0, 0, 0, 9, 16);
assertEquals(-360, gocaTop4.x);
assertEquals(343, gocaTop4.y, "Top row in Model 4 must start at gy = 343");
// Center roundtrip
Point centerBase = t4.gocaToBase(0, 0);
assertEquals(360, centerBase.x);
assertEquals(343, centerBase.y);
Point centerGoca = t4.baseToGoca(centerBase.x, centerBase.y);
assertEquals(0, centerGoca.x);
assertEquals(0, centerGoca.y);
}
}
@@ -0,0 +1,347 @@
package haus.nightmare.lib3270j.telnet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
public class TelnetFSMPhase1Test {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private InputProcessor inputProcessor;
private TelnetFSM fsm;
private MockConnection connection;
private EbcdicTranslator translator;
private static class MockConnection extends TelnetConnection {
final List<byte[]> sentData = new ArrayList<>();
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
super(config, fsm);
}
@Override
public synchronized void sendRaw(byte[] data) {
sentData.add(data.clone());
}
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
sentData.add(b);
}
}
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
dsProcessor = new DataStreamProcessor(screenBuffer, translator);
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
for (int b : bytes) {
fsm.feedByte(b & 0xFF);
}
}
@Test
public void testTn3270eSequencingWaitsForSendDeviceType() {
config.setTn3270eEnabled(true);
fsm.onConnected();
connection.sentData.clear();
// 1. Server sends DO TN3270E
feedBytes(IAC, DO, TELOPT_TN3270E);
// Client must reply IAC WILL TN3270E only, and MUST NOT emit unsolicited DEVICE-TYPE REQUEST
assertEquals(1, connection.sentData.size(), "Only WILL TN3270E should be sent initially");
byte[] willPkt = connection.sentData.get(0);
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_TN3270E }, willPkt);
connection.sentData.clear();
// 2. Server sends IAC SB TN3270E SEND DEVICE-TYPE IAC SE
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
// Client must now send IAC SB TN3270E DEVICE-TYPE REQUEST <type> IAC SE
assertEquals(1, connection.sentData.size(), "Client should send DEVICE-TYPE REQUEST after SEND command");
byte[] reqPkt = connection.sentData.get(0);
assertTrue((reqPkt[0] & 0xFF) == IAC && (reqPkt[1] & 0xFF) == SB && (reqPkt[2] & 0xFF) == TELOPT_TN3270E);
assertEquals(OP_DEVICE_TYPE, reqPkt[3] & 0xFF);
assertEquals(OP_REQUEST, reqPkt[4] & 0xFF);
String devType = new String(reqPkt, 5, reqPkt.length - 7);
assertEquals("IBM-3279-4-E", devType);
}
@Test
public void testTn3270eResponseSignalingAlwaysResponse() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
// Functions: RESPONSES + BIND-IMAGE
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
connection.sentData.clear();
// Send 3270 data record with ALWAYS_RESPONSE (0x02) and seq = 0x0042
int seq = 0x0042;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(DT_3270_DATA); // byte 0: data type
bos.write(0x00); // byte 1: request flag
bos.write(RSF_ALWAYS_RESPONSE);// byte 2: response flag (0x02)
bos.write((seq >> 8) & 0xFF); // byte 3: seq hi
bos.write(seq & 0xFF); // byte 4: seq lo
bos.write(0xF5); // EraseWrite
bos.write(0xC3); // WCC
bos.write(0x11); bos.write(0x40); bos.write(0x40); // SBA 0
bos.write(translator.unicodeToEbcdic('A'));
bos.write(IAC);
bos.write(EOR);
for (byte b : bos.toByteArray()) fsm.feedByte(b & 0xFF);
// Client must emit positive response: DT_RESPONSE (0x02), 0x00, RSF_POSITIVE_RESPONSE (0x00), seq, POS_DEVICE_END (0x00), IAC, EOR
boolean foundPositiveResponse = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length >= 8 &&
(pkt[0] & 0xFF) == DT_RESPONSE &&
(pkt[2] & 0xFF) == RSF_POSITIVE_RESPONSE &&
((((pkt[3] & 0xFF) << 8) | (pkt[4] & 0xFF)) == seq) &&
(pkt[5] & 0xFF) == POS_DEVICE_END &&
(pkt[6] & 0xFF) == IAC &&
(pkt[7] & 0xFF) == EOR) {
foundPositiveResponse = true;
}
}
assertTrue(foundPositiveResponse, "Expected positive response with matching sequence number for RSF_ALWAYS_RESPONSE");
}
@Test
public void testTn3270eResponseSignalingNoResponse() {
config.setTn3270eEnabled(true);
fsm.onConnected();
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
connection.sentData.clear();
// Send 3270 data record with NO_RESPONSE (0x00)
int seq = 0x0099;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(DT_3270_DATA);
bos.write(0x00);
bos.write(RSF_NO_RESPONSE); // 0x00
bos.write((seq >> 8) & 0xFF);
bos.write(seq & 0xFF);
bos.write(0xF5);
bos.write(0xC3);
bos.write(IAC);
bos.write(EOR);
for (byte b : bos.toByteArray()) fsm.feedByte(b & 0xFF);
// No response packets should be sent
assertEquals(0, connection.sentData.size(), "No response should be generated when RSF_NO_RESPONSE (0x00)");
}
@Test
public void testSysReqOutOfBandRoutingAndToggle() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Complete TN3270E negotiation
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SYSREQ,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Send BIND image to bind session
byte[] bindPacket = new byte[EH_SIZE + 35];
bindPacket[0] = DT_BIND_IMAGE;
bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1;
bindPacket[EH_SIZE + 24] = 0x02; // Model 2 (24x80)
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
bStream.write(bindPacket, 0, bindPacket.length);
bStream.write(IAC);
bStream.write(EOR);
for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
assertTrue(fsm.isTn3270eBound());
connection.sentData.clear();
// Lock keyboard as if host app is waiting
inputProcessor.setKeyboardLocked(true);
assertTrue(inputProcessor.isKeyboardLocked());
// 1. User presses SYSREQ key (AID 0xF0)
inputProcessor.sendAid(DS3270Constants.AID_SYSREQ);
// Keyboard should now be unlocked and IAC AO (0xFF 0xF5) sent out-of-band
assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should be unlocked on SYSREQ");
boolean foundIacAo = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 2 && (pkt[0] & 0xFF) == IAC && (pkt[1] & 0xFF) == AO) {
foundIacAo = true;
}
}
assertTrue(foundIacAo, "Expected raw IAC AO out-of-band telnet command");
assertEquals(ConnectionState.CONNECTED_SSCP, fsm.getConnectionState(), "Session should switch to CONNECTED_SSCP");
connection.sentData.clear();
// 2. User types in SSCP-LU mode (e.g. "LOGOFF") and presses Enter
inputProcessor.typeCharacter('L');
inputProcessor.typeCharacter('O');
inputProcessor.typeCharacter('G');
inputProcessor.typeCharacter('O');
inputProcessor.typeCharacter('F');
inputProcessor.typeCharacter('F');
inputProcessor.sendAid(DS3270Constants.AID_ENTER);
// Verify sent record has header DT_SSCP_LU_DATA (0x07)
assertEquals(1, connection.sentData.size(), "SSCP-LU input should produce one record");
byte[] sscpPkt = connection.sentData.get(0);
assertEquals(DT_SSCP_LU_DATA, sscpPkt[0] & 0xFF, "Data type should be DT_SSCP_LU_DATA (0x07)");
assertEquals(translator.unicodeToEbcdic('L'), sscpPkt[5] & 0xFF);
assertEquals(translator.unicodeToEbcdic('O'), sscpPkt[6] & 0xFF);
connection.sentData.clear();
// 3. Host sends inbound SSCP-LU unformatted prompt
ByteArrayOutputStream ussPrompt = new ByteArrayOutputStream();
ussPrompt.write(DT_SSCP_LU_DATA);
ussPrompt.write(0); ussPrompt.write(0); ussPrompt.write(0); ussPrompt.write(2);
ussPrompt.write(translator.unicodeToEbcdic('U'));
ussPrompt.write(translator.unicodeToEbcdic('S'));
ussPrompt.write(translator.unicodeToEbcdic('S'));
ussPrompt.write(IAC);
ussPrompt.write(EOR);
for (byte b : ussPrompt.toByteArray()) fsm.feedByte(b & 0xFF);
assertEquals('U', translator.ebcdicToUnicode(screenBuffer.getCellEC(80)));
assertEquals('S', translator.ebcdicToUnicode(screenBuffer.getCellEC(81)));
assertEquals('S', translator.ebcdicToUnicode(screenBuffer.getCellEC(82)));
// 4. User presses SYSREQ again to toggle back to 3270 session
inputProcessor.sendAid(DS3270Constants.AID_SYSREQ);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(), "Second SYSREQ should toggle back to CONNECTED_TN3270E");
}
@Test
public void testTn3270eResponseSignalingErrorResponse() {
config.setTn3270eEnabled(true);
fsm.onConnected();
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Bind session
byte[] bindPacket = new byte[EH_SIZE + 35];
bindPacket[0] = DT_BIND_IMAGE;
bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1;
bindPacket[EH_SIZE + 24] = 0x02;
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
bStream.write(bindPacket, 0, bindPacket.length);
bStream.write(IAC);
bStream.write(EOR);
for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF);
connection.sentData.clear();
// Send a malformed 3270 record that triggers an error with ERROR_RESPONSE (0x01)
int seq = 0x0077;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(DT_3270_DATA);
bos.write(0x00);
bos.write(RSF_ERROR_RESPONSE); // 0x01
bos.write((seq >> 8) & 0xFF);
bos.write(seq & 0xFF);
bos.write(0xF5); // EraseWrite
// Truncated SBA order without address bytes to trigger an exception in dsProcessor
bos.write(0x11); // SBA without address bytes
bos.write(IAC);
bos.write(EOR);
// Note: dsProcessor gracefully handles or if it throws, negative response is sent.
// Let's verify what happens when exception or normal:
for (byte b : bos.toByteArray()) fsm.feedByte(b & 0xFF);
}
}
@@ -153,9 +153,12 @@ public class TelnetFSMPhase2Test {
fsm.onConnected();
output.reset();
// Server sends DO TN3270E -> client sends WILL TN3270E and requests "LU_A"
// Server sends DO TN3270E -> client sends WILL TN3270E
fsm.feedBytes(new byte[] { (byte) IAC, (byte) DO, (byte) TELOPT_TN3270E }, 0, 3);
// Server sends SB TN3270E SEND DEVICE-TYPE -> client requests "LU_A"
fsm.feedBytes(new byte[] { (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, (byte) OP_SEND, (byte) OP_DEVICE_TYPE, (byte) IAC, (byte) SE }, 0, 7);
String sent1 = new String(output.toByteArray());
assertTrue(sent1.contains("LU_A"));
output.reset();