package service import ( "fmt" "math/rand" "regexp" "sort" "strconv" "strings" "gcy_hpc_server/internal/model" ) var paramNameRegex = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) // ValidateParams checks that all required parameters are present and values match their types. // Parameters not in the schema are silently ignored. func ValidateParams(params []model.ParameterSchema, values map[string]string) error { var errs []string for _, p := range params { if !paramNameRegex.MatchString(p.Name) { errs = append(errs, fmt.Sprintf("invalid parameter name %q: must match ^[A-Za-z_][A-Za-z0-9_]*$", p.Name)) continue } val, ok := values[p.Name] if p.Required && !ok { errs = append(errs, fmt.Sprintf("required parameter %q is missing", p.Name)) continue } if !ok { continue } switch p.Type { case model.ParamTypeInteger: if _, err := strconv.Atoi(val); err != nil { errs = append(errs, fmt.Sprintf("parameter %q must be an integer, got %q", p.Name, val)) } case model.ParamTypeBoolean: if val != "true" && val != "false" && val != "1" && val != "0" { errs = append(errs, fmt.Sprintf("parameter %q must be a boolean (true/false/1/0), got %q", p.Name, val)) } case model.ParamTypeEnum: if len(p.Options) > 0 { found := false for _, opt := range p.Options { if val == opt { found = true break } } if !found { errs = append(errs, fmt.Sprintf("parameter %q must be one of %v, got %q", p.Name, p.Options, val)) } } case model.ParamTypeFile, model.ParamTypeDirectory: case model.ParamTypeString: } } if len(errs) > 0 { return fmt.Errorf("parameter validation failed: %s", strings.Join(errs, "; ")) } return nil } // RenderScript replaces $PARAM tokens in the template with user-provided values. // Only tokens defined in the schema are replaced. Replacement is done longest-name-first // to avoid partial matches (e.g., $JOB_NAME before $JOB). // All values are shell-escaped using single-quote wrapping. func RenderScript(template string, params []model.ParameterSchema, values map[string]string) string { sorted := make([]model.ParameterSchema, len(params)) copy(sorted, params) sort.Slice(sorted, func(i, j int) bool { return len(sorted[i].Name) > len(sorted[j].Name) }) result := template for _, p := range sorted { val, ok := values[p.Name] if !ok { if p.Default != "" { val = p.Default } else { continue } } escaped := "'" + strings.ReplaceAll(val, "'", "'\\''") + "'" result = strings.ReplaceAll(result, "$"+p.Name, escaped) } return result } // SanitizeDirName sanitizes a directory name. func SanitizeDirName(name string) string { replacer := strings.NewReplacer(" ", "_", "/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_") return replacer.Replace(name) } // RandomSuffix generates a random suffix of length n. func RandomSuffix(n int) string { const charset = "abcdefghijklmnopqrstuvwxyz0123456789" b := make([]byte, n) for i := range b { b[i] = charset[rand.Intn(len(charset))] } return string(b) }