feat: add strict TOML configuration validation
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateAbsolutePath(name, value string, platform Platform, allowEmpty bool) (string, error) {
|
||||
if value == "" && allowEmpty {
|
||||
return "", nil
|
||||
}
|
||||
if value == "" || strings.IndexByte(value, 0) >= 0 {
|
||||
return "", fmt.Errorf("%s must be a nonempty absolute path", name)
|
||||
}
|
||||
if platform == PlatformWindows {
|
||||
if !isWindowsAbsolute(value) {
|
||||
return "", fmt.Errorf("%s must be an absolute Windows path", name)
|
||||
}
|
||||
return cleanWindowsPath(value), nil
|
||||
}
|
||||
if !filepath.IsAbs(value) {
|
||||
return "", fmt.Errorf("%s must be an absolute Unix path", name)
|
||||
}
|
||||
return filepath.Clean(value), nil
|
||||
}
|
||||
|
||||
func isWindowsAbsolute(value string) bool {
|
||||
if strings.HasPrefix(value, `\\`) {
|
||||
parts := strings.Split(strings.TrimPrefix(value, `\\`), `\`)
|
||||
return len(parts) >= 2 && parts[0] != "" && parts[1] != ""
|
||||
}
|
||||
return len(value) >= 3 && ((value[0] >= 'A' && value[0] <= 'Z') || (value[0] >= 'a' && value[0] <= 'z')) &&
|
||||
value[1] == ':' && (value[2] == '\\' || value[2] == '/')
|
||||
}
|
||||
|
||||
func cleanWindowsPath(value string) string {
|
||||
value = strings.ReplaceAll(value, "/", `\`)
|
||||
prefix := value[:3]
|
||||
rest := strings.TrimPrefix(value[3:], `\`)
|
||||
if strings.HasPrefix(value, `\\`) {
|
||||
components := strings.Split(strings.TrimPrefix(value, `\\`), `\`)
|
||||
prefix = `\\` + components[0] + `\` + components[1] + `\`
|
||||
rest = strings.Join(components[2:], `\`)
|
||||
}
|
||||
parts := make([]string, 0)
|
||||
for _, part := range strings.Split(rest, `\`) {
|
||||
switch part {
|
||||
case "", ".":
|
||||
continue
|
||||
case "..":
|
||||
if len(parts) > 0 {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
default:
|
||||
parts = append(parts, part)
|
||||
}
|
||||
}
|
||||
return prefix + strings.Join(parts, `\`)
|
||||
}
|
||||
|
||||
func checkNativeExecutable(name, value string, platform Platform) (string, error) {
|
||||
if (platform == PlatformWindows) != (runtime.GOOS == "windows") {
|
||||
return "", fmt.Errorf("%s filesystem validation requires a native %s process", name, map[bool]string{true: "Windows", false: "Unix"}[platform == PlatformWindows])
|
||||
}
|
||||
canonical, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if evaluated, evalErr := filepath.EvalSymlinks(canonical); evalErr == nil {
|
||||
canonical = evaluated
|
||||
}
|
||||
info, err := os.Stat(canonical)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", fmt.Errorf("%s must name a regular file", name)
|
||||
}
|
||||
if platform == PlatformUnix && info.Mode().Perm()&0o111 == 0 {
|
||||
return "", fmt.Errorf("%s is not executable", name)
|
||||
}
|
||||
if platform == PlatformWindows {
|
||||
extension := strings.ToLower(filepath.Ext(canonical))
|
||||
if extension != ".exe" && extension != ".com" && extension != ".cmd" && extension != ".bat" {
|
||||
return "", fmt.Errorf("%s is not a recognized Windows executable", name)
|
||||
}
|
||||
}
|
||||
return canonical, nil
|
||||
}
|
||||
|
||||
func checkNativeDirectory(name, value string, platform Platform) (string, error) {
|
||||
if (platform == PlatformWindows) != (runtime.GOOS == "windows") {
|
||||
return "", fmt.Errorf("%s filesystem validation requires a native target", name)
|
||||
}
|
||||
canonical, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if evaluated, evalErr := filepath.EvalSymlinks(canonical); evalErr == nil {
|
||||
canonical = evaluated
|
||||
}
|
||||
info, err := os.Stat(canonical)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("%s must name a directory", name)
|
||||
}
|
||||
return canonical, nil
|
||||
}
|
||||
|
||||
func checkNativeRegularFile(name, value string, platform Platform) (string, error) {
|
||||
if (platform == PlatformWindows) != (runtime.GOOS == "windows") {
|
||||
return "", fmt.Errorf("%s filesystem validation requires a native target", name)
|
||||
}
|
||||
canonical, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if evaluated, evalErr := filepath.EvalSymlinks(canonical); evalErr == nil {
|
||||
canonical = evaluated
|
||||
}
|
||||
info, err := os.Stat(canonical)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", fmt.Errorf("%s must name a regular file", name)
|
||||
}
|
||||
return canonical, nil
|
||||
}
|
||||
Reference in New Issue
Block a user