verstak-desktop/internal/core/files/path_policy.go

86 lines
2.0 KiB
Go

package files
import (
"fmt"
"path"
"path/filepath"
"strings"
"unicode"
)
func NormalizeRelativeDir(relativeDir string) (string, error) {
return normalizeRelativePath(relativeDir, true)
}
func NormalizeRelativeFile(relativePath string) (string, error) {
return normalizeRelativePath(relativePath, false)
}
func IsReservedPath(relativePath string) bool {
normalized := strings.ReplaceAll(relativePath, "\\", "/")
cleaned := path.Clean(normalized)
if cleaned == "." {
cleaned = ""
}
if cleaned == "" {
return false
}
first := strings.Split(cleaned, "/")[0]
return strings.EqualFold(first, ".verstak")
}
func normalizeRelativePath(input string, allowRoot bool) (string, error) {
if strings.Contains(input, "\x00") {
return "", fmt.Errorf("invalid-path: null-byte")
}
if strings.Contains(input, "\\") {
return "", fmt.Errorf("invalid-path: backslash not allowed")
}
if looksAbsolute(input) {
return "", fmt.Errorf("invalid-path: absolute path rejected")
}
normalized := input
for _, part := range strings.Split(normalized, "/") {
if part == ".." {
return "", fmt.Errorf("invalid-path: path-traversal")
}
}
cleaned := path.Clean(normalized)
if cleaned == "." {
cleaned = ""
}
if cleaned == "" && !allowRoot {
return "", fmt.Errorf("invalid-path: empty path")
}
if cleaned == ".." || strings.HasPrefix(cleaned, "../") {
return "", fmt.Errorf("invalid-path: path-traversal")
}
if IsReservedPathNoNormalize(cleaned) {
return "", fmt.Errorf("reserved-path: .verstak is internal")
}
return cleaned, nil
}
func IsReservedPathNoNormalize(cleaned string) bool {
if cleaned == "" {
return false
}
first := strings.Split(cleaned, "/")[0]
return strings.EqualFold(first, ".verstak")
}
func looksAbsolute(input string) bool {
if input == "" {
return false
}
if filepath.IsAbs(input) || strings.HasPrefix(input, "/") || strings.HasPrefix(input, "\\\\") || strings.HasPrefix(input, "\\") {
return true
}
if len(input) >= 2 && input[1] == ':' && unicode.IsLetter(rune(input[0])) {
return true
}
return false
}