feat: update go to 1.25, add permissions and envs (#49)

* chore(ci): update go to 1.25, add permissions and envs
* fix(ci): update pr-lint.yml
* chore: update go, fix linting
* fix: tests and linting
* fix(lint): lint fixes, renovate should now pass
* fix: updates, security upgrades
* chore: workflow updates, lint
* fix: more lint, checkmake, and other fixes
* fix: more lint, convert scripts to POSIX compliant
* fix: simplify codeql workflow
* tests: increase test coverage, fix found issues
* fix(lint): editorconfig checking, add to linters
* fix(lint): shellcheck, add to linters
* fix(lint): apply cr comment suggestions
* fix(ci): remove step-security/harden-runner
* fix(lint): remove duplication, apply cr fixes
* fix(ci): tests in CI/CD pipeline
* chore(lint): deduplication of strings
* fix(lint): apply cr comment suggestions
* fix(ci): actionlint
* fix(lint): apply cr comment suggestions
* chore: lint, add deps management
This commit is contained in:
2025-10-10 12:14:42 +03:00
committed by GitHub
parent 958f5952a0
commit 3f65b813bd
100 changed files with 6997 additions and 1225 deletions

View File

@@ -58,4 +58,4 @@ const (
MinHardMemoryLimitMB = 64
// MaxHardMemoryLimitMB is the maximum hard memory limit (8192MB = 8GB).
MaxHardMemoryLimitMB = 8192
)
)

View File

@@ -154,4 +154,4 @@ func GetEnableGracefulDegradation() bool {
// GetEnableResourceMonitoring returns whether resource monitoring is enabled.
func GetEnableResourceMonitoring() bool {
return viper.GetBool("resourceLimits.enableResourceMonitoring")
}
}

View File

@@ -1,13 +1,15 @@
package config
import (
"flag"
"os"
"path/filepath"
"sync/atomic"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/ivuorinen/gibidify/utils"
"github.com/ivuorinen/gibidify/gibidiutils"
)
// LoadConfig reads configuration from a YAML file.
@@ -15,13 +17,18 @@ import (
// 1. $XDG_CONFIG_HOME/gibidify/config.yaml
// 2. $HOME/.config/gibidify/config.yaml
// 3. The current directory as fallback.
//
// Note: LoadConfig relies on isRunningTest() which requires the testing package
// to have registered its flags (e.g., via flag.Parse() or during test initialization).
// If called too early (e.g., from init() or before TestMain), test detection may not work reliably.
// For explicit control, use SetRunningInTest() before calling LoadConfig.
func LoadConfig() {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
// Validate XDG_CONFIG_HOME for path traversal attempts
if err := utils.ValidateConfigPath(xdgConfig); err != nil {
if err := gibidiutils.ValidateConfigPath(xdgConfig); err != nil {
logrus.Warnf("Invalid XDG_CONFIG_HOME path, using default config: %v", err)
} else {
configPath := filepath.Join(xdgConfig, "gibidify")
@@ -37,7 +44,14 @@ func LoadConfig() {
}
if err := viper.ReadInConfig(); err != nil {
logrus.Infof("Config file not found, using default values: %v", err)
// Suppress this info-level log when running tests.
// Prefer an explicit test flag (SetRunningInTest) but fall back to runtime detection.
if runningInTest.Load() || isRunningTest() {
// Keep a debug-level record so tests that enable debug can still see it.
logrus.Debugf("Config file not found (tests): %v", err)
} else {
logrus.Infof("Config file not found, using default values: %v", err)
}
setDefaultConfig()
} else {
logrus.Infof("Using config file: %s", viper.ConfigFileUsed())
@@ -87,4 +101,31 @@ func setDefaultConfig() {
viper.SetDefault("resourceLimits.hardMemoryLimitMB", DefaultHardMemoryLimitMB)
viper.SetDefault("resourceLimits.enableGracefulDegradation", true)
viper.SetDefault("resourceLimits.enableResourceMonitoring", true)
}
}
var runningInTest atomic.Bool
// SetRunningInTest allows tests to explicitly indicate they are running under `go test`.
// Call this from TestMain in tests to suppress noisy info logs while still allowing
// debug-level output for tests that enable it.
func SetRunningInTest(b bool) {
runningInTest.Store(b)
}
// isRunningTest attempts to detect if the binary is running under `go test`.
// Prefer checking for standard test flags registered by the testing package.
// This is reliable when `go test` initializes the flag set.
//
// IMPORTANT: This function relies on flag.Lookup which returns nil if the testing
// package hasn't registered test flags yet. Callers must invoke this after flag
// parsing (or test flag registration) has occurred. If invoked too early (e.g.,
// from init() or early in TestMain before flags are parsed), detection will fail.
// For explicit control, use SetRunningInTest() instead.
func isRunningTest() bool {
// Look for the well-known test flags created by the testing package.
// If any are present in the flag registry, we're running under `go test`.
if flag.Lookup("test.v") != nil || flag.Lookup("test.run") != nil || flag.Lookup("test.bench") != nil {
return true
}
return false
}

View File

@@ -79,15 +79,15 @@ func TestLoadConfigWithValidation(t *testing.T) {
configContent := `
fileSizeLimit: 100
ignoreDirectories:
- node_modules
- ""
- .git
- node_modules
- ""
- .git
`
tempDir := t.TempDir()
configFile := tempDir + "/config.yaml"
err := os.WriteFile(configFile, []byte(configContent), 0o644)
err := os.WriteFile(configFile, []byte(configContent), 0o600)
if err != nil {
t.Fatalf("Failed to write config file: %v", err)
}
@@ -104,7 +104,10 @@ ignoreDirectories:
t.Errorf("Expected default file size limit after validation failure, got %d", config.GetFileSizeLimit())
}
if containsString(config.GetIgnoredDirectories(), "") {
t.Errorf("Expected ignored directories not to contain empty string after validation failure, got %v", config.GetIgnoredDirectories())
t.Errorf(
"Expected ignored directories not to contain empty string after validation failure, got %v",
config.GetIgnoredDirectories(),
)
}
}
@@ -117,4 +120,4 @@ func containsString(slice []string, item string) bool {
}
}
return false
}
}

