Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42eef519a6 | |||
|
b57d8f960d
|
|||
|
09600260b5
|
|||
|
00c7b0770c
|
@@ -1,4 +1,5 @@
|
|||||||
j3270.log.*
|
j3270.log.*
|
||||||
|
build/*
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.jar
|
*.jar
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||||
[](https://adoptium.net)
|
[](https://adoptium.net)
|
||||||
[](LICENSE)
|
[](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.
|
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:
|
# Build executable JAR in 2 seconds:
|
||||||
sh ./build_all.sh
|
sh ./build_all.sh
|
||||||
|
|
||||||
# Run all 56 automated unit tests in ~500ms:
|
# Run all 57 automated unit tests in ~500ms:
|
||||||
sh ./test_all.sh
|
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
|
## 📜 Acknowledgements
|
||||||
|
|
||||||
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
||||||
|
|||||||
@@ -8,3 +8,6 @@
|
|||||||
- IND$FILE CMS and TSO:
|
- 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
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();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repaint();
|
repaint();
|
||||||
@@ -723,6 +725,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshScreen() {
|
private void refreshScreen() {
|
||||||
|
if (client != null) {
|
||||||
|
client.getScreenBuffer().updateDisplaySnapshot();
|
||||||
|
}
|
||||||
repaint();
|
repaint();
|
||||||
// Notify parent to update status bar too
|
// Notify parent to update status bar too
|
||||||
Container parent = getParent();
|
Container parent = getParent();
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,6 +207,7 @@ public class InputProcessor {
|
|||||||
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
||||||
screen.setCursorAddress(nextRowAddr);
|
screen.setCursorAddress(nextRowAddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
setKeyboardLocked(false);
|
setKeyboardLocked(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -309,6 +311,7 @@ public class InputProcessor {
|
|||||||
addr -= screen.getCols();
|
addr -= screen.getCols();
|
||||||
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorDown() {
|
public void cursorDown() {
|
||||||
@@ -316,18 +319,21 @@ public class InputProcessor {
|
|||||||
addr += screen.getCols();
|
addr += screen.getCols();
|
||||||
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorLeft() {
|
public void cursorLeft() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr = screen.decrementAddress(addr);
|
addr = screen.decrementAddress(addr);
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorRight() {
|
public void cursorRight() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr = screen.incrementAddress(addr);
|
addr = screen.incrementAddress(addr);
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorHome() {
|
public void cursorHome() {
|
||||||
@@ -336,11 +342,13 @@ public class InputProcessor {
|
|||||||
} else {
|
} else {
|
||||||
screen.setCursorAddress(0);
|
screen.setCursorAddress(0);
|
||||||
}
|
}
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tab() {
|
public void tab() {
|
||||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLastAid() { return lastAid; }
|
public int getLastAid() { return lastAid; }
|
||||||
@@ -348,6 +356,7 @@ public class InputProcessor {
|
|||||||
|
|
||||||
public void setCursorAddress(int baddr) {
|
public void setCursorAddress(int baddr) {
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backTab() {
|
public void backTab() {
|
||||||
@@ -361,6 +370,7 @@ public class InputProcessor {
|
|||||||
if (screen.getCell(addr).isFieldAttribute()) {
|
if (screen.getCell(addr).isFieldAttribute()) {
|
||||||
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
||||||
screen.setCursorAddress(screen.incrementAddress(addr));
|
screen.setCursorAddress(screen.incrementAddress(addr));
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,6 +387,7 @@ public class InputProcessor {
|
|||||||
ea.ucs4 = 0;
|
ea.ucs4 = 0;
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
@@ -398,6 +409,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteChar() {
|
public void deleteChar() {
|
||||||
@@ -426,6 +438,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backspace() {
|
public void backspace() {
|
||||||
@@ -439,6 +452,7 @@ public class InputProcessor {
|
|||||||
if (keyboardLocked) return;
|
if (keyboardLocked) return;
|
||||||
screen.eraseAllUnprotected();
|
screen.eraseAllUnprotected();
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
||||||
@@ -455,6 +469,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(target);
|
screen.setCursorAddress(target);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert Duplicate (DUP) code and advance to next field. */
|
/** 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);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
tab();
|
tab();
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert Field Mark (FM) code. */
|
/** Insert Field Mark (FM) code. */
|
||||||
@@ -495,6 +512,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attention key (sends Telnet IP). */
|
/** Attention key (sends Telnet IP). */
|
||||||
@@ -571,6 +589,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(i).ucs4 = 0;
|
screen.getCell(i).ucs4 = 0;
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +627,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return fieldLen;
|
return fieldLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ public class ScreenBuffer {
|
|||||||
defaultFA.ic = 1;
|
defaultFA.ic = 1;
|
||||||
|
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allocateBuffers() {
|
private void allocateBuffers() {
|
||||||
@@ -139,7 +140,7 @@ public class ScreenBuffer {
|
|||||||
public boolean isScreenAlt() { return screenAlt; }
|
public boolean isScreenAlt() { return screenAlt; }
|
||||||
|
|
||||||
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
|
/** 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;
|
if (newAltRows == altRows && newAltCols == altCols) return;
|
||||||
this.altRows = newAltRows;
|
this.altRows = newAltRows;
|
||||||
this.altCols = newAltCols;
|
this.altCols = newAltCols;
|
||||||
@@ -149,11 +150,15 @@ public class ScreenBuffer {
|
|||||||
this.maxCols = Math.max(maxCols, newAltCols);
|
this.maxCols = Math.max(maxCols, newAltCols);
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
}
|
}
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Cursor ==========
|
// ========== Cursor ==========
|
||||||
public int getCursorAddress() { return cursorAddress; }
|
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 getCursorRow() { return cursorAddress / cols; }
|
||||||
public int getCursorCol() { 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.
|
* Perform an erase, optionally using the alternate screen size.
|
||||||
*/
|
*/
|
||||||
public void erase(boolean alt) {
|
public synchronized void erase(boolean alt) {
|
||||||
clear();
|
clear();
|
||||||
int newRows = alt ? altRows : defRows;
|
int newRows = alt ? altRows : defRows;
|
||||||
int newCols = alt ? altCols : defCols;
|
int newCols = alt ? altCols : defCols;
|
||||||
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
||||||
|
updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rows = newRows;
|
rows = newRows;
|
||||||
cols = newCols;
|
cols = newCols;
|
||||||
screenAlt = alt;
|
screenAlt = alt;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the entire buffer. */
|
/** Clear the entire buffer. */
|
||||||
public void clear() {
|
public synchronized void clear() {
|
||||||
for (ExtendedAttribute ea : buffer) {
|
for (ExtendedAttribute ea : buffer) {
|
||||||
ea.clear();
|
ea.clear();
|
||||||
}
|
}
|
||||||
@@ -196,12 +203,13 @@ public class ScreenBuffer {
|
|||||||
defaultCs = 0x00;
|
defaultCs = 0x00;
|
||||||
defaultIc = 0x00;
|
defaultIc = 0x00;
|
||||||
replyMode = SF_SRM_FIELD;
|
replyMode = SF_SRM_FIELD;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erase all unprotected fields.
|
* Erase all unprotected fields.
|
||||||
*/
|
*/
|
||||||
public void eraseAllUnprotected() {
|
public synchronized void eraseAllUnprotected() {
|
||||||
int size = rows * cols;
|
int size = rows * cols;
|
||||||
boolean inUnprotected = false;
|
boolean inUnprotected = false;
|
||||||
|
|
||||||
@@ -229,6 +237,7 @@ public class ScreenBuffer {
|
|||||||
// Move cursor to first unprotected field
|
// Move cursor to first unprotected field
|
||||||
cursorAddress = findNextUnprotected(0);
|
cursorAddress = findNextUnprotected(0);
|
||||||
screenChanged = true;
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Field attribute navigation ==========
|
// ========== Field attribute navigation ==========
|
||||||
@@ -322,7 +331,7 @@ public class ScreenBuffer {
|
|||||||
|
|
||||||
// ========== Setters for model reconfiguration ==========
|
// ========== 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) {
|
int altRows, int altCols) {
|
||||||
this.maxRows = maxRows;
|
this.maxRows = maxRows;
|
||||||
this.maxCols = maxCols;
|
this.maxCols = maxCols;
|
||||||
@@ -333,6 +342,7 @@ public class ScreenBuffer {
|
|||||||
this.rows = defRows;
|
this.rows = defRows;
|
||||||
this.cols = defCols;
|
this.cols = defCols;
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void translateToUnicode() {
|
public void translateToUnicode() {
|
||||||
|
|||||||
@@ -44,10 +44,91 @@ public class InputProcessorTest {
|
|||||||
|
|
||||||
assertEquals('A', screen.getCell(1).ucs4);
|
assertEquals('A', screen.getCell(1).ucs4);
|
||||||
assertEquals('B', screen.getCell(2).ucs4);
|
assertEquals('B', screen.getCell(2).ucs4);
|
||||||
|
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||||
|
assertEquals('B', screen.getDisplayCell(2).ucs4);
|
||||||
|
|
||||||
input.eraseInput();
|
input.eraseInput();
|
||||||
|
|
||||||
assertEquals(0, screen.getCell(1).ec);
|
assertEquals(0, screen.getCell(1).ec);
|
||||||
assertEquals(0, screen.getCell(2).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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user