Keep par0dy with a3270
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 1m3s

This commit is contained in:
2026-08-25 01:12:50 +00:00
parent b5a4978b24
commit dbe0cdaa89
5 changed files with 167 additions and 76 deletions
@@ -662,8 +662,15 @@ public class DataStreamProcessor {
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
// AID byte // AID byte
outputWrite(AID_NO); outputWrite(aid);
// Cursor address // Cursor address
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
outputWrite(caddr[0] & 0xFF); outputWrite(caddr[0] & 0xFF);
@@ -710,56 +717,36 @@ public class DataStreamProcessor {
outputWrite(caddr[1] & 0xFF); outputWrite(caddr[1] & 0xFF);
if (!screen.isFormatted()) { if (!screen.isFormatted()) {
// Unformatted screen: send data up to last non-null // Unformatted screen: send all non-null characters (null suppression)
int lastNonNull = -1; for (int i = 0; i < size; i++) {
for (int i = size - 1; i >= 0; i--) { int b = screen.getCell(i).ec & 0xFF;
if (screen.getCell(i).ec != 0) { if (b != 0x00) {
lastNonNull = i; outputWrite(b);
break;
}
}
if (lastNonNull >= 0) {
for (int i = 0; i <= lastNonNull; i++) {
int b = screen.getCell(i).ec & 0xFF;
outputWrite(b != 0 ? b : 0x40);
} }
} }
} else { } else {
// Formatted screen: send modified fields with trailing NULLs stripped // Formatted screen: send modified fields with null suppression per 3270 spec
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i); ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) { if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
int fieldStart = (i + 1) % size; int fieldStart = (i + 1) % size;
// Collect field data and find last non-null byte // Always send SBA and address of first character position in field
ByteArrayOutputStream fieldData = new ByteArrayOutputStream();
int pos = fieldStart;
int lastNonNull = -1;
int fieldLen = 0;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
fieldData.write(b);
if (b != 0x00) {
lastNonNull = fieldLen;
}
fieldLen++;
pos = (pos + 1) % size;
if (pos == fieldStart)
break;
}
// Always send SBA and address
outputWrite(ORDER_SBA); outputWrite(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
outputWrite(addr[0] & 0xFF); outputWrite(addr[0] & 0xFF);
outputWrite(addr[1] & 0xFF); outputWrite(addr[1] & 0xFF);
// Send field data (strip trailing nulls) // Send all non-null characters in field (suppressing 0x00)
if (lastNonNull >= 0) { int pos = fieldStart;
byte[] allData = fieldData.toByteArray(); while (!screen.getCell(pos).isFieldAttribute()) {
for (int k = 0; k <= lastNonNull; k++) { int b = screen.getCell(pos).ec & 0xFF;
outputWrite(allData[k] & 0xFF); if (b != 0x00) {
outputWrite(b);
} }
pos = (pos + 1) % size;
if (pos == fieldStart)
break;
} }
} }
} }
@@ -95,8 +95,10 @@ public class InputProcessor {
public void typeCharacter(char ch) { public void typeCharacter(char ch) {
if (keyboardLocked) return; if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size;
// Check if cursor is at a field attribute or in a protected field // Check if cursor is at a field attribute or in a protected field
ExtendedAttribute ea = screen.getCell(baddr); ExtendedAttribute ea = screen.getCell(baddr);
@@ -120,8 +122,10 @@ public class InputProcessor {
// Insert mode: shift characters right from cursor to end of field // Insert mode: shift characters right from cursor to end of field
// Find end of field // Find end of field
int endAddr = baddr; int endAddr = baddr;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute()) { int count = 0;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = screen.incrementAddress(endAddr); endAddr = screen.incrementAddress(endAddr);
count++;
if (endAddr == baddr) break; // wrapped around (unformatted) if (endAddr == baddr) break; // wrapped around (unformatted)
} }
// Check if last position is non-null (field overflow) // Check if last position is non-null (field overflow)
@@ -131,11 +135,13 @@ public class InputProcessor {
} }
// Shift right from endAddr-1 down to baddr // Shift right from endAddr-1 down to baddr
int dst = endAddr; int dst = endAddr;
while (dst != baddr) { int shiftCount = 0;
while (dst != baddr && shiftCount < size) {
int src = screen.decrementAddress(dst); int src = screen.decrementAddress(dst);
screen.getCell(dst).ec = screen.getCell(src).ec; screen.getCell(dst).ec = screen.getCell(src).ec;
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4; screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
dst = src; dst = src;
shiftCount++;
} }
} }
@@ -155,9 +161,11 @@ public class InputProcessor {
// Advance cursor // Advance cursor
int startAdvance = baddr; int startAdvance = baddr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
int advCount = 0;
// Skip over field attributes safely // Skip over field attributes safely
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance) { while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
advCount++;
} }
screen.setCursorAddress(baddr); screen.setCursorAddress(baddr);
screen.markAllChanged(); screen.markAllChanged();
@@ -247,39 +255,28 @@ public class InputProcessor {
if (screen.isFormatted()) { if (screen.isFormatted()) {
// Send modified fields with SBA // Send modified fields with SBA
// Per 3270 Data Stream Architecture: strip trailing NULLs (0x00) from field data // Per 3270 Data Stream Architecture: suppress NULLs (0x00) from field data
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i); ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) { if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int fieldStart = (i + 1) % size; int fieldStart = (i + 1) % size;
// First, collect field data and find last non-null byte // Always send SBA and address of first character in field
ByteArrayOutputStream fieldData = new ByteArrayOutputStream();
int pos = fieldStart;
int lastNonNull = -1;
int fieldLen = 0;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
fieldData.write(b);
if (b != 0x00) {
lastNonNull = fieldLen;
}
fieldLen++;
pos = (pos + 1) % size;
if (pos == fieldStart) break;
}
// Always send SBA and address
out.write(ORDER_SBA); out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols()); byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF); out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF); out.write(addr[1] & 0xFF);
// Only send data if there is any (strip trailing nulls) // Send all non-null characters in field (suppressing 0x00)
if (lastNonNull >= 0) { int pos = fieldStart;
byte[] allData = fieldData.toByteArray(); while (!screen.getCell(pos).isFieldAttribute()) {
out.write(allData, 0, lastNonNull + 1); int b = screen.getCell(pos).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart) break;
} }
} }
} }
@@ -359,6 +356,8 @@ public class InputProcessor {
return false; return false;
} }
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return false;
address = ((address % size) + size) % size;
int faPos = screen.findFieldAttribute(address); int faPos = screen.findFieldAttribute(address);
if (faPos < 0) { if (faPos < 0) {
System.out.println("LP: no FA found for addr=" + address); System.out.println("LP: no FA found for addr=" + address);
@@ -461,11 +460,14 @@ public class InputProcessor {
} }
public void backTab() { public void backTab() {
// Find previous unprotected field if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int addr = screen.getCursorAddress();
addr = ((addr % size) + size) % size;
int start = screen.decrementAddress(addr); int start = screen.decrementAddress(addr);
addr = start; addr = start;
int count = 0;
do { do {
addr = screen.decrementAddress(addr); addr = screen.decrementAddress(addr);
if (screen.getCell(addr).isFieldAttribute()) { if (screen.getCell(addr).isFieldAttribute()) {
@@ -475,7 +477,8 @@ public class InputProcessor {
return; return;
} }
} }
} while (addr != start); count++;
} while (addr != start && count < size);
} }
public void eraseEof() { public void eraseEof() {
@@ -493,15 +496,19 @@ public class InputProcessor {
} }
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
addr = ((addr % size) + size) % size;
byte faVal = screen.getFieldAttributeAt(addr); byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return; if (faIsProtected(faVal & 0xFF)) return;
// Erase from cursor to end of field // Erase from cursor to end of field
while (!screen.getCell(addr).isFieldAttribute()) { int count = 0;
while (!screen.getCell(addr).isFieldAttribute() && count < size) {
ExtendedAttribute ea = screen.getCell(addr); ExtendedAttribute ea = screen.getCell(addr);
ea.ec = 0; ea.ec = 0;
ea.ucs4 = 0; ea.ucs4 = 0;
addr = screen.incrementAddress(addr); addr = screen.incrementAddress(addr);
count++;
} }
// Set MDT // Set MDT
@@ -516,13 +523,20 @@ public class InputProcessor {
public void deleteChar() { public void deleteChar() {
if (!screen.isFormatted()) return; if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
addr = ((addr % size) + size) % size;
// Cannot delete a field attribute
if (screen.getCell(addr).isFieldAttribute()) return;
byte faVal = screen.getFieldAttributeAt(addr); byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return; if (faIsProtected(faVal & 0xFF)) return;
// Shift characters left within the field // Shift characters left within the field
int shiftAddr = addr; int shiftAddr = addr;
int size = screen.getRows() * screen.getCols(); int count = 0;
while (true) { while (count < size) {
int next = screen.incrementAddress(shiftAddr); int next = screen.incrementAddress(shiftAddr);
if (screen.getCell(next).isFieldAttribute()) { if (screen.getCell(next).isFieldAttribute()) {
screen.getCell(shiftAddr).ec = 0; screen.getCell(shiftAddr).ec = 0;
@@ -532,6 +546,7 @@ public class InputProcessor {
screen.getCell(shiftAddr).ec = screen.getCell(next).ec; screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4; screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
shiftAddr = next; shiftAddr = next;
count++;
} }
int faAddr = screen.findFieldAttribute(addr); int faAddr = screen.findFieldAttribute(addr);
@@ -543,8 +558,19 @@ public class InputProcessor {
} }
public void backspace() { public void backspace() {
if (screen.getCursorAddress() == 0) return; int addr = screen.getCursorAddress();
cursorLeft(); int prev = screen.decrementAddress(addr);
if (screen.isFormatted()) {
// If previous position is a field attribute or in protected field, do not back up over it
if (screen.getCell(prev).isFieldAttribute()) {
return;
}
byte faVal = screen.getFieldAttributeAt(prev);
if (faIsProtected(faVal & 0xFF)) {
return;
}
}
screen.setCursorAddress(prev);
deleteChar(); deleteChar();
} }
@@ -715,11 +741,13 @@ public class InputProcessor {
int fieldLen = 0; int fieldLen = 0;
int pos = fieldStart; int pos = fieldStart;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
while (!screen.getCell(pos).isFieldAttribute()) { int count = 0;
while (!screen.getCell(pos).isFieldAttribute() && count < size) {
screen.getCell(pos).ec = 0; screen.getCell(pos).ec = 0;
screen.getCell(pos).ucs4 = 0; screen.getCell(pos).ucs4 = 0;
fieldLen++; fieldLen++;
pos = screen.incrementAddress(pos); pos = screen.incrementAddress(pos);
count++;
if (pos == fieldStart) break; if (pos == fieldStart) break;
} }
@@ -272,13 +272,17 @@ public class ScreenBuffer {
public int findFieldAttribute(int baddr) { public int findFieldAttribute(int baddr) {
if (!formatted) return -1; if (!formatted) return -1;
int size = rows * cols; int size = rows * cols;
if (size <= 0) return -1;
baddr = ((baddr % size) + size) % size;
int start = baddr; int start = baddr;
int count = 0;
do { do {
if (buffer[baddr].isFieldAttribute()) { if (buffer[baddr].isFieldAttribute()) {
return baddr; return baddr;
} }
baddr = (baddr > 0) ? baddr - 1 : size - 1; baddr = (baddr > 0) ? baddr - 1 : size - 1;
} while (baddr != start); count++;
} while (baddr != start && count < size);
return -1; return -1;
} }
@@ -296,8 +300,12 @@ public class ScreenBuffer {
* Returns 0 if none found. * Returns 0 if none found.
*/ */
public int findNextUnprotected(int baddr) { public int findNextUnprotected(int baddr) {
if (!formatted) return 0;
int size = rows * cols; int size = rows * cols;
if (size <= 0) return 0;
baddr = ((baddr % size) + size) % size;
int start = baddr; int start = baddr;
int count = 0;
do { do {
int next = (baddr + 1) % size; int next = (baddr + 1) % size;
if (buffer[baddr].isFieldAttribute() if (buffer[baddr].isFieldAttribute()
@@ -306,7 +314,8 @@ public class ScreenBuffer {
return next; return next;
} }
baddr = next; baddr = next;
} while (baddr != start); count++;
} while (baddr != start && count < size);
return 0; return 0;
} }
@@ -329,12 +338,17 @@ public class ScreenBuffer {
/** Increment buffer address (wrapping). */ /** Increment buffer address (wrapping). */
public int incrementAddress(int addr) { public int incrementAddress(int addr) {
return (addr + 1) % (rows * cols); int size = rows * cols;
if (size <= 0) return 0;
return ((addr % size) + size + 1) % size;
} }
/** Decrement buffer address (wrapping). */ /** Decrement buffer address (wrapping). */
public int decrementAddress(int addr) { public int decrementAddress(int addr) {
return (addr > 0) ? addr - 1 : (rows * cols) - 1; int size = rows * cols;
if (size <= 0) return 0;
int norm = ((addr % size) + size) % size;
return (norm > 0) ? norm - 1 : size - 1;
} }
/** Convert buffer address to row. */ /** Convert buffer address to row. */
@@ -66,4 +66,20 @@ public class DataStreamProcessorTest {
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'"); assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
} }
} }
@Test
public void testReadBufferPropagatesLastAid() {
org.lib3270j.input.InputProcessor input = new org.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.setOutputCallback(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");
}
} }
@@ -153,4 +153,50 @@ public class InputProcessorTest {
assertEquals((byte) 0xFF, sf[34]); assertEquals((byte) 0xFF, sf[34]);
assertEquals((byte) AID_ENTER, sf[35]); 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.setOutputCallback(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'
}
} }