View File

@@ -6,240 +6,532 @@ import (
"github.com/spf13/viper"
"github.com/ivuorinen/gibidify/utils"
"github.com/ivuorinen/gibidify/gibidiutils"
)
// ValidateConfig validates the loaded configuration.
func ValidateConfig() error {
var validationErrors []string
// Validate file size limit
// validateFileSizeLimit validates the file size limit configuration.
func validateFileSizeLimit() []string {
var errors []string
fileSizeLimit := viper.GetInt64("fileSizeLimit")
if fileSizeLimit < MinFileSizeLimit {
validationErrors = append(validationErrors, fmt.Sprintf("fileSizeLimit (%d) is below minimum (%d)", fileSizeLimit, MinFileSizeLimit))
errors = append(
errors,
fmt.Sprintf("fileSizeLimit (%d) is below minimum (%d)", fileSizeLimit, MinFileSizeLimit),
)
}
if fileSizeLimit > MaxFileSizeLimit {
validationErrors = append(validationErrors, fmt.Sprintf("fileSizeLimit (%d) exceeds maximum (%d)", fileSizeLimit, MaxFileSizeLimit))
errors = append(
errors,
fmt.Sprintf("fileSizeLimit (%d) exceeds maximum (%d)", fileSizeLimit, MaxFileSizeLimit),
)
}
return errors
}
// Validate ignore directories
// validateIgnoreDirectories validates the ignore directories configuration.
func validateIgnoreDirectories() []string {
var errors []string
ignoreDirectories := viper.GetStringSlice("ignoreDirectories")
for i, dir := range ignoreDirectories {
dir = strings.TrimSpace(dir)
if dir == "" {
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] is empty", i))
errors = append(errors, fmt.Sprintf("ignoreDirectories[%d] is empty", i))
continue
}
if strings.Contains(dir, "/") {
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] (%s) contains path separator - only directory names are allowed", i, dir))
errors = append(
errors,
fmt.Sprintf(
"ignoreDirectories[%d] (%s) contains path separator - only directory names are allowed",
i,
dir,
),
)
}
if strings.HasPrefix(dir, ".") && dir != ".git" && dir != ".vscode" && dir != ".idea" {
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] (%s) starts with dot - this may cause unexpected behavior", i, dir))
errors = append(
errors,
fmt.Sprintf("ignoreDirectories[%d] (%s) starts with dot - this may cause unexpected behavior", i, dir),
)
}
}
return errors
}
// Validate supported output formats if configured
// validateSupportedFormats validates the supported output formats configuration.
func validateSupportedFormats() []string {
var errors []string
if viper.IsSet("supportedFormats") {
supportedFormats := viper.GetStringSlice("supportedFormats")
validFormats := map[string]bool{"json": true, "yaml": true, "markdown": true}
for i, format := range supportedFormats {
format = strings.ToLower(strings.TrimSpace(format))
if !validFormats[format] {
validationErrors = append(validationErrors, fmt.Sprintf("supportedFormats[%d] (%s) is not a valid format (json, yaml, markdown)", i, format))
errors = append(
errors,
fmt.Sprintf("supportedFormats[%d] (%s) is not a valid format (json, yaml, markdown)", i, format),
)
}
}
}
return errors
}
// Validate concurrency settings if configured
// validateConcurrencySettings validates the concurrency settings configuration.
func validateConcurrencySettings() []string {
var errors []string
if viper.IsSet("maxConcurrency") {
maxConcurrency := viper.GetInt("maxConcurrency")
if maxConcurrency < 1 {
validationErrors = append(validationErrors, fmt.Sprintf("maxConcurrency (%d) must be at least 1", maxConcurrency))
errors = append(
errors,
fmt.Sprintf("maxConcurrency (%d) must be at least 1", maxConcurrency),
)
}
if maxConcurrency > 100 {
validationErrors = append(validationErrors, fmt.Sprintf("maxConcurrency (%d) is unreasonably high (max 100)", maxConcurrency))
errors = append(
errors,
fmt.Sprintf("maxConcurrency (%d) is unreasonably high (max 100)", maxConcurrency),
)
}
}
return errors
}
// Validate file patterns if configured
// validateFilePatterns validates the file patterns configuration.
func validateFilePatterns() []string {
var errors []string
if viper.IsSet("filePatterns") {
filePatterns := viper.GetStringSlice("filePatterns")
for i, pattern := range filePatterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
validationErrors = append(validationErrors, fmt.Sprintf("filePatterns[%d] is empty", i))
errors = append(errors, fmt.Sprintf("filePatterns[%d] is empty", i))
continue
}
// Basic validation - patterns should contain at least one alphanumeric character
if !strings.ContainsAny(pattern, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") {
validationErrors = append(validationErrors, fmt.Sprintf("filePatterns[%d] (%s) appears to be invalid", i, pattern))
errors = append(
errors,
fmt.Sprintf("filePatterns[%d] (%s) appears to be invalid", i, pattern),
)
}
}
}
return errors
}
// Validate FileTypeRegistry configuration
if viper.IsSet("fileTypes.customImageExtensions") {
customImages := viper.GetStringSlice("fileTypes.customImageExtensions")
for i, ext := range customImages {
ext = strings.TrimSpace(ext)
if ext == "" {
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customImageExtensions[%d] is empty", i))
continue
}
if !strings.HasPrefix(ext, ".") {
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customImageExtensions[%d] (%s) must start with a dot", i, ext))
}
}
// validateFileTypes validates the FileTypeRegistry configuration.
// validateCustomImageExtensions validates custom image extensions configuration.
func validateCustomImageExtensions() []string {
var errors []string
if !viper.IsSet("fileTypes.customImageExtensions") {
return errors
}
if viper.IsSet("fileTypes.customBinaryExtensions") {
customBinary := viper.GetStringSlice("fileTypes.customBinaryExtensions")
for i, ext := range customBinary {
ext = strings.TrimSpace(ext)
if ext == "" {
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customBinaryExtensions[%d] is empty", i))
continue
}
if !strings.HasPrefix(ext, ".") {
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customBinaryExtensions[%d] (%s) must start with a dot", i, ext))
}
customImages := viper.GetStringSlice("fileTypes.customImageExtensions")
for i, ext := range customImages {
ext = strings.TrimSpace(ext)
if ext == "" {
errors = append(
errors,
fmt.Sprintf("fileTypes.customImageExtensions[%d] is empty", i),
)
continue
}
if !strings.HasPrefix(ext, ".") {
errors = append(
errors,
fmt.Sprintf("fileTypes.customImageExtensions[%d] (%s) must start with a dot", i, ext),
)
}
}
return errors
}
// validateCustomBinaryExtensions validates custom binary extensions configuration.
func validateCustomBinaryExtensions() []string {
var errors []string
if !viper.IsSet("fileTypes.customBinaryExtensions") {
return errors
}
if viper.IsSet("fileTypes.customLanguages") {
customLangs := viper.GetStringMapString("fileTypes.customLanguages")
for ext, lang := range customLangs {
ext = strings.TrimSpace(ext)
lang = strings.TrimSpace(lang)
if ext == "" {
validationErrors = append(validationErrors, "fileTypes.customLanguages contains empty extension key")
continue
}
if !strings.HasPrefix(ext, ".") {
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customLanguages extension (%s) must start with a dot", ext))
}
if lang == "" {
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customLanguages[%s] has empty language value", ext))
}
customBinary := viper.GetStringSlice("fileTypes.customBinaryExtensions")
for i, ext := range customBinary {
ext = strings.TrimSpace(ext)
if ext == "" {
errors = append(
errors,
fmt.Sprintf("fileTypes.customBinaryExtensions[%d] is empty", i),
)
continue
}
if !strings.HasPrefix(ext, ".") {
errors = append(
errors,
fmt.Sprintf("fileTypes.customBinaryExtensions[%d] (%s) must start with a dot", i, ext),
)
}
}
return errors
}
// validateCustomLanguages validates custom languages configuration.
func validateCustomLanguages() []string {
var errors []string
if !viper.IsSet("fileTypes.customLanguages") {
return errors
}
// Validate back-pressure configuration
if viper.IsSet("backpressure.maxPendingFiles") {
maxPendingFiles := viper.GetInt("backpressure.maxPendingFiles")
if maxPendingFiles < 1 {
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingFiles (%d) must be at least 1", maxPendingFiles))
customLangs := viper.GetStringMapString("fileTypes.customLanguages")
for ext, lang := range customLangs {
ext = strings.TrimSpace(ext)
lang = strings.TrimSpace(lang)
if ext == "" {
errors = append(errors, "fileTypes.customLanguages contains empty extension key")
continue
}
if maxPendingFiles > 100000 {
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingFiles (%d) is unreasonably high (max 100000)", maxPendingFiles))
if !strings.HasPrefix(ext, ".") {
errors = append(
errors,
fmt.Sprintf("fileTypes.customLanguages extension (%s) must start with a dot", ext),
)
}
if lang == "" {
errors = append(
errors,
fmt.Sprintf("fileTypes.customLanguages[%s] has empty language value", ext),
)
}
}
return errors
}
// validateFileTypes validates the FileTypeRegistry configuration.
func validateFileTypes() []string {
var errors []string
errors = append(errors, validateCustomImageExtensions()...)
errors = append(errors, validateCustomBinaryExtensions()...)
errors = append(errors, validateCustomLanguages()...)
return errors
}
// validateBackpressureConfig validates the back-pressure configuration.
// validateBackpressureMaxPendingFiles validates max pending files configuration.
func validateBackpressureMaxPendingFiles() []string {
var errors []string
if !viper.IsSet("backpressure.maxPendingFiles") {
return errors
}
if viper.IsSet("backpressure.maxPendingWrites") {
maxPendingWrites := viper.GetInt("backpressure.maxPendingWrites")
if maxPendingWrites < 1 {
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingWrites (%d) must be at least 1", maxPendingWrites))
}
if maxPendingWrites > 10000 {
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingWrites (%d) is unreasonably high (max 10000)", maxPendingWrites))
}
maxPendingFiles := viper.GetInt("backpressure.maxPendingFiles")
if maxPendingFiles < 1 {
errors = append(
errors,
fmt.Sprintf("backpressure.maxPendingFiles (%d) must be at least 1", maxPendingFiles),
)
}
if maxPendingFiles > 100000 {
errors = append(
errors,
fmt.Sprintf("backpressure.maxPendingFiles (%d) is unreasonably high (max 100000)", maxPendingFiles),
)
}
return errors
}
// validateBackpressureMaxPendingWrites validates max pending writes configuration.
func validateBackpressureMaxPendingWrites() []string {
var errors []string
if !viper.IsSet("backpressure.maxPendingWrites") {
return errors
}
if viper.IsSet("backpressure.maxMemoryUsage") {
maxMemoryUsage := viper.GetInt64("backpressure.maxMemoryUsage")
if maxMemoryUsage < 1048576 { // 1MB minimum
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxMemoryUsage (%d) must be at least 1MB (1048576 bytes)", maxMemoryUsage))
}
if maxMemoryUsage > 10737418240 { // 10GB maximum
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxMemoryUsage (%d) is unreasonably high (max 10GB)", maxMemoryUsage))
}
maxPendingWrites := viper.GetInt("backpressure.maxPendingWrites")
if maxPendingWrites < 1 {
errors = append(
errors,
fmt.Sprintf("backpressure.maxPendingWrites (%d) must be at least 1", maxPendingWrites),
)
}
if maxPendingWrites > 10000 {
errors = append(
errors,
fmt.Sprintf("backpressure.maxPendingWrites (%d) is unreasonably high (max 10000)", maxPendingWrites),
)
}
return errors
}
// validateBackpressureMaxMemoryUsage validates max memory usage configuration.
func validateBackpressureMaxMemoryUsage() []string {
var errors []string
if !viper.IsSet("backpressure.maxMemoryUsage") {
return errors
}
if viper.IsSet("backpressure.memoryCheckInterval") {
interval := viper.GetInt("backpressure.memoryCheckInterval")
if interval < 1 {
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.memoryCheckInterval (%d) must be at least 1", interval))
}
if interval > 100000 {
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.memoryCheckInterval (%d) is unreasonably high (max 100000)", interval))
}
maxMemoryUsage := viper.GetInt64("backpressure.maxMemoryUsage")
if maxMemoryUsage < 1048576 { // 1MB minimum
errors = append(
errors,
fmt.Sprintf("backpressure.maxMemoryUsage (%d) must be at least 1MB (1048576 bytes)", maxMemoryUsage),
)
}
if maxMemoryUsage > 104857600 { // 100MB maximum
errors = append(
errors,
fmt.Sprintf("backpressure.maxMemoryUsage (%d) is unreasonably high (max 100MB)", maxMemoryUsage),
)
}
return errors
}
// validateBackpressureMemoryCheckInterval validates memory check interval configuration.
func validateBackpressureMemoryCheckInterval() []string {
var errors []string
if !viper.IsSet("backpressure.memoryCheckInterval") {
return errors
}
// Validate resource limits configuration
if viper.IsSet("resourceLimits.maxFiles") {
maxFiles := viper.GetInt("resourceLimits.maxFiles")
if maxFiles < MinMaxFiles {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.maxFiles (%d) must be at least %d", maxFiles, MinMaxFiles))
}
if maxFiles > MaxMaxFiles {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.maxFiles (%d) exceeds maximum (%d)", maxFiles, MaxMaxFiles))
}
interval := viper.GetInt("backpressure.memoryCheckInterval")
if interval < 1 {
errors = append(
errors,
fmt.Sprintf("backpressure.memoryCheckInterval (%d) must be at least 1", interval),
)
}
if interval > 100000 {
errors = append(
errors,
fmt.Sprintf("backpressure.memoryCheckInterval (%d) is unreasonably high (max 100000)", interval),
)
}
return errors
}
// validateBackpressureConfig validates the back-pressure configuration.
func validateBackpressureConfig() []string {
var errors []string
errors = append(errors, validateBackpressureMaxPendingFiles()...)
errors = append(errors, validateBackpressureMaxPendingWrites()...)
errors = append(errors, validateBackpressureMaxMemoryUsage()...)
errors = append(errors, validateBackpressureMemoryCheckInterval()...)
return errors
}
// validateResourceLimits validates the resource limits configuration.
// validateResourceLimitsMaxFiles validates max files configuration.
func validateResourceLimitsMaxFiles() []string {
var errors []string
if !viper.IsSet("resourceLimits.maxFiles") {
return errors
}
if viper.IsSet("resourceLimits.maxTotalSize") {
maxTotalSize := viper.GetInt64("resourceLimits.maxTotalSize")
if maxTotalSize < MinMaxTotalSize {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.maxTotalSize (%d) must be at least %d", maxTotalSize, MinMaxTotalSize))
}
if maxTotalSize > MaxMaxTotalSize {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.maxTotalSize (%d) exceeds maximum (%d)", maxTotalSize, MaxMaxTotalSize))
}
maxFiles := viper.GetInt("resourceLimits.maxFiles")
if maxFiles < MinMaxFiles {
errors = append(
errors,
fmt.Sprintf("resourceLimits.maxFiles (%d) must be at least %d", maxFiles, MinMaxFiles),
)
}
if maxFiles > MaxMaxFiles {
errors = append(
errors,
fmt.Sprintf("resourceLimits.maxFiles (%d) exceeds maximum (%d)", maxFiles, MaxMaxFiles),
)
}
return errors
}
// validateResourceLimitsMaxTotalSize validates max total size configuration.
func validateResourceLimitsMaxTotalSize() []string {
var errors []string
if !viper.IsSet("resourceLimits.maxTotalSize") {
return errors
}
maxTotalSize := viper.GetInt64("resourceLimits.maxTotalSize")
if maxTotalSize < MinMaxTotalSize {
errors = append(
errors,
fmt.Sprintf("resourceLimits.maxTotalSize (%d) must be at least %d", maxTotalSize, MinMaxTotalSize),
)
}
if maxTotalSize > MaxMaxTotalSize {
errors = append(
errors,
fmt.Sprintf("resourceLimits.maxTotalSize (%d) exceeds maximum (%d)", maxTotalSize, MaxMaxTotalSize),
)
}
return errors
}
// validateResourceLimitsTimeouts validates timeout configurations.
func validateResourceLimitsTimeouts() []string {
var errors []string
if viper.IsSet("resourceLimits.fileProcessingTimeoutSec") {
timeout := viper.GetInt("resourceLimits.fileProcessingTimeoutSec")
if timeout < MinFileProcessingTimeoutSec {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.fileProcessingTimeoutSec (%d) must be at least %d", timeout, MinFileProcessingTimeoutSec))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.fileProcessingTimeoutSec (%d) must be at least %d",
timeout,
MinFileProcessingTimeoutSec,
),
)
}
if timeout > MaxFileProcessingTimeoutSec {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.fileProcessingTimeoutSec (%d) exceeds maximum (%d)", timeout, MaxFileProcessingTimeoutSec))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.fileProcessingTimeoutSec (%d) exceeds maximum (%d)",
timeout,
MaxFileProcessingTimeoutSec,
),
)
}
}
if viper.IsSet("resourceLimits.overallTimeoutSec") {
timeout := viper.GetInt("resourceLimits.overallTimeoutSec")
if timeout < MinOverallTimeoutSec {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.overallTimeoutSec (%d) must be at least %d", timeout, MinOverallTimeoutSec))
errors = append(
errors,
fmt.Sprintf("resourceLimits.overallTimeoutSec (%d) must be at least %d", timeout, MinOverallTimeoutSec),
)
}
if timeout > MaxOverallTimeoutSec {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.overallTimeoutSec (%d) exceeds maximum (%d)", timeout, MaxOverallTimeoutSec))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.overallTimeoutSec (%d) exceeds maximum (%d)",
timeout,
MaxOverallTimeoutSec,
),
)
}
}
return errors
}
// validateResourceLimitsConcurrency validates concurrency configurations.
func validateResourceLimitsConcurrency() []string {
var errors []string
if viper.IsSet("resourceLimits.maxConcurrentReads") {
maxReads := viper.GetInt("resourceLimits.maxConcurrentReads")
if maxReads < MinMaxConcurrentReads {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.maxConcurrentReads (%d) must be at least %d", maxReads, MinMaxConcurrentReads))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.maxConcurrentReads (%d) must be at least %d",
maxReads,
MinMaxConcurrentReads,
),
)
}
if maxReads > MaxMaxConcurrentReads {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.maxConcurrentReads (%d) exceeds maximum (%d)", maxReads, MaxMaxConcurrentReads))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.maxConcurrentReads (%d) exceeds maximum (%d)",
maxReads,
MaxMaxConcurrentReads,
),
)
}
}
if viper.IsSet("resourceLimits.rateLimitFilesPerSec") {
rateLimit := viper.GetInt("resourceLimits.rateLimitFilesPerSec")
if rateLimit < MinRateLimitFilesPerSec {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.rateLimitFilesPerSec (%d) must be at least %d", rateLimit, MinRateLimitFilesPerSec))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.rateLimitFilesPerSec (%d) must be at least %d",
rateLimit,
MinRateLimitFilesPerSec,
),
)
}
if rateLimit > MaxRateLimitFilesPerSec {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.rateLimitFilesPerSec (%d) exceeds maximum (%d)", rateLimit, MaxRateLimitFilesPerSec))
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.rateLimitFilesPerSec (%d) exceeds maximum (%d)",
rateLimit,
MaxRateLimitFilesPerSec,
),
)
}
}
if viper.IsSet("resourceLimits.hardMemoryLimitMB") {
memLimit := viper.GetInt("resourceLimits.hardMemoryLimitMB")
if memLimit < MinHardMemoryLimitMB {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.hardMemoryLimitMB (%d) must be at least %d", memLimit, MinHardMemoryLimitMB))
}
if memLimit > MaxHardMemoryLimitMB {
validationErrors = append(validationErrors, fmt.Sprintf("resourceLimits.hardMemoryLimitMB (%d) exceeds maximum (%d)", memLimit, MaxHardMemoryLimitMB))
}
return errors
}
// validateResourceLimitsMemory validates memory limit configuration.
func validateResourceLimitsMemory() []string {
var errors []string
if !viper.IsSet("resourceLimits.hardMemoryLimitMB") {
return errors
}
memLimit := viper.GetInt("resourceLimits.hardMemoryLimitMB")
if memLimit < MinHardMemoryLimitMB {
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.hardMemoryLimitMB (%d) must be at least %d",
memLimit,
MinHardMemoryLimitMB,
),
)
}
if memLimit > MaxHardMemoryLimitMB {
errors = append(
errors,
fmt.Sprintf(
"resourceLimits.hardMemoryLimitMB (%d) exceeds maximum (%d)",
memLimit,
MaxHardMemoryLimitMB,
),
)
}
return errors
}
// validateResourceLimits validates the resource limits configuration.
func validateResourceLimits() []string {
var errors []string
errors = append(errors, validateResourceLimitsMaxFiles()...)
errors = append(errors, validateResourceLimitsMaxTotalSize()...)
errors = append(errors, validateResourceLimitsTimeouts()...)
errors = append(errors, validateResourceLimitsConcurrency()...)
errors = append(errors, validateResourceLimitsMemory()...)
return errors
}
// ValidateConfig validates the loaded configuration.
func ValidateConfig() error {
var validationErrors []string
// Collect validation errors from all validation helpers
validationErrors = append(validationErrors, validateFileSizeLimit()...)
validationErrors = append(validationErrors, validateIgnoreDirectories()...)
validationErrors = append(validationErrors, validateSupportedFormats()...)
validationErrors = append(validationErrors, validateConcurrencySettings()...)
validationErrors = append(validationErrors, validateFilePatterns()...)
validationErrors = append(validationErrors, validateFileTypes()...)
validationErrors = append(validationErrors, validateBackpressureConfig()...)
validationErrors = append(validationErrors, validateResourceLimits()...)
if len(validationErrors) > 0 {
return utils.NewStructuredError(
utils.ErrorTypeConfiguration,
utils.CodeConfigValidation,
return gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeConfiguration,
gibidiutils.CodeConfigValidation,
"configuration validation failed: "+strings.Join(validationErrors, "; "),
"",
map[string]interface{}{"validation_errors": validationErrors},
@@ -253,9 +545,9 @@ func ValidateConfig() error {
func ValidateFileSize(size int64) error {
limit := GetFileSizeLimit()
if size > limit {
return utils.NewStructuredError(
utils.ErrorTypeValidation,
utils.CodeValidationSize,
return gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationSize,
fmt.Sprintf("file size (%d bytes) exceeds limit (%d bytes)", size, limit),
"",
map[string]interface{}{"file_size": size, "size_limit": limit},
@@ -267,9 +559,9 @@ func ValidateFileSize(size int64) error {
// ValidateOutputFormat checks if an output format is valid.
func ValidateOutputFormat(format string) error {
if !IsValidFormat(format) {
return utils.NewStructuredError(
utils.ErrorTypeValidation,
utils.CodeValidationFormat,
return gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
fmt.Sprintf("unsupported output format: %s (supported: json, yaml, markdown)", format),
"",
map[string]interface{}{"format": format},
@@ -281,9 +573,9 @@ func ValidateOutputFormat(format string) error {
// ValidateConcurrency checks if a concurrency level is valid.
func ValidateConcurrency(concurrency int) error {
if concurrency < 1 {
return utils.NewStructuredError(
utils.ErrorTypeValidation,
utils.CodeValidationFormat,
return gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
fmt.Sprintf("concurrency (%d) must be at least 1", concurrency),
"",
map[string]interface{}{"concurrency": concurrency},
@@ -293,9 +585,9 @@ func ValidateConcurrency(concurrency int) error {
if viper.IsSet("maxConcurrency") {
maxConcurrency := GetMaxConcurrency()
if concurrency > maxConcurrency {
return utils.NewStructuredError(
utils.ErrorTypeValidation,
utils.CodeValidationFormat,
return gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
fmt.Sprintf("concurrency (%d) exceeds maximum (%d)", concurrency, maxConcurrency),
"",
map[string]interface{}{"concurrency": concurrency, "max_concurrency": maxConcurrency},
@@ -304,4 +596,4 @@ func ValidateConcurrency(concurrency int) error {
}
return nil
}
}

View File

@@ -1,13 +1,14 @@
package config_test
import (
"errors"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/ivuorinen/gibidify/config"
"github.com/ivuorinen/gibidify/utils"
"github.com/ivuorinen/gibidify/gibidiutils"
)
// TestValidateConfig tests the configuration validation functionality.
@@ -112,21 +113,19 @@ func TestValidateConfig(t *testing.T) {
}
// Check that it's a structured error
var structErr *utils.StructuredError
var structErr *gibidiutils.StructuredError
if !errorAs(err, &structErr) {
t.Errorf("Expected structured error, got %T", err)
return
}
if structErr.Type != utils.ErrorTypeConfiguration {
t.Errorf("Expected error type %v, got %v", utils.ErrorTypeConfiguration, structErr.Type)
if structErr.Type != gibidiutils.ErrorTypeConfiguration {
t.Errorf("Expected error type %v, got %v", gibidiutils.ErrorTypeConfiguration, structErr.Type)
}
if structErr.Code != utils.CodeConfigValidation {
t.Errorf("Expected error code %v, got %v", utils.CodeConfigValidation, structErr.Code)
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
if structErr.Code != gibidiutils.CodeConfigValidation {
t.Errorf("Expected error code %v, got %v", gibidiutils.CodeConfigValidation, structErr.Code)
}
} else if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
})
}
@@ -235,11 +234,12 @@ func errorAs(err error, target interface{}) bool {
if err == nil {
return false
}
if structErr, ok := err.(*utils.StructuredError); ok {
if ptr, ok := target.(**utils.StructuredError); ok {
var structErr *gibidiutils.StructuredError
if errors.As(err, &structErr) {
if ptr, ok := target.(**gibidiutils.StructuredError); ok {
*ptr = structErr
return true
}
}
return false
}
}