diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index f29547c..81b6e77 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -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++) { - int b = screen.getCell(i).ec & 0xFF; - outputWrite(b != 0 ? b : 0x40); + // Unformatted screen: send all non-null characters (null suppression) + for (int i = 0; i < size; i++) { + int b = screen.getCell(i).ec & 0xFF; + 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; } } } diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index db8c834..ada9695 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -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; } diff --git a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java index 7032585..957c38a 100644 --- a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java @@ -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. */ diff --git a/lib3270j/src/test/java/org/lib3270j/datastream/DataStreamProcessorTest.java b/lib3270j/src/test/java/org/lib3270j/datastream/DataStreamProcessorTest.java index 3488d0d..c951019 100644 --- a/lib3270j/src/test/java/org/lib3270j/datastream/DataStreamProcessorTest.java +++ b/lib3270j/src/test/java/org/lib3270j/datastream/DataStreamProcessorTest.java @@ -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 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"); + } } diff --git a/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java b/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java index 829db33..82e3100 100644 --- a/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java +++ b/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java @@ -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 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' + } }