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 }