package builder import ( "fmt" "os" "regexp" "strings" "gopkg.in/yaml.v3" ) // repoNameRegex validates repository names for safe use in NATS subjects. // Only allows alphanumeric, dashes, and underscores (no dots or wildcards). var repoNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) // validURLPrefixes are the allowed prefixes for repository URLs. var validURLPrefixes = []string{ "git+https://", "git+ssh://", "git+file://", } // RepoConfig holds configuration for a single repository. type RepoConfig struct { URL string `yaml:"url"` DefaultBranch string `yaml:"default_branch"` } // Config holds the builder configuration. type Config struct { Repos map[string]RepoConfig `yaml:"repos"` } // LoadConfig loads configuration from a YAML file. func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } if err := cfg.Validate(); err != nil { return nil, err } return &cfg, nil } // Validate checks that the configuration is valid. func (c *Config) Validate() error { if len(c.Repos) == 0 { return fmt.Errorf("no repos configured") } for name, repo := range c.Repos { // Validate repo name for safe use in NATS subjects if !repoNameRegex.MatchString(name) { return fmt.Errorf("repo name %q contains invalid characters (only alphanumeric, dash, underscore allowed)", name) } if repo.URL == "" { return fmt.Errorf("repo %q: url is required", name) } // Validate URL format validURL := false for _, prefix := range validURLPrefixes { if strings.HasPrefix(repo.URL, prefix) { validURL = true break } } if !validURL { return fmt.Errorf("repo %q: url must start with git+https://, git+ssh://, or git+file://", name) } if repo.DefaultBranch == "" { return fmt.Errorf("repo %q: default_branch is required", name) } } return nil } // GetRepo returns the configuration for a repository, or an error if not found. func (c *Config) GetRepo(name string) (*RepoConfig, error) { repo, ok := c.Repos[name] if !ok { return nil, fmt.Errorf("repo %q not found in configuration", name) } return &repo, nil }