package files import ( "errors" "io/fs" "os" "path/filepath" "strings" ) const ( maxSubfolderDepth = 4 maxNameBytes = 255 ) var ErrUnsafePath = errors.New("path is not allowed") func ResolveInside(root, relative string) (string, error) { cleaned := filepath.Clean(filepath.FromSlash(relative)) if relative == "" || cleaned == "." || !filepath.IsLocal(cleaned) { return "", ErrUnsafePath } resolvedRoot, err := filepath.EvalSymlinks(root) if err != nil { return "", err } candidate := filepath.Join(resolvedRoot, cleaned) info, err := os.Lstat(candidate) if err != nil { return "", err } if info.Mode()&fs.ModeSymlink != 0 { return "", ErrUnsafePath } resolved, err := filepath.EvalSymlinks(candidate) if err != nil { return "", err } if !IsWithin(resolvedRoot, resolved) { return "", ErrUnsafePath } return resolved, nil } func IsWithin(root, target string) bool { relative, err := filepath.Rel(root, target) return err == nil && relative != "." && filepath.IsLocal(relative) } func SanitizeSubfolder(raw string) (string, error) { trimmed := strings.Trim(strings.TrimSpace(raw), "/") if trimmed == "" { return "", nil } parts := strings.Split(trimmed, "/") if len(parts) > maxSubfolderDepth { return "", ErrUnsafePath } for index, part := range parts { part = strings.TrimSpace(part) if part == "" || part == "." || part == ".." || len(part) > maxNameBytes || strings.ContainsFunc(part, isUnsafeRune) { return "", ErrUnsafePath } parts[index] = part } return filepath.Join(parts...), nil } func EnsureSubdirectory(root, subfolder string) (string, error) { current := root if subfolder == "" { return current, nil } for _, part := range strings.Split(subfolder, string(filepath.Separator)) { current = filepath.Join(current, part) info, err := os.Lstat(current) switch { case errors.Is(err, fs.ErrNotExist): if err := os.Mkdir(current, 0o755); err != nil && !errors.Is(err, fs.ErrExist) { return "", err } case err != nil: return "", err case !info.IsDir(): return "", ErrUnsafePath } } return current, nil } func isUnsafeRune(character rune) bool { return character < 0x20 || character == 0x7f || character == '\\' }