From bd4c1f304003ac12a85e93dc1c6973990be2bcfa Mon Sep 17 00:00:00 2001 From: Rudi Date: Thu, 24 Sep 2026 15:35:08 +0000 Subject: [PATCH] Add Read Modified Query from Moshix bug report --- .../datastream/DataStreamProcessor.java | 60 ++++++++++--- .../datastream/DataStreamProcessorTest.java | 87 +++++++++++++++++++ 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 2cac4d4..b1fc3e3 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -53,6 +53,7 @@ public class DataStreamProcessor { private boolean unlockPending = false; private boolean unlockSysPending = false; private boolean rcvdRead = false; + private boolean qreplyPending = false; // Modal SA (set attribute) character attributes private byte currentFg = 0; @@ -183,11 +184,13 @@ public class DataStreamProcessor { switch (cmd) { case CMD_W: case SNA_CMD_W: + qreplyPending = false; programSymbolManager.commitStagedSymbols(); processWrite(data, offset, length, false); break; case CMD_EW: case SNA_CMD_EW: + qreplyPending = false; programSymbolManager.commitStagedSymbols(); if (log.isLoggable(Level.FINE)) { log.fine(">>> ERASE/WRITE: clearing screen (default size)"); @@ -208,6 +211,7 @@ public class DataStreamProcessor { break; case CMD_EWA: case SNA_CMD_EWA: + qreplyPending = false; programSymbolManager.commitStagedSymbols(); if (log.isLoggable(Level.FINE)) { log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); @@ -236,7 +240,12 @@ public class DataStreamProcessor { case SNA_CMD_RM: rcvdRead = true; programSymbolManager.commitStagedSymbols(); - processReadModified(false); + if (qreplyPending) { + qreplyPending = false; + sendAllQueryReplies(); + } else { + processReadModified(false); + } break; case CMD_RMA: case SNA_CMD_RMA: @@ -246,6 +255,7 @@ public class DataStreamProcessor { break; case CMD_EAU: case SNA_CMD_EAU: + qreplyPending = false; programSymbolManager.commitStagedSymbols(); log.info(">>> EAU: erasing all unprotected fields"); screen.eraseAllUnprotected(); @@ -924,13 +934,26 @@ public class DataStreamProcessor { // ========== Read Buffer ========== public void processReadBuffer() { + int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) + ? inputProcessor.getLastAid() + : AID_NO; + processReadBuffer(aid); + } + + public void processReadBuffer(int aid) { + if (inputProcessor != null) { + inputProcessor.setLastAid(AID_NO); + byte[] data = inputProcessor.buildReadBufferInboundData(aid); + if (outputSender != null) { + outputSender.send3270Data(data); + } + return; + } + outputPos = 0; int size = screen.getRows() * screen.getCols(); - int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) - ? inputProcessor.getLastAid() - : AID_NO; if (inputProcessor != null) { inputProcessor.setLastAid(AID_NO); } @@ -1030,12 +1053,18 @@ public class DataStreamProcessor { // ========== Read Modified ========== public void processReadModified(boolean all) { + int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) + ? inputProcessor.getLastAid() + : AID_NO; + processReadModified(aid, all); + } + + public void processReadModified(int aid, boolean all) { if (ftDft != null && ftDft.readModified()) { return; } if (inputProcessor != null) { - int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO; inputProcessor.setLastAid(AID_NO); byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all); if (outputSender != null) { @@ -1046,9 +1075,6 @@ public class DataStreamProcessor { outputPos = 0; - int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) - ? inputProcessor.getLastAid() - : AID_NO; if (inputProcessor != null) { inputProcessor.setLastAid(AID_NO); } @@ -1211,9 +1237,11 @@ public class DataStreamProcessor { log.info("ReadPartition Query — sending base query replies"); graphicsPlane.clear(); gocaDecoder.resetDefaults(); + qreplyPending = true; sendAllQueryReplies(); break; case SF_RP_QLIST: + qreplyPending = true; if (fieldLen >= 6) { int listType = data[offset + 5] & 0xFF; log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType)); @@ -1236,14 +1264,16 @@ public class DataStreamProcessor { sendAllQueryReplies(); } break; + case CMD_RMA: case SNA_CMD_RMA: - processReadModified(true); + processReadModified(AID_QREPLY, true); break; case SNA_CMD_RB: - processReadBuffer(); + processReadBuffer(AID_QREPLY); break; + case CMD_RM: case SNA_CMD_RM: - processReadModified(false); + processReadModified(AID_QREPLY, false); break; default: log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type)); @@ -1674,6 +1704,14 @@ public class DataStreamProcessor { processReadModified(true); } + public boolean isQreplyPending() { + return qreplyPending; + } + + public void setQreplyPending(boolean qreplyPending) { + this.qreplyPending = qreplyPending; + } + public void processWriteStructuredField(byte[] data) { if (data != null && data.length > 0) { processWriteStructuredField(data, 0, data.length); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DataStreamProcessorTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DataStreamProcessorTest.java index 9b8da4d..947712d 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DataStreamProcessorTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DataStreamProcessorTest.java @@ -271,4 +271,91 @@ public class DataStreamProcessorTest { assertEquals('8', screen.getCell(12).ucs4); assertEquals('9', screen.getCell(13).ucs4); } + + @Test + public void testReadPartitionReadModifiedReturnsAidQreply() { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + processor.setOutputSender(out::writeBytes); + + // Send WSF Read Partition with SNA_CMD_RM (0xF6) + byte[] sfRm = new byte[] { + (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RM + }; + out.reset(); + processor.processRecord(sfRm, 0, sfRm.length, true); + + byte[] sent = out.toByteArray(); + assertTrue(sent.length >= 3, "Inbound data stream must contain at least AID and cursor address"); + assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified AID must be AID_QREPLY (0x61)"); + + // Send WSF Read Partition with CMD_RM (0x06) + byte[] sfRmNonSna = new byte[] { + (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) CMD_RM + }; + out.reset(); + processor.processRecord(sfRmNonSna, 0, sfRmNonSna.length, true); + + sent = out.toByteArray(); + assertTrue(sent.length >= 3); + assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified (non-SNA) AID must be AID_QREPLY (0x61)"); + } + + @Test + public void testReadPartitionReadModifiedAllReturnsAidQreply() { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + processor.setOutputSender(out::writeBytes); + + // Send WSF Read Partition with SNA_CMD_RMA (0x6E) + byte[] sfRma = new byte[] { + (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RMA + }; + out.reset(); + processor.processRecord(sfRma, 0, sfRma.length, true); + + byte[] sent = out.toByteArray(); + assertTrue(sent.length >= 3); + assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Modified All AID must be AID_QREPLY (0x61)"); + } + + @Test + public void testReadPartitionReadBufferReturnsAidQreply() { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + processor.setOutputSender(out::writeBytes); + + // Send WSF Read Partition with SNA_CMD_RB (0xF2) + byte[] sfRb = new byte[] { + (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SNA_CMD_RB + }; + out.reset(); + processor.processRecord(sfRb, 0, sfRb.length, true); + + byte[] sent = out.toByteArray(); + assertTrue(sent.length >= 3); + assertEquals((byte) AID_QREPLY, sent[0], "Read Partition Read Buffer AID must be AID_QREPLY (0x61)"); + } + + @Test + public void testReadModifiedAfterQueryRespondsWithQueryReply() { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + processor.setOutputSender(out::writeBytes); + + // 1. Send WSF Read Partition Query + byte[] sfQuery = new byte[] { + (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, (byte) 0xFF, (byte) SF_RP_QUERY + }; + out.reset(); + processor.processRecord(sfQuery, 0, sfQuery.length, true); + + assertTrue(processor.isQreplyPending(), "qreplyPending should be true after query"); + + // 2. Mainframe issues channel Read Modified command (0x06) to poll the query reply + byte[] rmCmd = new byte[] { (byte) CMD_RM }; + out.reset(); + processor.processRecord(rmCmd, 0, rmCmd.length, true); + + byte[] sent = out.toByteArray(); + assertTrue(sent.length > 0, "Should have received query reply data"); + assertEquals((byte) AID_SF, sent[0], "Query reply header AID must be AID_SF (0x88)"); + assertFalse(processor.isQreplyPending(), "qreplyPending should be cleared after reply is sent"); + } }