Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8d076bf89e
|
|||
|
ac27c1ed5c
|
|||
|
6e682d3b5c
|
@@ -16,7 +16,7 @@ An **x3270-aligned and IBM Host On-Demand (HoD) compatible IBM 3270 mainframe te
|
|||||||
Ready-to-run executable JARs and standalone library packages are built and tested across all three Java LTS baselines (**Java 11, Java 17, and Java 21**) by Gitea Actions:
|
Ready-to-run executable JARs and standalone library packages are built and tested across all three Java LTS baselines (**Java 11, Java 17, and Java 21**) by Gitea Actions:
|
||||||
|
|
||||||
- **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)**
|
- **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)**
|
||||||
- **Standalone Protocol Library (`lib3270j.jar`)**: Built by setup/build scripts and published to Maven (`haus.nightmare.j3270:lib3270j:1.2.4`).
|
- **Standalone Protocol Library (`lib3270j.jar`)**: Built by setup/build scripts and published to Maven (`haus.nightmare.j3270:lib3270j:1.3.0`).
|
||||||
- **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)**: Download standalone JARs tailored for each LTS runtime under the latest workflow run:
|
- **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)**: Download standalone JARs tailored for each LTS runtime under the latest workflow run:
|
||||||
- `j3270-java11-jar` (Java 11 LTS)
|
- `j3270-java11-jar` (Java 11 LTS)
|
||||||
- `j3270-java17-jar` (Java 17 LTS — primary Gitea runner baseline)
|
- `j3270-java17-jar` (Java 17 LTS — primary Gitea runner baseline)
|
||||||
@@ -126,7 +126,7 @@ java -jar j3270.jar -d mainframe.example.com
|
|||||||
- **Persistent Profiles**: Configuration profiles and session preferences saved in INI files (`j3270.ini`).
|
- **Persistent Profiles**: Configuration profiles and session preferences saved in INI files (`j3270.ini`).
|
||||||
|
|
||||||
### 📦 Modular Zero-Dependency Core
|
### 📦 Modular Zero-Dependency Core
|
||||||
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only). Distributed as standalone JAR (`build/lib3270j.jar`) and published Maven coordinate `haus.nightmare.j3270:lib3270j:1.2.4`.
|
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only). Distributed as standalone JAR (`build/lib3270j.jar`) and published Maven coordinate `haus.nightmare.j3270:lib3270j:1.3.0`.
|
||||||
- **`j3270`**: Standalone desktop Swing terminal application.
|
- **`j3270`**: Standalone desktop Swing terminal application.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -180,6 +180,14 @@ All pushes and pull requests are verified across **Java 11, 17, and 21 LTS** run
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| ADMCHART | IMON | PUBVM Logon |
|
||||||
|
| :---: | :---: | :---: |
|
||||||
|
|  |  |  |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📜 Acknowledgements
|
## 📜 Acknowledgements
|
||||||
|
|
||||||
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
allprojects {
|
allprojects {
|
||||||
group = 'haus.nightmare.j3270'
|
group = 'haus.nightmare.j3270'
|
||||||
version = '1.2.4'
|
version = '1.3.0'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import java.util.logging.*;
|
|||||||
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
|
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(J3270App.class.getName());
|
private static final Logger log = Logger.getLogger(J3270App.class.getName());
|
||||||
|
private static final Logger PKG_LOG_ROOT = Logger.getLogger("haus.nightmare");
|
||||||
|
private static final Logger PKG_LOG_APP = Logger.getLogger("haus.nightmare.j3270");
|
||||||
|
private static final Logger PKG_LOG_LIB = Logger.getLogger("haus.nightmare.lib3270j");
|
||||||
|
|
||||||
// Active open windows tracking
|
// Active open windows tracking
|
||||||
private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>();
|
private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||||
@@ -1462,9 +1465,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
consoleHandler.setFilter(appFilter);
|
consoleHandler.setFilter(appFilter);
|
||||||
globalRoot.addHandler(consoleHandler);
|
globalRoot.addHandler(consoleHandler);
|
||||||
|
|
||||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
PKG_LOG_ROOT.setLevel(Level.ALL);
|
||||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
PKG_LOG_APP.setLevel(Level.ALL);
|
||||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
PKG_LOG_LIB.setLevel(Level.ALL);
|
||||||
|
|
||||||
try {
|
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) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public final class Version {
|
|||||||
/**
|
/**
|
||||||
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "1.2.4";
|
public static final String VERSION = "1.3.0";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonical application name.
|
* Canonical application name.
|
||||||
@@ -22,7 +22,7 @@ public final class Version {
|
|||||||
* Inspects the package implementation version first (populated when loaded from a JAR),
|
* Inspects the package implementation version first (populated when loaded from a JAR),
|
||||||
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
|
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
|
||||||
*
|
*
|
||||||
* @return the version string (e.g. "1.2.4")
|
* @return the version string (e.g. "1.3.0")
|
||||||
*/
|
*/
|
||||||
public static String getVersion() {
|
public static String getVersion() {
|
||||||
Package pkg = Version.class.getPackage();
|
Package pkg = Version.class.getPackage();
|
||||||
|
|||||||
@@ -29,33 +29,33 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
public class Phase27BuildAndRenderTest {
|
public class Phase27BuildAndRenderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Phase 27.1 & 27.2: Release version 1.2.4 synchronized across build files and android.useAndroidX removed")
|
@DisplayName("Phase 27.1 & 27.2: Release version 1.3.0 synchronized across build files and android.useAndroidX removed")
|
||||||
public void testVersionSynchronizationAndGradleProperties() throws Exception {
|
public void testVersionSynchronizationAndGradleProperties() throws Exception {
|
||||||
File projectRoot = new File(".").getCanonicalFile();
|
File projectRoot = new File(".").getCanonicalFile();
|
||||||
if (!new File(projectRoot, "settings.gradle").exists() && projectRoot.getParentFile() != null) {
|
if (!new File(projectRoot, "settings.gradle").exists() && projectRoot.getParentFile() != null) {
|
||||||
projectRoot = projectRoot.getParentFile();
|
projectRoot = projectRoot.getParentFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Verify build.gradle version = '1.2.4'
|
// 1. Verify build.gradle version = '1.3.0'
|
||||||
File buildGradle = new File(projectRoot, "build.gradle");
|
File buildGradle = new File(projectRoot, "build.gradle");
|
||||||
assertTrue(buildGradle.exists(), "build.gradle must exist");
|
assertTrue(buildGradle.exists(), "build.gradle must exist");
|
||||||
String buildGradleContent = Files.readString(buildGradle.toPath());
|
String buildGradleContent = Files.readString(buildGradle.toPath());
|
||||||
assertTrue(buildGradleContent.contains("version = '1.2.4'"), "build.gradle must define version = '1.2.4'");
|
assertTrue(buildGradleContent.contains("version = '1.3.0'"), "build.gradle must define version = '1.3.0'");
|
||||||
assertFalse(buildGradleContent.contains("0.1.0"), "build.gradle must not contain 0.1.0");
|
assertFalse(buildGradleContent.contains("0.1.0"), "build.gradle must not contain 0.1.0");
|
||||||
|
|
||||||
|
|
||||||
// 3. Verify setup.ps1 Implementation-Version: 1.2.4
|
// 3. Verify setup.ps1 Implementation-Version: 1.3.0
|
||||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||||
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
||||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||||
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must specify Implementation-Version: 1.2.4");
|
assertTrue(setupPs1Content.contains("Implementation-Version: 1.3.0"), "setup.ps1 must specify Implementation-Version: 1.3.0");
|
||||||
assertFalse(setupPs1Content.contains("Implementation-Version: 0.1.0"), "setup.ps1 must not contain 0.1.0");
|
assertFalse(setupPs1Content.contains("Implementation-Version: 0.1.0"), "setup.ps1 must not contain 0.1.0");
|
||||||
|
|
||||||
// 4. Verify setup.sh Implementation-Version: 1.2.4
|
// 4. Verify setup.sh Implementation-Version: 1.3.0
|
||||||
File setupSh = new File(projectRoot, "setup.sh");
|
File setupSh = new File(projectRoot, "setup.sh");
|
||||||
assertTrue(setupSh.exists(), "setup.sh must exist");
|
assertTrue(setupSh.exists(), "setup.sh must exist");
|
||||||
String setupShContent = Files.readString(setupSh.toPath());
|
String setupShContent = Files.readString(setupSh.toPath());
|
||||||
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must specify Implementation-Version: 1.2.4");
|
assertTrue(setupShContent.contains("Implementation-Version: 1.3.0"), "setup.sh must specify Implementation-Version: 1.3.0");
|
||||||
assertFalse(setupShContent.contains("Implementation-Version: 0.1.0"), "setup.sh must not contain 0.1.0");
|
assertFalse(setupShContent.contains("Implementation-Version: 0.1.0"), "setup.sh must not contain 0.1.0");
|
||||||
|
|
||||||
// 5. Verify gradle.properties android.useAndroidX removed
|
// 5. Verify gradle.properties android.useAndroidX removed
|
||||||
|
|||||||
@@ -20,28 +20,28 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
public class Phase31AboutAndInterfaceTest {
|
public class Phase31AboutAndInterfaceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Phase 31.1: Dedicated Version classes report v1.2.4 single source of truth")
|
@DisplayName("Phase 31.1: Dedicated Version classes report v1.3.0 single source of truth")
|
||||||
public void testVersionClasses() {
|
public void testVersionClasses() {
|
||||||
// lib3270j
|
// lib3270j
|
||||||
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.VERSION);
|
assertEquals("1.3.0", haus.nightmare.lib3270j.Version.VERSION);
|
||||||
assertEquals("lib3270j", haus.nightmare.lib3270j.Version.NAME);
|
assertEquals("lib3270j", haus.nightmare.lib3270j.Version.NAME);
|
||||||
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.getVersion());
|
assertEquals("1.3.0", haus.nightmare.lib3270j.Version.getVersion());
|
||||||
|
|
||||||
// j3270
|
// j3270
|
||||||
assertEquals("1.2.4", haus.nightmare.j3270.Version.VERSION);
|
assertEquals("1.3.0", haus.nightmare.j3270.Version.VERSION);
|
||||||
assertEquals("j3270", haus.nightmare.j3270.Version.NAME);
|
assertEquals("j3270", haus.nightmare.j3270.Version.NAME);
|
||||||
assertEquals("1.2.4", haus.nightmare.j3270.Version.getVersion());
|
assertEquals("1.3.0", haus.nightmare.j3270.Version.getVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Phase 31.2: Help > About dialogue text properly reports j3270 v1.2.4 and lib3270j v1.2.4")
|
@DisplayName("Phase 31.2: Help > About dialogue text properly reports j3270 v1.3.0 and lib3270j v1.3.0")
|
||||||
public void testAboutTextContent() {
|
public void testAboutTextContent() {
|
||||||
String aboutText = J3270App.getAboutText();
|
String aboutText = J3270App.getAboutText();
|
||||||
assertNotNull(aboutText, "About text must not be null");
|
assertNotNull(aboutText, "About text must not be null");
|
||||||
|
|
||||||
// Verify version reports
|
// Verify version reports
|
||||||
assertTrue(aboutText.contains("j3270 v1.2.4"), "About text must report j3270 v1.2.4");
|
assertTrue(aboutText.contains("j3270 v1.3.0"), "About text must report j3270 v1.3.0");
|
||||||
assertTrue(aboutText.contains("lib3270j v1.2.4"), "About text must report lib3270j v1.2.4");
|
assertTrue(aboutText.contains("lib3270j v1.3.0"), "About text must report lib3270j v1.3.0");
|
||||||
|
|
||||||
// Verify stale pre-release versions are eradicated
|
// Verify stale pre-release versions are eradicated
|
||||||
assertFalse(aboutText.contains("0.2.0"), "About text must not contain stale version 0.2.0");
|
assertFalse(aboutText.contains("0.2.0"), "About text must not contain stale version 0.2.0");
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ public class ConnectionConfig {
|
|||||||
private int reconnectMaxRetries = 5;
|
private int reconnectMaxRetries = 5;
|
||||||
private int tcpUserTimeoutMs = 0;
|
private int tcpUserTimeoutMs = 0;
|
||||||
|
|
||||||
|
// Framing buffer sanity limits (RFC 885 / RFC 2355 EOR protection)
|
||||||
|
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 131072; // 128KB (2x max 64KB structured field; covers multi-SF GDDM + PS loads)
|
||||||
|
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||||
|
|
||||||
// Host operating system type (TSO, CMS, CICS)
|
// Host operating system type (TSO, CMS, CICS)
|
||||||
private String hostType = "TSO";
|
private String hostType = "TSO";
|
||||||
private boolean savedConnection = false;
|
private boolean savedConnection = false;
|
||||||
@@ -549,6 +553,11 @@ public class ConnectionConfig {
|
|||||||
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
|
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
|
||||||
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
|
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
|
||||||
|
|
||||||
|
public int getMaxFramingBufferSize() { return maxFramingBufferSize; }
|
||||||
|
public void setMaxFramingBufferSize(int maxFramingBufferSize) {
|
||||||
|
this.maxFramingBufferSize = maxFramingBufferSize > 0 ? maxFramingBufferSize : DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
|
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
|
||||||
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
|
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
|
||||||
pcfg.setUseTls(useTls);
|
pcfg.setUseTls(useTls);
|
||||||
@@ -559,6 +568,7 @@ public class ConnectionConfig {
|
|||||||
pcfg.setPrinterLuName(associatedPrinterLu);
|
pcfg.setPrinterLuName(associatedPrinterLu);
|
||||||
pcfg.setAssociatedDisplayLuName(luName);
|
pcfg.setAssociatedDisplayLuName(luName);
|
||||||
pcfg.setCodePage(codePage);
|
pcfg.setCodePage(codePage);
|
||||||
|
pcfg.setMaxFramingBufferSize(maxFramingBufferSize);
|
||||||
return pcfg;
|
return pcfg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public final class Version {
|
|||||||
/**
|
/**
|
||||||
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "1.2.4";
|
public static final String VERSION = "1.3.0";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonical library name.
|
* Canonical library name.
|
||||||
@@ -22,7 +22,7 @@ public final class Version {
|
|||||||
* Inspects the package implementation version first (populated when loaded from a JAR),
|
* Inspects the package implementation version first (populated when loaded from a JAR),
|
||||||
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
|
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
|
||||||
*
|
*
|
||||||
* @return the version string (e.g. "1.2.4")
|
* @return the version string (e.g. "1.3.0")
|
||||||
*/
|
*/
|
||||||
public static String getVersion() {
|
public static String getVersion() {
|
||||||
Package pkg = Version.class.getPackage();
|
Package pkg = Version.class.getPackage();
|
||||||
|
|||||||
+45
-17
@@ -41,10 +41,12 @@ public class DataStreamProcessor {
|
|||||||
private haus.nightmare.lib3270j.input.InputProcessor inputProcessor;
|
private haus.nightmare.lib3270j.input.InputProcessor inputProcessor;
|
||||||
|
|
||||||
// Graphics & Programmed Symbols
|
// Graphics & Programmed Symbols
|
||||||
|
private static final int MAX_GOCA_ACCUMULATOR_SIZE = 262144; // 256KB cap (4x advertised 63KB QR_SEGMENT limit)
|
||||||
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
||||||
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
|
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
|
||||||
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
||||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
private final FastByteBuffer gocaAccumulator = new FastByteBuffer(8192, MAX_GOCA_ACCUMULATOR_SIZE);
|
||||||
|
private boolean gocaDiscardMode = false;
|
||||||
private int currentGocaSubtype = 0;
|
private int currentGocaSubtype = 0;
|
||||||
|
|
||||||
// Phase 10: Auto-Unlock & Contention Resolution State
|
// Phase 10: Auto-Unlock & Contention Resolution State
|
||||||
@@ -1539,32 +1541,58 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
if (flags == 0x80) { // SPAN_FIRST
|
if (flags == 0x80) { // SPAN_FIRST
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
|
gocaDiscardMode = false;
|
||||||
currentGocaSubtype = sfSubId;
|
currentGocaSubtype = sfSubId;
|
||||||
if (orderLen > 0) {
|
if (orderLen > 0) {
|
||||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
try {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
} catch (FramingBufferOverflowException e) {
|
||||||
|
log.warning("GOCA accumulator exceeded " + MAX_GOCA_ACCUMULATOR_SIZE + " bytes on SPAN_FIRST; discarding spanned object");
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
gocaDiscardMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (flags == 0x00) { // SPAN_MIDDLE
|
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||||
if (orderLen > 0) {
|
if (orderLen > 0 && !gocaDiscardMode) {
|
||||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
try {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
} catch (FramingBufferOverflowException e) {
|
||||||
|
log.warning("GOCA accumulator exceeded " + MAX_GOCA_ACCUMULATOR_SIZE + " bytes on SPAN_MIDDLE; discarding spanned object");
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
gocaDiscardMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (flags == 0x40) { // SPAN_LAST
|
} else if (flags == 0x40) { // SPAN_LAST
|
||||||
if (orderLen > 0) {
|
if (!gocaDiscardMode) {
|
||||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
if (orderLen > 0) {
|
||||||
|
try {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
} catch (FramingBufferOverflowException e) {
|
||||||
|
log.warning("GOCA accumulator exceeded " + MAX_GOCA_ACCUMULATOR_SIZE + " bytes on SPAN_LAST; discarding spanned object");
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
gocaDiscardMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!gocaDiscardMode) {
|
||||||
|
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
||||||
|
try {
|
||||||
|
java.nio.file.Files.write(java.nio.file.Paths.get("/Users/rudi/Projects/j3270/captured_goca.bin"), fullStream);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||||
|
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||||
|
} else {
|
||||||
|
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||||
|
}
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
gocaDiscardMode = false;
|
||||||
try {
|
|
||||||
java.nio.file.Files.write(java.nio.file.Paths.get("/Users/rudi/Projects/j3270/captured_goca.bin"), fullStream);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
|
||||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
|
||||||
} else {
|
|
||||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
|
||||||
}
|
|
||||||
notifyScreenUpdated();
|
|
||||||
} else { // SPAN_ONLY (0xC0)
|
} else { // SPAN_ONLY (0xC0)
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
|
gocaDiscardMode = false;
|
||||||
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
||||||
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||||
|
|||||||
@@ -17,21 +17,49 @@ import java.util.Arrays;
|
|||||||
*/
|
*/
|
||||||
public class FastByteBuffer extends OutputStream {
|
public class FastByteBuffer extends OutputStream {
|
||||||
|
|
||||||
|
public static final int DEFAULT_INITIAL_CAPACITY = 32768;
|
||||||
|
public static final int DEFAULT_MAX_CAPACITY = 131072; // 128KB (2x max 64KB structured field; covers multi-SF GDDM + PS loads)
|
||||||
|
|
||||||
|
private final int initialCapacity;
|
||||||
|
private int maxCapacity;
|
||||||
private byte[] buf;
|
private byte[] buf;
|
||||||
private int count;
|
private int count;
|
||||||
|
|
||||||
public FastByteBuffer() {
|
public FastByteBuffer() {
|
||||||
this(32768);
|
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_CAPACITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FastByteBuffer(int initialCapacity) {
|
public FastByteBuffer(int initialCapacity) {
|
||||||
this.buf = new byte[Math.max(32, initialCapacity)];
|
this(initialCapacity, Math.max(initialCapacity, DEFAULT_MAX_CAPACITY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public FastByteBuffer(int initialCapacity, int maxCapacity) {
|
||||||
|
this.initialCapacity = Math.max(32, initialCapacity);
|
||||||
|
this.maxCapacity = Math.max(this.initialCapacity, maxCapacity);
|
||||||
|
this.buf = new byte[this.initialCapacity];
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxCapacity() {
|
||||||
|
return maxCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxCapacity(int maxCapacity) {
|
||||||
|
this.maxCapacity = Math.max(this.initialCapacity, maxCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInitialCapacity() {
|
||||||
|
return initialCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureCapacity(int minCapacity) {
|
private void ensureCapacity(int minCapacity) {
|
||||||
|
if (minCapacity > maxCapacity) {
|
||||||
|
throw new FramingBufferOverflowException(
|
||||||
|
"FastByteBuffer exceeded maximum capacity of " + maxCapacity + " bytes (requested: " + minCapacity + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
if (minCapacity > buf.length) {
|
if (minCapacity > buf.length) {
|
||||||
int newCap = Math.max(buf.length << 1, minCapacity);
|
int newCap = Math.min(maxCapacity, Math.max(buf.length << 1, minCapacity));
|
||||||
buf = Arrays.copyOf(buf, newCap);
|
buf = Arrays.copyOf(buf, newCap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +92,9 @@ public class FastByteBuffer extends OutputStream {
|
|||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
count = 0;
|
count = 0;
|
||||||
|
if (buf.length > maxCapacity) {
|
||||||
|
buf = new byte[maxCapacity];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when an inbound framing buffer exceeds its maximum configured capacity
|
||||||
|
* (typically 64KB) without encountering an end-of-record delimiter (IAC EOR).
|
||||||
|
*/
|
||||||
|
public class FramingBufferOverflowException extends RuntimeException {
|
||||||
|
|
||||||
|
public FramingBufferOverflowException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FramingBufferOverflowException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ public class FTDft {
|
|||||||
private int pendingByte = -1;
|
private int pendingByte = -1;
|
||||||
|
|
||||||
// Record formatting buffers and SO/SI tracking
|
// Record formatting buffers and SO/SI tracking
|
||||||
|
private static final int MAX_DFT_RECORD_BUFFER_SIZE = 131072; // 128KB sanity cap
|
||||||
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
|
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
|
||||||
private boolean dbcsMode = false;
|
private boolean dbcsMode = false;
|
||||||
private boolean hasPendingByteInDBCS = false;
|
private boolean hasPendingByteInDBCS = false;
|
||||||
@@ -549,9 +550,17 @@ public class FTDft {
|
|||||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||||
|
|
||||||
if (isRecfmV) {
|
if (isRecfmV) {
|
||||||
|
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
|
||||||
|
}
|
||||||
downloadRecordBuffer.write(data, offset, length);
|
downloadRecordBuffer.write(data, offset, length);
|
||||||
processVariableDownloadRecords(config);
|
processVariableDownloadRecords(config);
|
||||||
} else if (isRecfmF && lrecl > 0) {
|
} else if (isRecfmF && lrecl > 0) {
|
||||||
|
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
|
||||||
|
downloadRecordBuffer.reset();
|
||||||
|
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
|
||||||
|
}
|
||||||
downloadRecordBuffer.write(data, offset, length);
|
downloadRecordBuffer.write(data, offset, length);
|
||||||
processFixedDownloadRecords(lrecl, config);
|
processFixedDownloadRecords(lrecl, config);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public class NvtProcessor {
|
|||||||
private static final int STATE_OSC = 4;
|
private static final int STATE_OSC = 4;
|
||||||
private static final int STATE_OSC_ESC = 5;
|
private static final int STATE_OSC_ESC = 5;
|
||||||
|
|
||||||
|
private static final int MAX_ESC_BUFFER_SIZE = 4096;
|
||||||
private int parseState = STATE_NORMAL;
|
private int parseState = STATE_NORMAL;
|
||||||
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
||||||
|
|
||||||
@@ -470,32 +471,51 @@ public class NvtProcessor {
|
|||||||
} else if (parseState == STATE_OSC) {
|
} else if (parseState == STATE_OSC) {
|
||||||
if (b == 0x07) { // BEL terminator
|
if (b == 0x07) { // BEL terminator
|
||||||
handleOSC(escBuffer.toByteArray());
|
handleOSC(escBuffer.toByteArray());
|
||||||
|
escBuffer.reset();
|
||||||
parseState = STATE_NORMAL;
|
parseState = STATE_NORMAL;
|
||||||
} else if (b == 0x1B) { // Possible ESC \ (ST)
|
} else if (b == 0x1B) { // Possible ESC \ (ST)
|
||||||
parseState = STATE_OSC_ESC;
|
parseState = STATE_OSC_ESC;
|
||||||
} else if (b == 0x9C) { // 8-bit ST
|
} else if (b == 0x9C) { // 8-bit ST
|
||||||
handleOSC(escBuffer.toByteArray());
|
handleOSC(escBuffer.toByteArray());
|
||||||
|
escBuffer.reset();
|
||||||
parseState = STATE_NORMAL;
|
parseState = STATE_NORMAL;
|
||||||
} else {
|
} else {
|
||||||
escBuffer.write(b);
|
if (escBuffer.size() < MAX_ESC_BUFFER_SIZE) {
|
||||||
|
escBuffer.write(b);
|
||||||
|
} else {
|
||||||
|
escBuffer.reset();
|
||||||
|
parseState = STATE_NORMAL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (parseState == STATE_OSC_ESC) {
|
} else if (parseState == STATE_OSC_ESC) {
|
||||||
if (b == '\\') { // ESC \ (ST) terminator
|
if (b == '\\') { // ESC \ (ST) terminator
|
||||||
handleOSC(escBuffer.toByteArray());
|
handleOSC(escBuffer.toByteArray());
|
||||||
|
escBuffer.reset();
|
||||||
parseState = STATE_NORMAL;
|
parseState = STATE_NORMAL;
|
||||||
} else {
|
} else {
|
||||||
// False alarm on ST, put ESC and byte into OSC buffer
|
// False alarm on ST, put ESC and byte into OSC buffer
|
||||||
escBuffer.write(0x1B);
|
if (escBuffer.size() + 2 <= MAX_ESC_BUFFER_SIZE) {
|
||||||
escBuffer.write(b);
|
escBuffer.write(0x1B);
|
||||||
parseState = STATE_OSC;
|
escBuffer.write(b);
|
||||||
|
parseState = STATE_OSC;
|
||||||
|
} else {
|
||||||
|
escBuffer.reset();
|
||||||
|
parseState = STATE_NORMAL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (parseState == STATE_CSI) {
|
} else if (parseState == STATE_CSI) {
|
||||||
escBuffer.write(b);
|
if (escBuffer.size() < MAX_ESC_BUFFER_SIZE) {
|
||||||
|
escBuffer.write(b);
|
||||||
|
}
|
||||||
// CSI final bytes are in the range 0x40..0x7E
|
// CSI final bytes are in the range 0x40..0x7E
|
||||||
if (b >= 0x40 && b <= 0x7E) {
|
if (b >= 0x40 && b <= 0x7E) {
|
||||||
byte[] seq = escBuffer.toByteArray();
|
byte[] seq = escBuffer.toByteArray();
|
||||||
|
escBuffer.reset();
|
||||||
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||||
parseState = STATE_NORMAL;
|
parseState = STATE_NORMAL;
|
||||||
|
} else if (escBuffer.size() >= MAX_ESC_BUFFER_SIZE) {
|
||||||
|
escBuffer.reset();
|
||||||
|
parseState = STATE_NORMAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public class PD3270 {
|
|||||||
private String destination;
|
private String destination;
|
||||||
private boolean open = false;
|
private boolean open = false;
|
||||||
|
|
||||||
|
/** Maximum size of in-memory print capture buffer (1 MB) to prevent unbounded heap growth. */
|
||||||
|
public static final int MAX_MEMORY_CAPTURE_SIZE = 1048576;
|
||||||
|
|
||||||
private OutputStream targetOutputStream;
|
private OutputStream targetOutputStream;
|
||||||
private Writer targetWriter;
|
private Writer targetWriter;
|
||||||
private Process pipeProcess;
|
private Process pipeProcess;
|
||||||
@@ -33,6 +36,13 @@ public class PD3270 {
|
|||||||
private int pageCount = 0;
|
private int pageCount = 0;
|
||||||
private Charset outputCharset = StandardCharsets.UTF_8;
|
private Charset outputCharset = StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
private void appendToMemoryStream(byte[] data, int offset, int length) {
|
||||||
|
int remaining = MAX_MEMORY_CAPTURE_SIZE - memoryStream.size();
|
||||||
|
if (remaining > 0) {
|
||||||
|
memoryStream.write(data, offset, Math.min(length, remaining));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public PD3270() {
|
public PD3270() {
|
||||||
this(new PrinterConfig());
|
this(new PrinterConfig());
|
||||||
}
|
}
|
||||||
@@ -102,7 +112,7 @@ public class PD3270 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
memoryStream.write(data, offset, length);
|
appendToMemoryStream(data, offset, length);
|
||||||
byteCount += length;
|
byteCount += length;
|
||||||
|
|
||||||
if (targetOutputStream != null) {
|
if (targetOutputStream != null) {
|
||||||
@@ -129,7 +139,7 @@ public class PD3270 {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
byte[] b = String.valueOf(c).getBytes(outputCharset);
|
byte[] b = String.valueOf(c).getBytes(outputCharset);
|
||||||
memoryStream.write(b);
|
appendToMemoryStream(b, 0, b.length);
|
||||||
byteCount += b.length;
|
byteCount += b.length;
|
||||||
|
|
||||||
if (targetWriter != null) {
|
if (targetWriter != null) {
|
||||||
@@ -158,7 +168,7 @@ public class PD3270 {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
byte[] b = s.getBytes(outputCharset);
|
byte[] b = s.getBytes(outputCharset);
|
||||||
memoryStream.write(b);
|
appendToMemoryStream(b, 0, b.length);
|
||||||
byteCount += b.length;
|
byteCount += b.length;
|
||||||
|
|
||||||
if (targetWriter != null) {
|
if (targetWriter != null) {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ public class PrinterConfig {
|
|||||||
private long autoFlushTimeoutMs = 2000;
|
private long autoFlushTimeoutMs = 2000;
|
||||||
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
|
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
|
||||||
private char graphicErrorReplacementChar = '?';
|
private char graphicErrorReplacementChar = '?';
|
||||||
|
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 131072; // 128KB
|
||||||
|
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||||
|
|
||||||
public PrinterConfig() {
|
public PrinterConfig() {
|
||||||
initDefaultChannelTable();
|
initDefaultChannelTable();
|
||||||
@@ -227,4 +229,9 @@ public class PrinterConfig {
|
|||||||
public int[] getChannelTable() {
|
public int[] getChannelTable() {
|
||||||
return channelTable.clone();
|
return channelTable.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxFramingBufferSize() { return maxFramingBufferSize; }
|
||||||
|
public void setMaxFramingBufferSize(int maxFramingBufferSize) {
|
||||||
|
this.maxFramingBufferSize = maxFramingBufferSize > 0 ? maxFramingBufferSize : DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,12 +58,19 @@ public class Telnet3270EP implements Runnable {
|
|||||||
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
|
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
|
||||||
|
|
||||||
// Buffer for Telnet streaming
|
// Buffer for Telnet streaming
|
||||||
private final ByteArrayOutputStream recordBuffer = new ByteArrayOutputStream();
|
private static final int MAX_PRINTER_SUBNEG_SIZE = 8192;
|
||||||
|
private final int maxFramingBufferSize;
|
||||||
|
private boolean recordDiscardMode = false;
|
||||||
|
private boolean subnegDiscardMode = false;
|
||||||
|
private final haus.nightmare.lib3270j.datastream.FastByteBuffer recordBuffer;
|
||||||
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
|
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
|
||||||
private boolean inSubnegotiation = false;
|
private boolean inSubnegotiation = false;
|
||||||
|
|
||||||
public Telnet3270EP(PrinterConfig config) {
|
public Telnet3270EP(PrinterConfig config) {
|
||||||
this.config = config != null ? config : new PrinterConfig();
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.maxFramingBufferSize = this.config.getMaxFramingBufferSize();
|
||||||
|
this.recordBuffer = new haus.nightmare.lib3270j.datastream.FastByteBuffer(
|
||||||
|
Math.min(32768, this.maxFramingBufferSize), this.maxFramingBufferSize);
|
||||||
this.pd = new PD3270(this.config);
|
this.pd = new PD3270(this.config);
|
||||||
this.translator = new EbcdicTranslator(this.config.getCodePage());
|
this.translator = new EbcdicTranslator(this.config.getCodePage());
|
||||||
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||||
@@ -73,6 +80,9 @@ public class Telnet3270EP implements Runnable {
|
|||||||
|
|
||||||
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
|
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
|
||||||
this.config = config != null ? config : new PrinterConfig();
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.maxFramingBufferSize = this.config.getMaxFramingBufferSize();
|
||||||
|
this.recordBuffer = new haus.nightmare.lib3270j.datastream.FastByteBuffer(
|
||||||
|
Math.min(32768, this.maxFramingBufferSize), this.maxFramingBufferSize);
|
||||||
this.pd = pd != null ? pd : new PD3270(this.config);
|
this.pd = pd != null ? pd : new PD3270(this.config);
|
||||||
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
||||||
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||||
@@ -191,6 +201,7 @@ public class Telnet3270EP implements Runnable {
|
|||||||
this.recordBuffer.reset();
|
this.recordBuffer.reset();
|
||||||
this.subnegBuffer.reset();
|
this.subnegBuffer.reset();
|
||||||
this.inSubnegotiation = false;
|
this.inSubnegotiation = false;
|
||||||
|
this.recordDiscardMode = false;
|
||||||
|
|
||||||
scs.resetSCSFormatDefaults();
|
scs.resetSCSFormatDefaults();
|
||||||
printPs.erasePrintBuffer();
|
printPs.erasePrintBuffer();
|
||||||
@@ -376,14 +387,31 @@ public class Telnet3270EP implements Runnable {
|
|||||||
if (idx < end && (data[idx] & 0xFF) == TelnetConstants.SE) {
|
if (idx < end && (data[idx] & 0xFF) == TelnetConstants.SE) {
|
||||||
idx++;
|
idx++;
|
||||||
inSubnegotiation = false;
|
inSubnegotiation = false;
|
||||||
processSubnegotiation(subnegBuffer.toByteArray());
|
if (!subnegDiscardMode) {
|
||||||
|
processSubnegotiation(subnegBuffer.toByteArray());
|
||||||
|
}
|
||||||
subnegBuffer.reset();
|
subnegBuffer.reset();
|
||||||
|
subnegDiscardMode = false;
|
||||||
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
|
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
|
||||||
subnegBuffer.write(TelnetConstants.IAC);
|
if (!subnegDiscardMode) {
|
||||||
|
if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
|
||||||
|
subnegBuffer.write(TelnetConstants.IAC);
|
||||||
|
} else {
|
||||||
|
log.warning("Printer subnegotiation exceeded " + MAX_PRINTER_SUBNEG_SIZE + " bytes; discarding until IAC SE");
|
||||||
|
subnegBuffer.reset();
|
||||||
|
subnegDiscardMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!subnegDiscardMode) {
|
||||||
subnegBuffer.write(b);
|
if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
|
||||||
|
subnegBuffer.write(b);
|
||||||
|
} else {
|
||||||
|
log.warning("Printer subnegotiation exceeded " + MAX_PRINTER_SUBNEG_SIZE + " bytes; discarding until IAC SE");
|
||||||
|
subnegBuffer.reset();
|
||||||
|
subnegDiscardMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -394,15 +422,19 @@ public class Telnet3270EP implements Runnable {
|
|||||||
|
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case TelnetConstants.IAC:
|
case TelnetConstants.IAC:
|
||||||
recordBuffer.write(TelnetConstants.IAC);
|
appendPrinterByte(TelnetConstants.IAC);
|
||||||
break;
|
break;
|
||||||
case TelnetConstants.SB:
|
case TelnetConstants.SB:
|
||||||
inSubnegotiation = true;
|
inSubnegotiation = true;
|
||||||
|
subnegDiscardMode = false;
|
||||||
subnegBuffer.reset();
|
subnegBuffer.reset();
|
||||||
break;
|
break;
|
||||||
case TelnetConstants.EOR:
|
case TelnetConstants.EOR:
|
||||||
processCompleteRecord(recordBuffer.toByteArray());
|
if (!recordDiscardMode) {
|
||||||
|
processCompleteRecord(recordBuffer.toByteArray());
|
||||||
|
}
|
||||||
recordBuffer.reset();
|
recordBuffer.reset();
|
||||||
|
recordDiscardMode = false;
|
||||||
break;
|
break;
|
||||||
case TelnetConstants.DO:
|
case TelnetConstants.DO:
|
||||||
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
|
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
|
||||||
@@ -420,11 +452,33 @@ public class Telnet3270EP implements Runnable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recordBuffer.write(b);
|
appendPrinterByte(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendPrinterByte(int b) {
|
||||||
|
if (recordDiscardMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordBuffer.size() >= maxFramingBufferSize) {
|
||||||
|
log.warning("Printer record buffer exceeded maximum capacity (" + maxFramingBufferSize +
|
||||||
|
" bytes) without IAC EOR; discarding until next EOR");
|
||||||
|
recordDiscardMode = true;
|
||||||
|
recordBuffer.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recordBuffer.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRecordDiscardMode() {
|
||||||
|
return recordDiscardMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxFramingBufferSize() {
|
||||||
|
return maxFramingBufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
private void handleTelnetDo(int option) {
|
private void handleTelnetDo(int option) {
|
||||||
try {
|
try {
|
||||||
if (option == TelnetConstants.TELOPT_TN3270E) {
|
if (option == TelnetConstants.TELOPT_TN3270E) {
|
||||||
|
|||||||
@@ -46,8 +46,15 @@ public class TelnetFSM {
|
|||||||
private final boolean[] myOpts = new boolean[256]; // options we have enabled
|
private final boolean[] myOpts = new boolean[256]; // options we have enabled
|
||||||
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
|
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
|
||||||
|
|
||||||
// 3270 input buffer (accumulated between telnet framing)
|
// 3270 input buffer (accumulated between telnet framing, bounded at maxFramingBufferSize)
|
||||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
|
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf;
|
||||||
|
|
||||||
|
// Discard mode if framing buffer overflows prior to receiving IAC EOR
|
||||||
|
private boolean recordDiscardMode = false;
|
||||||
|
|
||||||
|
// Sanity cap for Telnet subnegotiation parameters (RFC 1091, RFC 1572, RFC 2355)
|
||||||
|
private static final int MAX_SUBNEG_BUFFER_SIZE = 8192;
|
||||||
|
private boolean sbDiscardMode = false;
|
||||||
|
|
||||||
// Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C)
|
// Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C)
|
||||||
private final byte[] nvtSingleByteScratch = new byte[1];
|
private final byte[] nvtSingleByteScratch = new byte[1];
|
||||||
@@ -165,6 +172,8 @@ public class TelnetFSM {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.screenBuffer = screenBuffer;
|
this.screenBuffer = screenBuffer;
|
||||||
this.dsProcessor = dsProcessor;
|
this.dsProcessor = dsProcessor;
|
||||||
|
int maxFraming = (config != null) ? config.getMaxFramingBufferSize() : ConnectionConfig.DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||||
|
this.ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(Math.min(32768, maxFraming), maxFraming);
|
||||||
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||||
this.nvtProcessor.setOutputSender(this::sendNvtData);
|
this.nvtProcessor.setOutputSender(this::sendNvtData);
|
||||||
}
|
}
|
||||||
@@ -220,6 +229,8 @@ public class TelnetFSM {
|
|||||||
deferredWillTtype = false;
|
deferredWillTtype = false;
|
||||||
ttypeIndex = 0;
|
ttypeIndex = 0;
|
||||||
luIndex = 0;
|
luIndex = 0;
|
||||||
|
recordDiscardMode = false;
|
||||||
|
sbDiscardMode = false;
|
||||||
ibuf.reset();
|
ibuf.reset();
|
||||||
sbbuf.reset();
|
sbbuf.reset();
|
||||||
|
|
||||||
@@ -256,7 +267,7 @@ public class TelnetFSM {
|
|||||||
if (isPlainNvt) {
|
if (isPlainNvt) {
|
||||||
nvtProcessor.processNVTData(buf, start, i - start);
|
nvtProcessor.processNVTData(buf, start, i - start);
|
||||||
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
||||||
ibuf.write(buf, start, i - start);
|
appendInboundData(buf, start, i - start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (i < end) {
|
if (i < end) {
|
||||||
@@ -334,24 +345,80 @@ public class TelnetFSM {
|
|||||||
|
|
||||||
// Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending)
|
// Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending)
|
||||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
||||||
ibuf.write(c);
|
appendInboundByte(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendInboundByte(int c) {
|
||||||
|
if (recordDiscardMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ibuf.size() >= ibuf.getMaxCapacity()) {
|
||||||
|
handleFramingOverflow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ibuf.write(c);
|
||||||
|
} catch (haus.nightmare.lib3270j.datastream.FramingBufferOverflowException e) {
|
||||||
|
handleFramingOverflow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendInboundData(byte[] buf, int offset, int len) {
|
||||||
|
if (recordDiscardMode || buf == null || len <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ibuf.size() + len > ibuf.getMaxCapacity()) {
|
||||||
|
handleFramingOverflow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ibuf.write(buf, offset, len);
|
||||||
|
} catch (haus.nightmare.lib3270j.datastream.FramingBufferOverflowException e) {
|
||||||
|
handleFramingOverflow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFramingOverflow() {
|
||||||
|
log.warning("Inbound 3270 framing buffer exceeded maximum capacity (" + ibuf.getMaxCapacity() +
|
||||||
|
" bytes) without IAC EOR; discarding record until next EOR");
|
||||||
|
recordDiscardMode = true;
|
||||||
|
ibuf.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRecordDiscardMode() {
|
||||||
|
return recordDiscardMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxFramingBufferSize() {
|
||||||
|
return ibuf.getMaxCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentFramingBufferSize() {
|
||||||
|
return ibuf.size();
|
||||||
|
}
|
||||||
|
|
||||||
// ========== TNS_IAC ==========
|
// ========== TNS_IAC ==========
|
||||||
|
|
||||||
private void processIAC(int c) {
|
private void processIAC(int c) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case IAC: // Escaped IAC — literal 0xFF
|
case IAC: // Escaped IAC — literal 0xFF
|
||||||
ibuf.write(c);
|
if ((connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
|
||||||
|
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR])) {
|
||||||
|
nvtSingleByteScratch[0] = (byte) c;
|
||||||
|
nvtProcessor.processNVTData(nvtSingleByteScratch, 0, 1);
|
||||||
|
} else {
|
||||||
|
appendInboundByte(c);
|
||||||
|
}
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
break;
|
break;
|
||||||
case EOR: // End of record — process accumulated 3270 data
|
case EOR: // End of record — process accumulated 3270 data
|
||||||
log.fine("RCVD EOR");
|
log.fine("RCVD EOR");
|
||||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
if (!recordDiscardMode && (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING)) {
|
||||||
processEndOfRecord();
|
processEndOfRecord();
|
||||||
}
|
}
|
||||||
ibuf.reset();
|
ibuf.reset();
|
||||||
|
recordDiscardMode = false;
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
break;
|
break;
|
||||||
case WILL: state = TNS_WILL; break;
|
case WILL: state = TNS_WILL; break;
|
||||||
@@ -360,6 +427,7 @@ public class TelnetFSM {
|
|||||||
case DONT: state = TNS_DONT; break;
|
case DONT: state = TNS_DONT; break;
|
||||||
case SB:
|
case SB:
|
||||||
sbbuf.reset();
|
sbbuf.reset();
|
||||||
|
sbDiscardMode = false;
|
||||||
state = TNS_SB;
|
state = TNS_SB;
|
||||||
break;
|
break;
|
||||||
case GA:
|
case GA:
|
||||||
@@ -526,8 +594,15 @@ public class TelnetFSM {
|
|||||||
private void processSB(int c) {
|
private void processSB(int c) {
|
||||||
if (c == IAC) {
|
if (c == IAC) {
|
||||||
state = TNS_SB_IAC;
|
state = TNS_SB_IAC;
|
||||||
} else {
|
} else if (!sbDiscardMode) {
|
||||||
sbbuf.write(c);
|
if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
|
||||||
|
sbbuf.write(c);
|
||||||
|
} else {
|
||||||
|
log.warning("Telnet subnegotiation exceeded " + MAX_SUBNEG_BUFFER_SIZE +
|
||||||
|
" bytes without IAC SE; discarding until IAC SE");
|
||||||
|
sbbuf.reset();
|
||||||
|
sbDiscardMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,19 +611,34 @@ public class TelnetFSM {
|
|||||||
private void processSBIAC(int c) {
|
private void processSBIAC(int c) {
|
||||||
if (c == SE) {
|
if (c == SE) {
|
||||||
// Sub-negotiation complete
|
// Sub-negotiation complete
|
||||||
byte[] sbData = sbbuf.toByteArray();
|
if (!sbDiscardMode) {
|
||||||
sbbuf.reset();
|
byte[] sbData = sbbuf.toByteArray();
|
||||||
processSubNegotiation(sbData);
|
sbbuf.reset();
|
||||||
|
processSubNegotiation(sbData);
|
||||||
|
} else {
|
||||||
|
sbbuf.reset();
|
||||||
|
sbDiscardMode = false;
|
||||||
|
}
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
} else if (c == IAC) {
|
} else if (c == IAC) {
|
||||||
// Escaped IAC within sub-negotiation
|
// Escaped IAC within sub-negotiation
|
||||||
sbbuf.write(IAC);
|
if (!sbDiscardMode) {
|
||||||
|
if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
|
||||||
|
sbbuf.write(IAC);
|
||||||
|
} else {
|
||||||
|
log.warning("Telnet subnegotiation exceeded " + MAX_SUBNEG_BUFFER_SIZE +
|
||||||
|
" bytes without IAC SE; discarding until IAC SE");
|
||||||
|
sbbuf.reset();
|
||||||
|
sbDiscardMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
state = TNS_SB;
|
state = TNS_SB;
|
||||||
} else {
|
} else {
|
||||||
// Shouldn't happen, but recover
|
// Unexpected command after IAC inside SB (e.g. missing SE): abort SB and process command
|
||||||
log.warning("Unexpected byte " + c + " after IAC in SB");
|
log.warning("Unexpected byte " + c + " after IAC in SB");
|
||||||
sbbuf.reset();
|
sbbuf.reset();
|
||||||
state = TNS_DATA;
|
sbDiscardMode = false;
|
||||||
|
processIAC(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1634,6 +1724,7 @@ public class TelnetFSM {
|
|||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
java.util.Arrays.fill(myOpts, false);
|
java.util.Arrays.fill(myOpts, false);
|
||||||
java.util.Arrays.fill(hisOpts, false);
|
java.util.Arrays.fill(hisOpts, false);
|
||||||
|
recordDiscardMode = false;
|
||||||
ibuf.reset();
|
ibuf.reset();
|
||||||
sbbuf.reset();
|
sbbuf.reset();
|
||||||
tn3270eNegotiated = false;
|
tn3270eNegotiated = false;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class Phase30LibraryDistributionTest {
|
|||||||
String setupShContent = Files.readString(setupSh.toPath());
|
String setupShContent = Files.readString(setupSh.toPath());
|
||||||
assertTrue(setupShContent.contains("lib3270j.jar"), "setup.sh must package lib3270j.jar");
|
assertTrue(setupShContent.contains("lib3270j.jar"), "setup.sh must package lib3270j.jar");
|
||||||
assertTrue(setupShContent.contains("Implementation-Title: lib3270j"), "setup.sh must set Implementation-Title: lib3270j");
|
assertTrue(setupShContent.contains("Implementation-Title: lib3270j"), "setup.sh must set Implementation-Title: lib3270j");
|
||||||
assertTrue(setupShContent.contains("Implementation-Version: 1.2.4"), "setup.sh must set Implementation-Version: 1.2.4");
|
assertTrue(setupShContent.contains("Implementation-Version: 1.3.0"), "setup.sh must set Implementation-Version: 1.3.0");
|
||||||
|
|
||||||
// 3. setup.ps1
|
// 3. setup.ps1
|
||||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||||
@@ -74,7 +74,7 @@ public class Phase30LibraryDistributionTest {
|
|||||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||||
assertTrue(setupPs1Content.contains("lib3270j.jar"), "setup.ps1 must package lib3270j.jar");
|
assertTrue(setupPs1Content.contains("lib3270j.jar"), "setup.ps1 must package lib3270j.jar");
|
||||||
assertTrue(setupPs1Content.contains("Implementation-Title: lib3270j"), "setup.ps1 must set Implementation-Title: lib3270j");
|
assertTrue(setupPs1Content.contains("Implementation-Title: lib3270j"), "setup.ps1 must set Implementation-Title: lib3270j");
|
||||||
assertTrue(setupPs1Content.contains("Implementation-Version: 1.2.4"), "setup.ps1 must set Implementation-Version: 1.2.4");
|
assertTrue(setupPs1Content.contains("Implementation-Version: 1.3.0"), "setup.ps1 must set Implementation-Version: 1.3.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -112,12 +112,12 @@ public class Phase30LibraryDistributionTest {
|
|||||||
@DisplayName("Phase 30.2: Maven local published artifacts exist and validate if maven-publish was executed")
|
@DisplayName("Phase 30.2: Maven local published artifacts exist and validate if maven-publish was executed")
|
||||||
public void testMavenLocalPublishedArtifacts() throws Exception {
|
public void testMavenLocalPublishedArtifacts() throws Exception {
|
||||||
String userHome = System.getProperty("user.home");
|
String userHome = System.getProperty("user.home");
|
||||||
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/1.2.4");
|
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/1.3.0");
|
||||||
if (m2ArtifactDir.exists()) {
|
if (m2ArtifactDir.exists()) {
|
||||||
File pomFile = new File(m2ArtifactDir, "lib3270j-1.2.4.pom");
|
File pomFile = new File(m2ArtifactDir, "lib3270j-1.3.0.pom");
|
||||||
File jarFile = new File(m2ArtifactDir, "lib3270j-1.2.4.jar");
|
File jarFile = new File(m2ArtifactDir, "lib3270j-1.3.0.jar");
|
||||||
File sourcesFile = new File(m2ArtifactDir, "lib3270j-1.2.4-sources.jar");
|
File sourcesFile = new File(m2ArtifactDir, "lib3270j-1.3.0-sources.jar");
|
||||||
File javadocFile = new File(m2ArtifactDir, "lib3270j-1.2.4-javadoc.jar");
|
File javadocFile = new File(m2ArtifactDir, "lib3270j-1.3.0-javadoc.jar");
|
||||||
|
|
||||||
assertTrue(pomFile.exists() && pomFile.length() > 0, "POM file must exist and be non-empty");
|
assertTrue(pomFile.exists() && pomFile.length() > 0, "POM file must exist and be non-empty");
|
||||||
assertTrue(jarFile.exists() && jarFile.length() > 0, "JAR file must exist and be non-empty");
|
assertTrue(jarFile.exists() && jarFile.length() > 0, "JAR file must exist and be non-empty");
|
||||||
@@ -127,7 +127,7 @@ public class Phase30LibraryDistributionTest {
|
|||||||
String pomXml = Files.readString(pomFile.toPath());
|
String pomXml = Files.readString(pomFile.toPath());
|
||||||
assertTrue(pomXml.contains("<groupId>haus.nightmare.j3270</groupId>"), "POM must contain groupId");
|
assertTrue(pomXml.contains("<groupId>haus.nightmare.j3270</groupId>"), "POM must contain groupId");
|
||||||
assertTrue(pomXml.contains("<artifactId>lib3270j</artifactId>"), "POM must contain artifactId");
|
assertTrue(pomXml.contains("<artifactId>lib3270j</artifactId>"), "POM must contain artifactId");
|
||||||
assertTrue(pomXml.contains("<version>1.2.4</version>"), "POM must contain version 1.2.4");
|
assertTrue(pomXml.contains("<version>1.3.0</version>"), "POM must contain version 1.3.0");
|
||||||
assertTrue(pomXml.contains("The Unlicense"), "POM must contain Unlicense");
|
assertTrue(pomXml.contains("The Unlicense"), "POM must contain Unlicense");
|
||||||
assertFalse(pomXml.contains("Apache"), "lib3270j POM must not contain Apache license");
|
assertFalse(pomXml.contains("Apache"), "lib3270j POM must not contain Apache license");
|
||||||
assertFalse(pomXml.contains("<dependencies>"), "lib3270j POM must have zero external dependencies");
|
assertFalse(pomXml.contains("<dependencies>"), "lib3270j POM must have zero external dependencies");
|
||||||
|
|||||||
@@ -151,4 +151,40 @@ public class FastByteBufferTest {
|
|||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
|
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FastByteBuffer enforces maxCapacity and throws FramingBufferOverflowException")
|
||||||
|
public void testFastByteBufferMaxCapacityEnforcement() {
|
||||||
|
FastByteBuffer buf = new FastByteBuffer(64, 128);
|
||||||
|
assertEquals(128, buf.getMaxCapacity());
|
||||||
|
assertEquals(64, buf.getInitialCapacity());
|
||||||
|
|
||||||
|
byte[] chunk = new byte[64];
|
||||||
|
buf.write(chunk, 0, 64);
|
||||||
|
assertEquals(64, buf.size());
|
||||||
|
|
||||||
|
buf.write(chunk, 0, 64);
|
||||||
|
assertEquals(128, buf.size());
|
||||||
|
assertEquals(128, buf.buffer().length);
|
||||||
|
|
||||||
|
// Writing one more byte must throw FramingBufferOverflowException
|
||||||
|
assertThrows(FramingBufferOverflowException.class, () -> buf.write(0xFF));
|
||||||
|
|
||||||
|
// Writing a chunk that exceeds capacity must also throw
|
||||||
|
assertThrows(FramingBufferOverflowException.class, () -> buf.write(chunk, 0, 10));
|
||||||
|
|
||||||
|
// After reset, buffer is ready for use again
|
||||||
|
buf.reset();
|
||||||
|
assertEquals(0, buf.size());
|
||||||
|
buf.write(chunk, 0, 64);
|
||||||
|
assertEquals(64, buf.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FastByteBuffer default maxCapacity is 131072 bytes (128KB)")
|
||||||
|
public void testDefaultMaxCapacity() {
|
||||||
|
FastByteBuffer buf = new FastByteBuffer();
|
||||||
|
assertEquals(FastByteBuffer.DEFAULT_MAX_CAPACITY, buf.getMaxCapacity());
|
||||||
|
assertEquals(131072, buf.getMaxCapacity());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
package haus.nightmare.lib3270j.telnet;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||||
|
import haus.nightmare.lib3270j.nvt.NvtProcessor;
|
||||||
|
import haus.nightmare.lib3270j.printer.PrinterConfig;
|
||||||
|
import haus.nightmare.lib3270j.printer.Telnet3270EP;
|
||||||
|
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class FramingBufferBoundedCapTest {
|
||||||
|
|
||||||
|
private ConnectionConfig config;
|
||||||
|
private ScreenBuffer screenBuffer;
|
||||||
|
private DataStreamProcessor dsProcessor;
|
||||||
|
private TelnetFSM fsm;
|
||||||
|
private MockConnection connection;
|
||||||
|
|
||||||
|
private static class MockConnection extends TelnetConnection {
|
||||||
|
final List<byte[]> sentData = new ArrayList<>();
|
||||||
|
|
||||||
|
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||||
|
super(config, fsm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void sendRaw(byte[] data) {
|
||||||
|
sentData.add(data.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void sendRaw(byte[] data, int offset, int length) {
|
||||||
|
byte[] b = new byte[length];
|
||||||
|
System.arraycopy(data, offset, b, 0, length);
|
||||||
|
sentData.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||||
|
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
|
||||||
|
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
|
||||||
|
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
|
haus.nightmare.lib3270j.input.InputProcessor inputProcessor =
|
||||||
|
new haus.nightmare.lib3270j.input.InputProcessor(screenBuffer, new EbcdicTranslator(), fsm);
|
||||||
|
dsProcessor.setInputProcessor(inputProcessor);
|
||||||
|
connection = new MockConnection(config, fsm);
|
||||||
|
fsm.setConnection(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void feedBytes(int... bytes) {
|
||||||
|
byte[] b = new byte[bytes.length];
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
b[i] = (byte) bytes[i];
|
||||||
|
}
|
||||||
|
fsm.feedBytes(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Framing buffer caps at 128KB and enters discard mode if stream lacks IAC EOR")
|
||||||
|
public void testFramingBufferOverflowDiscardsUntilEor() {
|
||||||
|
fsm.onConnected();
|
||||||
|
|
||||||
|
// Negotiate BINARY and EOR to establish standard 3270 streaming regime
|
||||||
|
feedBytes(IAC, DO, TELOPT_EOR);
|
||||||
|
feedBytes(IAC, WILL, TELOPT_EOR);
|
||||||
|
feedBytes(IAC, DO, TELOPT_BINARY);
|
||||||
|
feedBytes(IAC, WILL, TELOPT_BINARY);
|
||||||
|
|
||||||
|
assertEquals(131072, fsm.getMaxFramingBufferSize(), "Default framing buffer cap should be 128KB (131072 bytes)");
|
||||||
|
assertFalse(fsm.isRecordDiscardMode());
|
||||||
|
|
||||||
|
// Feed chunks totaling over 128KB without any IAC EOR delimiter
|
||||||
|
byte[] chunk = new byte[8192];
|
||||||
|
Arrays.fill(chunk, (byte) 0xC1); // EBCDIC 'A'
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
fsm.feedBytes(chunk, 0, chunk.length); // 16 * 8192 = 131,072 bytes
|
||||||
|
}
|
||||||
|
assertFalse(fsm.isRecordDiscardMode(), "Buffer should be at exact cap without overflowing yet");
|
||||||
|
|
||||||
|
// Now feed an additional chunk exceeding the 128KB cap without IAC EOR
|
||||||
|
fsm.feedBytes(chunk, 0, 100);
|
||||||
|
|
||||||
|
assertTrue(fsm.isRecordDiscardMode(), "FSM must enter recordDiscardMode when cap is exceeded without IAC EOR");
|
||||||
|
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Buffer must be reset to free memory upon overflow");
|
||||||
|
|
||||||
|
// Further incoming data without IAC EOR must be discarded without growing the buffer
|
||||||
|
fsm.feedBytes(chunk, 0, chunk.length);
|
||||||
|
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Discard mode must drop subsequent data without accumulating memory");
|
||||||
|
assertTrue(fsm.isRecordDiscardMode());
|
||||||
|
|
||||||
|
// Feeding IAC EOR must clear discard mode and reset buffer cleanly
|
||||||
|
feedBytes(IAC, EOR);
|
||||||
|
assertFalse(fsm.isRecordDiscardMode(), "IAC EOR must clear recordDiscardMode");
|
||||||
|
assertEquals(0, fsm.getCurrentFramingBufferSize());
|
||||||
|
|
||||||
|
// A subsequent valid record must be processed normally
|
||||||
|
// 0xF5 (Erase/Write), 0xC2 (WCC: unlock keyboard), 0x11 (SBA), 0x00, 0x00, 0xC1 ('A'), IAC, EOR
|
||||||
|
byte[] valid3270 = new byte[] { (byte) 0xF5, (byte) 0xC2, (byte) 0x11, 0x00, 0x00, (byte) 0xC1, (byte) IAC, (byte) EOR };
|
||||||
|
fsm.feedBytes(valid3270, 0, valid3270.length);
|
||||||
|
|
||||||
|
assertEquals('A', screenBuffer.getDisplayCell(0).ucs4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Custom maxFramingBufferSize on ConnectionConfig is respected by TelnetFSM")
|
||||||
|
public void testCustomMaxFramingBufferSize() {
|
||||||
|
ConnectionConfig customConfig = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||||
|
customConfig.setMaxFramingBufferSize(1024); // 1KB custom limit
|
||||||
|
|
||||||
|
TelnetFSM customFsm = new TelnetFSM(customConfig, screenBuffer, dsProcessor);
|
||||||
|
customFsm.setConnection(connection);
|
||||||
|
customFsm.onConnected();
|
||||||
|
|
||||||
|
assertEquals(1024, customFsm.getMaxFramingBufferSize());
|
||||||
|
|
||||||
|
// Negotiate BINARY and EOR
|
||||||
|
byte[] neg = new byte[] {
|
||||||
|
(byte) IAC, (byte) DO, (byte) TELOPT_EOR,
|
||||||
|
(byte) IAC, (byte) WILL, (byte) TELOPT_EOR,
|
||||||
|
(byte) IAC, (byte) DO, (byte) TELOPT_BINARY,
|
||||||
|
(byte) IAC, (byte) WILL, (byte) TELOPT_BINARY
|
||||||
|
};
|
||||||
|
customFsm.feedBytes(neg, 0, neg.length);
|
||||||
|
|
||||||
|
byte[] payload500 = new byte[500];
|
||||||
|
Arrays.fill(payload500, (byte) 0xC1);
|
||||||
|
customFsm.feedBytes(payload500, 0, 500);
|
||||||
|
assertEquals(500, customFsm.getCurrentFramingBufferSize());
|
||||||
|
assertFalse(customFsm.isRecordDiscardMode());
|
||||||
|
|
||||||
|
customFsm.feedBytes(payload500, 0, 500);
|
||||||
|
assertEquals(1000, customFsm.getCurrentFramingBufferSize());
|
||||||
|
assertFalse(customFsm.isRecordDiscardMode());
|
||||||
|
|
||||||
|
// Overflow over 1024 limit
|
||||||
|
customFsm.feedBytes(payload500, 0, 100);
|
||||||
|
assertTrue(customFsm.isRecordDiscardMode());
|
||||||
|
assertEquals(0, customFsm.getCurrentFramingBufferSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Telnet subnegotiation buffer caps at 8KB if stream lacks IAC SE without leaking into data buffer")
|
||||||
|
public void testSubnegotiationBufferCap() {
|
||||||
|
fsm.onConnected();
|
||||||
|
feedBytes(IAC, DO, TELOPT_EOR);
|
||||||
|
feedBytes(IAC, WILL, TELOPT_EOR);
|
||||||
|
feedBytes(IAC, DO, TELOPT_BINARY);
|
||||||
|
feedBytes(IAC, WILL, TELOPT_BINARY);
|
||||||
|
|
||||||
|
// Begin subnegotiation: IAC SB TELOPT_TTYPE
|
||||||
|
feedBytes(IAC, SB, TELOPT_TTYPE);
|
||||||
|
|
||||||
|
// Feed > 8192 bytes without IAC SE
|
||||||
|
byte[] largeSb = new byte[9000];
|
||||||
|
Arrays.fill(largeSb, (byte) 'X');
|
||||||
|
fsm.feedBytes(largeSb, 0, largeSb.length);
|
||||||
|
|
||||||
|
// Overflowing SB bytes must NOT leak into the 3270 framing buffer while waiting for IAC SE
|
||||||
|
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Overflowing SB bytes must not leak into ibuf");
|
||||||
|
|
||||||
|
// Terminate the subnegotiation with IAC SE
|
||||||
|
feedBytes(IAC, SE);
|
||||||
|
assertEquals(0, fsm.getCurrentFramingBufferSize());
|
||||||
|
|
||||||
|
// Subsequent 3270 data record should be processed cleanly
|
||||||
|
byte[] valid3270 = new byte[] { (byte) 0xF5, (byte) 0xC2, (byte) 0x11, 0x00, 0x00, (byte) 0xC1, (byte) IAC, (byte) EOR };
|
||||||
|
fsm.feedBytes(valid3270, 0, valid3270.length);
|
||||||
|
assertEquals('A', screenBuffer.getDisplayCell(0).ucs4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Printer framing buffer caps at 128KB and enters discard mode without IAC EOR")
|
||||||
|
public void testPrinterFramingBufferCap() throws Exception {
|
||||||
|
PrinterConfig pcfg = new PrinterConfig("localhost", 23);
|
||||||
|
assertEquals(131072, pcfg.getMaxFramingBufferSize());
|
||||||
|
|
||||||
|
Telnet3270EP printer = new Telnet3270EP(pcfg);
|
||||||
|
assertEquals(131072, printer.getMaxFramingBufferSize());
|
||||||
|
assertFalse(printer.isRecordDiscardMode());
|
||||||
|
|
||||||
|
Method processIncoming = Telnet3270EP.class.getDeclaredMethod("processIncomingBytes", byte[].class, int.class, int.class);
|
||||||
|
processIncoming.setAccessible(true);
|
||||||
|
|
||||||
|
byte[] chunk = new byte[8192];
|
||||||
|
Arrays.fill(chunk, (byte) 0x40);
|
||||||
|
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
processIncoming.invoke(printer, chunk, 0, chunk.length); // 17 * 8192 = 139,264 bytes > 131,072
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(printer.isRecordDiscardMode(), "Printer must enter discard mode when record exceeds 128KB");
|
||||||
|
|
||||||
|
// Send IAC EOR
|
||||||
|
byte[] eor = new byte[] { (byte) IAC, (byte) EOR };
|
||||||
|
processIncoming.invoke(printer, eor, 0, eor.length);
|
||||||
|
|
||||||
|
assertFalse(printer.isRecordDiscardMode(), "Printer must clear discard mode on IAC EOR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("NvtProcessor caps escape sequence buffer at 4KB if terminator is missing")
|
||||||
|
public void testNvtProcessorEscapeBufferCap() {
|
||||||
|
NvtProcessor nvt = fsm.getNvtProcessor();
|
||||||
|
|
||||||
|
// Stream ESC [ followed by 5000 digits without terminating byte (0x40..0x7E)
|
||||||
|
byte[] escStart = new byte[] { 0x1B, '[' };
|
||||||
|
nvt.processNVTData(escStart, 0, escStart.length);
|
||||||
|
|
||||||
|
byte[] digits = new byte[5000];
|
||||||
|
Arrays.fill(digits, (byte) '1');
|
||||||
|
nvt.processNVTData(digits, 0, digits.length);
|
||||||
|
|
||||||
|
// After escBuffer reaches 4096 bytes, it aborts escape parsing and returns to STATE_NORMAL.
|
||||||
|
// Subsequent digits are rendered as plain text in the screen buffer starting at cell 0.
|
||||||
|
assertEquals('1', screenBuffer.getCell(0).ucs4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -417,7 +417,7 @@ if (Test-Path $libResDir) {
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
Write-LogInfo "Packaging standalone lib3270j library JAR..."
|
Write-LogInfo "Packaging standalone lib3270j library JAR..."
|
||||||
$libManifestFile = Join-Path $BuildDir "LIB_MANIFEST.MF"
|
$libManifestFile = Join-Path $BuildDir "LIB_MANIFEST.MF"
|
||||||
$libManifestText = "Manifest-Version: 1.0`r`nImplementation-Title: lib3270j`r`nImplementation-Version: 1.2.4`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
$libManifestText = "Manifest-Version: 1.0`r`nImplementation-Title: lib3270j`r`nImplementation-Version: 1.3.0`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||||
[System.IO.File]::WriteAllText($libManifestFile, $libManifestText)
|
[System.IO.File]::WriteAllText($libManifestFile, $libManifestText)
|
||||||
|
|
||||||
$libJarFile = Join-Path $BuildDir "lib3270j.jar"
|
$libJarFile = Join-Path $BuildDir "lib3270j.jar"
|
||||||
@@ -425,7 +425,7 @@ $libJarFile = Join-Path $BuildDir "lib3270j.jar"
|
|||||||
|
|
||||||
Write-LogInfo "Packaging standalone executable JAR..."
|
Write-LogInfo "Packaging standalone executable JAR..."
|
||||||
$manifestFile = Join-Path $BuildDir "MANIFEST.MF"
|
$manifestFile = Join-Path $BuildDir "MANIFEST.MF"
|
||||||
$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 1.2.4`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 1.3.0`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||||
[System.IO.File]::WriteAllText($manifestFile, $manifestText)
|
[System.IO.File]::WriteAllText($manifestFile, $manifestText)
|
||||||
|
|
||||||
$jarFile = Join-Path $BuildDir "j3270.jar"
|
$jarFile = Join-Path $BuildDir "j3270.jar"
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ log_info "Packaging standalone lib3270j library JAR..."
|
|||||||
cat << 'EOF' > "$BUILD_DIR/LIB_MANIFEST.MF"
|
cat << 'EOF' > "$BUILD_DIR/LIB_MANIFEST.MF"
|
||||||
Manifest-Version: 1.0
|
Manifest-Version: 1.0
|
||||||
Implementation-Title: lib3270j
|
Implementation-Title: lib3270j
|
||||||
Implementation-Version: 1.2.4
|
Implementation-Version: 1.3.0
|
||||||
Created-By: j3270 setup.sh
|
Created-By: j3270 setup.sh
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
@@ -390,7 +390,7 @@ cat << 'EOF' > "$BUILD_DIR/MANIFEST.MF"
|
|||||||
Manifest-Version: 1.0
|
Manifest-Version: 1.0
|
||||||
Main-Class: haus.nightmare.j3270.J3270App
|
Main-Class: haus.nightmare.j3270.J3270App
|
||||||
Implementation-Title: j3270
|
Implementation-Title: j3270
|
||||||
Implementation-Version: 1.2.4
|
Implementation-Version: 1.3.0
|
||||||
Created-By: j3270 setup.sh
|
Created-By: j3270 setup.sh
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Reference in New Issue
Block a user