diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index 5568da7..eba175c 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -221,8 +221,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { int ox = getRenderOffsetX(); int oy = getRenderOffsetY(); ScreenBuffer sb = client.getScreenBuffer(); - int gridW = sb.getDisplayCols() * cellWidth; - int gridH = sb.getDisplayRows() * cellHeight; + int gridW = (sb != null ? sb.getDisplayCols() : 80) * cellWidth; + int gridH = (sb != null ? sb.getDisplayRows() : 24) * cellHeight; int gWidth = client.getGraphicsPlane().getCanvasWidth(); int gHeight = client.getGraphicsPlane().getCanvasHeight(); int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox); @@ -253,6 +253,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { if (isGraphic) { clearSelection(); + sb.setCursorAddress(clickAddr); int gridW = displayCols * cellWidth; int gridH = displayRows * cellHeight; int gWidth = client.getGraphicsPlane().getCanvasWidth(); @@ -995,11 +996,17 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { double curX = x; double curY = y; + if (dir == GocaConstants.CD_TB) { + curY += ch; + } else if (dir == GocaConstants.CD_RL) { + curX -= cw; + } + for (int i = 0; i < text.length(); i++) { String s = text.substring(i, i + 1); int charW = fm.stringWidth(s); int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0)); - int drawY = (int) Math.round(curY + ascent + Math.max(0, (ch - fm.getHeight()) / 2.0)); + int drawY = (int) Math.round(curY); g2.drawString(s, drawX, drawY); switch (dir) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java index c14f72a..896a9d5 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java @@ -81,6 +81,10 @@ public class EbcdicTranslator { return activeCodePage.isDBCS(); } + public synchronized boolean isDBCS() { + return isDBCSCodePage(); + } + /** * Translate double-byte EBCDIC pair (b1, b2) to Unicode. */ 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 8f8d6b6..0352ec5 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -1020,7 +1020,7 @@ public class DataStreamProcessor { switch (type) { case SF_RP_QUERY: - log.info("ReadPartition Query — sending all query replies"); + log.info("ReadPartition Query — sending base query replies"); graphicsPlane.clear(); gocaDecoder.resetDefaults(); sendAllQueryReplies(); @@ -1029,7 +1029,9 @@ public class DataStreamProcessor { if (fieldLen >= 6) { int listType = data[offset + 5] & 0xFF; log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType)); - if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) { + if (listType == SF_RPQ_ALL) { + sendCompleteQueryReplies(); + } else if (listType == SF_RPQ_EQUIV) { sendAllQueryReplies(); } else if (listType == SF_RPQ_LIST) { // Send only requested query replies @@ -1071,7 +1073,22 @@ public class DataStreamProcessor { if ((i + 1) % 32 == 0) sb.append("\n "); } - log.warning(">>> SENDING Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim()); + log.warning(">>> SENDING Base Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim()); + if (outputSender != null) { + outputSender.send3270Data(qr); + } + } + + private void sendCompleteQueryReplies() { + byte[] qr = qrBuilder.buildCompleteQueryReplies(screen.getMaxCols(), screen.getMaxRows(), + screen.getMaxCols() * screen.getMaxRows()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < qr.length; i++) { + sb.append(String.format("%02x ", qr[i] & 0xFF)); + if ((i + 1) % 32 == 0) + sb.append("\n "); + } + log.warning(">>> SENDING Complete Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim()); if (outputSender != null) { outputSender.send3270Data(qr); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java index 4cb35e2..2ca7b47 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java @@ -15,16 +15,17 @@ public class QueryReplyBuilder { private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName()); - // Canned values from 3279-2 (matching sf.c) private static final int SW_3279_2 = 0x09; private static final int SH_3279_2 = 0x0c; - private static final int Xr_3279_2 = 0x000a02e5; - private static final int Yr_3279_2 = 0x0002006f; + + // Usable Area physical dimensions matching IBM Host On-Demand QR_USEAREA_STRING (Inches) + private static final int Xr_HOD = 0x00020089; + private static final int Yr_HOD = 0x00020085; private final ScreenBuffer screen; private GraphicsMode graphicsMode = GraphicsMode.BOTH; - // Base query reply codes (text mode) + // Base query reply codes (text mode, matches HOD DS3270.java queryEquiv) private static final int[] SUPPORTED_QR_BASE = { QR_SUMMARY, // 0x80 — summary must list itself QR_USABLE_AREA, // 0x81 @@ -34,10 +35,11 @@ public class QueryReplyBuilder { QR_HIGHLIGHTING, // 0x87 QR_REPLY_MODES, // 0x88 QR_DDM, // 0x95 - Distributed Data Management (file transfer) - QR_IMP_PART, // 0xa6 + QR_AUXDA, // 0x99 - Auxiliary Devices + QR_IMP_PART, // 0xa6 - Implicit Partition Sizes }; - // Vector graphics query reply codes matching HOD DS3270.java line 1723 + // Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING private static final int[] SUPPORTED_QR_VECTOR = { QR_SUMMARY, // 0x80 QR_USABLE_AREA, // 0x81 @@ -46,17 +48,17 @@ public class QueryReplyBuilder { QR_COLOR, // 0x86 QR_HIGHLIGHTING, // 0x87 QR_REPLY_MODES, // 0x88 - QR_SAVE_RESTORE, // 0x8c + QR_OUTLINING, // 0x8c QR_DDM, // 0x95 - QR_TRANSPARENCY, // 0x99 + QR_AUXDA, // 0x99 QR_IMP_PART, // 0xa6 - QR_RPQ_NAMES, // 0xa8 - QR_GRAPHICS, // 0xb0 - QR_GIMAGE, // 0xb1 - QR_AUX_DEV, // 0xb2 - QR_OEM_FMT, // 0xb3 - QR_GCOLOR, // 0xb4 - QR_GSYMBOLS, // 0xb6 + QR_TRANSPARENCY, // 0xa8 + QR_SEGMENT, // 0xb0 + QR_PROCEDURE, // 0xb1 + QR_LINETYPE, // 0xb2 + QR_PORT, // 0xb3 + QR_GRCOLOR, // 0xb4 + QR_GRSYMBOLSET, // 0xb6 }; public QueryReplyBuilder(ScreenBuffer screen) { @@ -77,7 +79,8 @@ public class QueryReplyBuilder { } /** - * Build all query replies as a single AID_SF + structured field response. + * Build base query replies in response to a generic Read Partition Query (0x02). + * Returns base text/presentation summary structured fields (matches HOD DS3270.java line 1723). */ public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) { ByteArrayOutputStream out = new ByteArrayOutputStream(512); @@ -85,62 +88,90 @@ public class QueryReplyBuilder { // AID byte for structured field out.write(AID_SF); - // Summary + // Summary (0x80) - lists all supported capabilities appendQueryReply(out, QR_SUMMARY, buildSummary()); - // Usable Area + // Usable Area (0x81) appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); - // Alpha Partitions + // Alpha Partitions (0x84) appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); - // Character Sets + // Character Sets (0x85) appendQueryReply(out, QR_CHARSETS, buildCharsets()); - // Color + // Color (0x86) appendQueryReply(out, QR_COLOR, buildColor()); - // Highlighting + // Highlighting (0x87) appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); // Reply Modes (0x88) appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); - if (graphicsMode.isVectorGraphicsEnabled()) { - // Save/Restore (0x8C) - appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); + boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); + if (isDbcs) { + // Outlining (0x8C) + appendQueryReply(out, QR_OUTLINING, buildOutlining()); + // DBCS Asia (0x91) + appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); } // Distributed Data Management (0x95) appendQueryReply(out, QR_DDM, buildDdm(4096)); - if (graphicsMode.isVectorGraphicsEnabled()) { - // Transparency (0x99) - appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); - } + // Auxiliary Devices (0x99) + appendQueryReply(out, QR_AUXDA, buildAuxDa()); // Implicit Partition (0xA6) appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); - // Vector Graphics QRs if enabled + log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")"); + return out.toByteArray(); + } + + /** + * Build complete query replies including vector graphics (when SF_RPQ_ALL 0x80 is requested). + */ + public byte[] buildCompleteQueryReplies(int maxCols, int maxRows, int bufferSize) { + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + out.write(AID_SF); + + appendQueryReply(out, QR_SUMMARY, buildSummary()); + appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); + appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); + appendQueryReply(out, QR_CHARSETS, buildCharsets()); + appendQueryReply(out, QR_COLOR, buildColor()); + appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); + appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); + + appendQueryReply(out, QR_OUTLINING, buildOutlining()); + boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); + if (isDbcs) { + appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); + } + appendQueryReply(out, QR_DDM, buildDdm(4096)); + appendQueryReply(out, QR_AUXDA, buildAuxDa()); + appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); + if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8 - appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0 - appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1 - appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2 - appendOemFmt(out); // 0xB3 - appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4 - appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6 + appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8 + appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0 + appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1 + appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2 + appendPort(out); // 0xB3 + appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4 + appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6 } - log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")"); + log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")"); return out.toByteArray(); } /** * Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST). * For any unsupported requested query code, emits a QR_NULL (0xFF) structured field - * matching x3270 sf.c behavior. + * matching HOD DS3270.java line 1835. */ public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) { if (requestedCodes == null || requestedCodes.length == 0) { @@ -174,72 +205,70 @@ public class QueryReplyBuilder { case QR_REPLY_MODES: appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); break; - case QR_SAVE_RESTORE: - if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); + case QR_OUTLINING: // 0x8C + appendQueryReply(out, QR_OUTLINING, buildOutlining()); + break; + case QR_DBCS_ASIA: // 0x91 + if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { + appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_DDM: + case QR_DDM: // 0x95 appendQueryReply(out, QR_DDM, buildDdm(4096)); break; - case QR_TRANSPARENCY: + case QR_AUXDA: // 0x99 + appendQueryReply(out, QR_AUXDA, buildAuxDa()); + break; + case QR_IMP_PART: // 0xA6 + appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); + break; + case QR_TRANSPARENCY: // 0xA8 if (graphicsMode.isVectorGraphicsEnabled()) { appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_IMP_PART: - appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); - break; - case QR_RPQ_NAMES: - case QR_RPQNAMES: + case QR_SEGMENT: // 0xB0 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, code, buildRpqNames()); + appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_GRAPHICS: + case QR_PROCEDURE: // 0xB1 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); + appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_GIMAGE: + case QR_LINETYPE: // 0xB2 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); + appendQueryReply(out, QR_LINETYPE, buildLineType()); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_AUX_DEV: + case QR_PORT: // 0xB3 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); + appendPort(out); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_OEM_FMT: + case QR_GRCOLOR: // 0xB4 if (graphicsMode.isVectorGraphicsEnabled()) { - appendOemFmt(out); + appendQueryReply(out, QR_GRCOLOR, buildGrColor()); } else { appendQueryReply(out, QR_NULL, new byte[0]); } break; - case QR_GCOLOR: + case QR_GRSYMBOLSET: // 0xB6 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_GCOLOR, buildGColor()); - } else { - appendQueryReply(out, QR_NULL, new byte[0]); - } - break; - case QR_GSYMBOLS: - if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); + appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); } else { appendQueryReply(out, QR_NULL, new byte[0]); } @@ -268,8 +297,12 @@ public class QueryReplyBuilder { private byte[] buildSummary() { ByteArrayOutputStream out = new ByteArrayOutputStream(); int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; + boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); for (int code : codes) { out.write(code); + if (isDbcs && code == QR_OUTLINING) { + out.write(QR_DBCS_ASIA); // 0x91 + } } return out.toByteArray(); } @@ -282,17 +315,17 @@ public class QueryReplyBuilder { out.write(maxCols & 0xFF); // usable width low out.write((maxRows >> 8) & 0xFF); // usable height high out.write(maxRows & 0xFF); // usable height low - out.write(0x01); // units (mm) - // Xr (4 bytes) - canned from 3279-2 - out.write((Xr_3279_2 >> 24) & 0xFF); - out.write((Xr_3279_2 >> 16) & 0xFF); - out.write((Xr_3279_2 >> 8) & 0xFF); - out.write(Xr_3279_2 & 0xFF); - // Yr (4 bytes) - canned from 3279-2 - out.write((Yr_3279_2 >> 24) & 0xFF); - out.write((Yr_3279_2 >> 16) & 0xFF); - out.write((Yr_3279_2 >> 8) & 0xFF); - out.write(Yr_3279_2 & 0xFF); + out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) + // Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING + out.write((Xr_HOD >> 24) & 0xFF); + out.write((Xr_HOD >> 16) & 0xFF); + out.write((Xr_HOD >> 8) & 0xFF); + out.write(Xr_HOD & 0xFF); + // Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING + out.write((Yr_HOD >> 24) & 0xFF); + out.write((Yr_HOD >> 16) & 0xFF); + out.write((Yr_HOD >> 8) & 0xFF); + out.write(Yr_HOD & 0xFF); int charW = getCharWidth(); int charH = getCharHeight(); out.write(charW); // AW @@ -304,6 +337,9 @@ public class QueryReplyBuilder { } public int getCharWidth() { + if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { + return 12; + } return SW_3279_2; // 9 } @@ -458,7 +494,28 @@ public class QueryReplyBuilder { return out.toByteArray(); } - private byte[] buildGraphics(int maxCols, int maxRows) { + public byte[] buildOutlining() { + // HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000") + return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + } + + public byte[] buildDbcsAsia() { + // HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001") + return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 }; + } + + public byte[] buildAuxDa() { + // HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000") + return new byte[]{ 0x00, 0x00 }; + } + + public byte[] buildTransparency() { + // HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff") + return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }; + } + + public byte[] buildSegment(int maxCols, int maxRows) { + // HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000") return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, @@ -467,7 +524,12 @@ public class QueryReplyBuilder { }; } - private byte[] buildGImage(int maxCols, int maxRows) { + public byte[] buildGraphics(int maxCols, int maxRows) { + return buildSegment(maxCols, maxRows); + } + + public byte[] buildProcedure(int maxCols, int maxRows) { + // HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0") return new byte[]{ 0x00, 0x01, 0x00, 0x00, @@ -478,11 +540,12 @@ public class QueryReplyBuilder { }; } - public byte[] buildAuxDevice() { - return buildAuxDev(); + public byte[] buildGImage(int maxCols, int maxRows) { + return buildProcedure(maxCols, maxRows); } - private byte[] buildAuxDev() { + public byte[] buildLineType() { + // HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b") return new byte[]{ 0x00, 0x09, 0x00, 0x07, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, @@ -490,38 +553,36 @@ public class QueryReplyBuilder { }; } - private byte[] buildSaveRestore() { - // HOD DS3270.java line 1768: 6 bytes payload - return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + public byte[] buildAuxDev() { + return buildLineType(); } - private byte[] buildTransparency() { - // HOD DS3270.java line 1782: 2 bytes payload - return new byte[]{ 0x00, 0x00 }; + public byte[] buildAuxDevice() { + return buildLineType(); } - private byte[] buildRpqNames() { - // HOD DS3270.java line 1798: 5 bytes payload - return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }; - } - - private void appendOemFmt(ByteArrayOutputStream out) { - // HOD DS3270.java line 1814: 4 distinct OEM format sub-fields - appendQueryReply(out, QR_OEM_FMT, new byte[]{ + public void appendPort(ByteArrayOutputStream out) { + // HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total) + appendQueryReply(out, QR_PORT, new byte[]{ 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF }); - appendQueryReply(out, QR_OEM_FMT, new byte[]{ + appendQueryReply(out, QR_PORT, new byte[]{ 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 }); - appendQueryReply(out, QR_OEM_FMT, new byte[]{ + appendQueryReply(out, QR_PORT, new byte[]{ 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF }); - appendQueryReply(out, QR_OEM_FMT, new byte[]{ + appendQueryReply(out, QR_PORT, new byte[]{ 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C }); } - private byte[] buildGColor() { + public void appendOemFmt(ByteArrayOutputStream out) { + appendPort(out); + } + + public byte[] buildGrColor() { + // HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) ByteArrayOutputStream out = new ByteArrayOutputStream(110); out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF); out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10); @@ -541,11 +602,36 @@ public class QueryReplyBuilder { return out.toByteArray(); } - private byte[] buildGSymbols() { + public byte[] buildGColor() { + return buildGrColor(); + } + + public byte[] buildGrSymbolSet() { + int charW = getCharWidth(); + int charH = getCharHeight(); + int cgcsgid = 0x02B9; + int cpgid = 0x0025; + if (screen != null && screen.getTranslator() != null) { + cgcsgid = screen.getTranslator().getCgcsgid(); + cpgid = screen.getTranslator().getCpgid(); + } + // HOD QueryReply3270Constants.java QR_GRSYMBOLSET_S_STRING ("\u0000!\u0081\u00b6...") return new byte[]{ - 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12, - 0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, - 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + 0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12, + 0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00, + (byte) charW, (byte) charH, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; } + + public byte[] buildGSymbols() { + return buildGrSymbolSet(); + } + + public byte[] buildSaveRestore() { + return buildOutlining(); + } + + public byte[] buildRpqNames() { + return buildTransparency(); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java index 7a08e5a..3be663f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java @@ -148,6 +148,8 @@ public class GocaDecoder { return graphicCursorY; } + public static final int GDDM_CURSOR_OFFSET_Y = 0; + public synchronized void setGraphicCursorPosition(int x, int y) { this.graphicCursorX = x; this.graphicCursorY = y; @@ -925,11 +927,7 @@ public class GocaDecoder { } break; } - case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position - if (idx + 5 <= end) { - this.graphicCursorX = readCoord(data, idx + 2); - this.graphicCursorY = readCoord(data, idx + 4); - } + case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position (HODGraphicCursorPosition - No-op per HOD architecture) if (idx + 1 < end) { int len = data[idx + 1] & 0xFF; idx += 2 + len; @@ -1372,24 +1370,41 @@ public class GocaDecoder { double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0; double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0; - int totalW = textLen * (charWidth > 0 ? charWidth : 9); - int totalH = (charHeight > 0 ? charHeight : 14); - trackPoint(startX, startY); - trackPoint(startX + totalW, startY + totalH); - trackPoint(startX + totalW, startY - totalH); + int cellW = (charWidth > 0 ? charWidth : 9); + int cellH = (charHeight > 0 ? charHeight : 16); + switch (charDir) { + case GocaConstants.CD_TB: + trackPoint(startX, startY); + trackPoint(startX + cellW, startY - textLen * cellH); + break; + case GocaConstants.CD_RL: + trackPoint(startX, startY); + trackPoint(startX - textLen * cellW, startY + cellH); + break; + case GocaConstants.CD_BT: + trackPoint(startX, startY); + trackPoint(startX + cellW, startY + textLen * cellH); + break; + case GocaConstants.CD_LR: + case GocaConstants.CD_DEFAULT: + default: + trackPoint(startX, startY); + trackPoint(startX + textLen * cellW, startY + cellH); + break; + } if (charSet != 0 && programSymbolManager != null) { for (int i = 0; i < textLen; i++) { int code = data[pos + i] & 0xFF; double px = plane.mapXDouble(startX); - double py = plane.mapYDouble(startY) - ch; + double py = plane.mapYDouble(startY); ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code); if (slot != null) { int[] rgb = slot.getRgbPixels(curColor, 0); int symW = slot.getWidth(); int symH = slot.getHeight(); int ipx = (int) Math.round(px); - int ipy = (int) Math.round(py); + int ipy = (int) Math.round(py - ch); int icw = (int) Math.round(cw); int ich = (int) Math.round(ch); for (int dy = 0; dy < ich; dy++) { @@ -1420,15 +1435,33 @@ public class GocaDecoder { String text = new String(chars); if (charPrecision == GocaConstants.CP_STROKE) { - plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text, + plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text, curColor, cw, ch, charDir, charAngle); } else { - plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text, + plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text, curColor, cw, ch, charDir, charAngle); } - curX = startX + (textLen * (charWidth > 0 ? charWidth : 9)); - curY = startY; + switch (charDir) { + case GocaConstants.CD_TB: + curX = startX; + curY = startY - (textLen * (charHeight > 0 ? charHeight : 16)); + break; + case GocaConstants.CD_RL: + curX = startX - (textLen * (charWidth > 0 ? charWidth : 9)); + curY = startY; + break; + case GocaConstants.CD_BT: + curX = startX; + curY = startY + (textLen * (charHeight > 0 ? charHeight : 16)); + break; + case GocaConstants.CD_LR: + case GocaConstants.CD_DEFAULT: + default: + curX = startX + (textLen * (charWidth > 0 ? charWidth : 9)); + curY = startY; + break; + } } /** diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java index 111371e..dcee093 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -885,10 +885,15 @@ public class GraphicsPlane { if (text == null || text.isEmpty()) return; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; - double curX = x; - double curY = y; double cw = cellWidth > 0 ? cellWidth : 12.0; double ch = cellHeight > 0 ? cellHeight : 20.0; + double curX = x; + double curY = y; + if (dir == GocaConstants.CD_TB) { + curY += ch; + } else if (dir == GocaConstants.CD_RL) { + curX -= cw; + } for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); @@ -942,7 +947,7 @@ public class GraphicsPlane { int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF); int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF); px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw; - py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch; + py[p] = y - ((double) vy / VectorSymbolData.VSS_HEIGHT) * ch; ipx[p] = (int) Math.round(px[p]); ipy[p] = (int) Math.round(py[p]); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index d299250..2ca9f4f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -177,6 +177,10 @@ public class InputProcessor { */ public void sendAid(int aidCode) { System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked); + if (aidCode == AID_SYSREQ) { + sysReq(); + return; + } if (keyboardLocked && aidCode != AID_CLEAR) { System.err.println("Keyboard locked, dropping AID"); return; @@ -358,21 +362,12 @@ public class InputProcessor { if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) { int gx = gocaDecoder.getGraphicCursorX(); int gy = gocaDecoder.getGraphicCursorY(); - int cursorAddr = screen != null ? screen.getCursorAddress() : 0; int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80; + int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24; + int cursorAddr = screen != null ? screen.getCursorAddress() : 0; int row = cursorAddr / cols; int col = cursorAddr % cols; - if (gocaDecoder.getGraphicsPlane() != null) { - int px = gocaDecoder.getGraphicsPlane().mapX(gx); - int py = gocaDecoder.getGraphicsPlane().mapY(gy); - int canvasW = gocaDecoder.getGraphicsPlane().getCanvasWidth(); - int canvasH = gocaDecoder.getGraphicsPlane().getCanvasHeight(); - int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 43; - if (canvasH > 0) row = (py * numRows) / canvasH; - if (canvasW > 0) col = (px * cols) / canvasW; - } - byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput( gx, gy, row, col, button, true, isShift, isCtrl ); @@ -382,8 +377,8 @@ public class InputProcessor { sfHex.append(String.format("%02X ", b & 0xFF)); } log.info(String.format( - "sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]", - gx, gy, row, col, button, isShift, isCtrl, sfHex.toString().trim() + "sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]", + gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim() )); // Structured Field AID (0x88) + 56-byte Graphic Input SF @@ -394,7 +389,7 @@ public class InputProcessor { // Trailing AID + cursor address + modified fields matching HOD DS3270.sendMouseAid out.write(aidCode); - byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + byte[] caddr = encodeAddress(cursorAddr, numRows, cols); out.write(caddr[0] & 0xFF); out.write(caddr[1] & 0xFF); @@ -755,7 +750,11 @@ public class InputProcessor { /** SysReq key. */ public void sysReq() { - reset(); + if (fsm != null && fsm.isTn3270eNegotiated()) { + fsm.handleSysReq(); + } else { + reset(); + } } /** Reset (unlock keyboard, cancel insert mode). */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/protocol/DS3270Constants.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/protocol/DS3270Constants.java index 785d634..9f4d86a 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/protocol/DS3270Constants.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/protocol/DS3270Constants.java @@ -208,29 +208,40 @@ public final class DS3270Constants { public static final int SF_TRANSFER_DATA = 0xd0; // ========== Query Reply codes ========== - public static final int QR_SUMMARY = 0x80; - public static final int QR_USABLE_AREA = 0x81; - public static final int QR_IMAGE = 0x82; - public static final int QR_TEXT_PART = 0x83; - public static final int QR_ALPHA_PART = 0x84; - public static final int QR_CHARSETS = 0x85; - public static final int QR_COLOR = 0x86; - public static final int QR_HIGHLIGHTING = 0x87; - public static final int QR_REPLY_MODES = 0x88; - public static final int QR_SAVE_RESTORE = 0x8c; - public static final int QR_DBCS_ASIA = 0x91; - public static final int QR_DDM = 0x95; - public static final int QR_TRANSPARENCY = 0x99; - public static final int QR_RPQNAMES = 0xa1; - public static final int QR_IMP_PART = 0xa6; - public static final int QR_RPQ_NAMES = 0xa8; - public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics - public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes - public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device - public static final int QR_OEM_FMT = 0xb3; // OEM Format - public static final int QR_GCOLOR = 0xb4; // Graphic Color Table - public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets - public static final int QR_NULL = 0xff; + public static final int QR_SUMMARY = 0x80; // Summary + public static final int QR_USABLE_AREA = 0x81; // Usable Area + public static final int QR_IMAGE = 0x82; // Image (non-GOCA) + public static final int QR_TEXT_PART = 0x83; // Text Partitions + public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions + public static final int QR_CHARSETS = 0x85; // Character Sets + public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color + public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting + public static final int QR_REPLY_MODES = 0x88; // Reply Modes + public static final int QR_OUTLINING = 0x8c; // Field Outlining + public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING + public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia + public static final int QR_DDM = 0x95; // Distributed Data Management + public static final int QR_AUXDA = 0x99; // Auxiliary Devices + public static final int QR_FILE = 0x9f; // File Transfer + public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer) + public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy) + public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes + public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART + public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency + public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8 + public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics) + public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT + public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA) + public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE + public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev) + public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE + public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format) + public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT + public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table + public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR + public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets + public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET + public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported) // ========== Screen model sizes ========== public static final int MODEL_2_ROWS = 24; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java index a03d7d9..544e664 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java @@ -229,6 +229,11 @@ public class ScreenBuffer { this.cursorAddress = addr; this.displayCursorAddress = addr; } + public synchronized void setCursorPosition(int row, int col) { + int r = Math.max(0, Math.min(row, rows - 1)); + int c = Math.max(0, Math.min(col, cols - 1)); + setCursorAddress(r * cols + c); + } public int getCursorRow() { return cursorAddress / cols; } public int getCursorCol() { return cursorAddress % cols; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java index 34fa892..95d3f9d 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -402,11 +402,7 @@ public class TelnetFSM { } else if (!myOpts[opt]) { myOpts[opt] = true; sendCommand(WILL, opt); - // Start TN3270E sub-negotiation: send device type request - if (!tn3270eDeviceTypeSent) { - sendTN3270EDeviceTypeRequest(); - tn3270eDeviceTypeSent = true; - } + tn3270eDeviceTypeSent = false; } break; @@ -863,25 +859,52 @@ public class TelnetFSM { changeState(ConnectionState.CONNECTED_TN3270E); tn3270eSubmode = TN3270ESubmode.E_3270; } - dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true); - notifyScreenUpdate(); - } - // Send positive response if required - if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { - sendTN3270EPositiveResponse(seqNumber); + try { + dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true); + notifyScreenUpdate(); + // Send positive response if required + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + } catch (Exception e) { + log.log(Level.WARNING, "Error processing 3270 record", e); + if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) { + sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK); + } + } + } else { + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } } break; case DT_SSCP_LU_DATA: - if (connectionState == ConnectionState.CONNECTED_UNBOUND) { - // Clear screen on first SSCP-LU transition to remove stale data - screenBuffer.clear(); + if (connectionState != ConnectionState.CONNECTED_SSCP) { + if (connectionState == ConnectionState.CONNECTED_UNBOUND) { + // Clear screen on first SSCP-LU transition to remove stale data + screenBuffer.clear(); + } changeState(ConnectionState.CONNECTED_SSCP); tn3270eSubmode = TN3270ESubmode.E_SSCP; } if (data.length > EH_SIZE) { - dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE); - notifyScreenUpdate(); + try { + dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE); + notifyScreenUpdate(); + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + } catch (Exception e) { + log.log(Level.WARNING, "Error processing SSCP-LU record", e); + if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) { + sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK); + } + } + } else { + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } } break; @@ -899,7 +922,21 @@ public class TelnetFSM { changeState(ConnectionState.CONNECTED_E_NVT); tn3270eSubmode = TN3270ESubmode.E_NVT; if (data.length > EH_SIZE) { - processNVTData(data, EH_SIZE, data.length - EH_SIZE); + try { + processNVTData(data, EH_SIZE, data.length - EH_SIZE); + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + } catch (Exception e) { + log.log(Level.WARNING, "Error processing NVT record", e); + if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) { + sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK); + } + } + } else { + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } } break; @@ -1393,6 +1430,31 @@ public class TelnetFSM { return tn3270eBound || connectionState == ConnectionState.CONNECTED_3270; } + public boolean isTn3270eBound() { + return tn3270eBound; + } + + public void handleSysReq() { + if (tn3270eNegotiated) { + byte[] ao = new byte[] { (byte) IAC, (byte) AO }; + sendBytes(ao); + screenBuffer.clear(); + screenBuffer.setCursorAddress(0); + screenBuffer.markAllChanged(); + if (dsProcessor != null && dsProcessor.getInputProcessor() != null) { + dsProcessor.getInputProcessor().reset(); + } + if (connectionState != ConnectionState.CONNECTED_SSCP) { + changeState(ConnectionState.CONNECTED_SSCP); + tn3270eSubmode = TN3270ESubmode.E_SSCP; + } else if (tn3270eBound) { + changeState(ConnectionState.CONNECTED_TN3270E); + tn3270eSubmode = TN3270ESubmode.E_3270; + } + notifyScreenUpdate(); + } + } + private static String tn3270eOpName(int op) { switch (op) { case OP_ASSOCIATE: return "ASSOCIATE"; diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java index e451f0a..488693f 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java @@ -22,10 +22,16 @@ public class QueryReplyBuilderTest { assertTrue(replies.length > 0); assertEquals((byte) AID_SF, replies[0]); - // In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present + // 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 < replies.length - 3; i++) { - if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) { + for (int i = 0; i < completeReplies.length - 3; i++) { + if ((completeReplies[i] & 0xFF) == 0x81 && (completeReplies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; break; } @@ -41,31 +47,27 @@ public class QueryReplyBuilderTest { assertTrue(replies.length > 0); assertEquals((byte) AID_SF, replies[0]); - // In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be present - boolean hasB0 = false; - for (int i = 0; i < replies.length - 3; i++) { - if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) { - hasB0 = true; - break; - } + // In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary + int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); + for (int i = 5; i < 1 + sumLen; i++) { + assertNotEquals(QR_SEGMENT, replies[i] & 0xFF); } - assertFalse(hasB0); } @Test public void testBuildAllQueryRepliesWithVectorGraphics() { qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS); - byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); assertNotNull(replies); - // Vector Graphics QR 0xB0 and 0xB4 must be present + // 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_GRAPHICS) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; } - if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRCOLOR) { hasB4 = true; } } @@ -76,17 +78,17 @@ public class QueryReplyBuilderTest { @Test public void testBuildAllQueryRepliesWithBoth() { qrBuilder.setGraphicsMode(GraphicsMode.BOTH); - byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); assertNotNull(replies); // Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A) 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_GRAPHICS) { + 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 ((replies[i] & 0xFF) == 0x81 && (completeReplyIsCharsets(replies, i))) { if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) { hasCharsetsWithLoadPs = true; } @@ -96,17 +98,21 @@ public class QueryReplyBuilderTest { assertTrue(hasCharsetsWithLoadPs); } + 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.buildAllQueryReplies(80, 43, 80 * 43); + byte[] replies = qrBuilder.buildCompleteQueryReplies(80, 43, 80 * 43); assertNotNull(replies); - // Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present + // 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_GRAPHICS) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_SEGMENT) { hasB0 = true; } if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) { @@ -240,7 +246,7 @@ public class QueryReplyBuilderTest { @Test public void testQueryReplyImageAndRpqNames() { qrBuilder.setGraphicsMode(GraphicsMode.BOTH); - byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE }; + byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_TRANSPARENCY, (byte) QR_PROCEDURE }; byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); assertNotNull(replies); @@ -251,15 +257,56 @@ public class QueryReplyBuilderTest { assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF - // Second SF should be QR_RPQNAMES (0xA1) matching requested code + // 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_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1 + assertEquals(QR_TRANSPARENCY, replies[pos2 + 3] & 0xFF); // 0xA8 - // Third SF should be QR_GIMAGE (0xB1) + // Third SF should be QR_PROCEDURE (0xB1) int pos3 = pos2 + len2; assertEquals(0x81, replies[pos3 + 2] & 0xFF); - assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1 + 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 (0x01), 0x00 + assertEquals(0x01, 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 QR_USEAREA_STRING) + assertEquals(0x00, replies[11] & 0xFF); + + // Xr (4 bytes): 0x00020089 (matching HOD) + int xr = ((replies[12] & 0xFF) << 24) | ((replies[13] & 0xFF) << 16) | ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF); + assertEquals(0x00020089, xr); + + // Yr (4 bytes): 0x00020085 (matching HOD) + int yr = ((replies[16] & 0xFF) << 24) | ((replies[17] & 0xFF) << 16) | ((replies[18] & 0xFF) << 8) | (replies[19] & 0xFF); + assertEquals(0x00020085, 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)); } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java new file mode 100644 index 0000000..aabc584 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java @@ -0,0 +1,271 @@ +package haus.nightmare.lib3270j.datastream; + +import org.junit.jupiter.api.Test; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +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 QueryReplyPhase2Test { + + private final EbcdicTranslator translator = new EbcdicTranslator(); + private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); + + /** + * Helper to parse structured fields out of a query reply stream. + */ + private static class ParsedSF { + int length; + int sfid; + int qcode; + byte[] payload; + + ParsedSF(int length, int sfid, int qcode, byte[] payload) { + this.length = length; + this.sfid = sfid; + this.qcode = qcode; + this.payload = payload; + } + } + + private List parseQueryReplies(byte[] data) { + List list = new ArrayList<>(); + assertNotNull(data); + assertTrue(data.length > 0); + assertEquals((byte) AID_SF, data[0]); + + int pos = 1; + while (pos < data.length) { + int len = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF); + assertTrue(len >= 4, "Structured field length must be >= 4 at pos " + pos); + int sfid = data[pos + 2] & 0xFF; + int qcode = data[pos + 3] & 0xFF; + byte[] payload = new byte[len - 4]; + System.arraycopy(data, pos + 4, payload, 0, payload.length); + list.add(new ParsedSF(len, sfid, qcode, payload)); + pos += len; + } + assertEquals(data.length, pos, "All bytes in query reply must be accounted for"); + return list; + } + + @Test + public void testQCodeConstantsValuesMatchHod() { + assertEquals(0x80, QR_SUMMARY); + assertEquals(0x81, QR_USABLE_AREA); + assertEquals(0x84, QR_ALPHA_PART); + assertEquals(0x85, QR_CHARSETS); + assertEquals(0x86, QR_COLOR); + assertEquals(0x87, QR_HIGHLIGHTING); + assertEquals(0x88, QR_REPLY_MODES); + assertEquals(0x8C, QR_OUTLINING); + assertEquals(0x91, QR_DBCS_ASIA); + assertEquals(0x95, QR_DDM); + assertEquals(0x99, QR_AUXDA); + assertEquals(0xA6, QR_IMP_PART); + assertEquals(0xA8, QR_TRANSPARENCY); + assertEquals(0xB0, QR_SEGMENT); + assertEquals(0xB1, QR_PROCEDURE); + assertEquals(0xB2, QR_LINETYPE); + assertEquals(0xB3, QR_PORT); + assertEquals(0xB4, QR_GRCOLOR); + assertEquals(0xB6, QR_GRSYMBOLSET); + assertEquals(0xFF, QR_NULL); + } + + @Test + public void testBaseGenericQueryReplySizeAndStructure() { + qrBuilder.setGraphicsMode(GraphicsMode.BOTH); + byte[] reply = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + assertNotNull(reply); + + List sfs = parseQueryReplies(reply); + // Base query reply must contain exactly 10 structured fields for SBCS: + // 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets), + // 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part) + assertEquals(10, sfs.size()); + + assertEquals(QR_SUMMARY, sfs.get(0).qcode); + assertEquals(QR_USABLE_AREA, sfs.get(1).qcode); + assertEquals(QR_ALPHA_PART, sfs.get(2).qcode); + assertEquals(QR_CHARSETS, sfs.get(3).qcode); + assertEquals(QR_COLOR, sfs.get(4).qcode); + assertEquals(QR_HIGHLIGHTING, sfs.get(5).qcode); + assertEquals(QR_REPLY_MODES, sfs.get(6).qcode); + assertEquals(QR_DDM, sfs.get(7).qcode); + assertEquals(QR_AUXDA, sfs.get(8).qcode); + assertEquals(QR_IMP_PART, sfs.get(9).qcode); + + // Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply + for (ParsedSF sf : sfs) { + assertNotEquals(QR_TRANSPARENCY, sf.qcode); + assertNotEquals(QR_SEGMENT, sf.qcode); + assertNotEquals(QR_PROCEDURE, sf.qcode); + assertNotEquals(QR_LINETYPE, sf.qcode); + assertNotEquals(QR_PORT, sf.qcode); + assertNotEquals(QR_GRCOLOR, sf.qcode); + assertNotEquals(QR_GRSYMBOLSET, sf.qcode); + } + + // Verify summary structured field advertises full capabilities + byte[] summaryPayload = sfs.get(0).payload; + assertTrue(summaryPayload.length >= 17); + assertEquals(QR_SUMMARY, summaryPayload[0] & 0xFF); + assertEquals(QR_USABLE_AREA, summaryPayload[1] & 0xFF); + } + + @Test + public void testTargetedQueryListAdmDrawResponse() { + qrBuilder.setGraphicsMode(GraphicsMode.BOTH); + // ADMDRAW vector graphics startup query list: 0x8C, 0xA8, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB6 + byte[] requested = new byte[] { + (byte) QR_OUTLINING, // 0x8C + (byte) QR_TRANSPARENCY, // 0xA8 + (byte) QR_SEGMENT, // 0xB0 + (byte) QR_PROCEDURE, // 0xB1 + (byte) QR_LINETYPE, // 0xB2 + (byte) QR_PORT, // 0xB3 + (byte) QR_GRCOLOR, // 0xB4 + (byte) QR_GRSYMBOLSET // 0xB6 + }; + + byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); + List sfs = parseQueryReplies(replies); + + // 8 requested QCODEs produce 11 SFs because QR_PORT emits 4 OEM format sub-fields + assertEquals(11, sfs.size()); + + // 1. QR_OUTLINING (0x8C): 10 bytes + assertEquals(QR_OUTLINING, sfs.get(0).qcode); + assertEquals(10, sfs.get(0).length); + assertArrayEquals(new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, sfs.get(0).payload); + + // 2. QR_TRANSPARENCY (0xA8): 9 bytes + assertEquals(QR_TRANSPARENCY, sfs.get(1).qcode); + assertEquals(9, sfs.get(1).length); + assertArrayEquals(new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }, sfs.get(1).payload); + + // 3. QR_SEGMENT (0xB0): 11 bytes + assertEquals(QR_SEGMENT, sfs.get(2).qcode); + assertEquals(11, sfs.get(2).length); + assertArrayEquals(new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 }, sfs.get(2).payload); + + // 4. QR_PROCEDURE (0xB1): 21 bytes + assertEquals(QR_PROCEDURE, sfs.get(3).qcode); + assertEquals(21, sfs.get(3).length); + assertArrayEquals(new byte[]{ + 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00, 0x06, 0x40, 0x06, 0x40, 0x06, 0x01, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0 + }, sfs.get(3).payload); + + // 5. QR_LINETYPE (0xB2): 24 bytes + assertEquals(QR_LINETYPE, sfs.get(4).qcode); + assertEquals(24, sfs.get(4).length); + assertArrayEquals(new byte[]{ + 0x00, 0x09, 0x00, 0x07, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, + 0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08 + }, sfs.get(4).payload); + + // 6-9. QR_PORT (0xB3): 4 subfields (17, 15, 17, 15 bytes) + assertEquals(QR_PORT, sfs.get(5).qcode); + assertEquals(17, sfs.get(5).length); + assertEquals(QR_PORT, sfs.get(6).qcode); + assertEquals(15, sfs.get(6).length); + assertEquals(QR_PORT, sfs.get(7).qcode); + assertEquals(17, sfs.get(7).length); + assertEquals(QR_PORT, sfs.get(8).qcode); + assertEquals(15, sfs.get(8).length); + + // 10. QR_GRCOLOR (0xB4): 109 bytes + assertEquals(QR_GRCOLOR, sfs.get(9).qcode); + assertEquals(109, sfs.get(9).length); + + // 11. QR_GRSYMBOLSET (0xB6): 33 bytes + assertEquals(QR_GRSYMBOLSET, sfs.get(10).qcode); + assertEquals(33, sfs.get(10).length); + } + + @Test + public void testUnsupportedQCodesEmitQrNull() { + qrBuilder.setGraphicsMode(GraphicsMode.BOTH); + byte[] requested = new byte[] { (byte) 0x12, (byte) 0x7E, (byte) 0xEE }; + byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); + + List sfs = parseQueryReplies(replies); + assertEquals(3, sfs.size()); + for (ParsedSF sf : sfs) { + assertEquals(QR_NULL, sf.qcode); + assertEquals(4, sf.length); + assertEquals(0, sf.payload.length); + } + } + + @Test + public void testDynamicCellDimensions() { + // In Vector Graphics mode, SDH must be 16 (0x10) for 3179G + qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS); + assertEquals(9, qrBuilder.getCharWidth()); + assertEquals(16, qrBuilder.getCharHeight()); + + // In Text-only mode (GraphicsMode.NONE), SDH must be 12 (0x0C) for 3279-2 + qrBuilder.setGraphicsMode(GraphicsMode.NONE); + assertEquals(9, qrBuilder.getCharWidth()); + assertEquals(12, qrBuilder.getCharHeight()); + } + + @Test + public void testDataStreamProcessorQueryListDispatch() { + ByteArrayOutputStream captured = new ByteArrayOutputStream(); + DataStreamProcessor processor = new DataStreamProcessor(screen, translator); + processor.setOutputSender(data -> { + try { + captured.write(data); + } catch (Exception e) { + fail(e); + } + }); + + // Test SF_RP_QUERY (0x02): generic query -> base query reply + byte[] genericQuery = new byte[] { + (byte) CMD_WSF, + 0x00, 0x05, // Length = 5 + (byte) SF_READ_PART, 0x00, // Partition = 0 + (byte) SF_RP_QUERY // Type = 0x02 + }; + + captured.reset(); + processor.processRecord(genericQuery, 0, genericQuery.length, false); + byte[] sent = captured.toByteArray(); + assertTrue(sent.length > 0); + assertEquals((byte) AID_SF, sent[0]); + List sfs = parseQueryReplies(sent); + assertEquals(10, sfs.size()); + + // Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4 + byte[] queryList = new byte[] { + (byte) CMD_WSF, + 0x00, 0x08, // Length = 8 + (byte) SF_READ_PART, 0x00, + (byte) SF_RP_QLIST, + (byte) SF_RPQ_LIST, // 0x00 + (byte) QR_SEGMENT, // 0xB0 + (byte) QR_GRCOLOR // 0xB4 + }; + + captured.reset(); + processor.processRecord(queryList, 0, queryList.length, false); + sent = captured.toByteArray(); + assertTrue(sent.length > 0); + List listSfs = parseQueryReplies(sent); + assertEquals(2, listSfs.size()); + assertEquals(QR_SEGMENT, listSfs.get(0).qcode); + assertEquals(QR_GRCOLOR, listSfs.get(1).qcode); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/input/Phase3InteractiveSelectionTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/Phase3InteractiveSelectionTest.java new file mode 100644 index 0000000..e987cb7 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/Phase3InteractiveSelectionTest.java @@ -0,0 +1,272 @@ +package haus.nightmare.lib3270j.input; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.graphics.GddmCoordinateTransform; +import haus.nightmare.lib3270j.graphics.GocaDecoder; +import haus.nightmare.lib3270j.graphics.GraphicsPlane; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.awt.Point; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verification test suite for Phase 3: Interactive Selection, Cursor Addressing & ADMDRAW Alignment. + */ +public class Phase3InteractiveSelectionTest { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + + @BeforeEach + public void setUp() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); + } + + @Test + public void testScreenBufferSetCursorPosition() { + screen.setCursorPosition(5, 12); + assertEquals(5, screen.getCursorRow()); + assertEquals(12, screen.getCursorCol()); + assertEquals(5 * 80 + 12, screen.getCursorAddress()); + + // Test clamping + screen.setCursorPosition(-2, -5); + assertEquals(0, screen.getCursorRow()); + assertEquals(0, screen.getCursorCol()); + assertEquals(0, screen.getCursorAddress()); + + screen.setCursorPosition(100, 200); + assertEquals(23, screen.getCursorRow()); + assertEquals(79, screen.getCursorCol()); + assertEquals(23 * 80 + 79, screen.getCursorAddress()); + } + + @Test + public void testImmediateLightPenSelectionSpaceDesignator() { + // Unprotected selectable field at pos 0 with space designator (0x40) + screen.erase(false); + screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL)); + screen.getCell(1).ec = 0x40; // ' ' + screen.getCell(1).ucs4 = ' '; + screen.getCell(2).ec = (byte) 0xC1; // 'A' + screen.getCell(2).ucs4 = 'A'; + screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + + AtomicReference sentData = new AtomicReference<>(); + InputProcessor input = new InputProcessor(screen, translator, null) { + @Override + protected void sendAidResponse(byte[] data) { + sentData.set(data); + } + }; + + // Click at address 5 inside the field + boolean handled = input.lightPenSelect(5); + assertTrue(handled); + assertEquals(5, screen.getCursorAddress(), "Cursor should be positioned at clicked address"); + assertTrue(faIsModified(screen.getCell(0).fa & 0xFF), "MDT should be set"); + + byte[] sent = sentData.get(); + assertNotNull(sent); + assertEquals((byte) AID_SELECT, sent[0], "Immediate space designator must send AID_SELECT (0x7E)"); + + // Format: AID(1) + CursorAddr(2) + SBA(1) + DesignatorAddr(2) = 6 bytes (NO text content!) + assertEquals(6, sent.length, "AID_SELECT must NOT include field text content"); + assertEquals((byte) ORDER_SBA, sent[3]); + } + + @Test + public void testImmediateLightPenSelectionNullDesignator() { + // Unprotected selectable field at pos 0 with null designator (0x00) + screen.erase(false); + screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL)); + screen.getCell(1).ec = 0x00; // null + screen.getCell(1).ucs4 = 0; + screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + + AtomicReference sentData = new AtomicReference<>(); + InputProcessor input = new InputProcessor(screen, translator, null) { + @Override + protected void sendAidResponse(byte[] data) { + sentData.set(data); + } + }; + + boolean handled = input.lightPenSelect(2); + assertTrue(handled); + assertEquals(2, screen.getCursorAddress()); + assertTrue(faIsModified(screen.getCell(0).fa & 0xFF)); + + byte[] sent = sentData.get(); + assertNotNull(sent); + assertEquals((byte) AID_SELECT, sent[0]); + } + + @Test + public void testImmediateEnterLightPenSelectionAmpersandDesignator() { + // Selectable field at pos 0 with '&' designator (0x50) + screen.erase(false); + screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL)); + screen.getCell(1).ec = 0x50; // '&' + screen.getCell(1).ucs4 = '&'; + screen.getCell(2).ec = (byte) 0xC1; // 'A' + screen.getCell(2).ucs4 = 'A'; + screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + + AtomicReference sentData = new AtomicReference<>(); + InputProcessor input = new InputProcessor(screen, translator, null) { + @Override + protected void sendAidResponse(byte[] data) { + sentData.set(data); + } + }; + + boolean handled = input.lightPenSelect(1); + assertTrue(handled); + assertEquals(1, screen.getCursorAddress()); + assertTrue(faIsModified(screen.getCell(0).fa & 0xFF)); + + byte[] sent = sentData.get(); + assertNotNull(sent); + assertEquals((byte) AID_ENTER, sent[0], "Immediate '&' designator must send AID_ENTER (0x7D)"); + } + + @Test + public void testDeferredLightPenSelectionToggle() { + // Protected selectable field at pos 0 with '?' designator (0x6F) + screen.erase(false); + screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_NORM_SEL)); + screen.getCell(1).ec = 0x6F; // '?' + screen.getCell(1).ucs4 = '?'; + screen.getCell(2).ec = (byte) 0xC3; // 'C' + screen.getCell(2).ucs4 = 'C'; + screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + + AtomicReference sentData = new AtomicReference<>(); + InputProcessor input = new InputProcessor(screen, translator, null) { + @Override + protected void sendAidResponse(byte[] data) { + sentData.set(data); + } + }; + + // First click: toggle '?' -> '>' and set MDT (no transmission) + boolean handled = input.lightPenSelect(1); + assertTrue(handled); + assertEquals((byte) 0x6E, screen.getCell(1).ec, "Designator must change to '>' (0x6E)"); + assertEquals('>', (char) screen.getCell(1).ucs4); + assertTrue(faIsModified(screen.getCell(0).fa & 0xFF), "MDT must be set"); + assertNull(sentData.get(), "Deferred selection must NOT transmit immediately"); + + // Now user presses Enter: transmits AID_ENTER with '>' and modified field + input.sendAid(AID_ENTER); + byte[] sent = sentData.get(); + assertNotNull(sent); + assertEquals((byte) AID_ENTER, sent[0]); + assertEquals((byte) ORDER_SBA, sent[3]); + assertEquals((byte) 0x6E, sent[6], "Sent field content must contain '>' (0x6E)"); + assertEquals((byte) 0xC3, sent[7], "Sent field content must contain 'C'"); + + sentData.set(null); + + // Second click: toggle '>' -> '?' and clear MDT + handled = input.lightPenSelect(1); + assertTrue(handled); + assertEquals((byte) 0x6F, screen.getCell(1).ec, "Designator must change back to '?' (0x6F)"); + assertEquals('?', (char) screen.getCell(1).ucs4); + assertFalse(faIsModified(screen.getCell(0).fa & 0xFF), "MDT must be cleared"); + assertNull(sentData.get(), "Deferred deselection must NOT transmit immediately"); + } + + @Test + public void testSendGraphicMouseAidCoordinatesAndCursorSynchronization() { + screen.erase(false); + screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY)); + screen.getCell(1).ec = (byte) 0xD6; // 'O' + screen.getCell(2).ec = (byte) 0xD2; // 'K' + screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + + // Click at row 3, col 15 -> address = 3 * 80 + 15 = 255 + screen.setCursorPosition(3, 15); + + GraphicsPlane plane = new GraphicsPlane(720, 384); + plane.setScreenDimensions(80, 24); + GocaDecoder goca = new GocaDecoder(plane); + goca.setGraphicsCursorActive(true); + goca.setGraphicCursorPosition(200, 100); + + AtomicReference sentData = new AtomicReference<>(); + InputProcessor input = new InputProcessor(screen, translator, null) { + @Override + protected void sendAidResponse(byte[] data) { + sentData.set(data); + } + }; + input.setGocaDecoder(goca); + + input.sendGraphicMouseAid(AID_ENTER, 1, false, false); + + byte[] sent = sentData.get(); + assertNotNull(sent); + + // AID_SF (0x88) + 56 SF bytes + AID_ENTER (0x7D) + 2 cursor bytes + SBA + ... + assertEquals((byte) AID_SF, sent[0]); + assertEquals(0x00, sent[1]); + assertEquals(0x34, sent[2]); // 52-byte SF length mask + + // Coordinate verification in SF + int gx = (sent[25] << 8) | (sent[26] & 0xFF); + int gy = (sent[27] << 8) | (sent[28] & 0xFF); + assertEquals(200, (short) gx); + assertEquals(100, (short) gy); + + // Trailing AID and cursor SBA at index 57-59 + assertEquals((byte) AID_ENTER, sent[57]); + byte[] expectedCursor = encodeAddress(3 * 80 + 15, 24, 80); + assertEquals(expectedCursor[0], sent[58], "Trailing cursor address byte 0 must match clicked row/col"); + assertEquals(expectedCursor[1], sent[59], "Trailing cursor address byte 1 must match clicked row/col"); + } + + @Test + public void testGddmCoordinateTransformModel2AndModel4() { + // Model 2 (80x24) + GddmCoordinateTransform t2 = new GddmCoordinateTransform(80, 24, 9, 16); + assertEquals(720, t2.getTotalWidth()); + assertEquals(384, t2.getTotalHeight()); + assertEquals(360, t2.getXMax()); + assertEquals(191, t2.getYMax()); + + // Row 0 click (y in [0..15] out of 384) + Point gocaTop = t2.screenPixelToGoca(0, 0, 0, 0, 9, 16); + assertEquals(-360, gocaTop.x); + assertEquals(191, gocaTop.y, "Top row in Model 2 must start at gy = 191"); + + // Model 4 (80x43) + GddmCoordinateTransform t4 = new GddmCoordinateTransform(80, 43, 9, 16); + assertEquals(720, t4.getTotalWidth()); + assertEquals(688, t4.getTotalHeight()); + assertEquals(360, t4.getXMax()); + assertEquals(343, t4.getYMax()); + + Point gocaTop4 = t4.screenPixelToGoca(0, 0, 0, 0, 9, 16); + assertEquals(-360, gocaTop4.x); + assertEquals(343, gocaTop4.y, "Top row in Model 4 must start at gy = 343"); + + // Center roundtrip + Point centerBase = t4.gocaToBase(0, 0); + assertEquals(360, centerBase.x); + assertEquals(343, centerBase.y); + Point centerGoca = t4.baseToGoca(centerBase.x, centerBase.y); + assertEquals(0, centerGoca.x); + assertEquals(0, centerGoca.y); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase1Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase1Test.java new file mode 100644 index 0000000..995f98b --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase1Test.java @@ -0,0 +1,347 @@ +package haus.nightmare.lib3270j.telnet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.ConnectionState; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.datastream.DataStreamProcessor; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.protocol.DS3270Constants; +import haus.nightmare.lib3270j.protocol.TelnetConstants; +import haus.nightmare.lib3270j.protocol.TN3270EConstants; +import haus.nightmare.lib3270j.screen.ScreenBuffer; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static haus.nightmare.lib3270j.protocol.TelnetConstants.*; +import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*; + +public class TelnetFSMPhase1Test { + + private ConnectionConfig config; + private ScreenBuffer screenBuffer; + private DataStreamProcessor dsProcessor; + private InputProcessor inputProcessor; + private TelnetFSM fsm; + private MockConnection connection; + private EbcdicTranslator translator; + + private static class MockConnection extends TelnetConnection { + final List sentData = new ArrayList<>(); + + MockConnection(ConnectionConfig config, TelnetFSM fsm) { + super(config, fsm); + } + + @Override + public synchronized void sendRaw(byte[] data) { + sentData.add(data.clone()); + } + + @Override + public synchronized void sendRaw(byte[] data, int offset, int length) { + byte[] b = new byte[length]; + System.arraycopy(data, offset, b, 0, length); + sentData.add(b); + } + } + + @BeforeEach + public void setup() { + translator = new EbcdicTranslator(); + config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4); + screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + dsProcessor = new DataStreamProcessor(screenBuffer, translator); + fsm = new TelnetFSM(config, screenBuffer, dsProcessor); + inputProcessor = new InputProcessor(screenBuffer, translator, fsm); + dsProcessor.setInputProcessor(inputProcessor); + connection = new MockConnection(config, fsm); + fsm.setConnection(connection); + } + + private void feedBytes(int... bytes) { + for (int b : bytes) { + fsm.feedByte(b & 0xFF); + } + } + + @Test + public void testTn3270eSequencingWaitsForSendDeviceType() { + config.setTn3270eEnabled(true); + fsm.onConnected(); + connection.sentData.clear(); + + // 1. Server sends DO TN3270E + feedBytes(IAC, DO, TELOPT_TN3270E); + + // Client must reply IAC WILL TN3270E only, and MUST NOT emit unsolicited DEVICE-TYPE REQUEST + assertEquals(1, connection.sentData.size(), "Only WILL TN3270E should be sent initially"); + byte[] willPkt = connection.sentData.get(0); + assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_TN3270E }, willPkt); + connection.sentData.clear(); + + // 2. Server sends IAC SB TN3270E SEND DEVICE-TYPE IAC SE + feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE); + + // Client must now send IAC SB TN3270E DEVICE-TYPE REQUEST IAC SE + assertEquals(1, connection.sentData.size(), "Client should send DEVICE-TYPE REQUEST after SEND command"); + byte[] reqPkt = connection.sentData.get(0); + assertTrue((reqPkt[0] & 0xFF) == IAC && (reqPkt[1] & 0xFF) == SB && (reqPkt[2] & 0xFF) == TELOPT_TN3270E); + assertEquals(OP_DEVICE_TYPE, reqPkt[3] & 0xFF); + assertEquals(OP_REQUEST, reqPkt[4] & 0xFF); + String devType = new String(reqPkt, 5, reqPkt.length - 7); + assertEquals("IBM-3279-4-E", devType); + } + + @Test + public void testTn3270eResponseSignalingAlwaysResponse() { + config.setTn3270eEnabled(true); + fsm.onConnected(); + + // Negotiate TN3270E + feedBytes(IAC, DO, TELOPT_TN3270E); + feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE); + byte[] devTypeIs = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + 0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E', + (byte) IAC, (byte) SE + }; + for (byte b : devTypeIs) fsm.feedByte(b & 0xFF); + + // Functions: RESPONSES + BIND-IMAGE + byte[] funcsReq = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + (byte) OP_FUNCTIONS, (byte) OP_REQUEST, + (byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, + (byte) IAC, (byte) SE + }; + for (byte b : funcsReq) fsm.feedByte(b & 0xFF); + connection.sentData.clear(); + + // Send 3270 data record with ALWAYS_RESPONSE (0x02) and seq = 0x0042 + int seq = 0x0042; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(DT_3270_DATA); // byte 0: data type + bos.write(0x00); // byte 1: request flag + bos.write(RSF_ALWAYS_RESPONSE);// byte 2: response flag (0x02) + bos.write((seq >> 8) & 0xFF); // byte 3: seq hi + bos.write(seq & 0xFF); // byte 4: seq lo + bos.write(0xF5); // EraseWrite + bos.write(0xC3); // WCC + bos.write(0x11); bos.write(0x40); bos.write(0x40); // SBA 0 + bos.write(translator.unicodeToEbcdic('A')); + bos.write(IAC); + bos.write(EOR); + + for (byte b : bos.toByteArray()) fsm.feedByte(b & 0xFF); + + // Client must emit positive response: DT_RESPONSE (0x02), 0x00, RSF_POSITIVE_RESPONSE (0x00), seq, POS_DEVICE_END (0x00), IAC, EOR + boolean foundPositiveResponse = false; + for (byte[] pkt : connection.sentData) { + if (pkt.length >= 8 && + (pkt[0] & 0xFF) == DT_RESPONSE && + (pkt[2] & 0xFF) == RSF_POSITIVE_RESPONSE && + ((((pkt[3] & 0xFF) << 8) | (pkt[4] & 0xFF)) == seq) && + (pkt[5] & 0xFF) == POS_DEVICE_END && + (pkt[6] & 0xFF) == IAC && + (pkt[7] & 0xFF) == EOR) { + foundPositiveResponse = true; + } + } + assertTrue(foundPositiveResponse, "Expected positive response with matching sequence number for RSF_ALWAYS_RESPONSE"); + } + + @Test + public void testTn3270eResponseSignalingNoResponse() { + config.setTn3270eEnabled(true); + fsm.onConnected(); + + feedBytes(IAC, DO, TELOPT_TN3270E); + feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE); + byte[] devTypeIs = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + 0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E', + (byte) IAC, (byte) SE + }; + for (byte b : devTypeIs) fsm.feedByte(b & 0xFF); + + byte[] funcsReq = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + (byte) OP_FUNCTIONS, (byte) OP_REQUEST, + (byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, + (byte) IAC, (byte) SE + }; + for (byte b : funcsReq) fsm.feedByte(b & 0xFF); + connection.sentData.clear(); + + // Send 3270 data record with NO_RESPONSE (0x00) + int seq = 0x0099; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(DT_3270_DATA); + bos.write(0x00); + bos.write(RSF_NO_RESPONSE); // 0x00 + bos.write((seq >> 8) & 0xFF); + bos.write(seq & 0xFF); + bos.write(0xF5); + bos.write(0xC3); + bos.write(IAC); + bos.write(EOR); + + for (byte b : bos.toByteArray()) fsm.feedByte(b & 0xFF); + + // No response packets should be sent + assertEquals(0, connection.sentData.size(), "No response should be generated when RSF_NO_RESPONSE (0x00)"); + } + + @Test + public void testSysReqOutOfBandRoutingAndToggle() { + config.setTn3270eEnabled(true); + fsm.onConnected(); + + // Complete TN3270E negotiation + feedBytes(IAC, DO, TELOPT_TN3270E); + feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE); + byte[] devTypeIs = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + 0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E', + (byte) IAC, (byte) SE + }; + for (byte b : devTypeIs) fsm.feedByte(b & 0xFF); + + byte[] funcsReq = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + (byte) OP_FUNCTIONS, (byte) OP_REQUEST, + (byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SYSREQ, + (byte) IAC, (byte) SE + }; + for (byte b : funcsReq) fsm.feedByte(b & 0xFF); + + // Send BIND image to bind session + byte[] bindPacket = new byte[EH_SIZE + 35]; + bindPacket[0] = DT_BIND_IMAGE; + bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1; + bindPacket[EH_SIZE + 24] = 0x02; // Model 2 (24x80) + ByteArrayOutputStream bStream = new ByteArrayOutputStream(); + bStream.write(bindPacket, 0, bindPacket.length); + bStream.write(IAC); + bStream.write(EOR); + for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF); + + assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState()); + assertTrue(fsm.isTn3270eBound()); + connection.sentData.clear(); + + // Lock keyboard as if host app is waiting + inputProcessor.setKeyboardLocked(true); + assertTrue(inputProcessor.isKeyboardLocked()); + + // 1. User presses SYSREQ key (AID 0xF0) + inputProcessor.sendAid(DS3270Constants.AID_SYSREQ); + + // Keyboard should now be unlocked and IAC AO (0xFF 0xF5) sent out-of-band + assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should be unlocked on SYSREQ"); + boolean foundIacAo = false; + for (byte[] pkt : connection.sentData) { + if (pkt.length == 2 && (pkt[0] & 0xFF) == IAC && (pkt[1] & 0xFF) == AO) { + foundIacAo = true; + } + } + assertTrue(foundIacAo, "Expected raw IAC AO out-of-band telnet command"); + assertEquals(ConnectionState.CONNECTED_SSCP, fsm.getConnectionState(), "Session should switch to CONNECTED_SSCP"); + connection.sentData.clear(); + + // 2. User types in SSCP-LU mode (e.g. "LOGOFF") and presses Enter + inputProcessor.typeCharacter('L'); + inputProcessor.typeCharacter('O'); + inputProcessor.typeCharacter('G'); + inputProcessor.typeCharacter('O'); + inputProcessor.typeCharacter('F'); + inputProcessor.typeCharacter('F'); + inputProcessor.sendAid(DS3270Constants.AID_ENTER); + + // Verify sent record has header DT_SSCP_LU_DATA (0x07) + assertEquals(1, connection.sentData.size(), "SSCP-LU input should produce one record"); + byte[] sscpPkt = connection.sentData.get(0); + assertEquals(DT_SSCP_LU_DATA, sscpPkt[0] & 0xFF, "Data type should be DT_SSCP_LU_DATA (0x07)"); + assertEquals(translator.unicodeToEbcdic('L'), sscpPkt[5] & 0xFF); + assertEquals(translator.unicodeToEbcdic('O'), sscpPkt[6] & 0xFF); + connection.sentData.clear(); + + // 3. Host sends inbound SSCP-LU unformatted prompt + ByteArrayOutputStream ussPrompt = new ByteArrayOutputStream(); + ussPrompt.write(DT_SSCP_LU_DATA); + ussPrompt.write(0); ussPrompt.write(0); ussPrompt.write(0); ussPrompt.write(2); + ussPrompt.write(translator.unicodeToEbcdic('U')); + ussPrompt.write(translator.unicodeToEbcdic('S')); + ussPrompt.write(translator.unicodeToEbcdic('S')); + ussPrompt.write(IAC); + ussPrompt.write(EOR); + for (byte b : ussPrompt.toByteArray()) fsm.feedByte(b & 0xFF); + + assertEquals('U', translator.ebcdicToUnicode(screenBuffer.getCellEC(80))); + assertEquals('S', translator.ebcdicToUnicode(screenBuffer.getCellEC(81))); + assertEquals('S', translator.ebcdicToUnicode(screenBuffer.getCellEC(82))); + + // 4. User presses SYSREQ again to toggle back to 3270 session + inputProcessor.sendAid(DS3270Constants.AID_SYSREQ); + assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(), "Second SYSREQ should toggle back to CONNECTED_TN3270E"); + } + + @Test + public void testTn3270eResponseSignalingErrorResponse() { + config.setTn3270eEnabled(true); + fsm.onConnected(); + + feedBytes(IAC, DO, TELOPT_TN3270E); + feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE); + byte[] devTypeIs = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + 0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E', + (byte) IAC, (byte) SE + }; + for (byte b : devTypeIs) fsm.feedByte(b & 0xFF); + + byte[] funcsReq = new byte[]{ + (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, + (byte) OP_FUNCTIONS, (byte) OP_REQUEST, + (byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, + (byte) IAC, (byte) SE + }; + for (byte b : funcsReq) fsm.feedByte(b & 0xFF); + + // Bind session + byte[] bindPacket = new byte[EH_SIZE + 35]; + bindPacket[0] = DT_BIND_IMAGE; + bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1; + bindPacket[EH_SIZE + 24] = 0x02; + ByteArrayOutputStream bStream = new ByteArrayOutputStream(); + bStream.write(bindPacket, 0, bindPacket.length); + bStream.write(IAC); + bStream.write(EOR); + for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF); + connection.sentData.clear(); + + // Send a malformed 3270 record that triggers an error with ERROR_RESPONSE (0x01) + int seq = 0x0077; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(DT_3270_DATA); + bos.write(0x00); + bos.write(RSF_ERROR_RESPONSE); // 0x01 + bos.write((seq >> 8) & 0xFF); + bos.write(seq & 0xFF); + bos.write(0xF5); // EraseWrite + // Truncated SBA order without address bytes to trigger an exception in dsProcessor + bos.write(0x11); // SBA without address bytes + bos.write(IAC); + bos.write(EOR); + + // Note: dsProcessor gracefully handles or if it throws, negative response is sent. + // Let's verify what happens when exception or normal: + for (byte b : bos.toByteArray()) fsm.feedByte(b & 0xFF); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase2Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase2Test.java index 59d331f..252f4bc 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase2Test.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/TelnetFSMPhase2Test.java @@ -153,9 +153,12 @@ public class TelnetFSMPhase2Test { fsm.onConnected(); output.reset(); - // Server sends DO TN3270E -> client sends WILL TN3270E and requests "LU_A" + // Server sends DO TN3270E -> client sends WILL TN3270E fsm.feedBytes(new byte[] { (byte) IAC, (byte) DO, (byte) TELOPT_TN3270E }, 0, 3); + // Server sends SB TN3270E SEND DEVICE-TYPE -> client requests "LU_A" + fsm.feedBytes(new byte[] { (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, (byte) OP_SEND, (byte) OP_DEVICE_TYPE, (byte) IAC, (byte) SE }, 0, 7); + String sent1 = new String(output.toByteArray()); assertTrue(sent1.contains("LU_A")); output.reset();