green-screen-proxy 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +23 -4
- package/dist/cli.js.map +1 -0
- package/dist/deploy.d.ts +2 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +252 -0
- package/dist/deploy.js.map +1 -0
- package/dist/hp6530/connection.d.ts +1 -0
- package/dist/hp6530/connection.d.ts.map +1 -0
- package/dist/hp6530/connection.js +1 -0
- package/dist/hp6530/connection.js.map +1 -0
- package/dist/hp6530/constants.d.ts +1 -0
- package/dist/hp6530/constants.d.ts.map +1 -0
- package/dist/hp6530/constants.js +1 -0
- package/dist/hp6530/constants.js.map +1 -0
- package/dist/hp6530/encoder.d.ts +1 -0
- package/dist/hp6530/encoder.d.ts.map +1 -0
- package/dist/hp6530/encoder.js +1 -0
- package/dist/hp6530/encoder.js.map +1 -0
- package/dist/hp6530/parser.d.ts +1 -0
- package/dist/hp6530/parser.d.ts.map +1 -0
- package/dist/hp6530/parser.js +17 -15
- package/dist/hp6530/parser.js.map +1 -0
- package/dist/hp6530/screen.d.ts +1 -0
- package/dist/hp6530/screen.d.ts.map +1 -0
- package/dist/hp6530/screen.js +1 -0
- package/dist/hp6530/screen.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/mock-routes.d.ts +1 -0
- package/dist/mock/mock-routes.d.ts.map +1 -0
- package/dist/mock/mock-routes.js +1 -0
- package/dist/mock/mock-routes.js.map +1 -0
- package/dist/protocols/hp6530-handler.d.ts +3 -1
- package/dist/protocols/hp6530-handler.d.ts.map +1 -0
- package/dist/protocols/hp6530-handler.js +1 -0
- package/dist/protocols/hp6530-handler.js.map +1 -0
- package/dist/protocols/index.d.ts +5 -2
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/index.js +1 -0
- package/dist/protocols/index.js.map +1 -0
- package/dist/protocols/tn3270-handler.d.ts +3 -1
- package/dist/protocols/tn3270-handler.d.ts.map +1 -0
- package/dist/protocols/tn3270-handler.js +1 -0
- package/dist/protocols/tn3270-handler.js.map +1 -0
- package/dist/protocols/tn5250-handler.d.ts +25 -1
- package/dist/protocols/tn5250-handler.d.ts.map +1 -0
- package/dist/protocols/tn5250-handler.js +150 -1
- package/dist/protocols/tn5250-handler.js.map +1 -0
- package/dist/protocols/types.d.ts +3 -23
- package/dist/protocols/types.d.ts.map +1 -0
- package/dist/protocols/types.js +1 -0
- package/dist/protocols/types.js.map +1 -0
- package/dist/protocols/vt-handler.d.ts +3 -1
- package/dist/protocols/vt-handler.d.ts.map +1 -0
- package/dist/protocols/vt-handler.js +1 -0
- package/dist/protocols/vt-handler.js.map +1 -0
- package/dist/routes.d.ts +1 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +1 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +13 -28
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +5 -10
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1 -0
- package/dist/session.js.map +1 -0
- package/dist/standalone.d.ts +3 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +6 -0
- package/dist/standalone.js.map +1 -0
- package/dist/tn3270/connection.d.ts +1 -0
- package/dist/tn3270/connection.d.ts.map +1 -0
- package/dist/tn3270/connection.js +1 -0
- package/dist/tn3270/connection.js.map +1 -0
- package/dist/tn3270/constants.d.ts +1 -0
- package/dist/tn3270/constants.d.ts.map +1 -0
- package/dist/tn3270/constants.js +1 -0
- package/dist/tn3270/constants.js.map +1 -0
- package/dist/tn3270/encoder.d.ts +1 -0
- package/dist/tn3270/encoder.d.ts.map +1 -0
- package/dist/tn3270/encoder.js +1 -0
- package/dist/tn3270/encoder.js.map +1 -0
- package/dist/tn3270/parser.d.ts +1 -0
- package/dist/tn3270/parser.d.ts.map +1 -0
- package/dist/tn3270/parser.js +1 -0
- package/dist/tn3270/parser.js.map +1 -0
- package/dist/tn3270/screen.d.ts +1 -0
- package/dist/tn3270/screen.d.ts.map +1 -0
- package/dist/tn3270/screen.js +1 -0
- package/dist/tn3270/screen.js.map +1 -0
- package/dist/tn5250/connection.d.ts +1 -0
- package/dist/tn5250/connection.d.ts.map +1 -0
- package/dist/tn5250/connection.js +7 -8
- package/dist/tn5250/connection.js.map +1 -0
- package/dist/tn5250/constants.d.ts +1 -0
- package/dist/tn5250/constants.d.ts.map +1 -0
- package/dist/tn5250/constants.js +1 -0
- package/dist/tn5250/constants.js.map +1 -0
- package/dist/tn5250/ebcdic.d.ts +2 -0
- package/dist/tn5250/ebcdic.d.ts.map +1 -0
- package/dist/tn5250/ebcdic.js +25 -0
- package/dist/tn5250/ebcdic.js.map +1 -0
- package/dist/tn5250/encoder.d.ts +10 -1
- package/dist/tn5250/encoder.d.ts.map +1 -0
- package/dist/tn5250/encoder.js +17 -19
- package/dist/tn5250/encoder.js.map +1 -0
- package/dist/tn5250/parser.d.ts +31 -5
- package/dist/tn5250/parser.d.ts.map +1 -0
- package/dist/tn5250/parser.js +234 -79
- package/dist/tn5250/parser.js.map +1 -0
- package/dist/tn5250/screen.d.ts +12 -0
- package/dist/tn5250/screen.d.ts.map +1 -0
- package/dist/tn5250/screen.js +29 -2
- package/dist/tn5250/screen.js.map +1 -0
- package/dist/vt/connection.d.ts +1 -0
- package/dist/vt/connection.d.ts.map +1 -0
- package/dist/vt/connection.js +1 -0
- package/dist/vt/connection.js.map +1 -0
- package/dist/vt/constants.d.ts +1 -0
- package/dist/vt/constants.d.ts.map +1 -0
- package/dist/vt/constants.js +1 -0
- package/dist/vt/constants.js.map +1 -0
- package/dist/vt/encoder.d.ts +1 -0
- package/dist/vt/encoder.d.ts.map +1 -0
- package/dist/vt/encoder.js +1 -0
- package/dist/vt/encoder.js.map +1 -0
- package/dist/vt/parser.d.ts +1 -0
- package/dist/vt/parser.d.ts.map +1 -0
- package/dist/vt/parser.js +35 -33
- package/dist/vt/parser.js.map +1 -0
- package/dist/vt/screen.d.ts +1 -0
- package/dist/vt/screen.d.ts.map +1 -0
- package/dist/vt/screen.js +1 -0
- package/dist/vt/screen.js.map +1 -0
- package/dist/websocket.d.ts +1 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +241 -14
- package/dist/websocket.js.map +1 -0
- package/package.json +9 -6
package/dist/tn5250/encoder.js
CHANGED
|
@@ -19,14 +19,10 @@ export class TN5250Encoder {
|
|
|
19
19
|
if (aidByte === undefined)
|
|
20
20
|
return null;
|
|
21
21
|
const parts = [];
|
|
22
|
-
// Row and column of cursor (1-based for the protocol)
|
|
23
22
|
const cursorRow = this.screen.cursorRow;
|
|
24
23
|
const cursorCol = this.screen.cursorCol;
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
// GDS header (6 bytes) + response data
|
|
28
|
-
const header = this.buildGDSHeader();
|
|
29
|
-
parts.push(header);
|
|
24
|
+
// GDS header: length(2) + record_type(2) + var(1) + reserved(1) + opcode(1)
|
|
25
|
+
parts.push(this.buildGDSHeader());
|
|
30
26
|
// Cursor position + AID byte
|
|
31
27
|
parts.push(Buffer.from([cursorRow, cursorCol, aidByte]));
|
|
32
28
|
// For certain aid keys (like SysReq), no field data is sent
|
|
@@ -40,16 +36,16 @@ export class TN5250Encoder {
|
|
|
40
36
|
if (!this.screen.isInputField(field))
|
|
41
37
|
continue;
|
|
42
38
|
// SBA order to indicate field position
|
|
43
|
-
parts.push(Buffer.from([0x11, field.row, field.col]));
|
|
39
|
+
parts.push(Buffer.from([0x11, field.row, field.col]));
|
|
44
40
|
// Field data in EBCDIC
|
|
45
41
|
const value = this.screen.getFieldValue(field);
|
|
46
42
|
const ebcdicData = Buffer.alloc(value.length);
|
|
47
43
|
for (let i = 0; i < value.length; i++) {
|
|
48
44
|
ebcdicData[i] = charToEbcdic(value[i]);
|
|
49
45
|
}
|
|
50
|
-
// Trim trailing spaces
|
|
46
|
+
// Trim trailing spaces and nulls
|
|
51
47
|
let trimLen = ebcdicData.length;
|
|
52
|
-
while (trimLen > 0 && ebcdicData[trimLen - 1] === EBCDIC_SPACE) {
|
|
48
|
+
while (trimLen > 0 && (ebcdicData[trimLen - 1] === EBCDIC_SPACE || ebcdicData[trimLen - 1] === 0x00)) {
|
|
53
49
|
trimLen--;
|
|
54
50
|
}
|
|
55
51
|
if (trimLen > 0) {
|
|
@@ -59,24 +55,25 @@ export class TN5250Encoder {
|
|
|
59
55
|
return this.wrapWithEOR(Buffer.concat(parts));
|
|
60
56
|
}
|
|
61
57
|
/**
|
|
62
|
-
* Build a GDS
|
|
58
|
+
* Build a GDS header for a client response (matching tn5250j format).
|
|
59
|
+
* 10-byte header:
|
|
60
|
+
* Bytes 0-1: record length (filled by wrapWithEOR)
|
|
61
|
+
* Bytes 2-3: record type 0x12A0 (SNA GDS Variable)
|
|
62
|
+
* Bytes 4-5: reserved 0x0000
|
|
63
|
+
* Byte 6: sub-header length 0x04
|
|
64
|
+
* Byte 7: flags 0x00
|
|
65
|
+
* Byte 8: reserved 0x00
|
|
66
|
+
* Byte 9: opcode 0x03 (PUT/GET response)
|
|
63
67
|
*/
|
|
64
68
|
buildGDSHeader() {
|
|
65
|
-
|
|
66
|
-
// The actual header format:
|
|
67
|
-
// Bytes 0-1: record length (will be filled)
|
|
68
|
-
// Bytes 2-3: record type = 0x12A0
|
|
69
|
-
// Byte 4: variable indicator (0x00)
|
|
70
|
-
// Byte 5: reserved (0x00)
|
|
71
|
-
// Byte 6: opcode (0x00 for response, 0x04 for response to save screen)
|
|
72
|
-
return Buffer.from([0x00, 0x00, 0x12, 0xA0, 0x00, 0x00, 0x00]);
|
|
69
|
+
return Buffer.from([0x00, 0x00, 0x12, 0xA0, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03]);
|
|
73
70
|
}
|
|
74
71
|
/**
|
|
75
72
|
* Wrap data with Telnet IAC EOR framing.
|
|
76
73
|
* Also escapes any 0xFF bytes in the data as IAC IAC.
|
|
77
74
|
*/
|
|
78
75
|
wrapWithEOR(data) {
|
|
79
|
-
// Update
|
|
76
|
+
// Update GDS record length in the first 2 bytes (includes itself)
|
|
80
77
|
if (data.length >= 2) {
|
|
81
78
|
const len = data.length;
|
|
82
79
|
data[0] = (len >> 8) & 0xFF;
|
|
@@ -119,3 +116,4 @@ export class TN5250Encoder {
|
|
|
119
116
|
return true;
|
|
120
117
|
}
|
|
121
118
|
}
|
|
119
|
+
//# sourceMappingURL=encoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/tn5250/encoder.ts"],"names":[],"mappings":"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"}
|
package/dist/tn5250/parser.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { ScreenBuffer } from './screen.js';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class TN5250Parser {
|
|
6
6
|
private screen;
|
|
7
|
+
/** When true, the next SF order should clear stale fields first */
|
|
8
|
+
private pendingFieldsClear;
|
|
7
9
|
constructor(screen: ScreenBuffer);
|
|
8
10
|
/**
|
|
9
11
|
* Parse a complete 5250 record (after Telnet framing is removed).
|
|
@@ -12,6 +14,14 @@ export declare class TN5250Parser {
|
|
|
12
14
|
parseRecord(record: Buffer): boolean;
|
|
13
15
|
/** Try to parse data that doesn't have a proper GDS header */
|
|
14
16
|
private tryParseRawData;
|
|
17
|
+
/**
|
|
18
|
+
* Handle records with non-standard framing (e.g. opcode 0x04 from
|
|
19
|
+
* pub400.com). These records contain valid commands (CLEAR_UNIT, WTD)
|
|
20
|
+
* but with extra sub-record marker bytes (0x04) between the GDS header
|
|
21
|
+
* and the actual commands. We scan for the first known command byte,
|
|
22
|
+
* skipping 0x04 markers, and hand off to parseCommands.
|
|
23
|
+
*/
|
|
24
|
+
private parseCommandsFromOffset;
|
|
15
25
|
/** Parse one or more 5250 commands starting at offset */
|
|
16
26
|
private parseCommands;
|
|
17
27
|
/**
|
|
@@ -19,15 +29,31 @@ export declare class TN5250Parser {
|
|
|
19
29
|
* Updates the screen buffer and returns the new position.
|
|
20
30
|
*/
|
|
21
31
|
private parseOrders;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Parse a bare field attribute byte (0x20-0x3F) that appears after SBA.
|
|
34
|
+
* In basic 5250, this is just 1 byte — no FFW/FCW follows.
|
|
35
|
+
* FFW/FCW are only present with explicit SF (Start Field, 0x1D) order.
|
|
36
|
+
*/
|
|
25
37
|
private parseFieldAttribute;
|
|
26
|
-
/**
|
|
27
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Parse SF (Start Field) order with FFW and optional FCW.
|
|
40
|
+
* Format: SF(0x1D) FFW1 FFW2 [FCW1 FCW2]
|
|
41
|
+
* FFW1 always has bit 6 set (0x40+). No trailing attribute byte.
|
|
42
|
+
* Display attribute is derived from FFW2.
|
|
43
|
+
*/
|
|
44
|
+
private parseStartField;
|
|
45
|
+
/**
|
|
46
|
+
* Decode a display attribute byte (0x20–0x3F) into an ATTR constant.
|
|
47
|
+
* Only recognises the bits that determine display type; falls back to
|
|
48
|
+
* the SA context (displayAttr) for modifier-only bytes (0x30, 0x38, etc.).
|
|
49
|
+
*/
|
|
50
|
+
private decodeDisplayAttr;
|
|
51
|
+
/** Clear stale fields when the first SF order arrives after a Reset MDT WTD */
|
|
52
|
+
private clearStaleFieldsOnce;
|
|
28
53
|
/**
|
|
29
54
|
* After parsing a complete screen, calculate field lengths.
|
|
30
55
|
* Call this after all records for a screen have been parsed.
|
|
31
56
|
*/
|
|
32
57
|
calculateFieldLengths(): void;
|
|
33
58
|
}
|
|
59
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/tn5250/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAY,MAAM,aAAa,CAAC;AAIrD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;IAC7B,mEAAmE;IACnE,OAAO,CAAC,kBAAkB,CAAS;gBAEvB,MAAM,EAAE,YAAY;IAIhC;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IA6EpC,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAKvB;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAiB/B,yDAAyD;IACzD,OAAO,CAAC,aAAa;IAkGrB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAqKnB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAsC3B;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAuEvB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAQzB,+EAA+E;IAC/E,OAAO,CAAC,oBAAoB;IAO5B;;;OAGG;IACH,qBAAqB,IAAI,IAAI;CA4E9B"}
|
package/dist/tn5250/parser.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { CMD, ORDER, OPCODE, ATTR } from './constants.js';
|
|
2
|
-
import { ebcdicToChar } from './ebcdic.js';
|
|
2
|
+
import { ebcdicToChar, ebcdicSymbolChar } from './ebcdic.js';
|
|
3
3
|
/**
|
|
4
4
|
* Parses 5250 data stream records and updates the screen buffer.
|
|
5
5
|
*/
|
|
6
6
|
export class TN5250Parser {
|
|
7
7
|
screen;
|
|
8
|
+
/** When true, the next SF order should clear stale fields first */
|
|
9
|
+
pendingFieldsClear = false;
|
|
8
10
|
constructor(screen) {
|
|
9
11
|
this.screen = screen;
|
|
10
12
|
}
|
|
@@ -31,32 +33,48 @@ export class TN5250Parser {
|
|
|
31
33
|
}
|
|
32
34
|
const recordLen = (record[0] << 8) | record[1];
|
|
33
35
|
const recordType = (record[2] << 8) | record[3];
|
|
34
|
-
const varFlag = record[4];
|
|
35
|
-
const reserved = record[5];
|
|
36
|
-
const opcode = record[6];
|
|
37
36
|
// Check for GDS record type
|
|
38
37
|
if (recordType !== 0x12A0) {
|
|
39
|
-
// Not a GDS record — might be raw data or something else
|
|
40
38
|
return this.tryParseRawData(record);
|
|
41
39
|
}
|
|
40
|
+
// Determine header layout: standard 10-byte header has a 4-byte sub-header
|
|
41
|
+
// at bytes 6-9: [sub_header_len=0x04][flags][reserved][opcode]
|
|
42
|
+
// Data starts at byte 10. Older/simpler records may use 7-byte layout.
|
|
43
|
+
let opcode;
|
|
44
|
+
let dataOffset;
|
|
45
|
+
if (record.length >= 10 && record[6] === 0x04) {
|
|
46
|
+
// 10-byte header with 4-byte sub-header
|
|
47
|
+
opcode = record[9];
|
|
48
|
+
dataOffset = 10;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// 7-byte header (opcode at byte 6)
|
|
52
|
+
opcode = record[6];
|
|
53
|
+
dataOffset = 7;
|
|
54
|
+
}
|
|
42
55
|
let modified = false;
|
|
43
56
|
switch (opcode) {
|
|
44
57
|
case OPCODE.OUTPUT:
|
|
45
58
|
case OPCODE.PUT_GET:
|
|
46
|
-
//
|
|
47
|
-
|
|
59
|
+
// Use parseCommandsFromOffset to scan for CLEAR_UNIT/WTD within the data.
|
|
60
|
+
// The data may contain sub-record markers (0x04) before the actual commands,
|
|
61
|
+
// which parseCommands would misinterpret. parseCommandsFromOffset handles this
|
|
62
|
+
// by scanning for known command bytes.
|
|
63
|
+
if (record.length > dataOffset) {
|
|
64
|
+
modified = this.parseCommandsFromOffset(record, dataOffset);
|
|
65
|
+
}
|
|
48
66
|
break;
|
|
49
67
|
case OPCODE.INVITE:
|
|
50
|
-
// Invite: server is ready for input (no screen data typically)
|
|
51
68
|
break;
|
|
52
69
|
case OPCODE.SAVE_SCREEN:
|
|
53
70
|
case OPCODE.RESTORE_SCREEN:
|
|
54
|
-
|
|
71
|
+
if (record.length > dataOffset) {
|
|
72
|
+
modified = this.parseCommandsFromOffset(record, dataOffset);
|
|
73
|
+
}
|
|
55
74
|
break;
|
|
56
75
|
default:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
modified = this.parseCommands(record, 7);
|
|
76
|
+
if (record.length > dataOffset) {
|
|
77
|
+
modified = this.parseCommandsFromOffset(record, dataOffset);
|
|
60
78
|
}
|
|
61
79
|
break;
|
|
62
80
|
}
|
|
@@ -67,6 +85,30 @@ export class TN5250Parser {
|
|
|
67
85
|
// Some servers send command data without the full GDS wrapper
|
|
68
86
|
return this.parseCommands(record, 0);
|
|
69
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Handle records with non-standard framing (e.g. opcode 0x04 from
|
|
90
|
+
* pub400.com). These records contain valid commands (CLEAR_UNIT, WTD)
|
|
91
|
+
* but with extra sub-record marker bytes (0x04) between the GDS header
|
|
92
|
+
* and the actual commands. We scan for the first known command byte,
|
|
93
|
+
* skipping 0x04 markers, and hand off to parseCommands.
|
|
94
|
+
*/
|
|
95
|
+
parseCommandsFromOffset(data, start) {
|
|
96
|
+
for (let i = start; i < data.length; i++) {
|
|
97
|
+
// Skip sub-record markers (0x04)
|
|
98
|
+
if (data[i] === 0x04)
|
|
99
|
+
continue;
|
|
100
|
+
// Known command bytes — hand off to normal parsing
|
|
101
|
+
if (data[i] === CMD.WRITE_TO_DISPLAY ||
|
|
102
|
+
data[i] === CMD.CLEAR_UNIT ||
|
|
103
|
+
data[i] === CMD.CLEAR_UNIT_ALT ||
|
|
104
|
+
data[i] === CMD.WRITE_STRUCTURED_FIELD ||
|
|
105
|
+
data[i] === CMD.WRITE_ERROR_CODE ||
|
|
106
|
+
data[i] === CMD.WRITE_ERROR_CODE_WIN) {
|
|
107
|
+
return this.parseCommands(data, i);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
70
112
|
/** Parse one or more 5250 commands starting at offset */
|
|
71
113
|
parseCommands(data, offset) {
|
|
72
114
|
let pos = offset;
|
|
@@ -96,6 +138,8 @@ export class TN5250Parser {
|
|
|
96
138
|
for (const f of this.screen.fields) {
|
|
97
139
|
f.modified = false;
|
|
98
140
|
}
|
|
141
|
+
// Mark that subsequent SF orders in this WTD should clear stale fields
|
|
142
|
+
this.pendingFieldsClear = true;
|
|
99
143
|
}
|
|
100
144
|
// CC1 bit 6: Clear all input fields (null fill)
|
|
101
145
|
if (cc1 & 0x40) {
|
|
@@ -160,14 +204,14 @@ export class TN5250Parser {
|
|
|
160
204
|
parseOrders(data, pos) {
|
|
161
205
|
let currentAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
|
|
162
206
|
let currentAttr = ATTR.NORMAL;
|
|
207
|
+
let useSymbolCharSet = false; // SA type 0x22 can switch to APL/symbol CGCS
|
|
208
|
+
let afterSBA = false; // Track if we just processed an SBA order (field attrs follow SBA)
|
|
163
209
|
while (pos < data.length) {
|
|
164
210
|
const byte = data[pos];
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
211
|
+
// Within a WTD, all bytes are orders or EBCDIC data.
|
|
212
|
+
// Command bytes like CLEAR_UNIT (0x40 = EBCDIC space) and WTD (0x11 = SBA)
|
|
213
|
+
// overlap with valid order/data values, so we cannot break on them.
|
|
214
|
+
// parseOrders consumes data until the end of the buffer.
|
|
171
215
|
switch (byte) {
|
|
172
216
|
case ORDER.SBA: {
|
|
173
217
|
// Set Buffer Address: 2 bytes follow (row, col)
|
|
@@ -177,7 +221,8 @@ export class TN5250Parser {
|
|
|
177
221
|
const row = data[pos++];
|
|
178
222
|
const col = data[pos++];
|
|
179
223
|
currentAddr = this.screen.offset(row, col);
|
|
180
|
-
|
|
224
|
+
afterSBA = true; // Field attribute may follow
|
|
225
|
+
continue; // skip the afterSBA = false at end of loop
|
|
181
226
|
}
|
|
182
227
|
case ORDER.IC: {
|
|
183
228
|
// Insert Cursor: set cursor to current address
|
|
@@ -230,13 +275,14 @@ export class TN5250Parser {
|
|
|
230
275
|
break;
|
|
231
276
|
}
|
|
232
277
|
case ORDER.SOH: {
|
|
233
|
-
// Start of Header: variable-length header for input fields
|
|
278
|
+
// Start of Header: variable-length header for input fields.
|
|
279
|
+
// Format: [0x01] [length] [data...]
|
|
280
|
+
// The length byte includes SOH byte + itself, so remaining = length - 2.
|
|
234
281
|
if (pos + 1 >= data.length)
|
|
235
282
|
return data.length;
|
|
236
283
|
pos++;
|
|
237
284
|
const hdrLen = data[pos++];
|
|
238
|
-
|
|
239
|
-
pos += Math.max(0, hdrLen - 2); // length includes the length byte itself sometimes
|
|
285
|
+
pos += Math.max(0, hdrLen - 2);
|
|
240
286
|
break;
|
|
241
287
|
}
|
|
242
288
|
case ORDER.TD: {
|
|
@@ -259,35 +305,49 @@ export class TN5250Parser {
|
|
|
259
305
|
const attrValue = data[pos++];
|
|
260
306
|
// Apply attribute at current position
|
|
261
307
|
this.screen.setAttrAt(currentAddr, attrValue);
|
|
262
|
-
|
|
308
|
+
// Preserve afterSBA — WEA can appear between SBA and a field attribute
|
|
309
|
+
continue;
|
|
263
310
|
}
|
|
264
311
|
case ORDER.SA: {
|
|
265
312
|
// Set Attribute: 2 bytes (attr type + value)
|
|
313
|
+
// Type 0x00 = all/reset, 0x20 = extended highlighting,
|
|
314
|
+
// 0x21 = foreground color, 0x22 = character set (CGCS).
|
|
315
|
+
// Only update display attribute for types that affect it.
|
|
266
316
|
if (pos + 2 >= data.length)
|
|
267
317
|
return data.length;
|
|
268
318
|
pos++;
|
|
269
319
|
const saType = data[pos++];
|
|
270
320
|
const saValue = data[pos++];
|
|
271
|
-
|
|
272
|
-
|
|
321
|
+
if (saType === 0x00) {
|
|
322
|
+
currentAttr = saValue;
|
|
323
|
+
useSymbolCharSet = false; // reset
|
|
324
|
+
}
|
|
325
|
+
else if (saType === 0x20) {
|
|
326
|
+
currentAttr = saValue;
|
|
327
|
+
}
|
|
328
|
+
else if (saType === 0x22) {
|
|
329
|
+
// Character set (CGCS) — non-default means APL/symbol glyphs
|
|
330
|
+
useSymbolCharSet = saValue !== 0x00;
|
|
331
|
+
}
|
|
332
|
+
// Preserve afterSBA — SA sets color/highlight context before a field attribute
|
|
333
|
+
continue;
|
|
273
334
|
}
|
|
274
335
|
case ORDER.SF: {
|
|
275
|
-
// Start Field:
|
|
276
|
-
// Actually, in 5250, SF isn't always 0x1D. The field definition
|
|
277
|
-
// comes after SBA as attribute byte (0x20-0x3F range).
|
|
278
|
-
// But let's handle explicit SF if encountered:
|
|
336
|
+
// Start Field: explicit SF order with FFW + optional FCW
|
|
279
337
|
pos++;
|
|
280
|
-
pos = this.
|
|
338
|
+
pos = this.parseStartField(data, pos, currentAddr, currentAttr);
|
|
339
|
+
currentAddr++; // attribute byte occupies one screen position
|
|
281
340
|
break;
|
|
282
341
|
}
|
|
283
342
|
default: {
|
|
284
|
-
//
|
|
285
|
-
if (byte >= 0x20 && byte <= 0x3F
|
|
343
|
+
// Field attribute bytes (0x20-0x3F) only appear immediately after SBA
|
|
344
|
+
if (afterSBA && byte >= 0x20 && byte <= 0x3F) {
|
|
286
345
|
pos = this.parseFieldAttribute(data, pos, currentAddr, currentAttr);
|
|
346
|
+
currentAddr++; // attribute byte occupies one screen position
|
|
287
347
|
}
|
|
288
348
|
else {
|
|
289
349
|
// Regular EBCDIC character data
|
|
290
|
-
const ch = ebcdicToChar(byte);
|
|
350
|
+
const ch = useSymbolCharSet ? ebcdicSymbolChar(byte) : ebcdicToChar(byte);
|
|
291
351
|
if (currentAddr < this.screen.size) {
|
|
292
352
|
this.screen.setCharAt(currentAddr, ch);
|
|
293
353
|
this.screen.setAttrAt(currentAddr, currentAttr);
|
|
@@ -298,84 +358,138 @@ export class TN5250Parser {
|
|
|
298
358
|
break;
|
|
299
359
|
}
|
|
300
360
|
}
|
|
361
|
+
afterSBA = false;
|
|
301
362
|
}
|
|
302
363
|
return pos;
|
|
303
364
|
}
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const byte = data[pos];
|
|
310
|
-
// 0x20 is very common as both space and attribute — don't treat as field attr
|
|
311
|
-
if (byte === 0x20)
|
|
312
|
-
return false;
|
|
313
|
-
// For other bytes in 0x21-0x3F range, check if followed by FFW-like bytes
|
|
314
|
-
if (pos + 2 < data.length) {
|
|
315
|
-
const next = data[pos + 1];
|
|
316
|
-
const after = data[pos + 2];
|
|
317
|
-
// FFW first byte typically has specific bit patterns
|
|
318
|
-
// If followed by reasonable FFW bytes, treat as field attribute
|
|
319
|
-
return (next & 0x40) !== 0 || next === 0x00;
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
/** Parse a field attribute byte and the following FFW/FCW */
|
|
365
|
+
/**
|
|
366
|
+
* Parse a bare field attribute byte (0x20-0x3F) that appears after SBA.
|
|
367
|
+
* In basic 5250, this is just 1 byte — no FFW/FCW follows.
|
|
368
|
+
* FFW/FCW are only present with explicit SF (Start Field, 0x1D) order.
|
|
369
|
+
*/
|
|
324
370
|
parseFieldAttribute(data, pos, addr, displayAttr) {
|
|
325
371
|
const attrByte = data[pos++];
|
|
326
372
|
// The attribute byte occupies a position on screen (but is not displayed)
|
|
327
|
-
// Mark this position with the attribute
|
|
328
373
|
if (addr < this.screen.size) {
|
|
329
|
-
this.screen.setCharAt(addr, ' ');
|
|
374
|
+
this.screen.setCharAt(addr, ' ');
|
|
375
|
+
}
|
|
376
|
+
const fieldStartAddr = addr + 1;
|
|
377
|
+
const { row, col } = this.screen.toRowCol(fieldStartAddr);
|
|
378
|
+
// Map attribute byte to display characteristic, falling back to SA context.
|
|
379
|
+
const fieldDisplayAttr = this.decodeDisplayAttr(attrByte, displayAttr);
|
|
380
|
+
// Determine input vs protected from the RAW attribute byte (not SA-enhanced).
|
|
381
|
+
// Lower 3 bits: 4-6 = underscore variants (input), 7 = non-display (input).
|
|
382
|
+
// This prevents SA context from promoting a normal/protected field to input.
|
|
383
|
+
const rawType = attrByte & 0x07;
|
|
384
|
+
const isInput = rawType >= 0x04; // underscore (4,5,6) or nondisplay (7)
|
|
385
|
+
const field = {
|
|
386
|
+
row,
|
|
387
|
+
col,
|
|
388
|
+
length: 0, // Calculated later
|
|
389
|
+
ffw1: isInput ? 0x00 : 0x20, // BYPASS bit set for output fields
|
|
390
|
+
ffw2: 0,
|
|
391
|
+
fcw1: 0,
|
|
392
|
+
fcw2: 0,
|
|
393
|
+
attribute: fieldDisplayAttr,
|
|
394
|
+
rawAttrByte: attrByte,
|
|
395
|
+
modified: false,
|
|
396
|
+
};
|
|
397
|
+
this.clearStaleFieldsOnce();
|
|
398
|
+
this.screen.fields.push(field);
|
|
399
|
+
return pos;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Parse SF (Start Field) order with FFW and optional FCW.
|
|
403
|
+
* Format: SF(0x1D) FFW1 FFW2 [FCW1 FCW2]
|
|
404
|
+
* FFW1 always has bit 6 set (0x40+). No trailing attribute byte.
|
|
405
|
+
* Display attribute is derived from FFW2.
|
|
406
|
+
*/
|
|
407
|
+
parseStartField(data, pos, addr, displayAttr) {
|
|
408
|
+
if (addr < this.screen.size) {
|
|
409
|
+
this.screen.setCharAt(addr, ' ');
|
|
330
410
|
}
|
|
331
|
-
// Parse FFW (Field Format Word) — 2 bytes
|
|
411
|
+
// Parse FFW (Field Format Word) — 2 bytes (FFW1 has bit 6 set)
|
|
332
412
|
if (pos + 1 >= data.length)
|
|
333
413
|
return pos;
|
|
334
414
|
const ffw1 = data[pos++];
|
|
335
415
|
const ffw2 = data[pos++];
|
|
336
416
|
// Check for FCW (Field Control Word) — optional, 2 bytes
|
|
417
|
+
// FCW1 has bit 7 set (>= 0x80)
|
|
337
418
|
let fcw1 = 0, fcw2 = 0;
|
|
338
419
|
if (pos + 1 < data.length) {
|
|
339
|
-
// FCW is present if the first byte has bit 7 set and is not another order
|
|
340
420
|
const maybeFcw = data[pos];
|
|
341
421
|
if (maybeFcw >= 0x80 && maybeFcw !== 0xFF) {
|
|
342
422
|
fcw1 = data[pos++];
|
|
343
423
|
fcw2 = data[pos++];
|
|
344
424
|
}
|
|
345
425
|
}
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
const fieldStartAddr = addr + 1; // Field data starts after the attribute byte
|
|
349
|
-
const { row, col } = this.screen.toRowCol(fieldStartAddr);
|
|
350
|
-
// Determine display attribute from the attribute byte
|
|
426
|
+
// Consume the trailing field attribute byte (always present after FFW/FCW).
|
|
427
|
+
// This byte (0x20–0x3F) specifies the display attribute for the field.
|
|
351
428
|
let fieldDisplayAttr = displayAttr;
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
429
|
+
let rawAttrByte = 0;
|
|
430
|
+
if (pos < data.length) {
|
|
431
|
+
const attrByte = data[pos];
|
|
432
|
+
if (attrByte >= 0x20 && attrByte <= 0x3F) {
|
|
433
|
+
pos++;
|
|
434
|
+
rawAttrByte = attrByte;
|
|
435
|
+
fieldDisplayAttr = this.decodeDisplayAttr(attrByte, displayAttr);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const fieldStartAddr = addr + 1;
|
|
439
|
+
const { row, col } = this.screen.toRowCol(fieldStartAddr);
|
|
440
|
+
// After SF + FFW + optional FCW + ATTR, the host may include a few stale
|
|
441
|
+
// bytes (field content initializers like nulls) before the next SBA order.
|
|
442
|
+
// These should not be displayed. Scan ahead (up to 4 bytes) for the next
|
|
443
|
+
// SBA — if found, skip everything in between.
|
|
444
|
+
{
|
|
445
|
+
let scan = pos;
|
|
446
|
+
const limit = Math.min(pos + 4, data.length);
|
|
447
|
+
while (scan < limit) {
|
|
448
|
+
if (data[scan] === ORDER.SBA) {
|
|
449
|
+
pos = scan; // skip stale bytes
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
scan++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
361
455
|
const field = {
|
|
362
456
|
row,
|
|
363
457
|
col,
|
|
364
|
-
length: 0,
|
|
458
|
+
length: 0,
|
|
365
459
|
ffw1,
|
|
366
460
|
ffw2,
|
|
367
461
|
fcw1,
|
|
368
462
|
fcw2,
|
|
369
463
|
attribute: fieldDisplayAttr,
|
|
464
|
+
rawAttrByte,
|
|
370
465
|
modified: false,
|
|
371
466
|
};
|
|
467
|
+
this.clearStaleFieldsOnce();
|
|
372
468
|
this.screen.fields.push(field);
|
|
373
469
|
return pos;
|
|
374
470
|
}
|
|
375
|
-
/**
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Decode a display attribute byte (0x20–0x3F) into an ATTR constant.
|
|
473
|
+
* Only recognises the bits that determine display type; falls back to
|
|
474
|
+
* the SA context (displayAttr) for modifier-only bytes (0x30, 0x38, etc.).
|
|
475
|
+
*/
|
|
476
|
+
decodeDisplayAttr(attrByte, displayAttr = ATTR.NORMAL) {
|
|
477
|
+
if ((attrByte & 0x07) === 0x07)
|
|
478
|
+
return ATTR.NON_DISPLAY;
|
|
479
|
+
if (attrByte & 0x04)
|
|
480
|
+
return ATTR.UNDERSCORE;
|
|
481
|
+
if (attrByte & 0x02)
|
|
482
|
+
return ATTR.HIGH_INTENSITY;
|
|
483
|
+
if (attrByte & 0x08)
|
|
484
|
+
return ATTR.HIGH_INTENSITY;
|
|
485
|
+
return displayAttr;
|
|
486
|
+
}
|
|
487
|
+
/** Clear stale fields when the first SF order arrives after a Reset MDT WTD */
|
|
488
|
+
clearStaleFieldsOnce() {
|
|
489
|
+
if (this.pendingFieldsClear) {
|
|
490
|
+
this.screen.fields = [];
|
|
491
|
+
this.pendingFieldsClear = false;
|
|
492
|
+
}
|
|
379
493
|
}
|
|
380
494
|
/**
|
|
381
495
|
* After parsing a complete screen, calculate field lengths.
|
|
@@ -385,6 +499,12 @@ export class TN5250Parser {
|
|
|
385
499
|
const fields = this.screen.fields;
|
|
386
500
|
if (fields.length === 0)
|
|
387
501
|
return;
|
|
502
|
+
// Sort fields by screen position for correct length calculation
|
|
503
|
+
fields.sort((a, b) => {
|
|
504
|
+
const posA = this.screen.offset(a.row, a.col);
|
|
505
|
+
const posB = this.screen.offset(b.row, b.col);
|
|
506
|
+
return posA - posB;
|
|
507
|
+
});
|
|
388
508
|
for (let i = 0; i < fields.length; i++) {
|
|
389
509
|
const current = fields[i];
|
|
390
510
|
const currentStart = this.screen.offset(current.row, current.col);
|
|
@@ -396,9 +516,9 @@ export class TN5250Parser {
|
|
|
396
516
|
current.length = Math.max(0, nextStart - currentStart - 1);
|
|
397
517
|
}
|
|
398
518
|
else {
|
|
399
|
-
// Last field
|
|
400
|
-
const
|
|
401
|
-
current.length = Math.max(0,
|
|
519
|
+
// Last field wraps to first field (5250 screen is circular)
|
|
520
|
+
const firstStart = this.screen.offset(fields[0].row, fields[0].col);
|
|
521
|
+
current.length = Math.max(0, (this.screen.size - currentStart) + firstStart - 1);
|
|
402
522
|
// Cap at a reasonable maximum
|
|
403
523
|
if (current.length > this.screen.cols * 2) {
|
|
404
524
|
current.length = this.screen.cols - current.col;
|
|
@@ -408,5 +528,40 @@ export class TN5250Parser {
|
|
|
408
528
|
if (current.length <= 0)
|
|
409
529
|
current.length = 1;
|
|
410
530
|
}
|
|
531
|
+
// Ensure cursor is in a functional input field. Skip UIM framework
|
|
532
|
+
// artifact fields whose OWN attribute byte doesn't indicate underscore
|
|
533
|
+
// or non-display (they may inherit underscore from SA context but aren't
|
|
534
|
+
// real interactive fields — they exist in the panel header).
|
|
535
|
+
{
|
|
536
|
+
const allInputs = fields.filter(f => this.screen.isInputField(f));
|
|
537
|
+
if (allInputs.length > 0) {
|
|
538
|
+
const lastPos = this.screen.offset(allInputs[allInputs.length - 1].row, allInputs[allInputs.length - 1].col);
|
|
539
|
+
const functional = allInputs.filter(f => this.screen.hasNativeUnderscore(f) || this.screen.hasNativeNonDisplay(f) ||
|
|
540
|
+
this.screen.offset(f.row, f.col) === lastPos);
|
|
541
|
+
const targets = functional.length > 0 ? functional : allInputs;
|
|
542
|
+
const cursorAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
|
|
543
|
+
const inTarget = targets.some(f => {
|
|
544
|
+
const start = this.screen.offset(f.row, f.col);
|
|
545
|
+
return cursorAddr >= start && cursorAddr < start + f.length;
|
|
546
|
+
});
|
|
547
|
+
if (!inTarget) {
|
|
548
|
+
const after = targets.find(f => this.screen.offset(f.row, f.col) >= cursorAddr);
|
|
549
|
+
const target = after || targets[targets.length - 1];
|
|
550
|
+
this.screen.cursorRow = target.row;
|
|
551
|
+
this.screen.cursorCol = target.col;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Deduplicate fields at the same position (keep the last one)
|
|
556
|
+
const seen = new Map();
|
|
557
|
+
for (let i = 0; i < fields.length; i++) {
|
|
558
|
+
const key = `${fields[i].row},${fields[i].col}`;
|
|
559
|
+
seen.set(key, i);
|
|
560
|
+
}
|
|
561
|
+
if (seen.size < fields.length) {
|
|
562
|
+
const keep = new Set(seen.values());
|
|
563
|
+
this.screen.fields = fields.filter((_, i) => keep.has(i));
|
|
564
|
+
}
|
|
411
565
|
}
|
|
412
566
|
}
|
|
567
|
+
//# sourceMappingURL=parser.js.map
|