package config import ( "fmt" "path/filepath" "regexp" "strings" "time" ) var windowsAbsPathRE = regexp.MustCompile(`^[A-Za-z]:[\\/].*`) var envVarNameRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) func validateEnvVarNameField(fieldName, value string) error { trimmed := strings.TrimSpace(value) if trimmed == "" { return fmt.Errorf("%s must be non-empty", fieldName) } if !envVarNameRE.MatchString(trimmed) { return fmt.Errorf("%s must be a valid environment variable name", fieldName) } return nil } func validateRelativeSafePath(fieldName, value string) error { trimmed := strings.TrimSpace(value) if trimmed == "" { return fmt.Errorf("%s must be non-empty", fieldName) } if filepath.IsAbs(trimmed) || strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\") || windowsAbsPathRE.MatchString(trimmed) { return fmt.Errorf("%s must be a relative path", fieldName) } normalized := strings.ReplaceAll(trimmed, "\\", "/") for _, segment := range strings.Split(normalized, "/") { if segment == ".." { return fmt.Errorf("%s must not contain path traversal", fieldName) } } return nil } func validatePathWithinRoot(fieldName, value, root string) error { normalizedValue := filepath.ToSlash(filepath.Clean(strings.TrimSpace(value))) normalizedRoot := filepath.ToSlash(filepath.Clean(strings.TrimSpace(root))) if normalizedValue == normalizedRoot { return nil } if strings.HasPrefix(normalizedValue, normalizedRoot+"/") { return nil } return fmt.Errorf("%s must be under %s/", fieldName, normalizedRoot) } func validateDuration(fieldName, value string) error { trimmed := strings.TrimSpace(value) if trimmed == "" { return nil } duration, err := time.ParseDuration(trimmed) if err != nil { return fmt.Errorf("%s must be a valid duration: %w", fieldName, err) } if duration <= 0 { return fmt.Errorf("%s must be positive", fieldName) } return nil }