GDDM Tweaking

This commit is contained in:
2026-08-27 17:05:23 +00:00
parent ebc0b5ae6f
commit 540a8dfd1d
13 changed files with 886 additions and 138 deletions
@@ -0,0 +1,190 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
public class FTDftTest {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private InputProcessor input;
private TestListener listener;
private FTDft dft;
private FTConfig config;
private static class TestListener implements FTDft.FTDftListener {
boolean dftRunningCalled = false;
boolean completeCalled = false;
boolean abortCalled = false;
String completeMsg = null;
String abortMsg = null;
long bytesTransferred = 0;
FTConstants.FTState state = FTConstants.FTState.RUNNING;
FTConfig config;
@Override
public void onDftRunning() {
dftRunningCalled = true;
}
@Override
public void onTransferComplete(String errorMessage) {
completeCalled = true;
completeMsg = errorMessage;
}
@Override
public void onTransferAborted(String errorMessage) {
abortCalled = true;
abortMsg = errorMessage;
}
@Override
public void onBytesTransferred(long bytes) {
bytesTransferred = bytes;
}
@Override
public FTConstants.FTState getCurrentState() {
return state;
}
@Override
public void setState(FTConstants.FTState state) {
this.state = state;
}
@Override
public FTConfig getConfig() {
return config;
}
@Override
public File getLocalFile() {
return null;
}
}
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
input = new InputProcessor(screen, translator, null);
listener = new TestListener();
config = new FTConfig();
config.setDirection(FTConfig.Direction.RECEIVE);
config.setTransferMode(FTConfig.TransferMode.BINARY);
listener.config = config;
dft = new FTDft(input, translator, listener);
}
@Test
public void testOpenMessageStreamAsciiAndHandleTrans14Error() {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream);
// 1. Simulate TR_OPEN_REQ with ASCII "FT:MSG" matching j3270.log:
// SF length = 0x0023, SF type = 0xD0, Request code = 0x0012, stream name "FT:MSG " at offset 28
byte[] openReq = new byte[] {
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // "FT:MSG "
};
dft.processStructuredField(openReq, 0, openReq.length);
// Should NOT trigger onDftRunning because it's a message stream
assertFalse(listener.dftRunningCalled);
// 2. Simulate TR_DATA_INSERT on the message stream with "TRANS14 Error reading file from host: ..."
String errorMsg = "TRANS14 Error reading file from host: file format invalid";
byte[] msgBytes = errorMsg.getBytes(StandardCharsets.ISO_8859_1);
int totalPayloadLen = msgBytes.length + 5;
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
sfOut.write(0); sfOut.write(0); // placeholder length
sfOut.write(0xD0); // SF_TRANSFER_DATA
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
sfOut.write(FTConstants.TR_BEGIN_DATA);
sfOut.write((totalPayloadLen >> 8) & 0xFF);
sfOut.write(totalPayloadLen & 0xFF);
sfOut.write(msgBytes, 0, msgBytes.length);
byte[] insertData = sfOut.toByteArray();
int sfLen = insertData.length;
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 0xFF);
dft.processStructuredField(insertData, 0, insertData.length);
// Verify that onTransferAborted was called with the error message
assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error");
assertNotNull(listener.abortMsg);
assertTrue(listener.abortMsg.contains("TRANS14"));
assertFalse(listener.completeCalled, "onTransferComplete should NOT be called for an error");
// Verify that the error message was NOT written to the destination file stream!
assertEquals(0, outStream.size(), "Error message should not be written to destination file");
}
@Test
public void testOpenDataStreamAndWriteData() {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
dft.initTransfer(null, outStream);
// 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA"
byte[] openReq = new byte[] {
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // "FT:DATA"
};
dft.processStructuredField(openReq, 0, openReq.length);
assertTrue(listener.dftRunningCalled, "onDftRunning should be called for file data stream");
// 2. Simulate TR_DATA_INSERT with binary file payload
byte[] fileData = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34 }; // "%PDF-1.4"
int totalPayloadLen = fileData.length + 5;
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
sfOut.write(0); sfOut.write(0);
sfOut.write(0xD0);
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
sfOut.write(FTConstants.TR_BEGIN_DATA);
sfOut.write((totalPayloadLen >> 8) & 0xFF);
sfOut.write(totalPayloadLen & 0xFF);
sfOut.write(fileData, 0, fileData.length);
byte[] insertData = sfOut.toByteArray();
int sfLen = insertData.length;
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
insertData[1] = (byte) (sfLen & 0xFF);
dft.processStructuredField(insertData, 0, insertData.length);
// Data must be written to file stream
assertArrayEquals(fileData, outStream.toByteArray());
assertEquals(8, listener.bytesTransferred);
}
}
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.Test;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*;
public class GddmCoordinateTransformTest {
@Test
public void testModel4PresentationSpaceMetrics() {
GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 43, 9, 16);
assertEquals(720, transform.getTotalWidth());
assertEquals(688, transform.getTotalHeight());
assertEquals(360, transform.getXMax());
assertEquals(343, transform.getYMax());
// Screen center (0, 0)
Point baseCenter = transform.gocaToBase(0, 0);
assertEquals(360, baseCenter.x);
assertEquals(343, baseCenter.y);
assertEquals(new Point(0, 0), transform.baseToGoca(360, 343));
// Bidirectional screen pixel transformation
// Cell width = 10, Cell height = 20, Offset = (100, 50)
int ox = 100, oy = 50, cw = 10, ch = 20;
Point screenPt = transform.gocaToScreenPixel(0, 0, ox, oy, cw, ch);
Point roundtripGoca = transform.screenPixelToGoca(screenPt.x, screenPt.y, ox, oy, cw, ch);
assertEquals(0, roundtripGoca.x);
assertEquals(0, roundtripGoca.y);
}
@Test
public void testModel2PresentationSpaceMetrics() {
GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 24, 9, 16);
assertEquals(720, transform.getTotalWidth());
assertEquals(384, transform.getTotalHeight());
assertEquals(360, transform.getXMax());
assertEquals(191, transform.getYMax());
}
}
@@ -221,6 +221,12 @@ public class GocaDecoderTest {
GraphicsPlane plane = new GraphicsPlane(1000, 750);
plane.setScreenDimensions(80, 43);
// Standard IBM 3179G 43-row presentation space (80*9 = 720 wide, 43*16 = 688 high)
assertEquals(720, plane.getTotalWidth());
assertEquals(688, plane.getTotalHeight());
assertEquals(360, plane.getXMax());
assertEquals(343, plane.getYMax());
// Screen center (0, 0) should map to canvas center
assertEquals(500, plane.mapX(0));
assertEquals(374, plane.mapY(0));
@@ -240,6 +246,13 @@ public class GocaDecoderTest {
assertEquals(0, plane.unmapY(plane.mapY(0)));
assertEquals(100, plane.unmapY(plane.mapY(100)));
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
// Standard IBM 3179G Model 2 (24-row) presentation space (24*16 = 384 high, yMax = 191)
plane.setScreenDimensions(80, 24);
assertEquals(720, plane.getTotalWidth());
assertEquals(384, plane.getTotalHeight());
assertEquals(360, plane.getXMax());
assertEquals(191, plane.getYMax());
}
@Test
@@ -735,8 +748,9 @@ public class GocaDecoderTest {
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
out.write(GocaConstants.G_GEAR);
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, GSCOL White inside GBAR)
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, Pattern 15/Empty, GSCOL White inside GBAR)
out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background)
out.write(GocaConstants.G_GSPT); out.write(0x0F); // Empty pattern (transparent interior)
out.write(GocaConstants.G_GBAR); out.write(0x80);
out.write(GocaConstants.G_GSCOL); out.write(0x07); // White line color
out.write(GocaConstants.G_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line
@@ -154,38 +154,170 @@ public class InputProcessorTest {
assertEquals((byte) 0xFF, sf[34]);
assertEquals((byte) AID_ENTER, sf[35]);
// Mouse Button 1 with picked segment 2 and tag 5
// Mouse Button 1 matching HODInput.java (0x04 trigger constants and 0x23 correlation descriptor)
byte[] sfMouse = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
294, 192, 1, true, false, false, 2, 5
294, 192, 1, true, false, false
);
assertEquals(56, sfMouse.length);
assertEquals(0x00, sfMouse[0]);
assertEquals(0x38, sfMouse[1]);
assertEquals(0x23, sfMouse[16]);
assertEquals(0x00, sfMouse[17]);
assertEquals(0x23, sfMouse[18]);
assertEquals(0x00, sfMouse[19]);
int mx = (sfMouse[24] << 8) | (sfMouse[25] & 0xFF);
int my = (sfMouse[26] << 8) | (sfMouse[27] & 0xFF);
assertEquals(294, (short) mx);
assertEquals(192, (short) my);
int segId = ((sfMouse[28] & 0xFF) << 24) | ((sfMouse[29] & 0xFF) << 16) |
((sfMouse[30] & 0xFF) << 8) | (sfMouse[31] & 0xFF);
assertEquals(2, segId);
int tag = ((sfMouse[32] & 0xFF) << 8) | (sfMouse[33] & 0xFF);
assertEquals(5, tag);
assertEquals(0x00, sfMouse[28]);
assertEquals(0x00, sfMouse[29]);
assertEquals(0x00, sfMouse[30]);
assertEquals(0x04, sfMouse[31]); // Mouse trigger class constant (0x04)
assertEquals(0x00, sfMouse[32]);
assertEquals(0x04, sfMouse[33]); // Mouse correlation class constant (0x04)
assertEquals(0x00, sfMouse[34]);
assertEquals(0x01, sfMouse[35]); // Button 1
// Mouse Button 2 (Action) with no segment (0)
// Mouse Button 2 (Action) with Shift modifier
byte[] sfMouse2 = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
0, 0, 2, true, true, false, 0, 0
0, 0, 2, true, true, false
);
int segId2 = ((sfMouse2[28] & 0xFF) << 24) | ((sfMouse2[29] & 0xFF) << 16) |
((sfMouse2[30] & 0xFF) << 8) | (sfMouse2[31] & 0xFF);
assertEquals(0, segId2);
int tag2 = ((sfMouse2[32] & 0xFF) << 8) | (sfMouse2[33] & 0xFF);
assertEquals(0, tag2);
assertEquals(0x23, sfMouse2[16]);
assertEquals(0x00, sfMouse2[17]);
assertEquals(0x23, sfMouse2[18]);
assertEquals(0x00, sfMouse2[19]);
assertEquals(0x04, sfMouse2[31]);
assertEquals(0x04, sfMouse2[33]);
assertEquals((byte) 0x80, sfMouse2[34]); // Shift modifier
assertEquals(0x02, sfMouse2[35]); // Button 2
}
@Test
public void testLightPenSelectImmediateSpace() {
screen.erase(false);
// Field attribute at pos 0: unprotected, intensified selectable (0x08)
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
// Designator character at pos 1: space (immediate select)
screen.getCell(1).ec = 0x40;
screen.getCell(1).ucs4 = ' ';
screen.getCell(2).ec = (byte) 0xC1;
screen.getCell(2).ucs4 = 'A';
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
public void sendAid(int aidCode) {
sentAid.set(aidCode);
}
};
boolean result = input.lightPenSelect(2);
assertTrue(result);
assertEquals(AID_SELECT, sentAid.get());
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
assertEquals(2, screen.getCursorAddress());
}
@Test
public void testLightPenSelectEnterImmediateAmpersand() {
screen.erase(false);
// Field attribute at pos 0: normal detectable (0x04)
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
// Designator character at pos 1: '&' (enter immediate)
screen.getCell(1).ec = 0x50;
screen.getCell(1).ucs4 = '&';
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
public void sendAid(int aidCode) {
sentAid.set(aidCode);
}
};
boolean result = input.lightPenSelect(1);
assertTrue(result);
assertEquals(AID_ENTER, sentAid.get());
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
}
@Test
public void testLightPenSelectDeferredToggle() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
// Designator at pos 1: '?' (deferred select)
screen.getCell(1).ec = 0x6F;
screen.getCell(1).ucs4 = '?';
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
public void sendAid(int aidCode) {
sentAid.set(aidCode);
}
};
// First click: ? -> > and MDT=1, no transmission
boolean r1 = input.lightPenSelect(1);
assertTrue(r1);
assertEquals(-1, sentAid.get());
assertEquals('>', screen.getCell(1).ucs4);
assertEquals((byte) 0x6E, screen.getCell(1).ec);
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
// Second click: > -> ? and MDT=0, no transmission
boolean r2 = input.lightPenSelect(1);
assertTrue(r2);
assertEquals(-1, sentAid.get());
assertEquals('?', screen.getCell(1).ucs4);
assertEquals((byte) 0x6F, screen.getCell(1).ec);
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0);
}
@Test
public void testLightPenSelectInvalidDesignatorRejected() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
// Designator at pos 1: 'A' (invalid designator)
screen.getCell(1).ec = (byte) 0xC1;
screen.getCell(1).ucs4 = 'A';
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
public void sendAid(int aidCode) {
sentAid.set(aidCode);
}
};
boolean result = input.lightPenSelect(1);
assertFalse(result);
assertEquals(-1, sentAid.get());
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0);
}
@Test
public void testCursorSelectKey() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
screen.getCell(1).ec = 0x50;
screen.getCell(1).ucs4 = '&';
screen.setCursorAddress(5);
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
public void sendAid(int aidCode) {
sentAid.set(aidCode);
}
};
boolean result = input.cursorSelect();
assertTrue(result);
assertEquals(AID_ENTER, sentAid.get());
assertEquals(5, screen.getCursorAddress());
}
@Test
public void testSendAidSuppressesNullsInModifiedField() {
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
@@ -231,4 +363,59 @@ public class InputProcessorTest {
assertEquals((byte) 0xC9, result[8]); // 'I'
assertEquals((byte) 0xE3, result[9]); // 'T'
}
@Test
public void testSendGraphicMouseAidFraming() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A'
screen.setCellFA(5, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(150, -80);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sent.set(data);
}
};
input.setGocaDecoder(goca);
input.sendGraphicMouseAid(AID_ENTER, 1, false, false);
byte[] result = sent.get();
assertNotNull(result);
// Total expected length:
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
assertEquals(64, result.length);
assertEquals((byte) AID_SF, result[0]);
// SF length = 56 (0x00 0x38)
assertEquals(0x00, result[1]);
assertEquals(0x38, result[2]);
// SF ID = 0x0F0F
assertEquals(0x0F, result[3]);
assertEquals(0x0F, result[4]);
// Coordinates in SF at index 1 + 24 = 25
int gx = (result[25] << 8) | (result[26] & 0xFF);
int gy = (result[27] << 8) | (result[28] & 0xFF);
assertEquals(150, (short) gx);
assertEquals(-80, (short) gy);
// Mouse constant at index 1 + 31 = 32
assertEquals(0x04, result[32]);
assertEquals(0x04, result[34]);
// Button 1 at index 1 + 35 = 36
assertEquals(0x01, result[36]);
// Trailing AID at index 57
assertEquals((byte) AID_ENTER, result[57]);
// Trailing SBA at 60
assertEquals((byte) ORDER_SBA, result[60]);
// Trailing field content 'A' at 63
assertEquals((byte) 0xC1, result[63]);
}
}