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 aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
// AID byte
outputWrite(AID_NO);
outputWrite(aid);
// Cursor address
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
outputWrite(caddr[0] & 0xFF);
@@ -710,56 +717,36 @@ public class DataStreamProcessor {
outputWrite(caddr[1] & 0xFF);
if (!screen.isFormatted()) {
// Unformatted screen: send data up to last non-null
int lastNonNull = -1;
for (int i = size - 1; i >= 0; i--) {
if (screen.getCell(i).ec != 0) {
lastNonNull = i;
break;
}
}
if (lastNonNull >= 0) {
for (int i = 0; i <= lastNonNull; i++) {
// Unformatted screen: send all non-null characters (null suppression)
for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
outputWrite(b != 0 ? b : 0x40);
if (b != 0x00) {
outputWrite(b);
}
}
} 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++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
int fieldStart = (i + 1) % size;
// Collect field data and find last non-null byte
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
// Always send SBA and address of first character position in field
outputWrite(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
outputWrite(addr[0] & 0xFF);
outputWrite(addr[1] & 0xFF);
// Send field data (strip trailing nulls)
if (lastNonNull >= 0) {
byte[] allData = fieldData.toByteArray();
for (int k = 0; k <= lastNonNull; k++) {
outputWrite(allData[k] & 0xFF);
// Send all non-null characters in field (suppressing 0x00)
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 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) {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
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
ExtendedAttribute ea = screen.getCell(baddr);
@@ -120,8 +122,10 @@ public class InputProcessor {
// Insert mode: shift characters right from cursor to end of field
// Find end of field
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);
count++;
if (endAddr == baddr) break; // wrapped around (unformatted)
}
// Check if last position is non-null (field overflow)
@@ -131,11 +135,13 @@ public class InputProcessor {
}
// Shift right from endAddr-1 down to baddr
int dst = endAddr;
while (dst != baddr) {
int shiftCount = 0;
while (dst != baddr && shiftCount < size) {
int src = screen.decrementAddress(dst);
screen.getCell(dst).ec = screen.getCell(src).ec;
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
dst = src;
shiftCount++;
}
}
@@ -155,9 +161,11 @@ public class InputProcessor {
// Advance cursor
int startAdvance = baddr;
baddr = (baddr + 1) % size;
int advCount = 0;
// 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;
advCount++;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
@@ -247,39 +255,28 @@ public class InputProcessor {
if (screen.isFormatted()) {
// 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();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int fieldStart = (i + 1) % size;
// First, collect field data and find last non-null byte
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
// Always send SBA and address of first character in field
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
// Only send data if there is any (strip trailing nulls)
if (lastNonNull >= 0) {
byte[] allData = fieldData.toByteArray();
out.write(allData, 0, lastNonNull + 1);
// Send all non-null characters in field (suppressing 0x00)
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
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;
}
int size = screen.getRows() * screen.getCols();
if (size <= 0) return false;
address = ((address % size) + size) % size;
int faPos = screen.findFieldAttribute(address);
if (faPos < 0) {
System.out.println("LP: no FA found for addr=" + address);
@@ -461,11 +460,14 @@ public class InputProcessor {
}
public void backTab() {
// Find previous unprotected field
int addr = screen.getCursorAddress();
if (!screen.isFormatted()) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int addr = screen.getCursorAddress();
addr = ((addr % size) + size) % size;
int start = screen.decrementAddress(addr);
addr = start;
int count = 0;
do {
addr = screen.decrementAddress(addr);
if (screen.getCell(addr).isFieldAttribute()) {
@@ -475,7 +477,8 @@ public class InputProcessor {
return;
}
}
} while (addr != start);
count++;
} while (addr != start && count < size);
}
public void eraseEof() {
@@ -493,15 +496,19 @@ public class InputProcessor {
}
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
addr = ((addr % size) + size) % size;
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// 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);
ea.ec = 0;
ea.ucs4 = 0;
addr = screen.incrementAddress(addr);
count++;
}
// Set MDT
@@ -516,13 +523,20 @@ public class InputProcessor {
public void deleteChar() {
if (!screen.isFormatted()) return;
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);
if (faIsProtected(faVal & 0xFF)) return;
// Shift characters left within the field
int shiftAddr = addr;
int size = screen.getRows() * screen.getCols();
while (true) {
int count = 0;
while (count < size) {
int next = screen.incrementAddress(shiftAddr);
if (screen.getCell(next).isFieldAttribute()) {
screen.getCell(shiftAddr).ec = 0;
@@ -532,6 +546,7 @@ public class InputProcessor {
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
shiftAddr = next;
count++;
}
int faAddr = screen.findFieldAttribute(addr);
@@ -543,8 +558,19 @@ public class InputProcessor {
}
public void backspace() {
if (screen.getCursorAddress() == 0) return;
cursorLeft();
int addr = screen.getCursorAddress();
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();
}
@@ -715,11 +741,13 @@ public class InputProcessor {
int fieldLen = 0;
int pos = fieldStart;
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).ucs4 = 0;
fieldLen++;
pos = screen.incrementAddress(pos);
count++;
if (pos == fieldStart) break;
}
@@ -272,13 +272,17 @@ public class ScreenBuffer {
public int findFieldAttribute(int baddr) {
if (!formatted) return -1;
int size = rows * cols;
if (size <= 0) return -1;
baddr = ((baddr % size) + size) % size;
int start = baddr;
int count = 0;
do {
if (buffer[baddr].isFieldAttribute()) {
return baddr;
}
baddr = (baddr > 0) ? baddr - 1 : size - 1;
} while (baddr != start);
count++;
} while (baddr != start && count < size);
return -1;
}
@@ -296,8 +300,12 @@ public class ScreenBuffer {
* Returns 0 if none found.
*/
public int findNextUnprotected(int baddr) {
if (!formatted) return 0;
int size = rows * cols;
if (size <= 0) return 0;
baddr = ((baddr % size) + size) % size;
int start = baddr;
int count = 0;
do {
int next = (baddr + 1) % size;
if (buffer[baddr].isFieldAttribute()
@@ -306,7 +314,8 @@ public class ScreenBuffer {
return next;
}
baddr = next;
} while (baddr != start);
count++;
} while (baddr != start && count < size);
return 0;
}
@@ -329,12 +338,17 @@ public class ScreenBuffer {
/** Increment buffer address (wrapping). */
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). */
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. */
@@ -66,4 +66,20 @@ public class DataStreamProcessorTest {
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) 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'
}
}