Patch memory hole cleaner, and bounce version
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 45s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 52s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m47s

This commit is contained in:
2026-10-19 00:42:44 +00:00
parent 6e682d3b5c
commit ac27c1ed5c
18 changed files with 183 additions and 98 deletions
@@ -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());
}
}
@@ -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 };