Files
cdp_hub/internal/domain/instance_name.go
T

84 lines
2.4 KiB
Go

package domain
import (
"errors"
"fmt"
"hash/fnv"
"strings"
"unicode/utf8"
)
const maxInstanceNameRunes = 80
var errInvalidInstanceName = errors.New("invalid instance name")
// NormalizeInstanceName validates the user-visible instance name before it is
// stored or used to derive a Windows profile directory. The validation keeps
// the display name readable instead of silently rewriting user input.
func NormalizeInstanceName(value string) (string, error) {
name := strings.TrimSpace(value)
if name == "" {
return "", fmt.Errorf("%w: name is required", errInvalidInstanceName)
}
if utf8.RuneCountInString(name) > maxInstanceNameRunes {
return "", fmt.Errorf("%w: name is too long", errInvalidInstanceName)
}
if strings.HasSuffix(name, ".") {
return "", fmt.Errorf("%w: name cannot end with a period", errInvalidInstanceName)
}
for _, value := range name {
if value < 0x20 || strings.ContainsRune(`<>:"/\\|?*`, value) {
return "", fmt.Errorf("%w: name contains a Windows-reserved character", errInvalidInstanceName)
}
}
base := strings.ToUpper(strings.Split(name, ".")[0])
if isReservedWindowsDeviceName(base) {
return "", fmt.Errorf("%w: name is a reserved Windows device name", errInvalidInstanceName)
}
return name, nil
}
// InstanceProfileDirectoryName derives a compact, readable, and unique
// profile directory component. The stable hash keeps paths distinct even
// when display names share a truncated prefix.
func InstanceProfileDirectoryName(name, instanceID string) (string, error) {
name, err := NormalizeInstanceName(name)
if err != nil {
return "", err
}
instanceID = strings.TrimSpace(instanceID)
if instanceID == "" {
return "", fmt.Errorf("%w: instance ID is required", errInvalidInstanceName)
}
segment := truncateUTF8(name, 64)
hash := fnv.New32a()
_, _ = hash.Write([]byte(instanceID))
return fmt.Sprintf("instance-%s-%08x", segment, hash.Sum32()), nil
}
func truncateUTF8(value string, limit int) string {
if len(value) <= limit {
return value
}
count := 0
for index := range value {
if index > limit {
break
}
count = index
}
if count == 0 {
return value
}
return value[:count]
}
func isReservedWindowsDeviceName(value string) bool {
switch value {
case "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9":
return true
default:
return false
}
}