248 lines
8.2 KiB
Go
248 lines
8.2 KiB
Go
package worktree
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// CreateOwnedDirectoryLink creates a live directory reference below root.
|
|
// root is created only through real (non-symlink) ancestors, and name must be
|
|
// a single path segment. The caller owns root; this function never alters the
|
|
// target or an existing entry.
|
|
func CreateOwnedDirectoryLink(root, name, target string) (string, error) {
|
|
if !isOwnedDirectoryLinkPath(root, name) {
|
|
return "", fmt.Errorf("invalid owned link path")
|
|
}
|
|
canonicalTarget, err := canonicalDirectoryLinkTarget(target)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := mkdirOwned(root); err != nil {
|
|
return "", err
|
|
}
|
|
link, err := ownedDirectoryLinkPath(root, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = os.Lstat(link)
|
|
if err == nil {
|
|
return "", fmt.Errorf("owned link entry already exists: %s", name)
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return "", fmt.Errorf("inspect owned link entry: %w", err)
|
|
}
|
|
if err := createPlatformDirectoryLink(canonicalTarget, link); err != nil {
|
|
return "", fmt.Errorf("create directory link: %w", err)
|
|
}
|
|
if err := verifyCreatedOwnedDirectoryLink(root, link); err != nil {
|
|
return "", err
|
|
}
|
|
return link, nil
|
|
}
|
|
|
|
func isOwnedDirectoryLinkPath(root, name string) bool {
|
|
return root != "" && filepath.IsAbs(root) && name != "" && filepath.Base(name) == name && name != "." && name != ".."
|
|
}
|
|
|
|
func canonicalDirectoryLinkTarget(target string) (string, error) {
|
|
canonicalTarget, err := filepath.EvalSymlinks(target)
|
|
if err != nil {
|
|
return "", fmt.Errorf("canonicalize link target: %w", err)
|
|
}
|
|
info, err := os.Stat(canonicalTarget)
|
|
if err != nil || !info.IsDir() {
|
|
return "", fmt.Errorf("link target is not a directory: %w", err)
|
|
}
|
|
return canonicalTarget, nil
|
|
}
|
|
|
|
func ownedDirectoryLinkPath(root, name string) (string, error) {
|
|
link := filepath.Join(root, name)
|
|
if filepath.Dir(link) != filepath.Clean(root) {
|
|
return "", fmt.Errorf("link escapes owned root")
|
|
}
|
|
return link, nil
|
|
}
|
|
|
|
func verifyCreatedOwnedDirectoryLink(root, link string) error {
|
|
if err := requireRealDir(root); err != nil {
|
|
_ = os.Remove(link)
|
|
return err
|
|
}
|
|
if err := requirePlatformDirectoryLink(link); err != nil {
|
|
_ = os.Remove(link)
|
|
return fmt.Errorf("owned link changed during creation")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EnsureOwnedDirectoryLink returns an existing matching live link or creates
|
|
// it. A non-link/collision, or a link to another directory, fails closed and is
|
|
// never replaced.
|
|
//
|
|
// The existing link is matched by filesystem identity, not by resolved path.
|
|
// filepath.EvalSymlinks does not traverse a Windows junction — it returns the
|
|
// link's own normalized path — so a path comparison rejected every unchanged
|
|
// junction as a target mismatch, and each relaunch or resume of a task with
|
|
// workspace source links failed. os.Stat follows a junction and a Unix symlink
|
|
// alike, and os.SameFile compares volume and file index, which is also immune
|
|
// to 8.3 short paths and path case.
|
|
func EnsureOwnedDirectoryLink(root, name, target string) (string, bool, error) {
|
|
link := filepath.Join(root, name)
|
|
info, err := os.Lstat(link)
|
|
if err == nil {
|
|
if !isPlatformDirectoryLink(info, link) {
|
|
return "", false, fmt.Errorf("owned link entry already exists: %s", name)
|
|
}
|
|
actual, err := os.Stat(link)
|
|
if err != nil {
|
|
return "", false, fmt.Errorf("resolve owned link: %w", err)
|
|
}
|
|
expected, err := os.Stat(target)
|
|
if err != nil {
|
|
return "", false, fmt.Errorf("inspect link target: %w", err)
|
|
}
|
|
if !os.SameFile(actual, expected) {
|
|
return "", false, fmt.Errorf("owned link target mismatch: %s", name)
|
|
}
|
|
return link, false, nil
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return "", false, fmt.Errorf("inspect owned link entry: %w", err)
|
|
}
|
|
created, err := CreateOwnedDirectoryLink(root, name, target)
|
|
return created, err == nil, err
|
|
}
|
|
|
|
// IsSelfReferentialDirectoryLink reports whether root/name is a platform
|
|
// directory link whose target is root itself — the shape an earlier release
|
|
// planted inside a user's own repository for local-executor tasks, and which
|
|
// EnsureOwnedDirectoryLink then accepts as valid forever.
|
|
//
|
|
// It deliberately only reports. Such an entry cannot be shown to be
|
|
// Kandev-owned: Kandev writes no ownership marker into user-owned sources, and
|
|
// a user (or the repository itself) may keep a link of the same name and target
|
|
// on purpose, so removing it could destroy content that is not ours. Nor could
|
|
// a stat-then-remove sequence close the window between the check and the
|
|
// unlink. Callers surface it and leave removal to the user.
|
|
func IsSelfReferentialDirectoryLink(root, name string) (bool, error) {
|
|
if !isOwnedDirectoryLinkPath(root, name) {
|
|
return false, fmt.Errorf("invalid owned link path")
|
|
}
|
|
link, err := ownedDirectoryLinkPath(root, name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
info, err := os.Lstat(link)
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("inspect owned link entry: %w", err)
|
|
}
|
|
if !isPlatformDirectoryLink(info, link) {
|
|
return false, nil
|
|
}
|
|
return linkTargetsRoot(root, link)
|
|
}
|
|
|
|
// linkTargetsRoot compares filesystem identity, not resolved path text.
|
|
// filepath.EvalSymlinks does not traverse a Windows junction — it returns the
|
|
// link's own normalized path — so a path comparison never matches. os.Stat
|
|
// follows a junction and a Unix symlink alike, and os.SameFile then compares
|
|
// volume and file index, which also absorbs 8.3 short paths and case.
|
|
func linkTargetsRoot(root, link string) (bool, error) {
|
|
linkInfo, err := os.Stat(link)
|
|
if err != nil {
|
|
return false, fmt.Errorf("resolve owned link: %w", err)
|
|
}
|
|
rootInfo, err := os.Stat(root)
|
|
if err != nil {
|
|
return false, fmt.Errorf("resolve owned root: %w", err)
|
|
}
|
|
return os.SameFile(linkInfo, rootInfo), nil
|
|
}
|
|
|
|
func mkdirOwned(root string) error {
|
|
parent := filepath.Dir(root)
|
|
if err := requireNoSymlinkAncestors(parent); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(parent, 0o755); err != nil {
|
|
return fmt.Errorf("create owned parent: %w", err)
|
|
}
|
|
if err := requireRealDir(parent); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Mkdir(root, 0o755); err != nil && !os.IsExist(err) {
|
|
return fmt.Errorf("create owned root: %w", err)
|
|
}
|
|
return requireRealDir(root)
|
|
}
|
|
|
|
// requireNoSymlinkAncestors checks every existing control-path component
|
|
// before MkdirAll can traverse it. It deliberately uses Lstat so a junction
|
|
// or symlink is rejected rather than resolved.
|
|
func requireNoSymlinkAncestors(path string) error {
|
|
clean := filepath.Clean(path)
|
|
volume := filepath.VolumeName(clean)
|
|
rest := strings.TrimPrefix(clean, volume)
|
|
rest = strings.TrimPrefix(rest, string(filepath.Separator))
|
|
current := volume + string(filepath.Separator)
|
|
for _, part := range strings.Split(rest, string(filepath.Separator)) {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
current = filepath.Join(current, part)
|
|
info, err := os.Lstat(current)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("inspect owned ancestor: %w", err)
|
|
}
|
|
if isPlatformDirectoryLink(info, current) {
|
|
return fmt.Errorf("owned control ancestor is symlink: %s", current)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func requireRealDir(path string) error {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect owned path: %w", err)
|
|
}
|
|
if isPlatformDirectoryLink(info, path) || !info.IsDir() {
|
|
return fmt.Errorf("owned path is not a real directory: %s", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TaskRoot returns the canonical Kandev-owned root for a task directory name.
|
|
// It deliberately accepts only the same single-segment names used by worktree
|
|
// creation, preventing a persisted environment row from selecting another
|
|
// directory beneath (or outside) the configured tasks base.
|
|
func (m *Manager) TaskRoot(taskDirName string) (string, error) {
|
|
if taskDirName == "" || filepath.Base(taskDirName) != taskDirName || taskDirName == "." || taskDirName == ".." {
|
|
return "", fmt.Errorf("invalid task directory name")
|
|
}
|
|
base, err := m.config.ExpandedTasksBasePath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
base, err = filepath.Abs(base)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := requireRealDir(base); err != nil {
|
|
return "", err
|
|
}
|
|
if err := requireNoSymlinkAncestors(base); err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(base, taskDirName), nil
|
|
}
|