package characterdata import ( "errors" "fmt" "unicode" "unicode/utf8" "golang.org/x/text/encoding/simplifiedchinese" ) const ( CharacterNameMinBytes = 2 CharacterNameMaxBytes = 30 ) var ErrCharacterNameInvalid = errors.New("character name is invalid") // ValidateCharacterName owns the durable-name safety boundary shared by // create, duplicate-check, and rename. It keeps the current client's GB18030 // byte limits while rejecting invisible/control data and punctuation already // unsafe in the current NoPack UI/protocol paths. func ValidateCharacterName(name string) error { if name == "" || !utf8.ValidString(name) { return fmt.Errorf("%w: name must be non-empty valid UTF-8", ErrCharacterNameInvalid) } encoded, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(name)) if err != nil { return fmt.Errorf("%w: encode GB18030: %v", ErrCharacterNameInvalid, err) } if len(encoded) < CharacterNameMinBytes || len(encoded) > CharacterNameMaxBytes { return fmt.Errorf("%w: encoded length %d is outside %d..%d", ErrCharacterNameInvalid, len(encoded), CharacterNameMinBytes, CharacterNameMaxBytes) } for _, value := range name { if unicode.IsSpace(value) || unicode.IsControl(value) || unicode.Is(unicode.Cf, value) || !unicode.IsGraphic(value) { return fmt.Errorf("%w: name contains whitespace, control, or invisible data", ErrCharacterNameInvalid) } switch value { case '\'', '\\', '%', '<', '>', '"', '|': return fmt.Errorf("%w: name contains forbidden character %q", ErrCharacterNameInvalid, value) } } return nil }