Fix bugs reported in Discord
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s

This commit is contained in:
2026-09-12 14:58:48 +00:00
parent 46a022d86b
commit c28c097e25
4 changed files with 298 additions and 46 deletions
@@ -54,6 +54,12 @@ public class DataStreamProcessor {
private boolean unlockSysPending = false;
private boolean rcvdRead = false;
// Modal SA (set attribute) character attributes
private byte currentFg = 0;
private byte currentBg = 0;
private byte currentGr = 0;
private byte currentCs = 0;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
public interface OutputSender {
@@ -240,6 +246,10 @@ public class DataStreamProcessor {
programSymbolManager.commitStagedSymbols();
log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected();
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
break;
case CMD_WSF:
case SNA_CMD_WSF:
@@ -378,11 +388,19 @@ public class DataStreamProcessor {
if (wccReset(wcc)) {
// Reset all character attributes to defaults
log.fine("WCC reset: clearing default attributes");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
}
if (eraseFirst) {
screen.clear();
log.fine("Cleared screen for Erase/Write");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
}
// Process orders and data starting at byte 2
@@ -390,9 +408,6 @@ public class DataStreamProcessor {
int end = offset + length;
int baddr = screen.getBufferAddress();
int size = screen.getRows() * screen.getCols();
// Current SA (set attribute) values for character-mode
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
boolean lastWasOrder = false;
while (pos < end) {
@@ -427,10 +442,6 @@ public class DataStreamProcessor {
// FA position is a display position that shows as blank
ea.ec = 0;
ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
screen.setFormatted(true);
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
@@ -455,10 +466,6 @@ public class DataStreamProcessor {
ea.clear();
ea.ec = 0;
ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
for (int i = 0; i < nPairs; i++) {
int attrType = data[pos + 2 + i * 2] & 0xFF;
@@ -485,6 +492,12 @@ public class DataStreamProcessor {
int attrType = data[pos + 1] & 0xFF;
int attrValue = data[pos + 2] & 0xFF;
switch (attrType) {
case XA_ALL:
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
break;
case XA_FOREGROUND:
currentFg = (byte) attrValue;
break;
@@ -82,4 +82,120 @@ public class DataStreamProcessorTest {
assertNotNull(sentData.get());
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
}
@Test
public void testCharacterAttributesPersistAcrossStartField() throws java.io.IOException {
// User's exact ISPF Option 0 sequence:
// SBA(7, 1) -> SF(prot,skip) -> SA(yellow) -> ' 4 ' -> SBA(7, 7) -> SF(prot,skip) -> 'DISPLAY'
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3); // WCC
// SBA(7, 1) -> address 7*80 + 1 = 561
byte[] addr1 = encodeAddress(561, 24, 80);
stream.write(ORDER_SBA);
stream.write(addr1);
// SF(prot, skip)
stream.write(ORDER_SF);
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
// SA(yellow) -> XA_FOREGROUND, COLOR_YELLOW (0xF6)
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
// Data: ' 4 '
stream.write(translator.stringToEbcdic(" 4 "));
// SBA(7, 7) -> address 7*80 + 7 = 567
byte[] addr2 = encodeAddress(567, 24, 80);
stream.write(ORDER_SBA);
stream.write(addr2);
// SF(prot, skip)
stream.write(ORDER_SF);
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
// Data: 'DISPLAY'
stream.write(translator.stringToEbcdic("DISPLAY"));
byte[] record = stream.toByteArray();
processor.processRecord(record, 0, record.length, true);
// Positions 562..566 (' 4 ') must be yellow (0xF6)
for (int i = 562; i <= 566; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should have yellow foreground (0xF6)");
}
// Positions 568..574 ('DISPLAY') across the second SF must also retain yellow (0xF6)
for (int i = 568; i <= 574; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " ('DISPLAY') should retain yellow foreground (0xF6)");
}
// Now test that SA with XA_ALL resets character attributes to default (0)
java.io.ByteArrayOutputStream resetStream = new java.io.ByteArrayOutputStream();
resetStream.write(CMD_W);
resetStream.write(0xC3);
resetStream.write(ORDER_SA);
resetStream.write(XA_ALL);
resetStream.write(0x00);
resetStream.write(translator.stringToEbcdic("TEST"));
byte[] resetRecord = resetStream.toByteArray();
processor.processRecord(resetRecord, 0, resetRecord.length, true);
for (int i = 575; i < 579; i++) {
assertEquals((byte) 0, screen.getCell(i).fg, "Cell at " + i + " should have default foreground (0) after SA(XA_ALL)");
}
}
@Test
public void testCharacterAttributesPersistAcrossStartFieldExtended() throws java.io.IOException {
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3);
// SA(yellow)
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
// SFE with 1 pair (3270 FA)
stream.write(ORDER_SFE);
stream.write(0x01); // 1 pair
stream.write(XA_3270);
stream.write(FA_PRINTABLE);
// Data 'HELLO'
stream.write(translator.stringToEbcdic("HELLO"));
byte[] record = stream.toByteArray();
processor.processRecord(record, 0, record.length, true);
// Check cells of HELLO (positions 1..5) have fg == 0xF6
for (int i = 1; i <= 5; i++) {
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should retain yellow foreground across SFE");
}
}
@Test
public void testCharacterAttributesResetOnErase() throws java.io.IOException {
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
stream.write(CMD_EW);
stream.write(0xC3);
stream.write(ORDER_SA);
stream.write(XA_FOREGROUND);
stream.write(0xF6);
stream.write(translator.stringToEbcdic("A"));
processor.processRecord(stream.toByteArray(), 0, stream.size(), true);
assertEquals((byte) 0xF6, screen.getCell(0).fg);
// New EW command resets attributes
java.io.ByteArrayOutputStream ewStream = new java.io.ByteArrayOutputStream();
ewStream.write(CMD_EW);
ewStream.write(0xC3);
ewStream.write(translator.stringToEbcdic("B"));
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
assertEquals((byte) 0, screen.getCell(0).fg);
}
}