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 } }