package auth import ( "fmt" "strings" ) const MaxResolveCommandNames = 32 // ValidateCommandNames checks relative command basenames for resolve-commands. func ValidateCommandNames(names []string) ([]string, error) { if len(names) == 0 { return nil, fmt.Errorf("names is required") } if len(names) > MaxResolveCommandNames { return nil, fmt.Errorf("at most %d names allowed", MaxResolveCommandNames) } cleaned := make([]string, 0, len(names)) seen := make(map[string]struct{}, len(names)) for _, raw := range names { name := strings.TrimSpace(raw) if name == "" { return nil, fmt.Errorf("command name must not be empty") } if strings.Contains(name, "/") { return nil, fmt.Errorf("command name must be a basename without /") } if len(name) > 64 { return nil, fmt.Errorf("command name too long") } for _, runeValue := range name { if (runeValue >= 'a' && runeValue <= 'z') || (runeValue >= 'A' && runeValue <= 'Z') || (runeValue >= '0' && runeValue <= '9') || runeValue == '.' || runeValue == '_' || runeValue == '-' || runeValue == '+' { continue } return nil, fmt.Errorf("command name contains invalid characters") } if _, exists := seen[name]; exists { continue } seen[name] = struct{}{} cleaned = append(cleaned, name) } if len(cleaned) == 0 { return nil, fmt.Errorf("names is required") } return cleaned, nil } // BuildResolveCommandsRemote builds a shell snippet that prints name=path lines. func BuildResolveCommandsRemote(names []string) string { var builder strings.Builder for _, name := range names { quoted := shellQuote(name) builder.WriteString("path=$(command -v -- ") builder.WriteString(quoted) builder.WriteString(" 2>/dev/null || true); printf '%s=%s\\n' ") builder.WriteString(quoted) builder.WriteString(" \"$path\"; ") } return builder.String() } // ParseResolveCommandsOutput parses name=path lines into found paths and missing names. func ParseResolveCommandsOutput(names []string, stdout string) (paths map[string]string, missing []string) { paths = make(map[string]string, len(names)) wanted := make(map[string]struct{}, len(names)) for _, name := range names { wanted[name] = struct{}{} } for _, line := range strings.Split(stdout, "\n") { line = strings.TrimSpace(line) if line == "" { continue } name, path, ok := strings.Cut(line, "=") if !ok { continue } name = strings.TrimSpace(name) path = strings.TrimSpace(path) if _, ok := wanted[name]; !ok { continue } if path == "" || !strings.HasPrefix(path, "/") { continue } paths[name] = path } for _, name := range names { if _, ok := paths[name]; !ok { missing = append(missing, name) } } return paths, missing }