diff --git a/README.md b/README.md index 50203cc..d0eae7b 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,13 @@ Here's [a video introducing the library][introVideo] as well. Code page support ----------------- -By default, go3270 will use the EBCDIC IBM CP 1047 code page. If your clients are set to CP 1047, no changes are necessary. +When clients connect, the `NegotiateTelnet()` function returns an implementation of the `DevInfo` interface. That interface has a `DevInfo.Codepage()` method, which returns an implementation of the `Codepage` interface for the detected client code page if known and supported (`nil` otherwise). The `ScreenOpts` structure you provide to the `ShowScreenOpts()` function has a `Codepage` field, which is where you provide `DevInfo.Codepage()` to ensure the UTF-8–EBCDIC translation is correct for the particular client. If `ScreenOpts.Codepage` is `nil` (either because you don't set it, or if `DevInfo.Codepage()` returns `nil`), then go3270 will use its global default code page for that interaction with the client. The global default is the CP1047 code page. -You may globally set the go3270 code page by calling the SetCodepage() function during your application initialization (this should be set before you use the library for handling any client connections; this is a global setting, not a per-connection setting). SetCodepage() accepts a Codepage interface, which provides methods to encode Go UTF-8 strings to EBCDIC, and decode EBCDIC byte slices to Go UTF-8 strings. +Automatic code page detection support code pages 37, 924, 1047, 1140, and the x3270 "bracket" code page. Note that x3270 does not report a different code page ID between CP37 and "bracket". Since "bracket" is the default code page for x3270, go3270 will assume that if the client is in the x3270 family and the codepage is 37, "bracket" is the correct code page to use. + +All of the examples under this repository demonstrate the correct handling of code pages -- the `DevInfo` is remembered from the telnet negotiation, and `DevInfo.Codepage()` is passed to all screen send and receive calls. + +You may change the global code page default by calling the SetCodepage() function during your application initialization (this should be set before you use the library for handling any client connections; this is a global setting, not a per-connection setting). SetCodepage() accepts a Codepage interface, which provides methods to encode Go UTF-8 strings to EBCDIC, and decode EBCDIC byte slices to Go UTF-8 strings. go3270 currently provides functions that return suitable interfaces for: @@ -41,11 +45,11 @@ go3270 currently provides functions that return suitable interfaces for: * CP 924: `Codepage924()`, a variation of CP 1047 with the Euro symbol and other changes to bring it in line with ISO 8859-15 (Latin-9). * CP 1047: `Codepage1047()`, the "modern" U.S. EBCDIC code page, which maps the full ISO 8859-1 (Latin-1) character set. * CP 1140: `Codepage1140()`, which is the same as CP 37 except the Euro symbol replaces the ¤ currency sign at position 0x9F. - * brackets: `CodepageBrackets()`, which is the default c/x3270 codepage (closest to CP 1047, with with `^` and `¬` swapped back to where they are in CP 37) + * bracket: `CodepageBracket()`, which is the default c/x3270 codepage (closest to CP 1047, with with `^` and `¬` swapped back to where they are in CP 37) If there are other standard EBCDIC code pages that you would like support for, let me know. -To configure go3270 to use one of the codepages, you may do something like: +To configure go3270 to use one of the codepages by default, you may do something like: ``` import ( @@ -57,6 +61,8 @@ func init() { } ``` +But with the new codepage detection support, you shouldn't rely on the default codepage: you should always pass the `DevInfo.Codepage()` value to the `ShowScreenOpts()` function or as the last optional argument to the `HandleScreen()` or `HandleScreenAlt()` functions. The global default should only be a fallback if the client code page isn't detected correctly. + Additionally, most characters from the "graphic escape" code page 310 are supported in all of the go3270-provided codepage implementations. Correct display on the client will depend on its support of graphic escape and correct characters being available in its font. Use the corresponding Unicode characters in your Go UTF-8 strings and they will be sent as the EBCDIC two-byte sequence of 0x08 followed by the position in code page 310. GE sequences are also processed on incoming field values. 3270 information diff --git a/ebcdic.go b/ebcdic.go index dab1914..eb7c411 100644 --- a/ebcdic.go +++ b/ebcdic.go @@ -18,6 +18,11 @@ type Codepage interface { // Encode converts a UTF-8 string into a slice of EBCDIC bytes. Encode(s string) []byte + + // ID returns the name of this codepage. Usually a numeric string like + // "037" or "1047", but could also be a name such as "bracket" if IBM has + // not assigned a number to the particular codepage. + ID() string } // After careful consideration, I have decided that the default code page we @@ -41,15 +46,19 @@ type Codepage interface { // In IBM PCOMM, CP37 is the default. For correct mapping of [, ], Ý, ¨, ^, // and ¬, you must switch the session parameters from "037 United States" to // "1047 United States". -var currentCodepage Codepage = Codepage1047() +var defaultCodepage Codepage = Codepage1047() // SetCodepage sets the codepage/character set that go3270 uses. This is a // global setting, so if you're expecting clients to be configured to use a // character set other than go3270's default, cp1047, you should probably set // this during your application initialization and then leave it unchanged // after. This is _not_ a per-connection setting. +// +// For per-client codepage, set the ScreenOpts.Codepage field in the calls to +// ShowScreenOpts() or the codepage argument to HandleScreen() and +// HandleScreenAlt(). func SetCodepage(cs Codepage) { - currentCodepage = cs + defaultCodepage = cs } // Internal implementation of the Charset interface we'll use for the codepage @@ -77,6 +86,8 @@ type codepage struct { // Map of Unicode code points to graphic escape EBCDIC bytes. u2ge map[rune]byte + + id string } // Codepage037 returns an interface that implements the IBM CP 037 code page. @@ -85,6 +96,7 @@ type codepage struct { // https://raw.githubusercontent.com/unicode-org/icu-data/refs/heads/main/charset/data/ucm/glibc-IBM037-2.1.2.ucm func Codepage037() Codepage { return &codepage{ + id: "037", e2u: []rune{ /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ /* 0x */ 0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, 0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, @@ -137,6 +149,7 @@ func Codepage037() Codepage { // https://raw.githubusercontent.com/unicode-org/icu-data/refs/heads/main/charset/data/ucm/ibm-924_P100-1998.ucm func Codepage924() Codepage { return &codepage{ + id: "924", e2u: []rune{ /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ /* 0x */ 0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, 0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, @@ -194,6 +207,7 @@ func Codepage924() Codepage { // https://raw.githubusercontent.com/unicode-org/icu-data/refs/heads/main/charset/data/ucm/glibc-IBM1047-2.1.2.ucm func Codepage1047() Codepage { return &codepage{ + id: "1047", e2u: []rune{ /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ /* 0x */ 0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, 0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, @@ -247,6 +261,7 @@ func Codepage1047() Codepage { // https://raw.githubusercontent.com/unicode-org/icu-data/refs/heads/main/charset/data/ucm/ibm-1140_P100-1997.ucm func Codepage1140() Codepage { return &codepage{ + id: "1140", e2u: []rune{ /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ /* 0x */ 0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, 0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, @@ -295,14 +310,15 @@ func Codepage1140() Codepage { } } -// CodepageBrackets returns an interface that implements the "brackets" code +// CodepageBrackets returns an interface that implements the "bracket" code // page as used by c/x3270 by default. I have also seen this referred to as // CP037-2. // // See: // https://x3270.miraheze.org/wiki/Why_are_the_square_bracket_characters_displayed_wrong%3F -func CodepageBrackets() Codepage { +func CodepageBracket() Codepage { return &codepage{ + id: "bracket", e2u: []rune{ /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ /* 0x */ 0x00, 0x01, 0x02, 0x03, 0x9C, 0x09, 0x86, 0x7F, 0x97, 0x8D, 0x8E, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, @@ -462,3 +478,7 @@ func (cp *codepage) Encode(s string) []byte { return out } + +func (cp *codepage) ID() string { + return cp.id +} diff --git a/example1/example1.go b/example1/example1.go index 904d202..57d188b 100644 --- a/example1/example1.go +++ b/example1/example1.go @@ -7,7 +7,6 @@ package main import ( "fmt" "net" - "os" "strings" "time" @@ -15,8 +14,8 @@ import ( ) func init() { - // put the go3270 library in debug mode - go3270.Debug = os.Stderr + // put the go3270 library in debug mode: + // go3270.Debug = os.Stderr } // A Screen is an array of go3270.Field structs. We will build two screens, @@ -41,6 +40,9 @@ var screen1 = go3270.Screen{ {Row: 8, Col: 6, Intense: true, Content: "enter"}, {Row: 8, Col: 12, Content: "to submit your name."}, {Row: 10, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"}, // a blank field for error messages + {Row: 14, Col: 0, Content: "Detected code page:"}, + {Row: 14, Col: 20, Name: "codepage"}, + {Row: 15, Col: 0, Content: "The following should be left and right square brackets: [ ]"}, {Row: 22, Col: 0, Content: "PF3 Exit"}, } @@ -91,14 +93,21 @@ func handle(conn net.Conn) { defer conn.Close() // Always begin new connection by negotiating the telnet options - if _, err := go3270.NegotiateTelnet(conn); err != nil { + devinfo, err := go3270.NegotiateTelnet(conn) + if err != nil { fmt.Printf("ERROR: %v\n", err) return } fieldValues := make(map[string]string) + + if devinfo.Codepage() == nil { + fieldValues["codepage"] = "(unknown)" + } else { + fieldValues["codepage"] = devinfo.Codepage().ID() + } + var response go3270.Response - var err error // We will loop forever until the user quits with PF3 mainLoop: @@ -111,11 +120,16 @@ mainLoop: fieldValues["password"] = "" // Show the first screen, and wait to get a client response. Place - // the cursor at the beginning of the first input field. - // We're passing in the fieldValues map to carry values over from - // the previous submission. We could pass nil, instead, if always want + // the cursor at the beginning of the first input field. We're + // passing in the fieldValues map to carry values over from the + // previous submission. We could pass nil, instead, if always want // the fields to start out blank. - response, err = go3270.ShowScreen(screen1, fieldValues, 4, 20, conn) + response, err = go3270.ShowScreenOpts(screen1, fieldValues, conn, + go3270.ScreenOpts{ + Codepage: devinfo.Codepage(), + CursorRow: 4, + CursorCol: 20, + }) if err != nil { fmt.Println(err) return @@ -123,7 +137,11 @@ mainLoop: // If the user pressed PF3, exit if response.AID == go3270.AIDPF3 { - go3270.ShowScreenNoResponse(goodbyeScreen, nil, 0, 0, conn) + go3270.ShowScreenOpts(goodbyeScreen, nil, conn, + go3270.ScreenOpts{ + Codepage: devinfo.Codepage(), + NoResponse: true, + }) time.Sleep(2 * time.Second) break mainLoop } @@ -137,7 +155,8 @@ mainLoop: fieldValues = response.Values if strings.TrimSpace(fieldValues["fname"]) == "" && strings.TrimSpace(fieldValues["lname"]) == "" { - fieldValues["errormsg"] = "First and Last Name fields are required." + fieldValues["errormsg"] = + "First and Last Name fields are required." continue screen1Loop } if strings.TrimSpace(fieldValues["fname"]) == "" { @@ -163,12 +182,14 @@ mainLoop: if passwordLength == 1 { passwordPlural = "" } - fieldValues["passwordOutput"] = fmt.Sprintf("Your password was %d character%s long", + fieldValues["passwordOutput"] = fmt.Sprintf( + "Your password was %d character%s long", passwordLength, passwordPlural) fieldValues["position"] = fmt.Sprintf( "When you pressed enter the cursor was at row %d column %d.", response.Row+1, response.Col+1) - response, err := go3270.ShowScreen(screen2, fieldValues, 0, 0, conn) + response, err := go3270.ShowScreenOpts(screen2, fieldValues, conn, + go3270.ScreenOpts{Codepage: devinfo.Codepage()}) if err != nil { fmt.Println(err) return @@ -176,7 +197,11 @@ mainLoop: // If the user pressed PF3, exit if response.AID == go3270.AIDPF3 { - go3270.ShowScreenNoResponse(goodbyeScreen, nil, 0, 0, conn) + go3270.ShowScreenOpts(goodbyeScreen, nil, conn, + go3270.ScreenOpts{ + Codepage: devinfo.Codepage(), + NoResponse: true, + }) time.Sleep(2 * time.Second) break } diff --git a/example2/example2.go b/example2/example2.go index ee4c609..a2378a9 100644 --- a/example2/example2.go +++ b/example2/example2.go @@ -7,15 +7,14 @@ package main import ( "fmt" "net" - "os" "strings" "github.com/racingmars/go3270" ) func init() { - // put the go3270 library in debug mode - go3270.Debug = os.Stderr + // put the go3270 library in debug mode: + // go3270.Debug = os.Stderr } // A Screen is an array of go3270.Field structs. We will build two screens, @@ -90,7 +89,8 @@ func handle(conn net.Conn) { defer conn.Close() // Always begin new connection by negotiating the telnet options - if _, err := go3270.NegotiateTelnet(conn); err != nil { + devinfo, err := go3270.NegotiateTelnet(conn) + if err != nil { fmt.Println(err) return } @@ -109,7 +109,9 @@ func handle(conn net.Conn) { []go3270.AID{go3270.AIDPF3}, // keys that are "exit" keys "errormsg", // the field to write error message into 4, 20, // the row and column to place the cursor - conn) + conn, // network connection to client + devinfo.Codepage(), // Client code page + ) if err != nil { fmt.Println(err) return @@ -128,7 +130,8 @@ func handle(conn net.Conn) { if passwordLength == 1 { passwordPlural = "" } - fieldValues["passwordOutput"] = fmt.Sprintf("Your password was %d character%s long", + fieldValues["passwordOutput"] = fmt.Sprintf( + "Your password was %d character%s long", passwordLength, passwordPlural) response, err = go3270.HandleScreen( @@ -139,7 +142,7 @@ func handle(conn net.Conn) { []go3270.AID{go3270.AIDPF3}, // keys that are "exit" keys "errormsg", // the field to write error message into 0, 0, // the row and column to place the cursor - conn) + conn, devinfo.Codepage()) if err != nil { fmt.Println(err) return diff --git a/example3/example3.go b/example3/example3.go index ef9ab43..b2f9d3f 100644 --- a/example3/example3.go +++ b/example3/example3.go @@ -49,7 +49,8 @@ func handle(conn net.Conn) { defer conn.Close() // Always begin new connection by negotiating the telnet options - if _, err := go3270.NegotiateTelnet(conn); err != nil { + devinfo, err := go3270.NegotiateTelnet(conn) + if err != nil { fmt.Println(err) return } @@ -67,7 +68,11 @@ func handle(conn net.Conn) { for { screen[3].Content = time.Now().UTC().Format("15:04:05") response, err := go3270.ShowScreenOpts(screen, nil, conn, - go3270.ScreenOpts{CursorRow: 23, CursorCol: 0}) + go3270.ScreenOpts{ + CursorRow: 23, + CursorCol: 0, + Codepage: devinfo.Codepage(), + }) if err != nil { // User dropped connection, maybe? We'll end things. done <- true @@ -98,7 +103,11 @@ func handle(conn net.Conn) { // Send the updated time, without clearing the screen refresh[0].Content = time.Now().UTC().Format("15:04:05") _, err := go3270.ShowScreenOpts(refresh, nil, conn, - go3270.ScreenOpts{NoClear: true, NoResponse: true}) + go3270.ScreenOpts{ + NoClear: true, + NoResponse: true, + Codepage: devinfo.Codepage(), + }) if err != nil { // Bail out return diff --git a/example4/help.go b/example4/help.go index 74b6db8..fb26db6 100644 --- a/example4/help.go +++ b/example4/help.go @@ -48,7 +48,7 @@ var helpScreen = go3270.Screen{ // generator function for it is a stand-alone function and not a method on the // session. func help(returnTransaction go3270.Tx) go3270.Tx { - return func(conn net.Conn, _ go3270.DevInfo, data any) ( + return func(conn net.Conn, devinfo go3270.DevInfo, data any) ( go3270.Tx, any, error) { _, err := go3270.HandleScreen( helpScreen, // the screen to display @@ -58,7 +58,8 @@ func help(returnTransaction go3270.Tx) go3270.Tx { nil, "errormsg", // name of field to put error messages in 23, 79, // cursor coordinates - conn) + conn, + devinfo.Codepage()) if err != nil { return nil, nil, err } diff --git a/example4/login.go b/example4/login.go index ba9a73b..b804440 100644 --- a/example4/login.go +++ b/example4/login.go @@ -58,7 +58,7 @@ var loginScreenRules = go3270.Rules{ // login transaction accepts a string value in data if the login screen // should be initialized with an error message. -func (sess *session) login(conn net.Conn, _ go3270.DevInfo, +func (sess *session) login(conn net.Conn, devinfo go3270.DevInfo, data any) (go3270.Tx, any, error) { fieldValues := make(map[string]string) @@ -81,7 +81,8 @@ func (sess *session) login(conn net.Conn, _ go3270.DevInfo, go3270.AIDClear}, loginErr, // name of field to put error messages in 4, 16, // cursor coordinates - conn) + conn, + devinfo.Codepage()) if err != nil { return nil, nil, err } @@ -170,7 +171,7 @@ type newuserData struct { errmsg string } -func (sess *session) newuser(conn net.Conn, _ go3270.DevInfo, data any) ( +func (sess *session) newuser(conn net.Conn, devinfo go3270.DevInfo, data any) ( go3270.Tx, any, error) { fieldValues := make(map[string]string) @@ -195,7 +196,8 @@ func (sess *session) newuser(conn net.Conn, _ go3270.DevInfo, data any) ( }, loginErr, // name of field to put error messages in 4, 16, // cursor coordinates - conn) + conn, + devinfo.Codepage()) if err != nil { return nil, nil, err } diff --git a/example4/mainmenu.go b/example4/mainmenu.go index fdaca98..12b70c4 100644 --- a/example4/mainmenu.go +++ b/example4/mainmenu.go @@ -79,7 +79,7 @@ type mainmenuData struct { // mainmenu transaction accepts a mainmenuData struct as the data if the // option field or error message should be populated. -func (sess *session) mainmenu(conn net.Conn, _ go3270.DevInfo, data any) ( +func (sess *session) mainmenu(conn net.Conn, devinfo go3270.DevInfo, data any) ( go3270.Tx, any, error) { fieldValues := make(map[string]string) @@ -107,7 +107,8 @@ func (sess *session) mainmenu(conn net.Conn, _ go3270.DevInfo, data any) ( }, mainmenuError, // name of field to put error messages in 1, 13, // cursor coordinates - conn) + conn, + devinfo.Codepage()) if err != nil { return nil, nil, err } @@ -183,7 +184,7 @@ var exampleScreen = go3270.Screen{ // exampleFeature is a transaction that will act as a placeholder for real // application functionality. It accepts a string in the data which will // be displayed on the panel. -func (sess *session) exampleFeature(conn net.Conn, _ go3270.DevInfo, +func (sess *session) exampleFeature(conn net.Conn, devinfo go3270.DevInfo, data any) (go3270.Tx, any, error) { fieldValues := make(map[string]string) @@ -210,7 +211,8 @@ func (sess *session) exampleFeature(conn net.Conn, _ go3270.DevInfo, }, mainmenuError, // name of field to put error messages in 23, 79, // cursor coordinates - conn) + conn, + devinfo.Codepage()) if err != nil { return nil, nil, err } diff --git a/example5/bigscreen.go b/example5/bigscreen.go index 1a58c54..dca88c9 100644 --- a/example5/bigscreen.go +++ b/example5/bigscreen.go @@ -22,6 +22,9 @@ var biglayout = go3270.Screen{ {Row: 4, Col: 0, Content: "Terminal Type . . ."}, {Row: 4, Col: 21, Name: "termtype", Intense: true}, + {Row: 4, Col: 40, Content: "Code page . . ."}, + {Row: 4, Col: 56, Name: "codepage", Intense: true}, + {Row: 5, Col: 0, Content: "Rows . . . . . . . ."}, {Row: 5, Col: 21, Name: "rows", Intense: true}, @@ -43,6 +46,10 @@ func bigscreen(conn net.Conn, devinfo go3270.DevInfo, data any) ( rows, cols := devinfo.AltDimensions() termtype := devinfo.TerminalType() + codepage := "(unknown)" + if devinfo.Codepage() != nil { + codepage = devinfo.Codepage().ID() + } // Make a local copy of the screen definition that we can append lines to. screen := make(go3270.Screen, len(biglayout)) @@ -79,6 +86,7 @@ func bigscreen(conn net.Conn, devinfo go3270.DevInfo, data any) ( fieldValues := map[string]string{ "termtype": termtype, + "codepage": codepage, "rows": strconv.Itoa(rows), "cols": strconv.Itoa(cols), } @@ -100,8 +108,9 @@ func bigscreen(conn net.Conn, devinfo go3270.DevInfo, data any) ( }, "errormsg", // name of field to put error messages in rows-1, 18, // cursor coordinates - conn, // network connection - devinfo, // device info for alternate screen size support + conn, // network connection + devinfo, // device info for alternate screen size support + devinfo.Codepage(), // client code page ) if err != nil { return nil, nil, err diff --git a/example5/normalscreen.go b/example5/normalscreen.go index 0c6dee5..49c8438 100644 --- a/example5/normalscreen.go +++ b/example5/normalscreen.go @@ -23,6 +23,9 @@ var normallayout = go3270.Screen{ {Row: 4, Col: 0, Content: "Terminal Type . . ."}, {Row: 4, Col: 21, Name: "termtype", Intense: true}, + {Row: 4, Col: 40, Content: "Code page . . ."}, + {Row: 4, Col: 56, Name: "codepage", Intense: true}, + {Row: 5, Col: 0, Content: "Rows . . . . . . . ."}, {Row: 5, Col: 21, Name: "rows", Intense: true}, {Row: 5, Col: 28, Content: "(but currently using 24)"}, @@ -46,6 +49,10 @@ func normalscreen(conn net.Conn, devinfo go3270.DevInfo, data any) ( rows, cols := devinfo.AltDimensions() termtype := devinfo.TerminalType() + codepage := "(unknown)" + if devinfo.Codepage() != nil { + codepage = devinfo.Codepage().ID() + } // Make a local copy of the screen definition that we can append lines to. screen := make(go3270.Screen, len(normallayout)) @@ -66,6 +73,7 @@ func normalscreen(conn net.Conn, devinfo go3270.DevInfo, data any) ( fieldValues := map[string]string{ "termtype": termtype, + "codepage": codepage, "rows": strconv.Itoa(rows), "cols": strconv.Itoa(cols), } @@ -83,7 +91,8 @@ func normalscreen(conn net.Conn, devinfo go3270.DevInfo, data any) ( }, "errormsg", // name of field to put error messages in 1, 1, // cursor coordinates - conn, // network connection + conn, // network connection + devinfo.Codepage(), // client code page ) if err != nil { return nil, nil, err diff --git a/looper.go b/looper.go index d225201..ca01f03 100644 --- a/looper.go +++ b/looper.go @@ -81,6 +81,13 @@ type FieldRules struct { // user submission. // - crow and ccol are the initial cursor position. // - conn is the network connection to the 3270 client. +// - codepage is an optional argument (implemented this way as a varargs +// argument as a hack to add this feature without breaking API backward +// compatability) for the codepage to use. Typically you should pass in the +// return value from DevInfo.Codepage() each time to get the correct +// codepage that was detected when the client connected. If nil, the +// global default code page (default 1047, but changed with the +// SetCodepage() function) will be used. // // HandleScreen will return when the user: 1) presses a key in pfkeys AND all // fields pass validation, OR 2) the user presses a key in exitkeys. In all @@ -90,9 +97,9 @@ type FieldRules struct { // For alternate screen support (larger than 24x80), use HandleScreenAlt(). func HandleScreen(screen Screen, rules Rules, values map[string]string, pfkeys, exitkeys []AID, errorField string, crow, ccol int, - conn net.Conn) (Response, error) { + conn net.Conn, codepage ...Codepage) (Response, error) { return HandleScreenAlt(screen, rules, values, pfkeys, exitkeys, errorField, - crow, ccol, conn, nil) + crow, ccol, conn, nil, codepage...) } // HandleScreenAlt is identical to HandleScreen, but writes to the "alternate" @@ -102,7 +109,12 @@ func HandleScreen(screen Screen, rules Rules, values map[string]string, // terminals to the default 24x80 mode. func HandleScreenAlt(screen Screen, rules Rules, values map[string]string, pfkeys, exitkeys []AID, errorField string, crow, ccol int, - conn net.Conn, dev DevInfo) (Response, error) { + conn net.Conn, dev DevInfo, codepage ...Codepage) (Response, error) { + + var cp Codepage + if len(codepage) > 0 { + cp = codepage[0] + } // Save the original field values for any named fields to support // the MustChange rule. Also build a map of named fields. @@ -142,7 +154,8 @@ mainloop: } resp, err := ShowScreenOpts(screen, myValues, conn, - ScreenOpts{CursorRow: crow, CursorCol: ccol, AltScreen: dev}) + ScreenOpts{CursorRow: crow, CursorCol: ccol, AltScreen: dev, + Codepage: cp}) if err != nil { return resp, err } @@ -154,8 +167,8 @@ mainloop: // If we got an unexpected key, set error message and restart loop if !aidInArray(resp.AID, pfkeys) { - if !(resp.AID == AIDClear || resp.AID == AIDPA1 || resp.AID == AIDPA2 || - resp.AID == AIDPA3) { + if !(resp.AID == AIDClear || resp.AID == AIDPA1 || + resp.AID == AIDPA2 || resp.AID == AIDPA3) { myValues = mergeFieldValues(myValues, resp.Values) } myValues[errorField] = fmt.Sprintf("%s: unknown key", @@ -179,12 +192,15 @@ mainloop: if _, ok := myValues[field]; !ok { continue } - if rules[field].MustChange && myValues[field] == origValues[field] { + if rules[field].MustChange && + myValues[field] == origValues[field] { myValues[errorField] = rules[field].ErrorText continue mainloop } - if rules[field].Validator != nil && !rules[field].Validator(myValues[field]) { - myValues[errorField] = fmt.Sprintf("Value for %s is not valid", field) + if rules[field].Validator != nil && + !rules[field].Validator(myValues[field]) { + myValues[errorField] = fmt.Sprintf( + "Value for %s is not valid", field) continue mainloop } } diff --git a/response.go b/response.go index 5209179..7f32e3f 100644 --- a/response.go +++ b/response.go @@ -63,7 +63,9 @@ const ( aidQueryResponse AID = 0x88 ) -func readResponse(c net.Conn, fm fieldmap, dev DevInfo) (Response, error) { +func readResponse(c net.Conn, fm fieldmap, dev DevInfo, + cp Codepage) (Response, error) { + var r Response aid, err := readAID(c) if err != nil { @@ -92,7 +94,7 @@ func readResponse(c net.Conn, fm fieldmap, dev DevInfo) (Response, error) { r.Row = row var fieldValues map[string]string - if fieldValues, err = readFields(c, fm, cols); err != nil { + if fieldValues, err = readFields(c, fm, cols, cp); err != nil { return r, err } @@ -142,12 +144,19 @@ func readPosition(c net.Conn, cols int) (row, col, addr int, err error) { return row, col, addr, nil } -func readFields(c net.Conn, fm fieldmap, cols int) (map[string]string, error) { +func readFields(c net.Conn, fm fieldmap, cols int, + cp Codepage) (map[string]string, error) { + var infield bool var fieldpos int var fieldval bytes.Buffer var values = make(map[string]string) + // Provide default codepage + if cp == nil { + cp = defaultCodepage + } + // consume bytes until we get 0xffef for { // Read a byte @@ -160,7 +169,7 @@ func readFields(c net.Conn, fm fieldmap, cols int) (map[string]string, error) { if eor { // Finish the current field if infield { - value := currentCodepage.Decode(fieldval.Bytes()) + value := cp.Decode(fieldval.Bytes()) debugf("Field %d: %s\n", fieldpos, value) handleField(fieldpos, value, fm, values) } @@ -172,7 +181,7 @@ func readFields(c net.Conn, fm fieldmap, cols int) (map[string]string, error) { if b == 0x11 { // Finish the previous field, if necessary if infield { - value := currentCodepage.Decode(fieldval.Bytes()) + value := cp.Decode(fieldval.Bytes()) debugf("Field %d: %s\n", fieldpos, value) handleField(fieldpos, value, fm, values) } diff --git a/screen.go b/screen.go index 74f8bd1..cc71387 100644 --- a/screen.go +++ b/screen.go @@ -116,6 +116,17 @@ type ScreenOpts struct { // dimensions of the DevInfo.AltDimensions() values. AltScreen DevInfo + // Codepage is the Codepage implementation to use when sending text to the + // client and translating incoming field text from the client. Typically + // you should pass in the return value from DevInfo.Codepage() each time + // to get the correct codepage that was detected when the client + // connected. If nil, the global default code page (default 1047, but + // changed with the SetCodepage() function) will be used. NOTE: providing + // a DevInfo to ScreenOpts.AltScreen does _not_ automatically set this + // value, you must set it explicitly on every call that accepts + // ScreenOpts. + Codepage Codepage + // NoResponse will draw the screen and immediately return, without // waiting for any input data from the remote client. NoResponse bool @@ -188,7 +199,7 @@ func ShowScreenOpts(screen Screen, var resp Response fm, err := showScreenInternal(screen, values, opts.CursorRow, - opts.CursorCol, conn, !opts.NoClear, opts.AltScreen) + opts.CursorCol, conn, !opts.NoClear, opts.AltScreen, opts.Codepage) if err != nil { return resp, err } @@ -201,7 +212,7 @@ func ShowScreenOpts(screen Screen, } if !opts.NoResponse { - resp, err = readResponse(conn, fm, opts.AltScreen) + resp, err = readResponse(conn, fm, opts.AltScreen, opts.Codepage) if err != nil { return resp, err } @@ -222,6 +233,9 @@ func ShowScreenOpts(screen Screen, } // Deprecated: use ShowScreenOpts with default/empty ScreenOpts. +// +// NOTE: this deprecated function is NOT codepage-aware. The global code +// page set by SetCodepage will always be used. func ShowScreen(screen Screen, values map[string]string, crow, ccol int, conn net.Conn) (Response, error) { @@ -230,6 +244,9 @@ func ShowScreen(screen Screen, values map[string]string, crow, ccol int, } // Deprecated: use ShowScreenOpts with ScreenOpts.NoResponse = true. +// +// NOTE: this deprecated function is NOT codepage-aware. The global code +// page set by SetCodepage will always be used. func ShowScreenNoResponse(screen Screen, values map[string]string, crow, ccol int, conn net.Conn) error { @@ -239,7 +256,13 @@ func ShowScreenNoResponse(screen Screen, values map[string]string, } func showScreenInternal(screen Screen, values map[string]string, - crow, ccol int, conn net.Conn, clear bool, dev DevInfo) (fieldmap, error) { + crow, ccol int, conn net.Conn, clear bool, dev DevInfo, + cp Codepage) (fieldmap, error) { + + // Provide default Codepage implementation + if cp == nil { + cp = defaultCodepage + } rows, cols := 24, 80 if dev != nil { @@ -286,7 +309,7 @@ func showScreenInternal(screen Screen, values map[string]string, } } if content != "" { - b.Write(currentCodepage.Encode(content)) + b.Write(cp.Encode(content)) } // If a writable field, add it to the field map. We add 1 to bufaddr diff --git a/telnet.go b/telnet.go index 1f8ffa0..faf8d76 100644 --- a/telnet.go +++ b/telnet.go @@ -33,6 +33,14 @@ type DevInfo interface { // from AltDimensions(). TerminalType() string + // Codepage is the Codepage interface that implements the EBCDIC + // translation for the detected code page for the terminal, if supported. + // This may be nil if the client code page is unknown. Whenever calling + // the screen functions, always pass the value returned by this Codepage() + // function in the ScreenOpts (nil is allowed to default to the global + // default codepage). + Codepage() Codepage + // Private version of AltDimensions() so callers can't fake us out; only // real implementations returned by NegotiateTelnet() will work. altDimensions() (rows, cols int) @@ -284,32 +292,42 @@ func getTerminalType(conn net.Conn) (string, error) { var modelRegex = regexp.MustCompile(`^IBM-\d{4}-([2-5])`) func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) { + var rows, cols, cpid int + var codepage Codepage + var isx3270 bool + // tn3270e restricts to a small list of valid models, but since we're // not doing tn3270e, we are seeing a variety of model numbers. We'll // generically handle anything claiming to be a -2, -3, -4, or -5 type. + // + // We'll default to known terminal sizes in case we don't get the + // structured field query response later. modelresult := modelRegex.FindStringSubmatch(termtype) if len(modelresult) == 2 { switch modelresult[1] { case "2": - return &deviceInfo{24, 80, termtype}, nil + rows = 24 + cols = 80 case "3": - return &deviceInfo{32, 80, termtype}, nil + rows = 32 + cols = 80 case "4": - return &deviceInfo{43, 80, termtype}, nil + rows = 43 + cols = 80 case "5": - return &deviceInfo{27, 132, termtype}, nil + rows = 27 + cols = 132 } + } else if termtype != "IBM-DYNAMIC" { + // If it's not a fixed-size type, it should be IBM-DYNAMIC. If it + // isn't, we don't know how to deal with it. We'll just fall back on a + // simple 24x80 assumption. + rows = 24 + cols = 80 + termtype = "unknown (" + termtype + ")" } - // If it's not a fixed-size type, it should be IBM-DYNAMIC. If it isn't, - // we don't know how to deal with it. We'll just fall back on a simple - // 24x80 assumption. - if termtype != "IBM-DYNAMIC" { - return &deviceInfo{24, 80, "unknown (" + termtype + ")"}, nil - } - - // For IBM-DYNAMIC, we need to discover the alternate screen size with - // a structured field query. + // Now we'll discover the terminal size and character set. // First, we perform an ERASE / WRITE ALTERNATE to clear the screen // and put it in alternate screen mode. (EWA, reset WCC, telnet EOR) @@ -320,7 +338,8 @@ func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) { // Now we need to send the Write Structured Field command (0xf3) with the // "Read Partition - Query" structured field. Note that we're // telnet-escaping the 0xff in the data, but the subfield length is the - // *unescaped* length (5). + // *unescaped* length, including the 2 length bytes but excluding the + // telnet EOR (5). if _, err := conn.Write([]byte{0xf3, 0, 5, 0x01, 0xff, 0xff, 0x02, 0xff, 0xef}); err != nil { return nil, err @@ -334,9 +353,9 @@ func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) { conn.SetReadDeadline(time.Time{}) if err != nil && errors.Is(err, os.ErrDeadlineExceeded) { // Timeout. In this case, we'll assume it's because the client didn't - // reply to our query command. In that case, we'll fall back to - // treating the terminal as a 24x80. - return &deviceInfo{24, 80, termtype}, nil + // reply to our query command. In that case, we'll return whatever + // we're already assuming. + return &deviceInfo{24, 80, termtype, nil}, nil } else if err != nil { return nil, err } @@ -344,11 +363,10 @@ func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) { return nil, ErrTelnetError } - var rows, cols int - // There are an arbitrary number of query reply structured fields. We - // are only interested in the "Usable Area" SFID=0x81 QCODE=0x81 field, - // so we'll just consume any others. Consume all data until the EOR is - // received. + // There are an arbitrary number of query reply structured fields. We are + // only interested in the "Usable Area" SFID=0x81 QCODE=0x81 field and + // "Character Sets" QCODE=0x85 field so we'll just consume any others. + // Consume all data until the EOR is received. for { // Two bytes are big-endian length. buf, err := telnetReadN(conn, 2) @@ -371,42 +389,125 @@ func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) { return nil, ErrTelnetError } - // Note that because length isn't at the beginning, offsets in buf - // are 2 less than in the 3270 datastream documentation. - - if !(buf[0] == 0x81 && buf[1] == 0x81) { - // Not 'Usable Area' query reply + // Note that because length isn't at the beginning, offsets in buf are + // 2 less than in the 3270 data stream documentation. + if buf[0] == 0x81 && buf[1] == 0x81 { + // Usable Area + rows, cols, err = getUsableArea(buf) + if err != nil { + return nil, err + } + } else if buf[0] == 0x81 && buf[1] == 0x85 { + // Character Sets + cpid = getCodepageID(buf) + } else if buf[0] == 0x81 && buf[1] == 0xA1 { + // RPQ Names. We use this to determine if the client is x3270 + // family. + isx3270 = getRPQNames(buf) + } else { + // Not a field we're interested in continue } - - // A valid Usable Area reply will always include at least 18 (20 with - // length) bytes. - if l < 18 { - return nil, ErrTelnetError - } - - // big-endian two byte values - cols = int(buf[4])<<8 + int(buf[5]) - rows = int(buf[6])<<8 + int(buf[7]) } + switch cpid { + case 37: + // If x3270 family, assume that this is really the default "bracket" + // codepage, which reports as 37, not true CP37. + if isx3270 { + codepage = CodepageBracket() + } else { + codepage = Codepage037() + } + case 924: + codepage = Codepage924() + case 1047: + codepage = Codepage1047() + case 1140: + codepage = Codepage1140() + default: + // nil codepage will be accepted in ScreenOpts to default to the + // global default codepage. + codepage = nil + } + + return &deviceInfo{rows, cols, termtype, codepage}, nil +} + +// getUsableArea processes the "Query Reply (Usable Area)" response to return +// the rows and columns count of the terminal. The byte slice passed in to buf +// must begin with {0x81, 0x81}. +func getUsableArea(buf []byte) (rows, cols int, err error) { + // A valid Usable Area reply will always include at least 18 (20 with + // length) bytes. + if len(buf) < 18 || buf[0] != 0x81 || buf[1] != 0x81 { + return 0, 0, ErrTelnetError + } + + // big-endian two byte values + cols = int(buf[4])<<8 + int(buf[5]) + rows = int(buf[6])<<8 + int(buf[7]) + if rows == 0 || cols == 0 { - // We got an IBM-DYNAMIC device type, but it didn't include a - // Usable Area query response. - return nil, ErrUnknownTerminal + // Got a Usable Area response but the values are 0? + return 0, 0, ErrUnknownTerminal } - // We support 12- and 14-bit addressing. Using 16-bit addressing would - // require a mode change and the current API design doesn't support - // tracking the state necessary for that. + // We support 12- and 14-bit addressing. Using 16-bit addressing + // would require a mode change and the current API design doesn't + // support tracking the state necessary for that. // - // We'll limit the reported screen size to what fits in 14-bit addressing - // by removing rows if necessary. + // We'll limit the reported screen size to what fits in 14-bit + // addressing by removing rows if necessary. for rows*cols >= 1<<14 { rows-- } - return &deviceInfo{rows, cols, termtype}, nil + return rows, cols, nil +} + +// getCodepageID processes the "Query Reply (Character Sets)" response to +// return the integer code page number if present. If unable, returns 0. The +// byte slice passed in to buf must begin with {0x81, 0x85}. +func getCodepageID(buf []byte) int { + // Initial validity check. + if len(buf) < 11 || buf[0] != 0x81 || buf[1] != 0x85 { + return 0 + } + + // If the GF bit is not set, no point in continuing. + if buf[2]&(1<<1) != 1<<1 { + return 0 + } + + // Descriptor length -- and do we have at least one descriptor? + dl := int(buf[10]) + if len(buf) < 11+dl { + return 0 + } + + // No matter how long the descriptor is, the code page will 2-byte big + // endian integer in the last two bytes. + cpid := int(buf[11+dl-2])<<8 + int(buf[11+dl-1]) + + return cpid +} + +// getRPGNames checks the "Query Reply (RPQ NAMES)" response to see if the +// client is in the x3270 family. The byte slice passed in to buf must begin +// with {0x81, 0xA1}. +func getRPQNames(buf []byte) bool { + if len(buf) < 16 { + return false + } + + // "x3270" in EBCDIC + if buf[11] == 0xa7 && buf[12] == 0xf3 && buf[13] == 0xf2 && + buf[14] == 0xf7 && buf[15] == 0xf0 { + return true + } + + return false } // UnNegotiateTelnet will naively (e.g. not checking client responses) attempt @@ -543,6 +644,7 @@ func telnetReadN(conn net.Conn, n int) ([]byte, error) { type deviceInfo struct { rows, cols int termtype string + codepage Codepage } func (d *deviceInfo) AltDimensions() (rows, cols int) { @@ -556,3 +658,7 @@ func (d *deviceInfo) TerminalType() string { func (d *deviceInfo) altDimensions() (rows, cols int) { return d.rows, d.cols } + +func (d *deviceInfo) Codepage() Codepage { + return d.codepage +}