This commit is contained in:
2026-08-28 13:00:12 -04:00
parent 79258c0c44
commit 037ad1f941
13 changed files with 1720 additions and 155 deletions
@@ -0,0 +1,138 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class DataStreamProcessorPhase2Test {
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
haus.nightmare.lib3270j.charset.EbcdicTranslator translator = new haus.nightmare.lib3270j.charset.EbcdicTranslator();
screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_2, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testDoubleFFAndCountFF() {
byte[] raw = new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF, (byte) 0xFF, 0x03 };
assertEquals(3, DataStreamProcessor.countFF(raw, raw.length));
byte[] doubled = DataStreamProcessor.doubleFF(raw, raw.length);
assertEquals(9, doubled.length);
assertEquals(0x01, doubled[0]);
assertEquals((byte) 0xFF, doubled[1]);
assertEquals((byte) 0xFF, doubled[2]);
assertEquals(0x02, doubled[3]);
assertEquals((byte) 0xFF, doubled[4]);
assertEquals((byte) 0xFF, doubled[5]);
assertEquals((byte) 0xFF, doubled[6]);
assertEquals((byte) 0xFF, doubled[7]);
assertEquals(0x03, doubled[8]);
}
@Test
public void testFastX2BinAndCmd2Ebc() {
// Test x2bin 6-bit decode
assertEquals(0, DataStreamProcessor.x2bin(0x40)); // space = 0
assertEquals(1, DataStreamProcessor.x2bin(0xC1)); // 'A' = 1
assertEquals(2, DataStreamProcessor.x2bin(0xC2)); // 'B' = 2
// Test cmd2ebc
assertEquals(SNA_CMD_W, DataStreamProcessor.cmd2ebc(CMD_W));
assertEquals(SNA_CMD_EW, DataStreamProcessor.cmd2ebc(CMD_EW));
assertEquals(SNA_CMD_WSF, DataStreamProcessor.cmd2ebc(CMD_WSF));
}
@Test
public void testExtendedAttributePlanes() {
// SFE with FA + Outlining + Validation + FG Color
byte[] sfeRecord = new byte[] {
(byte) CMD_EW, 0x00, // EW with null WCC
(byte) ORDER_SFE, 0x04, // 4 pairs
(byte) XA_3270, (byte) FA_PRINTABLE,
(byte) XA_OUTLINING, (byte) 0x0F, // Full box outline
(byte) XA_VALIDATION, (byte) 0x01,
(byte) XA_FOREGROUND, (byte) 0xF2 // Red
};
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
ExtendedAttribute cell = screen.getCell(0);
assertTrue(cell.isFieldAttribute());
assertEquals((byte) 0x0F, cell.ol);
assertEquals((byte) 0x01, cell.vl);
assertEquals((byte) 0xF2, cell.fg);
}
@Test
public void testReadBufferInExtendedFieldMode() {
// Set up field with FG color
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).fg = (byte) 0xF4; // Green
screen.setReplyMode((byte) SF_SRM_XFIELD);
// Execute Read Buffer command (0xF2)
byte[] rbRecord = new byte[] { (byte) CMD_RB };
processor.processRecord(rbRecord, 0, rbRecord.length, false);
byte[] sent = output.toByteArray();
assertTrue(sent.length > 5);
assertEquals(AID_NO, sent[0] & 0xFF);
// At position 0, response should contain ORDER_SFE (0x29) instead of ORDER_SF (0x1D)
assertEquals(ORDER_SFE, sent[3] & 0xFF);
assertEquals(2, sent[4] & 0xFF); // 2 pairs (3270 FA and FG)
assertEquals(XA_3270, sent[5] & 0xFF);
assertEquals(FA_PRINTABLE, sent[6] & 0xFF);
assertEquals(XA_FOREGROUND, sent[7] & 0xFF);
assertEquals(0xF4, sent[8] & 0xFF);
}
@Test
public void testStructuredFieldPartitions() {
// WSF Create Partition (pid = 1, 32 rows, 80 cols)
byte[] createPart = new byte[] {
(byte) CMD_WSF,
0x00, 0x08, // Length = 8
(byte) SF_CREATE_PART, 0x01, // PID = 1
0x00, 80, // Cols = 80
0x00, 32 // Rows = 32
};
processor.processRecord(createPart, 0, createPart.length, false);
assertEquals(1, screen.getActivePartition());
assertTrue(screen.isExplicitPartitionActive());
// WSF Activate Partition 0
byte[] activatePart = new byte[] {
(byte) CMD_WSF,
0x00, 0x04,
(byte) SF_ACTIVATE_PART, 0x00
};
processor.processRecord(activatePart, 0, activatePart.length, false);
assertEquals(0, screen.getActivePartition());
assertFalse(screen.isExplicitPartitionActive());
// WSF Destroy Partition 1
byte[] destroyPart = new byte[] {
(byte) CMD_WSF,
0x00, 0x04,
(byte) SF_DESTROY_PART, 0x01
};
processor.processRecord(destroyPart, 0, destroyPart.length, false);
}
}
@@ -0,0 +1,141 @@
package haus.nightmare.lib3270j.nvt;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
public class NvtProcessorTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private NvtProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_2, translator);
processor = new NvtProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testPlainTextProcessing() {
byte[] text = "HELLO 3270".getBytes();
processor.processNVTData(text, 0, text.length);
assertEquals(10, screen.getCursorAddress());
assertEquals('H', screen.getCell(0).ucs4);
assertEquals('E', screen.getCell(1).ucs4);
assertEquals('L', screen.getCell(2).ucs4);
assertEquals('L', screen.getCell(3).ucs4);
assertEquals('O', screen.getCell(4).ucs4);
assertEquals(' ', screen.getCell(5).ucs4);
assertEquals('3', screen.getCell(6).ucs4);
assertEquals('2', screen.getCell(7).ucs4);
assertEquals('7', screen.getCell(8).ucs4);
assertEquals('0', screen.getCell(9).ucs4);
}
@Test
public void testCursorMovementAnsiSequences() {
// CUP: ESC [ 5 ; 10 H (Row 5, Col 10 => 1-indexed, so row 4, col 9)
byte[] cup = "\u001B[5;10H".getBytes();
processor.processNVTData(cup, 0, cup.length);
assertEquals(4 * 80 + 9, screen.getCursorAddress());
// CUU: ESC [ 2 A (Up 2 rows => row 2, col 9)
byte[] cuu = "\u001B[2A".getBytes();
processor.processNVTData(cuu, 0, cuu.length);
assertEquals(2 * 80 + 9, screen.getCursorAddress());
// CUD: ESC [ 3 B (Down 3 rows => row 5, col 9)
byte[] cud = "\u001B[3B".getBytes();
processor.processNVTData(cud, 0, cud.length);
assertEquals(5 * 80 + 9, screen.getCursorAddress());
// CUB: ESC [ 4 D (Left 4 cols => row 5, col 5)
byte[] cub = "\u001B[4D".getBytes();
processor.processNVTData(cub, 0, cub.length);
assertEquals(5 * 80 + 5, screen.getCursorAddress());
// CUF: ESC [ 10 C (Right 10 cols => row 5, col 15)
byte[] cuf = "\u001B[10C".getBytes();
processor.processNVTData(cuf, 0, cuf.length);
assertEquals(5 * 80 + 15, screen.getCursorAddress());
}
@Test
public void testSgrColorAndAttributes() {
// ESC [ 1 ; 4 ; 31 ; 42 m (Bold, Underline, Red FG, Green BG)
byte[] sgr = "\u001B[1;4;31;42mX".getBytes();
processor.processNVTData(sgr, 0, sgr.length);
ExtendedAttribute cell = screen.getCell(0);
assertEquals('X', cell.ucs4);
assertEquals((byte) haus.nightmare.lib3270j.protocol.DS3270Constants.HOST_COLOR_RED, cell.fg);
assertEquals((byte) haus.nightmare.lib3270j.protocol.DS3270Constants.HOST_COLOR_GREEN, cell.bg);
assertTrue((cell.gr & 0x08) != 0); // GR_INTENSIFY
assertTrue((cell.gr & 0x04) != 0); // GR_UNDERLINE
}
@Test
public void testEraseInDisplay() {
byte[] initial = "ABCDEFGHIJ".getBytes();
processor.processNVTData(initial, 0, initial.length);
assertEquals('A', screen.getCell(0).ucs4);
// ESC [ 2 J (Clear entire display)
byte[] ed2 = "\u001B[2J".getBytes();
processor.processNVTData(ed2, 0, ed2.length);
for (int i = 0; i < 10; i++) {
assertEquals(0, screen.getCell(i).ucs4);
}
}
@Test
public void testSaveAndRestoreCursor() {
byte[] move = "\u001B[10;20H".getBytes();
processor.processNVTData(move, 0, move.length);
int savedAddr = screen.getCursorAddress();
// DECSC: ESC 7 (save cursor)
byte[] save = "\u001B7".getBytes();
processor.processNVTData(save, 0, save.length);
// Move elsewhere
byte[] move2 = "\u001B[1;1H".getBytes();
processor.processNVTData(move2, 0, move2.length);
assertEquals(0, screen.getCursorAddress());
// DECRC: ESC 8 (restore cursor)
byte[] restore = "\u001B8".getBytes();
processor.processNVTData(restore, 0, restore.length);
assertEquals(savedAddr, screen.getCursorAddress());
}
@Test
public void testSendNvtMethods() throws IOException {
processor.sendNVTString("LOGIN\n");
byte[] sent = output.toByteArray();
assertTrue(sent.length >= 6);
assertEquals('L', (char) sent[0]);
assertEquals('O', (char) sent[1]);
assertEquals('G', (char) sent[2]);
assertEquals('I', (char) sent[3]);
assertEquals('N', (char) sent[4]);
// Normalized newline to CRLF
assertEquals('\r', (char) sent[5]);
assertEquals('\n', (char) sent[6]);
}
}
@@ -0,0 +1,170 @@
package haus.nightmare.lib3270j.telnet;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.List;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TelnetFSMPhase2Test {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private TelnetFSM fsm;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
config = new ConnectionConfig("test.host", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
dsProcessor = new DataStreamProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
output = new ByteArrayOutputStream();
// Create a mock TelnetConnection
TelnetConnection connection = new TelnetConnection(config, fsm) {
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
output.write(data, offset, length);
}
};
fsm.setConnection(connection);
}
@Test
public void testSnaBindImageParsingSSIZE03() {
fsm.onConnected();
// Craft SNA BIND RU: 5-byte TN3270E header (DT_BIND_IMAGE) + 30-byte BIND image
byte[] bindData = new byte[EH_SIZE + 30];
bindData[0] = (byte) DT_BIND_IMAGE;
bindData[1] = 0;
bindData[2] = (byte) RSF_ALWAYS_RESPONSE;
bindData[3] = 0;
bindData[4] = 1; // Seq = 1
// SSIZE at offset 24 = 0x03 (default 24x80, alt = model max 43x80)
bindData[EH_SIZE + 20] = 24; // RD
bindData[EH_SIZE + 21] = 80; // CD
bindData[EH_SIZE + 24] = 0x03; // SSIZE
fsm.process_bind(bindData, RSF_ALWAYS_RESPONSE, 1);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
assertTrue(fsm.checkSessionBound());
assertEquals(43, screenBuffer.getAltRows());
assertEquals(80, screenBuffer.getAltCols());
// Verify positive response was sent
byte[] sent = output.toByteArray();
assertTrue(sent.length >= EH_SIZE + 1);
assertEquals((byte) DT_RESPONSE, sent[0]);
assertEquals((byte) RSF_POSITIVE_RESPONSE, sent[2]);
assertEquals((byte) POS_DEVICE_END, sent[5]);
}
@Test
public void testSnaBindImageParsingSSIZE7F() {
fsm.onConnected();
byte[] bindData = new byte[EH_SIZE + 30];
bindData[0] = (byte) DT_BIND_IMAGE;
bindData[1] = 0;
bindData[2] = (byte) RSF_NO_RESPONSE;
bindData[3] = 0;
bindData[4] = 2;
// SSIZE at offset 24 = 0x7F (separate default and alternate: 32x80 alt)
bindData[EH_SIZE + 20] = 24; // RD
bindData[EH_SIZE + 21] = 80; // CD
bindData[EH_SIZE + 22] = 32; // RA
bindData[EH_SIZE + 23] = 80; // CA
bindData[EH_SIZE + 24] = 0x7F; // SSIZE
fsm.process_bind(bindData, RSF_NO_RESPONSE, 2);
assertEquals(32, screenBuffer.getAltRows());
assertEquals(80, screenBuffer.getAltCols());
}
@Test
public void testSnaUnbindTransitionsToUnboundAndClearsScreen() {
fsm.onConnected();
screenBuffer.getCell(0).ucs4 = 'X';
fsm.process_unbind(UNBIND_NORMAL, RSF_ALWAYS_RESPONSE, 10);
assertEquals(ConnectionState.CONNECTED_UNBOUND, fsm.getConnectionState());
assertFalse(fsm.checkSessionBound());
assertEquals(0, screenBuffer.getCell(0).ucs4); // Cleared
}
@Test
public void testSnaDfcClearAndSignal() {
fsm.onConnected();
screenBuffer.getCell(5).ucs4 = 'Z';
fsm.process_DFC(DFC_CLEAR, RSF_NO_RESPONSE, 5);
assertEquals(0, screenBuffer.getCell(5).ucs4); // Cleared
boolean[] alarmHeard = new boolean[1];
fsm.addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() {}
@Override public void onScreenSizeChanged(int rows, int cols) {}
@Override public void onSoundAlarm() { alarmHeard[0] = true; }
});
fsm.process_DFC(DFC_SIGNAL, RSF_NO_RESPONSE, 6);
assertTrue(alarmHeard[0]);
}
@Test
public void testDeviceNamePoolManagement() {
fsm.addDeviceName("POOL1", "LU001", -1);
fsm.addDeviceName("POOL1", "LU002", -1);
fsm.addDeviceName("POOL1", "LU000", 0);
assertEquals("LU000", fsm.getDeviceName("POOL1", 0));
assertEquals("LU001", fsm.getDeviceName("POOL1", 1));
assertEquals("LU002", fsm.getDeviceName("POOL1", 2));
fsm.removeDeviceName("POOL1", "LU001", -1);
assertEquals("LU002", fsm.getDeviceName("POOL1", 1));
List<String> poolList = fsm.getDevicePoolList("POOL1");
assertEquals(2, poolList.size());
}
@Test
public void testLuPoolCyclingOnReject() {
config.setLuNames(Arrays.asList("LU_A", "LU_B", "LU_C"));
fsm.onConnected();
output.reset();
// Server sends DO TN3270E -> client sends WILL TN3270E and requests "LU_A"
fsm.feedBytes(new byte[] { (byte) IAC, (byte) DO, (byte) TELOPT_TN3270E }, 0, 3);
String sent1 = new String(output.toByteArray());
assertTrue(sent1.contains("LU_A"));
output.reset();
// Server rejects device type with REASON_DEVICE_IN_USE
fsm.feedBytes(new byte[] { (byte) IAC, (byte) SB, (byte) TELOPT_TN3270E, (byte) OP_DEVICE_TYPE, (byte) OP_REJECT, (byte) OP_REASON, (byte) REASON_DEVICE_IN_USE, (byte) IAC, (byte) SE }, 0, 9);
// Client should have automatically retried with next LU ("LU_B")
String sent2 = new String(output.toByteArray());
assertTrue(sent2.contains("LU_B"));
}
}