Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s

This commit is contained in:
2026-09-22 13:38:12 +00:00
parent d05f1a4c45
commit 3d3a939ece
29 changed files with 1773 additions and 92 deletions
@@ -131,7 +131,18 @@ public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements EC
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
}
public short getAttribute() { return GetAttribute(); }
public boolean IsDBCS() {
return delegate != null && delegate.isDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsDBCSOnly() {
return delegate != null && delegate.isDBCSOnly();
}
public boolean isDBCSOnly() { return IsDBCSOnly(); }
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
}
@@ -511,4 +511,61 @@ public class ECLPS implements ECLConstants, Serializable, Cloneable {
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return delegate.GetScreen(buffer, len, row, col, length, plane);
}
// ========== DBCS Character Status ==========
public boolean IsDBCSChar(int pos) {
return delegate != null && delegate.IsDBCSChar(pos);
}
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
public boolean IsDBCSChar(int row, int col) {
return delegate != null && delegate.IsDBCSChar(row, col);
}
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
public boolean IsDBCS1stChar(int pos) {
return delegate != null && delegate.IsDBCS1stChar(pos);
}
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
public boolean IsDBCS1stChar(int row, int col) {
return delegate != null && delegate.IsDBCS1stChar(row, col);
}
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
public boolean IsDBCS2ndChar(int pos) {
return delegate != null && delegate.IsDBCS2ndChar(pos);
}
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
public boolean IsDBCS2ndChar(int row, int col) {
return delegate != null && delegate.IsDBCS2ndChar(row, col);
}
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
public boolean IsSOChar(int pos) {
return delegate != null && delegate.IsSOChar(pos);
}
public boolean isSOChar(int pos) { return IsSOChar(pos); }
public boolean IsSIChar(int pos) {
return delegate != null && delegate.IsSIChar(pos);
}
public boolean isSIChar(int pos) { return IsSIChar(pos); }
public boolean IsSOSIChar(int pos) {
return delegate != null && delegate.IsSOSIChar(pos);
}
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
}
@@ -27,11 +27,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int[] sbcsMapping, String nioCharsetName) {
super(id, description, cpgid, cgcsgid, sbcsMapping);
if (nioCharsetName != null) {
try {
if (Charset.isSupported(nioCharsetName)) {
this.nioCharset = Charset.forName(nioCharsetName);
}
} catch (Exception ignored) {}
for (String candidate : nioCharsetName.split(",")) {
candidate = candidate.trim();
try {
if (Charset.isSupported(candidate)) {
this.nioCharset = Charset.forName(candidate);
break;
}
} catch (Exception ignored) {}
}
}
// Initialize ideographic space (0x4040 <-> \u3000)
registerDbcsPair(0x4040, '\u3000');
@@ -250,4 +254,78 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
public byte[] utf8WithSOSIToEbcdic(String s) {
return stringToEbcdic(s, true);
}
/**
* Double-byte character set Graphic Character Set Global ID (DBGID/CGCSGID) matching HoD DBGIDTable.
*/
public int getDbCgcsgid() {
switch (getCpgid()) {
case 930:
case 939:
case 290:
return 370; // 0x0172
case 933:
return 934; // 0x03A6
case 935:
return 937; // 0x03A9
case 937:
return 935; // 0x03A7
case 1390:
case 1399:
case 1364:
case 1371:
case 1388:
return 65535; // 0xFFFF
default:
return 370;
}
}
/**
* Double-byte character set Code Page Global ID (DBCS CPGID) matching HoD DBGIDTable.
*/
public int getDbCpgid() {
switch (getCpgid()) {
case 930:
case 939:
case 290:
case 1390:
case 1399:
return 300; // 0x012C
case 933:
case 1364:
return 834; // 0x0342
case 935:
case 1388:
return 837; // 0x0345
case 937:
case 1371:
return 835; // 0x0343
default:
return 300;
}
}
/**
* Double-byte character set section start byte matching HoD DBCSSectTable.
*/
public byte getDbcsSectionStart() {
return 0x41;
}
/**
* Double-byte character set section end byte matching HoD DBCSSectTable.
*/
public byte getDbcsSectionEnd() {
switch (getCpgid()) {
case 290:
case 930:
case 935:
case 939:
return 0x7F;
default:
return (byte) 0xFE;
}
}
}
@@ -311,6 +311,12 @@ public class CodePageRegistry {
register(new Cp1388());
register(new Cp1371());
// Japanese Katakana & Latin SBCS / DBCS Euro
register(new Cp290());
register(new Cp1027());
register(new Cp1390());
register(new Cp1399());
// Setup Extended Aliases
addAlias("ar", "420");
addAlias("arabic", "420");
@@ -360,10 +366,12 @@ public class CodePageRegistry {
addAlias("zh-tw-ext", "1371");
// HoD Converter aliases mapping all HoD converter names to CodePage / Charset
addAlias("1390", "930");
addAlias("1390jis2004", "930");
addAlias("1399", "939");
addAlias("1399jis2004", "939");
addAlias("1390", "1390");
addAlias("1390jis2004", "1390");
addAlias("1399", "1399");
addAlias("1399jis2004", "1399");
addAlias("290", "290");
addAlias("1027", "1027");
addAlias("937macau", "937");
addAlias("1364", "933");
addAlias("1379", "937");
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1027 (Japanese Latin Extended SBCS).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp939.singleByteToChar.
* CCSID / CPGID: 1027, GCSGID: 1172.
*/
public class Cp1027 extends AbstractCodePage {
public static final String ID = "1027";
public static final String DESCRIPTION = "Japanese Latin Extended - EBCDIC";
public static final int CPGID = 1027;
public static final int GCSGID = 1172;
private static final String SBCS_STR =
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
" \ufffd\uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\u00a2.<(+|" +
"&\uff69\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\uff70\uff71!$*);\u00ac" +
"-/\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\ufffd,%_>?" +
"\uff7a\uff7b\uff7c\uff7d\uff7e\uff7f\uff80\uff81\uff82`:#@'=\"" +
"\ufffdabcdefghi\uff83\uff84\uff85\uff86\uff87\uff88" +
"\ufffdjklmnopqr\uff89\uff8a\uff8b\uff8c\uff8d\uff8e" +
"\u203e~stuvwxyz\uff8f\uff90\uff91[\uff92\uff93^" +
"\u00a3\u00a5\uff94\uff95\uff96\uff97\uff98\uff99\uff9a\uff9b\uff9c\uff9d]\uff9e\uff9f" +
"{ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"}JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"\\\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
public static final int[] MAPPING = new int[256];
static {
for (int i = 0; i < 256; i++) {
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
}
}
public Cp1027() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
}
}
@@ -0,0 +1,23 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1390 (Japanese Katakana Extended Mixed DBCS with Euro support: host SBCS CP290+Euro / DBCS CP16684).
* CCSID / CPGID: 1390, GCSGID: 1172.
*/
public class Cp1390 extends AbstractDBCSCodePage {
public static final String ID = "1390";
public static final String DESCRIPTION = "Japanese Katakana Extended Mixed DBCS (with Euro)";
public static final int CPGID = 1390;
public static final int GCSGID = 1172;
public static final int[] MAPPING = Cp290.MAPPING.clone();
static {
// Euro symbol at 0xE1
MAPPING[0xE1] = 0x20AC;
}
public Cp1390() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1390,IBM1390,Cp1390,x-IBM930,IBM930,Cp930");
}
}
@@ -0,0 +1,23 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1399 (Japanese Latin Extended Mixed DBCS with Euro support: host SBCS CP1027+Euro / DBCS CP16684).
* CCSID / CPGID: 1399, GCSGID: 1172.
*/
public class Cp1399 extends AbstractDBCSCodePage {
public static final String ID = "1399";
public static final String DESCRIPTION = "Japanese Latin Extended Mixed DBCS (with Euro)";
public static final int CPGID = 1399;
public static final int GCSGID = 1172;
public static final int[] MAPPING = Cp1027.MAPPING.clone();
static {
// Euro symbol at 0xE1
MAPPING[0xE1] = 0x20AC;
}
public Cp1399() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1399,IBM1399,Cp1399,x-IBM939,IBM939,Cp939");
}
}
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 290 (Japanese Katakana Extended SBCS).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp290.
* CCSID / CPGID: 290, GCSGID: 1172.
*/
public class Cp290 extends AbstractCodePage {
public static final String ID = "290";
public static final String DESCRIPTION = "Japanese Katakana Extended - EBCDIC";
public static final int CPGID = 290;
public static final int GCSGID = 1172;
private static final String SBCS_STR =
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
" \uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\uff69\u00a3.<(+|" +
"&\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\ufffd\uff70\ufffd!\u00a5*);\u00ac" +
"-/\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ufffd,%_>?" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000:#@'=\"" +
"\u0000\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\uff7a\u0000\uff7b\uff7c\uff7d\uff7e" +
"\uff7f\uff80\uff81\uff82\uff83\uff84\uff85\uff86\uff87\uff88\uff89\u0000\ufffd\uff8a\uff8b\uff8c" +
"\u0000\u203e\uff8d\uff8e\uff8f\uff90\uff91\uff92\uff93\uff94\uff95\u0000\uff96\uff97\uff98\uff99" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\uff9a\uff9b\uff9c\uff9d\uff9e\uff9f" +
"\u0000ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"\u0000JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"$\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
public static final int[] MAPPING = new int[256];
static {
for (int i = 0; i < 256; i++) {
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
}
}
public Cp290() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
}
}
@@ -12,6 +12,6 @@ public class Cp930 extends AbstractDBCSCodePage {
public static final int GCSGID = 1172;
public Cp930() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930");
super(ID, DESCRIPTION, CPGID, GCSGID, Cp290.MAPPING, "x-IBM930,IBM930,Cp930");
}
}
@@ -12,6 +12,6 @@ public class Cp939 extends AbstractDBCSCodePage {
public static final int GCSGID = 1172;
public Cp939() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939");
super(ID, DESCRIPTION, CPGID, GCSGID, Cp1027.MAPPING, "x-IBM939,IBM939,Cp939");
}
}
@@ -90,6 +90,35 @@ public class EbcdicTranslator {
return isDBCSCodePage();
}
public synchronized int getDbCgcsgid() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbCgcsgid();
}
return 370;
}
public synchronized int getDbCpgid() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbCpgid();
}
return 300;
}
public synchronized byte getDbcsSectionStart() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionStart();
}
return 0x41;
}
public synchronized byte getDbcsSectionEnd() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionEnd();
}
return 0x7F;
}
// =========================================================================
// Custom Character Translation Overrides
// =========================================================================
@@ -60,6 +60,8 @@ public class DataStreamProcessor {
private byte currentGr = 0;
private byte currentCs = 0;
private byte currentOl = 0;
private byte currentTr = 0;
private boolean inDBCS = false;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
@@ -252,6 +254,7 @@ public class DataStreamProcessor {
currentGr = 0;
currentCs = 0;
currentOl = 0;
currentTr = 0;
break;
case CMD_WSF:
case SNA_CMD_WSF:
@@ -395,6 +398,8 @@ public class DataStreamProcessor {
currentGr = 0;
currentCs = 0;
currentOl = 0;
currentTr = 0;
inDBCS = false;
}
if (eraseFirst) {
@@ -405,6 +410,8 @@ public class DataStreamProcessor {
currentGr = 0;
currentCs = 0;
currentOl = 0;
currentTr = 0;
inDBCS = false;
}
// Process orders and data starting at byte 2
@@ -435,6 +442,7 @@ public class DataStreamProcessor {
}
case ORDER_SF: { // Start Field
inDBCS = false;
if (pos + 1 >= end) {
pos = end;
break;
@@ -456,6 +464,7 @@ public class DataStreamProcessor {
}
case ORDER_SFE: { // Start Field Extended
inDBCS = false;
if (pos + 1 >= end) {
pos = end;
break;
@@ -502,6 +511,7 @@ public class DataStreamProcessor {
currentGr = 0;
currentCs = 0;
currentOl = 0;
currentTr = 0;
break;
case XA_FOREGROUND:
currentFg = (byte) attrValue;
@@ -530,6 +540,9 @@ public class DataStreamProcessor {
case 0x84:
currentOl = (byte) attrValue;
break;
case XA_TRANSPARENCY:
currentTr = (byte) attrValue;
break;
}
pos += 3;
lastWasOrder = true;
@@ -591,6 +604,44 @@ public class DataStreamProcessor {
if (toAddr >= size)
toAddr = toAddr % size;
boolean isDbcsSession = (translator != null && translator.isDBCS());
if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS) && pos + 4 < end) {
int fillChar2 = data[pos + 4] & 0xFF;
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(fillChar, fillChar2) : '?';
do {
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) fillChar;
ea1.ucs4 = uni;
ea1.fg = currentFg;
ea1.bg = currentBg;
ea1.gr = (byte) currentGr;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ea1.ol = currentOl;
ea1.tr = currentTr;
baddr = (baddr + 1) % size;
if (baddr == toAddr) break;
ExtendedAttribute ea2 = screen.getCell(baddr);
ea2.fa = 0;
ea2.ec = (byte) fillChar2;
ea2.ucs4 = uni;
ea2.fg = currentFg;
ea2.bg = currentBg;
ea2.gr = (byte) currentGr;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
ea2.ol = currentOl;
ea2.tr = currentTr;
baddr = (baddr + 1) % size;
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
pos += 5;
lastWasOrder = true;
break;
}
// Handle GE (graphic escape) prefix
byte fillCs = currentCs;
char fillUcs4 = 0;
@@ -614,7 +665,9 @@ public class DataStreamProcessor {
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = fillCs;
ea.db = ExtendedAttribute.DB_NONE;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
@@ -644,6 +697,7 @@ public class DataStreamProcessor {
ea.gr = 0;
ea.cs = 0;
ea.ol = 0;
ea.tr = 0;
}
}
baddr = (baddr + 1) % size;
@@ -669,6 +723,7 @@ public class DataStreamProcessor {
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2;
@@ -677,6 +732,98 @@ public class DataStreamProcessor {
}
default: {
boolean isDbcsSession = (translator != null && translator.isDBCS());
if (isDbcsSession && b == 0x0E) { // Shift-Out (enter DBCS mode)
inDBCS = true;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) 0x0E;
ea.ucs4 = ' ';
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_SO;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
} else if (isDbcsSession && b == 0x0F) { // Shift-In (exit DBCS mode)
inDBCS = false;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) 0x0F;
ea.ucs4 = ' ';
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_SI;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
} else if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS)) {
if (pos + 1 < end) {
int b2 = data[pos + 1] & 0xFF;
if (b2 == 0x0F) { // Orphaned single byte before SI
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) b;
ea.ucs4 = translator.ebcdicToUnicode(b);
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = currentCs;
ea.ol = currentOl;
ea.tr = currentTr;
ea.db = ExtendedAttribute.DB_NONE;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
}
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(b, b2) : '?';
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) b;
ea1.ucs4 = uni;
ea1.fg = currentFg;
ea1.bg = currentBg;
ea1.gr = (byte) currentGr;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ea1.ol = currentOl;
ea1.tr = currentTr;
int nextAddr = (baddr + 1) % size;
ExtendedAttribute ea2 = screen.getCell(nextAddr);
ea2.fa = 0;
ea2.ec = (byte) b2;
ea2.ucs4 = uni;
ea2.fg = currentFg;
ea2.bg = currentBg;
ea2.gr = (byte) currentGr;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
ea2.ol = currentOl;
ea2.tr = currentTr;
baddr = (baddr + 2) % size;
screen.setBufferAddress(baddr);
pos += 2;
lastWasOrder = false;
break;
}
}
// Regular data byte (EBCDIC character)
// Format control codes display as nulls/blanks per 3270 spec
boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF ||
@@ -688,14 +835,18 @@ public class DataStreamProcessor {
if (isFCOrder) {
ea.ec = 0; // Display as null/blank
ea.ucs4 = 0;
} else {
ea.ec = (byte) b;
ea.ucs4 = (translator != null) ? translator.ebcdicToUnicode(b) : (char) b;
}
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = currentCs;
ea.db = ExtendedAttribute.DB_NONE;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
@@ -796,7 +947,7 @@ public class DataStreamProcessor {
// Buffer contents depending on Reply Mode
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
// Extended Field or Character Mode
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0, curTr = 0;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
@@ -807,6 +958,7 @@ public class DataStreamProcessor {
if (ea.gr != 0) count++;
if (ea.cs != 0) count++;
if (ea.ol != 0) count++;
if (ea.tr != 0) count++;
outputWrite(ORDER_SFE);
outputWrite(count);
@@ -825,6 +977,7 @@ public class DataStreamProcessor {
}
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
if (ea.tr != 0) { outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF); }
} else {
if (mode == SF_SRM_CHAR) {
// In character mode, output SA if character attributes differ
@@ -850,6 +1003,10 @@ public class DataStreamProcessor {
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
curCs = ea.cs;
}
if (ea.tr != curTr) {
outputWrite(ORDER_SA); outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF);
curTr = ea.tr;
}
}
outputWrite(ea.ec & 0xFF);
}
@@ -129,6 +129,7 @@ public class QueryReplyBuilder {
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_AUXDA, // 0x99 - Auxiliary Devices
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
QR_TRANSPARENCY, // 0xa8 - Background Transparency
};
// Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
@@ -218,6 +219,9 @@ public class QueryReplyBuilder {
// Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
// Background Transparency (0xA8)
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray();
}
@@ -245,9 +249,9 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
@@ -317,11 +321,7 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
break;
case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) {
@@ -390,10 +390,17 @@ public class QueryReplyBuilder {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
boolean wroteDbcs = false;
for (int code : codes) {
if (isDbcs && !wroteDbcs && code == QR_DDM) {
out.write(QR_OUTLINING);
out.write(QR_DBCS_ASIA);
wroteDbcs = true;
}
out.write(code);
if (isDbcs && code == QR_OUTLINING) {
if (isDbcs && code == QR_OUTLINING && !wroteDbcs) {
out.write(QR_DBCS_ASIA); // 0x91
wroteDbcs = true;
}
}
return out.toByteArray();
@@ -441,7 +448,7 @@ public class QueryReplyBuilder {
public int getCharWidth(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12;
return 12; // 0x0C
}
return SW_3279_2; // 9
}
@@ -452,6 +459,9 @@ public class QueryReplyBuilder {
}
public int getCharHeight(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 24; // 0x18
}
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
}
@@ -477,9 +487,54 @@ public class QueryReplyBuilder {
int cgcsgid = 0x02B9;
int cpgid = 0x0025;
boolean isDbcs = false;
if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid();
isDbcs = screen.getTranslator().isDBCS();
}
if (isDbcs) {
int dbCgcsgid = screen.getTranslator().getDbCgcsgid();
int dbCpgid = screen.getTranslator().getDbCpgid();
byte secStart = screen.getTranslator().getDbcsSectionStart();
byte secEnd = screen.getTranslator().getDbcsSectionEnd();
// 42-byte DBCS Character Sets QR payload (total 46 bytes structured field, matching HoD DS3270.java xxxqrcs)
ByteArrayOutputStream out = new ByteArrayOutputStream(42);
out.write(0x8E); // flags: GE | DBCS / 2-byte charsets (0x8E)
out.write(0x00); // flags 2
out.write(charW); // SDW (12)
out.write(charH); // SDH (24)
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); // 4 reserved bytes
out.write(0x0B); // DL = 11 bytes per descriptor
// Descriptor 1: Base SBCS (11 bytes)
out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Descriptor 2: DBCS Set 1 (11 bytes)
out.write(0x01); // Set ID = 1
out.write(0x00); // Flags
out.write(0x80); // Local ID = 0x80 (DBCS)
out.write((charW * 2) & 0xFF); // Double cell width (24)
out.write(charH & 0xFF); // Cell height (24)
out.write(secStart); // Section start (e.g. 0x41)
out.write(secEnd); // Section end (e.g. 0xFE or 0x7F)
out.write((dbCgcsgid >> 8) & 0xFF); out.write(dbCgcsgid & 0xFF);
out.write((dbCpgid >> 8) & 0xFF); out.write(dbCpgid & 0xFF);
// Descriptor 3: APL / Text (11 bytes)
out.write(0x01); out.write(0x00); out.write(0xF1);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0xC3); // CGCSGID 963
out.write(0x01); out.write(0x36); // CPGID 310
return out.toByteArray();
}
if (graphicsMode.isProgrammedSymbolsEnabled()) {
@@ -531,6 +586,7 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
public byte[] buildColor() {
return STATIC_QR_COLOR.clone();
}
@@ -5,13 +5,27 @@ package haus.nightmare.lib3270j.ecl;
*/
public interface ECLConstants {
// Presentation space logical planes
int PLANE_TEXT = 1; // Unicode/EBCDIC characters
int PLANE_COLOR = 2; // Color attributes
int PLANE_HILITE = 3; // Extended highlighting
int PLANE_EXTENDED = 4; // Extended character sets / outlining
int PLANE_FIELD = 5; // Field attribute definition bytes
int PLANE_DBCS = 6; // Double-byte character plane
// Presentation space logical planes (Bitmask & ordinal definitions matching IBM Host On-Demand)
int PLANE_TEXT = 1; // Unicode/EBCDIC characters
int PLANE_COLOR = 2; // Color attributes
int PLANE_HILITE = 3; // Extended highlighting
int PLANE_EXTENDED = 4; // Extended character sets / outlining
int PLANE_FIELD = 5; // Field attribute definition bytes
int PLANE_DBCS = 6; // Double-byte character plane (lib3270j convention)
int PLANE_GRID = 8; // Grid lines / outlining plane
// Canonical IBM Host On-Demand ECLConstants plane bitmasks
int TEXT_PLANE = 1;
int COLOR_PLANE = 2;
int DBCS_PLANE = 4;
int GRID_PLANE = 8;
int FIELD_PLANE = 16;
int EXFIELD_PLANE = 32;
int NLS_PLANE = 64;
int HOST_PLANE = 128;
int RAWTEXT_PLANE = 256;
int COLOR_ATTRIBUTES_PLANE = 512;
int ALL_PLANES = 319;
// Color definitions (IBM 3279 standard 16-color palette)
char COLOR_NEUTRAL_BLACK = 0;
@@ -295,6 +295,54 @@ public class ECLField {
}
public void Erase() { erase(); }
/**
* Check whether this field contains any DBCS characters or SO/SI delimiters.
*/
public boolean isDBCS() {
if (ps == null || length <= 0) return false;
ScreenBuffer sb = ps.getScreenBuffer();
if (sb == null) return false;
int size = sb.getRows() * sb.getCols();
if (size <= 0) return false;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
if (ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI)) {
return true;
}
}
return false;
}
public boolean IsDBCS() { return isDBCS(); }
/**
* Check whether this field contains only DBCS characters (excluding nulls / spaces).
*/
public boolean isDBCSOnly() {
if (ps == null || length <= 0) return false;
ScreenBuffer sb = ps.getScreenBuffer();
if (sb == null) return false;
int size = sb.getRows() * sb.getCols();
if (size <= 0) return false;
boolean hasDBCS = false;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
if (ea != null) {
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
hasDBCS = true;
} else if (ea.ec != 0 && ea.ec != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
return false;
}
}
}
return hasDBCS;
}
public boolean IsDBCSOnly() { return isDBCSOnly(); }
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -25,9 +25,22 @@ public class ECLFieldList extends Vector<ECLField> {
refresh();
}
public ECLFieldList(ECLPS ps) {
this(ps, ps != null ? ps.getScreenBuffer() : null);
}
public ECLField getField(int index) {
return (index >= 0 && index < size()) ? get(index) : null;
}
public ECLField GetField(int index) {
return getField(index);
}
public ECLPS getPS() { return ps; }
public ScreenBuffer getScreenBuffer() { return screen; }
/**
* Refresh the field list by scanning the presentation space buffer.
*/
@@ -264,10 +264,12 @@ public class ECLPS implements ECLConstants {
case PLANE_TEXT:
if (ea.isFieldAttribute()) {
destBuffer[i] = ' ';
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = ' ';
} else if (ea.ucs4 != 0) {
destBuffer[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
destBuffer[i] = (translator != null) ? translator.ebcdicToUnicode(ea.ec & 0xFF) : (char) (ea.ec & 0xFF);
} else {
destBuffer[i] = ' ';
}
@@ -278,12 +280,43 @@ public class ECLPS implements ECLConstants {
case PLANE_HILITE:
destBuffer[i] = (char) (ea.gr & 0xFF);
break;
case PLANE_EXTENDED:
destBuffer[i] = (char) (ea.cs & 0xFF);
case DBCS_PLANE: // 4 (HoD DBCS_PLANE) or PLANE_EXTENDED for SBCS
if (translator != null && translator.isDBCS()) {
if (ea.db == ExtendedAttribute.DB_LEFT) {
destBuffer[i] = (char) 0xC0; // DB_DBHIGH (first half of DBCS)
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
destBuffer[i] = (char) 0x80; // DB_DBCS (second half of DBCS)
} else if (ea.db == ExtendedAttribute.DB_SO) {
destBuffer[i] = (char) 0x20; // DB_SO
} else if (ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = (char) 0x10; // DB_SI
} else {
destBuffer[i] = 0;
}
} else {
destBuffer[i] = (char) (ea.cs & 0xFF);
}
break;
case PLANE_FIELD:
case FIELD_PLANE: // 16
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
break;
case PLANE_DBCS: // 6
if (ea.db == ExtendedAttribute.DB_LEFT) {
destBuffer[i] = (char) 0xC0;
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
destBuffer[i] = (char) 0x80;
} else if (ea.db == ExtendedAttribute.DB_SO) {
destBuffer[i] = (char) 0x20;
} else if (ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = (char) 0x10;
} else {
destBuffer[i] = 0;
}
break;
case GRID_PLANE: // 8 (HoD GRID_PLANE / Outlining)
destBuffer[i] = (char) (ea.ol & 0xFF);
break;
default:
destBuffer[i] = ' ';
break;
@@ -292,6 +325,107 @@ public class ECLPS implements ECLConstants {
return copyLen;
}
public synchronized boolean IsDBCSChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT);
}
public synchronized boolean isDBCSChar(int pos) {
return IsDBCSChar(pos);
}
public synchronized boolean IsDBCSChar(int row, int col) {
if (screen == null) return false;
return IsDBCSChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCSChar(int row, int col) {
return IsDBCSChar(row, col);
}
public synchronized boolean IsDBCS1stChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && ea.db == ExtendedAttribute.DB_LEFT;
}
public synchronized boolean isDBCS1stChar(int pos) {
return IsDBCS1stChar(pos);
}
public synchronized boolean IsDBCS1stChar(int row, int col) {
if (screen == null) return false;
return IsDBCS1stChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCS1stChar(int row, int col) {
return IsDBCS1stChar(row, col);
}
public synchronized boolean IsDBCS2ndChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && ea.db == ExtendedAttribute.DB_RIGHT;
}
public synchronized boolean isDBCS2ndChar(int pos) {
return IsDBCS2ndChar(pos);
}
public synchronized boolean IsDBCS2ndChar(int row, int col) {
if (screen == null) return false;
return IsDBCS2ndChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCS2ndChar(int row, int col) {
return IsDBCS2ndChar(row, col);
}
public synchronized boolean IsSOChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_SO || (ea.ec & 0xFF) == 0x0E);
}
public synchronized boolean isSOChar(int pos) {
return IsSOChar(pos);
}
public synchronized boolean IsSIChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_SI || (ea.ec & 0xFF) == 0x0F);
}
public synchronized boolean isSIChar(int pos) {
return IsSIChar(pos);
}
public synchronized boolean IsSOSIChar(int pos) {
return IsSOChar(pos) || IsSIChar(pos);
}
public synchronized boolean isSOSIChar(int pos) {
return IsSOSIChar(pos);
}
public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) {
if (cArray == null || len <= 0 || screen == null) return 0;
int minR = Math.max(1, Math.min(sRow, eRow));
@@ -816,15 +816,14 @@ public class FTDft {
for (int i = 0; i < lineStr.length(); i++) {
char ch = lineStr.charAt(i);
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1;
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF);
if (charIsDbcs) {
int dbcsVal = (isDbcs && translator != null) ? translator.unicodeToDbcs(ch) : -1;
if (dbcsVal != -1) {
if (!inDbcsState && !noSo) {
converted.write(0x0E); // Shift-Out
inDbcsState = true;
}
converted.write((ebc >> 8) & 0xFF);
converted.write(ebc & 0xFF);
converted.write((dbcsVal >> 8) & 0xFF);
converted.write(dbcsVal & 0xFF);
} else {
if (inDbcsState && !noSo) {
converted.write(0x0F); // Shift-In
@@ -292,9 +292,10 @@ public class InputProcessor {
}
}
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode)
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode, or DBCS)
boolean isAplChar = false;
int ebc = -1;
int dbcs = -1;
if (aplKeyboardMode) {
int aplCode = getAplCodeForChar(ch);
if (aplCode >= 0) {
@@ -303,10 +304,62 @@ public class InputProcessor {
isAplChar = true;
}
}
if (ebc < 0) {
ebc = translator.unicodeToEbcdic(ch);
if (ebc < 0 && translator != null && translator.isDBCS()) {
dbcs = translator.unicodeToDbcs(ch);
}
if (ebc < 0 && dbcs < 0) {
ebc = (translator != null) ? translator.unicodeToEbcdic(ch) : (int) ch;
}
if (ebc < 0 && dbcs < 0) return;
if (dbcs >= 0) {
// DBCS character typing
if (insertMode) {
if (!screen.insertChar(baddr, ch)) {
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
}
setKeyboardLocked(true);
return;
}
} else {
int nextPos = screen.incrementAddress(baddr);
if (screen.isFormatted() && screen.getCell(nextPos).isFieldAttribute()) {
// Cannot fit 2-cell DBCS at the last position of the field
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
}
setKeyboardLocked(true);
return;
}
int b1 = (dbcs >> 8) & 0xFF;
int b2 = dbcs & 0xFF;
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) b1;
ea1.ucs4 = ch;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ExtendedAttribute ea2 = screen.getCell(nextPos);
ea2.fa = 0;
ea2.ec = (byte) b2;
ea2.ucs4 = ch;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
}
screen.setCursorAddress(screen.incrementAddress(nextPos));
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
return;
}
if (ebc < 0) return;
if (insertMode) {
// Insert mode: shift characters right from cursor to end of field
@@ -1476,38 +1529,12 @@ public class InputProcessor {
}
public void deleteChar() {
if (!screen.isFormatted()) return;
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
addr = ((addr % size) + size) % size;
// Cannot delete a field attribute
if (screen.getCell(addr).isFieldAttribute()) return;
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// Shift characters left within the field
int shiftAddr = addr;
int count = 0;
while (count < size) {
int next = screen.incrementAddress(shiftAddr);
if (screen.getCell(next).isFieldAttribute()) {
screen.getCell(shiftAddr).ec = 0;
screen.getCell(shiftAddr).ucs4 = 0;
break;
}
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
shiftAddr = next;
count++;
}
int faAddr = screen.findFieldAttribute(addr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
if (screen.isFormatted()) {
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
}
screen.deleteChar(addr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
@@ -1525,6 +1552,9 @@ public class InputProcessor {
return;
}
}
if (screen.getCell(prev).db == ExtendedAttribute.DB_RIGHT) {
prev = screen.decrementAddress(prev);
}
screen.setCursorAddress(prev);
deleteChar();
}
@@ -97,6 +97,11 @@ public class ExtendedAttribute {
/** Validation attribute. */
public byte vl;
// Transparency attribute values (0x46 / XA_TRANSPARENCY)
public static final byte TR_DEFAULT = 0x00;
public static final byte TR_OR = (byte) 0xF0;
public static final byte TR_OPAQUE = (byte) 0xFF;
/** Transparency attribute. */
public byte tr;
@@ -727,6 +727,14 @@ public class ScreenBuffer {
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (!ea.isFieldAttribute()) {
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT) {
// Preserve decoded DBCS character
continue;
}
if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
ea.ucs4 = ' ';
continue;
}
if (ea.ec == 0) {
ea.ucs4 = 0;
} else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE
@@ -736,6 +744,9 @@ public class ScreenBuffer {
}
}
}
if (translator != null && translator.isDBCS()) {
processSOSI();
}
}
private char getAplGraphic(int ec) {
@@ -1124,7 +1135,7 @@ public class ScreenBuffer {
int count = 0;
while (count < size) {
int next = incrementAddress(shiftAddr);
if (getCell(next).isFieldAttribute()) {
if (getCell(next).isFieldAttribute() || count == size - 1) {
getCell(shiftAddr).clear();
break;
}
@@ -0,0 +1,204 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class DBCSDataStreamFullTest {
@Test
public void testDBCSDataStreamShiftOutShiftIn() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// Data stream:
// EW, WCC, SBA (0, 0), SO (0x0E), 0x40 0x40 (\u3000 ideographic space), SI (0x0F)
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // Shift-Out
(byte) 0x40, (byte) 0x40, // DBCS ideographic space
(byte) 0x0F // Shift-In
};
processor.processRecord(stream, 0, stream.length, true);
ExtendedAttribute cell0 = screen.getCell(0);
assertEquals(ExtendedAttribute.DB_SO, cell0.db, "Cell 0 must be DB_SO");
ExtendedAttribute cell1 = screen.getCell(1);
assertEquals(ExtendedAttribute.DB_LEFT, cell1.db, "Cell 1 must be DB_LEFT");
assertEquals('\u3000', cell1.ucs4, "Cell 1 ucs4 must be \\u3000");
ExtendedAttribute cell2 = screen.getCell(2);
assertEquals(ExtendedAttribute.DB_RIGHT, cell2.db, "Cell 2 must be DB_RIGHT");
assertEquals('\u3000', cell2.ucs4, "Cell 2 ucs4 must be \\u3000");
ExtendedAttribute cell3 = screen.getCell(3);
assertEquals(ExtendedAttribute.DB_SI, cell3.db, "Cell 3 must be DB_SI");
}
@Test
public void testDBCSRepeatToAddressOrder() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// In DBCS mode (after SO), ORDER_RA consumes 2 data bytes and fills pairs
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // SO at 0
(byte) ORDER_RA,
(byte) 0x40, (byte) 0x4B, // RA up to address 11 (covers cells 1..10)
(byte) 0x40, (byte) 0x40, // 2-byte repeat char (\u3000)
(byte) 0x0F // SI at 11
};
processor.processRecord(stream, 0, stream.length, true);
assertEquals(ExtendedAttribute.DB_SO, screen.getCell(0).db);
for (int i = 1; i < 11; i += 2) {
assertEquals(ExtendedAttribute.DB_LEFT, screen.getCell(i).db, "Cell " + i + " must be DB_LEFT");
assertEquals('\u3000', screen.getCell(i).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, screen.getCell(i + 1).db, "Cell " + (i + 1) + " must be DB_RIGHT");
assertEquals('\u3000', screen.getCell(i + 1).ucs4);
}
assertEquals(ExtendedAttribute.DB_SI, screen.getCell(11).db);
}
@Test
public void testECLPlanesDBCSAndOutlining() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) 0x0E, // SO at 0
(byte) 0x40, (byte) 0x40, // DBCS at 1, 2
(byte) 0x0F // SI at 3
};
processor.processRecord(stream, 0, stream.length, true);
// Add outlining to cell 1
screen.getCell(1).ol = (byte) (ExtendedAttribute.OL_BOX);
ECLPS eclPS = new ECLPS(screen, null, translator);
char[] dbcsPlane = new char[4];
int count = eclPS.getPlane(ECLConstants.DBCS_PLANE, dbcsPlane, 0, 4);
assertEquals(4, count);
assertEquals(0x20, dbcsPlane[0], "SO must be 0x20 in DBCS plane");
assertEquals(0xC0, dbcsPlane[1], "DB_LEFT must be 0xC0 (DB_DBHIGH)");
assertEquals(0x80, dbcsPlane[2], "DB_RIGHT must be 0x80 (DB_DBCS)");
assertEquals(0x10, dbcsPlane[3], "SI must be 0x10 in DBCS plane");
char[] gridPlane = new char[4];
eclPS.getPlane(ECLConstants.GRID_PLANE, gridPlane, 0, 4);
assertEquals((char) (ExtendedAttribute.OL_BOX & 0xFF), gridPlane[1]);
// Helper methods
assertTrue(eclPS.IsSOChar(0));
assertFalse(eclPS.IsDBCSChar(0));
assertTrue(eclPS.IsDBCSChar(1));
assertTrue(eclPS.IsDBCS1stChar(1));
assertFalse(eclPS.IsDBCS2ndChar(1));
assertTrue(eclPS.IsDBCSChar(2));
assertFalse(eclPS.IsDBCS1stChar(2));
assertTrue(eclPS.IsDBCS2ndChar(2));
assertTrue(eclPS.IsSIChar(3));
assertFalse(eclPS.IsDBCSChar(3));
}
@Test
public void testECLFieldDBCSMethods() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
DataStreamProcessor processor = new DataStreamProcessor(screen, translator);
// Field with SO + DBCS + SI
byte[] stream = new byte[] {
(byte) CMD_EW,
(byte) 0xC3,
(byte) ORDER_SBA,
(byte) 0x40, (byte) 0x40,
(byte) ORDER_SF,
(byte) FA_PRINTABLE, // Field at 0
(byte) 0x0E, // SO at 1
(byte) 0x40, (byte) 0x40, // DBCS at 2, 3
(byte) 0x0F // SI at 4
};
processor.processRecord(stream, 0, stream.length, true);
ECLPS eclPS = new ECLPS(screen, null, translator);
ECLFieldList fieldList = new ECLFieldList(eclPS);
fieldList.Refresh();
assertEquals(1, fieldList.size());
ECLField field = fieldList.getField(0);
assertTrue(field.isDBCS(), "Field should report isDBCS = true");
assertTrue(field.isDBCSOnly(), "Field should report isDBCSOnly = true");
}
@Test
public void testQueryReplyBuilderDBCSCharsets() {
EbcdicTranslator translator = new EbcdicTranslator("930");
ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
QueryReplyBuilder builder = new QueryReplyBuilder(screen);
byte[] charsets = builder.buildCharsets();
assertNotNull(charsets);
assertEquals(42, charsets.length, "DBCS Character Sets QR payload must be 42 bytes (matching HoD xxxqrcs)");
assertEquals((byte) 0x8E, charsets[0], "Flags 1 must be 0x8E");
assertEquals((byte) 0x00, charsets[1], "Flags 2 must be 0x00");
assertEquals(12, charsets[2], "SDW must be 12");
assertEquals(24, charsets[3], "SDH must be 24");
assertEquals(0x0B, charsets[8], "Descriptor length DL must be 11 (0x0B)");
// Descriptor 2 (DBCS): offset 9 + 11 = 20
assertEquals(0x01, charsets[20], "Set ID must be 1");
assertEquals((byte) 0x80, charsets[22], "Local ID must be 0x80 (DBCS)");
assertEquals(24, charsets[23], "DBCS cell width must be double (24)");
assertEquals(24, charsets[24], "DBCS cell height must be 24");
assertEquals(0x41, charsets[25], "Section start must be 0x41");
assertEquals(0x7F, charsets[26], "Section end for 930 must be 0x7F");
// DB CGCSGID = 370 (0x0172), DB CPGID = 300 (0x012C)
int cgcsgid = ((charsets[27] & 0xFF) << 8) | (charsets[28] & 0xFF);
int cpgid = ((charsets[29] & 0xFF) << 8) | (charsets[30] & 0xFF);
assertEquals(370, cgcsgid);
assertEquals(300, cpgid);
// Summary Query Reply contains Outlining (0x8C) and DBCS Asia (0x91)
byte[] summary = builder.buildSummary();
boolean hasOutlining = false;
boolean hasDbcsAsia = false;
for (byte b : summary) {
if ((b & 0xFF) == QR_OUTLINING) hasOutlining = true;
if ((b & 0xFF) == QR_DBCS_ASIA) hasDbcsAsia = true;
}
assertTrue(hasOutlining, "Summary must include QR_OUTLINING");
assertTrue(hasDbcsAsia, "Summary must include QR_DBCS_ASIA");
}
}
@@ -49,9 +49,24 @@ public class QueryReplyBuilderTest {
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary
int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
boolean hasB0 = false;
boolean hasA8 = false;
for (int i = 5; i < 1 + sumLen; i++) {
assertNotEquals(QR_SEGMENT, replies[i] & 0xFF);
if ((replies[i] & 0xFF) == QR_SEGMENT) hasB0 = true;
if ((replies[i] & 0xFF) == QR_TRANSPARENCY) hasA8 = true;
}
assertFalse(hasB0);
assertTrue(hasA8, "QR_TRANSPARENCY (0xA8) should be advertised even in GraphicsMode.NONE");
// And QR_TRANSPARENCY structured field payload must be present in replies
boolean hasA8Payload = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_TRANSPARENCY) {
hasA8Payload = true;
break;
}
}
assertTrue(hasA8Payload, "QR_TRANSPARENCY structured field should be included in buildAllQueryReplies");
}
@Test
@@ -87,10 +87,10 @@ public class QueryReplyPhase2Test {
assertNotNull(reply);
List<ParsedSF> sfs = parseQueryReplies(reply);
// Base query reply must contain exactly 10 structured fields for SBCS:
// Base query reply must contain exactly 11 structured fields for SBCS:
// 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets),
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part)
assertEquals(10, sfs.size());
// 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part), 0xA8 (Transparency)
assertEquals(11, sfs.size());
assertEquals(QR_SUMMARY, sfs.get(0).qcode);
assertEquals(QR_USABLE_AREA, sfs.get(1).qcode);
@@ -102,10 +102,10 @@ public class QueryReplyPhase2Test {
assertEquals(QR_DDM, sfs.get(7).qcode);
assertEquals(QR_AUXDA, sfs.get(8).qcode);
assertEquals(QR_IMP_PART, sfs.get(9).qcode);
assertEquals(QR_TRANSPARENCY, sfs.get(10).qcode);
// Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply
// Verify that Vector Graphics SF payloads (0xB0..0xB6) are NOT sent in base reply
for (ParsedSF sf : sfs) {
assertNotEquals(QR_TRANSPARENCY, sf.qcode);
assertNotEquals(QR_SEGMENT, sf.qcode);
assertNotEquals(QR_PROCEDURE, sf.qcode);
assertNotEquals(QR_LINETYPE, sf.qcode);
@@ -246,7 +246,7 @@ public class QueryReplyPhase2Test {
assertTrue(sent.length > 0);
assertEquals((byte) AID_SF, sent[0]);
List<ParsedSF> sfs = parseQueryReplies(sent);
assertEquals(10, sfs.size());
assertEquals(11, sfs.size());
// Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4
byte[] queryList = new byte[] {
@@ -0,0 +1,143 @@
package haus.nightmare.lib3270j.datastream;
import haus.nightmare.lib3270j.TerminalModel;
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 static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TransparencyDataStreamTest {
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
EbcdicTranslator translator = new EbcdicTranslator();
screen = new ScreenBuffer(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 testSetAttributeTransparency() {
// Send SFE with XA_TRANSPARENCY = XAT_OPAQUE (0xFF)
byte[] sfeRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SFE, 0x02,
(byte) XA_3270, (byte) FA_PRINTABLE,
(byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE
};
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
ExtendedAttribute cell0 = screen.getCell(0);
assertTrue(cell0.isFieldAttribute());
assertEquals(ExtendedAttribute.TR_OPAQUE, cell0.tr);
// Send SA with XA_TRANSPARENCY = XAT_OPAQUE followed by character 'A' (0xC1)
byte[] saRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x01,
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
(byte) 0xC1 // 'A'
};
processor.processRecord(saRecord, 0, saRecord.length, true);
ExtendedAttribute charCell1 = screen.getCell(1);
assertFalse(charCell1.isFieldAttribute());
assertEquals((byte) 0xC1, charCell1.ec);
assertEquals(ExtendedAttribute.TR_OPAQUE, charCell1.tr);
// Send SA with XA_ALL (0x00) followed by character 'B' (0xC2) -> should reset tr to 0
byte[] resetRecord = new byte[] {
(byte) CMD_W, 0x00,
(byte) ORDER_SBA, 0x00, 0x02,
(byte) ORDER_SA, (byte) XA_ALL, 0x00,
(byte) 0xC2 // 'B'
};
processor.processRecord(resetRecord, 0, resetRecord.length, true);
ExtendedAttribute charCell2 = screen.getCell(2);
assertFalse(charCell2.isFieldAttribute());
assertEquals((byte) 0xC2, charCell2.ec);
assertEquals(ExtendedAttribute.TR_DEFAULT, charCell2.tr);
}
@Test
public void testRepeatToAddressWithTransparency() {
// Set SA Transparency = Opaque, then RA to address 5 with char 'C' (0xC3)
byte[] raRecord = new byte[] {
(byte) CMD_EW, 0x00,
(byte) ORDER_SBA, 0x00, 0x00,
(byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE,
(byte) ORDER_RA, 0x00, 0x05, (byte) 0xC3
};
processor.processRecord(raRecord, 0, raRecord.length, true);
for (int i = 0; i < 5; i++) {
ExtendedAttribute cell = screen.getCell(i);
assertEquals((byte) 0xC3, cell.ec);
assertEquals(ExtendedAttribute.TR_OPAQUE, cell.tr, "Cell at " + i + " should have TR_OPAQUE");
}
}
@Test
public void testReadBufferExtendedFieldIncludesTransparency() {
screen.setReplyMode((byte) SF_SRM_XFIELD);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
processor.processReadBuffer();
byte[] result = output.toByteArray();
assertTrue(result.length > 3);
assertEquals((byte) AID_NO, result[0]);
// Find ORDER_SFE
boolean foundTr = false;
for (int i = 3; i < result.length - 2; i++) {
if ((result[i] & 0xFF) == ORDER_SFE) {
int pairCount = result[i + 1] & 0xFF;
int pos = i + 2;
for (int p = 0; p < pairCount; p++) {
int attrType = result[pos] & 0xFF;
int attrVal = result[pos + 1] & 0xFF;
if (attrType == XA_TRANSPARENCY && attrVal == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
foundTr = true;
}
pos += 2;
}
}
}
assertTrue(foundTr, "Read Buffer in SF_SRM_XFIELD mode should report XA_TRANSPARENCY");
}
@Test
public void testReadBufferCharacterModeIncludesTransparency() {
screen.setReplyMode((byte) SF_SRM_CHAR);
screen.getCell(0).ec = (byte) 0xC1;
screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE;
processor.processReadBuffer();
byte[] result = output.toByteArray();
assertTrue(result.length > 3);
boolean foundSaTr = false;
for (int i = 3; i < result.length - 2; i++) {
if ((result[i] & 0xFF) == ORDER_SA && (result[i + 1] & 0xFF) == XA_TRANSPARENCY && (result[i + 2] & 0xFF) == (ExtendedAttribute.TR_OPAQUE & 0xFF)) {
foundSaTr = true;
break;
}
}
assertTrue(foundSaTr, "Read Buffer in SF_SRM_CHAR mode should output ORDER_SA for XA_TRANSPARENCY");
}
}