package haus.nightmare.lib3270j.datastream; import org.junit.jupiter.api.Test; import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.charset.EbcdicTranslator; import haus.nightmare.lib3270j.graphics.GraphicsMode; import haus.nightmare.lib3270j.screen.ScreenBuffer; import static org.junit.jupiter.api.Assertions.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; public class QueryReplyBuilderTest { private final EbcdicTranslator translator = new EbcdicTranslator(); private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); @Test public void testBuildAllQueryRepliesDefaultBoth() { assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode()); byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); assertNotNull(replies); assertTrue(replies.length > 0); assertEquals((byte) AID_SF, replies[0]); // In base query reply (buildAllQueryReplies), Summary (0x80) advertises QR_SEGMENT (0xB0) // while the base reply itself contains base SFs (Usable Area, Charsets, Color, Highlighting, Reply Modes, DDM, AuxDA, ImpPart) assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY assertEquals(QR_SUMMARY, replies[4] & 0xFF); // 0x80 // In buildCompleteQueryReplies, Vector Graphics QR 0xB0 structured field payload is present byte[] completeReplies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); boolean hasB0 = false; for (int i = 0; i < completeReplies.length - 3; i++) { if ((completeReplies[i] & 0xFF) == 0x81 && (completeReplies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; break; } } assertTrue(hasB0); } @Test public void testBuildAllQueryRepliesExplicitNone() { qrBuilder.setGraphicsMode(GraphicsMode.NONE); byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); assertNotNull(replies); assertTrue(replies.length > 0); assertEquals((byte) AID_SF, replies[0]); // In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); boolean hasB0 = false; boolean hasA8 = false; for (int i = 5; i < 1 + sumLen; i++) { if ((replies[i] & 0xFF) == QR_SEGMENT) hasB0 = true; if ((replies[i] & 0xFF) == QR_TRANSPARENCY) hasA8 = true; } assertFalse(hasB0); assertTrue(hasA8, "QR_TRANSPARENCY (0xA8) should be advertised even in GraphicsMode.NONE"); // And QR_TRANSPARENCY structured field payload must be present in replies boolean hasA8Payload = false; for (int i = 0; i < replies.length - 3; i++) { if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_TRANSPARENCY) { hasA8Payload = true; break; } } assertTrue(hasA8Payload, "QR_TRANSPARENCY structured field should be included in buildAllQueryReplies"); } @Test public void testBuildAllQueryRepliesWithVectorGraphics() { qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS); byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); assertNotNull(replies); // In complete replies, Vector Graphics QR 0xB0 and 0xB4 must be present boolean hasB0 = false; boolean hasB4 = false; for (int i = 0; i < replies.length - 3; i++) { if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; } if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRCOLOR) { hasB4 = true; } } assertTrue(hasB0); assertTrue(hasB4); } @Test public void testBuildAllQueryRepliesWithBoth() { qrBuilder.setGraphicsMode(GraphicsMode.BOTH); byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); assertNotNull(replies); // Vector Graphics (0xB0) must be present and Charsets must be present (0x85) boolean hasB0 = false; boolean hasCharsets = false; for (int i = 0; i < replies.length - 3; i++) { if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; } if ((replies[i] & 0xFF) == 0x81 && (completeReplyIsCharsets(replies, i))) { hasCharsets = true; } } assertTrue(hasB0); assertTrue(hasCharsets); } private boolean completeReplyIsCharsets(byte[] replies, int i) { return (replies[i + 1] & 0xFF) == QR_CHARSETS; } @Test public void testBuildAllQueryRepliesWithProgrammedSymbols() { qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS); byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); assertNotNull(replies); // Charsets with LoadPS (0x0A) must be present, and QR_SEGMENT must NOT be present boolean hasB0 = false; boolean hasCharsetsWithLoadPs = false; for (int i = 0; i < replies.length - 3; i++) { if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; } if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) { if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) { hasCharsetsWithLoadPs = true; } } } assertFalse(hasB0); assertTrue(hasCharsetsWithLoadPs); } @Test public void testBuildFilteredQueryRepliesWithSupportedCodes() { byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); assertEquals((byte) AID_SF, replies[0]); // First SF should be DDM: length (2 bytes), SFID_QREPLY (0x81), QR_DDM (0x95) int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); assertEquals(0x81, replies[3] & 0xFF); assertEquals(QR_DDM, replies[4] & 0xFF); // Second SF should be Usable Area: 23 bytes (4 bytes header + 19 bytes payload) int pos2 = 1 + len1; int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF); assertEquals(23, len2); assertEquals(0x81, replies[pos2 + 2] & 0xFF); assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF); // Offset 0x15 (21 in SF payload = pos2 + 21): Buffer size high int bufSize = ((replies[pos2 + 21] & 0xFF) << 8) | (replies[pos2 + 22] & 0xFF); assertEquals(80 * 43, bufSize); } @Test public void testBuildFilteredQueryRepliesWithUnsupportedCode() { byte[] requested = new byte[] { (byte) 0x77 }; // Unsupported code byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); assertEquals((byte) AID_SF, replies[0]); // Should emit QR_NULL (0xFF) assertEquals(0x81, replies[3] & 0xFF); assertEquals(QR_NULL, replies[4] & 0xFF); } @Test public void testImplicitPartitionModel4() { byte[] requested = new byte[] { (byte) QR_IMP_PART }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); assertEquals((byte) AID_SF, replies[0]); int len = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY assertEquals(QR_IMP_PART, replies[4] & 0xFF); // 0xA6 // Payload starts at index 5: 22 bytes total // Default size = 80 x 24 int defCols = ((replies[10] & 0xFF) << 8) | (replies[11] & 0xFF); int defRows = ((replies[12] & 0xFF) << 8) | (replies[13] & 0xFF); assertEquals(80, defCols); assertEquals(24, defRows); // Alt size = 80 x 43 int altCols = ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF); int altRows = ((replies[16] & 0xFF) << 8) | (replies[17] & 0xFF); assertEquals(80, altCols); assertEquals(43, altRows); } @Test public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() { qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS); byte[] requested = new byte[] { (byte) QR_CHARSETS }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); int offset = -1; for (int i = 0; i < replies.length - 3; i++) { if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) { offset = i + 2; break; } } assertTrue(offset >= 0, "QR_CHARSETS must be present"); // QR_CHARSETS payload structure: // Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte) // DL is at offset + 8, and is 7 bytes per descriptor. int dl = replies[offset + 8] & 0xFF; assertEquals(7, dl); int descOffset = offset + 9; // Descriptor 1: SET 0 (Base) -> flags 0x10 assertEquals(0x00, replies[descOffset] & 0xFF); assertEquals(0x10, replies[descOffset + 1] & 0xFF); // Descriptor 2: SET 1 (APL) -> flags 0x00 assertEquals(0x01, replies[descOffset + 7] & 0xFF); assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF); // Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane) assertEquals(0x02, replies[descOffset + 14] & 0xFF); assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF); // Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane) assertEquals(0x03, replies[descOffset + 21] & 0xFF); assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF); // Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) assertEquals(0x04, replies[descOffset + 28] & 0xFF); assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF); // Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) assertEquals(0x05, replies[descOffset + 35] & 0xFF); assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF); // Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) assertEquals(0x06, replies[descOffset + 42] & 0xFF); assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF); // Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane) assertEquals(0x07, replies[descOffset + 49] & 0xFF); assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF); } @Test public void testQueryReplyImageAndRpqNames() { qrBuilder.setGraphicsMode(GraphicsMode.BOTH); byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_TRANSPARENCY, (byte) QR_PROCEDURE }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); assertEquals((byte) AID_SF, replies[0]); // First SF should be QR_NULL (0xFF) because 0x82 is 3270 Image SF (not supported on 3179G) int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF // Second SF should be QR_TRANSPARENCY (0xA8) matching requested code int pos2 = 1 + len1; int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF); assertEquals(0x81, replies[pos2 + 2] & 0xFF); assertEquals(QR_TRANSPARENCY, replies[pos2 + 3] & 0xFF); // 0xA8 // Third SF should be QR_PROCEDURE (0xB1) int pos3 = pos2 + len2; assertEquals(0x81, replies[pos3 + 2] & 0xFF); assertEquals(QR_PROCEDURE, replies[pos3 + 3] & 0xFF); // 0xB1 } @Test public void testUsableAreaMetricsMatchHOD() { qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS); byte[] requested = new byte[] { (byte) QR_USABLE_AREA }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); assertEquals((byte) AID_SF, replies[0]); // Length (2 bytes), SFID (0x81), QR_USABLE_AREA (0x81) assertEquals(23, ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF)); assertEquals(0x81, replies[3] & 0xFF); assertEquals(QR_USABLE_AREA, replies[4] & 0xFF); // Flags: 12/14 bit addressing | Graphics (0x03), 0x00 assertEquals(0x03, replies[5] & 0xFF); assertEquals(0x00, replies[6] & 0xFF); // Usable width and height: 80, 43 assertEquals(80, ((replies[7] & 0xFF) << 8) | (replies[8] & 0xFF)); assertEquals(43, ((replies[9] & 0xFF) << 8) | (replies[10] & 0xFF)); // Units: 0x00 (Inches, matching IBM Host On-Demand DS3270.java) assertEquals(0x00, replies[11] & 0xFF); // Xr (4 bytes): 0x00010060 (96 dpi matching HOD) int xr = ((replies[12] & 0xFF) << 24) | ((replies[13] & 0xFF) << 16) | ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF); assertEquals(0x00010060, xr); // Yr (4 bytes): 0x00010060 (96 dpi matching HOD) int yr = ((replies[16] & 0xFF) << 24) | ((replies[17] & 0xFF) << 16) | ((replies[18] & 0xFF) << 8) | (replies[19] & 0xFF); assertEquals(0x00010060, yr); // AW and AH: 9 and 16 assertEquals(9, replies[20] & 0xFF); assertEquals(16, replies[21] & 0xFF); // Buffer size: 80 * 43 = 3440 assertEquals(80 * 43, ((replies[22] & 0xFF) << 8) | (replies[23] & 0xFF)); } @Test public void testGrSymbolSetMatchesHOD() { qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS); byte[] requested = new byte[] { (byte) QR_GRSYMBOLSET }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); assertEquals((byte) AID_SF, replies[0]); assertEquals(33, ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF)); assertEquals(0x81, replies[3] & 0xFF); assertEquals(QR_GRSYMBOLSET, replies[4] & 0xFF); } }