Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
880196c5c5
|
||
|
|
2437b49fc1
|
||
|
|
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:
|
||||
|
||||
- **[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.2.5`).
|
||||
- **[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-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`).
|
||||
|
||||
### 📦 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.2.5`.
|
||||
- **`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
|
||||
|
||||
- [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 {
|
||||
group = 'haus.nightmare.j3270'
|
||||
version = '1.2.4'
|
||||
version = '1.2.5'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
@@ -28,6 +28,9 @@ import java.util.logging.*;
|
||||
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
|
||||
|
||||
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
|
||||
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);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
PKG_LOG_ROOT.setLevel(Level.ALL);
|
||||
PKG_LOG_APP.setLevel(Level.ALL);
|
||||
PKG_LOG_LIB.setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
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.
|
||||
*/
|
||||
public static final String VERSION = "1.2.4";
|
||||
public static final String VERSION = "1.2.5";
|
||||
|
||||
/**
|
||||
* Canonical application name.
|
||||
@@ -22,7 +22,7 @@ public final class Version {
|
||||
* 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.
|
||||
*
|
||||
* @return the version string (e.g. "1.2.4")
|
||||
* @return the version string (e.g. "1.2.5")
|
||||
*/
|
||||
public static String getVersion() {
|
||||
Package pkg = Version.class.getPackage();
|
||||
|
||||
@@ -353,14 +353,14 @@ public class FileTransferDialog extends JDialog {
|
||||
boolean isTso = (hostType == FTConfig.HostType.TSO);
|
||||
boolean isCms = (hostType == FTConfig.HostType.CMS);
|
||||
|
||||
appendCheck.setEnabled(!isSend);
|
||||
overwriteCheck.setEnabled(!isSend);
|
||||
appendCheck.setEnabled(true);
|
||||
overwriteCheck.setEnabled(true);
|
||||
|
||||
crCheck.setEnabled(isAscii);
|
||||
remapCheck.setEnabled(isAscii);
|
||||
|
||||
recfmField.setEnabled(isSend && isTso);
|
||||
lreclField.setEnabled(isSend && isTso);
|
||||
recfmField.setEnabled(isSend && (isTso || isCms));
|
||||
lreclField.setEnabled(isSend && (isTso || isCms));
|
||||
blksizeField.setEnabled(isSend && isTso);
|
||||
unitsCombo.setEnabled(isSend && isTso);
|
||||
spaceField.setEnabled(isSend && isTso);
|
||||
@@ -405,7 +405,7 @@ public class FileTransferDialog extends JDialog {
|
||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||
hostFileField.setText(dialog.getSelectedHostFile());
|
||||
HostDirectoryEntry entry = dialog.getSelectedEntry();
|
||||
if (entry != null) {
|
||||
if (entry != null && hostType == FTConfig.HostType.TSO) {
|
||||
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
|
||||
recfmField.setText(entry.getRecfm().trim());
|
||||
}
|
||||
@@ -443,30 +443,54 @@ public class FileTransferDialog extends JDialog {
|
||||
return;
|
||||
}
|
||||
|
||||
FTConfig.HostType selectedHostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
boolean isReceive = receiveRadio.isSelected();
|
||||
boolean isSend = sendRadio.isSelected();
|
||||
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem());
|
||||
config.setHostType(selectedHostType);
|
||||
config.setLocalFilename(localFileField.getText().trim());
|
||||
config.setHostFilename(hostFileField.getText().trim());
|
||||
config.setReceive(receiveRadio.isSelected());
|
||||
config.setReceive(isReceive);
|
||||
config.setAscii(asciiRadio.isSelected());
|
||||
config.setCrFlag(crCheck.isSelected());
|
||||
config.setRemapFlag(remapCheck.isSelected());
|
||||
config.setAppend(appendCheck.isSelected());
|
||||
config.setOverwrite(overwriteCheck.isSelected());
|
||||
if (isReceive) {
|
||||
config.setAppend(appendCheck.isSelected());
|
||||
config.setOverwrite(overwriteCheck.isSelected());
|
||||
} else {
|
||||
if (overwriteCheck.isSelected()) {
|
||||
config.setExistAction(FTConfig.ExistAction.REPLACE);
|
||||
}
|
||||
if (appendCheck.isSelected()) {
|
||||
config.setAppend(true);
|
||||
}
|
||||
}
|
||||
|
||||
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
||||
if (mtu != null) {
|
||||
config.setDftBufferSize(mtu);
|
||||
}
|
||||
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
||||
config.setSpace(spaceField.getText().trim());
|
||||
config.setDirectoryBlocks(dirField.getText().trim());
|
||||
config.setOptions(optionsField.getText().trim());
|
||||
if (isSend) {
|
||||
if (selectedHostType == FTConfig.HostType.TSO) {
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
String spaceText = spaceField.getText().trim();
|
||||
String dirText = dirField.getText().trim();
|
||||
if (!spaceText.isEmpty() || !dirText.isEmpty()) {
|
||||
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
||||
config.setSpace(spaceText);
|
||||
config.setDirectoryBlocks(dirText);
|
||||
}
|
||||
} else if (selectedHostType == FTConfig.HostType.CMS) {
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setOptions(optionsField.getText().trim());
|
||||
}
|
||||
}
|
||||
|
||||
recordSessionTransfer();
|
||||
|
||||
|
||||
@@ -29,36 +29,42 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
public class Phase27BuildAndRenderTest {
|
||||
|
||||
@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 synchronized across build files and android.useAndroidX removed")
|
||||
public void testVersionSynchronizationAndGradleProperties() throws Exception {
|
||||
File projectRoot = new File(".").getCanonicalFile();
|
||||
if (!new File(projectRoot, "settings.gradle").exists() && projectRoot.getParentFile() != null) {
|
||||
projectRoot = projectRoot.getParentFile();
|
||||
}
|
||||
|
||||
// 1. Verify build.gradle version = '1.2.4'
|
||||
String expectedVersion = Version.VERSION;
|
||||
assertNotNull(expectedVersion, "Version.VERSION must not be null");
|
||||
assertFalse(expectedVersion.isBlank(), "Version.VERSION must not be blank");
|
||||
|
||||
// 1. Verify build.gradle defines the expected version
|
||||
File buildGradle = new File(projectRoot, "build.gradle");
|
||||
assertTrue(buildGradle.exists(), "build.gradle must exist");
|
||||
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 = '" + expectedVersion + "'"),
|
||||
"build.gradle must define version = '" + expectedVersion + "'");
|
||||
assertFalse(buildGradleContent.contains("0.1.0"), "build.gradle must not contain 0.1.0");
|
||||
|
||||
|
||||
// 3. Verify setup.ps1 Implementation-Version: 1.2.4
|
||||
// 2. Verify setup.ps1 specifies the expected Implementation-Version
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
assertTrue(setupPs1.exists(), "setup.ps1 must exist");
|
||||
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: " + expectedVersion),
|
||||
"setup.ps1 must specify Implementation-Version: " + expectedVersion);
|
||||
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
|
||||
// 3. Verify setup.sh specifies the expected Implementation-Version
|
||||
File setupSh = new File(projectRoot, "setup.sh");
|
||||
assertTrue(setupSh.exists(), "setup.sh must exist");
|
||||
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: " + expectedVersion),
|
||||
"setup.sh must specify Implementation-Version: " + expectedVersion);
|
||||
assertFalse(setupShContent.contains("Implementation-Version: 0.1.0"), "setup.sh must not contain 0.1.0");
|
||||
|
||||
// 5. Verify gradle.properties android.useAndroidX removed
|
||||
// 4. Verify gradle.properties android.useAndroidX removed
|
||||
File gradleProps = new File(projectRoot, "gradle.properties");
|
||||
assertTrue(gradleProps.exists(), "gradle.properties must exist");
|
||||
String gradlePropsContent = Files.readString(gradleProps.toPath());
|
||||
|
||||
@@ -20,28 +20,35 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
public class Phase31AboutAndInterfaceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Phase 31.1: Dedicated Version classes report v1.2.4 single source of truth")
|
||||
@DisplayName("Phase 31.1: Dedicated Version classes report single source of truth")
|
||||
public void testVersionClasses() {
|
||||
// lib3270j
|
||||
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.VERSION);
|
||||
assertNotNull(haus.nightmare.lib3270j.Version.VERSION, "lib3270j Version.VERSION must not be null");
|
||||
assertFalse(haus.nightmare.lib3270j.Version.VERSION.isBlank(), "lib3270j Version.VERSION must not be blank");
|
||||
assertEquals("lib3270j", haus.nightmare.lib3270j.Version.NAME);
|
||||
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.getVersion());
|
||||
assertEquals(haus.nightmare.lib3270j.Version.VERSION, haus.nightmare.lib3270j.Version.getVersion());
|
||||
|
||||
// j3270
|
||||
assertEquals("1.2.4", haus.nightmare.j3270.Version.VERSION);
|
||||
assertNotNull(haus.nightmare.j3270.Version.VERSION, "j3270 Version.VERSION must not be null");
|
||||
assertFalse(haus.nightmare.j3270.Version.VERSION.isBlank(), "j3270 Version.VERSION must not be blank");
|
||||
assertEquals("j3270", haus.nightmare.j3270.Version.NAME);
|
||||
assertEquals("1.2.4", haus.nightmare.j3270.Version.getVersion());
|
||||
assertEquals(haus.nightmare.j3270.Version.VERSION, haus.nightmare.j3270.Version.getVersion());
|
||||
|
||||
// Both modules should report consistent versions
|
||||
assertEquals(haus.nightmare.lib3270j.Version.VERSION, haus.nightmare.j3270.Version.VERSION);
|
||||
}
|
||||
|
||||
@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 and lib3270j versions")
|
||||
public void testAboutTextContent() {
|
||||
String aboutText = J3270App.getAboutText();
|
||||
assertNotNull(aboutText, "About text must not be null");
|
||||
|
||||
// Verify version reports
|
||||
assertTrue(aboutText.contains("j3270 v1.2.4"), "About text must report j3270 v1.2.4");
|
||||
assertTrue(aboutText.contains("lib3270j v1.2.4"), "About text must report lib3270j v1.2.4");
|
||||
// Verify version reports dynamically
|
||||
assertTrue(aboutText.contains("j3270 v" + haus.nightmare.j3270.Version.getVersion()),
|
||||
"About text must report j3270 v" + haus.nightmare.j3270.Version.getVersion());
|
||||
assertTrue(aboutText.contains("lib3270j v" + haus.nightmare.lib3270j.Version.getVersion()),
|
||||
"About text must report lib3270j v" + haus.nightmare.lib3270j.Version.getVersion());
|
||||
|
||||
// Verify stale pre-release versions are eradicated
|
||||
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 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)
|
||||
private String hostType = "TSO";
|
||||
private boolean savedConnection = false;
|
||||
@@ -549,6 +553,11 @@ public class ConnectionConfig {
|
||||
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
|
||||
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() {
|
||||
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
|
||||
pcfg.setUseTls(useTls);
|
||||
@@ -559,6 +568,7 @@ public class ConnectionConfig {
|
||||
pcfg.setPrinterLuName(associatedPrinterLu);
|
||||
pcfg.setAssociatedDisplayLuName(luName);
|
||||
pcfg.setCodePage(codePage);
|
||||
pcfg.setMaxFramingBufferSize(maxFramingBufferSize);
|
||||
return pcfg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public final class Version {
|
||||
/**
|
||||
* Hardcoded fallback release version matching build.gradle and pom coordinates.
|
||||
*/
|
||||
public static final String VERSION = "1.2.4";
|
||||
public static final String VERSION = "1.2.5";
|
||||
|
||||
/**
|
||||
* Canonical library name.
|
||||
@@ -22,7 +22,7 @@ public final class Version {
|
||||
* 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.
|
||||
*
|
||||
* @return the version string (e.g. "1.2.4")
|
||||
* @return the version string (e.g. "1.2.5")
|
||||
*/
|
||||
public static String getVersion() {
|
||||
Package pkg = Version.class.getPackage();
|
||||
|
||||
+46
-17
@@ -41,10 +41,12 @@ public class DataStreamProcessor {
|
||||
private haus.nightmare.lib3270j.input.InputProcessor inputProcessor;
|
||||
|
||||
// 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.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 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;
|
||||
|
||||
// Phase 10: Auto-Unlock & Contention Resolution State
|
||||
@@ -1188,6 +1190,7 @@ public class DataStreamProcessor {
|
||||
processOutbound3270DS(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_TRANSFER_DATA: // 0xD0
|
||||
qreplyPending = false;
|
||||
if (ftDft != null) {
|
||||
ftDft.processStructuredField(data, pos, fieldLen);
|
||||
} else {
|
||||
@@ -1539,32 +1542,58 @@ public class DataStreamProcessor {
|
||||
|
||||
if (flags == 0x80) { // SPAN_FIRST
|
||||
gocaAccumulator.reset();
|
||||
gocaDiscardMode = false;
|
||||
currentGocaSubtype = sfSubId;
|
||||
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
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
if (orderLen > 0 && !gocaDiscardMode) {
|
||||
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
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
if (!gocaDiscardMode) {
|
||||
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();
|
||||
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();
|
||||
gocaDiscardMode = false;
|
||||
} else { // SPAN_ONLY (0xC0)
|
||||
gocaAccumulator.reset();
|
||||
gocaDiscardMode = false;
|
||||
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
||||
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||
|
||||
@@ -17,21 +17,49 @@ import java.util.Arrays;
|
||||
*/
|
||||
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 int count;
|
||||
|
||||
public FastByteBuffer() {
|
||||
this(32768);
|
||||
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_CAPACITY);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (minCapacity > maxCapacity) {
|
||||
throw new FramingBufferOverflowException(
|
||||
"FastByteBuffer exceeded maximum capacity of " + maxCapacity + " bytes (requested: " + minCapacity + ")"
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +92,9 @@ public class FastByteBuffer extends OutputStream {
|
||||
|
||||
public void reset() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ public class FTConfig {
|
||||
private CrAction crAction = CrAction.REMOVE;
|
||||
private boolean remapFlag = true;
|
||||
private ExistAction existAction = ExistAction.KEEP;
|
||||
private boolean allowLocalOverwrite = true;
|
||||
private RecordFormat recfm = RecordFormat.DEFAULT;
|
||||
private AllocationUnit units = AllocationUnit.DEFAULT;
|
||||
private int lrecl = 0;
|
||||
@@ -97,7 +98,7 @@ public class FTConfig {
|
||||
return isAscii() && crAction != CrAction.KEEP;
|
||||
}
|
||||
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
||||
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
|
||||
public boolean isOverwrite() { return allowLocalOverwrite || existAction == ExistAction.REPLACE; }
|
||||
public boolean isOptionNew() { return optionNew; }
|
||||
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
|
||||
public boolean isOptionNoso() { return optionNoso; }
|
||||
@@ -210,7 +211,7 @@ public class FTConfig {
|
||||
}
|
||||
|
||||
public void setOverwrite(boolean overwrite) {
|
||||
if (overwrite) this.existAction = ExistAction.REPLACE;
|
||||
this.allowLocalOverwrite = overwrite;
|
||||
}
|
||||
|
||||
public String getCodePage() { return codePage; }
|
||||
@@ -588,7 +589,7 @@ public class FTConfig {
|
||||
} else if (isOptionNew()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("NEW");
|
||||
} else if (isOverwrite()) {
|
||||
} else if (existAction == ExistAction.REPLACE && hostType != HostType.CMS) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("REPLACE");
|
||||
}
|
||||
@@ -607,12 +608,14 @@ public class FTConfig {
|
||||
default: break;
|
||||
}
|
||||
opts.append(")");
|
||||
if (lrecl > 0) {
|
||||
opts.append(" LRECL(").append(lrecl).append(")");
|
||||
}
|
||||
if (blksize > 0) {
|
||||
opts.append(" BLKSIZE(").append(blksize).append(")");
|
||||
}
|
||||
}
|
||||
if (lrecl > 0) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("LRECL(").append(lrecl).append(")");
|
||||
}
|
||||
if (blksize > 0) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("BLKSIZE(").append(blksize).append(")");
|
||||
}
|
||||
if (units != AllocationUnit.DEFAULT || primarySpace > 0 || directoryBlocks > 0) {
|
||||
AllocationUnit effectiveUnits = (units != AllocationUnit.DEFAULT) ? units : AllocationUnit.TRACKS;
|
||||
@@ -641,12 +644,10 @@ public class FTConfig {
|
||||
case VARIABLE: opts.append("V"); break;
|
||||
default: break;
|
||||
}
|
||||
if (lrecl > 0) {
|
||||
opts.append(" LRECL ").append(lrecl);
|
||||
}
|
||||
if (blksize > 0) {
|
||||
opts.append(" BLOCK ").append(blksize);
|
||||
}
|
||||
}
|
||||
if (lrecl > 0) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("LRECL ").append(lrecl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ public class FTCut {
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private boolean lastCr = false;
|
||||
private int lookaheadByte = -1;
|
||||
|
||||
public FTCut(ScreenBuffer screen, InputProcessor input,
|
||||
EbcdicTranslator translator, FTCutListener listener) {
|
||||
@@ -181,6 +182,7 @@ public class FTCut {
|
||||
xlateBufIx = 0;
|
||||
cutEof = false;
|
||||
lastCr = false;
|
||||
lookaheadByte = -1;
|
||||
retransmitRetries = 0;
|
||||
lastUploadCount = 0;
|
||||
lastUploadSeq = 0;
|
||||
@@ -217,11 +219,17 @@ public class FTCut {
|
||||
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
||||
// Not a CUT frame (likely local echo, menu return, or host error screen)
|
||||
if (xferInProgress) {
|
||||
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
||||
xferInProgress = false;
|
||||
String hostErr = scanScreenForErrors();
|
||||
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
|
||||
listener.onTransferAborted(abortMsg);
|
||||
String screenUpper = screen.getText().toUpperCase();
|
||||
if (screenUpper.contains("TRANS03") || screenUpper.contains("TRANS04") || screenUpper.contains("COMPLETE")) {
|
||||
log.info("CUT: Host screen indicated transfer complete");
|
||||
listener.onTransferComplete(null);
|
||||
} else {
|
||||
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
||||
String hostErr = scanScreenForErrors();
|
||||
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
|
||||
listener.onTransferAborted(abortMsg);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -270,6 +278,9 @@ public class FTCut {
|
||||
String line = lineSb.toString().trim();
|
||||
if (line.isEmpty()) continue;
|
||||
String lineUpper = line.toUpperCase();
|
||||
if (lineUpper.contains("TRANS03") || lineUpper.contains("TRANS04") || lineUpper.contains("COMPLETE") || lineUpper.contains("TERMINAL -> HOST COMPLETE")) {
|
||||
continue;
|
||||
}
|
||||
for (String prefix : errorPrefixes) {
|
||||
if (lineUpper.contains(prefix)) {
|
||||
return line;
|
||||
@@ -309,14 +320,18 @@ public class FTCut {
|
||||
switch (code) {
|
||||
case SC_HOST_ACK:
|
||||
log.info("CUT: HOST_ACK received — transfer running");
|
||||
listener.onCutRunning();
|
||||
if (listener.getCurrentState() == FTState.NONE) {
|
||||
return;
|
||||
}
|
||||
xferInProgress = true;
|
||||
expandedLength = 0;
|
||||
quadrant = -1;
|
||||
xlateBuffered = 0;
|
||||
xlateBufIx = 0;
|
||||
cutEof = false;
|
||||
lastCr = false;
|
||||
cutAck();
|
||||
listener.onCutRunning();
|
||||
break;
|
||||
|
||||
case SC_XFER_COMPLETE:
|
||||
@@ -378,6 +393,14 @@ public class FTCut {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listener.getCurrentState() == FTState.AWAIT_ACK) {
|
||||
listener.onCutRunning();
|
||||
if (listener.getCurrentState() == FTState.NONE) {
|
||||
return;
|
||||
}
|
||||
xferInProgress = true;
|
||||
}
|
||||
|
||||
FTConfig config = listener.getConfig();
|
||||
|
||||
int count = 0;
|
||||
@@ -439,6 +462,14 @@ public class FTCut {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listener.getCurrentState() == FTState.AWAIT_ACK) {
|
||||
listener.onCutRunning();
|
||||
if (listener.getCurrentState() == FTState.NONE) {
|
||||
return;
|
||||
}
|
||||
xferInProgress = true;
|
||||
}
|
||||
|
||||
FTConfig config = listener.getConfig();
|
||||
|
||||
int rawLength = (FTConstants.from6(screen.getCellEC(O_DT_LEN), translator) << 6) |
|
||||
@@ -615,6 +646,16 @@ public class FTCut {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private int readRawByte() throws IOException {
|
||||
if (lookaheadByte != -1) {
|
||||
int b = lookaheadByte;
|
||||
lookaheadByte = -1;
|
||||
return b;
|
||||
}
|
||||
if (inputStream == null) return -1;
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
private int xlateGetc(FTConfig config) throws IOException {
|
||||
if (xlateBuffered > 0) {
|
||||
int r = xlateBuf[xlateBufIx++];
|
||||
@@ -622,24 +663,40 @@ public class FTCut {
|
||||
return r;
|
||||
}
|
||||
|
||||
if (inputStream == null) return -1;
|
||||
|
||||
int c = inputStream.read();
|
||||
int c = readRawByte();
|
||||
if (c == -1) return -1;
|
||||
|
||||
int localByte = c & 0xFF;
|
||||
int[] cbuf = new int[32];
|
||||
int nc = 0;
|
||||
int[] cbuf = new int[4];
|
||||
|
||||
if (config.isAscii()) {
|
||||
if (config.isCrFlag() && !lastCr && localByte == '\n') {
|
||||
nc += uploadConvert('\r', cbuf, nc, config);
|
||||
if (config.isAscii() && config.isCrFlag()) {
|
||||
if (c == '\r') {
|
||||
int next = readRawByte();
|
||||
while (next == '\r') {
|
||||
next = readRawByte();
|
||||
}
|
||||
if (next != '\n' && next != -1) {
|
||||
lookaheadByte = next;
|
||||
}
|
||||
nc = uploadConvert('\r', cbuf, 0, config);
|
||||
nc += uploadConvert('\n', cbuf, nc, config);
|
||||
} else if (c == '\n') {
|
||||
int next = readRawByte();
|
||||
while (next == '\r') {
|
||||
next = readRawByte();
|
||||
}
|
||||
if (next != -1) {
|
||||
lookaheadByte = next;
|
||||
}
|
||||
nc = uploadConvert('\r', cbuf, 0, config);
|
||||
nc += uploadConvert('\n', cbuf, nc, config);
|
||||
} else {
|
||||
nc = uploadConvert(c & 0xFF, cbuf, 0, config);
|
||||
}
|
||||
lastCr = (localByte == '\r');
|
||||
} else {
|
||||
nc = uploadConvert(c & 0xFF, cbuf, 0, config);
|
||||
}
|
||||
|
||||
nc += uploadConvert(localByte, cbuf, nc, config);
|
||||
|
||||
if (nc > 1) {
|
||||
for (int i = 1; i < nc; i++) {
|
||||
xlateBuf[xlateBuffered++] = cbuf[i];
|
||||
@@ -665,10 +722,10 @@ public class FTCut {
|
||||
|
||||
if (!config.isAscii() || !config.isRemapFlag()) {
|
||||
ebc = localByte & 0xFF;
|
||||
} else if (localByte < 0x20 || (localByte >= 0x80 && localByte < 0x9F)) {
|
||||
ebc = localByte & 0xFF;
|
||||
} else if (localByte < 0x20 || localByte == 0x7F || (localByte >= 0x80 && localByte < 0x9F)) {
|
||||
ebc = FTConstants.FT2ASC[FTConstants.ASC2FT[localByte & 0xFF]];
|
||||
} else if (localByte == 0x9F) {
|
||||
ebc = 0xFF;
|
||||
ebc = FTConstants.FT2ASC[0xFF];
|
||||
} else {
|
||||
int standardEbc = translator.unicodeToEbcdic((char) localByte);
|
||||
if (standardEbc < 0) {
|
||||
|
||||
@@ -41,15 +41,19 @@ public class FTDft {
|
||||
private long recnum = 1;
|
||||
private boolean dftEof = false;
|
||||
private boolean messageFlag = false;
|
||||
private boolean dataStreamOpened = false;
|
||||
private long bytesTransferred = 0;
|
||||
private long estimatedTotalBytes = 0;
|
||||
private int hostLrecl = 0;
|
||||
private String hostRecfm = "";
|
||||
private int hostBlksize = 0;
|
||||
private int openRecsz = 0;
|
||||
private int customMtuSize = 0;
|
||||
private int pendingByte = -1;
|
||||
private int lookaheadByte = -1;
|
||||
|
||||
// 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 boolean dbcsMode = false;
|
||||
private boolean hasPendingByteInDBCS = false;
|
||||
@@ -158,15 +162,18 @@ public class FTDft {
|
||||
recnum = 1;
|
||||
dftEof = false;
|
||||
messageFlag = false;
|
||||
dataStreamOpened = false;
|
||||
bytesTransferred = 0;
|
||||
estimatedTotalBytes = 0;
|
||||
hostLrecl = 0;
|
||||
hostRecfm = "";
|
||||
hostBlksize = 0;
|
||||
openRecsz = 0;
|
||||
dftSaveBuf = null;
|
||||
dftSaveBufLen = 0;
|
||||
lastCr = false;
|
||||
pendingByte = -1;
|
||||
lookaheadByte = -1;
|
||||
downloadRecordBuffer.reset();
|
||||
dbcsMode = false;
|
||||
hasPendingByteInDBCS = false;
|
||||
@@ -215,8 +222,8 @@ public class FTDft {
|
||||
|
||||
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
||||
|
||||
// Check if this is a host error reply (e.g. TR_ERROR_REPLY 0x08, or contains TR_ERROR_HDR 0x6904)
|
||||
if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) {
|
||||
// Check if this is a host error reply (TR_ERROR_REPLY 0x08)
|
||||
if ((requestCode & 0xFF) == TR_ERROR_REPLY) {
|
||||
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
|
||||
return;
|
||||
}
|
||||
@@ -248,15 +255,6 @@ public class FTDft {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isErrorField(byte[] data, int start, int length) {
|
||||
int end = Math.min(start + length, data.length);
|
||||
for (int i = start; i + 3 < end; i++) {
|
||||
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
|
||||
if (hdr == TR_ERROR_HDR) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
|
||||
int end = Math.min(payloadStart + length, data.length);
|
||||
int errCode = 0;
|
||||
@@ -319,20 +317,24 @@ public class FTDft {
|
||||
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
||||
log.fine("DFT: Open request");
|
||||
|
||||
// Parse open request payload matching x3270 / DDM open
|
||||
// sfLength is the 2-byte length value at sfOffset
|
||||
// Parse open request payload matching x3270 / HoD Xfer3270:
|
||||
// sfLenVal is the 2-byte length value at sfOffset (or sfLength when 0x0000)
|
||||
int sfLenVal = ((data[sfOffset] & 0xFF) << 8) | (data[sfOffset + 1] & 0xFF);
|
||||
if (sfLenVal == 0) {
|
||||
sfLenVal = sfLength;
|
||||
}
|
||||
String nameBuf = "";
|
||||
int parsedRecsz = 0;
|
||||
|
||||
if (sfLenVal == 0x23 && sfOffset + 3 + 25 <= data.length) {
|
||||
nameBuf = extractName(data, sfOffset + 3 + 25, 7);
|
||||
} else if (sfLenVal == 0x29 && sfOffset + 3 + 31 <= data.length) {
|
||||
parsedRecsz = ((data[sfOffset + 3 + 27] & 0xFF) << 8) | (data[sfOffset + 3 + 28] & 0xFF);
|
||||
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
||||
} else {
|
||||
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
||||
}
|
||||
|
||||
// Check for host DDM file attributes embedded in open request
|
||||
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
||||
|
||||
int sfEnd = Math.min(sfOffset + sfLength, data.length);
|
||||
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
|
||||
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
|
||||
@@ -341,7 +343,16 @@ public class FTDft {
|
||||
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
||||
} else {
|
||||
messageFlag = false;
|
||||
dataStreamOpened = true;
|
||||
listener.onDftRunning();
|
||||
if (listener.getCurrentState() == FTState.NONE) {
|
||||
sendOpenFailed();
|
||||
return;
|
||||
}
|
||||
openRecsz = parsedRecsz;
|
||||
if (parsedRecsz > 0) {
|
||||
hostLrecl = parsedRecsz;
|
||||
}
|
||||
}
|
||||
|
||||
dftEof = false;
|
||||
@@ -351,28 +362,45 @@ public class FTDft {
|
||||
dftOpenAck();
|
||||
}
|
||||
|
||||
private void parseDdmAttributes(byte[] data, int start, int length) {
|
||||
int end = Math.min(start + length, data.length);
|
||||
int pos = start + 2; // skip TR_OPEN_REQ
|
||||
private void parseDdmAttributes(byte[] data, int offset, int length) {
|
||||
int pos = offset + 2; // skip 2-byte request code (0x0012)
|
||||
int end = Math.min(offset + length, data.length);
|
||||
while (pos + 3 <= end) {
|
||||
int attrType = data[pos] & 0xFF;
|
||||
int attrId = data[pos] & 0xFF;
|
||||
int attrLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
||||
if (attrLen < 3 || pos + attrLen > end) {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
if (attrType == DDM_HDR_LRECL && attrLen >= 5) {
|
||||
hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
||||
} else if (attrType == DDM_HDR_RECFM && attrLen >= 4) {
|
||||
int r = data[pos + 3] & 0xFF;
|
||||
hostRecfm = (r == 1) ? "F" : (r == 2 ? "V" : "U");
|
||||
} else if (attrType == DDM_HDR_BLKSIZE && attrLen >= 5) {
|
||||
hostBlksize = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
||||
} else if (attrType == DDM_HDR_FILESIZE && attrLen >= 7) {
|
||||
estimatedTotalBytes = (((long)(data[pos + 3] & 0xFF)) << 24) |
|
||||
(((long)(data[pos + 4] & 0xFF)) << 16) |
|
||||
(((long)(data[pos + 5] & 0xFF)) << 8) |
|
||||
((long)(data[pos + 6] & 0xFF));
|
||||
switch (attrId) {
|
||||
case DDM_HDR_LRECL:
|
||||
if (attrLen >= 5) {
|
||||
hostLrecl = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
||||
}
|
||||
break;
|
||||
case DDM_HDR_RECFM:
|
||||
if (attrLen >= 4) {
|
||||
int r = data[pos + 3] & 0xFF;
|
||||
if (r == 1 || r == 'F' || r == 0xC6) hostRecfm = "F";
|
||||
else if (r == 2 || r == 'V' || r == 0xE5) hostRecfm = "V";
|
||||
else if (r == 3 || r == 'U' || r == 0xE4) hostRecfm = "U";
|
||||
}
|
||||
break;
|
||||
case DDM_HDR_BLKSIZE:
|
||||
if (attrLen >= 5) {
|
||||
hostBlksize = ((data[pos + 3] & 0xFF) << 8) | (data[pos + 4] & 0xFF);
|
||||
}
|
||||
break;
|
||||
case DDM_HDR_FILESIZE:
|
||||
if (attrLen >= 7) {
|
||||
estimatedTotalBytes = ((long) (data[pos + 3] & 0xFF) << 24) |
|
||||
((long) (data[pos + 4] & 0xFF) << 16) |
|
||||
((long) (data[pos + 5] & 0xFF) << 8) |
|
||||
(data[pos + 6] & 0xFF);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
pos += attrLen;
|
||||
}
|
||||
@@ -514,7 +542,7 @@ public class FTDft {
|
||||
log.info("DFT host message: " + msg);
|
||||
|
||||
String msgUpper = msg.toUpperCase();
|
||||
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS") || msgUpper.contains("DFH0500") || msgUpper.contains("DFH0501")) {
|
||||
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.startsWith("TRANS04") || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS") || msgUpper.contains("DFH0500") || msgUpper.contains("DFH0501")) {
|
||||
listener.onTransferComplete(null);
|
||||
} else if (msgUpper.startsWith("TRANS") || msgUpper.startsWith("DFH") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || (listener != null && listener.getCurrentState() == FTState.ABORT_SENT)) {
|
||||
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
||||
@@ -525,8 +553,8 @@ public class FTDft {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping.
|
||||
* Conforms to IBM Host On-Demand specification.
|
||||
* Write download data to the local file, handling ASCII conversion and EOF stripping.
|
||||
* Conforms to x3270 / IBM Host On-Demand specification.
|
||||
*/
|
||||
private void writeDownloadData(byte[] data, int offset, int length,
|
||||
FTConfig config) throws IOException {
|
||||
@@ -542,68 +570,7 @@ public class FTDft {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
||||
"V".equalsIgnoreCase(hostRecfm);
|
||||
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
||||
"F".equalsIgnoreCase(hostRecfm);
|
||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||
|
||||
if (isRecfmV) {
|
||||
downloadRecordBuffer.write(data, offset, length);
|
||||
processVariableDownloadRecords(config);
|
||||
} else if (isRecfmF && lrecl > 0) {
|
||||
downloadRecordBuffer.write(data, offset, length);
|
||||
processFixedDownloadRecords(lrecl, config);
|
||||
} else {
|
||||
writeConvertedChunk(data, offset, length, config);
|
||||
}
|
||||
}
|
||||
|
||||
private void processVariableDownloadRecords(FTConfig config) throws IOException {
|
||||
while (downloadRecordBuffer.size() >= 4) {
|
||||
byte[] buf = downloadRecordBuffer.toByteArray();
|
||||
int rdwLen = ((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF);
|
||||
if (rdwLen < 4) {
|
||||
downloadRecordBuffer.reset();
|
||||
if (buf.length > 1) {
|
||||
downloadRecordBuffer.write(buf, 1, buf.length - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (buf.length < rdwLen) {
|
||||
break;
|
||||
}
|
||||
int recDataLen = rdwLen - 4;
|
||||
writeConvertedChunk(buf, 4, recDataLen, config);
|
||||
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
downloadRecordBuffer.reset();
|
||||
if (buf.length > rdwLen) {
|
||||
downloadRecordBuffer.write(buf, rdwLen, buf.length - rdwLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processFixedDownloadRecords(int lrecl, FTConfig config) throws IOException {
|
||||
while (downloadRecordBuffer.size() >= lrecl) {
|
||||
byte[] buf = downloadRecordBuffer.toByteArray();
|
||||
int end = lrecl;
|
||||
while (end > 0) {
|
||||
int b = buf[end - 1] & 0xFF;
|
||||
if (b == 0x20 || b == 0x40 || (config != null && config.isRemapFlag() && b == FT2ASC[0x40])) {
|
||||
end--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
writeConvertedChunk(buf, 0, end, config);
|
||||
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
downloadRecordBuffer.reset();
|
||||
if (buf.length > lrecl) {
|
||||
downloadRecordBuffer.write(buf, lrecl, buf.length - lrecl);
|
||||
}
|
||||
}
|
||||
writeConvertedChunk(data, offset, length, config);
|
||||
}
|
||||
|
||||
private void flushRemainingDownloadRecords() throws IOException {
|
||||
@@ -681,45 +648,36 @@ public class FTDft {
|
||||
|
||||
int bufferSize = getMTUSize();
|
||||
int numbytes = bufferSize - 27;
|
||||
if (openRecsz > 0 && openRecsz < numbytes) {
|
||||
numbytes = openRecsz;
|
||||
}
|
||||
byte[] readBuf = new byte[numbytes];
|
||||
int totalRead = 0;
|
||||
|
||||
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
||||
"F".equalsIgnoreCase(hostRecfm);
|
||||
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
||||
"V".equalsIgnoreCase(hostRecfm);
|
||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||
|
||||
try {
|
||||
if (config != null && config.isAscii() && isRecfmF && lrecl > 0) {
|
||||
totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config);
|
||||
} else if (config != null && config.isAscii() && isRecfmV) {
|
||||
totalRead = readVariableRecordUpload(readBuf, numbytes, config);
|
||||
} else {
|
||||
while (!dftEof && totalRead < numbytes) {
|
||||
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
||||
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
} else if (config != null && config.isAscii()) {
|
||||
int b = dftAsciiRead(config);
|
||||
if (b == -1) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
readBuf[totalRead++] = (byte) b;
|
||||
} else {
|
||||
if (inputStream == null) { dftEof = true; break; }
|
||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
while (!dftEof && totalRead < numbytes) {
|
||||
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
||||
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
} else if (config != null && config.isAscii()) {
|
||||
int b = dftAsciiRead(config);
|
||||
if (b == -1) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
readBuf[totalRead++] = (byte) b;
|
||||
} else {
|
||||
if (inputStream == null) { dftEof = true; break; }
|
||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -781,147 +739,6 @@ public class FTDft {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readNextUploadLineConverted(FTConfig config) throws IOException {
|
||||
if (inputStream == null || dftEof) return null;
|
||||
ByteArrayOutputStream lineBytes = new ByteArrayOutputStream();
|
||||
int c;
|
||||
boolean hasData = false;
|
||||
while ((c = inputStream.read()) != -1) {
|
||||
hasData = true;
|
||||
if (c == '\r') {
|
||||
continue; // ignore CR
|
||||
}
|
||||
if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
if (c == EOF_CTRL_Z || c == EOF_CTRL_D) {
|
||||
break;
|
||||
}
|
||||
lineBytes.write(c);
|
||||
}
|
||||
if (!hasData && c == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] raw = lineBytes.toByteArray();
|
||||
return convertLineForUpload(raw, config);
|
||||
}
|
||||
|
||||
private byte[] convertLineForUpload(byte[] raw, FTConfig config) {
|
||||
String lineStr = new String(raw, StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream converted = new ByteArrayOutputStream(raw.length + 8);
|
||||
boolean isDbcs = (config != null && config.isDbcs()) || (translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS());
|
||||
boolean noSo = config != null && config.isOptionNoso();
|
||||
boolean inDbcsState = false;
|
||||
|
||||
for (int i = 0; i < lineStr.length(); i++) {
|
||||
char ch = lineStr.charAt(i);
|
||||
int dbcsVal = (isDbcs && translator != null) ? translator.unicodeToDbcs(ch) : -1;
|
||||
if (dbcsVal != -1) {
|
||||
if (!inDbcsState && !noSo) {
|
||||
converted.write(0x0E); // Shift-Out
|
||||
inDbcsState = true;
|
||||
}
|
||||
converted.write((dbcsVal >> 8) & 0xFF);
|
||||
converted.write(dbcsVal & 0xFF);
|
||||
} else {
|
||||
if (inDbcsState && !noSo) {
|
||||
converted.write(0x0F); // Shift-In
|
||||
inDbcsState = false;
|
||||
}
|
||||
converted.write(convertAsciiByte(ch, config));
|
||||
}
|
||||
}
|
||||
if (inDbcsState && !noSo) {
|
||||
converted.write(0x0F); // Shift-In at end of record
|
||||
}
|
||||
return converted.toByteArray();
|
||||
}
|
||||
|
||||
private int readFixedRecordUpload(byte[] outBuf, int maxBytes, int lrecl, FTConfig config) throws IOException {
|
||||
int totalRead = 0;
|
||||
int padByte = (config != null && config.isRemapFlag()) ? FT2ASC[0x40] : 0x20;
|
||||
|
||||
while (totalRead + lrecl <= maxBytes && !dftEof) {
|
||||
byte[] rec;
|
||||
if (pendingUploadRecord != null) {
|
||||
rec = pendingUploadRecord;
|
||||
pendingUploadRecord = null;
|
||||
} else {
|
||||
rec = readNextUploadLineConverted(config);
|
||||
if (rec == null) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rec.length <= lrecl) {
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
||||
totalRead += rec.length;
|
||||
int padCount = lrecl - rec.length;
|
||||
for (int p = 0; p < padCount; p++) {
|
||||
outBuf[totalRead++] = (byte) padByte;
|
||||
}
|
||||
} else {
|
||||
// Longer than lrecl: split
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, lrecl);
|
||||
totalRead += lrecl;
|
||||
int rem = rec.length - lrecl;
|
||||
pendingUploadRecord = new byte[rem];
|
||||
System.arraycopy(rec, lrecl, pendingUploadRecord, 0, rem);
|
||||
}
|
||||
}
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
private int readVariableRecordUpload(byte[] outBuf, int maxBytes, FTConfig config) throws IOException {
|
||||
int totalRead = 0;
|
||||
while (totalRead + 4 < maxBytes && !dftEof) {
|
||||
byte[] rec;
|
||||
if (pendingUploadRecord != null) {
|
||||
rec = pendingUploadRecord;
|
||||
pendingUploadRecord = null;
|
||||
} else {
|
||||
rec = readNextUploadLineConverted(config);
|
||||
if (rec == null) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int needed = rec.length + 4;
|
||||
if (totalRead + needed <= maxBytes) {
|
||||
outBuf[totalRead++] = (byte) ((needed >> 8) & 0xFF);
|
||||
outBuf[totalRead++] = (byte) (needed & 0xFF);
|
||||
outBuf[totalRead++] = 0;
|
||||
outBuf[totalRead++] = 0;
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
||||
totalRead += rec.length;
|
||||
} else {
|
||||
if (totalRead > 0) {
|
||||
// Save for next MTU buffer
|
||||
pendingUploadRecord = rec;
|
||||
break;
|
||||
} else {
|
||||
// Single record exceeds entire MTU: split
|
||||
int chunk = maxBytes - 4;
|
||||
int rdwLen = chunk + 4;
|
||||
outBuf[totalRead++] = (byte) ((rdwLen >> 8) & 0xFF);
|
||||
outBuf[totalRead++] = (byte) (rdwLen & 0xFF);
|
||||
outBuf[totalRead++] = 0;
|
||||
outBuf[totalRead++] = 0;
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, chunk);
|
||||
totalRead += chunk;
|
||||
int rem = rec.length - chunk;
|
||||
pendingUploadRecord = new byte[rem];
|
||||
System.arraycopy(rec, chunk, pendingUploadRecord, 0, rem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly send a framed data packet buffer to the host using DFT structured fields.
|
||||
* Supports CICS, TSO, and VM/CMS data packaging.
|
||||
@@ -987,6 +804,16 @@ public class FTDft {
|
||||
}
|
||||
}
|
||||
|
||||
private int readRawByte() throws IOException {
|
||||
if (lookaheadByte != -1) {
|
||||
int b = lookaheadByte;
|
||||
lookaheadByte = -1;
|
||||
return b;
|
||||
}
|
||||
if (inputStream == null) return -1;
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a byte from local file for upload, handling ASCII conversion and remapping.
|
||||
* Matching x3270 dft_ascii_read logic.
|
||||
@@ -998,26 +825,39 @@ public class FTDft {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (inputStream == null) return -1;
|
||||
|
||||
int c = inputStream.read();
|
||||
int c = readRawByte();
|
||||
if (c == -1) return -1;
|
||||
|
||||
if (config.isCrFlag() && !lastCr && c == '\n') {
|
||||
lastCr = false;
|
||||
// Expand \n to \r\n: buffer \n in pendingByte, return \r now
|
||||
pendingByte = convertAsciiByte('\n', config);
|
||||
return convertAsciiByte('\r', config);
|
||||
if (config.isCrFlag()) {
|
||||
if (c == '\r') {
|
||||
int next = readRawByte();
|
||||
while (next == '\r') {
|
||||
next = readRawByte();
|
||||
}
|
||||
if (next != '\n' && next != -1) {
|
||||
lookaheadByte = next;
|
||||
}
|
||||
pendingByte = convertAsciiByte('\n', config);
|
||||
return convertAsciiByte('\r', config);
|
||||
} else if (c == '\n') {
|
||||
int next = readRawByte();
|
||||
while (next == '\r') {
|
||||
next = readRawByte();
|
||||
}
|
||||
if (next != -1) {
|
||||
lookaheadByte = next;
|
||||
}
|
||||
pendingByte = convertAsciiByte('\n', config);
|
||||
return convertAsciiByte('\r', config);
|
||||
}
|
||||
}
|
||||
lastCr = (c == '\r');
|
||||
|
||||
return convertAsciiByte((char) c, config);
|
||||
}
|
||||
|
||||
private int convertAsciiByte(char c, FTConfig config) {
|
||||
if (!config.isRemapFlag()) {
|
||||
int ebc = translator.unicodeToEbcdic(c);
|
||||
return ebc >= 0 ? ebc : 0x40;
|
||||
return c & 0xFF;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1025,7 +865,7 @@ public class FTDft {
|
||||
* Translate Unicode char c -> EBCDIC -> host pseudo-ASCII FT2ASC[ebc].
|
||||
*/
|
||||
int ebc;
|
||||
if (c < 0x20 || (c >= 0x80 && c < 0x9F)) {
|
||||
if (c < 0x20 || c == 0x7F || (c >= 0x80 && c < 0x9F)) {
|
||||
ebc = ASC2FT[c & 0xFF];
|
||||
} else if (c == 0x9F) {
|
||||
ebc = 0xFF;
|
||||
@@ -1050,9 +890,13 @@ public class FTDft {
|
||||
|
||||
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
|
||||
|
||||
if (listener.getCurrentState() != FTState.NONE) {
|
||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
||||
listener.onTransferComplete(null);
|
||||
if (messageFlag || !dataStreamOpened) {
|
||||
if (listener != null && listener.getCurrentState() != FTState.NONE) {
|
||||
log.info("DFT: File transfer completed on close (" + bytesTransferred + " bytes)");
|
||||
listener.onTransferComplete(null);
|
||||
}
|
||||
} else {
|
||||
dataStreamOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,13 +906,24 @@ public class FTDft {
|
||||
input.sendStructuredFieldData(createDataAck(recnum++));
|
||||
}
|
||||
|
||||
private void sendOpenFailed() {
|
||||
if (RB_OPEN_FAILED.length > 0 && (RB_OPEN_FAILED[0] & 0xFF) == AID_SF) {
|
||||
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
|
||||
} else {
|
||||
byte[] pkt = new byte[RB_OPEN_FAILED.length + 1];
|
||||
pkt[0] = (byte) AID_SF;
|
||||
System.arraycopy(RB_OPEN_FAILED, 0, pkt, 1, RB_OPEN_FAILED.length);
|
||||
input.sendStructuredFieldData(pkt);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Abort ==========
|
||||
|
||||
private void dftAbort(String message, int code) {
|
||||
log.warning("DFT: ABORT — " + message);
|
||||
|
||||
if (code == TR_OPEN_REQ) {
|
||||
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
|
||||
sendOpenFailed();
|
||||
} else {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
||||
out.write(AID_SF);
|
||||
|
||||
@@ -1977,8 +1977,15 @@ public class InputProcessor {
|
||||
public void sendAidForFT(int aidCode) {
|
||||
boolean wasLocked = keyboardLocked;
|
||||
keyboardLocked = false;
|
||||
sendAid(aidCode);
|
||||
keyboardLocked = wasLocked;
|
||||
int prevInhibit = (oia != null) ? oia.getInputInhibited() : ECLConstants.INHIBIT_NOT_INHIBITED;
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
try {
|
||||
sendAid(aidCode);
|
||||
} finally {
|
||||
keyboardLocked = wasLocked;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2039,11 +2046,11 @@ public class InputProcessor {
|
||||
// Find the field at cursor and erase it
|
||||
int curAddr = screen.getCursorAddress();
|
||||
byte faVal = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
if (screen.getCell(curAddr).isFieldAttribute() || faIsProtected(faVal & 0xFF)) {
|
||||
// Try to find first unprotected field
|
||||
curAddr = screen.findNextUnprotected(curAddr);
|
||||
faVal = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
if (screen.getCell(curAddr).isFieldAttribute() || faIsProtected(faVal & 0xFF)) {
|
||||
return -3; // No unprotected field
|
||||
}
|
||||
}
|
||||
@@ -2517,6 +2524,8 @@ public class InputProcessor {
|
||||
public void sendStructuredFieldData(byte[] data) {
|
||||
if (fsm != null) {
|
||||
fsm.send3270Data(data);
|
||||
} else if (outputSender != null) {
|
||||
outputSender.send3270Data(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ public class NvtProcessor {
|
||||
private static final int STATE_OSC = 4;
|
||||
private static final int STATE_OSC_ESC = 5;
|
||||
|
||||
private static final int MAX_ESC_BUFFER_SIZE = 4096;
|
||||
private int parseState = STATE_NORMAL;
|
||||
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
||||
|
||||
@@ -470,32 +471,51 @@ public class NvtProcessor {
|
||||
} else if (parseState == STATE_OSC) {
|
||||
if (b == 0x07) { // BEL terminator
|
||||
handleOSC(escBuffer.toByteArray());
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 0x1B) { // Possible ESC \ (ST)
|
||||
parseState = STATE_OSC_ESC;
|
||||
} else if (b == 0x9C) { // 8-bit ST
|
||||
handleOSC(escBuffer.toByteArray());
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
} 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) {
|
||||
if (b == '\\') { // ESC \ (ST) terminator
|
||||
handleOSC(escBuffer.toByteArray());
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
} else {
|
||||
// False alarm on ST, put ESC and byte into OSC buffer
|
||||
escBuffer.write(0x1B);
|
||||
escBuffer.write(b);
|
||||
parseState = STATE_OSC;
|
||||
if (escBuffer.size() + 2 <= MAX_ESC_BUFFER_SIZE) {
|
||||
escBuffer.write(0x1B);
|
||||
escBuffer.write(b);
|
||||
parseState = STATE_OSC;
|
||||
} else {
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
} 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
|
||||
if (b >= 0x40 && b <= 0x7E) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
escBuffer.reset();
|
||||
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||
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 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 Writer targetWriter;
|
||||
private Process pipeProcess;
|
||||
@@ -33,6 +36,13 @@ public class PD3270 {
|
||||
private int pageCount = 0;
|
||||
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() {
|
||||
this(new PrinterConfig());
|
||||
}
|
||||
@@ -102,7 +112,7 @@ public class PD3270 {
|
||||
}
|
||||
|
||||
try {
|
||||
memoryStream.write(data, offset, length);
|
||||
appendToMemoryStream(data, offset, length);
|
||||
byteCount += length;
|
||||
|
||||
if (targetOutputStream != null) {
|
||||
@@ -129,7 +139,7 @@ public class PD3270 {
|
||||
}
|
||||
try {
|
||||
byte[] b = String.valueOf(c).getBytes(outputCharset);
|
||||
memoryStream.write(b);
|
||||
appendToMemoryStream(b, 0, b.length);
|
||||
byteCount += b.length;
|
||||
|
||||
if (targetWriter != null) {
|
||||
@@ -158,7 +168,7 @@ public class PD3270 {
|
||||
}
|
||||
try {
|
||||
byte[] b = s.getBytes(outputCharset);
|
||||
memoryStream.write(b);
|
||||
appendToMemoryStream(b, 0, b.length);
|
||||
byteCount += b.length;
|
||||
|
||||
if (targetWriter != null) {
|
||||
|
||||
@@ -54,6 +54,8 @@ public class PrinterConfig {
|
||||
private long autoFlushTimeoutMs = 2000;
|
||||
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
|
||||
private char graphicErrorReplacementChar = '?';
|
||||
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 131072; // 128KB
|
||||
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
|
||||
public PrinterConfig() {
|
||||
initDefaultChannelTable();
|
||||
@@ -227,4 +229,9 @@ public class PrinterConfig {
|
||||
public int[] getChannelTable() {
|
||||
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;
|
||||
|
||||
// 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 boolean inSubnegotiation = false;
|
||||
|
||||
public Telnet3270EP(PrinterConfig config) {
|
||||
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.translator = new EbcdicTranslator(this.config.getCodePage());
|
||||
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) {
|
||||
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.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
||||
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||
@@ -191,6 +201,7 @@ public class Telnet3270EP implements Runnable {
|
||||
this.recordBuffer.reset();
|
||||
this.subnegBuffer.reset();
|
||||
this.inSubnegotiation = false;
|
||||
this.recordDiscardMode = false;
|
||||
|
||||
scs.resetSCSFormatDefaults();
|
||||
printPs.erasePrintBuffer();
|
||||
@@ -376,14 +387,31 @@ public class Telnet3270EP implements Runnable {
|
||||
if (idx < end && (data[idx] & 0xFF) == TelnetConstants.SE) {
|
||||
idx++;
|
||||
inSubnegotiation = false;
|
||||
processSubnegotiation(subnegBuffer.toByteArray());
|
||||
if (!subnegDiscardMode) {
|
||||
processSubnegotiation(subnegBuffer.toByteArray());
|
||||
}
|
||||
subnegBuffer.reset();
|
||||
subnegDiscardMode = false;
|
||||
} 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++;
|
||||
}
|
||||
} else {
|
||||
subnegBuffer.write(b);
|
||||
} else if (!subnegDiscardMode) {
|
||||
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;
|
||||
}
|
||||
@@ -394,15 +422,19 @@ public class Telnet3270EP implements Runnable {
|
||||
|
||||
switch (cmd) {
|
||||
case TelnetConstants.IAC:
|
||||
recordBuffer.write(TelnetConstants.IAC);
|
||||
appendPrinterByte(TelnetConstants.IAC);
|
||||
break;
|
||||
case TelnetConstants.SB:
|
||||
inSubnegotiation = true;
|
||||
subnegDiscardMode = false;
|
||||
subnegBuffer.reset();
|
||||
break;
|
||||
case TelnetConstants.EOR:
|
||||
processCompleteRecord(recordBuffer.toByteArray());
|
||||
if (!recordDiscardMode) {
|
||||
processCompleteRecord(recordBuffer.toByteArray());
|
||||
}
|
||||
recordBuffer.reset();
|
||||
recordDiscardMode = false;
|
||||
break;
|
||||
case TelnetConstants.DO:
|
||||
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
|
||||
@@ -420,11 +452,33 @@ public class Telnet3270EP implements Runnable {
|
||||
break;
|
||||
}
|
||||
} 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) {
|
||||
try {
|
||||
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[] hisOpts = new boolean[256]; // options the host has enabled
|
||||
|
||||
// 3270 input buffer (accumulated between telnet framing)
|
||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
|
||||
// 3270 input buffer (accumulated between telnet framing, bounded at maxFramingBufferSize)
|
||||
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)
|
||||
private final byte[] nvtSingleByteScratch = new byte[1];
|
||||
@@ -165,6 +172,8 @@ public class TelnetFSM {
|
||||
this.config = config;
|
||||
this.screenBuffer = screenBuffer;
|
||||
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.setOutputSender(this::sendNvtData);
|
||||
}
|
||||
@@ -220,6 +229,8 @@ public class TelnetFSM {
|
||||
deferredWillTtype = false;
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
recordDiscardMode = false;
|
||||
sbDiscardMode = false;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
|
||||
@@ -256,7 +267,7 @@ public class TelnetFSM {
|
||||
if (isPlainNvt) {
|
||||
nvtProcessor.processNVTData(buf, start, i - start);
|
||||
} 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) {
|
||||
@@ -334,24 +345,80 @@ public class TelnetFSM {
|
||||
|
||||
// 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]) {
|
||||
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 ==========
|
||||
|
||||
private void processIAC(int c) {
|
||||
switch (c) {
|
||||
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;
|
||||
break;
|
||||
case EOR: // End of record — process accumulated 3270 data
|
||||
log.fine("RCVD EOR");
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
if (!recordDiscardMode && (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING)) {
|
||||
processEndOfRecord();
|
||||
}
|
||||
ibuf.reset();
|
||||
recordDiscardMode = false;
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case WILL: state = TNS_WILL; break;
|
||||
@@ -360,6 +427,7 @@ public class TelnetFSM {
|
||||
case DONT: state = TNS_DONT; break;
|
||||
case SB:
|
||||
sbbuf.reset();
|
||||
sbDiscardMode = false;
|
||||
state = TNS_SB;
|
||||
break;
|
||||
case GA:
|
||||
@@ -514,6 +582,9 @@ public class TelnetFSM {
|
||||
|
||||
private void processDont(int opt) {
|
||||
log.info("RCVD DONT " + TelnetConstants.optionName(opt));
|
||||
if (opt == TELOPT_TTYPE) {
|
||||
ttypeIndex = 0;
|
||||
}
|
||||
if (myOpts[opt]) {
|
||||
myOpts[opt] = false;
|
||||
sendCommand(WONT, opt);
|
||||
@@ -526,8 +597,15 @@ public class TelnetFSM {
|
||||
private void processSB(int c) {
|
||||
if (c == IAC) {
|
||||
state = TNS_SB_IAC;
|
||||
} else {
|
||||
sbbuf.write(c);
|
||||
} else if (!sbDiscardMode) {
|
||||
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 +614,34 @@ public class TelnetFSM {
|
||||
private void processSBIAC(int c) {
|
||||
if (c == SE) {
|
||||
// Sub-negotiation complete
|
||||
byte[] sbData = sbbuf.toByteArray();
|
||||
sbbuf.reset();
|
||||
processSubNegotiation(sbData);
|
||||
if (!sbDiscardMode) {
|
||||
byte[] sbData = sbbuf.toByteArray();
|
||||
sbbuf.reset();
|
||||
processSubNegotiation(sbData);
|
||||
} else {
|
||||
sbbuf.reset();
|
||||
sbDiscardMode = false;
|
||||
}
|
||||
state = TNS_DATA;
|
||||
} else if (c == IAC) {
|
||||
// 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;
|
||||
} 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");
|
||||
sbbuf.reset();
|
||||
state = TNS_DATA;
|
||||
sbDiscardMode = false;
|
||||
processIAC(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1634,6 +1727,7 @@ public class TelnetFSM {
|
||||
state = TNS_DATA;
|
||||
java.util.Arrays.fill(myOpts, false);
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
recordDiscardMode = false;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
tn3270eNegotiated = false;
|
||||
|
||||
@@ -66,7 +66,8 @@ public class Phase30LibraryDistributionTest {
|
||||
String setupShContent = Files.readString(setupSh.toPath());
|
||||
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-Version: 1.2.4"), "setup.sh must set Implementation-Version: 1.2.4");
|
||||
assertTrue(setupShContent.contains("Implementation-Version: " + Version.VERSION),
|
||||
"setup.sh must set Implementation-Version: " + Version.VERSION);
|
||||
|
||||
// 3. setup.ps1
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
@@ -74,7 +75,8 @@ public class Phase30LibraryDistributionTest {
|
||||
String setupPs1Content = Files.readString(setupPs1.toPath());
|
||||
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-Version: 1.2.4"), "setup.ps1 must set Implementation-Version: 1.2.4");
|
||||
assertTrue(setupPs1Content.contains("Implementation-Version: " + Version.VERSION),
|
||||
"setup.ps1 must set Implementation-Version: " + Version.VERSION);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -112,12 +114,13 @@ public class Phase30LibraryDistributionTest {
|
||||
@DisplayName("Phase 30.2: Maven local published artifacts exist and validate if maven-publish was executed")
|
||||
public void testMavenLocalPublishedArtifacts() throws Exception {
|
||||
String userHome = System.getProperty("user.home");
|
||||
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/1.2.4");
|
||||
String version = Version.VERSION;
|
||||
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/" + version);
|
||||
if (m2ArtifactDir.exists()) {
|
||||
File pomFile = new File(m2ArtifactDir, "lib3270j-1.2.4.pom");
|
||||
File jarFile = new File(m2ArtifactDir, "lib3270j-1.2.4.jar");
|
||||
File sourcesFile = new File(m2ArtifactDir, "lib3270j-1.2.4-sources.jar");
|
||||
File javadocFile = new File(m2ArtifactDir, "lib3270j-1.2.4-javadoc.jar");
|
||||
File pomFile = new File(m2ArtifactDir, "lib3270j-" + version + ".pom");
|
||||
File jarFile = new File(m2ArtifactDir, "lib3270j-" + version + ".jar");
|
||||
File sourcesFile = new File(m2ArtifactDir, "lib3270j-" + version + "-sources.jar");
|
||||
File javadocFile = new File(m2ArtifactDir, "lib3270j-" + version + "-javadoc.jar");
|
||||
|
||||
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");
|
||||
@@ -127,7 +130,7 @@ public class Phase30LibraryDistributionTest {
|
||||
String pomXml = Files.readString(pomFile.toPath());
|
||||
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("<version>1.2.4</version>"), "POM must contain version 1.2.4");
|
||||
assertTrue(pomXml.contains("<version>" + version + "</version>"), "POM must contain version " + version);
|
||||
assertTrue(pomXml.contains("The Unlicense"), "POM must contain Unlicense");
|
||||
assertFalse(pomXml.contains("Apache"), "lib3270j POM must not contain Apache license");
|
||||
assertFalse(pomXml.contains("<dependencies>"), "lib3270j POM must have zero external dependencies");
|
||||
|
||||
@@ -151,4 +151,40 @@ public class FastByteBufferTest {
|
||||
executor.shutdown();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,4 +187,33 @@ public class FTConfigTest {
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, config.getUnits());
|
||||
assertTrue(config.buildCommand().contains("SPACE(12,4,8) TRACKS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetOverwriteDoesNotEmitReplaceOnSend() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.CMS);
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("TEST FILE A");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setOverwrite(true);
|
||||
|
||||
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF", config.buildCommand());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCmsSendIgnoresBlksizeAndReplace() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setHostType(FTConfig.HostType.CMS);
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostFilename("TEST FILE A");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setRecfm("F");
|
||||
config.setLrecl("80");
|
||||
config.setBlksize("800");
|
||||
config.setExistAction(FTConfig.ExistAction.REPLACE);
|
||||
|
||||
assertEquals("IND$FILE PUT TEST FILE A (ASCII CRLF RECFM F LRECL 80", config.buildCommand());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,4 +576,96 @@ public class FTDftTest {
|
||||
assertTrue(ftDft.resendInboundDataBufferToHost());
|
||||
assertEquals(2, inputProcessor.sentStructuredFields.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZosUploadRecfmVAndFNormalizesAllNewlineVariants() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostType(FTConfig.HostType.TSO);
|
||||
config.setHostFilename("'USER.TEST.DATA'");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setRemapFlag(true);
|
||||
config.setRecfm("V");
|
||||
config.setLrecl("255");
|
||||
listener.config = config;
|
||||
|
||||
// Mix \n, \r\n, \n\r (\c\r), bare \r, and \r\r\n
|
||||
String originalText = "LINE1\nLINE2\r\nLINE3\n\rLINE4\rLINE5\r\r\n";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.ISO_8859_1));
|
||||
ftDft.initTransfer(in, null);
|
||||
|
||||
byte[] getReq = new byte[] {
|
||||
(byte) 0x00, (byte) 0x05, (byte) SF_TRANSFER_DATA,
|
||||
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
|
||||
};
|
||||
ftDft.processStructuredField(getReq, 0, getReq.length);
|
||||
|
||||
assertEquals(1, inputProcessor.sentStructuredFields.size());
|
||||
byte[] payload = extractDataFromGetReply(inputProcessor.sentStructuredFields.get(0));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : payload) {
|
||||
int ub = b & 0xFF;
|
||||
if (ub == 0x0D) {
|
||||
sb.append("\r");
|
||||
} else if (ub == 0x0A) {
|
||||
sb.append("\n");
|
||||
} else {
|
||||
int ebc = ASC2FT[ub];
|
||||
sb.append(translator.ebcdicToUnicode(ebc));
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("LINE1\r\nLINE2\r\nLINE3\r\nLINE4\r\nLINE5\r\n", sb.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZvmUploadOpenWithSizeAndCloseLifecycle() {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setHostType(FTConfig.HostType.CMS);
|
||||
config.setHostFilename("TEST EXEC A");
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrAction(FTConfig.CrAction.REMOVE);
|
||||
config.setRemapFlag(true);
|
||||
listener.config = config;
|
||||
|
||||
String originalText = "/* REXX */\nSAY 'HELLO FROM Z/VM'\nEXIT 0\n";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(originalText.getBytes(StandardCharsets.UTF_8));
|
||||
ftDft.initTransfer(in, null);
|
||||
|
||||
// Simulate z/VM 41-byte (0x0029) OpenForUploadWithSize with recsz = 16 (0x0010) at offset 30..31
|
||||
byte[] openWithSize = new byte[41];
|
||||
openWithSize[0] = 0x00;
|
||||
openWithSize[1] = 0x29;
|
||||
openWithSize[2] = (byte) SF_TRANSFER_DATA;
|
||||
openWithSize[3] = (byte) (TR_OPEN_REQ >> 8);
|
||||
openWithSize[4] = (byte) (TR_OPEN_REQ & 0xFF);
|
||||
openWithSize[30] = 0x00;
|
||||
openWithSize[31] = 0x10; // recsz = 16
|
||||
byte[] ftDataName = "FT:DATA".getBytes(StandardCharsets.US_ASCII);
|
||||
System.arraycopy(ftDataName, 0, openWithSize, 34, ftDataName.length);
|
||||
|
||||
ftDft.processStructuredField(openWithSize, 0, openWithSize.length);
|
||||
assertTrue(listener.dftRunningCalled);
|
||||
assertEquals(16, ftDft.getHostLrecl());
|
||||
|
||||
// Request 1st chunk — must be capped at recsz = 16 bytes
|
||||
byte[] getReq = new byte[] {
|
||||
0x00, 0x05, (byte) SF_TRANSFER_DATA,
|
||||
(byte) (TR_GET_REQ >> 8), (byte) (TR_GET_REQ & 0xFF)
|
||||
};
|
||||
ftDft.processStructuredField(getReq, 0, getReq.length);
|
||||
byte[] chunk1 = extractDataFromGetReply(inputProcessor.sentStructuredFields.get(1));
|
||||
assertEquals(16, chunk1.length);
|
||||
|
||||
// Close FT:DATA stream — should NOT mark transfer complete before FT:MSG
|
||||
byte[] closeReq = new byte[] {
|
||||
0x00, 0x05, (byte) SF_TRANSFER_DATA,
|
||||
(byte) (TR_CLOSE_REQ >> 8), (byte) (TR_CLOSE_REQ & 0xFF)
|
||||
};
|
||||
ftDft.processStructuredField(closeReq, 0, closeReq.length);
|
||||
assertFalse(listener.completeCalled, "Closing FT:DATA should wait for FT:MSG before completing");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +88,17 @@ public class FTPhase3Test {
|
||||
config.setLrecl(80);
|
||||
|
||||
String cmd = config.buildCommand();
|
||||
assertTrue(cmd.contains("REPLACE"));
|
||||
assertTrue(cmd.contains("BLOCK 4096"));
|
||||
assertFalse(cmd.contains("REPLACE"));
|
||||
assertFalse(cmd.contains("BLOCK 4096"));
|
||||
assertTrue(cmd.contains("RECFM F"));
|
||||
assertTrue(cmd.contains("LRECL 80"));
|
||||
|
||||
config.setHostType(FTConfig.HostType.TSO);
|
||||
config.setHostFilename("'USER.TEST.DATA'");
|
||||
String tsoCmd = config.buildCommand();
|
||||
assertTrue(tsoCmd.contains("BLKSIZE(4096)"));
|
||||
assertTrue(tsoCmd.contains("RECFM(F)"));
|
||||
assertTrue(tsoCmd.contains("LRECL(80)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+22
-31
@@ -293,12 +293,14 @@ public class FTProtocolHardeningPhase16Test {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.SEND);
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrFlag(true);
|
||||
config.setRemapFlag(false);
|
||||
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||
config.setLrecl("10");
|
||||
listener.config = config;
|
||||
|
||||
// Local content has two lines: "SHORT" (5 chars) and "LONGER LINE THAN TEN" (20 chars)
|
||||
String localContent = "SHORT\nLONGER LINE THAN TEN\n";
|
||||
// Local content has mixed newline styles (\n, \r\n, \n\r)
|
||||
String localContent = "SHORT\nLONGER LINE\r\nTHIRD\n\r";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(localContent.getBytes(StandardCharsets.UTF_8));
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
@@ -311,16 +313,9 @@ public class FTProtocolHardeningPhase16Test {
|
||||
byte[] reply = input.sentStructuredFields.get(0);
|
||||
int headerLen = 17;
|
||||
int dataLen = reply.length - headerLen;
|
||||
assertTrue(dataLen >= 30, "Should have packed at least 3 fixed records of 10 bytes each");
|
||||
|
||||
String rec1 = new String(reply, headerLen, 10, StandardCharsets.UTF_8);
|
||||
assertEquals("SHORT ", rec1);
|
||||
|
||||
String rec2 = new String(reply, headerLen + 10, 10, StandardCharsets.UTF_8);
|
||||
assertEquals("LONGER LIN", rec2);
|
||||
|
||||
String rec3 = new String(reply, headerLen + 20, 10, StandardCharsets.UTF_8);
|
||||
assertEquals("E THAN TEN", rec3);
|
||||
String payload = new String(reply, headerLen, dataLen, StandardCharsets.UTF_8);
|
||||
assertEquals("SHORT\r\nLONGER LINE\r\nTHIRD\r\n", payload);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -328,6 +323,8 @@ public class FTProtocolHardeningPhase16Test {
|
||||
FTConfig config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
config.setCrFlag(true);
|
||||
config.setRemapFlag(false);
|
||||
config.setRecfm(FTConfig.RecordFormat.FIXED);
|
||||
config.setLrecl("10");
|
||||
listener.config = config;
|
||||
@@ -336,8 +333,8 @@ public class FTProtocolHardeningPhase16Test {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(in, out);
|
||||
|
||||
// Host sends 10-byte fixed record "HELLO "
|
||||
byte[] hostRecord = "HELLO ".getBytes(StandardCharsets.UTF_8);
|
||||
// Host sends CRLF-terminated record "HELLO\r\n" + EOF (0x1A)
|
||||
byte[] hostRecord = "HELLO\r\n\u001a".getBytes(StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream sf = new ByteArrayOutputStream();
|
||||
sf.write(0x00); sf.write(hostRecord.length + 8);
|
||||
sf.write(SF_TRANSFER_DATA);
|
||||
@@ -353,17 +350,19 @@ public class FTProtocolHardeningPhase16Test {
|
||||
|
||||
ftDft.processStructuredField(pkt, 0, pkt.length);
|
||||
|
||||
// Verify output has "HELLO" stripped of trailing spaces plus local line separator
|
||||
// Verify output has CR and ^Z stripped
|
||||
String result = new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
assertEquals("HELLO" + System.lineSeparator(), result);
|
||||
assertEquals("HELLO\n", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecfmVRdwUploadAndDownload() throws Exception {
|
||||
// 1. Upload RECFM V
|
||||
// 1. Upload RECFM V (stream CRLF-normalized, no binary RDW injection in ASCII mode)
|
||||
FTConfig sendConfig = new FTConfig();
|
||||
sendConfig.setDirection(FTConfig.Direction.SEND);
|
||||
sendConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
sendConfig.setCrFlag(true);
|
||||
sendConfig.setRemapFlag(false);
|
||||
sendConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||
listener.config = sendConfig;
|
||||
|
||||
@@ -378,37 +377,29 @@ public class FTProtocolHardeningPhase16Test {
|
||||
assertEquals(1, input.sentStructuredFields.size());
|
||||
byte[] reply = input.sentStructuredFields.get(0);
|
||||
int headerLen = 17;
|
||||
|
||||
// Verify 4-byte RDW header: length should be testLine.length() + 4
|
||||
int rdwLen = ((reply[headerLen] & 0xFF) << 8) | (reply[headerLen + 1] & 0xFF);
|
||||
assertEquals(testLine.length() + 4, rdwLen);
|
||||
assertEquals(0, reply[headerLen + 2]);
|
||||
assertEquals(0, reply[headerLen + 3]);
|
||||
String payload = new String(reply, headerLen + 4, testLine.length(), StandardCharsets.UTF_8);
|
||||
assertEquals(testLine, payload);
|
||||
String payload = new String(reply, headerLen, reply.length - headerLen, StandardCharsets.UTF_8);
|
||||
assertEquals(testLine + "\r\n", payload);
|
||||
|
||||
// 2. Download RECFM V
|
||||
FTConfig recvConfig = new FTConfig();
|
||||
recvConfig.setDirection(FTConfig.Direction.RECEIVE);
|
||||
recvConfig.setTransferMode(FTConfig.TransferMode.ASCII);
|
||||
recvConfig.setCrFlag(true);
|
||||
recvConfig.setRemapFlag(false);
|
||||
recvConfig.setRecfm(FTConfig.RecordFormat.VARIABLE);
|
||||
listener.config = recvConfig;
|
||||
|
||||
ByteArrayOutputStream downOut = new ByteArrayOutputStream();
|
||||
ftDft.initTransfer(new ByteArrayInputStream(new byte[0]), downOut);
|
||||
|
||||
// Host sends RDW header + "TEST V DATA"
|
||||
byte[] vData = "TEST V DATA".getBytes(StandardCharsets.UTF_8);
|
||||
int vRdwLen = vData.length + 4;
|
||||
byte[] vData = "TEST V DATA\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream sfDown = new ByteArrayOutputStream();
|
||||
sfDown.write(0x00); sfDown.write(0x00);
|
||||
sfDown.write(SF_TRANSFER_DATA);
|
||||
sfDown.write(0x47); sfDown.write(0x04);
|
||||
sfDown.write(TR_BEGIN_DATA);
|
||||
int fLen = vRdwLen + 5;
|
||||
int fLen = vData.length + 5;
|
||||
sfDown.write((fLen >> 8) & 0xFF); sfDown.write(fLen & 0xFF);
|
||||
sfDown.write((vRdwLen >> 8) & 0xFF); sfDown.write(vRdwLen & 0xFF);
|
||||
sfDown.write(0x00); sfDown.write(0x00);
|
||||
sfDown.write(vData);
|
||||
|
||||
byte[] downPkt = sfDown.toByteArray();
|
||||
@@ -418,7 +409,7 @@ public class FTProtocolHardeningPhase16Test {
|
||||
ftDft.processStructuredField(downPkt, 0, downPkt.length);
|
||||
|
||||
String downResult = new String(downOut.toByteArray(), StandardCharsets.UTF_8);
|
||||
assertEquals("TEST V DATA" + System.lineSeparator(), downResult);
|
||||
assertEquals("TEST V DATA\n", downResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+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..."
|
||||
$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.2.5`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||
[System.IO.File]::WriteAllText($libManifestFile, $libManifestText)
|
||||
|
||||
$libJarFile = Join-Path $BuildDir "lib3270j.jar"
|
||||
@@ -425,7 +425,7 @@ $libJarFile = Join-Path $BuildDir "lib3270j.jar"
|
||||
|
||||
Write-LogInfo "Packaging standalone executable JAR..."
|
||||
$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.2.5`r`nCreated-By: j3270 setup.ps1`r`n`r`n"
|
||||
[System.IO.File]::WriteAllText($manifestFile, $manifestText)
|
||||
|
||||
$jarFile = Join-Path $BuildDir "j3270.jar"
|
||||
|
||||
@@ -376,7 +376,7 @@ log_info "Packaging standalone lib3270j library JAR..."
|
||||
cat << 'EOF' > "$BUILD_DIR/LIB_MANIFEST.MF"
|
||||
Manifest-Version: 1.0
|
||||
Implementation-Title: lib3270j
|
||||
Implementation-Version: 1.2.4
|
||||
Implementation-Version: 1.2.5
|
||||
Created-By: j3270 setup.sh
|
||||
|
||||
EOF
|
||||
@@ -390,7 +390,7 @@ cat << 'EOF' > "$BUILD_DIR/MANIFEST.MF"
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: haus.nightmare.j3270.J3270App
|
||||
Implementation-Title: j3270
|
||||
Implementation-Version: 1.2.4
|
||||
Implementation-Version: 1.2.5
|
||||
Created-By: j3270 setup.sh
|
||||
|
||||
EOF
|
||||
|
||||
Reference in New Issue
Block a user