Files
Erupe/common/stringsupport/string_convert.go
Houmgaor f640cfee27 fix: log SJIS decoding errors instead of silently discarding them
Add SJISToUTF8Lossy() that wraps SJISToUTF8() and logs decode errors at
slog.Debug level. Replace all 31 call sites across 17 files that previously
discarded the error with `_, _ =`. This makes garbled text from malformed
SJIS client data debuggable without adding noise at default log levels.
2026-02-22 17:01:22 +01:00

173 lines
4.1 KiB
Go

package stringsupport
import (
"bytes"
"fmt"
"io"
"log/slog"
"strconv"
"strings"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/transform"
)
// UTF8ToSJIS encodes a UTF-8 string to Shift-JIS bytes, silently dropping any
// runes that cannot be represented in Shift-JIS.
func UTF8ToSJIS(x string) []byte {
e := japanese.ShiftJIS.NewEncoder()
xt, _, err := transform.String(e, x)
if err != nil {
// Filter out runes that can't be encoded to Shift-JIS instead of
// crashing the server (see PR #116).
var filtered []rune
for _, r := range x {
if _, _, err := transform.String(japanese.ShiftJIS.NewEncoder(), string(r)); err == nil {
filtered = append(filtered, r)
}
}
xt, _, _ = transform.String(japanese.ShiftJIS.NewEncoder(), string(filtered))
}
return []byte(xt)
}
// SJISToUTF8 decodes Shift-JIS bytes to a UTF-8 string.
func SJISToUTF8(b []byte) (string, error) {
d := japanese.ShiftJIS.NewDecoder()
result, err := io.ReadAll(transform.NewReader(bytes.NewReader(b), d))
if err != nil {
return "", fmt.Errorf("ShiftJIS decode: %w", err)
}
return string(result), nil
}
// SJISToUTF8Lossy decodes Shift-JIS bytes to a UTF-8 string, logging
// any decoding error at debug level instead of returning it.
func SJISToUTF8Lossy(b []byte) string {
s, err := SJISToUTF8(b)
if err != nil {
slog.Debug("SJIS decode failed", "error", err, "raw_len", len(b))
}
return s
}
// ToNGWord converts a UTF-8 string into a slice of uint16 values in the
// Shift-JIS byte-swapped format used by the MHF NG-word (chat filter) system.
func ToNGWord(x string) []uint16 {
var w []uint16
for _, r := range x {
if r > 0xFF {
t := UTF8ToSJIS(string(r))
if len(t) > 1 {
w = append(w, uint16(t[1])<<8|uint16(t[0]))
} else if len(t) == 1 {
w = append(w, uint16(t[0]))
}
// Skip runes that produced no SJIS output (unsupported characters)
} else {
w = append(w, uint16(r))
}
}
return w
}
// PaddedString returns a fixed-width null-terminated byte slice of the given
// size. If t is true the string is first encoded to Shift-JIS.
func PaddedString(x string, size uint, t bool) []byte {
if t {
e := japanese.ShiftJIS.NewEncoder()
xt, _, err := transform.String(e, x)
if err != nil {
return make([]byte, size)
}
x = xt
}
out := make([]byte, size)
copy(out, x)
out[len(out)-1] = 0
return out
}
// CSVAdd appends v to the comma-separated integer list if not already present.
func CSVAdd(csv string, v int) string {
if len(csv) == 0 {
return strconv.Itoa(v)
}
if CSVContains(csv, v) {
return csv
} else {
return csv + "," + strconv.Itoa(v)
}
}
// CSVRemove removes v from the comma-separated integer list.
func CSVRemove(csv string, v int) string {
s := strings.Split(csv, ",")
for i, e := range s {
if e == strconv.Itoa(v) {
s[i] = s[len(s)-1]
s = s[:len(s)-1]
}
}
return strings.Join(s, ",")
}
// CSVContains reports whether v is present in the comma-separated integer list.
func CSVContains(csv string, v int) bool {
s := strings.Split(csv, ",")
for i := 0; i < len(s); i++ {
j, _ := strconv.ParseInt(s[i], 10, 32)
if int(j) == v {
return true
}
}
return false
}
// CSVLength returns the number of elements in the comma-separated list.
func CSVLength(csv string) int {
if csv == "" {
return 0
}
s := strings.Split(csv, ",")
return len(s)
}
// CSVElems parses the comma-separated integer list into an int slice.
func CSVElems(csv string) []int {
var r []int
if csv == "" {
return r
}
s := strings.Split(csv, ",")
for i := 0; i < len(s); i++ {
j, _ := strconv.ParseInt(s[i], 10, 32)
r = append(r, int(j))
}
return r
}
// CSVGetIndex returns the integer at position i in the comma-separated list,
// or 0 if i is out of range.
func CSVGetIndex(csv string, i int) int {
s := CSVElems(csv)
if i < len(s) {
return s[i]
}
return 0
}
// CSVSetIndex replaces the integer at position i in the comma-separated list
// with v. If i is out of range the list is returned unchanged.
func CSVSetIndex(csv string, i int, v int) string {
s := CSVElems(csv)
if i < len(s) {
s[i] = v
}
var r []string
for j := 0; j < len(s); j++ {
r = append(r, fmt.Sprintf(`%d`, s[j]))
}
return strings.Join(r, ",")
}