4 Commits

Author SHA1 Message Date
rudi 42eef519a6 Repo clean
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-22 13:17:31 +00:00
rudi b57d8f960d How had I not tested on Linux before
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 45s
2026-08-21 21:00:10 -04:00
rudi 09600260b5 Add some cleaning
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 37s
2026-08-21 18:56:37 -04:00
rudi 00c7b0770c Missed a folder!
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 46s
2026-08-21 16:35:18 -04:00
47 changed files with 350 additions and 20 deletions
+1
View File
@@ -1,4 +1,5 @@
j3270.log.*
build/*
*.log
.DS_Store
*.jar
+2 -8
View File
@@ -3,7 +3,7 @@
[![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions)
[![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases)
[![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net)
[![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE)
[![License](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
@@ -76,7 +76,7 @@ The project includes self-contained, portable build scripts:
# Build executable JAR in 2 seconds:
sh ./build_all.sh
# Run all 56 automated unit tests in ~500ms:
# Run all 57 automated unit tests in ~500ms:
sh ./test_all.sh
```
@@ -90,12 +90,6 @@ The resulting standalone JAR is created at `build/j3270.jar`.
---
## 🤝 Contributing
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
---
## 📜 Acknowledgements
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
+3
View File
@@ -8,3 +8,6 @@
- IND$FILE CMS and TSO:
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
- Local keyboard input and cursor updates not rendering in terminal:
Fixed in ScreenBuffer, InputProcessor, and TerminalPanel by ensuring display snapshot and cursor address are updated synchronously on user input operations (typing, backspace, delete, cursor movement, erase) so the presentation layer immediately renders user keystrokes.
-6
View File
@@ -1,6 +0,0 @@
Manifest-Version: 1.0
Main-Class: org.pubvm.j3270.J3270App
Implementation-Title: j3270
Implementation-Version: 0.1.0
Created-By: j3270 build_all.sh
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -190,6 +190,8 @@ public class TerminalPanel extends JPanel {
ScreenBuffer sb = client.getScreenBuffer();
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
clearSelection();
refreshScreen();
return;
}
}
repaint();
@@ -723,6 +725,9 @@ public class TerminalPanel extends JPanel {
}
private void refreshScreen() {
if (client != null) {
client.getScreenBuffer().updateDisplaySnapshot();
}
repaint();
// Notify parent to update status bar too
Container parent = getParent();
@@ -152,6 +152,7 @@ public class InputProcessor {
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/**
@@ -206,6 +207,7 @@ public class InputProcessor {
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
screen.setCursorAddress(nextRowAddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
setKeyboardLocked(false);
return;
}
@@ -309,6 +311,7 @@ public class InputProcessor {
addr -= screen.getCols();
if (addr < 0) addr += screen.getRows() * screen.getCols();
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorDown() {
@@ -316,18 +319,21 @@ public class InputProcessor {
addr += screen.getCols();
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorLeft() {
int addr = screen.getCursorAddress();
addr = screen.decrementAddress(addr);
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorRight() {
int addr = screen.getCursorAddress();
addr = screen.incrementAddress(addr);
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public void cursorHome() {
@@ -336,11 +342,13 @@ public class InputProcessor {
} else {
screen.setCursorAddress(0);
}
screen.updateDisplaySnapshot();
}
public void tab() {
int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
}
public int getLastAid() { return lastAid; }
@@ -348,6 +356,7 @@ public class InputProcessor {
public void setCursorAddress(int baddr) {
screen.setCursorAddress(baddr);
screen.updateDisplaySnapshot();
}
public void backTab() {
@@ -361,6 +370,7 @@ public class InputProcessor {
if (screen.getCell(addr).isFieldAttribute()) {
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
screen.setCursorAddress(screen.incrementAddress(addr));
screen.updateDisplaySnapshot();
return;
}
}
@@ -377,6 +387,7 @@ public class InputProcessor {
ea.ucs4 = 0;
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return;
}
int addr = screen.getCursorAddress();
@@ -398,6 +409,7 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
public void deleteChar() {
@@ -426,6 +438,7 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
public void backspace() {
@@ -439,6 +452,7 @@ public class InputProcessor {
if (keyboardLocked) return;
screen.eraseAllUnprotected();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
@@ -455,6 +469,7 @@ public class InputProcessor {
}
screen.setCursorAddress(target);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Insert Duplicate (DUP) code and advance to next field. */
@@ -472,6 +487,8 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
tab();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Insert Field Mark (FM) code. */
@@ -495,6 +512,7 @@ public class InputProcessor {
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/** Attention key (sends Telnet IP). */
@@ -571,6 +589,7 @@ public class InputProcessor {
screen.getCell(i).ucs4 = 0;
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return size;
}
@@ -608,6 +627,7 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return fieldLen;
}
@@ -60,6 +60,7 @@ public class ScreenBuffer {
defaultFA.ic = 1;
allocateBuffers();
updateDisplaySnapshot();
}
private void allocateBuffers() {
@@ -139,7 +140,7 @@ public class ScreenBuffer {
public boolean isScreenAlt() { return screenAlt; }
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
public void setAlternateDimensions(int newAltRows, int newAltCols) {
public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) {
if (newAltRows == altRows && newAltCols == altCols) return;
this.altRows = newAltRows;
this.altCols = newAltCols;
@@ -149,11 +150,15 @@ public class ScreenBuffer {
this.maxCols = Math.max(maxCols, newAltCols);
allocateBuffers();
}
updateDisplaySnapshot();
}
// ========== Cursor ==========
public int getCursorAddress() { return cursorAddress; }
public void setCursorAddress(int addr) { this.cursorAddress = addr; }
public synchronized void setCursorAddress(int addr) {
this.cursorAddress = addr;
this.displayCursorAddress = addr;
}
public int getCursorRow() { return cursorAddress / cols; }
public int getCursorCol() { return cursorAddress % cols; }
@@ -168,20 +173,22 @@ public class ScreenBuffer {
/**
* Perform an erase, optionally using the alternate screen size.
*/
public void erase(boolean alt) {
public synchronized void erase(boolean alt) {
clear();
int newRows = alt ? altRows : defRows;
int newCols = alt ? altCols : defCols;
if (alt == screenAlt && rows == newRows && cols == newCols) {
updateDisplaySnapshot();
return;
}
rows = newRows;
cols = newCols;
screenAlt = alt;
updateDisplaySnapshot();
}
/** Clear the entire buffer. */
public void clear() {
public synchronized void clear() {
for (ExtendedAttribute ea : buffer) {
ea.clear();
}
@@ -196,12 +203,13 @@ public class ScreenBuffer {
defaultCs = 0x00;
defaultIc = 0x00;
replyMode = SF_SRM_FIELD;
updateDisplaySnapshot();
}
/**
* Erase all unprotected fields.
*/
public void eraseAllUnprotected() {
public synchronized void eraseAllUnprotected() {
int size = rows * cols;
boolean inUnprotected = false;
@@ -229,6 +237,7 @@ public class ScreenBuffer {
// Move cursor to first unprotected field
cursorAddress = findNextUnprotected(0);
screenChanged = true;
updateDisplaySnapshot();
}
// ========== Field attribute navigation ==========
@@ -322,7 +331,7 @@ public class ScreenBuffer {
// ========== Setters for model reconfiguration ==========
public void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
int altRows, int altCols) {
this.maxRows = maxRows;
this.maxCols = maxCols;
@@ -333,6 +342,7 @@ public class ScreenBuffer {
this.rows = defRows;
this.cols = defCols;
allocateBuffers();
updateDisplaySnapshot();
}
public void translateToUnicode() {
@@ -44,10 +44,91 @@ public class InputProcessorTest {
assertEquals('A', screen.getCell(1).ucs4);
assertEquals('B', screen.getCell(2).ucs4);
assertEquals('A', screen.getDisplayCell(1).ucs4);
assertEquals('B', screen.getDisplayCell(2).ucs4);
input.eraseInput();
assertEquals(0, screen.getCell(1).ec);
assertEquals(0, screen.getCell(2).ec);
assertEquals(0, screen.getDisplayCell(1).ec);
assertEquals(0, screen.getDisplayCell(2).ec);
}
@Test
public void testTypeCharacterUpdatesDisplaySnapshot() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field starting at 1
screen.setCursorAddress(1);
screen.updateDisplaySnapshot(); // Initial snapshot
// Type "logon"
input.typeCharacter('l');
input.typeCharacter('o');
input.typeCharacter('g');
input.typeCharacter('o');
input.typeCharacter('n');
// Verify underlying buffer
assertEquals('l', screen.getCell(1).ucs4);
assertEquals('o', screen.getCell(2).ucs4);
assertEquals('g', screen.getCell(3).ucs4);
assertEquals('o', screen.getCell(4).ucs4);
assertEquals('n', screen.getCell(5).ucs4);
// Verify display snapshot (what TerminalPanel renders)
assertEquals('l', screen.getDisplayCell(1).ucs4);
assertEquals('o', screen.getDisplayCell(2).ucs4);
assertEquals('g', screen.getDisplayCell(3).ucs4);
assertEquals('o', screen.getDisplayCell(4).ucs4);
assertEquals('n', screen.getDisplayCell(5).ucs4);
assertEquals(6, screen.getDisplayCursorAddress());
assertEquals(6, screen.getCursorAddress());
}
@Test
public void testCursorMovementUpdatesDisplayCursorAddress() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCursorAddress(10);
assertEquals(10, screen.getDisplayCursorAddress());
input.cursorRight();
assertEquals(11, screen.getDisplayCursorAddress());
input.cursorLeft();
assertEquals(10, screen.getDisplayCursorAddress());
screen.setCursorAddress(45);
assertEquals(45, screen.getDisplayCursorAddress());
}
@Test
public void testDeleteAndEraseEofUpdateDisplaySnapshot() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE);
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(1);
input.typeCharacter('A');
input.typeCharacter('B');
input.typeCharacter('C');
assertEquals('A', screen.getDisplayCell(1).ucs4);
assertEquals('B', screen.getDisplayCell(2).ucs4);
assertEquals('C', screen.getDisplayCell(3).ucs4);
// Backspace over 'C'
input.backspace();
assertEquals(3, screen.getCursorAddress());
assertEquals(3, screen.getDisplayCursorAddress());
assertEquals(0, screen.getDisplayCell(3).ucs4);
// Erase EOF from position 2 ('B')
screen.setCursorAddress(2);
input.eraseEof();
assertEquals(0, screen.getDisplayCell(2).ucs4);
assertEquals('A', screen.getDisplayCell(1).ucs4);
}
}
@@ -0,0 +1,222 @@
package org.lib3270j.telnet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lib3270j.ConnectionConfig;
import org.lib3270j.ConnectionState;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.protocol.TelnetConstants;
import org.lib3270j.screen.ScreenBuffer;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class TelnetFSMTest {
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);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
for (int b : bytes) {
fsm.feedByte(b & 0xFF);
}
}
@Test
public void testHostPrefixParsingPlainTn3270() {
ConnectionConfig c1 = ConnectionConfig.parseHostString("P:mainframe.example.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c1.isTn3270eEnabled(), "P: prefix should disable TN3270E");
assertFalse(c1.isUseTls());
assertEquals("mainframe.example.com", c1.getHost());
ConnectionConfig c2 = ConnectionConfig.parseHostString("plain:zos.net:2323", 23, TerminalModel.IBM_3279_4);
assertFalse(c2.isTn3270eEnabled());
assertEquals("zos.net", c2.getHost());
assertEquals(2323, c2.getPort());
ConnectionConfig c3 = ConnectionConfig.parseHostString("L:P:secure.mainframe.com:992", 0, TerminalModel.IBM_3279_4);
assertTrue(c3.isUseTls(), "L: should enable TLS");
assertFalse(c3.isTn3270eEnabled(), "P: should disable TN3270E");
assertEquals("secure.mainframe.com", c3.getHost());
assertEquals(992, c3.getPort());
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.ibm.com", c4.getHost());
}
@Test
public void testPlainTn3270NegotiationRejectsTn3270E() {
config.setTn3270eEnabled(false);
fsm.onConnected();
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
// Host offers DO TN3270E -> client must reply WONT TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
assertFalse(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host offers WILL TN3270E -> client must reply DONT TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
assertFalse(fsm.getHisOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host negotiates standard 3270 options: DO BINARY, WILL BINARY, DO EOR, WILL EOR, DO TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// Client should have transitioned to CONNECTED_3270
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
// Host asks for TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
// Verify sent packets contain WONT TN3270E, DONT TN3270E, and TTYPE IS
boolean foundWontTn3270e = false;
boolean foundTtypeIs = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.WONT && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E) {
foundWontTn3270e = true;
}
if (pkt.length >= 4 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TTYPE) {
foundTtypeIs = true;
}
}
assertTrue(foundWontTn3270e, "Expected WONT TN3270E response");
assertTrue(foundTtypeIs, "Expected TTYPE IS response");
}
@Test
public void testTn3270eFunctionsNegotiationWithoutBindImageTransitionsToConnected() {
config.setTn3270eEnabled(true);
fsm.onConnected();
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
// Host offers DO TN3270E -> client accepts with WILL TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
assertTrue(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host responds with DEVICE-TYPE IS IBM-3279-4-E CONNECT LU01
byte[] devTypeIs = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, // OP_DEVICE_TYPE, OP_IS
'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
0x01, // OP_CONNECT
'L', 'U', '0', '1',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
// Host responds with FUNCTIONS REQUEST <empty> (e.g. pytn3270 / RFC 2355 server)
byte[] funcsReq = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x03, 0x07, // OP_FUNCTIONS, OP_REQUEST
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Per RFC 2355: Client must reply with FUNCTIONS IS, and without BIND-IMAGE, session is bound immediately
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(),
"Expected CONNECTED_TN3270E when BIND-IMAGE is not negotiated");
// Verify that client sent SB TN3270E FUNCTIONS IS SE
boolean foundFunctionsIs = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length >= 5 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E &&
(pkt[3] & 0xFF) == 0x03 && (pkt[4] & 0xFF) == 0x04) {
foundFunctionsIs = true;
}
}
assertTrue(foundFunctionsIs, "Expected FUNCTIONS IS reply from client upon receiving FUNCTIONS REQUEST");
}
@Test
public void testPlainDataStreamFallbackInTn3270eMode() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E with empty functions
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
byte[] devTypeIs = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x03, 0x07, (byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// Now host sends raw 3270 data stream without 5-byte TN3270E header (e.g. EraseWrite 0xF5)
// 0xF5 = EraseWrite, 0xC3 = WCC, 0x11 = SBA, 0x40 0x40 = Pos 0, "HELLO"
EbcdicTranslator trans = new EbcdicTranslator();
String msg = "HELLO";
byte[] rawStream = new byte[3 + 2 + msg.length() + 2];
rawStream[0] = (byte) 0xF5; // EraseWrite
rawStream[1] = (byte) 0xC3; // WCC
rawStream[2] = 0x11; // SBA
rawStream[3] = 0x40; rawStream[4] = 0x40; // addr 0
for (int i = 0; i < msg.length(); i++) {
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
}
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
// State must have automatically switched to CONNECTED_3270 and screen updated
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
assertEquals('H', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('E', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4)));
}
}