diff --git a/README.md b/README.md index 26d1ab9..4017d15 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,14 @@ Requires **Java 11 or higher** (Java 11, 17, 21+): # Launch interactive GUI java -jar j3270.jar +# Connect to TLS/SSL mainframe with custom port +java -jar j3270.jar -s mainframe.example.com 992 +# Or using standard x3270 L: prefix +java -jar j3270.jar L:mainframe.example.com:992 + +# Connect with unverified/self-signed certificate verification bypass +java -jar j3270.jar --tls --insecure mainframe.example.com 992 + # Launch with custom configuration file java -jar j3270.jar -c config.ini @@ -36,6 +44,10 @@ java -jar j3270.jar -c config.ini ## ✨ Features - **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption. +- **SSL/TLS Security**: + - Encrypted TN3270 over TLS connections on standard port `992` or custom ports. + - Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection. + - Optional unverified/insecure mode for testing and headless scripting. - **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS. - **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported). - **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering. @@ -59,7 +71,7 @@ The project includes self-contained, portable build scripts: # Build executable JAR in 2 seconds: sh ./build_all.sh -# Run all 25 automated unit tests in ~130ms: +# Run all 31 automated unit tests in ~180ms: sh ./test_all.sh ``` diff --git a/build/j3270/org/pubvm/j3270/J3270App.class b/build/j3270/org/pubvm/j3270/J3270App.class index 7251b12..ea768d4 100644 Binary files a/build/j3270/org/pubvm/j3270/J3270App.class and b/build/j3270/org/pubvm/j3270/J3270App.class differ diff --git a/build/j3270/org/pubvm/j3270/config/Settings.class b/build/j3270/org/pubvm/j3270/config/Settings.class index 0614591..8047394 100644 Binary files a/build/j3270/org/pubvm/j3270/config/Settings.class and b/build/j3270/org/pubvm/j3270/config/Settings.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class index e8ba19b..a5dadfe 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class and b/build/j3270/org/pubvm/j3270/ui/ConnectDialog.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class index 145519e..187f269 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$1.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class index 7e3d3e9..c981afe 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$2.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class index f5efe87..aeb554d 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$3.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class index 3d9b553..5a81cd4 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog$4.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class b/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class index d925b0d..fb1d43f 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class and b/build/j3270/org/pubvm/j3270/ui/SettingsDialog.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class b/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class index c4929f9..e9d7423 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class and b/build/j3270/org/pubvm/j3270/ui/StatusBar$1.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/StatusBar.class b/build/j3270/org/pubvm/j3270/ui/StatusBar.class index e157413..d8919de 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/StatusBar.class and b/build/j3270/org/pubvm/j3270/ui/StatusBar.class differ diff --git a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class index 4268587..a1ae124 100644 Binary files a/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class and b/build/j3270/org/pubvm/j3270/ui/TerminalPanel.class differ diff --git a/build/lib3270j/org/lib3270j/ConnectionConfig.class b/build/lib3270j/org/lib3270j/ConnectionConfig.class index c724b82..68b87e6 100644 Binary files a/build/lib3270j/org/lib3270j/ConnectionConfig.class and b/build/lib3270j/org/lib3270j/ConnectionConfig.class differ diff --git a/build/lib3270j/org/lib3270j/Telnet3270Client.class b/build/lib3270j/org/lib3270j/Telnet3270Client.class index b4871f2..fbbebed 100644 Binary files a/build/lib3270j/org/lib3270j/Telnet3270Client.class and b/build/lib3270j/org/lib3270j/Telnet3270Client.class differ diff --git a/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class b/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class index 2469a25..9b83470 100644 Binary files a/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class and b/build/lib3270j/org/lib3270j/charset/EbcdicTranslator.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class index 244f2ca..e6c9497 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class and b/build/lib3270j/org/lib3270j/datastream/DataStreamProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class index afd2c0f..4aa268e 100644 Binary files a/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class and b/build/lib3270j/org/lib3270j/datastream/QueryReplyBuilder.class differ diff --git a/build/lib3270j/org/lib3270j/input/InputProcessor.class b/build/lib3270j/org/lib3270j/input/InputProcessor.class index e0afc12..fa8e87d 100644 Binary files a/build/lib3270j/org/lib3270j/input/InputProcessor.class and b/build/lib3270j/org/lib3270j/input/InputProcessor.class differ diff --git a/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class b/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class index 1fdbea2..cfe63a2 100644 Binary files a/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class and b/build/lib3270j/org/lib3270j/protocol/DS3270Constants.class differ diff --git a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class index 5f4c7a0..6508f81 100644 Binary files a/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class and b/build/lib3270j/org/lib3270j/telnet/TelnetConnection.class differ diff --git a/j3270/src/main/java/org/pubvm/j3270/J3270App.java b/j3270/src/main/java/org/pubvm/j3270/J3270App.java index 0aae383..25788a1 100644 --- a/j3270/src/main/java/org/pubvm/j3270/J3270App.java +++ b/j3270/src/main/java/org/pubvm/j3270/J3270App.java @@ -227,8 +227,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate private void showConnectDialog() { ConnectDialog dialog = new ConnectDialog(this); - dialog.setInitialHost(lastHost); - dialog.setInitialPort(lastPort); + String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : org.pubvm.j3270.config.Settings.getAutoConnectHost(); + int initialPort = lastPort > 0 ? lastPort : org.pubvm.j3270.config.Settings.getAutoConnectPort(); + dialog.setInitialHost(initialHost); + dialog.setInitialPort(initialPort); + dialog.setInitialTls(org.pubvm.j3270.config.Settings.getAutoConnectTls()); + dialog.setInitialVerifyCert(org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert()); dialog.setVisible(true); if (dialog.isConfirmed()) { @@ -255,10 +259,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate // Save for auto-connect org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost()); org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort()); + org.pubvm.j3270.config.Settings.setAutoConnectTls(config.isUseTls()); + org.pubvm.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert()); // Disconnect existing connection disconnect(); + // Wire interactive certificate verifier if none configured + if (config.getCertificateVerifier() == null) { + config.setCertificateVerifier((chain, authType, exception) -> + org.pubvm.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception) + ); + } + client = new Telnet3270Client(config); client.addConnectionListener(this); client.addScreenUpdateListener(this); @@ -266,7 +279,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.setClient(client); statusBar.setClient(client); - setTitle("j3270 — " + config.getHost() + ":" + config.getPort()); + String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; + setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); // Resize window to match model terminalPanel.guardedPack(); @@ -426,11 +440,24 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate public static void main(String[] args) { boolean debug = false; + boolean cliTls = false; + boolean cliNoVerifyCert = false; + org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null; String configFile = null; java.util.List remainingArgs = new java.util.ArrayList<>(); for (int i = 0; i < args.length; i++) { if ("--debug".equals(args[i]) || "-d".equals(args[i])) { debug = true; + } else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) { + cliTls = true; + } else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) { + cliNoVerifyCert = true; + } else if (args[i].startsWith("--graphics=")) { + cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11)); + } else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) { + cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[++i]); + } else if ("--no-graphics".equals(args[i])) { + cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.NONE; } else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) { configFile = args[++i]; } else { @@ -439,33 +466,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } // Configure logging for application packages + Level logLevel = debug ? Level.ALL : Level.INFO; Logger globalRoot = Logger.getLogger(""); for (java.util.logging.Handler h : globalRoot.getHandlers()) { globalRoot.removeHandler(h); } - globalRoot.setLevel(Level.WARNING); + globalRoot.setLevel(Level.ALL); - ConsoleHandler handler = new ConsoleHandler(); - handler.setLevel(debug ? Level.ALL : Level.INFO); - handler.setFormatter(new SimpleFormatter()); + java.util.logging.Filter appFilter = record -> { + String name = record.getLoggerName(); + return name != null && (name.startsWith("org.lib3270j") || name.startsWith("org.pubvm.j3270")); + }; - Logger appLogger = Logger.getLogger("org.pubvm.j3270"); - appLogger.setLevel(Level.ALL); - appLogger.addHandler(handler); - appLogger.setUseParentHandlers(false); + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(logLevel); + consoleHandler.setFormatter(new SimpleFormatter()); + consoleHandler.setFilter(appFilter); + globalRoot.addHandler(consoleHandler); - Logger libLogger = Logger.getLogger("org.lib3270j"); - libLogger.setLevel(Level.ALL); - libLogger.addHandler(handler); - libLogger.setUseParentHandlers(false); + Logger.getLogger("org.pubvm").setLevel(logLevel); + Logger.getLogger("org.pubvm.j3270").setLevel(logLevel); + Logger.getLogger("org.lib3270j").setLevel(logLevel); try { - java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false); + java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) { + @Override + public synchronized void publish(java.util.logging.LogRecord record) { + super.publish(record); + flush(); + } + }; fileHandler.setLevel(Level.ALL); fileHandler.setFormatter(new SimpleFormatter()); - appLogger.addHandler(fileHandler); - libLogger.addHandler(fileHandler); - log.info("Logging protocol trace to j3270.log"); + fileHandler.setFilter(appFilter); + globalRoot.addHandler(fileHandler); + log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")"); } catch (Exception e) { System.err.println("Could not create j3270.log: " + e.getMessage()); } @@ -492,18 +527,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("apple.awt.application.name", "j3270"); + final boolean finalTls = cliTls; + final boolean finalNoVerify = cliNoVerifyCert; + final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode; + SwingUtilities.invokeLater(() -> { J3270App app = new J3270App(); app.setVisible(true); // If host:port given on command line, connect directly if (!remainingArgs.isEmpty()) { - String host = remainingArgs.get(0); - int port = 23; + String hostArg = remainingArgs.get(0); + int port = finalTls ? 992 : 23; if (remainingArgs.size() >= 2) { try { port = Integer.parseInt(remainingArgs.get(1)); - } catch (NumberFormatException e) { + } catch (NumberFormatException ignored) { } } TerminalModel model = TerminalModel.IBM_3279_4; @@ -511,10 +550,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate try { int modelNum = Integer.parseInt(remainingArgs.get(2)); model = TerminalModel.forModel(modelNum, true); - } catch (Exception e) { + } catch (Exception ignored) { } } - ConnectionConfig config = new ConnectionConfig(host, port, model); + ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model); + if (finalTls) { + config.setUseTls(true); + } + if (finalNoVerify) { + config.setTlsVerifyCert(false); + } + if (finalGraphicsMode != null) { + config.setGraphicsMode(finalGraphicsMode); + } else { + config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode()); + } app.connect(config); } else { org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings @@ -526,8 +576,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate case AUTO_CONNECT: String host = org.pubvm.j3270.config.Settings.getAutoConnectHost(); int port = org.pubvm.j3270.config.Settings.getAutoConnectPort(); + boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls(); + boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert(); if (host != null && !host.isEmpty()) { - app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4)); + ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); + config.setTlsVerifyCert(verify); + if (finalGraphicsMode != null) { + config.setGraphicsMode(finalGraphicsMode); + } else { + config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode()); + } + app.connect(config); } else { SwingUtilities.invokeLater(app::showConnectDialog); } diff --git a/j3270/src/main/java/org/pubvm/j3270/config/Settings.java b/j3270/src/main/java/org/pubvm/j3270/config/Settings.java index 28b7cac..ff780b7 100644 --- a/j3270/src/main/java/org/pubvm/j3270/config/Settings.java +++ b/j3270/src/main/java/org/pubvm/j3270/config/Settings.java @@ -62,6 +62,31 @@ public class Settings { prefs.putInt("autoConnectPort", port); } + public static boolean getAutoConnectTls() { + return prefs.getBoolean("autoConnectTls", false); + } + + public static void setAutoConnectTls(boolean tls) { + prefs.putBoolean("autoConnectTls", tls); + } + + public static boolean getAutoConnectVerifyCert() { + return prefs.getBoolean("autoConnectVerifyCert", true); + } + + public static void setAutoConnectVerifyCert(boolean verify) { + prefs.putBoolean("autoConnectVerifyCert", verify); + } + + public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() { + String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name()); + return org.lib3270j.graphics.GraphicsMode.fromString(modeStr); + } + + public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) { + prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name()); + } + public static Color getColorOverride(int index, Color defaultColor) { String hex = prefs.get("color_" + index, null); try { @@ -194,15 +219,33 @@ public class Settings { break; case "behavior": + case "connection": switch (key) { case "startupBehavior": setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase())); break; - case "autoConnectHost": setAutoConnectHost(value); break; - case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break; + case "autoConnectHost": + case "host": + setAutoConnectHost(value); + break; + case "autoConnectPort": + case "port": + setAutoConnectPort(Integer.parseInt(value)); + break; + case "autoConnectTls": + case "tls": + case "ssl": + case "useTls": + setAutoConnectTls(Boolean.parseBoolean(value)); + break; + case "autoConnectVerifyCert": + case "verifyCert": + case "tlsVerifyCert": + setAutoConnectVerifyCert(Boolean.parseBoolean(value)); + break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; default: - log.warning("Unknown behavior key: " + key); + log.warning("Unknown behavior/connection key: " + key); } break; @@ -222,6 +265,14 @@ public class Settings { setKeyBinding(key, value); break; + case "graphics": + if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) { + setGraphicsMode(org.lib3270j.graphics.GraphicsMode.fromString(value)); + } else { + log.warning("Unknown graphics key: " + key); + } + break; + default: // Allow bare keys outside any section — treat as raw prefs log.fine("Setting raw preference: " + key + " = " + value); @@ -256,10 +307,17 @@ public class Settings { if (acHost != null && !acHost.isEmpty()) { w.println("autoConnectHost = " + acHost); w.println("autoConnectPort = " + getAutoConnectPort()); + w.println("autoConnectTls = " + getAutoConnectTls()); + w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert()); } w.println("blockSelectMode = " + getBlockSelectMode()); w.println(); + // [graphics] + w.println("[graphics]"); + w.println("graphicsMode = " + getGraphicsMode().name()); + w.println(); + // [colors] w.println("[colors]"); // Host colors 0-15 diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java b/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java index 30ac8b8..b6ac0bb 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/ConnectDialog.java @@ -14,7 +14,10 @@ public class ConnectDialog extends JDialog { private JTextField hostField; private JTextField portField; private JComboBox modelCombo; + private JComboBox graphicsCombo; private JTextField luField; + private JCheckBox tlsCheckBox; + private JCheckBox verifyCertCheckBox; private boolean confirmed; private ConnectionConfig result; @@ -93,6 +96,56 @@ public class ConnectDialog extends JDialog { luField = createDarkField(12); mainPanel.add(luField, gbc); + // Graphics Mode + gbc.gridx = 0; + gbc.gridy = 4; + gbc.weightx = 0; + JLabel graphicsLabel = new JLabel("Graphics:"); + graphicsLabel.setForeground(fg); + graphicsLabel.setFont(labelFont); + mainPanel.add(graphicsLabel, gbc); + gbc.gridx = 1; + gbc.weightx = 1.0; + graphicsCombo = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values()); + graphicsCombo.setSelectedItem(org.pubvm.j3270.config.Settings.getGraphicsMode()); + graphicsCombo.setBackground(new Color(45, 45, 45)); + graphicsCombo.setForeground(fg); + graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + mainPanel.add(graphicsCombo, gbc); + + // TLS / SSL Checkbox + gbc.gridx = 1; + gbc.gridy = 5; + gbc.weightx = 1.0; + tlsCheckBox = new JCheckBox("Enable TLS/SSL"); + tlsCheckBox.setBackground(new Color(30, 30, 30)); + tlsCheckBox.setForeground(fg); + tlsCheckBox.setFont(labelFont); + tlsCheckBox.setFocusPainted(false); + tlsCheckBox.addActionListener(e -> { + boolean isTls = tlsCheckBox.isSelected(); + verifyCertCheckBox.setEnabled(isTls); + String currentPort = portField.getText().trim(); + if (isTls && "23".equals(currentPort)) { + portField.setText("992"); + } else if (!isTls && "992".equals(currentPort)) { + portField.setText("23"); + } + }); + mainPanel.add(tlsCheckBox, gbc); + + // Verify Certificate Checkbox + gbc.gridx = 1; + gbc.gridy = 6; + verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); + verifyCertCheckBox.setBackground(new Color(30, 30, 30)); + verifyCertCheckBox.setForeground(new Color(160, 160, 160)); + verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + verifyCertCheckBox.setSelected(true); + verifyCertCheckBox.setEnabled(false); + verifyCertCheckBox.setFocusPainted(false); + mainPanel.add(verifyCertCheckBox, gbc); + // Buttons JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttonPanel.setBackground(new Color(30, 30, 30)); @@ -116,7 +169,7 @@ public class ConnectDialog extends JDialog { buttonPanel.add(connectBtn); gbc.gridx = 0; - gbc.gridy = 4; + gbc.gridy = 7; gbc.gridwidth = 2; mainPanel.add(buttonPanel, gbc); @@ -158,6 +211,9 @@ public class ConnectDialog extends JDialog { if (!lu.isEmpty()) { result.setLuName(lu); } + result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()); + result.setUseTls(tlsCheckBox.isSelected()); + result.setTlsVerifyCert(verifyCertCheckBox.isSelected()); confirmed = true; dispose(); } @@ -178,4 +234,13 @@ public class ConnectDialog extends JDialog { public void setInitialPort(int port) { portField.setText(String.valueOf(port)); } + + public void setInitialTls(boolean tls) { + tlsCheckBox.setSelected(tls); + verifyCertCheckBox.setEnabled(tls); + } + + public void setInitialVerifyCert(boolean verify) { + verifyCertCheckBox.setSelected(verify); + } } diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java b/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java index 7ef2eee..716f616 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/SettingsDialog.java @@ -31,6 +31,7 @@ public class SettingsDialog extends JDialog { // Appearance tab private JComboBox fontBox; private JSpinner fontSizeSpinner; + private JComboBox graphicsModeBox; // Behavior tab private JComboBox startupBehaviorBox; @@ -264,8 +265,21 @@ public class SettingsDialog extends JDialog { gbc.fill = GridBagConstraints.HORIZONTAL; panel.add(fontSizeSpinner, gbc); - // Fill remaining space + // Graphics Mode + gbc.gridx = 0; gbc.gridy = 2; + gbc.fill = GridBagConstraints.NONE; + JLabel graphicsLabel = new JLabel("Graphics Mode:"); + panel.add(graphicsLabel, gbc); + + graphicsModeBox = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values()); + graphicsModeBox.setSelectedItem(Settings.getGraphicsMode()); + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(graphicsModeBox, gbc); + + // Fill remaining space + gbc.gridy = 3; gbc.weighty = 1.0; panel.add(Box.createGlue(), gbc); @@ -611,6 +625,9 @@ public class SettingsDialog extends JDialog { Settings.setFontFamily(fontFam); } Settings.setFontSize((Integer) fontSizeSpinner.getValue()); + if (graphicsModeBox.getSelectedItem() != null) { + Settings.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem()); + } // Apply Behavior Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem(); diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java b/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java index fa6a038..e3a7733 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java @@ -15,6 +15,7 @@ import java.awt.*; public class StatusBar extends JPanel { private final JLabel connectionStatus; + private final JLabel tlsStatus; private final JLabel cursorPosition; private final JLabel luName; private final JLabel lockStatus; @@ -37,6 +38,7 @@ public class StatusBar extends JPanel { Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM); + tlsStatus = createLabel("", oiaFont, OIA_FG); luName = createLabel("", oiaFont, OIA_FG); lockStatus = createLabel("", oiaFont, OIA_ALERT); modelInfo = createLabel("", oiaFont, OIA_DIM); @@ -44,6 +46,8 @@ public class StatusBar extends JPanel { add(Box.createHorizontalStrut(6)); add(connectionStatus); + add(Box.createHorizontalStrut(10)); + add(tlsStatus); add(Box.createHorizontalStrut(12)); add(luName); add(Box.createHorizontalStrut(12)); @@ -110,6 +114,26 @@ public class StatusBar extends JPanel { break; } + // TLS Status + if (state.isConnected() && client.getConfig().isUseTls()) { + boolean verified = client.getConfig().isTlsVerifyCert(); + javax.net.ssl.SSLSession session = client.getSslSession(); + String cipher = session != null ? session.getCipherSuite() : "TLS"; + String protocol = session != null ? session.getProtocol() : "TLS"; + if (verified) { + tlsStatus.setText("🔒 TLS"); + tlsStatus.setForeground(OIA_FG); + tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)"); + } else { + tlsStatus.setText("🔓 TLS (Unverified)"); + tlsStatus.setForeground(new Color(255, 180, 80)); + tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)"); + } + } else { + tlsStatus.setText(""); + tlsStatus.setToolTipText(null); + } + // LU name String lu = ""; if (client.getConnectionState().isTn3270e()) { diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java index 6514486..ef23b1c 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java @@ -921,13 +921,21 @@ public class TerminalPanel extends JPanel { g2.setColor(bgColor); g2.fillRect(x, y, cellWidth, cellHeight); - // Draw character - char ch = ea.ucs4; - if (ch > 0x20 && ch != 0xFF) { - Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont; - g2.setFont(f); - g2.setColor(fgColor); - g2.drawString(String.valueOf(ch), x, y + fontAscent); + // Draw character or Programmed Symbol + int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0); + boolean drawnAsPs = false; + if (cs >= 0x40 && client.getProgramSymbolManager() != null) { + drawnAsPs = client.getProgramSymbolManager().drawSymbol(g2, cs, ea.ec & 0xFF, x, y, cellWidth, cellHeight, fgColor, bgColor); + } + + if (!drawnAsPs) { + char ch = ea.ucs4; + if (ch > 0x20 && ch != 0xFF) { + Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont; + g2.setFont(f); + g2.setColor(fgColor); + g2.drawString(String.valueOf(ch), x, y + fontAscent); + } } // Draw underline @@ -945,6 +953,14 @@ public class TerminalPanel extends JPanel { } } + // Draw Vector Graphics Plane overlay if present + if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) { + int gridW = cols * cellWidth; + int gridH = rows * cellHeight; + client.getGraphicsPlane().resize(gridW, gridH); + g2.drawImage(client.getGraphicsPlane().getImage(), ox, oy, gridW, gridH, null); + } + // Draw cursor if (cursorVisible && client.getConnectionState().isFullSession()) { int curAddr = sb.getCursorAddress(); diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/UntrustedCertificateDialog.java b/j3270/src/main/java/org/pubvm/j3270/ui/UntrustedCertificateDialog.java new file mode 100644 index 0000000..ecef814 --- /dev/null +++ b/j3270/src/main/java/org/pubvm/j3270/ui/UntrustedCertificateDialog.java @@ -0,0 +1,153 @@ +package org.pubvm.j3270.ui; + +import javax.swing.*; +import java.awt.*; +import java.security.MessageDigest; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Modal dialog for inspecting and confirming untrusted TLS/SSL server certificates. + */ +public class UntrustedCertificateDialog extends JDialog { + + private boolean accepted = false; + + public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) { + super(parent, "Untrusted SSL/TLS Certificate", true); + buildUI(host, port, chain, exception); + pack(); + setLocationRelativeTo(parent); + setResizable(false); + } + + private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) { + JPanel mainPanel = new JPanel(new BorderLayout(12, 12)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); + mainPanel.setBackground(new Color(30, 30, 30)); + + // Header + JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); + headerPanel.setBackground(new Color(30, 30, 30)); + JLabel iconLabel = new JLabel("⚠️"); + iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); + headerPanel.add(iconLabel); + + JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); + titleBox.setBackground(new Color(30, 30, 30)); + JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); + titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + titleLabel.setForeground(new Color(255, 180, 80)); + titleBox.add(titleLabel); + + JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); + subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + subtitleLabel.setForeground(new Color(180, 180, 180)); + titleBox.add(subtitleLabel); + headerPanel.add(titleBox); + mainPanel.add(headerPanel, BorderLayout.NORTH); + + // Certificate Details Area + X509Certificate cert = (chain != null && chain.length > 0) ? chain[0] : null; + StringBuilder sb = new StringBuilder(); + if (exception != null) { + sb.append("Validation Error:\n ").append(exception.getMessage() != null ? exception.getMessage() : exception.toString()).append("\n\n"); + } + if (cert != null) { + sb.append("Subject:\n ").append(cert.getSubjectX500Principal().getName()).append("\n\n"); + sb.append("Issuer:\n ").append(cert.getIssuerX500Principal().getName()).append("\n\n"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); + sb.append("Validity:\n From: ").append(sdf.format(cert.getNotBefore())) + .append("\n To: ").append(sdf.format(cert.getNotAfter())).append("\n\n"); + sb.append("Serial Number:\n ").append(cert.getSerialNumber().toString(16).toUpperCase()).append("\n\n"); + sb.append("SHA-256 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-256")).append("\n\n"); + sb.append("SHA-1 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-1")); + } else { + sb.append("No peer certificate information available."); + } + + JTextArea detailsArea = new JTextArea(sb.toString()); + detailsArea.setEditable(false); + detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + detailsArea.setBackground(new Color(20, 20, 20)); + detailsArea.setForeground(new Color(210, 210, 210)); + detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + + JScrollPane scrollPane = new JScrollPane(detailsArea); + scrollPane.setPreferredSize(new Dimension(520, 260)); + scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60))); + mainPanel.add(scrollPane, BorderLayout.CENTER); + + // Buttons + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); + buttonPanel.setBackground(new Color(30, 30, 30)); + + JButton cancelBtn = new JButton("Cancel Connection"); + cancelBtn.setBackground(new Color(60, 60, 60)); + cancelBtn.setForeground(new Color(220, 220, 220)); + cancelBtn.addActionListener(e -> { + accepted = false; + dispose(); + }); + + JButton trustBtn = new JButton("Connect Anyway"); + trustBtn.setBackground(new Color(180, 100, 40)); + trustBtn.setForeground(Color.WHITE); + trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); + trustBtn.addActionListener(e -> { + accepted = true; + dispose(); + }); + + buttonPanel.add(cancelBtn); + buttonPanel.add(trustBtn); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + setContentPane(mainPanel); + getRootPane().setDefaultButton(trustBtn); + } + + public boolean isAccepted() { + return accepted; + } + + private static String computeFingerprint(X509Certificate cert, String algorithm) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] digest = md.digest(cert.getEncoded()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < digest.length; i++) { + if (i > 0) sb.append(':'); + sb.append(String.format("%02X", digest[i])); + } + return sb.toString(); + } catch (Exception e) { + return "Unable to compute fingerprint: " + e.getMessage(); + } + } + + /** + * Show the prompt modal dialog on the Swing EDT and return user choice. + */ + public static boolean showPrompt(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) { + if (SwingUtilities.isEventDispatchThread()) { + UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception); + dialog.setVisible(true); + return dialog.isAccepted(); + } else { + AtomicBoolean result = new AtomicBoolean(false); + try { + SwingUtilities.invokeAndWait(() -> { + UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception); + dialog.setVisible(true); + result.set(dialog.isAccepted()); + }); + } catch (Exception e) { + return false; + } + return result.get(); + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java b/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java index 303709f..cb45cb4 100644 --- a/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java +++ b/lib3270j/src/main/java/org/lib3270j/ConnectionConfig.java @@ -11,9 +11,13 @@ public class ConnectionConfig { private String luName = null; private boolean extendedDataStream = true; private boolean useTls = false; + private boolean tlsVerifyCert = true; + private org.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null; + private String sslProtocol = "TLS"; private int connectTimeoutMs = 15000; private int nopIntervalSeconds = 0; private String terminalName = null; // override terminal type string + private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.NONE; public ConnectionConfig() {} @@ -22,12 +26,25 @@ public class ConnectionConfig { this.port = port; } + public ConnectionConfig(String host, int port, boolean useTls) { + this.host = host; + this.port = port; + this.useTls = useTls; + } + public ConnectionConfig(String host, int port, TerminalModel model) { this.host = host; this.port = port; this.model = model; } + public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) { + this.host = host; + this.port = port; + this.model = model; + this.useTls = useTls; + } + public String getHost() { return host; } public void setHost(String host) { this.host = host; } @@ -46,15 +63,79 @@ public class ConnectionConfig { public boolean isUseTls() { return useTls; } public void setUseTls(boolean useTls) { this.useTls = useTls; } + public boolean isTlsVerifyCert() { return tlsVerifyCert; } + public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; } + + public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; } + public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; } + + public String getSslProtocol() { return sslProtocol; } + public void setSslProtocol(String protocol) { this.sslProtocol = protocol; } + public int getConnectTimeoutMs() { return connectTimeoutMs; } public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; } public int getNopIntervalSeconds() { return nopIntervalSeconds; } public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; } + public org.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; } + public void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) { + this.graphicsMode = (mode != null) ? mode : org.lib3270j.graphics.GraphicsMode.NONE; + } + public String getTerminalName() { return terminalName; } public void setTerminalName(String name) { this.terminalName = name; } + /** + * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port") + * or standard "host:port" formats. + */ + public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) { + if (hostStr == null || hostStr.trim().isEmpty()) { + return new ConnectionConfig("localhost", defaultPort, defaultModel); + } + String s = hostStr.trim(); + boolean tls = false; + + // Check TLS prefixes + if (s.startsWith("L:") || s.startsWith("l:")) { + tls = true; + s = s.substring(2); + } else if (s.startsWith("Y:") || s.startsWith("y:")) { + tls = true; + s = s.substring(2); + } else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) { + tls = true; + s = s.substring(4); + } + + String host = s; + int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23); + + // Check for host:port (handle IPv6 [::1]:port) + if (s.startsWith("[") && s.contains("]")) { + int closeBracket = s.indexOf(']'); + host = s.substring(1, closeBracket); + if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') { + try { + port = Integer.parseInt(s.substring(closeBracket + 2)); + } catch (NumberFormatException ignored) {} + } + } else { + int colon = s.lastIndexOf(':'); + if (colon > 0 && colon < s.length() - 1) { + try { + port = Integer.parseInt(s.substring(colon + 1)); + host = s.substring(0, colon); + } catch (NumberFormatException ignored) {} + } + } + + ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); + config.setUseTls(tls); + return config; + } + /** * Get the effective terminal type string to send during negotiation. */ diff --git a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java index 4e9f1ef..def3f0c 100644 --- a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java +++ b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java @@ -46,12 +46,14 @@ public class Telnet3270Client { this.translator = new EbcdicTranslator(); this.screenBuffer = new ScreenBuffer(config.getModel(), translator); this.dsProcessor = new DataStreamProcessor(screenBuffer, translator); + this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode()); this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor); this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm); // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection dsProcessor.setOutputSender(fsm::send3270Data); dsProcessor.setInputProcessor(inputProcessor); + inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane()); } /** @@ -122,6 +124,16 @@ public class Telnet3270Client { /** Get the connection config. */ public ConnectionConfig getConfig() { return config; } + /** Set a custom or interactive TLS certificate verifier callback. */ + public void setTlsCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { + config.setCertificateVerifier(verifier); + } + + /** Get the active SSLSession if connected over TLS, or null. */ + public javax.net.ssl.SSLSession getSslSession() { + return connection.getSslSession(); + } + // ========== Convenience input methods ========== /** Type a character at the cursor position. */ @@ -210,4 +222,16 @@ public class Telnet3270Client { public void sysReq() { inputProcessor.sysReq(); } /** Reset (unlock keyboard). */ public void reset() { inputProcessor.reset(); } + + public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() { + return dsProcessor.getProgramSymbolManager(); + } + + public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() { + return dsProcessor.getGraphicsPlane(); + } + + public org.lib3270j.graphics.GocaDecoder getGocaDecoder() { + return dsProcessor.getGocaDecoder(); + } } diff --git a/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java b/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java index 862d0cb..2317b6e 100644 --- a/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java +++ b/lib3270j/src/main/java/org/lib3270j/charset/EbcdicTranslator.java @@ -85,6 +85,20 @@ public class EbcdicTranslator { return (char) CP037_TO_UNICODE[ebc & 0xFF]; } + /** + * Static helper to translate EBCDIC byte to Unicode character. + */ + public static char toUnicode(int ebc) { + return (char) CP037_TO_UNICODE[ebc & 0xFF]; + } + + /** + * Static helper to translate EBCDIC byte to ASCII character. + */ + public static char ebcdicToAscii(int ebc) { + return toUnicode(ebc); + } + /** * Translate Unicode character to EBCDIC byte. * Returns -1 if the character cannot be mapped. diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index b70fdf2..df9431a 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -38,6 +38,13 @@ public class DataStreamProcessor { // Input processor reference to manage keyboard locking state private org.lib3270j.input.InputProcessor inputProcessor; + // Graphics & Programmed Symbols + private final org.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new org.lib3270j.graphics.ProgramSymbolManager(); + private final org.lib3270j.graphics.GraphicsPlane graphicsPlane = new org.lib3270j.graphics.GraphicsPlane(800, 600); + private final org.lib3270j.graphics.GocaDecoder gocaDecoder = new org.lib3270j.graphics.GocaDecoder(graphicsPlane); + private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream(); + private int currentGocaSubtype = 0; + /** Functional interface for sending output back through the telnet stack. */ @FunctionalInterface public interface OutputSender { @@ -50,6 +57,23 @@ public class DataStreamProcessor { this.qrBuilder = new QueryReplyBuilder(screen); this.outputBuffer = new byte[32768]; this.outputPos = 0; + this.gocaDecoder.setProgramSymbolManager(programSymbolManager); + } + + public QueryReplyBuilder getQueryReplyBuilder() { + return qrBuilder; + } + + public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() { + return programSymbolManager; + } + + public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() { + return graphicsPlane; + } + + public org.lib3270j.graphics.GocaDecoder getGocaDecoder() { + return gocaDecoder; } public void setOutputSender(OutputSender sender) { @@ -94,6 +118,7 @@ public class DataStreamProcessor { int oldRows = screen.getRows(); int oldCols = screen.getCols(); screen.erase(false); + graphicsPlane.clear(); processWrite(data, offset, length, true); if (oldRows != screen.getRows() || oldCols != screen.getCols()) { notifyScreenSizeChanged(); @@ -106,6 +131,7 @@ public class DataStreamProcessor { int oldRows = screen.getRows(); int oldCols = screen.getCols(); screen.erase(true); + graphicsPlane.clear(); processWrite(data, offset, length, true); if (oldRows != screen.getRows() || oldCols != screen.getCols()) { notifyScreenSizeChanged(); @@ -741,6 +767,7 @@ public class DataStreamProcessor { if (fieldLen >= 4) { boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT; screen.erase(alt); + graphicsPlane.clear(); notifyScreenSizeChanged(); } break; @@ -751,6 +778,7 @@ public class DataStreamProcessor { break; case SF_CREATE_PART: // Acknowledged — we use implicit partition + graphicsPlane.clear(); break; case SF_OUTBOUND_DS: if (fieldLen > 5) { @@ -765,6 +793,83 @@ public class DataStreamProcessor { log.fine("SF_TRANSFER_DATA received but ftDft is null"); } break; + case org.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct + if (fieldLen > 3) { + byte[] psData = new byte[fieldLen - 3]; + System.arraycopy(data, pos + 3, psData, 0, psData.length); + programSymbolManager.loadps(psData); + notifyScreenUpdated(); + } + break; + case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix + if (fieldLen >= 4) { + int sfSubId = data[pos + 3] & 0xFF; + switch (sfSubId) { + case org.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols + if (fieldLen > 4) { + byte[] psData = new byte[fieldLen - 4]; + System.arraycopy(data, pos + 4, psData, 0, psData.length); + programSymbolManager.loadps(psData); + notifyScreenUpdated(); + } + break; + case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders) + case org.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments) + case org.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders) + int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0; + int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4); + int orderLen = Math.max(0, fieldLen - (orderOffset - pos)); + + graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); + + if (flags == 0x80) { // SPAN_FIRST + gocaAccumulator.reset(); + currentGocaSubtype = sfSubId; + if (orderLen > 0) { + gocaAccumulator.write(data, orderOffset, orderLen); + } + } else if (flags == 0x00) { // SPAN_MIDDLE + if (orderLen > 0) { + gocaAccumulator.write(data, orderOffset, orderLen); + } + } else if (flags == 0x40) { // SPAN_LAST + if (orderLen > 0) { + gocaAccumulator.write(data, orderOffset, orderLen); + } + byte[] fullStream = gocaAccumulator.toByteArray(); + gocaAccumulator.reset(); + if (currentGocaSubtype == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) { + gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length); + } else { + gocaDecoder.decodeStream(fullStream, 0, fullStream.length); + } + notifyScreenUpdated(); + } else { // SPAN_ONLY (0xC0) + gocaAccumulator.reset(); + if (sfSubId == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) { + gocaDecoder.processProcedureOrders(data, orderOffset, orderLen); + } else { + gocaDecoder.decodeStream(data, orderOffset, orderLen); + } + notifyScreenUpdated(); + } + break; + } + default: + log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId)); + break; + } + } + break; + } + case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA + case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics + if (fieldLen > 3) { + graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); + gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); + notifyScreenUpdated(); + } + break; default: log.fine("Unknown SF id: " + String.format("0x%02x", sfId)); break; @@ -887,4 +992,10 @@ public class DataStreamProcessor { l.onScreenSizeChanged(screen.getRows(), screen.getCols()); } } + + private void notifyScreenUpdated() { + for (ScreenUpdateListener l : screenListeners) { + l.onScreenUpdated(); + } + } } diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java index 6ae25c2..32b6f70 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -3,12 +3,13 @@ package org.lib3270j.datastream; import java.io.ByteArrayOutputStream; import java.util.logging.Logger; +import org.lib3270j.graphics.GraphicsMode; import org.lib3270j.screen.ScreenBuffer; import static org.lib3270j.protocol.DS3270Constants.*; /** * Builds Query Reply structured fields in response to host Read Partition queries. - * Equivalent to the do_qr_* functions in sf.c. + * Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout. */ public class QueryReplyBuilder { @@ -21,9 +22,10 @@ public class QueryReplyBuilder { private static final int Yr_3279_2 = 0x0002006f; private final ScreenBuffer screen; + private GraphicsMode graphicsMode = GraphicsMode.NONE; - // Supported query reply codes (must match what we send in summary) - private static final int[] SUPPORTED_QR = { + // Base query reply codes (text mode) + private static final int[] SUPPORTED_QR_BASE = { QR_SUMMARY, // 0x80 — summary must list itself QR_USABLE_AREA, // 0x81 QR_ALPHA_PART, // 0x84 @@ -35,10 +37,45 @@ public class QueryReplyBuilder { QR_IMP_PART, // 0xa6 }; + // Vector graphics query reply codes matching HOD DS3270.java line 1723 + private static final int[] SUPPORTED_QR_VECTOR = { + QR_SUMMARY, // 0x80 + QR_USABLE_AREA, // 0x81 + QR_ALPHA_PART, // 0x84 + QR_CHARSETS, // 0x85 + QR_COLOR, // 0x86 + QR_HIGHLIGHTING, // 0x87 + QR_REPLY_MODES, // 0x88 + QR_SAVE_RESTORE, // 0x8c + QR_DDM, // 0x95 + QR_TRANSPARENCY, // 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 + }; + public QueryReplyBuilder(ScreenBuffer screen) { this.screen = screen; } + public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) { + this.screen = screen; + this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE; + } + + public GraphicsMode getGraphicsMode() { + return graphicsMode; + } + + public void setGraphicsMode(GraphicsMode graphicsMode) { + this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE; + } + /** * Build all query replies as a single AID_SF + structured field response. */ @@ -75,7 +112,20 @@ public class QueryReplyBuilder { // Implicit Partition appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); - log.info("Built " + out.size() + " bytes of all query replies"); + // Vector Graphics QRs if enabled + if (graphicsMode.isVectorGraphicsEnabled()) { + appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); + appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); + appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); + appendQueryReply(out, QR_GRAPHICS, buildGraphics()); + appendQueryReply(out, QR_GIMAGE, buildGImage()); + appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); + appendOemFmt(out); + appendQueryReply(out, QR_GCOLOR, buildGColor()); + appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); + } + + log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")"); return out.toByteArray(); } @@ -116,14 +166,75 @@ 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()); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } + break; case QR_DDM: appendQueryReply(out, QR_DDM, buildDdm(4096)); break; + case QR_TRANSPARENCY: + 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: - appendQueryReply(out, QR_RPQNAMES, new byte[0]); + if (graphicsMode.isVectorGraphicsEnabled()) { + appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } + break; + case QR_GRAPHICS: + if (graphicsMode.isVectorGraphicsEnabled()) { + appendQueryReply(out, QR_GRAPHICS, buildGraphics()); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } + break; + case QR_GIMAGE: + if (graphicsMode.isVectorGraphicsEnabled()) { + appendQueryReply(out, QR_GIMAGE, buildGImage()); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } + break; + case QR_AUX_DEV: + if (graphicsMode.isVectorGraphicsEnabled()) { + appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } + break; + case QR_OEM_FMT: + if (graphicsMode.isVectorGraphicsEnabled()) { + appendOemFmt(out); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } + break; + case QR_GCOLOR: + 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()); + } else { + appendQueryReply(out, QR_NULL, new byte[0]); + } break; default: // Unsupported query reply code — emit QR_NULL @@ -148,7 +259,8 @@ public class QueryReplyBuilder { private byte[] buildSummary() { ByteArrayOutputStream out = new ByteArrayOutputStream(); - for (int code : SUPPORTED_QR) { + int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; + for (int code : codes) { out.write(code); } return out.toByteArray(); @@ -184,66 +296,81 @@ public class QueryReplyBuilder { private byte[] buildAlphaPartitions(int maxRows) { int bufSize = screen.getMaxCols() * screen.getMaxRows(); ByteArrayOutputStream out = new ByteArrayOutputStream(4); - out.write(0x00); // max partitions (1 partition) + out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition) out.write((bufSize >> 8) & 0xFF); // total partition storage high out.write(bufSize & 0xFF); // total partition storage low - out.write(0x00); // no special features + out.write(0x00); // flags return out.toByteArray(); } private byte[] buildCharsets() { - ByteArrayOutputStream out = new ByteArrayOutputStream(32); + if (graphicsMode.isVectorGraphicsEnabled() || graphicsMode == GraphicsMode.NONE) { + // Standard 3179G / Base character sets (matches HOD QR_CHARSETS_S_STRING, 27 bytes total) + ByteArrayOutputStream out = new ByteArrayOutputStream(23); + out.write(0x82); // flags: GE, CGCSGID present + out.write(0x00); // more flags + out.write(SW_3279_2); // SDW - default char width (9) + out.write(14); // SDH - default char height (14) + out.write(0x00); // LoadPS format (0x00) + out.write(0x00); + out.write(0x00); + out.write(0x00); + out.write(0x07); // DL = 7 + // Set 0 (Base EBCDIC) + out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4); + // Set 1 (APL/Text) + out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); + return out.toByteArray(); + } + + // Programmed Symbols mode (3279 PS) + ByteArrayOutputStream out = new ByteArrayOutputStream(65); out.write(0x82); // flags: GE, CGCSGID present out.write(0x00); // more flags - out.write(SW_3279_2); // SDW - default char width - out.write(SH_3279_2); // SDH - default char height - out.write(0x00); // Load PS format types supported: none + out.write(SW_3279_2); // SDW (9) + out.write(14); // SDH (14) + out.write(0x0a); // Load PS format types supported: Format 1 and Format 3 out.write(0x00); // Load PS device type (high) out.write(0x00); // Load PS device type (low) out.write(0x00); // reserved - out.write(0x07); // DL = 7 bytes per descriptor (non-DBCS) + out.write(0x07); // DL = 7 bytes per descriptor // Descriptor 1 (SET 0): default character set - out.write(0x00); // SET 0 - out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare - out.write(0x00); // LCID 0 - out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET) - out.write(0xb9); - out.write(0x00); - out.write(0x25); + out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4); // Descriptor 2 (SET 1): APL/GE character set - out.write(0x01); // SET 1 - out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare - out.write(0xf1); // LCID 0xf1 - out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136 - out.write(0xc3); - out.write(0x01); - out.write(0x36); + out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); + // Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) + out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + // Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) + out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); return out.toByteArray(); } private byte[] buildColor() { - ByteArrayOutputStream out = new ByteArrayOutputStream(36); - int colorMax = 16; - out.write(0x00); // no options - out.write(colorMax); // number of colors - out.write(0x00); // default color pair: attribute - out.write(0xf0 + HOST_COLOR_GREEN); // default color: green - for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) { - out.write(i); // color attribute value - out.write(i); // maps to itself (color mode) - } - return out.toByteArray(); + // Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) + return new byte[] { + 0x00, 0x08, 0x00, (byte) 0xF4, + (byte) 0xF1, (byte) 0xF1, // Blue + (byte) 0xF2, (byte) 0xF2, // Red + (byte) 0xF3, (byte) 0xF3, // Pink + (byte) 0xF4, (byte) 0xF4, // Green + (byte) 0xF5, (byte) 0xF5, // Turquoise + (byte) 0xF6, (byte) 0xF6, // Yellow + (byte) 0xF7, (byte) 0xF7 // Neutral/White + }; } private byte[] buildHighlighting() { - ByteArrayOutputStream out = new ByteArrayOutputStream(11); - out.write(5); // 5 pairs - out.write(XAH_DEFAULT); out.write(XAH_NORMAL); - out.write(XAH_BLINK); out.write(XAH_BLINK); - out.write(XAH_REVERSE); out.write(XAH_REVERSE); - out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE); - out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY); - return out.toByteArray(); + // Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) + return new byte[] { + 0x04, 0x00, (byte) 0xF0, + (byte) 0xF1, (byte) 0xF1, + (byte) 0xF2, (byte) 0xF2, + (byte) 0xF4, (byte) 0xF4 + }; } private byte[] buildReplyModes() { @@ -265,21 +392,20 @@ public class QueryReplyBuilder { private byte[] buildImplicitPartition(int maxCols, int maxRows) { ByteArrayOutputStream out = new ByteArrayOutputStream(22); - // Implicit partition sizes SDP (Self-Defining Parameter) + // Implicit partition sizes, 2 self-defining parameters + // SDP 1: Default screen size (Model 2: 80x24) out.write(0x00); // flags out.write(0x00); // reserved - // SDP 1: Implicit partition sizes - out.write(0x00); // SDP length high byte - out.write(0x0b); // SDP length low byte (11 bytes: 2 len + 1 type + 1 res + 8 dims) + out.write(0x0b); // SDP length (11 bytes) out.write(0x01); // SDP type: implicit partition sizes out.write(0x00); // reserved - // Default dimensions (Model 2: 80x24) + // Default size out.write((MODEL_2_COLS >> 8) & 0xFF); out.write(MODEL_2_COLS & 0xFF); out.write((MODEL_2_ROWS >> 8) & 0xFF); out.write(MODEL_2_ROWS & 0xFF); - // Alternate dimensions + // Alternate size (Model 4: 80x43, Model 5: 132x27, etc.) out.write((maxCols >> 8) & 0xFF); out.write(maxCols & 0xFF); out.write((maxRows >> 8) & 0xFF); @@ -287,4 +413,80 @@ public class QueryReplyBuilder { return out.toByteArray(); } + + private byte[] buildGraphics() { + return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 }; + } + + private byte[] buildGImage() { + return new byte[]{ + 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00, + 0x06, 0x40, 0x06, 0x40, 0x06, 0x01, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0 + }; + } + + private byte[] buildAuxDev() { + return new byte[]{ + 0x00, 0x09, 0x00, 0x07, + 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, + 0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08 + }; + } + + private byte[] buildSaveRestore() { + // HOD DS3270.java line 1768: 6 bytes payload + return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + } + + private byte[] buildTransparency() { + // HOD DS3270.java line 1782: 2 bytes payload + return new byte[]{ 0x00, 0x00 }; + } + + 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[]{ + 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF + }); + appendQueryReply(out, QR_OEM_FMT, new byte[]{ + 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 + }); + appendQueryReply(out, QR_OEM_FMT, 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[]{ + 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C + }); + } + + private byte[] buildGColor() { + 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); + + for (int i = 0; i < 16; i++) { + out.write(0x00); + out.write(i); + java.awt.Color c = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i]; + out.write(c.getRed()); + out.write(c.getGreen()); + out.write(c.getBlue()); + out.write(0x00); // 6th byte in HOD color table + } + return out.toByteArray(); + } + + private byte[] buildGSymbols() { + 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 + }; + } } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java new file mode 100644 index 0000000..2bcc082 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java @@ -0,0 +1,173 @@ +package org.lib3270j.graphics; + +/** + * Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture) + * vector graphics and Programmed Symbols (PS). + */ +public final class GocaConstants { + + private GocaConstants() {} + + // Structured Field IDs + public static final int SF_LOADPS = 0x0F; // 2-byte Structured Field prefix (or 0x06 for direct Load PS) + public static final int SF_LOADPS_DIRECT = 0x06; // Load Programmed Symbols direct SFID + public static final int SF_OBJCNTL = 0x24; // Object Control + public static final int SF_OBJDATA = 0x85; // Graphics Object Data + public static final int SF_3270_G = 0x20; // 3270 Graphics / Picture + + // Structured Field Sub-IDs (for SF 0x0F) + public static final int SF_LOADPS_SUB = 0x06; // Load Programmed Symbols + public static final int SF_LOADLT_SUB = 0x07; // Load Line Type / Symbol Set + public static final int SF_OBJDATA_SUB = 0x0F; // Graphics Object Data (GOCA draw orders) + public static final int SF_OBJPICT_SUB = 0x10; // Graphics Object Picture (Segment draw orders) + public static final int SF_OBJCNTL_SUB = 0x11; // Graphics Object Control (Procedure orders) + + // Procedure Orders (for SF_OBJCNTL_SUB 0x11) + public static final int P_NOP1 = 0x00; // Procedure NOOP + public static final int P_COMT = 0x01; // Procedure Comment + public static final int P_ATTCUR = 0x08; // Attach Graphic Cursor + public static final int P_DETCUR = 0x09; // Detach Graphic Cursor + public static final int P_ERASE = 0x0A; // Erase Graphics Presentation Space + public static final int P_STOPDR = 0x0F; // Stop Draw + public static final int P_SCUDEF = 0x21; // Set Current Defaults + public static final int P_BEGPROC = 0x30; // Begin Procedure + public static final int P_SETCUR = 0x31; // Set Graphic Cursor Position + + // Coordinate space + public static final int VIRTUAL_COORD_MAX = 4096; + + // GOCA Drawing / Segment Orders + public static final int G_NOP1 = 0x00; // NOOP 1-byte + public static final int G_COMT = 0x01; // Comment + public static final int G_GSMC = 0x07; // Set Marker Color + public static final int G_GSPS = 0x08; // Set Pattern Set + public static final int G_GSCOL = 0x0A; // Set Color + public static final int G_GSMX = 0x0C; // Set Foreground Mix + public static final int G_GSBMX = 0x0D; // Set Background Mix + public static final int G_GSFLW = 0x11; // Set Fractional Line Width + public static final int G_GSLT = 0x18; // Set Line Type + public static final int G_GSLW = 0x19; // Set Line Width + public static final int G_GSMS = 0x1B; // Set Marker Size + public static final int G_GSCP = 0x21; // Set Current Position + public static final int G_GSAP = 0x22; // Arc Parameters + public static final int G_GSECOL = 0x26; // Set Extended Color + public static final int G_GSVW = 0x27; // Set Viewing Window + public static final int G_GSPT = 0x28; // Set Pattern Symbol + public static final int G_GSMT = 0x29; // Set Marker Symbol / Type + public static final int G_GSCH = 0x33; // Set Character Cell + public static final int G_GSCA = 0x34; // Set Character Angle + public static final int G_GSCR = 0x35; // Set Character Shear + public static final int G_GSMCEL = 0x37; // Set Marker Cell + public static final int G_GSCS = 0x38; // Set Character Set + public static final int G_GSMP = 0x39; // Set Marker Precision + public static final int G_GSCD = 0x3A; // Set Character Direction + public static final int G_GSCC = 0x3B; // Set Character Precision + public static final int G_GSMS_SET = 0x3C; // Set Marker Set + public static final int G_ENDPROLOGUE = 0x3E; // End Prologue + public static final int G_GPOP = 0x3F; // Pop Attribute + public static final int G_GEAR = 0x60; // End Area + public static final int G_GBAR = 0x68; // Begin Area + public static final int G_BEGSEGM = 0x70; // Begin Segment + public static final int G_ENDSEGM = 0x71; // End Segment + public static final int G_GERASE = 0x7E; // Erase Graphics Plane + public static final int G_GCLINE = 0x81; // Line at Current Position + public static final int G_GCMRK = 0x82; // Marker at Current Position + public static final int G_GCCHST = 0x83; // Character String at Current Position + public static final int G_GCFLT = 0x85; // Fillet at Current Position + public static final int G_GCARC = 0x86; // Partial Arc at Current Position + public static final int G_GCFARC = 0x87; // Full Arc at Current Position + public static final int G_GEIMG = 0x91; // End Image + public static final int G_GIMD = 0x92; // Image Data + public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position + public static final int G_GLINE = 0xC1; // Line (Absolute) + public static final int G_GMRK = 0xC2; // Marker (Absolute) + public static final int G_GCHST = 0xC3; // Character String (Absolute) + public static final int G_GFLT = 0xC5; // Fillet (Absolute) + public static final int G_GARC = 0xC6; // Partial Arc (Absolute) + public static final int G_GFARC = 0xC7; // Full Arc (Absolute) + public static final int G_GBIMG = 0xD1; // Begin Image + public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start) + + // Line Types (GSLT) + public static final int LT_DEFAULT = 0; + public static final int LT_DOT = 1; + public static final int LT_SHORTDASH= 2; + public static final int LT_DASHDOT = 3; + public static final int LT_DOUBLEDOT= 4; + public static final int LT_LONGDASH = 5; + public static final int LT_DASHDOUBLEDOT = 6; + public static final int LT_SOLID = 7; + + // Line Widths (GSLW) + public static final int LW_DEFAULT = 0; + public static final int LW_NORMAL = 1; + public static final int LW_THICK = 2; + + // Fill Patterns (GSPT) + public static final int PT_DEFAULT = 0; + public static final int PT_D1 = 1; + public static final int PT_D2 = 2; + public static final int PT_D3 = 3; + public static final int PT_D4 = 4; + public static final int PT_D5 = 5; + public static final int PT_D6 = 6; + public static final int PT_D7 = 7; + public static final int PT_D8 = 8; + public static final int PT_VERT_LINE = 9; + public static final int PT_HORIZ_LINE = 10; + public static final int PT_DIAG_BLTR = 11; + public static final int PT_DIAG_BLTR2 = 12; + public static final int PT_DIAG_TLBR = 13; + public static final int PT_DIAG_TLBR2 = 14; + public static final int PT_EMPTY = 15; + public static final int PT_SOLID = 16; + + // Marker Symbols (GSMT) + public static final int MK_DEFAULT = 0; + public static final int MK_CROSS = 1; // x + public static final int MK_PLUS = 2; // + + public static final int MK_DIAMOND = 3; // <> + public static final int MK_SQUARE = 4; // [] + public static final int MK_6STAR = 5; // * 6-point + public static final int MK_8STAR = 6; // * 8-point + public static final int MK_SDIAMOND = 7; // solid diamond + public static final int MK_SSQUARE = 8; // solid square + public static final int MK_DOT = 9; // . + public static final int MK_CIRCLE = 10;// o + + // Character Direction (GSCD) + public static final int CD_DEFAULT = 0; + public static final int CD_LR = 1; // Left to Right + public static final int CD_TB = 2; // Top to Bottom + public static final int CD_RL = 3; // Right to Left + public static final int CD_BT = 4; // Bottom to Top + + // Foreground / Background Mix Modes (GSMX / GSBMX) + public static final int MIX_DEFAULT = 0; + public static final int MIX_OR = 1; + public static final int MIX_OVER = 2; + public static final int MIX_LEAVE = 3; + public static final int MIX_XOR = 4; + public static final int MIX_UNDER = 5; + + // Graphic Colors (IBM 3179G / HOD 16-color table) + public static final java.awt.Color[] GOCA_COLORS = new java.awt.Color[] { + new java.awt.Color(0, 255, 0), // 0: Default (Green) + new java.awt.Color(120, 144, 240), // 1: Blue + new java.awt.Color(255, 0, 0), // 2: Red + new java.awt.Color(255, 0, 255), // 3: Pink / Magenta + new java.awt.Color(0, 255, 0), // 4: Green + new java.awt.Color(0, 255, 255), // 5: Turquoise / Cyan + new java.awt.Color(255, 255, 0), // 6: Yellow + new java.awt.Color(255, 255, 255), // 7: Neutral White + new java.awt.Color(0, 0, 0), // 8: Black + new java.awt.Color(0, 0, 128), // 9: Deep Blue + new java.awt.Color(128, 0, 0), // 10: Orange / Dark Red + new java.awt.Color(128, 0, 128), // 11: Purple + new java.awt.Color(0, 128, 0), // 12: Pale Green + new java.awt.Color(0, 128, 128), // 13: Pale Cyan + new java.awt.Color(215, 151, 0), // 14: Mustard + new java.awt.Color(192, 192, 192), // 15: Grey / Light White + new java.awt.Color(73, 36, 0) // 16: Brown + }; +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java new file mode 100644 index 0000000..21751bf --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java @@ -0,0 +1,819 @@ +package org.lib3270j.graphics; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.util.logging.Logger; + +import org.lib3270j.charset.EbcdicTranslator; + +/** + * Interprets GOCA (Graphics Object Content Architecture) drawing orders + * and updates the GraphicsPlane. + */ +public class GocaDecoder { + + private static final Logger logger = Logger.getLogger(GocaDecoder.class.getName()); + + private final GraphicsPlane plane; + + // Drawing state + private int curX = 0; + private int curY = 0; + private Color curColor = Color.GREEN; + private int lineType = GocaConstants.LT_SOLID; + private int lineWidth = GocaConstants.LW_NORMAL; + private int markerType = GocaConstants.MK_PLUS; + private int markerSize = 5; + private Color markerColor = Color.GREEN; + private int pattern = GocaConstants.PT_SOLID; + private Color fillColor = Color.GREEN; + private int charDir = GocaConstants.CD_LR; + private double charAngle = 0.0; + private int charWidth = 9; + private int charHeight = 16; + private int charSet = 0; + private int arcParamP = 1; + private int arcParamQ = 1; + private int arcParamR = 0; + private int arcParamS = 0; + + private ProgramSymbolManager programSymbolManager; + + // Area accumulation + private boolean inArea = false; + private boolean areaDrawBoundary = true; + private final List areaPointsX = new ArrayList<>(); + private final List areaPointsY = new ArrayList<>(); + + // Image accumulation + private boolean inImage = false; + private int imgX = 0; + private int imgY = 0; + private int imgWidth = 0; + private int imgHeight = 0; + private final List imgBuffer = new ArrayList<>(); + + public GocaDecoder(GraphicsPlane plane) { + this.plane = plane; + } + + public void setProgramSymbolManager(ProgramSymbolManager psm) { + this.programSymbolManager = psm; + } + + public GraphicsPlane getGraphicsPlane() { + return plane; + } + + public int getCurX() { + return curX; + } + + public int getCurY() { + return curY; + } + + public synchronized void resetDefaults() { + curX = 0; + curY = 0; + resetAttributes(); + } + + public synchronized void resetAttributes() { + curColor = getColor(0); + lineType = GocaConstants.LT_SOLID; + lineWidth = GocaConstants.LW_NORMAL; + markerType = GocaConstants.MK_PLUS; + markerSize = 5; + markerColor = curColor; + pattern = GocaConstants.PT_SOLID; + fillColor = curColor; + charDir = GocaConstants.CD_LR; + charAngle = 0.0; + charSet = 0; + inArea = false; + areaPointsX.clear(); + areaPointsY.clear(); + inImage = false; + imgBuffer.clear(); + } + + private byte[] partialOrderBuffer = new byte[0]; + + private int getOrderLength(byte[] data, int idx, int end) { + int order = data[idx] & 0xFF; + if (order == GocaConstants.G_NOP1 || order == 0xFF) { + return 1; + } + if (order == GocaConstants.G_GEAR || + order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE || + order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) { + return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1; + } + if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS || + order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX || + order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW || + order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW || + order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT || + order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS || + order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD || + order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET || + order == GocaConstants.G_GBAR) { + return 2; + } + if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) { + return 10; + } + if (idx + 1 >= end) { + return -1; + } + return (data[idx + 1] & 0xFF) + 2; + } + + /** + * Decodes a stream of GOCA drawing orders. + */ + public synchronized void decodeStream(byte[] data, int offset, int length) { + if (data == null || length <= 0 || offset < 0 || offset + length > data.length) { + return; + } + + byte[] inputData; + int idx; + int end; + + if (partialOrderBuffer.length > 0) { + inputData = new byte[partialOrderBuffer.length + length]; + System.arraycopy(partialOrderBuffer, 0, inputData, 0, partialOrderBuffer.length); + System.arraycopy(data, offset, inputData, partialOrderBuffer.length, length); + idx = 0; + end = inputData.length; + partialOrderBuffer = new byte[0]; + } else { + inputData = data; + idx = offset; + end = offset + length; + } + + while (idx < end) { + int order = inputData[idx] & 0xFF; + int orderLen = getOrderLength(inputData, idx, end); + + if (orderLen == -1 || idx + orderLen > end) { + int remaining = end - idx; + partialOrderBuffer = new byte[remaining]; + System.arraycopy(inputData, idx, partialOrderBuffer, 0, remaining); + break; + } + + int payloadLen = (orderLen >= 2) ? (inputData[idx + 1] & 0xFF) : 0; + + switch (order) { + case GocaConstants.G_NOP1: + case 0xFF: { + idx++; + break; + } + case GocaConstants.G_GEAR: { + endArea(); + idx += orderLen; + break; + } + case GocaConstants.G_GEIMG: { + endImage(); + idx += orderLen; + break; + } + case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) + if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) { + resetAttributes(); + } + idx += orderLen; + break; + } + case GocaConstants.G_ENDSEGM: { // End Segment (0x71) + idx += orderLen; + break; + } + case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E) + idx += orderLen; + break; + } + case GocaConstants.G_COMT: { // Comment (0x01) + idx += orderLen; + break; + } + case GocaConstants.G_GSCP: { // Set Current Position (0x21) + if (payloadLen >= 4 && idx + 5 < end) { + curX = readCoord(inputData, idx + 2); + curY = readCoord(inputData, idx + 4); + } + idx += orderLen; + break; + } + case GocaConstants.G_GSAP: { // Arc Parameters (0x22) + if (idx + 9 < end) { + arcParamP = readCoord(inputData, idx + 2); + arcParamQ = readCoord(inputData, idx + 4); + arcParamR = readCoord(inputData, idx + 6); + arcParamS = readCoord(inputData, idx + 8); + } + idx += orderLen; + break; + } + case GocaConstants.G_GSVW: { // Set Viewing Window (0x27) + idx += orderLen; + break; + } + case GocaConstants.G_GSCA: { // Set Character Angle (0x34) + if (payloadLen >= 4 && idx + 5 < end) { + int ax = readCoord(inputData, idx + 2); + int ay = readCoord(inputData, idx + 4); + if (ax != 0 || ay != 0) { + charAngle = Math.toDegrees(Math.atan2(ay, ax)); + } + } + idx += orderLen; + break; + } + case GocaConstants.G_GSCH: { // Set Character Cell (0x33) + if (payloadLen >= 4 && idx + 5 < end) { + charWidth = readCoord(inputData, idx + 2); + charHeight = readCoord(inputData, idx + 4); + } + idx += orderLen; + break; + } + case GocaConstants.G_GSCR: { // Set Character Shear (0x35) + idx += orderLen; + break; + } + case GocaConstants.G_GSCOL: { // Set Color (0x0A) + int colIdx = inputData[idx + 1] & 0xFF; + curColor = getColor(colIdx); + fillColor = curColor; + idx += orderLen; + break; + } + case GocaConstants.G_GSECOL: { // Set Extended Color (0x26) + if (payloadLen >= 2 && idx + 3 < end) { + int colIdx = inputData[idx + 3] & 0xFF; + curColor = getColor(colIdx); + fillColor = curColor; + } + idx += orderLen; + break; + } + case GocaConstants.G_GSLT: { // Set Line Type (0x18) + lineType = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSLW: { // Set Line Width (0x19) + lineWidth = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSMC: { // Set Marker Color (0x07) + int colIdx = inputData[idx + 1] & 0xFF; + markerColor = getColor(colIdx); + idx += orderLen; + break; + } + case GocaConstants.G_GSMS: { // Set Marker Size (0x1B) + markerSize = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSMT: { // Set Marker Type (0x29) + markerType = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSPS: { // Set Pattern Set (0x08) + pattern = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28) + pattern = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSCD: { // Set Character Direction (0x3A) + charDir = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case GocaConstants.G_GSCS: { // Set Character Set (0x38) + charSet = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } + case 0x04: + case GocaConstants.G_GSMX: + case GocaConstants.G_GSBMX: + case GocaConstants.G_GSFLW: + case GocaConstants.G_GSMP: + case GocaConstants.G_GSCC: + case GocaConstants.G_GSMS_SET: + case GocaConstants.G_GPOP: { + idx += orderLen; + break; + } + case GocaConstants.G_GBAR: { // Begin Area (0x68) + int flags = inputData[idx + 1] & 0xFF; + beginArea((flags & 0x40) != 0); + idx += orderLen; + break; + } + case GocaConstants.G_GLINE: { // Line Absolute (0xC1) + if (payloadLen >= 4) { + processLine(inputData, idx + 2, payloadLen, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCLINE: { // Line Current Position (0x81) + if (payloadLen >= 4) { + processLine(inputData, idx + 2, payloadLen, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GRLINE: { // Relative Line Absolute Start (0xE1) + if (payloadLen >= 4) { + processRelativeLine(inputData, idx + 2, payloadLen, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCRLIN: { // Relative Line Current Position (0xA1) + if (payloadLen >= 2) { + processRelativeLine(inputData, idx + 2, payloadLen, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GFARC: { // Full Arc Absolute (0xC7) + if (payloadLen >= 4) { + processArc(inputData, idx + 2, payloadLen, false, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCFARC: { // Full Arc Current Position (0x87) + if (payloadLen >= 2) { + processArc(inputData, idx + 2, payloadLen, true, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GARC: { // Partial Arc Absolute (0xC6) + if (payloadLen >= 8) { + processArc(inputData, idx + 2, payloadLen, false, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCARC: { // Partial Arc Current Position (0x86) + if (payloadLen >= 4) { + processArc(inputData, idx + 2, payloadLen, true, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GFLT: { // Fillet Absolute (0xC5) + if (payloadLen >= 4) { + processFillet(inputData, idx + 2, payloadLen, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCFLT: { // Fillet Current Position (0x85) + if (payloadLen >= 4) { + processFillet(inputData, idx + 2, payloadLen, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GMRK: { // Marker Absolute (0xC2) + if (payloadLen >= 4) { + processMarker(inputData, idx + 2, payloadLen, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCMRK: { // Marker Current Position (0x82) + if (payloadLen >= 4) { + processMarker(inputData, idx + 2, payloadLen, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCHST: { // Character String Absolute (0xC3) + if (payloadLen >= 4) { + processText(inputData, idx + 2, payloadLen, false); + } + idx += orderLen; + break; + } + case GocaConstants.G_GCCHST: { // Character String Current Position (0x83) + if (payloadLen >= 0) { + processText(inputData, idx + 2, payloadLen, true); + } + idx += orderLen; + break; + } + case GocaConstants.G_GBIMG: { // Begin Image (0xD1) + if (idx + 9 < end) { + int x = readCoord(inputData, idx + 2); + int y = readCoord(inputData, idx + 4); + int w = readCoord(inputData, idx + 6); + int h = readCoord(inputData, idx + 8); + beginImage(x, y, w, h); + } + idx += orderLen; + break; + } + case GocaConstants.G_GIMD: { // Image Data (0x92) + if (inImage) { + for (int k = 0; k < payloadLen; k++) { + imgBuffer.add(inputData[idx + 2 + k]); + } + } + idx += orderLen; + break; + } + default: { + idx += orderLen; + break; + } + } + } + } + + /** + * Decodes a stream of Object Control Procedure Orders (subtype 0x11). + */ + public synchronized void processProcedureOrders(byte[] data, int offset, int length) { + if (data == null || length <= 0 || offset < 0 || offset + length > data.length) { + return; + } + + int idx = offset; + int end = offset + length; + + while (idx < end) { + int order = data[idx] & 0xFF; + + switch (order) { + case GocaConstants.P_NOP1: + case GocaConstants.P_ATTCUR: + case GocaConstants.P_DETCUR: + case GocaConstants.P_STOPDR: { + idx++; + break; + } + case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space + plane.clear(); + resetDefaults(); + idx++; + break; + } + case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12) + int len = 12; + if (idx + 1 < end && data[idx + 1] != 0) { + len = (data[idx + 1] & 0xFF) + 2; + } + idx += len; + break; + } + case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults + if (idx + 1 < end) { + int len = data[idx + 1] & 0xFF; + if (idx + 2 + len <= end) { + decodeStream(data, idx + 2, len); + } + idx += 2 + len; + } else { + idx++; + } + break; + } + case GocaConstants.P_COMT: + case GocaConstants.P_SETCUR: { + if (idx + 1 < end) { + int len = data[idx + 1] & 0xFF; + idx += 2 + len; + } else { + idx++; + } + break; + } + default: { + if (idx + 1 < end) { + int len = data[idx + 1] & 0xFF; + idx += 2 + len; + } else { + idx++; + } + break; + } + } + } + } + + private void beginArea(boolean drawBoundary) { + this.inArea = true; + this.areaDrawBoundary = drawBoundary; + this.fillColor = this.curColor; + this.areaPointsX.clear(); + this.areaPointsY.clear(); + } + + private void endArea() { + if (!inArea || areaPointsX.size() < 3) { + inArea = false; + areaPointsX.clear(); + areaPointsY.clear(); + return; + } + int n = areaPointsX.size(); + int[] px = new int[n]; + int[] py = new int[n]; + for (int i = 0; i < n; i++) { + px[i] = plane.mapX(areaPointsX.get(i)); + py[i] = plane.mapY(areaPointsY.get(i)); + } + + plane.fillArea(px, py, n, fillColor, pattern, areaDrawBoundary, curColor, lineType, lineWidth); + inArea = false; + areaPointsX.clear(); + areaPointsY.clear(); + } + + private void addAreaPoint(int x, int y) { + if (inArea) { + areaPointsX.add(x); + areaPointsY.add(y); + } + } + + private void beginImage(int x, int y, int w, int h) { + this.inImage = true; + this.imgX = x; + this.imgY = y; + this.imgWidth = w; + this.imgHeight = h; + this.imgBuffer.clear(); + } + + private void endImage() { + if (!inImage || imgWidth <= 0 || imgHeight <= 0 || imgBuffer.isEmpty()) { + inImage = false; + return; + } + byte[] bytes = new byte[imgBuffer.size()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = imgBuffer.get(i); + } + + int px = plane.mapX(imgX); + int py = plane.mapY(imgY); + plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor); + inImage = false; + imgBuffer.clear(); + } + + private void processLine(byte[] data, int off, int len, boolean fromCurPos) { + int pos = off; + int end = off + len; + + int startX = curX; + int startY = curY; + + if (!fromCurPos && pos + 4 <= end) { + startX = readCoord(data, pos); + startY = readCoord(data, pos + 2); + pos += 4; + } + + if (inArea) { + addAreaPoint(startX, startY); + } + + while (pos + 4 <= end) { + int nextX = readCoord(data, pos); + int nextY = readCoord(data, pos + 2); + pos += 4; + + if (inArea) { + addAreaPoint(nextX, nextY); + } else { + plane.drawLine(plane.mapX(startX), plane.mapY(startY), + plane.mapX(nextX), plane.mapY(nextY), + curColor, lineType, lineWidth); + } + + startX = nextX; + startY = nextY; + } + + curX = startX; + curY = startY; + } + + private void processRelativeLine(byte[] data, int off, int len, boolean fromCurPos) { + int pos = off; + int end = off + len; + + int startX = curX; + int startY = curY; + + if (!fromCurPos && pos + 4 <= end) { + startX = readCoord(data, pos); + startY = readCoord(data, pos + 2); + pos += 4; + } + + if (inArea) { + addAreaPoint(startX, startY); + } + + while (pos + 2 <= end) { + int dx = (byte) data[pos]; + int dy = (byte) data[pos + 1]; + pos += 2; + + int nextX = startX + dx; + int nextY = startY + dy; + + if (inArea) { + addAreaPoint(nextX, nextY); + } else { + plane.drawLine(plane.mapX(startX), plane.mapY(startY), + plane.mapX(nextX), plane.mapY(nextY), + curColor, lineType, lineWidth); + } + + startX = nextX; + startY = nextY; + } + + curX = startX; + curY = startY; + } + + private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) { + int pos = off; + int startX = curX; + int startY = curY; + + if (!fromCurPos && pos + 4 <= off + len) { + startX = readCoord(data, pos); + startY = readCoord(data, pos + 2); + pos += 4; + } + + double multiplier = 1.0; + if (pos + 2 <= off + len) { + multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0); + if (multiplier == 0.0) multiplier = 1.0; + } + + int dxP = Math.abs(arcParamP - arcParamR); + int dyQ = Math.abs(arcParamQ - arcParamS); + if (dxP == 0) dxP = 10; + if (dyQ == 0) dyQ = 10; + + int rxVirtual = (int) (dxP * multiplier); + int ryVirtual = (int) (dyQ * multiplier); + + int rx = Math.abs(plane.mapX(rxVirtual) - plane.mapX(0)); + int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0)); + if (rx <= 0) rx = 10; + if (ry <= 0) ry = 10; + + plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0, + curColor, lineType, lineWidth, isFull); + + curX = startX; + curY = startY; + } + + private void processFillet(byte[] data, int off, int len, boolean fromCurPos) { + int pos = off; + int end = off + len; + + List ptsX = new ArrayList<>(); + List ptsY = new ArrayList<>(); + + if (fromCurPos) { + ptsX.add(plane.mapX(curX)); + ptsY.add(plane.mapY(curY)); + } + + while (pos + 4 <= end) { + int x = readCoord(data, pos); + int y = readCoord(data, pos + 2); + ptsX.add(plane.mapX(x)); + ptsY.add(plane.mapY(y)); + curX = x; + curY = y; + pos += 4; + } + + if (ptsX.size() >= 2) { + int n = ptsX.size(); + int[] px = new int[n]; + int[] py = new int[n]; + for (int i = 0; i < n; i++) { + px[i] = ptsX.get(i); + py[i] = ptsY.get(i); + } + plane.drawFillet(px, py, n, curColor, lineType, lineWidth); + } + } + + private void processMarker(byte[] data, int off, int len, boolean fromCurPos) { + int pos = off; + int end = off + len; + + if (fromCurPos) { + plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor); + } + + while (pos + 4 <= end) { + int x = readCoord(data, pos); + int y = readCoord(data, pos + 2); + plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor); + curX = x; + curY = y; + pos += 4; + } + } + + private void processText(byte[] data, int off, int len, boolean fromCurPos) { + int pos = off; + int end = off + len; + + int startX = curX; + int startY = curY; + + if (!fromCurPos && pos + 4 <= end) { + startX = readCoord(data, pos); + startY = readCoord(data, pos + 2); + pos += 4; + } + + int textLen = end - pos; + if (textLen <= 0) return; + + int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12; + int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20; + + if (charSet != 0 && programSymbolManager != null) { + for (int i = 0; i < textLen; i++) { + int code = data[pos + i] & 0xFF; + int px = plane.mapX(startX); + int py = plane.mapY(startY) - ch; + boolean drawn = programSymbolManager.drawSymbol(plane.getGraphics(), charSet, code, px, py, cw, ch, curColor, null); + if (!drawn) { + char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]); + plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle); + } + startX += (charWidth > 0 ? charWidth : 9); + } + curX = startX; + curY = startY; + return; + } + + char[] chars = new char[textLen]; + for (int i = 0; i < textLen; i++) { + chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]); + } + String text = new String(chars); + + plane.drawVectorText(plane.mapX(startX), plane.mapY(startY) - ch, text, + curColor, cw, ch, charDir, charAngle); + + curX = startX + (textLen * (charWidth > 0 ? charWidth : 9)); + curY = startY; + } + + private int readCoord(byte[] data, int off) { + return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF)); + } + + private Color getColor(int colorIndex) { + if (colorIndex >= 0 && colorIndex < GocaConstants.GOCA_COLORS.length) { + return GocaConstants.GOCA_COLORS[colorIndex]; + } + return Color.GREEN; + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsMode.java b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsMode.java new file mode 100644 index 0000000..7744811 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsMode.java @@ -0,0 +1,70 @@ +package org.lib3270j.graphics; + +/** + * Graphics modes supported by j3270 / lib3270j. + */ +public enum GraphicsMode { + /** Text only (no graphics query replies, classic 3279-4 terminal behavior). */ + NONE("None (Text Only)"), + + /** Programmed Symbols only (custom character matrices and APL, single & triple plane). */ + PROGRAMMED_SYMBOLS("Programmed Symbols Only"), + + /** Vector graphics only (GOCA / 3179G drawing orders). */ + VECTOR_GRAPHICS("Vector Graphics Only"), + + /** Full graphics support (both Programmed Symbols and Vector Graphics). */ + BOTH("Both (Programmed Symbols & Vector Graphics)"); + + private final String description; + + GraphicsMode(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + public boolean isProgrammedSymbolsEnabled() { + return this == PROGRAMMED_SYMBOLS || this == BOTH; + } + + public boolean isVectorGraphicsEnabled() { + return this == VECTOR_GRAPHICS || this == BOTH; + } + + public static GraphicsMode fromString(String str) { + if (str == null || str.trim().isEmpty()) { + return NONE; + } + String s = str.trim().toUpperCase(); + switch (s) { + case "BOTH": + case "ALL": + case "FULL": + case "ON": + case "TRUE": + return BOTH; + case "PS": + case "PROGRAMMED_SYMBOLS": + case "PROGRAMMEDSYMBOLS": + case "SYMBOLS": + case "APL": + return PROGRAMMED_SYMBOLS; + case "VECTOR": + case "VECTOR_GRAPHICS": + case "VECTORGRAPHICS": + case "GOCA": + case "GDDM": + return VECTOR_GRAPHICS; + case "NONE": + case "OFF": + case "FALSE": + case "DISABLED": + case "TEXT": + default: + return NONE; + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java new file mode 100644 index 0000000..b752abd --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java @@ -0,0 +1,400 @@ +package org.lib3270j.graphics; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.geom.GeneralPath; +import java.awt.image.BufferedImage; +import java.util.logging.Logger; + +/** + * Offscreen rendering surface for GOCA vector graphics. + * Maintained as an ARGB BufferedImage that overlays the 3270 character cell matrix. + */ +public class GraphicsPlane { + + private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName()); + + // 17 Standard GOCA 8x8 Fill Patterns + public static final byte[][] PATTERN_DATA = new byte[][]{ + {-1, -1, -1, -1, -1, -1, -1, -1}, // 0: Solid (all 1s) + {-1, -1, -1, -18, -1, -1, -1, -18}, // 1: D1 + {-1, -69, -1, -18, -1, -69, -1, -18}, // 2: D2 + {119, -35, -69, -18, 119, -35, -69, -18}, // 3: D3 + {-69, -52, 51, -18, -69, -52, 51, -18}, // 4: D4 + {85, -86, 85, -86, 85, -86, 85, -86}, // 5: D5 (50% checker) + {68, 51, -52, 17, 68, 51, -52, 17}, // 6: D6 + {-120, 34, 68, 17, -120, 34, 68, 17}, // 7: D7 + {0, 68, 0, 17, 0, 68, 0, 17}, // 8: D8 (sparse dots) + {-128, -128, -128, -128, -128, -128, -128, -128}, // 9: Vertical line + {-1, 0, 0, 0, 0, 0, 0, 0}, // 10: Horizontal line + {1, 2, 4, 8, 16, 32, 64, -128}, // 11: Diagonal bottom-left to top-right + {3, 12, 48, -64, 3, 12, 48, -64}, // 12: Diagonal BL-TR dense + {-128, 64, 32, 16, 8, 4, 2, 1}, // 13: Diagonal top-left to bottom-right + {-64, 48, 12, 3, -64, 48, 12, 3}, // 14: Diagonal TL-BR dense + {0, 0, 0, 0, 0, 0, 0, 0}, // 15: Empty (transparent) + {-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid + }; + + private int canvasWidth = 800; + private int canvasHeight = 600; + private BufferedImage image; + private Graphics2D g2d; + private boolean hasContent = false; + + public GraphicsPlane(int width, int height) { + resize(width, height); + } + + public synchronized void resize(int width, int height) { + int w = Math.max(1, width); + int h = Math.max(1, height); + if (w == canvasWidth && h == canvasHeight && image != null) { + return; + } + this.canvasWidth = w; + this.canvasHeight = h; + + BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D newG2d = newImage.createGraphics(); + newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + newG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + if (image != null && hasContent) { + newG2d.drawImage(image, 0, 0, w, h, null); + } + + if (this.g2d != null) { + this.g2d.dispose(); + } + + this.image = newImage; + this.g2d = newG2d; + } + + public synchronized void clear() { + if (image != null && g2d != null) { + BufferedImage newImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D newG2d = newImage.createGraphics(); + newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (this.g2d != null) { + this.g2d.dispose(); + } + this.image = newImage; + this.g2d = newG2d; + } + hasContent = false; + } + + public synchronized boolean hasContent() { + return hasContent; + } + + public synchronized BufferedImage getImage() { + return image; + } + + public int getCanvasWidth() { + return canvasWidth; + } + + public int getCanvasHeight() { + return canvasHeight; + } + + public synchronized Graphics2D getGraphics() { + return g2d; + } + + private int screenCols = 80; + private int screenRows = 24; + + public void setScreenDimensions(int cols, int rows) { + this.screenCols = cols > 0 ? cols : 80; + this.screenRows = rows > 0 ? rows : 24; + } + + public int getScreenCols() { + return screenCols; + } + + public int getScreenRows() { + return screenRows; + } + + /** + * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X. + */ + public int mapX(int gocaX) { + int nominalWidth = screenCols * 9; + int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0); + int nx = gocaX + xMax; + return (int) Math.round((double) nx * canvasWidth / nominalWidth); + } + + /** + * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down). + */ + public int mapY(int gocaY) { + int nominalHeight = screenRows * 16; + int yMax = (nominalHeight - 1) / 2; + int ny = yMax - gocaY; + return (int) Math.round((double) ny * canvasHeight / nominalHeight); + } + + /** + * Draws an absolute or relative line. + */ + public synchronized void drawLine(int x1, int y1, int x2, int y2, Color color, int lineType, int lineWidth) { + if (g2d == null) return; + g2d.setColor(color != null ? color : Color.WHITE); + g2d.setStroke(createStroke(lineType, lineWidth)); + g2d.drawLine(x1, y1, x2, y2); + hasContent = true; + } + + /** + * Draws a full or partial arc / ellipse. + */ + public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg, + Color color, int lineType, int lineWidth, boolean isFull) { + if (g2d == null) return; + g2d.setColor(color != null ? color : Color.WHITE); + g2d.setStroke(createStroke(lineType, lineWidth)); + + double x = cx - rx; + double y = cy - ry; + double w = rx * 2.0; + double h = ry * 2.0; + + if (isFull) { + g2d.drawOval((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h)); + } else { + Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngleDeg, sweepAngleDeg, Arc2D.OPEN); + g2d.draw(arc); + } + hasContent = true; + } + + /** + * Draws a Fillet (spline / curve approximation across control points). + */ + public synchronized void drawFillet(int[] px, int[] py, int numPoints, Color color, int lineType, int lineWidth) { + if (g2d == null || numPoints < 2) return; + g2d.setColor(color != null ? color : Color.WHITE); + g2d.setStroke(createStroke(lineType, lineWidth)); + + GeneralPath path = new GeneralPath(); + path.moveTo(px[0], py[0]); + + if (numPoints == 2) { + path.lineTo(px[1], py[1]); + } else { + for (int i = 1; i < numPoints - 1; i++) { + double midX = (px[i] + px[i + 1]) / 2.0; + double midY = (py[i] + py[i + 1]) / 2.0; + path.quadTo(px[i], py[i], midX, midY); + } + path.lineTo(px[numPoints - 1], py[numPoints - 1]); + } + + g2d.draw(path); + hasContent = true; + } + + /** + * Fills a closed polygon area with a solid color or hatching pattern. + */ + public synchronized void fillArea(int[] px, int[] py, int numPoints, Color fillColor, int pattern, + boolean drawBoundary, Color boundaryColor, int lineType, int lineWidth) { + if (g2d == null || numPoints < 3) return; + + Polygon poly = new Polygon(px, py, numPoints); + + if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) { + g2d.setColor(fillColor != null ? fillColor : Color.WHITE); + g2d.fill(poly); + } else if (pattern != GocaConstants.PT_EMPTY) { + // Fill with pattern texture + BufferedImage patImg = createPatternTexture(pattern, fillColor != null ? fillColor : Color.WHITE); + java.awt.TexturePaint tp = new java.awt.TexturePaint(patImg, new java.awt.Rectangle(0, 0, 8, 8)); + g2d.setPaint(tp); + g2d.fill(poly); + } + + if (drawBoundary && boundaryColor != null) { + g2d.setColor(boundaryColor); + g2d.setStroke(createStroke(lineType, lineWidth)); + g2d.draw(poly); + } + hasContent = true; + } + + /** + * Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle). + */ + public synchronized void drawMarker(int x, int y, int markerType, int size, Color color) { + if (g2d == null) return; + g2d.setColor(color != null ? color : Color.WHITE); + g2d.setStroke(new BasicStroke(1.5f)); + + int s = Math.max(3, size > 0 ? size : 5); + + switch (markerType) { + case GocaConstants.MK_CROSS: // x + g2d.drawLine(x - s, y - s, x + s, y + s); + g2d.drawLine(x - s, y + s, x + s, y - s); + break; + case GocaConstants.MK_PLUS: // + + case GocaConstants.MK_DEFAULT: + g2d.drawLine(x - s, y, x + s, y); + g2d.drawLine(x, y - s, x, y + s); + break; + case GocaConstants.MK_DIAMOND: // <> + g2d.drawPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4); + break; + case GocaConstants.MK_SQUARE: // [] + g2d.drawRect(x - s, y - s, s * 2, s * 2); + break; + case GocaConstants.MK_6STAR: // 6-point star + g2d.drawLine(x - s, y, x + s, y); + g2d.drawLine(x - s / 2, y - s, x + s / 2, y + s); + g2d.drawLine(x - s / 2, y + s, x + s / 2, y - s); + break; + case GocaConstants.MK_8STAR: // 8-point star + g2d.drawLine(x - s, y, x + s, y); + g2d.drawLine(x, y - s, x, y + s); + g2d.drawLine(x - s, y - s, x + s, y + s); + g2d.drawLine(x - s, y + s, x + s, y - s); + break; + case GocaConstants.MK_SDIAMOND: // solid diamond + g2d.fillPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4); + break; + case GocaConstants.MK_SSQUARE: // solid square + g2d.fillRect(x - s, y - s, s * 2, s * 2); + break; + case GocaConstants.MK_DOT: // dot + g2d.fillOval(x - 2, y - 2, 4, 4); + break; + case GocaConstants.MK_CIRCLE: // circle + g2d.drawOval(x - s, y - s, s * 2, s * 2); + break; + default: + g2d.drawOval(x - s, y - s, s * 2, s * 2); + break; + } + hasContent = true; + } + + /** + * Draws stroked vector text using IBM Vector Symbol Set (VSS). + */ + public synchronized void drawVectorText(int x, int y, String text, Color color, + int cellWidth, int cellHeight, int dir, double angle) { + if (g2d == null || text == null || text.isEmpty()) return; + g2d.setColor(color != null ? color : Color.WHITE); + g2d.setStroke(new BasicStroke(1.2f)); + + int curX = x; + int curY = y; + int cw = cellWidth > 0 ? cellWidth : 12; + int ch = cellHeight > 0 ? cellHeight : 20; + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + drawVssChar(curX, curY, c, cw, ch); + + switch (dir) { + case GocaConstants.CD_TB: curY += ch; break; + case GocaConstants.CD_RL: curX -= cw; break; + case GocaConstants.CD_BT: curY -= ch; break; + case GocaConstants.CD_LR: + case GocaConstants.CD_DEFAULT: + default: + curX += cw; + break; + } + } + hasContent = true; + } + + private void drawVssChar(int x, int y, char c, int cw, int ch) { + int idx = (int) c; + if (idx < VectorSymbolData.VSS_SYMBOL_START || idx > VectorSymbolData.VSS_SYMBOL_END) { + // Draw simple bounding box or space + return; + } + // Fallback vector stroke rendering + g2d.drawString(String.valueOf(c), x, y + ch); + } + + /** + * Draws raw image pixel bitmap. + */ + public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, Color fgColor) { + if (g2d == null || imageData == null || width <= 0 || height <= 0) return; + + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF; + + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + int bitIndex = row * width + col; + int byteIdx = bitIndex / 8; + if (byteIdx < imageData.length) { + boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0; + if (bit) { + img.setRGB(col, row, fgRgb); + } + } + } + } + + g2d.drawImage(img, x, y, null); + hasContent = true; + } + + private Stroke createStroke(int lineType, int lineWidth) { + float width = (lineWidth == GocaConstants.LW_THICK) ? 2.5f : 1.2f; + + switch (lineType) { + case GocaConstants.LT_DOT: + return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f); + case GocaConstants.LT_SHORTDASH: + return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{4.0f, 2.0f}, 0.0f); + case GocaConstants.LT_DASHDOT: + return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{6.0f, 2.0f, 2.0f, 2.0f}, 0.0f); + case GocaConstants.LT_DOUBLEDOT: + return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f, 2.0f, 4.0f}, 0.0f); + case GocaConstants.LT_LONGDASH: + return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 3.0f}, 0.0f); + case GocaConstants.LT_DASHDOUBLEDOT: + return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f}, 0.0f); + case GocaConstants.LT_SOLID: + case GocaConstants.LT_DEFAULT: + default: + return new BasicStroke(width); + } + } + + private BufferedImage createPatternTexture(int patternIndex, Color color) { + BufferedImage pat = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB); + byte[] patternRows = (patternIndex >= 0 && patternIndex < PATTERN_DATA.length) ? PATTERN_DATA[patternIndex] : PATTERN_DATA[0]; + int rgb = color.getRGB(); + + for (int r = 0; r < 8; r++) { + int b = patternRows[r] & 0xFF; + for (int c = 0; c < 8; c++) { + if (((b >> (7 - c)) & 1) != 0) { + pat.setRGB(c, r, rgb); + } + } + } + return pat; + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java new file mode 100644 index 0000000..273d8fe --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java @@ -0,0 +1,268 @@ +package org.lib3270j.graphics; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Manages IBM 3270 Programmed Symbols (PS / APL) character sets. + * Handles the Load Programmed Symbols (LOADPS structured field 0x0F). + */ +public class ProgramSymbolManager { + + private static final Logger logger = Logger.getLogger(ProgramSymbolManager.class.getName()); + + public static final int NUMBER_SYMBOL_SETS = 10; + public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; // RWS 2..3 (Sets 0..1) + public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5) + public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9) + + private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS]; + + public ProgramSymbolManager() { + for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) { + boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS); + sets[i] = new ProgramSymbolSet(isTriple); + } + } + + /** + * Resets all symbol sets. + */ + public synchronized void clearAll() { + for (ProgramSymbolSet set : sets) { + set.clear(); + set.setLcid(0); + } + } + + /** + * Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE). + */ + public synchronized ProgramSymbolSet getSymbolSet(int lcid) { + if (lcid <= 0) { + return null; + } + for (ProgramSymbolSet set : sets) { + if (set.getLcid() == lcid) { + return set; + } + } + return null; + } + + /** + * Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE). + */ + public synchronized ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) { + ProgramSymbolSet set = getSymbolSet(lcid); + if (set == null) { + return null; + } + int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint; + return set.getSlot(index); + } + + /** + * Draws a programmed symbol character cell if defined. + * Returns true if symbol was drawn, false if not found. + */ + public synchronized boolean drawSymbol(Graphics2D g2d, int lcid, int codePoint, + int x, int y, int cellWidth, int cellHeight, + Color fgColor, Color bgColor) { + ProgramSymbolSet.SymbolSlot slot = getSymbol(lcid, codePoint); + if (slot != null) { + slot.draw(g2d, x, y, cellWidth, cellHeight, fgColor, bgColor); + return true; + } + return false; + } + + /** + * Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload. + */ + public synchronized void loadps(byte[] data) { + if (data == null || data.length < 4) { + logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)"); + return; + } + + int flags = data[0] & 0xFF; + int loadFormat = flags & 0x1F; + boolean clearAll = (flags & 0x40) != 0; + boolean hasExtHeader = (flags & 0x80) != 0; + + int lcid = data[1] & 0xFF; + int startCodePoint = data[2] & 0xFF; + int rws = data[3] & 0xFF; + + int setIndex; + switch (rws) { + case 2: setIndex = 0; break; + case 3: setIndex = 1; break; + case 4: setIndex = 2; break; + case 5: setIndex = 3; break; + case 6: setIndex = 4; break; + case 7: setIndex = 5; break; + case 8: setIndex = 6; break; + case 9: setIndex = 7; break; + case 10: setIndex = 8; break; + case 11: setIndex = 9; break; + default: + logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws)); + return; + } + + ProgramSymbolSet set = sets[setIndex]; + boolean isTriplePlane = (rws >= 4 && rws <= 7); + + int extHeaderLen = 0; + int cellWidth = 9; + int cellHeight = 16; + int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue + + if (hasExtHeader && data.length > 4) { + extHeaderLen = data[4] & 0xFF; + if (extHeaderLen > 3 && data.length > 6) { + int lw = data[6] & 0xFF; + if (lw > 0) cellWidth = lw; + } + if (extHeaderLen > 4 && data.length > 7) { + int lh = data[7] & 0xFF; + if (lh > 0) cellHeight = lh; + } + if (extHeaderLen >= 6 && data.length > 9) { + colorPlane = data[9] & 0xFF; + } + } + + if (clearAll) { + set.clear(); + } + set.setLcid(lcid); + + int offset = 4 + (hasExtHeader ? extHeaderLen : 0); + int remaining = data.length - offset; + int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint; + + int bytesPerSymbol; + if (loadFormat == 1) { + bytesPerSymbol = 18; // 9x16 cell in standard transmission format + } else { + bytesPerSymbol = (cellWidth * cellHeight + 7) / 8; + } + + if (bytesPerSymbol <= 0) { + bytesPerSymbol = 18; + } + + while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) { + byte[] pixelData; + ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex); + if (existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) { + pixelData = existing.getPixelData(); + } else { + pixelData = new byte[cellWidth * cellHeight]; + } + + if (loadFormat == 1) { + unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); + } else { + unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); + } + + set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane)); + + codeIndex++; + offset += bytesPerSymbol; + remaining -= bytesPerSymbol; + } + + logger.info(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)", + lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane", + cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint))); + } + + /** + * Unpacks Format 1 (9x16) symbol slice bit-pattern. + */ + private void unpackFormat1(byte[] src, int srcOff, byte[] dst, int width, int height, + boolean isTriplePlane, int colorPlane) { + // Format 1 transmits 18 bytes: + // Byte 0-1: contains column 0 for each of the 16 rows + // Bytes 2-17: contains columns 1-8 for each of the 16 rows + int planeMask = (colorPlane != 0) ? colorPlane : (isTriplePlane ? 7 : 1); + + for (int row = 0; row < 16 && row < height; row++) { + // Column 0 bit from byte 0 or byte 1 + int b0 = (row < 8) ? (src[srcOff] & 0xFF) : (src[srcOff + 1] & 0xFF); + int bitShift0 = 7 - (row % 8); + boolean bit0 = ((b0 >> bitShift0) & 1) != 0; + + int dstIdx0 = row * width; + if (dstIdx0 < dst.length) { + if (!isTriplePlane) { + dst[dstIdx0] = bit0 ? (byte) 1 : 0; + } else if (colorPlane == 0) { + dst[dstIdx0] = bit0 ? (byte) 7 : 0; + } else { + if (bit0) { + dst[dstIdx0] |= (byte) colorPlane; + } else { + dst[dstIdx0] &= (byte) ~colorPlane; + } + } + } + + // Columns 1..8 from bytes 2..17 + if (srcOff + 2 + row < src.length) { + int rowByte = src[srcOff + 2 + row] & 0xFF; + for (int col = 1; col < 9 && col < width; col++) { + int dstIdx = row * width + col; + if (dstIdx < dst.length) { + boolean bit = ((rowByte >> (8 - col)) & 1) != 0; + if (!isTriplePlane) { + dst[dstIdx] = bit ? (byte) 1 : 0; + } else if (colorPlane == 0) { + dst[dstIdx] = bit ? (byte) 7 : 0; + } else { + if (bit) { + dst[dstIdx] |= (byte) colorPlane; + } else { + dst[dstIdx] &= (byte) ~colorPlane; + } + } + } + } + } + } + } + + /** + * Unpacks Format 3 variable dimension bit-pattern. + */ + private void unpackFormat3(byte[] src, int srcOff, byte[] dst, int width, int height, + boolean isTriplePlane, int colorPlane) { + int totalPixels = width * height; + for (int i = 0; i < totalPixels; i++) { + int byteIndex = srcOff + (i / 8); + if (byteIndex >= src.length) break; + int bitIndex = 7 - (i % 8); + boolean bit = ((src[byteIndex] >> bitIndex) & 1) != 0; + + if (!isTriplePlane) { + dst[i] = bit ? (byte) 1 : 0; + } else if (colorPlane == 0) { + dst[i] = bit ? (byte) 7 : 0; + } else { + if (bit) { + dst[i] |= (byte) colorPlane; + } else { + dst[i] &= (byte) ~colorPlane; + } + } + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java new file mode 100644 index 0000000..2e4184e --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolSet.java @@ -0,0 +1,148 @@ +package org.lib3270j.graphics; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; + +/** + * Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE). + * Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite). + */ +public class ProgramSymbolSet { + + public static final int NUM_SLOTS = 191; // Code points 0x40 - 0xFE (0..190) + + private int lcid = 0; + private final boolean isTriplePlane; + private final SymbolSlot[] slots = new SymbolSlot[NUM_SLOTS]; + + public ProgramSymbolSet(boolean isTriplePlane) { + this.isTriplePlane = isTriplePlane; + } + + public int getLcid() { + return lcid; + } + + public void setLcid(int lcid) { + this.lcid = lcid; + } + + public boolean isTriplePlane() { + return isTriplePlane; + } + + public void clear() { + for (int i = 0; i < NUM_SLOTS; i++) { + slots[i] = null; + } + } + + public void clearSlot(int index) { + if (index >= 0 && index < NUM_SLOTS) { + slots[index] = null; + } + } + + public void setSlot(int index, SymbolSlot slot) { + if (index >= 0 && index < NUM_SLOTS) { + slots[index] = slot; + } + } + + public SymbolSlot getSlot(int index) { + if (index >= 0 && index < NUM_SLOTS) { + return slots[index]; + } + return null; + } + + /** + * Represents a single custom symbol bitmap. + */ + public static class SymbolSlot { + private final int width; + private final int height; + private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome) + private final boolean isTriplePlane; + private BufferedImage cachedImage; + private int cachedFgRgb = -1; + private int cachedBgRgb = -1; + + public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) { + this.width = width > 0 ? width : 9; + this.height = height > 0 ? height : 16; + this.pixelData = pixelData; + this.isTriplePlane = isTriplePlane; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public byte[] getPixelData() { + return pixelData; + } + + public boolean isTriplePlane() { + return isTriplePlane; + } + + /** + * Renders this symbol to a BufferedImage. + */ + public synchronized BufferedImage getImage(Color fgColor, Color bgColor) { + int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF; + int bgRgb = bgColor != null ? bgColor.getRGB() : 0x00000000; + + if (cachedImage != null && cachedFgRgb == fgRgb && cachedBgRgb == bgRgb) { + return cachedImage; + } + + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + int[] rgbArray = new int[width * height]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int idx = y * width + x; + int val = (idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0; + + if (val == 0) { + rgbArray[idx] = bgRgb; + } else if (!isTriplePlane) { + rgbArray[idx] = fgRgb; + } else { + // Triple-Plane RGB composite: + // val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue + int r = (val & 0x01) != 0 ? 255 : 0; + int g = (val & 0x02) != 0 ? 255 : 0; + int b = (val & 0x04) != 0 ? 255 : 0; + rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b; + } + } + } + + img.setRGB(0, 0, width, height, rgbArray, 0, width); + this.cachedImage = img; + this.cachedFgRgb = fgRgb; + this.cachedBgRgb = bgRgb; + return img; + } + + /** + * Draws the symbol directly onto a Graphics2D surface with aspect scaling. + */ + public void draw(Graphics2D g2d, int x, int y, int cellWidth, int cellHeight, Color fgColor, Color bgColor) { + BufferedImage img = getImage(fgColor, bgColor); + if (img != null) { + g2d.drawImage(img, x, y, cellWidth, cellHeight, null); + } + } + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/VectorSymbolData.java b/lib3270j/src/main/java/org/lib3270j/graphics/VectorSymbolData.java new file mode 100644 index 0000000..6e21405 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/VectorSymbolData.java @@ -0,0 +1,21 @@ +/* + * Decompiled with CFR 0.152. + */ +package org.lib3270j.graphics; + +import org.lib3270j.graphics.GocaConstants; + + +public final class VectorSymbolData { + public static final int VSS_SYMBOL_START = 33; + public static final int VSS_SYMBOL_END = 255; + public static final int VSS_WIDTH = 80; + public static final int VSS_HEIGHT = 110; + public static final char END_DEFAULT = '\u00ff'; + public static final int VSS_NUMBER_SYMBOLS = 223; + static final char[] vss_data = new char[]{'\u00c1', '\u0014', '\u0000', '+', '\u0000', '!', '\u0000', '(', '\u0000', '\u001e', '\u0000', '%', '\u0000', '!', '\u0000', '(', '\u0000', '$', '\u0000', '+', '\u0000', '!', '\u00c1', '\b', '\u0000', '(', '\u0000', '<', '\u0000', '(', '\u0000', 'd', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u00c1', '\b', '\u0000', '<', '\u0000', 'd', '\u0000', '<', '\u0000', 'Z', '\u00ff', '\u00c1', '\b', '\u0000', '\u001b', '\u0000', 'd', '\u0000', '\u001b', '\u0000', '2', '\u00c1', '\b', '\u0000', '5', '\u0000', 'd', '\u0000', '5', '\u0000', '2', '\u00c1', '\b', '\u0000', '\u0011', '\u0000', 'Z', '\u0000', '?', '\u0000', 'Z', '\u00c1', '\b', '\u0000', '\u0011', '\u0000', '<', '\u0000', '?', '\u0000', '<', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u0014', '\u0000', '(', '\u0000', 'd', '\u00c1', '(', '\u0000', '<', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u0019', '\u0000', '<', '\u0000', '7', '\u0000', '<', '\u0000', '<', '\u0000', '7', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '<', '\u0000', 'd', '\u00c1', '\u0014', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '\n', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'd', '\u00c1', '\u0014', '\u0000', '2', '\u0000', '2', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '(', '\u0000', '(', '\u0000', '2', '\u0000', '2', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', '2', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '(', '\u0000', '\n', '\u0000', '<', '\u0000', '#', '\u0000', 'U', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\n', '\u0000', 'Z', '\u0000', '\n', '\u0000', 'P', '\u0000', '<', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', 'Z', '\u0000', '(', '\u0000', 'd', '\u00ff', '\u00c1', '\u0010', '\u0000', '<', '\u0000', 'd', '\u0000', '(', '\u0000', 'U', '\u0000', '(', '\u0000', '-', '\u0000', '<', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0010', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '(', '\u0000', 'U', '\u0000', '(', '\u0000', '-', '\u0000', '\u0014', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '_', '\u0000', '<', '\u0000', '7', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '7', '\u0000', '<', '\u0000', '_', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u00c1', '\b', '\u0000', '(', '\u0000', '2', '\u0000', '(', '\u0000', 'd', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '2', '\u0000', '(', '\u0000', 'Z', '\u00c1', '\b', '\u0000', '<', '\u0000', 'F', '\u0000', '\u0014', '\u0000', 'F', '\u00ff', '\u00c1', '\u001c', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\n', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '<', '\u0000', 'F', '\u00ff', '\u00c1', '\u0014', '\u0000', '#', '\u0000', 'F', '\u0000', '-', '\u0000', 'F', '\u0000', '-', '\u0000', '<', '\u0000', '#', '\u0000', '<', '\u0000', '#', '\u0000', 'F', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', '(', '\u0000', 'F', '\u0000', 'd', '\u00ff', '\u00c1', '$', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', '<', '\u0000', '(', '\u0000', '<', '\u0000', 'Z', '\u0000', '2', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '#', '\u0000', '7', '\u0000', '_', '\u00ff', '\u00c1', '\f', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '(', '\u0000', 'd', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u00ff', '\u00c1', ' ', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', 'P', '\u0000', '<', '\u0000', 'Z', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u00ff', '\u00c1', ' ', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '<', '\u0000', 'd', '\u0000', '(', '\u0000', 'F', '\u0000', '<', '\u0000', '<', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u00ff', '\u00c1', '\u0014', '\u0000', '<', '\u0000', '2', '\u0000', '\u0014', '\u0000', '2', '\u0000', '\u0014', '\u0000', '<', '\u0000', '2', '\u0000', 'd', '\u0000', '2', '\u0000', '\u001e', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u00ff', '\u00c1', '(', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', '<', '\u0000', '(', '\u0000', '<', '\u0000', '<', '\u0000', '2', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u0014', '\u0000', '<', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '<', '\u0000', 'd', '\u0000', '<', '\u0000', 'Z', '\u0000', '(', '\u0000', '7', '\u0000', '(', '\u0000', '\u001e', '\u00ff', '\u00c1', '@', '\u0000', '2', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u0014', '\u0000', '<', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', '<', '\u0000', '(', '\u0000', '<', '\u0000', '<', '\u0000', '2', '\u0000', 'F', '\u0000', '<', '\u0000', 'P', '\u0000', '<', '\u0000', 'Z', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '\u001e', '\u0000', 'F', '\u00ff', '\u00c1', '0', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', '<', '\u0000', '(', '\u0000', '<', '\u0000', 'Z', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '<', '\u0000', '2', '\u0000', '<', '\u0000', '<', '\u0000', 'F', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u0014', '\u0000', '<', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u00c1', '\u0014', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u00ff', '\u00c1', '\u001c', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\n', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\u001e', '\u00c1', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u0014', '\u0000', '<', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '<', '\u00ff', '\u00c1', '\f', '\u0000', '2', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '2', '\u0000', '(', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '<', '\u0000', '<', '\u0000', '<', '\u00c1', '\b', '\u0000', '<', '\u0000', 'P', '\u0000', '\u0014', '\u0000', 'P', '\u00ff', '\u00c1', '\f', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '2', '\u0000', 'F', '\u0000', '\u0014', '\u0000', '(', '\u00ff', '\u00c1', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '%', '\u0000', '!', '\u0000', '(', '\u0000', '$', '\u0000', '+', '\u0000', '!', '\u0000', '(', '\u0000', '\u001e', '\u00c1', ' ', '\u0000', '(', '\u0000', '2', '\u0000', '(', '\u0000', 'A', '\u0000', '7', '\u0000', 'P', '\u0000', '7', '\u0000', '_', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0019', '\u0000', '_', '\u0000', '\u0019', '\u0000', 'U', '\u00ff', '\u00c1', '4', '\u0000', '<', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', '<', '\u0000', '_', '\u0000', '<', '\u0000', '2', '\u0000', '#', '\u0000', '2', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '#', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', 'F', '\u0000', '(', '\u0000', 'd', '\u0000', 'F', '\u0000', 'F', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '7', '\u0000', '\n', '\u0000', '7', '\u00ff', '\u00c1', '$', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '(', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', 'F', '\u0000', 'A', '\u0000', 'P', '\u0000', 'A', '\u0000', 'Z', '\u0000', '7', '\u0000', 'd', '\u0000', '\u000f', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '\u0019', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'F', '\u0000', '7', '\u0000', 'F', '\u00ff', '\u00c1', ' ', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u00ff', '\u00c1', '\u0018', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '(', '\u0000', 'A', '\u0000', 'Z', '\u0000', '7', '\u0000', 'd', '\u0000', '\u000f', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '\u0019', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0010', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'd', '\u0000', 'A', '\u0000', 'd', '\u00c1', '\b', '\u0000', '7', '\u0000', 'A', '\u0000', '\u000f', '\u0000', 'A', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'd', '\u0000', 'A', '\u0000', 'd', '\u00c1', '\b', '\u0000', '7', '\u0000', 'A', '\u0000', '\u000f', '\u0000', 'A', '\u00ff', '\u00c1', '\b', '\u0000', 'A', '\u0000', '<', '\u0000', '-', '\u0000', '<', '\u00c1', '$', '\u0000', '<', '\u0000', '<', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '2', '\u0000', 'd', '\u0000', '<', '\u0000', 'Z', '\u00ff', '\u00c1', '\b', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'd', '\u00c1', '\b', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'A', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u000f', '\u0000', 'A', '\u0000', 'A', '\u0000', 'A', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '<', '\u0000', 'd', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '2', '\u0000', '(', '\u0000', '2', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'd', '\u0000', 'A', '\u0000', 'd', '\u00ff', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\u001e', '\u0000', '\u0012', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '<', '\u0000', ':', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', 'F', '\u0000', '?', '\u0000', '\u001e', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', 'd', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0014', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u0000', 'd', '\u0000', '(', '\u0000', 'A', '\u0000', '\n', '\u0000', 'd', '\u0000', '\n', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0010', '\u0000', 'F', '\u0000', 'd', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '\n', '\u0000', 'd', '\u0000', '\n', '\u0000', '\u001e', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u0000', 'A', '\u0000', '(', '\u00ff', '\u00c1', '\u001c', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u0000', 'A', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '\u000f', '\u0000', 'A', '\u00ff', '\u00c1', '$', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u0000', 'A', '\u0000', '2', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u00c1', '\b', '\u0000', '(', '\u0000', '7', '\u0000', 'A', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u001c', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u0000', 'A', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '\u000f', '\u0000', 'A', '\u00c1', '\b', '\u0000', '\u001e', '\u0000', 'A', '\u0000', 'A', '\u0000', '\u001e', '\u00ff', '\u00c1', '0', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '(', '\u0000', 'A', '\u0000', '<', '\u0000', '<', '\u0000', 'A', '\u0000', '\u0019', '\u0000', 'A', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\n', '\u0000', 'd', '\u0000', 'F', '\u0000', 'd', '\u00ff', '\u00c1', '\u0018', '\u0000', 'A', '\u0000', 'd', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'd', '\u00ff', '\u00c1', '\f', '\u0000', '\n', '\u0000', 'd', '\u0000', '(', '\u0000', '\u001e', '\u0000', 'F', '\u0000', 'd', '\u00ff', '\u00c1', '\u001c', '\u0000', '\n', '\u0000', 'd', '\u0000', '\n', '\u0000', '(', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '(', '\u0000', '2', '\u0000', '<', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '(', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\b', '\u0000', '(', '\u0000', 'F', '\u0000', '(', '\u0000', '2', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', '\u001e', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\n', '\u0000', 'd', '\u0000', 'F', '\u0000', '\u001e', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', 'd', '\u0000', '(', '\u0000', 'A', '\u0000', 'A', '\u0000', 'd', '\u00c1', '\b', '\u0000', '(', '\u0000', 'A', '\u0000', '(', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0010', '\u0000', '\n', '\u0000', 'd', '\u0000', 'F', '\u0000', 'd', '\u0000', '\n', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0010', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '2', '\u0000', '\n', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', 'd', '\u0000', 'F', '\u0000', '(', '\u00ff', '\u00c1', '\u0010', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '2', '\u0000', 'd', '\u0000', '2', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u00ff', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', '_', '\u0000', '(', '\u0000', 'i', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '\u0000', '\u0000', '\n', '\u0000', 'P', '\u0000', '\n', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '\u0014', '\u0000', 'i', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00ff', '\u00c1', ' ', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', '<', '\u0000', '(', '\u0000', '<', '\u0000', 'A', '\u0000', '2', '\u0000', 'K', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '\u0014', '\u0000', 'A', '\u00ff', '\u00c1', ' ', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'd', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\b', '\u0000', '<', '\u0000', '(', '\u0000', 'F', '\u0000', '\u001e', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', '7', '\u0000', '2', '\u0000', '\u0014', '\u0000', '2', '\u00ff', '\u00c1', '\u0010', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'Z', '\u0000', '2', '\u0000', 'd', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u00ff', '\u00c1', '4', '\u0000', '\u0014', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '2', '\u0000', '\n', '\u0000', '<', '\u0000', '\u0014', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '2', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u00c1', '\b', '\u0000', '<', '\u0000', 'A', '\u0000', 'F', '\u0000', 'K', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', 'd', '\u00c1', '\u0014', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'K', '\u00c1', '\u0014', '\u0000', '(', '\u0000', 'f', '\u0000', '*', '\u0000', 'd', '\u0000', '(', '\u0000', 'b', '\u0000', '&', '\u0000', 'd', '\u0000', '(', '\u0000', 'f', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '-', '\u0000', '\u0014', '\u0000', '7', '\u0000', '\u001e', '\u0000', '7', '\u0000', 'K', '\u00c1', '\u0014', '\u0000', '7', '\u0000', 'f', '\u0000', '9', '\u0000', 'd', '\u0000', '7', '\u0000', 'b', '\u0000', '5', '\u0000', 'd', '\u0000', '7', '\u0000', 'f', '\u00ff', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '-', '\u0000', '7', '\u0000', 'K', '\u00c1', '\b', '\u0000', '#', '\u0000', '7', '\u0000', 'A', '\u0000', '\u001e', '\u00ff', '\u00c1', '\f', '\u0000', '(', '\u0000', 'd', '\u0000', '(', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0018', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', 'A', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '(', '\u0000', 'A', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\u0014', '\u0000', '(', '\u0000', 'A', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u0000', 'F', '\u0000', 'A', '\u0000', 'F', '\u0000', '\u001e', '\u00ff', '\u00c1', '\f', '\u0000', '\n', '\u0000', 'K', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u0014', '\u0000', '\u001e', '\u00c1', '\u0014', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '\u001e', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00ff', '\u00c1', ' ', '\u0000', '\u0014', '\u0000', '\n', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u00ff', '\u00c1', '0', '\u0000', 'F', '\u0000', '\u000f', '\u0000', 'A', '\u0000', '\u000f', '\u0000', '7', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'A', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u00ff', '\u00c1', '\u0010', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'A', '\u0000', '#', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'A', '\u0000', '\u0019', '\u0000', '\u001e', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'K', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u0000', '<', '\u0000', '2', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0010', '\u0000', '(', '\u0000', 'd', '\u0000', '(', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u00ff', '\u00c1', '\u0018', '\u0000', '9', '\u0000', 'K', '\u0000', '9', '\u0000', '(', '\u0000', '/', '\u0000', '\u001e', '\u0000', '\u001b', '\u0000', '\u001e', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\u0011', '\u0000', 'K', '\u00c1', '\b', '\u0000', '9', '\u0000', '(', '\u0000', 'C', '\u0000', '\u001e', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '(', '\u0000', '\u001e', '\u0000', 'A', '\u0000', 'K', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', 'K', '\u0000', '\n', '\u0000', '(', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '(', '\u0000', '2', '\u0000', '(', '\u0000', '<', '\u00c1', '\u0010', '\u0000', '(', '\u0000', '2', '\u0000', '<', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '(', '\u0000', 'F', '\u0000', 'K', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', 'K', '\u00c1', '\b', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', 'K', '\u00ff', '\u00c1', '\f', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '\u0014', '\u0000', '2', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\f', '\u0000', '<', '\u0000', 'K', '\u0000', '<', '\u0000', '2', '\u0000', '\u0014', '\u0000', '\n', '\u00ff', '\u00c1', '\u0010', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '<', '\u0000', 'K', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00ff', '\u00c1', '$', '\u0000', '2', '\u0000', 'd', '\u0000', '(', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u0014', '\u0000', '<', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u0014', '\u0000', '2', '\u0000', '\u0014', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', 'd', '\u0000', '(', '\u0000', '\u0014', '\u00ff', '\u00c1', '$', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '(', '\u0000', 'd', '\u0000', '2', '\u0000', 'Z', '\u0000', '2', '\u0000', 'F', '\u0000', '<', '\u0000', '<', '\u0000', '2', '\u0000', '2', '\u0000', '2', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u0014', '\u00ff', '\u00c1', '\u0018', '\u0000', '\u0019', '\u0000', '_', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '%', '\u0000', 'd', '\u0000', '+', '\u0000', '_', '\u0000', '2', '\u0000', '_', '\u0000', '7', '\u0000', 'd', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00ff', '\u00c1', '\u0014', '\u0000', '(', '\u0000', 'P', '\u0000', '%', '\u0000', 'M', '\u0000', '(', '\u0000', 'J', '\u0000', '+', '\u0000', 'M', '\u0000', '(', '\u0000', 'P', '\u00c1', '\b', '\u0000', '(', '\u0000', '2', '\u0000', '(', '\u0000', '\n', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', 'd', '\u0000', '(', '\u0000', '-', '\u00c1', '\u0018', '\u0000', '2', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '7', '\u0000', '2', '\u0000', '7', '\u00ff', '\u00c1', ' ', '\u0000', '<', '\u0000', 'Z', '\u0000', '2', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u0014', '\u0000', '\u001e', '\u00c1', '\u0010', '\u0000', '\u001e', '\u0000', '(', '\u0000', '(', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '(', '\u00c1', '\b', '\u0000', '\u000f', '\u0000', '<', '\u0000', '-', '\u0000', '<', '\u00ff', '\u00c1', '\u0018', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'P', '\u0000', '7', '\u0000', 'P', '\u0000', '7', '\u0000', '2', '\u0000', '\u0019', '\u0000', '2', '\u0000', '\u0019', '\u0000', 'P', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '2', '\u0000', '\u000f', '\u0000', '(', '\u00c1', '\b', '\u0000', '7', '\u0000', '2', '\u0000', 'A', '\u0000', '(', '\u00c1', '\b', '\u0000', '7', '\u0000', 'P', '\u0000', 'A', '\u0000', 'Z', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', 'd', '\u0000', '(', '\u0000', 'A', '\u0000', 'A', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'A', '\u0000', '7', '\u0000', 'A', '\u00c1', '\b', '\u0000', '7', '\u0000', '7', '\u0000', '\u0019', '\u0000', '7', '\u00c1', '\b', '\u0000', '(', '\u0000', 'A', '\u0000', '(', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u0014', '\u0000', '(', '\u0000', '2', '\u00c1', '\b', '\u0000', '(', '\u0000', 'F', '\u0000', '(', '\u0000', 'd', '\u00ff', '\u00c1', '8', '\u0000', 'A', '\u0000', '_', '\u0000', '7', '\u0000', 'i', '\u0000', '\u0019', '\u0000', 'i', '\u0000', '\u000f', '\u0000', '_', '\u0000', '\u000f', '\u0000', 'U', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '7', '\u0000', 'K', '\u0000', 'A', '\u0000', 'A', '\u0000', 'A', '\u0000', '7', '\u0000', '7', '\u0000', '-', '\u0000', '\u0019', '\u0000', '-', '\u0000', '\u000f', '\u0000', '7', '\u0000', '\u000f', '\u0000', 'A', '\u0000', '\u0019', '\u0000', 'K', '\u00c1', '\u0018', '\u0000', '7', '\u0000', '-', '\u0000', 'A', '\u0000', '#', '\u0000', 'A', '\u0000', '\u0019', '\u0000', '7', '\u0000', '\u000f', '\u0000', '\u0019', '\u0000', '\u000f', '\u0000', '\u000f', '\u0000', '\u0019', '\u00ff', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'i', '\u00c1', '\b', '\u0000', '4', '\u0000', 'i', '\u0000', '4', '\u0000', '_', '\u00ff', '\u00c1', '$', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '2', '\u0000', 'd', '\u0000', '<', '\u0000', 'Z', '\u0000', '<', '\u0000', 'A', '\u0000', '2', '\u0000', '7', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'd', '\u00c1', ' ', '\u0000', '2', '\u0000', 'F', '\u0000', '-', '\u0000', 'A', '\u0000', '#', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '#', '\u0000', 'Z', '\u0000', '-', '\u0000', 'Z', '\u0000', '2', '\u0000', 'U', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '-', '\u0000', 'd', '\u0000', '2', '\u0000', '_', '\u0000', '2', '\u0000', 'K', '\u0000', '7', '\u0000', 'F', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '<', '\u0000', '7', '\u0000', '<', '\u00c1', '\u001c', '\u0000', '2', '\u0000', 'K', '\u0000', '-', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'P', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '2', '\u0000', 'U', '\u00ff', '\u00c1', '\f', '\u0000', '(', '\u0000', 'K', '\u0000', '\u0011', '\u0000', '5', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\f', '\u0000', '?', '\u0000', 'K', '\u0000', '(', '\u0000', '5', '\u0000', '?', '\u0000', '\u001e', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', 'K', '\u0000', 'A', '\u0000', 'K', '\u0000', 'A', '\u0000', '7', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '7', '\u0000', '<', '\u0000', '7', '\u00ff', '\u00c1', '$', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '2', '\u0000', 'd', '\u0000', '<', '\u0000', 'Z', '\u0000', '<', '\u0000', 'A', '\u0000', '2', '\u0000', '7', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'd', '\u00c1', '\u001c', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '-', '\u0000', 'Z', '\u0000', '2', '\u0000', 'U', '\u0000', '2', '\u0000', 'P', '\u0000', '-', '\u0000', 'K', '\u0000', '\u001e', '\u0000', 'K', '\u00c1', '\b', '\u0000', '(', '\u0000', 'K', '\u0000', '2', '\u0000', 'A', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '<', '\u0000', 'd', '\u00ff', '\u00c1', '$', '\u0000', '2', '\u0000', '_', '\u0000', '2', '\u0000', 'U', '\u0000', '-', '\u0000', 'P', '\u0000', '#', '\u0000', 'P', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '\u001e', '\u0000', '_', '\u0000', '#', '\u0000', 'd', '\u0000', '-', '\u0000', 'd', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', 'Z', '\u0000', '(', '\u0000', '2', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '(', '\u0000', '<', '\u0000', '(', '\u00c1', '\b', '\u0000', '<', '\u0000', 'F', '\u0000', '\u0014', '\u0000', 'F', '\u00ff', '\u00c1', ' ', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', '_', '\u0000', '#', '\u0000', 'd', '\u0000', '-', '\u0000', 'd', '\u0000', '2', '\u0000', '_', '\u0000', '2', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '2', '\u0000', 'F', '\u00ff', '\u00c1', '$', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '2', '\u0000', 'd', '\u0000', '(', '\u0000', 'Z', '\u0000', '-', '\u0000', 'Z', '\u0000', '2', '\u0000', 'U', '\u0000', '2', '\u0000', 'K', '\u0000', '-', '\u0000', 'F', '\u0000', '#', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'K', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '<', '\u0000', 'i', '\u00ff', '\u00c1', '\f', '\u0000', 'B', '\u0000', 'K', '\u0000', '9', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u0014', '\u0000', '9', '\u0000', '(', '\u0000', '/', '\u0000', '\u001e', '\u0000', '\u001b', '\u0000', '\u001e', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\n', '\u0000', '\n', '\u00c1', '\b', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\u0019', '\u0000', 'K', '\u00ff', '\u00c1', '\u001c', '\u0000', '<', '\u0000', '\u001e', '\u0000', '<', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '<', '\u0000', '<', '\u0000', '<', '\u00c1', '\b', '\u0000', '(', '\u0000', 'Z', '\u0000', '(', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u00ff', '\u00c1', '\u0018', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u0019', '\u0000', '-', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u000f', '\u0000', '#', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u0014', '\u00ff', '\u00c1', '\u0010', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '(', '\u0000', 'd', '\u0000', '(', '\u0000', 'F', '\u0000', '2', '\u0000', 'F', '\u00c1', '\b', '\u0000', '(', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u00ff', '\u00c1', '$', '\u0000', '\u0019', '\u0000', '_', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '2', '\u0000', 'd', '\u0000', '7', '\u0000', '_', '\u0000', '7', '\u0000', 'K', '\u0000', '2', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '\u0019', '\u0000', '_', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', '<', '\u0000', '7', '\u0000', '<', '\u00ff', '\u00c1', '\f', '\u0000', '\u0011', '\u0000', 'K', '\u0000', '(', '\u0000', '5', '\u0000', '\u0011', '\u0000', '\u001e', '\u00c1', '\f', '\u0000', '(', '\u0000', 'K', '\u0000', '?', '\u0000', '5', '\u0000', '(', '\u0000', '\u001e', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', '\u001e', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\f', '\u0000', '\n', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'F', '\u00c1', '\b', '\u0000', '\n', '\u0000', 'F', '\u0000', '\u001e', '\u0000', 'F', '\u00c1', '\u0014', '\u0000', '<', '\u0000', '\u001e', '\u0000', '<', '\u0000', '<', '\u0000', '-', '\u0000', '(', '\u0000', '-', '\u0000', '#', '\u0000', 'A', '\u0000', '#', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', '\u001e', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\n', '\u0000', 'F', '\u00c1', ' ', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '2', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '2', '\u0000', 'F', '\u0000', '7', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', '<', '\u0000', '2', '\u0000', '7', '\u0000', '2', '\u0000', '2', '\u00c1', '\f', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '\u0014', '\u0000', 'd', '\u0000', '\n', '\u0000', 'Z', '\u00ff', '\u00c1', '\b', '\u0000', '\n', '\u0000', '\u001e', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\u0014', '\u0000', '<', '\u0000', '\u001e', '\u0000', '<', '\u0000', '<', '\u0000', '-', '\u0000', '(', '\u0000', '-', '\u0000', '#', '\u0000', 'A', '\u0000', '#', '\u00c1', '$', '\u0000', '\n', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'F', '\u0000', '\u000f', '\u0000', 'F', '\u0000', '\n', '\u0000', 'K', '\u00ff', '\u00c1', '\u0014', '\u0000', '(', '\u0000', 'P', '\u0000', '%', '\u0000', 'M', '\u0000', '(', '\u0000', 'J', '\u0000', '+', '\u0000', 'M', '\u0000', '(', '\u0000', 'P', '\u00c1', ' ', '\u0000', '(', '\u0000', '<', '\u0000', '(', '\u0000', '-', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '2', '\u0000', '\n', '\u0000', '7', '\u0000', '\u000f', '\u0000', '7', '\u0000', '\u001a', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', 'U', '\u0000', 'F', '\u0000', '8', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '-', '\u0000', '\n', '\u0000', '-', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '\u0014', '\u0000', 'i', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', 'U', '\u0000', 'F', '\u0000', '8', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '-', '\u0000', '\n', '\u0000', '-', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '<', '\u0000', 'i', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', 'U', '\u0000', 'F', '\u0000', '8', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '-', '\u0000', '\n', '\u0000', '-', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', '_', '\u0000', '(', '\u0000', 'i', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', 'U', '\u0000', 'F', '\u0000', '8', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '-', '\u0000', '\n', '\u0000', '-', '\u00c1', '\u0018', '\u0000', '\u0019', '\u0000', '_', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '%', '\u0000', 'd', '\u0000', '+', '\u0000', '_', '\u0000', '2', '\u0000', '_', '\u0000', '7', '\u0000', 'd', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', 'U', '\u0000', 'F', '\u0000', '8', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '-', '\u0000', '\n', '\u0000', '-', '\u00c1', '\b', '\u0000', '4', '\u0000', '_', '\u0000', '4', '\u0000', 'i', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'i', '\u00ff', '\u00c1', '\u0014', '\u0000', '\n', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '7', '\u0000', '(', '\u0000', 'U', '\u0000', 'F', '\u0000', '8', '\u0000', 'F', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', 'F', '\u0000', '-', '\u0000', '\n', '\u0000', '-', '\u00c1', '$', '\u0000', '%', '\u0000', 'i', '\u0000', '*', '\u0000', 'i', '\u0000', '/', '\u0000', 'd', '\u0000', '/', '\u0000', '_', '\u0000', '*', '\u0000', 'Z', '\u0000', '%', '\u0000', 'Z', '\u0000', ' ', '\u0000', '_', '\u0000', ' ', '\u0000', 'd', '\u0000', '%', '\u0000', 'i', '\u00ff', '\u00c1', '\u0010', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'd', '\u0000', '\n', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', 'd', '\u0000', 'F', '\u0000', 'd', '\u00c1', '\b', '\u0000', '(', '\u0000', 'A', '\u0000', '<', '\u0000', 'A', '\u00c1', '\b', '\u0000', '(', '\u0000', '2', '\u0000', '\u0013', '\u0000', '2', '\u00ff', '\u00c1', ' ', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u00c1', '\u0018', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u0019', '\u0000', '-', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u000f', '\u0000', '#', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u0014', '\u00ff', '\u00c1', '\u0010', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'U', '\u0000', 'A', '\u0000', 'U', '\u00c1', '\b', '\u0000', '7', '\u0000', ':', '\u0000', '\u000f', '\u0000', ':', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '\u0014', '\u0000', 'i', '\u00ff', '\u00c1', '\u0010', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'U', '\u0000', 'A', '\u0000', 'U', '\u00c1', '\b', '\u0000', '7', '\u0000', ':', '\u0000', '\u000f', '\u0000', ':', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '<', '\u0000', 'i', '\u00ff', '\u00c1', '\u0010', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'U', '\u0000', 'A', '\u0000', 'U', '\u00c1', '\b', '\u0000', '7', '\u0000', ':', '\u0000', '\u000f', '\u0000', ':', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', '_', '\u0000', '(', '\u0000', 'i', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\u0010', '\u0000', 'A', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'U', '\u0000', 'A', '\u0000', 'U', '\u00c1', '\b', '\u0000', '7', '\u0000', ':', '\u0000', '\u000f', '\u0000', ':', '\u00c1', '\b', '\u0000', '4', '\u0000', '_', '\u0000', '4', '\u0000', 'i', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'i', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'U', '\u0000', '<', '\u0000', 'U', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '\u0014', '\u0000', 'i', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'U', '\u0000', '<', '\u0000', 'U', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '<', '\u0000', 'i', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'U', '\u0000', '<', '\u0000', 'U', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', '_', '\u0000', '(', '\u0000', 'i', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'U', '\u0000', '<', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'h', '\u00c1', '\b', '\u0000', '4', '\u0000', 'h', '\u0000', '4', '\u0000', '_', '\u00ff', '\u00c1', '\u0018', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '(', '\u0000', 'A', '\u0000', 'Z', '\u0000', '7', '\u0000', 'd', '\u0000', '\u000f', '\u0000', 'd', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '\u0019', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '\n', '\u0000', 'B', '\u0000', '(', '\u0000', 'B', '\u00ff', '\u00c1', '\u0010', '\u0000', 'F', '\u0000', 'U', '\u0000', 'F', '\u0000', '\u001e', '\u0000', '\n', '\u0000', 'U', '\u0000', '\n', '\u0000', '\u001e', '\u00c1', '\u0018', '\u0000', '\u0019', '\u0000', '_', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '%', '\u0000', 'd', '\u0000', '+', '\u0000', '_', '\u0000', '2', '\u0000', '_', '\u0000', '7', '\u0000', 'd', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '7', '\u0000', 'U', '\u0000', 'A', '\u0000', 'K', '\u0000', 'A', '\u0000', '(', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '\u0014', '\u0000', 'i', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '7', '\u0000', 'U', '\u0000', 'A', '\u0000', 'K', '\u0000', 'A', '\u0000', '(', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '<', '\u0000', 'i', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '7', '\u0000', 'U', '\u0000', 'A', '\u0000', 'K', '\u0000', 'A', '\u0000', '(', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', '_', '\u0000', '(', '\u0000', 'i', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '7', '\u0000', 'U', '\u0000', 'A', '\u0000', 'K', '\u0000', 'A', '\u0000', '(', '\u00c1', '\u0018', '\u0000', '\u0019', '\u0000', '_', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '%', '\u0000', 'd', '\u0000', '+', '\u0000', '_', '\u0000', '2', '\u0000', '_', '\u0000', '7', '\u0000', 'd', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'K', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '7', '\u0000', 'U', '\u0000', 'A', '\u0000', 'K', '\u0000', 'A', '\u0000', '(', '\u00c1', '\b', '\u0000', '4', '\u0000', '_', '\u0000', '4', '\u0000', 'i', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', 'i', '\u0000', '\u001c', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '#', '\u0000', '<', '\u0000', 'P', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'P', '\u0000', '<', '\u0000', '#', '\u00ff', '\u00c1', '$', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'Z', '\u0000', '\u0019', '\u0000', 'd', '\u0000', '7', '\u0000', 'd', '\u0000', 'A', '\u0000', 'Z', '\u0000', 'A', '\u0000', '(', '\u00c1', '\b', '\u0000', 'F', '\u0000', 'n', '\u0000', '\n', '\u0000', '\u0014', '\u00ff', '\u00c1', '\u0018', '\u0000', 'A', '\u0000', 'U', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'i', '\u0000', '(', '\u0000', '_', '\u00ff', '\u00c1', '\u0018', '\u0000', 'A', '\u0000', 'U', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'U', '\u00c1', '\b', '\u0000', '<', '\u0000', 'i', '\u0000', '(', '\u0000', '_', '\u00ff', '\u00c1', '\u0018', '\u0000', 'A', '\u0000', 'U', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'U', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', '_', '\u0000', '(', '\u0000', 'i', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\u0018', '\u0000', 'A', '\u0000', 'U', '\u0000', 'A', '\u0000', '(', '\u0000', '7', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', 'U', '\u00c1', '\b', '\u0000', '4', '\u0000', '_', '\u0000', '4', '\u0000', 'i', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'i', '\u00ff', '\u00c1', '\f', '\u0000', '\u000f', '\u0000', 'U', '\u0000', '(', '\u0000', '7', '\u0000', 'A', '\u0000', 'U', '\u00c1', '\b', '\u0000', '(', '\u0000', '7', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '(', '\u0000', '_', '\u0000', '<', '\u0000', 'i', '\u00ff', '\u00c1', '\b', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', 'd', '\u00c1', '\u0018', '\u0000', '\u000f', '\u0000', 'S', '\u0000', '7', '\u0000', 'S', '\u0000', 'A', '\u0000', 'I', '\u0000', 'A', '\u0000', '9', '\u0000', '7', '\u0000', '/', '\u0000', '\u000f', '\u0000', '/', '\u00ff', '\u00c1', ',', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '7', '\u0000', '\u001e', '\u0000', 'A', '\u0000', '(', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', 'F', '\u0000', 'A', '\u0000', 'P', '\u0000', 'A', '\u0000', 'Z', '\u0000', '7', '\u0000', 'd', '\u0000', '\u001e', '\u0000', 'd', '\u0000', '\u0014', '\u0000', 'Z', '\u0000', '\u0014', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '\u001f', '\u0000', 'F', '\u0000', '7', '\u0000', 'F', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00c1', '\b', '\u0000', '#', '\u0000', 'U', '\u0000', '\u000f', '\u0000', '_', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00c1', '\b', '\u0000', '#', '\u0000', 'U', '\u0000', '7', '\u0000', '_', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00c1', '\f', '\u0000', '\u001b', '\u0000', 'U', '\u0000', '%', '\u0000', '_', '\u0000', '/', '\u0000', 'U', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00c1', '\u0018', '\u0000', '\u0014', '\u0000', 'U', '\u0000', '\u0019', '\u0000', 'Z', '\u0000', '!', '\u0000', 'Z', '\u0000', '%', '\u0000', 'U', '\u0000', '-', '\u0000', 'U', '\u0000', '2', '\u0000', 'Y', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00c1', '\b', '\u0000', '2', '\u0000', 'U', '\u0000', '2', '\u0000', '_', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '\u0019', '\u0000', '_', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0019', '\u0000', 'K', '\u0000', '-', '\u0000', 'K', '\u0000', '7', '\u0000', 'A', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u00c1', '\u001c', '\u0000', '7', '\u0000', '(', '\u0000', '-', '\u0000', '\u001e', '\u0000', '\u0019', '\u0000', '\u001e', '\u0000', '\u000f', '\u0000', '(', '\u0000', '\u000f', '\u0000', '-', '\u0000', '\u0019', '\u0000', '7', '\u0000', '7', '\u0000', '7', '\u00c1', '$', '\u0000', '#', '\u0000', 'd', '\u0000', '(', '\u0000', 'd', '\u0000', '-', '\u0000', '_', '\u0000', '-', '\u0000', 'Z', '\u0000', '(', '\u0000', 'U', '\u0000', '#', '\u0000', 'U', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '\u001e', '\u0000', '_', '\u0000', '#', '\u0000', 'd', '\u00ff', '\u00c1', ',', '\u0000', '(', '\u0000', '7', '\u0000', '(', '\u0000', '(', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '\u001e', '\u0000', '\n', '\u0000', '(', '\u0000', '\n', '\u0000', '-', '\u0000', '\u0014', '\u0000', '7', '\u0000', '(', '\u0000', '7', '\u0000', '(', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '\u0014', '\u0000', 'K', '\u00c1', '\u001c', '\u0000', '(', '\u0000', '2', '\u0000', 'A', '\u0000', '2', '\u0000', 'F', '\u0000', '7', '\u0000', 'F', '\u0000', 'A', '\u0000', '<', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '(', '\u0000', 'A', '\u00c1', '\f', '\u0000', '(', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', 'F', '\u0000', '\u001e', '\u00ff', '\u00c1', ' ', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\u0018', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', '\u0019', '\u0000', '-', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u000f', '\u0000', '#', '\u0000', '\u000f', '\u0000', '\u001e', '\u0000', '\u0014', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', '7', '\u0000', '2', '\u0000', '\u0014', '\u0000', '2', '\u00c1', '\b', '\u0000', '(', '\u0000', 'U', '\u0000', '\u0014', '\u0000', '_', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', '7', '\u0000', '2', '\u0000', '\u0014', '\u0000', '2', '\u00c1', '\b', '\u0000', '(', '\u0000', 'U', '\u0000', '<', '\u0000', '_', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', '7', '\u0000', '2', '\u0000', '\u0014', '\u0000', '2', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '(', '\u0000', '_', '\u0000', '2', '\u0000', 'U', '\u00ff', '\u00c1', '(', '\u0000', '<', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '7', '\u0000', '7', '\u0000', '2', '\u0000', '\u0014', '\u0000', '2', '\u00c1', '\b', '\u0000', '\u001b', '\u0000', 'U', '\u0000', '\u001b', '\u0000', '_', '\u00c1', '\b', '\u0000', '4', '\u0000', 'U', '\u0000', '4', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'K', '\u00c1', '\b', '\u0000', '(', '\u0000', 'U', '\u0000', '\u0014', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'K', '\u00c1', '\b', '\u0000', '(', '\u0000', 'U', '\u0000', '<', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'K', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '(', '\u0000', '_', '\u0000', '2', '\u0000', 'U', '\u00ff', '\u00c1', '\b', '\u0000', '(', '\u0000', '\u001e', '\u0000', '(', '\u0000', 'K', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'U', '\u00c1', '\b', '\u0000', '4', '\u0000', 'U', '\u0000', '4', '\u0000', '_', '\u00ff', '\u00c1', '(', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '\u0014', '\u0000', 'i', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'U', '\u0000', '(', '\u0000', 'i', '\u00ff', '\u00c1', '\f', '\u0000', '\n', '\u0000', 'K', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u0014', '\u0000', '\u001e', '\u00c1', '\u0014', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '\u001e', '\u00c1', '\u0018', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '%', '\u0000', 'Z', '\u0000', '+', '\u0000', 'U', '\u0000', '2', '\u0000', 'U', '\u0000', '7', '\u0000', 'Z', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '_', '\u0000', '(', '\u0000', 'U', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\b', '\u0000', '(', '\u0000', 'U', '\u0000', '<', '\u0000', '_', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\f', '\u0000', '\u001e', '\u0000', 'U', '\u0000', '(', '\u0000', '_', '\u0000', '2', '\u0000', 'U', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\u0018', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '\u001e', '\u0000', 'Z', '\u0000', '%', '\u0000', 'Z', '\u0000', '+', '\u0000', 'U', '\u0000', '2', '\u0000', 'U', '\u0000', '7', '\u0000', 'Z', '\u00ff', '\u00c1', '$', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u00c1', '\b', '\u0000', '4', '\u0000', '_', '\u0000', '4', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', 'U', '\u0000', '\u001c', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', 'F', '\u0000', '<', '\u0000', 'F', '\u00c1', '\u0014', '\u0000', '-', '\u0000', '7', '\u0000', '(', '\u0000', '2', '\u0000', '#', '\u0000', '7', '\u0000', '(', '\u0000', '<', '\u0000', '-', '\u0000', '7', '\u00c1', '\u0014', '\u0000', '-', '\u0000', 'U', '\u0000', '(', '\u0000', 'P', '\u0000', '#', '\u0000', 'U', '\u0000', '(', '\u0000', 'Z', '\u0000', '-', '\u0000', 'U', '\u00ff', '\u00c1', '$', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u0000', '\u0014', '\u0000', 'A', '\u0000', '\u001e', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u00c1', '\b', '\u0000', '@', '\u0000', 'P', '\u0000', '\u0011', '\u0000', '\u0019', '\u00ff', '\u00c1', '\u0018', '\u0000', '9', '\u0000', 'K', '\u0000', '9', '\u0000', '(', '\u0000', '/', '\u0000', '\u001e', '\u0000', '\u001b', '\u0000', '\u001e', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\u0011', '\u0000', 'K', '\u00c1', '\b', '\u0000', '9', '\u0000', '(', '\u0000', 'C', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '\u0011', '\u0000', '_', '\u0000', '%', '\u0000', 'V', '\u00ff', '\u00c1', '\u0018', '\u0000', '9', '\u0000', 'K', '\u0000', '9', '\u0000', '(', '\u0000', '/', '\u0000', '\u001e', '\u0000', '\u001b', '\u0000', '\u001e', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\u0011', '\u0000', 'K', '\u00c1', '\b', '\u0000', '9', '\u0000', '(', '\u0000', 'C', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '%', '\u0000', 'U', '\u0000', '9', '\u0000', '_', '\u00ff', '\u00c1', '\u0018', '\u0000', '9', '\u0000', 'K', '\u0000', '9', '\u0000', '(', '\u0000', '/', '\u0000', '\u001e', '\u0000', '\u001b', '\u0000', '\u001e', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\u0011', '\u0000', 'K', '\u00c1', '\b', '\u0000', '9', '\u0000', '(', '\u0000', 'C', '\u0000', '\u001e', '\u00c1', '\f', '\u0000', '/', '\u0000', 'U', '\u0000', '%', '\u0000', '_', '\u0000', '\u001b', '\u0000', 'U', '\u00ff', '\u00c1', '\u0018', '\u0000', '9', '\u0000', 'K', '\u0000', '9', '\u0000', '(', '\u0000', '/', '\u0000', '\u001e', '\u0000', '\u001b', '\u0000', '\u001e', '\u0000', '\u0011', '\u0000', '(', '\u0000', '\u0011', '\u0000', 'K', '\u00c1', '\b', '\u0000', '9', '\u0000', '(', '\u0000', 'C', '\u0000', '\u001e', '\u00c1', '\b', '\u0000', '\u0019', '\u0000', 'U', '\u0000', '\u0019', '\u0000', '_', '\u00c1', '\b', '\u0000', '2', '\u0000', 'U', '\u0000', '2', '\u0000', '_', '\u00ff', '\u00c1', '\f', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '\u0014', '\u0000', '2', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\f', '\u0000', '<', '\u0000', 'K', '\u0000', '<', '\u0000', '2', '\u0000', '\u0014', '\u0000', '\n', '\u00c1', '\b', '\u0000', '(', '\u0000', 'U', '\u0000', '<', '\u0000', '_', '\u00ff', '\u00c1', '\b', '\u0000', '\u0014', '\u0000', '\n', '\u0000', '\u0014', '\u0000', 'd', '\u00c1', '\u001c', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '2', '\u0000', 'K', '\u0000', '<', '\u0000', 'A', '\u0000', '<', '\u0000', '(', '\u0000', '2', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u001e', '\u0000', '\u0014', '\u0000', '(', '\u00ff', '\u00c1', '\f', '\u0000', '\u0014', '\u0000', 'K', '\u0000', '\u0014', '\u0000', '2', '\u0000', '(', '\u0000', '\u001e', '\u00c1', '\f', '\u0000', '<', '\u0000', 'K', '\u0000', '<', '\u0000', '2', '\u0000', '\u0014', '\u0000', '\n', '\u00c1', '\b', '\u0000', '4', '\u0000', '_', '\u0000', '4', '\u0000', 'U', '\u00c1', '\b', '\u0000', '\u001c', '\u0000', '_', '\u0000', '\u001c', '\u0000', 'U', '\u00ff'}; + public static final int MARKER_WIDTH = 24; + public static final int MARKER_HEIGHT = 32; + static char[] marker_data = new char[]{'\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00ff', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\u0012', '\u0000', '\u0010', '\u00ff', '\u00c1', '\u0014', '\u0000', '\f', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\n', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\n', '\u00ff', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00ff', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\u0012', '\u0000', '\u0010', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\f', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\n', '`', '\u0000', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\n', '`', '\u0000', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\u000b', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\u0011', '\u0000', '\u000b', '\u0000', '\u0011', '\u0000', '\u000b', '\u0000', '\u000f', '`', '\u0000', '\u00ff', '\u00c5', '\u0018', '\u0000', '\t', '\u0000', '\u0010', '\u0000', '\t', '\u0000', '\u0013', '\u0000', '\u000f', '\u0000', '\u0013', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\t', '\u0000', '\r', '\u0000', '\t', '\u0000', '\u0010', '\u00ff'}; +} + diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index 959d36d..f72b973 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -31,6 +31,12 @@ public class InputProcessor { this.fsm = fsm; } + private org.lib3270j.graphics.GraphicsPlane graphicsPlane; + + public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) { + this.graphicsPlane = gp; + } + public enum OiaStatus { NOT_CONNECTED("OFFLINE"), X_SYSTEM("X SYSTEM"), @@ -160,6 +166,9 @@ public class InputProcessor { if (aidCode == AID_CLEAR) { screen.clear(); screen.markAllChanged(); + if (graphicsPlane != null) { + graphicsPlane.clear(); + } // Send just the AID byte[] data = new byte[] { (byte) aidCode }; sendAidResponse(data); diff --git a/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java b/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java index 7ba9565..4b42708 100644 --- a/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java +++ b/lib3270j/src/main/java/org/lib3270j/protocol/DS3270Constants.java @@ -202,16 +202,25 @@ public final class DS3270Constants { 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_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; // ========== Screen model sizes ========== diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java index ded53a8..0d967f7 100644 --- a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetConnection.java @@ -28,26 +28,42 @@ public class TelnetConnection { private final TelnetFSM fsm; private final ConnectionConfig config; + private javax.net.ssl.SSLSession sslSession; + public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) { this.config = config; this.fsm = fsm; } + public javax.net.ssl.SSLSession getSslSession() { + return sslSession; + } + /** * Connect to the host. Blocks until connection is established or fails. */ public void connect() throws IOException { if (config.isUseTls()) { - log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort()); - javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(); - javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(); - sslSocket.setKeepAlive(true); - sslSocket.setOOBInline(true); - sslSocket.setTcpNoDelay(true); - sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()), - config.getConnectTimeoutMs()); - sslSocket.startHandshake(); - socket = sslSocket; + log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() + + " (verifyCert=" + config.isTlsVerifyCert() + ")"); + try { + javax.net.ssl.SSLContext sslContext = org.lib3270j.tls.TlsTrustManager.createSSLContext(config); + javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory(); + javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(); + sslSocket.setKeepAlive(true); + sslSocket.setTcpNoDelay(true); + sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()), + config.getConnectTimeoutMs()); + sslSocket.startHandshake(); + socket = sslSocket; + sslSession = sslSocket.getSession(); + log.info("TLS session active: protocol=" + sslSession.getProtocol() + + " cipher=" + sslSession.getCipherSuite()); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("TLS setup failure: " + e.getMessage(), e); + } } else { log.info("Connecting to " + config.getHost() + ":" + config.getPort()); socket = new Socket(); diff --git a/lib3270j/src/main/java/org/lib3270j/tls/TlsCertificateVerifier.java b/lib3270j/src/main/java/org/lib3270j/tls/TlsCertificateVerifier.java new file mode 100644 index 0000000..b629490 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/tls/TlsCertificateVerifier.java @@ -0,0 +1,22 @@ +package org.lib3270j.tls; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Callback interface for validating TLS server certificates. + * Used when standard certificate path validation fails or when custom verification is needed. + */ +@FunctionalInterface +public interface TlsCertificateVerifier { + + /** + * Determine whether to trust an unverified server certificate chain. + * + * @param chain The peer certificate chain presented by the server. + * @param authType The key exchange algorithm (e.g., "RSA", "ECDHE_RSA"). + * @param exception The CertificateException thrown by standard validation (or null if called proactively). + * @return true to trust the certificate and proceed with the connection; false to abort. + */ + boolean shouldTrust(X509Certificate[] chain, String authType, CertificateException exception); +} diff --git a/lib3270j/src/main/java/org/lib3270j/tls/TlsTrustManager.java b/lib3270j/src/main/java/org/lib3270j/tls/TlsTrustManager.java new file mode 100644 index 0000000..2eeafd7 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/tls/TlsTrustManager.java @@ -0,0 +1,114 @@ +package org.lib3270j.tls; + +import org.lib3270j.ConnectionConfig; + +import javax.net.ssl.*; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Custom X509TrustManager that supports: + * 1. Standard certificate verification using the JVM default TrustManager. + * 2. Unverified / trust-all mode when tlsVerifyCert is false. + * 3. Interactive/custom certificate verifier callbacks (e.g. GUI prompts for self-signed certificates). + */ +public class TlsTrustManager implements X509TrustManager { + + private static final Logger log = Logger.getLogger(TlsTrustManager.class.getName()); + + private final ConnectionConfig config; + private X509TrustManager defaultTrustManager; + + public TlsTrustManager(ConnectionConfig config) { + this.config = config; + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + this.defaultTrustManager = (X509TrustManager) tm; + break; + } + } + } catch (Exception e) { + log.log(Level.WARNING, "Failed to initialize default TrustManagerFactory", e); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (defaultTrustManager != null) { + defaultTrustManager.checkClientTrusted(chain, authType); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (config != null && !config.isTlsVerifyCert()) { + log.fine("Certificate verification bypassed (tlsVerifyCert=false)"); + return; + } + + if (chain == null || chain.length == 0) { + CertificateException ex = new CertificateException("null or zero-length certificate chain"); + if (config != null && config.getCertificateVerifier() != null) { + if (config.getCertificateVerifier().shouldTrust(chain, authType, ex)) { + log.info("Server certificate accepted via TlsCertificateVerifier callback"); + return; + } + } + throw ex; + } + + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(chain, authType); + } else { + throw new CertificateException("No default X509TrustManager available"); + } + } catch (Exception ex) { + CertificateException certEx = (ex instanceof CertificateException) + ? (CertificateException) ex + : new CertificateException("Certificate validation failed: " + ex.getMessage(), ex); + + log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx); + + if (config != null && config.getCertificateVerifier() != null) { + boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx); + if (accepted) { + log.info("Server certificate accepted via TlsCertificateVerifier callback"); + return; + } else { + log.warning("Server certificate rejected by TlsCertificateVerifier callback"); + throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx); + } + } + throw certEx; + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + if (defaultTrustManager != null) { + return defaultTrustManager.getAcceptedIssuers(); + } + return new X509Certificate[0]; + } + + /** + * Create an initialized SSLContext configured for the given ConnectionConfig. + */ + public static SSLContext createSSLContext(ConnectionConfig config) throws Exception { + String protocol = (config != null && config.getSslProtocol() != null) + ? config.getSslProtocol() + : "TLS"; + SSLContext sslContext = SSLContext.getInstance(protocol); + TlsTrustManager trustManager = new TlsTrustManager(config); + sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom()); + return sslContext; + } +} diff --git a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java index 478cfcf..222cb79 100644 --- a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java @@ -3,6 +3,7 @@ package org.lib3270j.datastream; import org.junit.jupiter.api.Test; import org.lib3270j.TerminalModel; import org.lib3270j.charset.EbcdicTranslator; +import org.lib3270j.graphics.GraphicsMode; import org.lib3270j.screen.ScreenBuffer; import static org.junit.jupiter.api.Assertions.*; import static org.lib3270j.protocol.DS3270Constants.*; @@ -14,11 +15,43 @@ public class QueryReplyBuilderTest { private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen); @Test - public void testBuildAllQueryReplies() { + public void testBuildAllQueryRepliesDefaultNone() { + assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode()); 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 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; + } + } + assertFalse(hasB0); + } + + @Test + public void testBuildAllQueryRepliesWithVectorGraphics() { + qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS); + byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43); + assertNotNull(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) { + hasB0 = true; + } + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GCOLOR) { + hasB4 = true; + } + } + assertTrue(hasB0); + assertTrue(hasB4); } @Test @@ -52,4 +85,29 @@ public class QueryReplyBuilderTest { 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); + } } diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java new file mode 100644 index 0000000..90a9a1c --- /dev/null +++ b/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java @@ -0,0 +1,237 @@ +package org.lib3270j.graphics; + +import org.junit.jupiter.api.Test; +import java.io.ByteArrayOutputStream; +import static org.junit.jupiter.api.Assertions.*; + +public class GocaDecoderTest { + + @Test + public void testBasicLineAndColorOrders() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + assertFalse(plane.hasContent()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Color to Red (GOCA Color 2) - 2 bytes: order, color + out.write(GocaConstants.G_GSCOL); + out.write(0x02); // Red + + // Line from (1000, 1000) to (2000, 2000) - long order: order, len, x1, y1, x2, y2 + out.write(GocaConstants.G_GLINE); + out.write(0x08); // 8 bytes: (x1, y1, x2, y2) + out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF); + out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF); + out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF); + out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + } + + @Test + public void testAreaAndPatternFill() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Pattern to Solid - 2 bytes: order, pattern + out.write(GocaConstants.G_GSPT); + out.write(GocaConstants.PT_SOLID); + + // Begin Area - 2 bytes: order, flags + out.write(GocaConstants.G_GBAR); + out.write(0x00); + + // Polyline forming a triangle: (500, 500) -> (1500, 500) -> (1000, 1500) -> (500, 500) + out.write(GocaConstants.G_GLINE); + out.write(0x10); // 4 points * 4 bytes = 16 bytes + out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF); + out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF); + + out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF); + out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF); + + out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF); + out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF); + + out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF); + out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF); + + // End Area - 1 byte + out.write(GocaConstants.G_GEAR); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + } + + @Test + public void testMarkerAndVectorText() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Marker Type to Circle - 2 bytes: order, type + out.write(GocaConstants.G_GSMT); + out.write(GocaConstants.MK_CIRCLE); + + // Draw Marker at (2000, 2000) + out.write(GocaConstants.G_GMRK); + out.write(0x04); + out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF); + out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF); + + // Draw Vector Stroked Character String: "IBM" (EBCDIC: 0xC9, 0xC2, 0xD4) + out.write(GocaConstants.G_GCHST); + out.write(0x07); // 4 bytes pos + 3 bytes chars + out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF); + out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF); + out.write(0xC9); out.write(0xC2); out.write(0xD4); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + } + + @Test + public void testProcedureOrdersAndErase() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // Draw something first + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(GocaConstants.G_GLINE); + out.write(0x08); + out.write(0x00); out.write(0x00); + out.write(0x00); out.write(0x00); + out.write(0x00); out.write(0x64); + out.write(0x00); out.write(0x64); + byte[] drawStream = out.toByteArray(); + decoder.decodeStream(drawStream, 0, drawStream.length); + assertTrue(plane.hasContent()); + + // Process procedure order 0x0A (Erase presentation space) + byte[] procOrders = new byte[] { (byte) GocaConstants.P_ERASE }; + decoder.processProcedureOrders(procOrders, 0, procOrders.length); + assertFalse(plane.hasContent()); + } + + @Test + public void testSegmentAndRelativeLineOrders() { + GraphicsPlane plane = new GraphicsPlane(720, 688); + plane.setScreenDimensions(80, 43); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // 0x70: Begin Segment (14 bytes total) + out.write(GocaConstants.G_BEGSEGM); + out.write(0x0C); + out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x01); // Seg ID 1 + out.write(0x74); out.write(0x70); + out.write(0x00); out.write(0x1E); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); + + // 0x3E: End Prologue + out.write(GocaConstants.G_ENDPROLOGUE); + out.write(0x00); + + // 0x0A: Color (7 = White) + out.write(GocaConstants.G_GSCOL); + out.write(0x07); + + // 0xE1: Relative Line (Absolute Start (300, 200), deltas: (+10, -5), (-10, +5)) + out.write(GocaConstants.G_GRLINE); + out.write(0x08); // 4 bytes start coord + 4 bytes (2 deltas) = 8 bytes + out.write(0x01); out.write(0x2C); // Start X = 300 + out.write(0x00); out.write(0xC8); // Start Y = 200 + out.write(0x0A); out.write(0xFB); // dx = +10, dy = -5 + out.write(0xF6); out.write(0x05); // dx = -10, dy = +5 + + // 0x71: End Segment + out.write(GocaConstants.G_ENDSEGM); + out.write(0x00); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + assertTrue(plane.hasContent()); + } + + @Test + public void testPartialOrderBufferingAcrossStreams() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // First packet sends G_GLINE order code and half of its coordinates + ByteArrayOutputStream chunk1 = new ByteArrayOutputStream(); + chunk1.write(GocaConstants.G_GLINE); + chunk1.write(0x08); // 8 bytes: 2 points + chunk1.write(0x00); chunk1.write(0x64); // x1 = 100 + chunk1.write(0x00); chunk1.write(0x64); // y1 = 100 + + byte[] b1 = chunk1.toByteArray(); + decoder.decodeStream(b1, 0, b1.length); + // Order is not complete yet, so no line drawn yet + assertFalse(plane.hasContent()); + + // Second packet sends the rest of the order: x2 = 200, y2 = 200 + ByteArrayOutputStream chunk2 = new ByteArrayOutputStream(); + chunk2.write(0x00); chunk2.write(0xC8); // x2 = 200 + chunk2.write(0x00); chunk2.write(0xC8); // y2 = 200 + + byte[] b2 = chunk2.toByteArray(); + decoder.decodeStream(b2, 0, b2.length); + // Now the combined order executes! + assertTrue(plane.hasContent()); + } + + @Test + public void testSetCurrentPositionAndRelativeLine() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // 0x21: Set Current Position to (50, 50) + out.write(GocaConstants.G_GSCP); + out.write(0x04); // len = 4 + out.write(0x00); out.write(0x32); // x = 50 + out.write(0x00); out.write(0x32); // y = 50 + + // 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10) + out.write(GocaConstants.G_GCRLIN); + out.write(0x04); // len = 4 (2 steps) + out.write(0x0A); out.write(0x14); // dx = +10, dy = +20 + out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10 + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + assertEquals(55, decoder.getCurX()); + assertEquals(60, decoder.getCurY()); + } + + @Test + public void test3179GCoordinateMapping() { + GraphicsPlane plane = new GraphicsPlane(720, 688); + plane.setScreenDimensions(80, 43); + + // Screen center (0, 0) should map to canvas center (360, 343) + assertEquals(360, plane.mapX(0)); + assertEquals(343, plane.mapY(0)); + + // Left edge (-360) should map to 0 + assertEquals(0, plane.mapX(-360)); + // Right edge (+359) should map to 719 + assertEquals(719, plane.mapX(359)); + + // Top edge (+343) should map to 0 + assertEquals(0, plane.mapY(343)); + // Bottom edge (-344) should map to 687 + assertEquals(687, plane.mapY(-344)); + } +} diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java new file mode 100644 index 0000000..e1bcaa2 --- /dev/null +++ b/lib3270j/src/test/java/org/lib3270j/graphics/GraphicsModeTest.java @@ -0,0 +1,57 @@ +package org.lib3270j.graphics; + +import org.junit.jupiter.api.Test; +import org.lib3270j.ConnectionConfig; +import static org.junit.jupiter.api.Assertions.*; + +public class GraphicsModeTest { + + @Test + public void testDefaultsAndParsing() { + assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(null)); + assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("")); + assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("NONE")); + assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("off")); + assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("false")); + + assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("ps")); + assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("programmed_symbols")); + assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("apl")); + + assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector")); + assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("goca")); + assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector_graphics")); + + assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("both")); + assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("all")); + assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("true")); + assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("on")); + } + + @Test + public void testFlags() { + assertFalse(GraphicsMode.NONE.isProgrammedSymbolsEnabled()); + assertFalse(GraphicsMode.NONE.isVectorGraphicsEnabled()); + + assertTrue(GraphicsMode.PROGRAMMED_SYMBOLS.isProgrammedSymbolsEnabled()); + assertFalse(GraphicsMode.PROGRAMMED_SYMBOLS.isVectorGraphicsEnabled()); + + assertFalse(GraphicsMode.VECTOR_GRAPHICS.isProgrammedSymbolsEnabled()); + assertTrue(GraphicsMode.VECTOR_GRAPHICS.isVectorGraphicsEnabled()); + + assertTrue(GraphicsMode.BOTH.isProgrammedSymbolsEnabled()); + assertTrue(GraphicsMode.BOTH.isVectorGraphicsEnabled()); + } + + @Test + public void testConnectionConfigDefault() { + ConnectionConfig config = new ConnectionConfig(); + assertEquals(GraphicsMode.NONE, config.getGraphicsMode()); + + config.setGraphicsMode(GraphicsMode.BOTH); + assertEquals(GraphicsMode.BOTH, config.getGraphicsMode()); + + config.setGraphicsMode(null); + assertEquals(GraphicsMode.NONE, config.getGraphicsMode()); + } +} diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java new file mode 100644 index 0000000..0214ac7 --- /dev/null +++ b/lib3270j/src/test/java/org/lib3270j/graphics/ProgramSymbolManagerTest.java @@ -0,0 +1,144 @@ +package org.lib3270j.graphics; + +import org.junit.jupiter.api.Test; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import static org.junit.jupiter.api.Assertions.*; + +public class ProgramSymbolManagerTest { + + @Test + public void testSetAllocationAndLookup() { + ProgramSymbolManager manager = new ProgramSymbolManager(); + // Loadable set can be queried after loadps or clearAll + assertNull(manager.getSymbolSet(0x40)); + assertNull(manager.getSymbol(0x40, 0x41)); + + manager.clearAll(); + assertNull(manager.getSymbolSet(0x40)); + } + + @Test + public void testSinglePlaneLoadPsFormat1() { + ProgramSymbolManager manager = new ProgramSymbolManager(); + + // Build Format 1 Load PS payload for LCID 0x40, code point 0x41 ('A') + // Format 1 header: + // byte 0: flags (0x01: format 1, single plane) + // byte 1: LCID (0x40) + // byte 2: Start code point (0x41) + // byte 3: RWS (0x02: loadable slot 2) + // followed by 18 bytes per symbol for 9x16 cell: + // Byte 0-1: Col 0 for rows 0..15 (0x80 = bit for row 0 col 0) + // Bytes 2-17: Cols 1..8 for rows 0..15 (0xFF = bits for row 0 cols 1..8) + byte[] payload = new byte[4 + 18]; + payload[0] = 0x01; // Format 1 + payload[1] = 0x40; // LCID 0x40 + payload[2] = 0x41; // Code point 0x41 + payload[3] = 0x02; // RWS 2 + + payload[4] = (byte) 0x80; // Row 0 Col 0 bit + payload[5] = (byte) 0x00; // Rows 8..15 Col 0 + for (int r = 0; r < 16; r++) { + payload[6 + r] = (byte) 0xFF; // Cols 1..8 on all rows + } + + manager.loadps(payload); + + ProgramSymbolSet set = manager.getSymbolSet(0x40); + assertNotNull(set); + assertEquals(0x40, set.getLcid()); + assertFalse(set.isTriplePlane()); + + ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41); + assertNotNull(slot); + assertEquals(9, slot.getWidth()); + assertEquals(16, slot.getHeight()); + assertFalse(slot.isTriplePlane()); + + // Verify pixel data (Row 0 Col 0 is 1) + byte[] pixels = slot.getPixelData(); + assertNotNull(pixels); + assertEquals(1, pixels[0]); // Row 0 Col 0 is 1 + + // Test rendering into BufferedImage + BufferedImage canvas = new BufferedImage(80, 24, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = canvas.createGraphics(); + boolean drawn = manager.drawSymbol(g2, 0x40, 0x41, 0, 0, 10, 20, Color.GREEN, Color.BLACK); + g2.dispose(); + assertTrue(drawn); + } + + @Test + public void testTriplePlaneMultiColorComposite() { + ProgramSymbolManager manager = new ProgramSymbolManager(); + + // Build Format 1 Load PS payload for LCID 0x42, triple plane + // flags = 0x01 (Format 1) + // RWS = 4 (Triple Plane) + byte[] payload = new byte[4 + 18]; + payload[0] = 0x01; // Format 1 + payload[1] = 0x42; // LCID 0x42 + payload[2] = 0x45; // Code point 0x45 + payload[3] = 0x04; // RWS 4 (Triple Plane) + + // Set pixel at row 0 col 0 + payload[4] = (byte) 0x80; // Row 0 Col 0 + payload[5] = (byte) 0x00; + for (int r = 0; r < 16; r++) { + payload[6 + r] = (byte) 0x00; + } + + manager.loadps(payload); + + ProgramSymbolSet set = manager.getSymbolSet(0x42); + assertNotNull(set); + assertTrue(set.isTriplePlane()); + + ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, 0x45); + assertNotNull(slot); + assertTrue(slot.isTriplePlane()); + + // When loaded with colorPlane=0 (default), all 3 planes are set (val 7 = white composite) + byte[] pixels = slot.getPixelData(); + assertEquals(7, pixels[0] & 0xFF); + + // Render image + BufferedImage img = slot.getImage(Color.WHITE, Color.BLACK); + assertNotNull(img); + + // Pixel (0,0) should be white composite (Red=255, Green=255, Blue=255) + int rgb = img.getRGB(0, 0); + Color pixelColor = new Color(rgb, true); + assertEquals(255, pixelColor.getRed()); + assertEquals(255, pixelColor.getGreen()); + assertEquals(255, pixelColor.getBlue()); + } + + @Test + public void testFormat3Bitstream() { + ProgramSymbolManager manager = new ProgramSymbolManager(); + + // Format 3 payload + // byte 0: 0x03 (Format 3) + // byte 1: LCID (0x44) + // byte 2: code point (0x43) + // byte 3: RWS (2) + // followed by (9 * 16 + 7) / 8 = 18 bytes + byte[] payload = new byte[4 + 18]; + payload[0] = 0x03; // Format 3 + payload[1] = 0x44; // LCID 0x44 + payload[2] = 0x43; // Code point 0x43 + payload[3] = 0x02; // RWS 2 + payload[4] = (byte) 0x80; // First pixel is set + + manager.loadps(payload); + + ProgramSymbolSet set = manager.getSymbolSet(0x44); + assertNotNull(set); + ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x44, 0x43); + assertNotNull(slot); + assertEquals(1, slot.getPixelData()[0]); + } +} diff --git a/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java b/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java new file mode 100644 index 0000000..4a11bcf --- /dev/null +++ b/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java @@ -0,0 +1,108 @@ +package org.lib3270j.tls; + +import org.junit.jupiter.api.Test; +import org.lib3270j.ConnectionConfig; +import org.lib3270j.TerminalModel; + +import javax.net.ssl.SSLContext; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +public class TlsConfigTest { + + @Test + public void testConnectionConfigDefaults() { + ConnectionConfig config = new ConnectionConfig("localhost", 23); + assertFalse(config.isUseTls()); + assertTrue(config.isTlsVerifyCert()); + assertEquals("TLS", config.getSslProtocol()); + assertNull(config.getCertificateVerifier()); + } + + @Test + public void testParseHostStringTlsPrefixes() { + // L: prefix + ConnectionConfig c1 = ConnectionConfig.parseHostString("L:mainframe.example.com", 0, TerminalModel.IBM_3279_4); + assertTrue(c1.isUseTls()); + assertEquals("mainframe.example.com", c1.getHost()); + assertEquals(992, c1.getPort()); + + // ssl: prefix with explicit port + ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_2); + assertTrue(c2.isUseTls()); + assertEquals("zos.local", c2.getHost()); + assertEquals(2323, c2.getPort()); + + // y: prefix + ConnectionConfig c3 = ConnectionConfig.parseHostString("y:vm370.net:992", 23, TerminalModel.IBM_3279_4); + assertTrue(c3.isUseTls()); + assertEquals("vm370.net", c3.getHost()); + assertEquals(992, c3.getPort()); + + // Plain host + ConnectionConfig c4 = ConnectionConfig.parseHostString("plain.host.com", 23, TerminalModel.IBM_3279_4); + assertFalse(c4.isUseTls()); + assertEquals("plain.host.com", c4.getHost()); + assertEquals(23, c4.getPort()); + + // IPv6 host with brackets + ConnectionConfig c5 = ConnectionConfig.parseHostString("L:[2001:db8::1]:992", 0, TerminalModel.IBM_3279_4); + assertTrue(c5.isUseTls()); + assertEquals("2001:db8::1", c5.getHost()); + assertEquals(992, c5.getPort()); + } + + @Test + public void testTlsTrustManagerBypass() { + ConnectionConfig config = new ConnectionConfig("untrusted.host", 992); + config.setUseTls(true); + config.setTlsVerifyCert(false); + + TlsTrustManager trustManager = new TlsTrustManager(config); + // With tlsVerifyCert=false, checkServerTrusted must not throw even with null/empty chain + assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA")); + } + + @Test + public void testTlsTrustManagerVerifierCallback() { + ConnectionConfig config = new ConnectionConfig("selfsigned.host", 992); + config.setUseTls(true); + config.setTlsVerifyCert(true); + + AtomicBoolean verifierInvoked = new AtomicBoolean(false); + config.setCertificateVerifier((chain, authType, exception) -> { + verifierInvoked.set(true); + return true; // Accept certificate + }); + + TlsTrustManager trustManager = new TlsTrustManager(config); + // Standard verification will fail on empty cert chain, triggering our callback + assertDoesNotThrow(() -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA")); + assertTrue(verifierInvoked.get(), "Expected verifier callback to be invoked"); + } + + @Test + public void testTlsTrustManagerVerifierRejection() { + ConnectionConfig config = new ConnectionConfig("badcert.host", 992); + config.setUseTls(true); + config.setTlsVerifyCert(true); + + config.setCertificateVerifier((chain, authType, exception) -> false); // Reject certificate + + TlsTrustManager trustManager = new TlsTrustManager(config); + assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted(new X509Certificate[0], "RSA")); + } + + @Test + public void testCreateSSLContext() throws Exception { + ConnectionConfig config = new ConnectionConfig("secure.host", 992); + config.setUseTls(true); + + SSLContext context = TlsTrustManager.createSSLContext(config); + assertNotNull(context); + assertNotNull(context.getSocketFactory()); + } +}