69 lines
1.7 KiB
Go
69 lines
1.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var windowsAbsPathRE = regexp.MustCompile(`^[A-Za-z]:[\\/].*`)
|
|
|
|
func sortedStringKeys[V any](values map[string]V) []string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
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
|
|
}
|