Patch memory hole cleaner, and bounce version
This commit is contained in:
@@ -70,7 +70,7 @@ public class ConnectionConfig {
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
// Framing buffer sanity limits (RFC 885 / RFC 2355 EOR protection)
|
||||
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 1048576; // 1MB (supports dense GDDM graphics, symbol sets, and DFT transfers)
|
||||
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)
|
||||
|
||||
@@ -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.3.0";
|
||||
|
||||
/**
|
||||
* 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.3.0")
|
||||
*/
|
||||
public static String getVersion() {
|
||||
Package pkg = Version.class.getPackage();
|
||||
|
||||
+45
-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
|
||||
@@ -1539,32 +1541,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);
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.Arrays;
|
||||
public class FastByteBuffer extends OutputStream {
|
||||
|
||||
public static final int DEFAULT_INITIAL_CAPACITY = 32768;
|
||||
public static final int DEFAULT_MAX_CAPACITY = 1048576; // 1MB (matches modern x3270 PDS_MAX for dense GDDM / GOCA vector graphics)
|
||||
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;
|
||||
|
||||
@@ -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,7 +54,7 @@ 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 = 1048576; // 1MB
|
||||
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 131072; // 128KB
|
||||
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
|
||||
public PrinterConfig() {
|
||||
|
||||
@@ -61,14 +61,16 @@ public class Telnet3270EP implements Runnable {
|
||||
private static final int MAX_PRINTER_SUBNEG_SIZE = 8192;
|
||||
private final int maxFramingBufferSize;
|
||||
private boolean recordDiscardMode = false;
|
||||
private final ByteArrayOutputStream recordBuffer;
|
||||
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 ByteArrayOutputStream(Math.min(32768, this.maxFramingBufferSize));
|
||||
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);
|
||||
@@ -79,7 +81,8 @@ 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 ByteArrayOutputStream(Math.min(32768, this.maxFramingBufferSize));
|
||||
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);
|
||||
@@ -384,21 +387,30 @@ 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) {
|
||||
if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
|
||||
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 {
|
||||
} else if (!subnegDiscardMode) {
|
||||
if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
|
||||
subnegBuffer.write(b);
|
||||
} else {
|
||||
log.warning("Printer subnegotiation exceeded " + MAX_PRINTER_SUBNEG_SIZE + " bytes; aborting SB");
|
||||
log.warning("Printer subnegotiation exceeded " + MAX_PRINTER_SUBNEG_SIZE + " bytes; discarding until IAC SE");
|
||||
subnegBuffer.reset();
|
||||
inSubnegotiation = false;
|
||||
subnegDiscardMode = true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -414,6 +426,7 @@ public class Telnet3270EP implements Runnable {
|
||||
break;
|
||||
case TelnetConstants.SB:
|
||||
inSubnegotiation = true;
|
||||
subnegDiscardMode = false;
|
||||
subnegBuffer.reset();
|
||||
break;
|
||||
case TelnetConstants.EOR:
|
||||
|
||||
@@ -54,6 +54,7 @@ public class TelnetFSM {
|
||||
|
||||
// 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];
|
||||
@@ -229,6 +230,7 @@ public class TelnetFSM {
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
recordDiscardMode = false;
|
||||
sbDiscardMode = false;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
|
||||
@@ -401,7 +403,13 @@ public class TelnetFSM {
|
||||
private void processIAC(int c) {
|
||||
switch (c) {
|
||||
case IAC: // Escaped IAC — literal 0xFF
|
||||
appendInboundByte(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
|
||||
@@ -419,6 +427,7 @@ public class TelnetFSM {
|
||||
case DONT: state = TNS_DONT; break;
|
||||
case SB:
|
||||
sbbuf.reset();
|
||||
sbDiscardMode = false;
|
||||
state = TNS_SB;
|
||||
break;
|
||||
case GA:
|
||||
@@ -585,14 +594,14 @@ public class TelnetFSM {
|
||||
private void processSB(int c) {
|
||||
if (c == IAC) {
|
||||
state = TNS_SB_IAC;
|
||||
} else {
|
||||
} 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 and aborting SB");
|
||||
" bytes without IAC SE; discarding until IAC SE");
|
||||
sbbuf.reset();
|
||||
state = TNS_DATA;
|
||||
sbDiscardMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,21 +611,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
|
||||
if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ 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: 1.3.0"), "setup.sh must set Implementation-Version: 1.3.0");
|
||||
|
||||
// 3. setup.ps1
|
||||
File setupPs1 = new File(projectRoot, "setup.ps1");
|
||||
@@ -74,7 +74,7 @@ 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: 1.3.0"), "setup.ps1 must set Implementation-Version: 1.3.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -112,12 +112,12 @@ 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");
|
||||
File m2ArtifactDir = new File(userHome, ".m2/repository/haus/nightmare/j3270/lib3270j/1.3.0");
|
||||
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-1.3.0.pom");
|
||||
File jarFile = new File(m2ArtifactDir, "lib3270j-1.3.0.jar");
|
||||
File sourcesFile = new File(m2ArtifactDir, "lib3270j-1.3.0-sources.jar");
|
||||
File javadocFile = new File(m2ArtifactDir, "lib3270j-1.3.0-javadoc.jar");
|
||||
|
||||
assertTrue(pomFile.exists() && pomFile.length() > 0, "POM file must exist and be non-empty");
|
||||
assertTrue(jarFile.exists() && jarFile.length() > 0, "JAR file must exist and be non-empty");
|
||||
@@ -127,7 +127,7 @@ public class Phase30LibraryDistributionTest {
|
||||
String pomXml = Files.readString(pomFile.toPath());
|
||||
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>1.3.0</version>"), "POM must contain version 1.3.0");
|
||||
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");
|
||||
|
||||
@@ -165,6 +165,7 @@ public class FastByteBufferTest {
|
||||
|
||||
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));
|
||||
@@ -180,10 +181,10 @@ public class FastByteBufferTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FastByteBuffer default maxCapacity is 1048576 bytes (1MB)")
|
||||
@DisplayName("FastByteBuffer default maxCapacity is 131072 bytes (128KB)")
|
||||
public void testDefaultMaxCapacity() {
|
||||
FastByteBuffer buf = new FastByteBuffer();
|
||||
assertEquals(FastByteBuffer.DEFAULT_MAX_CAPACITY, buf.getMaxCapacity());
|
||||
assertEquals(1048576, buf.getMaxCapacity());
|
||||
assertEquals(131072, buf.getMaxCapacity());
|
||||
}
|
||||
}
|
||||
|
||||
+30
-19
@@ -72,7 +72,7 @@ public class FramingBufferBoundedCapTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Framing buffer caps at 1MB and enters discard mode if stream lacks IAC EOR")
|
||||
@DisplayName("Framing buffer caps at 128KB and enters discard mode if stream lacks IAC EOR")
|
||||
public void testFramingBufferOverflowDiscardsUntilEor() {
|
||||
fsm.onConnected();
|
||||
|
||||
@@ -82,19 +82,19 @@ public class FramingBufferBoundedCapTest {
|
||||
feedBytes(IAC, DO, TELOPT_BINARY);
|
||||
feedBytes(IAC, WILL, TELOPT_BINARY);
|
||||
|
||||
assertEquals(1048576, fsm.getMaxFramingBufferSize(), "Default framing buffer cap should be 1MB (1048576 bytes)");
|
||||
assertEquals(131072, fsm.getMaxFramingBufferSize(), "Default framing buffer cap should be 128KB (131072 bytes)");
|
||||
assertFalse(fsm.isRecordDiscardMode());
|
||||
|
||||
// Feed chunks totaling over 1MB without any IAC EOR delimiter
|
||||
// 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 < 128; i++) {
|
||||
fsm.feedBytes(chunk, 0, chunk.length); // 128 * 8192 = 1,048,576 bytes
|
||||
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 1MB cap without IAC EOR
|
||||
// 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");
|
||||
@@ -156,45 +156,56 @@ public class FramingBufferBoundedCapTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Telnet subnegotiation buffer caps at 8KB if stream lacks IAC SE")
|
||||
@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[8200];
|
||||
byte[] largeSb = new byte[9000];
|
||||
Arrays.fill(largeSb, (byte) 'X');
|
||||
fsm.feedBytes(largeSb, 0, largeSb.length);
|
||||
|
||||
// Feed a regular byte: should be back in TNS_DATA, not accumulating in SB
|
||||
feedBytes(IAC, NOP);
|
||||
// Ensure no exception and session remains operational
|
||||
assertTrue(true);
|
||||
// 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 1MB and enters discard mode without IAC EOR")
|
||||
@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(1048576, pcfg.getMaxFramingBufferSize());
|
||||
assertEquals(131072, pcfg.getMaxFramingBufferSize());
|
||||
|
||||
Telnet3270EP printer = new Telnet3270EP(pcfg);
|
||||
assertEquals(1048576, printer.getMaxFramingBufferSize());
|
||||
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[32768];
|
||||
byte[] chunk = new byte[8192];
|
||||
Arrays.fill(chunk, (byte) 0x40);
|
||||
|
||||
for (int i = 0; i < 33; i++) {
|
||||
processIncoming.invoke(printer, chunk, 0, chunk.length); // 33 * 32768 = 1,081,344 bytes > 1,048,576
|
||||
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 1MB");
|
||||
assertTrue(printer.isRecordDiscardMode(), "Printer must enter discard mode when record exceeds 128KB");
|
||||
|
||||
// Send IAC EOR
|
||||
byte[] eor = new byte[] { (byte) IAC, (byte) EOR };
|
||||
|
||||
Reference in New Issue
Block a user