Refactor
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 44s

This commit is contained in:
2026-08-25 16:05:12 +00:00
parent 36b93df986
commit 7eebd2107c
56 changed files with 308 additions and 308 deletions
@@ -0,0 +1,66 @@
package haus.nightmare.lib3270j.charset;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EbcdicTranslatorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
@Test
public void testBasicAlphanumericTranslation() {
assertEquals('A', translator.ebcdicToUnicode(0xC1));
assertEquals('B', translator.ebcdicToUnicode(0xC2));
assertEquals('Z', translator.ebcdicToUnicode(0xE9));
assertEquals('a', translator.ebcdicToUnicode(0x81));
assertEquals('z', translator.ebcdicToUnicode(0xA9));
assertEquals('0', translator.ebcdicToUnicode(0xF0));
assertEquals('9', translator.ebcdicToUnicode(0xF9));
assertEquals(' ', translator.ebcdicToUnicode(0x40));
}
@Test
public void testReverseTranslation() {
assertEquals(0xC1, translator.unicodeToEbcdic('A'));
assertEquals(0x81, translator.unicodeToEbcdic('a'));
assertEquals(0xF0, translator.unicodeToEbcdic('0'));
assertEquals(0x40, translator.unicodeToEbcdic(' '));
assertEquals(0x4B, translator.unicodeToEbcdic('.'));
assertEquals(0x6B, translator.unicodeToEbcdic(','));
}
@Test
public void testSpecialCharactersCP037() {
// [ and ]
assertEquals(0xBA, translator.unicodeToEbcdic('['));
assertEquals(0xBB, translator.unicodeToEbcdic(']'));
assertEquals('[', translator.ebcdicToUnicode(0xBA));
assertEquals(']', translator.ebcdicToUnicode(0xBB));
// { and }
assertEquals(0xC0, translator.unicodeToEbcdic('{'));
assertEquals(0xD0, translator.unicodeToEbcdic('}'));
assertEquals('{', translator.ebcdicToUnicode(0xC0));
assertEquals('}', translator.ebcdicToUnicode(0xD0));
// Backslash \
assertEquals(0xE0, translator.unicodeToEbcdic('\\'));
assertEquals('\\', translator.ebcdicToUnicode(0xE0));
// Caret ^
assertEquals(0xB0, translator.unicodeToEbcdic('^'));
assertEquals('^', translator.ebcdicToUnicode(0xB0));
// Tilde ~
assertEquals(0xA1, translator.unicodeToEbcdic('~'));
assertEquals('~', translator.ebcdicToUnicode(0xA1));
}
@Test
public void testStringRoundTrip() {
String test = "Hello World! 123 [TEST] {VAR} \\";
byte[] ebcdic = translator.stringToEbcdic(test);
String restored = translator.ebcdicToString(ebcdic, 0, ebcdic.length);
assertEquals(test, restored);
}
}
@@ -0,0 +1,85 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class DataStreamProcessorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
@Test
public void testEraseWriteAndSBAOrder() {
// Build an EW record:
// Byte 0: CMD_EW (0x05)
// Byte 1: WCC (0xC3) - restore keyboard, reset MDT
// Byte 2: ORDER_SBA (0x11)
// Byte 3, 4: Encoded address for 0 (0x40, 0x40)
// Byte 5: ORDER_SF (0x1D)
// Byte 6: FA (0xC0) - unprotected
// Byte 7..11: EBCDIC "HELLO" (0xC8, 0xC5, 0xD3, 0xD3, 0xD6)
byte[] record = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) ORDER_SF,
(byte) FA_PRINTABLE,
(byte) 0xC8, (byte) 0xC5, (byte) 0xD3, (byte) 0xD3, (byte) 0xD6
};
processor.processRecord(record, 0, record.length, true);
assertTrue(screen.isFormatted());
assertTrue(screen.getCell(0).isFieldAttribute());
// Positions 1..5 should contain "HELLO"
assertEquals('H', screen.getCell(1).ucs4);
assertEquals('E', screen.getCell(2).ucs4);
assertEquals('L', screen.getCell(3).ucs4);
assertEquals('L', screen.getCell(4).ucs4);
assertEquals('O', screen.getCell(5).ucs4);
}
@Test
public void testRepeatToAddressOrder() {
// Write record with RA from 0 to 10 with character 'A' (0xC1)
byte[] record = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40, // Address 0
(byte) ORDER_RA,
(byte) 0x40, (byte) 0x4A, // Address 10 (0x40, 0x4A)
(byte) 0xC1 // EBCDIC 'A'
};
processor.processRecord(record, 0, record.length, true);
for (int i = 0; i < 10; i++) {
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
}
}
@Test
public void testReadBufferPropagatesLastAid() {
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
processor.setInputProcessor(input);
input.setLastAid(AID_ENTER);
java.util.concurrent.atomic.AtomicReference<byte[]> sentData = new java.util.concurrent.atomic.AtomicReference<>();
processor.setOutputSender(sentData::set);
byte[] rbRecord = new byte[] { (byte) CMD_RB };
processor.processRecord(rbRecord, 0, rbRecord.length, true);
assertNotNull(sentData.get());
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
}
}
@@ -0,0 +1,261 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.Test;
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 QueryReplyBuilderTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
@Test
public void testBuildAllQueryRepliesDefaultBoth() {
assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode());
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
assertTrue(replies.length > 0);
assertEquals((byte) AID_SF, replies[0]);
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must 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;
}
}
assertTrue(hasB0);
}
@Test
public void testBuildAllQueryRepliesExplicitNone() {
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
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;
}
}
assertFalse(hasB0);
}
@Test
public void testBuildAllQueryRepliesWithVectorGraphics() {
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(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) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) {
hasB4 = true;
}
}
assertTrue(hasB0);
assertTrue(hasB4);
}
@Test
public void testBuildAllQueryRepliesWithBoth() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] replies = qrBuilder.buildAllQueryReplies(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) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
hasCharsetsWithLoadPs = true;
}
}
}
assertTrue(hasB0);
assertTrue(hasCharsetsWithLoadPs);
}
@Test
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
// Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS 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) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
hasCharsetsWithLoadPs = true;
}
}
}
assertFalse(hasB0);
assertTrue(hasCharsetsWithLoadPs);
}
@Test
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// First SF should be DDM: length (2 bytes), SFID_QREPLY (0x81), QR_DDM (0x95)
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_DDM, replies[4] & 0xFF);
// Second SF should be Usable Area
int pos2 = 1 + len1;
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
}
@Test
public void testBuildFilteredQueryRepliesWithUnsupportedCode() {
byte[] requested = new byte[] { (byte) 0x77 }; // Unsupported code
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// Should emit QR_NULL (0xFF)
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_NULL, replies[4] & 0xFF);
}
@Test
public void testImplicitPartitionModel4() {
byte[] requested = new byte[] { (byte) QR_IMP_PART };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
int len = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
assertEquals(QR_IMP_PART, replies[4] & 0xFF); // 0xA6
// Payload starts at index 5: 22 bytes total
// Default size = 80 x 24
int defCols = ((replies[10] & 0xFF) << 8) | (replies[11] & 0xFF);
int defRows = ((replies[12] & 0xFF) << 8) | (replies[13] & 0xFF);
assertEquals(80, defCols);
assertEquals(24, defRows);
// Alt size = 80 x 43
int altCols = ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
int altRows = ((replies[16] & 0xFF) << 8) | (replies[17] & 0xFF);
assertEquals(80, altCols);
assertEquals(43, altRows);
}
@Test
public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() {
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
byte[] requested = new byte[] { (byte) QR_CHARSETS };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
int offset = -1;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
offset = i + 2;
break;
}
}
assertTrue(offset >= 0, "QR_CHARSETS must be present");
// QR_CHARSETS payload structure:
// Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte)
// DL is at offset + 8, and is 7 bytes per descriptor.
int dl = replies[offset + 8] & 0xFF;
assertEquals(7, dl);
int descOffset = offset + 9;
// Descriptor 1: SET 0 (Base) -> flags 0x10
assertEquals(0x00, replies[descOffset] & 0xFF);
assertEquals(0x10, replies[descOffset + 1] & 0xFF);
// Descriptor 2: SET 1 (APL) -> flags 0x00
assertEquals(0x01, replies[descOffset + 7] & 0xFF);
assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF);
// Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane)
assertEquals(0x02, replies[descOffset + 14] & 0xFF);
assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF);
// Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane)
assertEquals(0x03, replies[descOffset + 21] & 0xFF);
assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF);
// Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x04, replies[descOffset + 28] & 0xFF);
assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF);
// Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x05, replies[descOffset + 35] & 0xFF);
assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF);
// Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x06, replies[descOffset + 42] & 0xFF);
assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF);
// Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF);
}
@Test
public void testQueryReplyImageAndRpqNames() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// First SF should be QR_NULL (0xFF) because 0x82 is 3270 Image SF (not supported on 3179G)
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
// Second SF should be QR_RPQNAMES (0xA1) 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
// Third SF should be QR_GIMAGE (0xB1)
int pos3 = pos2 + len2;
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1
}
}
@@ -0,0 +1,83 @@
package haus.nightmare.lib3270j.ft;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FTConfigTest {
@Test
public void testTsoReceiveAscii() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("MY.DATASET");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET MY.DATASET ASCII CRLF", config.buildCommand());
}
@Test
public void testTsoReceiveBinary() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("'USER.BIN.DATA'");
config.setTransferMode(FTConfig.TransferMode.BINARY);
assertEquals("IND$FILE GET 'USER.BIN.DATA'", config.buildCommand());
}
@Test
public void testTsoSendFixedRecfm() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.TSO);
config.setDirection(FTConfig.Direction.SEND);
config.setHostFilename("MY.DATASET");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
config.setRecfm("F");
config.setLrecl("80");
config.setBlksize("3120");
assertEquals("IND$FILE PUT MY.DATASET ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120)", config.buildCommand());
}
@Test
public void testCmsReceiveBinaryNoNakedParen() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("PROFILE EXEC A");
config.setTransferMode(FTConfig.TransferMode.BINARY);
// In CMS, binary receive has no options, so NO open parenthesis '(' should be generated!
assertEquals("IND$FILE GET PROFILE EXEC A", config.buildCommand());
}
@Test
public void testCmsReceiveAscii() {
FTConfig config = new FTConfig();
config.setHostType(FTConfig.HostType.CMS);
config.setDirection(FTConfig.Direction.RECEIVE);
config.setHostFilename("TEST DATA A");
config.setTransferMode(FTConfig.TransferMode.ASCII);
config.setCrAction(FTConfig.CrAction.REMOVE);
assertEquals("IND$FILE GET TEST DATA A (ASCII CRLF", config.buildCommand());
}
@Test
public void testCmsSendWithRecfmAndLrecl() {
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("V");
config.setLrecl("132");
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM V LRECL 132", config.buildCommand());
}
}
@@ -0,0 +1,558 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
public class GocaDecoderTest {
@Test
public void testBasicLineAndColorOrders() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
assertFalse(plane.hasContent());
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Color to Red (GOCA Color 2) - 2 bytes: order, color
out.write(GocaConstants.G_GSCOL);
out.write(0x02); // Red
// Line from (1000, 1000) to (2000, 2000) - long order: order, len, x1, y1, x2, y2
out.write(GocaConstants.G_GLINE);
out.write(0x08); // 8 bytes: (x1, y1, x2, y2)
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
}
@Test
public void testAreaAndPatternFill() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Pattern to Solid - 2 bytes: order, pattern
out.write(GocaConstants.G_GSPT);
out.write(GocaConstants.PT_SOLID);
// Begin Area - 2 bytes: order, flags
out.write(GocaConstants.G_GBAR);
out.write(0x00);
// Polyline forming a triangle: (500, 500) -> (1500, 500) -> (1000, 1500) -> (500, 500)
out.write(GocaConstants.G_GLINE);
out.write(0x10); // 4 points * 4 bytes = 16 bytes
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
// End Area - 1 byte
out.write(GocaConstants.G_GEAR);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
}
@Test
public void testMarkerAndVectorText() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Marker Type to Circle - 2 bytes: order, type
out.write(GocaConstants.G_GSMT);
out.write(GocaConstants.MK_CIRCLE);
// Draw Marker at (2000, 2000)
out.write(GocaConstants.G_GMRK);
out.write(0x04);
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
// Draw Vector Stroked Character String: "IBM" (EBCDIC: 0xC9, 0xC2, 0xD4)
out.write(GocaConstants.G_GCHST);
out.write(0x07); // 4 bytes pos + 3 bytes chars
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
out.write(0xC9); out.write(0xC2); out.write(0xD4);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
}
@Test
public void testProcedureOrdersAndErase() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Draw something first
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GLINE);
out.write(0x08);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x64);
out.write(0x00); out.write(0x64);
byte[] drawStream = out.toByteArray();
decoder.decodeStream(drawStream, 0, drawStream.length);
assertTrue(plane.hasContent());
// Process procedure order 0x0A (Erase presentation space)
byte[] procOrders = new byte[] { (byte) GocaConstants.P_ERASE };
decoder.processProcedureOrders(procOrders, 0, procOrders.length);
assertFalse(plane.hasContent());
}
@Test
public void testSegmentAndRelativeLineOrders() {
GraphicsPlane plane = new GraphicsPlane(720, 688);
plane.setScreenDimensions(80, 43);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 0x70: Begin Segment (14 bytes total)
out.write(GocaConstants.G_BEGSEGM);
out.write(0x0C);
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x01); // Seg ID 1
out.write(0x74); out.write(0x70);
out.write(0x00); out.write(0x1E); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
// 0x3E: End Prologue
out.write(GocaConstants.G_ENDPROLOGUE);
out.write(0x00);
// 0x0A: Color (7 = White)
out.write(GocaConstants.G_GSCOL);
out.write(0x07);
// 0xE1: Relative Line (Absolute Start (300, 200), deltas: (+10, -5), (-10, +5))
out.write(GocaConstants.G_GRLINE);
out.write(0x08); // 4 bytes start coord + 4 bytes (2 deltas) = 8 bytes
out.write(0x01); out.write(0x2C); // Start X = 300
out.write(0x00); out.write(0xC8); // Start Y = 200
out.write(0x0A); out.write(0xFB); // dx = +10, dy = -5
out.write(0xF6); out.write(0x05); // dx = -10, dy = +5
// 0x71: End Segment
out.write(GocaConstants.G_ENDSEGM);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
}
@Test
public void testPartialOrderBufferingAcrossStreams() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// First packet sends G_GLINE order code and half of its coordinates
ByteArrayOutputStream chunk1 = new ByteArrayOutputStream();
chunk1.write(GocaConstants.G_GLINE);
chunk1.write(0x08); // 8 bytes: 2 points
chunk1.write(0x00); chunk1.write(0x64); // x1 = 100
chunk1.write(0x00); chunk1.write(0x64); // y1 = 100
byte[] b1 = chunk1.toByteArray();
decoder.decodeStream(b1, 0, b1.length);
// Order is not complete yet, so no line drawn yet
assertFalse(plane.hasContent());
// Second packet sends the rest of the order: x2 = 200, y2 = 200
ByteArrayOutputStream chunk2 = new ByteArrayOutputStream();
chunk2.write(0x00); chunk2.write(0xC8); // x2 = 200
chunk2.write(0x00); chunk2.write(0xC8); // y2 = 200
byte[] b2 = chunk2.toByteArray();
decoder.decodeStream(b2, 0, b2.length);
// Now the combined order executes!
assertTrue(plane.hasContent());
}
@Test
public void testSetCurrentPositionAndRelativeLine() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 0x21: Set Current Position to (50, 50)
out.write(GocaConstants.G_GSCP);
out.write(0x04); // len = 4
out.write(0x00); out.write(0x32); // x = 50
out.write(0x00); out.write(0x32); // y = 50
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
out.write(GocaConstants.G_GCRLIN);
out.write(0x04); // len = 4 (2 deltas * 2 bytes)
out.write(0x0A); out.write(0x14); // dx = +10, dy = +20
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
assertEquals(55, decoder.getCurX());
assertEquals(60, decoder.getCurY());
}
@Test
public void test3179GCoordinateMapping() {
GraphicsPlane plane = new GraphicsPlane(720, 516);
plane.setScreenDimensions(80, 43);
// Screen center (0, 0) should map to canvas center (360, 257)
assertEquals(360, plane.mapX(0));
assertEquals(257, plane.mapY(0));
// Left edge (-360) should map to 0
assertEquals(0, plane.mapX(-360));
// Right edge (+359) should map to 719
assertEquals(719, plane.mapX(359));
// Top edge (+257) should map to 0
assertEquals(0, plane.mapY(257));
// Bottom edge (-258) should map to 515
assertEquals(515, plane.mapY(-258));
// Bidirectional roundtrip mapping must be exact
assertEquals(0, plane.unmapX(plane.mapX(0)));
assertEquals(0, plane.unmapY(plane.mapY(0)));
assertEquals(100, plane.unmapY(plane.mapY(100)));
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
}
@Test
public void testImageOrdersGbimgGimdGeimg() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// G_GBIMG (0xD1): Begin Image: order, len=8, x=100, y=100, w=16, h=2
out.write(GocaConstants.G_GBIMG);
out.write(0x08);
out.write(0x00); out.write(0x64); // x = 100
out.write(0x00); out.write(0x64); // y = 100
out.write(0x00); out.write(0x10); // w = 16
out.write(0x00); out.write(0x02); // h = 2
// G_GIMD (0x92): Image Data: order, len=4, 4 bytes of bitmap (16x2 pixels = 32 bits = 4 bytes)
out.write(GocaConstants.G_GIMD);
out.write(0x04);
out.write(0xFF); out.write(0x00);
out.write(0xAA); out.write(0x55);
// G_GEIMG (0x91): End Image
out.write(GocaConstants.G_GEIMG);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
}
@Test
public void testDirectObjectControlSf24() {
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
haus.nightmare.lib3270j.datastream.DataStreamProcessor dsp = new haus.nightmare.lib3270j.datastream.DataStreamProcessor(
sb, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
// First draw something on graphics plane
dsp.getGraphicsPlane().setPixel(10, 10, 0xFFFFFFFF);
assertTrue(dsp.getGraphicsPlane().hasContent());
// Send WSF with SF_OBJCNTL (0x24) containing P_ERASE (0x0A)
byte[] wsf = new byte[] {
(byte) haus.nightmare.lib3270j.protocol.DS3270Constants.CMD_WSF,
0x00, 0x04, // Field length = 4
(byte) haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL, // 0x24
(byte) haus.nightmare.lib3270j.graphics.GocaConstants.P_ERASE // 0x0A
};
dsp.processRecord(wsf, 0, wsf.length, false);
assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE");
}
@Test
public void testGcallAndGscsWithLengthBytes() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Construct Segment 2: Begin Segment (id=2), Set Color, draw line, End Segment
ByteArrayOutputStream s2 = new ByteArrayOutputStream();
s2.write(GocaConstants.G_BEGSEGM);
s2.write(0x0C);
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x02); // id=2
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
s2.write(GocaConstants.G_GSCS); s2.write(0x01); s2.write(0x40); // charSet = 0x40
s2.write(GocaConstants.G_GSCOL); s2.write(0x02); // Color = Red
s2.write(GocaConstants.G_GLINE); s2.write(0x08);
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
s2.write(0x00); s2.write(0x32); s2.write(0x00); s2.write(0x32);
s2.write(GocaConstants.G_ENDSEGM); s2.write(0x00);
byte[] s2Bytes = s2.toByteArray();
decoder.decodeStream(s2Bytes, 0, s2Bytes.length);
// Segment 1 calls Segment 2 via GCALL (0x2A len=4 segId=2)
ByteArrayOutputStream s1 = new ByteArrayOutputStream();
s1.write(GocaConstants.G_BEGSEGM);
s1.write(0x0C);
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x01); // id=1
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
// GCALL (0x2A len=4 segId=2)
s1.write(GocaConstants.G_GCALL); s1.write(0x04);
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x02);
s1.write(GocaConstants.G_ENDSEGM); s1.write(0x00);
byte[] s1Bytes = s1.toByteArray();
decoder.decodeStream(s1Bytes, 0, s1Bytes.length);
assertEquals(0x40, decoder.getCharSet());
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
}
@Test
public void testAntialiasedLineRendering() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
int red = 0xFFFF0000;
// Draw a diagonal line with Xiaolin Wu anti-aliasing
plane.drawLine(10.0, 10.0, 50.0, 30.0, red, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
int pixel = buffer[y * 100 + x];
if (pixel != 0) {
nonZeroPixels++;
int alpha = (pixel >>> 24) & 0xFF;
int r = (pixel >>> 16) & 0xFF;
assertEquals(255, r, "Red channel must be preserved");
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
}
assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line");
assertTrue(hasIntermediateAlpha, "Expected Xiaolin Wu anti-aliasing to produce fractional alpha coverage");
}
@Test
public void testSubpixelArcAndAlphaBlending() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
int green = 0xFF00FF00;
plane.drawArc(50.0, 50.0, 30.0, 30.0, 0.0, 360.0, green, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int p : buffer) {
if (p != 0) {
nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
assertTrue(nonZeroPixels > 50, "Expected arc pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased arc edges with smooth alpha");
}
@Test
public void testVectorTextAntialiasedDrawing() {
GraphicsPlane plane = new GraphicsPlane(200, 100);
plane.clear();
int yellow = 0xFFFFFF00;
plane.drawVectorText(10.0, 10.0, "8% 1985 TAX", yellow, 12.0, 20.0, GocaConstants.CD_LR, 0.0);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int p : buffer) {
if (p != 0) {
nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased vector text strokes with fractional alpha");
}
@Test
public void testTextRendererPrecisionSwitching() {
GraphicsPlane plane = new GraphicsPlane(200, 100);
GocaDecoder decoder = new GocaDecoder(plane);
final boolean[] textRendererCalled = new boolean[1];
plane.setTextRenderer((p, x, y, text, color, cw, ch, dir, angle) -> {
textRendererCalled[0] = true;
});
// Test String Precision (Default): GCHST (0xC3) with "TAX"
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GCHST);
out.write(0x07); // length = 7 (4 bytes pos + 3 bytes text)
out.write(0x00); out.write(0x00); // x = 0
out.write(0x00); out.write(0x00); // y = 0
out.write(0xE3); // 'T' in EBCDIC
out.write(0xC1); // 'A' in EBCDIC
out.write(0xE7); // 'X' in EBCDIC
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(textRendererCalled[0], "Expected pluggable TextRenderer to be called for String precision");
// Now set Stroke precision (G_GSCC with 3):
textRendererCalled[0] = false;
ByteArrayOutputStream outStroke = new ByteArrayOutputStream();
outStroke.write(GocaConstants.G_GSCC);
outStroke.write(0x01);
outStroke.write(GocaConstants.CP_STROKE); // 3
outStroke.write(GocaConstants.G_GCHST);
outStroke.write(0x07);
outStroke.write(0x00); outStroke.write(0x00);
outStroke.write(0x00); outStroke.write(0x00);
outStroke.write(0xE3); outStroke.write(0xC1); outStroke.write(0xE7);
byte[] strokeStream = outStroke.toByteArray();
decoder.decodeStream(strokeStream, 0, strokeStream.length);
assertFalse(textRendererCalled[0], "Expected drawVectorText (not textRenderer) when precision is CP_STROKE");
assertTrue(plane.hasContent());
}
@Test
public void testThickLineRendering() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
plane.drawLine(10.0, 10.0, 50.0, 50.0, 0xFF00FF00, GocaConstants.LT_SOLID, GocaConstants.LW_THICK);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int nonZero = 0;
for (int p : buffer) {
if (p != 0) nonZero++;
}
assertTrue(nonZero > 60, "Expected thick line to occupy more pixels than standard 1px line");
}
@Test
public void testShortFormAttributeOrderParsing() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form with 0x80 flag (boundary=true, fill=false),
// followed immediately by GLINE (0xC1 len=12 for 3 points) and GEAR (0x60)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GBAR);
out.write(0x80); // Short 2-byte form with 0x80 flag! Must NOT be treated as length=128!
out.write(GocaConstants.G_GLINE);
out.write(0x0C); // 3 points = 12 bytes
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x32);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x32);
out.write(0x00); out.write(0x32);
out.write(GocaConstants.G_GEAR);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected GLINE and GEAR inside GBAR short-form 0x80 to be decoded properly");
}
@Test
public void testMultiPolygonAreaFilling() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
// Polygon 1 (e.g. Letter 'T' bar): (10,10) to (30,10) to (30,20) to (10,20) to (10,10)
// Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20)
// GEAR (0x60)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GBAR);
out.write(0x80);
// Polygon 1
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points = 20 bytes
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(20);
out.write(0x00); out.write(10); out.write(0x00); out.write(20);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
// Polygon 2 (disconnected start -> triggers new subpath)
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points = 20 bytes
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
out.write(0x00); out.write(22); out.write(0x00); out.write(20);
out.write(0x00); out.write(22); out.write(0x00); out.write(40);
out.write(0x00); out.write(18); out.write(0x00); out.write(40);
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
out.write(GocaConstants.G_GEAR);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int nonZero = 0;
for (int p : buffer) {
if (p != 0) nonZero++;
}
assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels");
}
}
@@ -0,0 +1,57 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import static org.junit.jupiter.api.Assertions.*;
public class GraphicsModeTest {
@Test
public void testDefaultsAndParsing() {
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(null));
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(""));
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("NONE"));
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("off"));
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("false"));
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("ps"));
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("programmed_symbols"));
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("apl"));
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector"));
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("goca"));
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector_graphics"));
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("both"));
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("all"));
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("true"));
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("on"));
}
@Test
public void testFlags() {
assertFalse(GraphicsMode.NONE.isProgrammedSymbolsEnabled());
assertFalse(GraphicsMode.NONE.isVectorGraphicsEnabled());
assertTrue(GraphicsMode.PROGRAMMED_SYMBOLS.isProgrammedSymbolsEnabled());
assertFalse(GraphicsMode.PROGRAMMED_SYMBOLS.isVectorGraphicsEnabled());
assertFalse(GraphicsMode.VECTOR_GRAPHICS.isProgrammedSymbolsEnabled());
assertTrue(GraphicsMode.VECTOR_GRAPHICS.isVectorGraphicsEnabled());
assertTrue(GraphicsMode.BOTH.isProgrammedSymbolsEnabled());
assertTrue(GraphicsMode.BOTH.isVectorGraphicsEnabled());
}
@Test
public void testConnectionConfigDefault() {
ConnectionConfig config = new ConnectionConfig();
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
config.setGraphicsMode(GraphicsMode.NONE);
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
config.setGraphicsMode(null);
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
}
}
@@ -0,0 +1,167 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ProgramSymbolManagerTest {
@Test
public void testSetAllocationAndLookup() {
ProgramSymbolManager manager = new ProgramSymbolManager();
// Loadable set can be queried after loadps or clearAll
assertNull(manager.getSymbolSet(0x40));
assertNull(manager.getSymbol(0x40, 0x41));
manager.clearAll();
assertNull(manager.getSymbolSet(0x40));
}
@Test
public void testSinglePlaneLoadPsFormat1() {
ProgramSymbolManager manager = new ProgramSymbolManager();
// Build Format 1 Load PS payload for LCID 0x40, code point 0x41 ('A')
// Format 1 header:
// byte 0: flags (0x01: format 1, single plane)
// byte 1: LCID (0x40)
// byte 2: Start code point (0x41)
// byte 3: RWS (0x02: loadable slot 2)
// followed by 18 bytes per symbol for 9x16 cell:
// Byte 0-1: Col 0 for rows 0..15 (0x80 = bit for row 0 col 0)
// Bytes 2-17: Cols 1..8 for rows 0..15 (0xFF = bits for row 0 cols 1..8)
byte[] payload = new byte[4 + 18];
payload[0] = 0x01; // Format 1
payload[1] = 0x40; // LCID 0x40
payload[2] = 0x41; // Code point 0x41
payload[3] = 0x02; // RWS 2
payload[4] = (byte) 0x80; // Row 0 Col 0 bit
payload[5] = (byte) 0x00; // Rows 8..15 Col 0
for (int r = 0; r < 16; r++) {
payload[6 + r] = (byte) 0xFF; // Cols 1..8 on all rows
}
manager.loadps(payload);
ProgramSymbolSet set = manager.getSymbolSet(0x40);
assertNotNull(set);
assertEquals(0x40, set.getLcid());
assertFalse(set.isTriplePlane());
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
assertNotNull(slot);
assertEquals(9, slot.getWidth());
assertEquals(12, slot.getHeight());
assertFalse(slot.isTriplePlane());
// Verify pixel data (Row 0 Col 0 is 1)
byte[] pixels = slot.getPixelData();
assertNotNull(pixels);
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
// Test rendering into 32-bit ARGB pixels
int fg = 0xFF00FF00; // Green
int bg = 0xFF000000; // Black
int[] rgb = slot.getRgbPixels(fg, bg);
assertNotNull(rgb);
assertEquals(9 * 12, rgb.length);
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
}
@Test
public void testExplicitCellDimensions() {
ProgramSymbolManager manager = new ProgramSymbolManager();
manager.setDefaultCellDimensions(9, 16);
byte[] payload = new byte[4 + 18];
payload[0] = 0x01; // Format 1
payload[1] = 0x40; // LCID 0x40
payload[2] = 0x41; // Code point 0x41
payload[3] = 0x02; // RWS 2
payload[4] = (byte) 0x80;
payload[5] = (byte) 0x00;
for (int r = 0; r < 16; r++) {
payload[6 + r] = (byte) 0xFF;
}
manager.loadps(payload);
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
assertNotNull(slot);
assertEquals(9, slot.getWidth());
assertEquals(16, slot.getHeight());
}
@Test
public void testTriplePlaneMultiColorComposite() {
ProgramSymbolManager manager = new ProgramSymbolManager();
// Build Format 1 Load PS payload for LCID 0x42, triple plane
// flags = 0x01 (Format 1)
// RWS = 4 (Triple Plane)
byte[] payload = new byte[4 + 18];
payload[0] = 0x01; // Format 1
payload[1] = 0x42; // LCID 0x42
payload[2] = 0x45; // Code point 0x45
payload[3] = 0x04; // RWS 4 (Triple Plane)
// Set pixel at row 0 col 0
payload[4] = (byte) 0x80; // Row 0 Col 0
payload[5] = (byte) 0x00;
for (int r = 0; r < 16; r++) {
payload[6 + r] = (byte) 0x00;
}
manager.loadps(payload);
ProgramSymbolSet set = manager.getSymbolSet(0x42);
assertNotNull(set);
assertTrue(set.isTriplePlane());
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, 0x45);
assertNotNull(slot);
assertTrue(slot.isTriplePlane());
// When loaded with colorPlane=0 (default), all 3 planes are set (val 7 = white composite)
byte[] pixels = slot.getPixelData();
assertEquals(7, pixels[0] & 0xFF);
// Render ARGB pixels
int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0xFF000000);
assertNotNull(rgb);
// Pixel (0,0) should be white composite (0xFFFFFFFF)
int pixelArgb = rgb[0];
int r = (pixelArgb >> 16) & 0xFF;
int g = (pixelArgb >> 8) & 0xFF;
int b = pixelArgb & 0xFF;
assertEquals(255, r);
assertEquals(255, g);
assertEquals(255, b);
}
@Test
public void testFormat3Bitstream() {
ProgramSymbolManager manager = new ProgramSymbolManager();
// Format 3 payload
// byte 0: 0x03 (Format 3)
// byte 1: LCID (0x44)
// byte 2: code point (0x43)
// byte 3: RWS (2)
// followed by (9 * 16 + 7) / 8 = 18 bytes
byte[] payload = new byte[4 + 18];
payload[0] = 0x03; // Format 3
payload[1] = 0x44; // LCID 0x44
payload[2] = 0x43; // Code point 0x43
payload[3] = 0x02; // RWS 2
payload[4] = (byte) 0x80; // First pixel is set
manager.loadps(payload);
ProgramSymbolSet set = manager.getSymbolSet(0x44);
assertNotNull(set);
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x44, 0x43);
assertNotNull(slot);
assertEquals(1, slot.getPixelData()[0]);
}
}
@@ -0,0 +1,203 @@
package haus.nightmare.lib3270j.input;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class InputProcessorTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
@Test
public void testKybdPrimeOnUnformattedScreen() {
InputProcessor input = new InputProcessor(screen, translator, null);
int cap = input.kybdPrime();
assertEquals(screen.getRows() * screen.getCols(), cap);
assertEquals(0, screen.getCursorAddress());
}
@Test
public void testKybdPrimeOnFormattedScreen() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field at 0 (length = 19)
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT)); // Protected field at 20
int cap = input.kybdPrime();
assertEquals(19, cap);
assertEquals(1, screen.getCursorAddress());
}
@Test
public void testEraseInput() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
input.typeCharacter('A');
input.typeCharacter('B');
assertEquals('A', screen.getCell(1).ucs4);
assertEquals('B', screen.getCell(2).ucs4);
assertEquals('A', screen.getDisplayCell(1).ucs4);
assertEquals('B', screen.getDisplayCell(2).ucs4);
input.eraseInput();
assertEquals(0, screen.getCell(1).ec);
assertEquals(0, screen.getCell(2).ec);
assertEquals(0, screen.getDisplayCell(1).ec);
assertEquals(0, screen.getDisplayCell(2).ec);
}
@Test
public void testTypeCharacterUpdatesDisplaySnapshot() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field starting at 1
screen.setCursorAddress(1);
screen.updateDisplaySnapshot(); // Initial snapshot
// Type "logon"
input.typeCharacter('l');
input.typeCharacter('o');
input.typeCharacter('g');
input.typeCharacter('o');
input.typeCharacter('n');
// Verify underlying buffer
assertEquals('l', screen.getCell(1).ucs4);
assertEquals('o', screen.getCell(2).ucs4);
assertEquals('g', screen.getCell(3).ucs4);
assertEquals('o', screen.getCell(4).ucs4);
assertEquals('n', screen.getCell(5).ucs4);
// Verify display snapshot (what TerminalPanel renders)
assertEquals('l', screen.getDisplayCell(1).ucs4);
assertEquals('o', screen.getDisplayCell(2).ucs4);
assertEquals('g', screen.getDisplayCell(3).ucs4);
assertEquals('o', screen.getDisplayCell(4).ucs4);
assertEquals('n', screen.getDisplayCell(5).ucs4);
assertEquals(6, screen.getDisplayCursorAddress());
assertEquals(6, screen.getCursorAddress());
}
@Test
public void testCursorMovementUpdatesDisplayCursorAddress() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCursorAddress(10);
assertEquals(10, screen.getDisplayCursorAddress());
input.cursorRight();
assertEquals(11, screen.getDisplayCursorAddress());
input.cursorLeft();
assertEquals(10, screen.getDisplayCursorAddress());
screen.setCursorAddress(45);
assertEquals(45, screen.getDisplayCursorAddress());
}
@Test
public void testDeleteAndEraseEofUpdateDisplaySnapshot() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE);
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(1);
input.typeCharacter('A');
input.typeCharacter('B');
input.typeCharacter('C');
assertEquals('A', screen.getDisplayCell(1).ucs4);
assertEquals('B', screen.getDisplayCell(2).ucs4);
assertEquals('C', screen.getDisplayCell(3).ucs4);
// Backspace over 'C'
input.backspace();
assertEquals(3, screen.getCursorAddress());
assertEquals(3, screen.getDisplayCursorAddress());
assertEquals(0, screen.getDisplayCell(3).ucs4);
// Erase EOF from position 2 ('B')
screen.setCursorAddress(2);
input.eraseEof();
assertEquals(0, screen.getDisplayCell(2).ucs4);
assertEquals('A', screen.getDisplayCell(1).ucs4);
}
@Test
public void testGraphicInputBuilderStructure() {
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
assertEquals(56, sf.length);
assertEquals(0x00, sf[0]);
assertEquals(0x34, sf[1]); // Length = 52
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
// Coordinates
int x = (sf[24] << 8) | (sf[25] & 0xFF);
int y = (sf[26] << 8) | (sf[27] & 0xFF);
assertEquals(100, (short) x);
assertEquals(-50, (short) y);
// Keyboard AID
assertEquals(0x07, sf[31]);
assertEquals(0x07, sf[33]);
assertEquals((byte) 0xFF, sf[34]);
assertEquals((byte) AID_ENTER, sf[35]);
}
@Test
public void testSendAidSuppressesNullsInModifiedField() {
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
public void sendAid(int aidCode) {
// Call super logic by hooking via fake FSM or overriding sendAid
super.sendAid(aidCode);
}
};
// Setup screen: field attribute at 0 (unprotected, modified)
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
// Pos 1 = null (0x00)
screen.getCell(1).ec = 0x00;
// Pos 2 = 'Q' (0xD8)
screen.getCell(2).ec = (byte) 0xD8;
// Pos 3 = 'U' (0xE4)
screen.getCell(3).ec = (byte) 0xE4;
// Pos 4 = 'I' (0xC9)
screen.getCell(4).ec = (byte) 0xC9;
// Pos 5 = 'T' (0xE3)
screen.getCell(5).ec = (byte) 0xE3;
// Next field at 10 (protected)
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
// Use custom callback or inspection
// Let's verify DataStreamProcessor ReadModified behavior with the same buffer
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
dsp.setInputProcessor(input);
dsp.setOutputSender(sent::set);
byte[] rmRecord = new byte[] { (byte) CMD_RM };
dsp.processRecord(rmRecord, 0, rmRecord.length, true);
byte[] result = sent.get();
assertNotNull(result);
// Result format: AID (1 byte), Cursor (2 bytes), SBA (1 byte), Addr (2 bytes), Data ('Q', 'U', 'I', 'T' - 4 bytes)
assertEquals(10, result.length, "Result should be AID(1) + Cursor(2) + SBA(1) + Addr(2) + Data(4)");
assertEquals((byte) ORDER_SBA, result[3]);
assertEquals((byte) 0xD8, result[6]); // 'Q'
assertEquals((byte) 0xE4, result[7]); // 'U'
assertEquals((byte) 0xC9, result[8]); // 'I'
assertEquals((byte) 0xE3, result[9]); // 'T'
}
}
@@ -0,0 +1,51 @@
package haus.nightmare.lib3270j.protocol;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class DS3270ConstantsTest {
@Test
public void test12BitAddressRoundTrip() {
int rows = 24;
int cols = 80;
int maxAddr = rows * cols; // 1920
for (int addr = 0; addr < maxAddr; addr += 17) {
byte[] encoded = encodeAddress(addr, rows, cols);
assertEquals(2, encoded.length);
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
assertEquals(addr, decoded, "Address " + addr + " failed round trip");
}
}
@Test
public void test14BitAddressRoundTrip() {
int rows = 60;
int cols = 80; // 4800 (> 4096, triggers 14-bit format)
int maxAddr = rows * cols;
for (int addr = 0; addr < maxAddr; addr += 123) {
byte[] encoded = encodeAddress(addr, rows, cols);
assertEquals(2, encoded.length);
int decoded = decodeAddress(encoded[0] & 0xFF, encoded[1] & 0xFF);
assertEquals(addr, decoded, "14-bit address " + addr + " failed round trip");
}
}
@Test
public void testFieldAttributePredicates() {
int protectedHighMod = FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL | FA_MODIFY;
assertTrue(faIsProtected(protectedHighMod));
assertTrue(faIsHigh(protectedHighMod));
assertTrue(faIsModified(protectedHighMod));
assertFalse(faIsNumeric(protectedHighMod));
assertFalse(faIsSkip(protectedHighMod));
int skipField = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC;
assertTrue(faIsSkip(skipField));
assertTrue(faIsProtected(skipField));
assertTrue(faIsNumeric(skipField));
}
}
@@ -0,0 +1,64 @@
package haus.nightmare.lib3270j.screen;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferTest {
private final EbcdicTranslator translator = new EbcdicTranslator();
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
@Test
public void testScreenInitialization() {
assertEquals(24, screen.getRows());
assertEquals(80, screen.getCols());
assertEquals(43, screen.getMaxRows());
assertEquals(80, screen.getMaxCols());
assertFalse(screen.isFormatted());
}
@Test
public void testScreenEraseAlternate() {
screen.erase(true);
assertTrue(screen.isScreenAlt());
assertEquals(43, screen.getRows());
assertEquals(80, screen.getCols());
screen.erase(false);
assertFalse(screen.isScreenAlt());
assertEquals(24, screen.getRows());
assertEquals(80, screen.getCols());
}
@Test
public void testFieldNavigationAndFindAttribute() {
screen.erase(false);
// Place field attribute at position 0 (unprotected)
screen.setCellFA(0, (byte) FA_PRINTABLE);
// Place field attribute at position 20 (protected)
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT));
assertTrue(screen.isFormatted());
assertEquals(0, screen.findFieldAttribute(5));
assertEquals(20, screen.findFieldAttribute(25));
// findNextUnprotected
int nextUnprot = screen.findNextUnprotected(0);
assertEquals(1, nextUnprot);
}
@Test
public void testGraphicEscapeTranslation() {
screen.erase(false);
// Set cell at position 10 to horizontal line (0xA2) with CS_GE (0x04)
ExtendedAttribute ea = screen.getCell(10);
ea.ec = (byte) 0xA2;
ea.cs = (byte) CS_GE;
screen.translateToUnicode();
assertEquals('\u2500', ea.ucs4); // '─'
}
}
@@ -0,0 +1,297 @@
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.protocol.TelnetConstants;
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.*;
public class TelnetFSMTest {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private TelnetFSM fsm;
private MockConnection connection;
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() {
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
for (int b : bytes) {
fsm.feedByte(b & 0xFF);
}
}
@Test
public void testHostPrefixParsingPlainTn3270() {
ConnectionConfig c1 = ConnectionConfig.parseHostString("P:mainframe.example.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c1.isTn3270eEnabled(), "P: prefix should disable TN3270E");
assertFalse(c1.isUseTls());
assertEquals("mainframe.example.com", c1.getHost());
ConnectionConfig c2 = ConnectionConfig.parseHostString("plain:zos.net:2323", 23, TerminalModel.IBM_3279_4);
assertFalse(c2.isTn3270eEnabled());
assertEquals("zos.net", c2.getHost());
assertEquals(2323, c2.getPort());
ConnectionConfig c3 = ConnectionConfig.parseHostString("L:P:secure.mainframe.com:992", 0, TerminalModel.IBM_3279_4);
assertTrue(c3.isUseTls(), "L: should enable TLS");
assertFalse(c3.isTn3270eEnabled(), "P: should disable TN3270E");
assertEquals("secure.mainframe.com", c3.getHost());
assertEquals(992, c3.getPort());
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.ibm.com", c4.getHost());
}
@Test
public void testPlainTn3270NegotiationRejectsTn3270E() {
config.setTn3270eEnabled(false);
fsm.onConnected();
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
// Host offers DO TN3270E -> client must reply WONT TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
assertFalse(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host offers WILL TN3270E -> client must reply DONT TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
assertFalse(fsm.getHisOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host negotiates standard 3270 options: DO BINARY, WILL BINARY, DO EOR, WILL EOR, DO TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// Client should have transitioned to CONNECTED_3270
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
// Host asks for TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
// Verify sent packets contain WONT TN3270E, DONT TN3270E, and TTYPE IS
boolean foundWontTn3270e = false;
boolean foundTtypeIs = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.WONT && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E) {
foundWontTn3270e = true;
}
if (pkt.length >= 4 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TTYPE) {
foundTtypeIs = true;
}
}
assertTrue(foundWontTn3270e, "Expected WONT TN3270E response");
assertTrue(foundTtypeIs, "Expected TTYPE IS response");
}
@Test
public void testTn3270eFunctionsNegotiationWithoutBindImageTransitionsToConnected() {
config.setTn3270eEnabled(true);
fsm.onConnected();
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
// Host offers DO TN3270E -> client accepts with WILL TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
assertTrue(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host responds with DEVICE-TYPE IS IBM-3279-4-E CONNECT LU01
byte[] devTypeIs = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, // OP_DEVICE_TYPE, OP_IS
'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
0x01, // OP_CONNECT
'L', 'U', '0', '1',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
// Host responds with FUNCTIONS REQUEST <empty> (e.g. pytn3270 / RFC 2355 server)
byte[] funcsReq = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x03, 0x07, // OP_FUNCTIONS, OP_REQUEST
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Per RFC 2355: Client must reply with FUNCTIONS IS, and without BIND-IMAGE, session is bound immediately
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(),
"Expected CONNECTED_TN3270E when BIND-IMAGE is not negotiated");
// Verify that client sent SB TN3270E FUNCTIONS IS SE
boolean foundFunctionsIs = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length >= 5 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E &&
(pkt[3] & 0xFF) == 0x03 && (pkt[4] & 0xFF) == 0x04) {
foundFunctionsIs = true;
}
}
assertTrue(foundFunctionsIs, "Expected FUNCTIONS IS reply from client upon receiving FUNCTIONS REQUEST");
}
@Test
public void testPlainDataStreamFallbackInTn3270eMode() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E with empty functions
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
byte[] devTypeIs = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x03, 0x07, (byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// Now host sends raw 3270 data stream without 5-byte TN3270E header (e.g. EraseWrite 0xF5)
// 0xF5 = EraseWrite, 0xC3 = WCC, 0x11 = SBA, 0x40 0x40 = Pos 0, "HELLO"
EbcdicTranslator trans = new EbcdicTranslator();
String msg = "HELLO";
byte[] rawStream = new byte[3 + 2 + msg.length() + 2];
rawStream[0] = (byte) 0xF5; // EraseWrite
rawStream[1] = (byte) 0xC3; // WCC
rawStream[2] = 0x11; // SBA
rawStream[3] = 0x40; rawStream[4] = 0x40; // addr 0
for (int i = 0; i < msg.length(); i++) {
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
}
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
// State must have automatically switched to CONNECTED_3270 and screen updated
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
assertEquals('H', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('E', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4)));
}
@Test
public void testTelnetTimingMarkOption6() {
fsm.onConnected();
connection.sentData.clear();
// Host sends DO TELOPT_TM (DO 6) -> Client must reply WILL TELOPT_TM (WILL 6) per RFC 860
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TM);
boolean foundWillTm = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.WILL && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) {
foundWillTm = true;
}
}
assertTrue(foundWillTm, "Expected IAC WILL TELOPT_TM reply upon receiving IAC DO TELOPT_TM");
// Host sends WILL TELOPT_TM (WILL 6) -> Client must reply DO TELOPT_TM (DO 6)
connection.sentData.clear();
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TM);
boolean foundDoTm = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.DO && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) {
foundDoTm = true;
}
}
assertTrue(foundDoTm, "Expected IAC DO TELOPT_TM reply upon receiving IAC WILL TELOPT_TM");
}
@Test
public void testIACAYT() {
fsm.onConnected();
connection.sentData.clear();
// Host sends IAC AYT
feedBytes(TelnetConstants.IAC, TelnetConstants.AYT);
boolean foundNop = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 2 && (pkt[0] & 0xFF) == TelnetConstants.IAC && (pkt[1] & 0xFF) == TelnetConstants.NOP) {
foundNop = true;
}
}
assertTrue(foundNop, "Expected IAC NOP acknowledgment for IAC AYT");
}
@Test
public void testTTypeCycling() {
fsm.onConnected();
connection.sentData.clear();
// Enable TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// First SEND request -> Expect configured type IBM-3279-4-E
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
byte[] lastPkt = connection.sentData.get(connection.sentData.size() - 1);
String resp1 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3279-4-E", resp1);
// Second SEND request -> Expect fallback IBM-3279-4
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
lastPkt = connection.sentData.get(connection.sentData.size() - 1);
String resp2 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3279-4", resp2);
// Third SEND request -> Expect monochrome fallback IBM-3278-4-E
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
lastPkt = connection.sentData.get(connection.sentData.size() - 1);
String resp3 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3278-4-E", resp3);
}
}
@@ -0,0 +1,108 @@
package haus.nightmare.lib3270j.tls;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import javax.net.ssl.SSLContext;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
public class TlsConfigTest {
@Test
public void testConnectionConfigDefaults() {
ConnectionConfig config = new ConnectionConfig("localhost", 23);
assertFalse(config.isUseTls());
assertTrue(config.isTlsVerifyCert());
assertEquals("TLS", config.getSslProtocol());
assertNull(config.getCertificateVerifier());
}
@Test
public void testParseHostStringTlsPrefixes() {
// L: prefix
ConnectionConfig c1 = ConnectionConfig.parseHostString("L:mainframe.example.com", 0, TerminalModel.IBM_3279_4);
assertTrue(c1.isUseTls());
assertEquals("mainframe.example.com", c1.getHost());
assertEquals(992, c1.getPort());
// ssl: prefix with explicit port
ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_4);
assertTrue(c2.isUseTls());
assertEquals("zos.local", c2.getHost());
assertEquals(2323, c2.getPort());
// y: prefix
ConnectionConfig c3 = ConnectionConfig.parseHostString("y:vm370.net:992", 23, TerminalModel.IBM_3279_4);
assertTrue(c3.isUseTls());
assertEquals("vm370.net", c3.getHost());
assertEquals(992, c3.getPort());
// Plain host
ConnectionConfig c4 = ConnectionConfig.parseHostString("plain.host.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c4.isUseTls());
assertEquals("plain.host.com", c4.getHost());
assertEquals(23, c4.getPort());
// IPv6 host with brackets
ConnectionConfig c5 = ConnectionConfig.parseHostString("L:[2001:db8::1]:992", 0, TerminalModel.IBM_3279_4);
assertTrue(c5.isUseTls());
assertEquals("2001:db8::1", c5.getHost());
assertEquals(992, c5.getPort());
}
@Test
public void testTlsTrustManagerBypass() {
ConnectionConfig config = new ConnectionConfig("untrusted.host", 992);
config.setUseTls(true);
config.setTlsVerifyCert(false);
TlsTrustManager trustManager = new TlsTrustManager(config);
// With tlsVerifyCert=false, checkServerTrusted must not throw even with null/empty chain
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
}
@Test
public void testTlsTrustManagerVerifierCallback() {
ConnectionConfig config = new ConnectionConfig("selfsigned.host", 992);
config.setUseTls(true);
config.setTlsVerifyCert(true);
AtomicBoolean verifierInvoked = new AtomicBoolean(false);
config.setCertificateVerifier((chain, authType, exception) -> {
verifierInvoked.set(true);
return true; // Accept certificate
});
TlsTrustManager trustManager = new TlsTrustManager(config);
// Standard verification will fail on empty cert chain, triggering our callback
assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
assertTrue(verifierInvoked.get(), "Expected verifier callback to be invoked");
}
@Test
public void testTlsTrustManagerVerifierRejection() {
ConnectionConfig config = new ConnectionConfig("badcert.host", 992);
config.setUseTls(true);
config.setTlsVerifyCert(true);
config.setCertificateVerifier((chain, authType, exception) -> false); // Reject certificate
TlsTrustManager trustManager = new TlsTrustManager(config);
assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA"));
}
@Test
public void testCreateSSLContext() throws Exception {
ConnectionConfig config = new ConnectionConfig("secure.host", 992);
config.setUseTls(true);
SSLContext context = TlsTrustManager.createSSLContext(config);
assertNotNull(context);
assertNotNull(context.getSocketFactory());
}
}