Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s

This commit is contained in:
2026-09-22 13:38:12 +00:00
parent d05f1a4c45
commit 3d3a939ece
29 changed files with 1773 additions and 92 deletions
@@ -0,0 +1,204 @@
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.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS;
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 DBCSDataStreamFullTest {
@Test
public void testDBCSDataStreamShiftOutShiftIn() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// Data stream:
// EW, WCC, SBA (0, 0), SO (0x0E), 0x40 0x40 (\u3000 ideographic space), SI (0x0F)
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // Shift-Out
(byte) 0x40, (byte) 0x40, // DBCS ideographic space
(byte) 0x0F // Shift-In
};
processor.processRecord(stream, 0, stream.length, true);
ExtendedAttribute cell0 = screen.getCell(0);
assertEquals(ExtendedAttribute.DB_SO, cell0.db, "Cell 0 must be DB_SO");
ExtendedAttribute cell1 = screen.getCell(1);
assertEquals(ExtendedAttribute.DB_LEFT, cell1.db, "Cell 1 must be DB_LEFT");
assertEquals('\u3000', cell1.ucs4, "Cell 1 ucs4 must be \\u3000");
ExtendedAttribute cell2 = screen.getCell(2);
assertEquals(ExtendedAttribute.DB_RIGHT, cell2.db, "Cell 2 must be DB_RIGHT");
assertEquals('\u3000', cell2.ucs4, "Cell 2 ucs4 must be \\u3000");
ExtendedAttribute cell3 = screen.getCell(3);
assertEquals(ExtendedAttribute.DB_SI, cell3.db, "Cell 3 must be DB_SI");
}
@Test
public void testDBCSRepeatToAddressOrder() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// In DBCS mode (after SO), ORDER_RA consumes 2 data bytes and fills pairs
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // SO at 0
(byte) ORDER_RA,
(byte) 0x40, (byte) 0x4B, // RA up to address 11 (covers cells 1..10)
(byte) 0x40, (byte) 0x40, // 2-byte repeat char (\u3000)
(byte) 0x0F // SI at 11
};
processor.processRecord(stream, 0, stream.length, true);
assertEquals(ExtendedAttribute.DB_SO, screen.getCell(0).db);
for (int i = 1; i < 11; i += 2) {
assertEquals(ExtendedAttribute.DB_LEFT, screen.getCell(i).db, "Cell " + i + " must be DB_LEFT");
assertEquals('\u3000', screen.getCell(i).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, screen.getCell(i + 1).db, "Cell " + (i + 1) + " must be DB_RIGHT");
assertEquals('\u3000', screen.getCell(i + 1).ucs4);
}
assertEquals(ExtendedAttribute.DB_SI, screen.getCell(11).db);
}
@Test
public void testECLPlanesDBCSAndOutlining() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // SO at 0
(byte) 0x40, (byte) 0x40, // DBCS at 1, 2
(byte) 0x0F // SI at 3
};
processor.processRecord(stream, 0, stream.length, true);
// Add outlining to cell 1
screen.getCell(1).ol = (byte) (ExtendedAttribute.OL_BOX);
ECLPS eclPS = new ECLPS(screen, null, translator);
char[] dbcsPlane = new char[4];
int count = eclPS.getPlane(ECLConstants.DBCS_PLANE, dbcsPlane, 0, 4);
assertEquals(4, count);
assertEquals(0x20, dbcsPlane[0], "SO must be 0x20 in DBCS plane");
assertEquals(0xC0, dbcsPlane[1], "DB_LEFT must be 0xC0 (DB_DBHIGH)");
assertEquals(0x80, dbcsPlane[2], "DB_RIGHT must be 0x80 (DB_DBCS)");
assertEquals(0x10, dbcsPlane[3], "SI must be 0x10 in DBCS plane");
char[] gridPlane = new char[4];
eclPS.getPlane(ECLConstants.GRID_PLANE, gridPlane, 0, 4);
assertEquals((char) (ExtendedAttribute.OL_BOX & 0xFF), gridPlane[1]);
// Helper methods
assertTrue(eclPS.IsSOChar(0));
assertFalse(eclPS.IsDBCSChar(0));
assertTrue(eclPS.IsDBCSChar(1));
assertTrue(eclPS.IsDBCS1stChar(1));
assertFalse(eclPS.IsDBCS2ndChar(1));
assertTrue(eclPS.IsDBCSChar(2));
assertFalse(eclPS.IsDBCS1stChar(2));
assertTrue(eclPS.IsDBCS2ndChar(2));
assertTrue(eclPS.IsSIChar(3));
assertFalse(eclPS.IsDBCSChar(3));
}
@Test
public void testECLFieldDBCSMethods() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// Field with SO + DBCS + SI
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) ORDER_SF,
(byte) FA_PRINTABLE, // Field at 0
(byte) 0x0E, // SO at 1
(byte) 0x40, (byte) 0x40, // DBCS at 2, 3
(byte) 0x0F // SI at 4
};
processor.processRecord(stream, 0, stream.length, true);
ECLPS eclPS = new ECLPS(screen, null, translator);
ECLFieldList fieldList = new ECLFieldList(eclPS);
fieldList.Refresh();
assertEquals(1, fieldList.size());
ECLField field = fieldList.getField(0);
assertTrue(field.isDBCS(), "Field should report isDBCS = true");
assertTrue(field.isDBCSOnly(), "Field should report isDBCSOnly = true");
}
@Test
public void testQueryReplyBuilderDBCSCharsets() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
QueryReplyBuilder builder = new QueryReplyBuilder(screen);
byte[] charsets = builder.buildCharsets();
assertNotNull(charsets);
assertEquals(42, charsets.length, "DBCS Character Sets QR payload must be 42 bytes (matching HoD xxxqrcs)");
assertEquals((byte) 0x8E, charsets[0], "Flags 1 must be 0x8E");
assertEquals((byte) 0x00, charsets[1], "Flags 2 must be 0x00");
assertEquals(12, charsets[2], "SDW must be 12");
assertEquals(24, charsets[3], "SDH must be 24");
assertEquals(0x0B, charsets[8], "Descriptor length DL must be 11 (0x0B)");
// Descriptor 2 (DBCS): offset 9 + 11 = 20
assertEquals(0x01, charsets[20], "Set ID must be 1");
assertEquals((byte) 0x80, charsets[22], "Local ID must be 0x80 (DBCS)");
assertEquals(24, charsets[23], "DBCS cell width must be double (24)");
assertEquals(24, charsets[24], "DBCS cell height must be 24");
assertEquals(0x41, charsets[25], "Section start must be 0x41");
assertEquals(0x7F, charsets[26], "Section end for 930 must be 0x7F");
// DB CGCSGID = 370 (0x0172), DB CPGID = 300 (0x012C)
int cgcsgid = ((charsets[27] & 0xFF) << 8) | (charsets[28] & 0xFF);
int cpgid = ((charsets[29] & 0xFF) << 8) | (charsets[30] & 0xFF);
assertEquals(370, cgcsgid);
assertEquals(300, cpgid);
// Summary Query Reply contains Outlining (0x8C) and DBCS Asia (0x91)
byte[] summary = builder.buildSummary();
boolean hasOutlining = false;
boolean hasDbcsAsia = false;
for (byte b : summary) {
if ((b & 0xFF) == QR_OUTLINING) hasOutlining = true;
if ((b & 0xFF) == QR_DBCS_ASIA) hasDbcsAsia = true;
}
assertTrue(hasOutlining, "Summary must include QR_OUTLINING");
assertTrue(hasDbcsAsia, "Summary must include QR_DBCS_ASIA");
}
}
@@ -49,9 +49,24 @@ public class QueryReplyBuilderTest {
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary
int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
boolean hasB0 = false;
boolean hasA8 = false;
for (int i = 5; i < 1 + sumLen; i++) {
assertNotEquals(QR_SEGMENT, replies[i] & 0xFF);
if ((replies[i] & 0xFF) == QR_SEGMENT) hasB0 = true;
if ((replies[i] & 0xFF) == QR_TRANSPARENCY) hasA8 = true;
}
assertFalse(hasB0);
assertTrue(hasA8, "QR_TRANSPARENCY (0xA8) should be advertised even in GraphicsMode.NONE");
// And QR_TRANSPARENCY structured field payload must be present in replies
boolean hasA8Payload = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_TRANSPARENCY) {
hasA8Payload = true;
break;
}
}
assertTrue(hasA8Payload, "QR_TRANSPARENCY structured field should be included in buildAllQueryReplies");
}
@Test
@@ -87,10 +87,10 @@ public class QueryReplyPhase2Test {
assertNotNull(reply);
List<ParsedSF> sfs = parseQueryReplies(reply);
// Base query reply must contain exactly 10 structured fields for SBCS:
// Base query reply must contain exactly 11 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());
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part), 0xA8 (Transparency)
assertEquals(11, sfs.size());
assertEquals(QR_SUMMARY, sfs.get(0).qcode);
assertEquals(QR_USABLE_AREA, sfs.get(1).qcode);
@@ -102,10 +102,10 @@ public class QueryReplyPhase2Test {
assertEquals(QR_DDM, sfs.get(7).qcode);
assertEquals(QR_AUXDA, sfs.get(8).qcode);
assertEquals(QR_IMP_PART, sfs.get(9).qcode);
assertEquals(QR_TRANSPARENCY, sfs.get(10).qcode);
// Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply
// Verify that Vector Graphics SF payloads (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);
@@ -246,7 +246,7 @@ public class QueryReplyPhase2Test {
assertTrue(sent.length > 0);
assertEquals((byte) AID_SF, sent[0]);
List<ParsedSF> sfs = parseQueryReplies(sent);
assertEquals(10, sfs.size());
assertEquals(11, sfs.size());
// Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4
byte[] queryList = new byte[] {
@@ -0,0 +1,143 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TransparencyDataStreamTest {
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
EbcdicTranslator translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testSetAttributeTransparency() {
// Send SFE with XA_TRANSPARENCY = XAT_OPAQUE (0xFF)
byte[] sfeRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SFE, 0x02,
(byte) XA_3270, (byte) FA_PRINTABLE,
(byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE
};
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
ExtendedAttribute cell0 = screen.getCell(0);
assertTrue(cell0.isFieldAttribute());
assertEquals(ExtendedAttribute.TR_OPAQUE, cell0.tr);
// Send SA with XA_TRANSPARENCY = XAT_OPAQUE followed by character 'A' (0xC1)
byte[] saRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x01,
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
(byte) 0xC1 // 'A'
};
processor.processRecord(saRecord, 0, saRecord.length, true);
ExtendedAttribute charCell1 = screen.getCell(1);
assertFalse(charCell1.isFieldAttribute());
assertEquals((byte) 0xC1, charCell1.ec);
assertEquals(ExtendedAttribute.TR_OPAQUE, charCell1.tr);
// Send SA with XA_ALL (0x00) followed by character 'B' (0xC2) -> should reset tr to 0
byte[] resetRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x02,
(byte) ORDER_SA, (byte) XA_ALL, 0x00,
(byte) 0xC2 // 'B'
};
processor.processRecord(resetRecord, 0, resetRecord.length, true);
ExtendedAttribute charCell2 = screen.getCell(2);
assertFalse(charCell2.isFieldAttribute());
assertEquals((byte) 0xC2, charCell2.ec);
assertEquals(ExtendedAttribute.TR_DEFAULT, charCell2.tr);
}
@Test
public void testRepeatToAddressWithTransparency() {
// Set SA Transparency = Opaque, then RA to address 5 with char 'C' (0xC3)
byte[] raRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SBA, 0x00, 0x00,
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
(byte) ORDER_RA, 0x00, 0x05, (byte) 0xC3
};
processor.processRecord(raRecord, 0, raRecord.length, true);
for (int i = 0; i < 5; i++) {
ExtendedAttribute cell = screen.getCell(i);
assertEquals((byte) 0xC3, cell.ec);
assertEquals(ExtendedAttribute.TR_OPAQUE, cell.tr, "Cell at " + i + " should have TR_OPAQUE");
}
}
@Test
public void testReadBufferExtendedFieldIncludesTransparency() {
screen.setReplyMode((byte) SF_SRM_XFIELD);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
processor.processReadBuffer();
byte[] result = output.toByteArray();
assertTrue(result.length > 3);
assertEquals((byte) AID_NO, result[0]);
// Find ORDER_SFE
boolean foundTr = false;
for (int i = 3; i < result.length - 2; i++) {
if ((result[i] & 0xFF) == ORDER_SFE) {
int pairCount = result[i + 1] & 0xFF;
int pos = i + 2;
for (int p = 0; p < pairCount; p++) {
int attrType = result[pos] & 0xFF;
int attrVal = result[pos + 1] & 0xFF;
if (attrType == XA_TRANSPARENCY && attrVal == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
foundTr = true;
}
pos += 2;
}
}
}
assertTrue(foundTr, "Read Buffer in SF_SRM_XFIELD mode should report XA_TRANSPARENCY");
}
@Test
public void testReadBufferCharacterModeIncludesTransparency() {
screen.setReplyMode((byte) SF_SRM_CHAR);
screen.getCell(0).ec = (byte) 0xC1;
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
processor.processReadBuffer();
byte[] result = output.toByteArray();
assertTrue(result.length > 3);
boolean foundSaTr = false;
for (int i = 3; i < result.length - 2; i++) {
if ((result[i] & 0xFF) == ORDER_SA && (result[i + 1] & 0xFF) == XA_TRANSPARENCY && (result[i + 2] & 0xFF) == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
foundSaTr = true;
break;
}
}
assertTrue(foundSaTr, "Read Buffer in SF_SRM_CHAR mode should output ORDER_SA for XA_TRANSPARENCY");
}
}