diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/turbopath | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'cli/internal/turbopath')
| -rw-r--r-- | cli/internal/turbopath/absolute_system_path.go | 258 | ||||
| -rw-r--r-- | cli/internal/turbopath/absolute_system_path_darwin.go | 23 | ||||
| -rw-r--r-- | cli/internal/turbopath/absolute_system_path_notdarwin.go | 13 | ||||
| -rw-r--r-- | cli/internal/turbopath/absolute_system_path_test.go | 174 | ||||
| -rw-r--r-- | cli/internal/turbopath/anchored_system_path.go | 75 | ||||
| -rw-r--r-- | cli/internal/turbopath/anchored_unix_path.go | 31 | ||||
| -rw-r--r-- | cli/internal/turbopath/find_up.go | 50 | ||||
| -rw-r--r-- | cli/internal/turbopath/relative_system_path.go | 44 | ||||
| -rw-r--r-- | cli/internal/turbopath/relative_unix_path.go | 31 | ||||
| -rw-r--r-- | cli/internal/turbopath/turbopath.go | 112 |
10 files changed, 811 insertions, 0 deletions
diff --git a/cli/internal/turbopath/absolute_system_path.go b/cli/internal/turbopath/absolute_system_path.go new file mode 100644 index 0000000..df65827 --- /dev/null +++ b/cli/internal/turbopath/absolute_system_path.go @@ -0,0 +1,258 @@ +package turbopath + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// AbsoluteSystemPath is a root-relative path using system separators. +type AbsoluteSystemPath string + +// _dirPermissions are the default permission bits we apply to directories. +const _dirPermissions = os.ModeDir | 0775 + +// _nonRelativeSentinel is the leading sentinel that indicates traversal. +const _nonRelativeSentinel = ".." + +// ToString returns a string represenation of this Path. +// Used for interfacing with APIs that require a string. +func (p AbsoluteSystemPath) ToString() string { + return string(p) +} + +// RelativeTo calculates the relative path between two `AbsoluteSystemPath`s. +func (p AbsoluteSystemPath) RelativeTo(basePath AbsoluteSystemPath) (AnchoredSystemPath, error) { + processed, err := filepath.Rel(basePath.ToString(), p.ToString()) + return AnchoredSystemPath(processed), err +} + +// Join appends relative path segments to this AbsoluteSystemPath. +func (p AbsoluteSystemPath) Join(additional ...RelativeSystemPath) AbsoluteSystemPath { + cast := RelativeSystemPathArray(additional) + return AbsoluteSystemPath(filepath.Join(p.ToString(), filepath.Join(cast.ToStringArray()...))) +} + +// ToStringDuringMigration returns a string representation of this path. +// These instances should eventually be removed. +func (p AbsoluteSystemPath) ToStringDuringMigration() string { + return p.ToString() +} + +// UntypedJoin is a Join that does not constrain the type of the arguments. +// This enables you to pass in strings, but does not protect you from garbage in. +func (p AbsoluteSystemPath) UntypedJoin(args ...string) AbsoluteSystemPath { + return AbsoluteSystemPath(filepath.Join(p.ToString(), filepath.Join(args...))) +} + +// Dir implements filepath.Dir() for an AbsoluteSystemPath +func (p AbsoluteSystemPath) Dir() AbsoluteSystemPath { + return AbsoluteSystemPath(filepath.Dir(p.ToString())) +} + +// Mkdir implements os.Mkdir(p, perm) +func (p AbsoluteSystemPath) Mkdir(perm os.FileMode) error { + return os.Mkdir(p.ToString(), perm) +} + +// MkdirAll implements os.MkdirAll(p, perm) +func (p AbsoluteSystemPath) MkdirAll(perm os.FileMode) error { + return os.MkdirAll(p.ToString(), perm) +} + +// Open implements os.Open(p) for an AbsoluteSystemPath +func (p AbsoluteSystemPath) Open() (*os.File, error) { + return os.Open(p.ToString()) +} + +// OpenFile implements os.OpenFile for an absolute path +func (p AbsoluteSystemPath) OpenFile(flags int, mode os.FileMode) (*os.File, error) { + return os.OpenFile(p.ToString(), flags, mode) +} + +// Lstat implements os.Lstat for absolute path +func (p AbsoluteSystemPath) Lstat() (os.FileInfo, error) { + return os.Lstat(p.ToString()) +} + +// Stat implements os.Stat for absolute path +func (p AbsoluteSystemPath) Stat() (os.FileInfo, error) { + return os.Stat(p.ToString()) +} + +// Findup checks all parent directories for a file. +func (p AbsoluteSystemPath) Findup(name RelativeSystemPath) (AbsoluteSystemPath, error) { + path, err := FindupFrom(name.ToString(), p.ToString()) + + return AbsoluteSystemPath(path), err + +} + +// Exists returns true if the given path exists. +func (p AbsoluteSystemPath) Exists() bool { + _, err := p.Lstat() + return err == nil +} + +// DirExists returns true if the given path exists and is a directory. +func (p AbsoluteSystemPath) DirExists() bool { + info, err := p.Lstat() + return err == nil && info.IsDir() +} + +// FileExists returns true if the given path exists and is a file. +func (p AbsoluteSystemPath) FileExists() bool { + info, err := os.Lstat(p.ToString()) + return err == nil && !info.IsDir() +} + +// ContainsPath returns true if this absolute path is a parent of the +// argument. +func (p AbsoluteSystemPath) ContainsPath(other AbsoluteSystemPath) (bool, error) { + // In Go, filepath.Rel can return a path that starts with "../" or equivalent. + // Checking filesystem-level contains can get extremely complicated + // (see https://github.com/golang/dep/blob/f13583b555deaa6742f141a9c1185af947720d60/internal/fs/fs.go#L33) + // As a compromise, rely on the stdlib to generate a relative path and then check + // if the first step is "../". + rel, err := filepath.Rel(p.ToString(), other.ToString()) + if err != nil { + return false, err + } + return !strings.HasPrefix(rel, _nonRelativeSentinel), nil +} + +// ReadFile reads the contents of the specified file +func (p AbsoluteSystemPath) ReadFile() ([]byte, error) { + return ioutil.ReadFile(p.ToString()) +} + +// VolumeName returns the volume of the specified path +func (p AbsoluteSystemPath) VolumeName() string { + return filepath.VolumeName(p.ToString()) +} + +// WriteFile writes the contents of the specified file +func (p AbsoluteSystemPath) WriteFile(contents []byte, mode os.FileMode) error { + return ioutil.WriteFile(p.ToString(), contents, mode) +} + +// EnsureDir ensures that the directory containing this file exists +func (p AbsoluteSystemPath) EnsureDir() error { + dir := p.Dir() + err := os.MkdirAll(dir.ToString(), _dirPermissions) + if err != nil && dir.FileExists() { + // It looks like this is a file and not a directory. Attempt to remove it; this can + // happen in some cases if you change a rule from outputting a file to a directory. + if err2 := dir.Remove(); err2 == nil { + err = os.MkdirAll(dir.ToString(), _dirPermissions) + } else { + return err + } + } + return err +} + +// MkdirAllMode Create directory at path and all necessary parents ensuring that path has the correct mode set +func (p AbsoluteSystemPath) MkdirAllMode(mode os.FileMode) error { + info, err := p.Lstat() + if err == nil { + if info.IsDir() && info.Mode() == mode { + // Dir exists with the correct mode + return nil + } else if info.IsDir() { + // Dir exists with incorrect mode + return os.Chmod(p.ToString(), mode) + } else { + // Path exists as file, remove it + if err := p.Remove(); err != nil { + return err + } + } + } + if err := os.MkdirAll(p.ToString(), mode); err != nil { + return err + } + // This is necessary only when umask results in creating a directory with permissions different than the one passed by the user + return os.Chmod(p.ToString(), mode) +} + +// Create is the AbsoluteSystemPath wrapper for os.Create +func (p AbsoluteSystemPath) Create() (*os.File, error) { + return os.Create(p.ToString()) +} + +// Ext implements filepath.Ext(p) for an absolute path +func (p AbsoluteSystemPath) Ext() string { + return filepath.Ext(p.ToString()) +} + +// RelativePathString returns the relative path from this AbsoluteSystemPath to another absolute path in string form as a string +func (p AbsoluteSystemPath) RelativePathString(path string) (string, error) { + return filepath.Rel(p.ToString(), path) +} + +// PathTo returns the relative path between two absolute paths +// This should likely eventually return an AnchoredSystemPath +func (p AbsoluteSystemPath) PathTo(other AbsoluteSystemPath) (string, error) { + return p.RelativePathString(other.ToString()) +} + +// Symlink implements os.Symlink(target, p) for absolute path +func (p AbsoluteSystemPath) Symlink(target string) error { + return os.Symlink(target, p.ToString()) +} + +// Readlink implements os.Readlink(p) for an absolute path +func (p AbsoluteSystemPath) Readlink() (string, error) { + return os.Readlink(p.ToString()) +} + +// Remove removes the file or (empty) directory at the given path +func (p AbsoluteSystemPath) Remove() error { + return os.Remove(p.ToString()) +} + +// RemoveAll implements os.RemoveAll for absolute paths. +func (p AbsoluteSystemPath) RemoveAll() error { + return os.RemoveAll(p.ToString()) +} + +// Base implements filepath.Base for an absolute path +func (p AbsoluteSystemPath) Base() string { + return filepath.Base(p.ToString()) +} + +// Rename implements os.Rename(p, dest) for absolute paths +func (p AbsoluteSystemPath) Rename(dest AbsoluteSystemPath) error { + return os.Rename(p.ToString(), dest.ToString()) +} + +// EvalSymlinks implements filepath.EvalSymlinks for absolute path +func (p AbsoluteSystemPath) EvalSymlinks() (AbsoluteSystemPath, error) { + result, err := filepath.EvalSymlinks(p.ToString()) + if err != nil { + return "", err + } + return AbsoluteSystemPath(result), nil +} + +// HasPrefix is strings.HasPrefix for paths, ensuring that it matches on separator boundaries. +// This does NOT perform Clean in advance. +func (p AbsoluteSystemPath) HasPrefix(prefix AbsoluteSystemPath) bool { + prefixLen := len(prefix) + pathLen := len(p) + + if prefixLen > pathLen { + // Can't be a prefix if longer. + return false + } else if prefixLen == pathLen { + // Can be a prefix if they're equal, but otherwise no. + return p == prefix + } + + // otherPath is definitely shorter than p. + // We need to confirm that p[len(otherPath)] is a system separator. + + return strings.HasPrefix(p.ToString(), prefix.ToString()) && os.IsPathSeparator(p[prefixLen]) +} diff --git a/cli/internal/turbopath/absolute_system_path_darwin.go b/cli/internal/turbopath/absolute_system_path_darwin.go new file mode 100644 index 0000000..e2c3bff --- /dev/null +++ b/cli/internal/turbopath/absolute_system_path_darwin.go @@ -0,0 +1,23 @@ +//go:build darwin +// +build darwin + +// Adapted from https://github.com/containerd/continuity/blob/b4ca35286886296377de39e6eafd1affae019fc3/driver/lchmod_unix.go +// Copyright The containerd Authors +// SPDX-License-Identifier: Apache-2.0 + +package turbopath + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// Lchmod changes the mode of a file not following symlinks. +func (p AbsoluteSystemPath) Lchmod(mode os.FileMode) error { + err := unix.Fchmodat(unix.AT_FDCWD, p.ToString(), uint32(mode), unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + err = &os.PathError{Op: "lchmod", Path: p.ToString(), Err: err} + } + return err +} diff --git a/cli/internal/turbopath/absolute_system_path_notdarwin.go b/cli/internal/turbopath/absolute_system_path_notdarwin.go new file mode 100644 index 0000000..1195888 --- /dev/null +++ b/cli/internal/turbopath/absolute_system_path_notdarwin.go @@ -0,0 +1,13 @@ +//go:build !darwin +// +build !darwin + +package turbopath + +import ( + "os" +) + +// Lchmod changes the mode of a file not following symlinks. +func (p AbsoluteSystemPath) Lchmod(mode os.FileMode) error { + return nil +} diff --git a/cli/internal/turbopath/absolute_system_path_test.go b/cli/internal/turbopath/absolute_system_path_test.go new file mode 100644 index 0000000..4ca36f9 --- /dev/null +++ b/cli/internal/turbopath/absolute_system_path_test.go @@ -0,0 +1,174 @@ +package turbopath + +import ( + "os" + "runtime" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" +) + +func Test_Mkdir(t *testing.T) { + type Case struct { + name string + isDir bool + exists bool + mode os.FileMode + expectedMode os.FileMode + } + + cases := []Case{ + { + name: "dir doesn't exist", + exists: false, + expectedMode: os.ModeDir | 0777, + }, + { + name: "path exists as file", + exists: true, + isDir: false, + mode: 0666, + expectedMode: os.ModeDir | 0755, + }, + { + name: "dir exists with incorrect mode", + exists: true, + isDir: false, + mode: os.ModeDir | 0755, + expectedMode: os.ModeDir | 0655, + }, + { + name: "dir exists with correct mode", + exists: true, + isDir: false, + mode: os.ModeDir | 0755, + expectedMode: os.ModeDir | 0755, + }, + } + + for _, testCase := range cases { + testDir := fs.NewDir(t, "system-path-mkdir-test") + testName := testCase.name + path := testDir.Join("foo") + if testCase.isDir { + err := os.Mkdir(path, testCase.mode) + assert.NilError(t, err, "%s: Mkdir", testName) + } else if testCase.exists { + file, err := os.Create(path) + assert.NilError(t, err, "%s: Create", testName) + err = file.Chmod(testCase.mode) + assert.NilError(t, err, "%s: Chmod", testName) + err = file.Close() + assert.NilError(t, err, "%s: Close", testName) + } + + testPath := AbsoluteSystemPath(path) + err := testPath.MkdirAllMode(testCase.expectedMode) + assert.NilError(t, err, "%s: Mkdir", testName) + + stat, err := testPath.Lstat() + assert.NilError(t, err, "%s: Lstat", testName) + assert.Assert(t, stat.IsDir(), testName) + + assert.Assert(t, stat.IsDir(), testName) + + if runtime.GOOS == "windows" { + // For windows os.Chmod will only change the writable bit so that's all we check + assert.Equal(t, stat.Mode().Perm()&0200, testCase.expectedMode.Perm()&0200, testName) + } else { + assert.Equal(t, stat.Mode(), testCase.expectedMode, testName) + } + + } +} + +func TestAbsoluteSystemPath_Findup(t *testing.T) { + tests := []struct { + name string + fs []AnchoredSystemPath + executionDirectory AnchoredSystemPath + fileName RelativeSystemPath + want AnchoredSystemPath + wantErr bool + }{ + { + name: "hello world", + fs: []AnchoredSystemPath{ + AnchoredUnixPath("one/two/three/four/.file").ToSystemPath(), + AnchoredUnixPath("one/two/three/four/.target").ToSystemPath(), + }, + executionDirectory: AnchoredUnixPath("one/two/three/four").ToSystemPath(), + fileName: RelativeUnixPath(".target").ToSystemPath(), + want: AnchoredUnixPath("one/two/three/four/.target").ToSystemPath(), + }, + { + name: "parent", + fs: []AnchoredSystemPath{ + AnchoredUnixPath("one/two/three/four/.file").ToSystemPath(), + AnchoredUnixPath("one/two/three/.target").ToSystemPath(), + }, + executionDirectory: AnchoredUnixPath("one/two/three/four").ToSystemPath(), + fileName: RelativeUnixPath(".target").ToSystemPath(), + want: AnchoredUnixPath("one/two/three/.target").ToSystemPath(), + }, + { + name: "gets the closest", + fs: []AnchoredSystemPath{ + AnchoredUnixPath("one/two/three/four/.file").ToSystemPath(), + AnchoredUnixPath("one/two/three/.target").ToSystemPath(), + AnchoredUnixPath("one/two/.target").ToSystemPath(), + }, + executionDirectory: AnchoredUnixPath("one/two/three/four").ToSystemPath(), + fileName: RelativeUnixPath(".target").ToSystemPath(), + want: AnchoredUnixPath("one/two/three/.target").ToSystemPath(), + }, + { + name: "nonexistent", + fs: []AnchoredSystemPath{ + AnchoredUnixPath("one/two/three/four/.file").ToSystemPath(), + }, + executionDirectory: AnchoredUnixPath("one/two/three/four").ToSystemPath(), + fileName: RelativeUnixPath(".nonexistent").ToSystemPath(), + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsRoot := AbsoluteSystemPath(t.TempDir()) + for _, file := range tt.fs { + path := file.RestoreAnchor(fsRoot) + assert.NilError(t, path.Dir().MkdirAll(0777)) + assert.NilError(t, path.WriteFile(nil, 0777)) + } + + got, err := tt.executionDirectory.RestoreAnchor(fsRoot).Findup(tt.fileName) + if tt.wantErr { + assert.ErrorIs(t, err, os.ErrNotExist) + return + } + if got != "" && got != tt.want.RestoreAnchor(fsRoot) { + t.Errorf("AbsoluteSystemPath.Findup() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestJoin(t *testing.T) { + rawRoot, err := os.Getwd() + if err != nil { + t.Fatalf("cwd %v", err) + } + root := AbsoluteSystemPathFromUpstream(rawRoot) + testRoot := root.Join("a", "b", "c") + dot := testRoot.Join(".") + if dot != testRoot { + t.Errorf(". path got %v, want %v", dot, testRoot) + } + + doubleDot := testRoot.Join("..") + expectedDoubleDot := root.Join("a", "b") + if doubleDot != expectedDoubleDot { + t.Errorf(".. path got %v, want %v", doubleDot, expectedDoubleDot) + } +} diff --git a/cli/internal/turbopath/anchored_system_path.go b/cli/internal/turbopath/anchored_system_path.go new file mode 100644 index 0000000..0957ead --- /dev/null +++ b/cli/internal/turbopath/anchored_system_path.go @@ -0,0 +1,75 @@ +package turbopath + +import ( + "os" + "path/filepath" + "strings" +) + +// AnchoredSystemPath is a path stemming from a specified root using system separators. +type AnchoredSystemPath string + +// ToString returns a string represenation of this Path. +// Used for interfacing with APIs that require a string. +func (p AnchoredSystemPath) ToString() string { + return string(p) +} + +// ToStringDuringMigration returns the string representation of this path, and is for +// use in situations where we expect a future path migration to remove the need for the +// string representation +func (p AnchoredSystemPath) ToStringDuringMigration() string { + return string(p) +} + +// ToSystemPath returns itself. +func (p AnchoredSystemPath) ToSystemPath() AnchoredSystemPath { + return p +} + +// ToUnixPath converts a AnchoredSystemPath to a AnchoredUnixPath. +func (p AnchoredSystemPath) ToUnixPath() AnchoredUnixPath { + return AnchoredUnixPath(filepath.ToSlash(p.ToString())) +} + +// RelativeTo calculates the relative path between two AnchoredSystemPath`s. +func (p AnchoredSystemPath) RelativeTo(basePath AnchoredSystemPath) (AnchoredSystemPath, error) { + processed, err := filepath.Rel(basePath.ToString(), p.ToString()) + return AnchoredSystemPath(processed), err +} + +// RestoreAnchor prefixes the AnchoredSystemPath with its anchor to return an AbsoluteSystemPath. +func (p AnchoredSystemPath) RestoreAnchor(anchor AbsoluteSystemPath) AbsoluteSystemPath { + return AbsoluteSystemPath(filepath.Join(anchor.ToString(), p.ToString())) +} + +// Dir returns filepath.Dir for the path. +func (p AnchoredSystemPath) Dir() AnchoredSystemPath { + return AnchoredSystemPath(filepath.Dir(p.ToString())) +} + +// Join appends relative path segments to this AnchoredSystemPath. +func (p AnchoredSystemPath) Join(additional ...RelativeSystemPath) AnchoredSystemPath { + cast := RelativeSystemPathArray(additional) + return AnchoredSystemPath(filepath.Join(p.ToString(), filepath.Join(cast.ToStringArray()...))) +} + +// HasPrefix is strings.HasPrefix for paths, ensuring that it matches on separator boundaries. +// This does NOT perform Clean in advance. +func (p AnchoredSystemPath) HasPrefix(prefix AnchoredSystemPath) bool { + prefixLen := len(prefix) + pathLen := len(p) + + if prefixLen > pathLen { + // Can't be a prefix if longer. + return false + } else if prefixLen == pathLen { + // Can be a prefix if they're equal, but otherwise no. + return p == prefix + } + + // otherPath is definitely shorter than p. + // We need to confirm that p[len(otherPath)] is a system separator. + + return strings.HasPrefix(p.ToString(), prefix.ToString()) && os.IsPathSeparator(p[prefixLen]) +} diff --git a/cli/internal/turbopath/anchored_unix_path.go b/cli/internal/turbopath/anchored_unix_path.go new file mode 100644 index 0000000..23e371a --- /dev/null +++ b/cli/internal/turbopath/anchored_unix_path.go @@ -0,0 +1,31 @@ +package turbopath + +import ( + "path" + "path/filepath" +) + +// AnchoredUnixPath is a path stemming from a specified root using Unix `/` separators. +type AnchoredUnixPath string + +// ToString returns a string represenation of this Path. +// Used for interfacing with APIs that require a string. +func (p AnchoredUnixPath) ToString() string { + return string(p) +} + +// ToSystemPath converts a AnchoredUnixPath to a AnchoredSystemPath. +func (p AnchoredUnixPath) ToSystemPath() AnchoredSystemPath { + return AnchoredSystemPath(filepath.FromSlash(p.ToString())) +} + +// ToUnixPath returns itself. +func (p AnchoredUnixPath) ToUnixPath() AnchoredUnixPath { + return p +} + +// Join appends relative path segments to this RelativeUnixPath. +func (p AnchoredUnixPath) Join(additional ...RelativeUnixPath) AnchoredUnixPath { + cast := RelativeUnixPathArray(additional) + return AnchoredUnixPath(path.Join(p.ToString(), path.Join(cast.ToStringArray()...))) +} diff --git a/cli/internal/turbopath/find_up.go b/cli/internal/turbopath/find_up.go new file mode 100644 index 0000000..bf7c39c --- /dev/null +++ b/cli/internal/turbopath/find_up.go @@ -0,0 +1,50 @@ +package turbopath + +import ( + "os" + "path/filepath" +) + +func hasFile(name, dir string) (bool, error) { + files, err := os.ReadDir(dir) + + if err != nil { + return false, err + } + + for _, f := range files { + if name == f.Name() { + return true, nil + } + } + + return false, nil +} + +func findupFrom(name, dir string) (string, error) { + for { + found, err := hasFile(name, dir) + + if err != nil { + return "", err + } + + if found { + return filepath.Join(dir, name), nil + } + + parent := filepath.Dir(dir) + + if parent == dir { + return "", nil + } + + dir = parent + } +} + +// FindupFrom Recursively finds a file by walking up parents in the file tree +// starting from a specific directory. +func FindupFrom(name, dir string) (string, error) { + return findupFrom(name, dir) +} diff --git a/cli/internal/turbopath/relative_system_path.go b/cli/internal/turbopath/relative_system_path.go new file mode 100644 index 0000000..d6115db --- /dev/null +++ b/cli/internal/turbopath/relative_system_path.go @@ -0,0 +1,44 @@ +package turbopath + +import ( + "fmt" + "path/filepath" +) + +// RelativeSystemPath is a relative path using system separators. +type RelativeSystemPath string + +// CheckedToRelativeSystemPath inspects a string and determines if it is a relative path. +func CheckedToRelativeSystemPath(s string) (RelativeSystemPath, error) { + if filepath.IsAbs(s) { + return "", fmt.Errorf("%v is not a relative path", s) + } + return RelativeSystemPath(filepath.Clean(s)), nil +} + +// MakeRelativeSystemPath joins the given segments in a system-appropriate way +func MakeRelativeSystemPath(segments ...string) RelativeSystemPath { + return RelativeSystemPath(filepath.Join(segments...)) +} + +// ToString returns a string represenation of this Path. +// Used for interfacing with APIs that require a string. +func (p RelativeSystemPath) ToString() string { + return string(p) +} + +// ToSystemPath returns itself. +func (p RelativeSystemPath) ToSystemPath() RelativeSystemPath { + return p +} + +// ToUnixPath converts from RelativeSystemPath to RelativeUnixPath. +func (p RelativeSystemPath) ToUnixPath() RelativeUnixPath { + return RelativeUnixPath(filepath.ToSlash(p.ToString())) +} + +// Join appends relative path segments to this RelativeSystemPath. +func (p RelativeSystemPath) Join(additional ...RelativeSystemPath) RelativeSystemPath { + cast := RelativeSystemPathArray(additional) + return RelativeSystemPath(filepath.Join(p.ToString(), filepath.Join(cast.ToStringArray()...))) +} diff --git a/cli/internal/turbopath/relative_unix_path.go b/cli/internal/turbopath/relative_unix_path.go new file mode 100644 index 0000000..05829e2 --- /dev/null +++ b/cli/internal/turbopath/relative_unix_path.go @@ -0,0 +1,31 @@ +package turbopath + +import ( + "path" + "path/filepath" +) + +// RelativeUnixPath is a relative path using Unix `/` separators. +type RelativeUnixPath string + +// ToString returns a string represenation of this Path. +// Used for interfacing with APIs that require a string. +func (p RelativeUnixPath) ToString() string { + return string(p) +} + +// ToSystemPath converts a RelativeUnixPath to a RelativeSystemPath. +func (p RelativeUnixPath) ToSystemPath() RelativeSystemPath { + return RelativeSystemPath(filepath.FromSlash(p.ToString())) +} + +// ToUnixPath converts a RelativeUnixPath to a RelativeSystemPath. +func (p RelativeUnixPath) ToUnixPath() RelativeUnixPath { + return p +} + +// Join appends relative path segments to this RelativeUnixPath. +func (p RelativeUnixPath) Join(additional ...RelativeUnixPath) RelativeUnixPath { + cast := RelativeUnixPathArray(additional) + return RelativeUnixPath(path.Join(p.ToString(), path.Join(cast.ToStringArray()...))) +} diff --git a/cli/internal/turbopath/turbopath.go b/cli/internal/turbopath/turbopath.go new file mode 100644 index 0000000..f50b75f --- /dev/null +++ b/cli/internal/turbopath/turbopath.go @@ -0,0 +1,112 @@ +// Package turbopath teaches the Go type system about six +// different types of paths: +// - AbsoluteSystemPath +// - RelativeSystemPath +// - AnchoredSystemPath +// - AbsoluteUnixPath +// - RelativeUnixPath +// - AnchoredUnixPath +// +// Between these two things it is assumed that we will be able to +// reasonably describe file paths being used within the system and +// have the type system enforce correctness instead of relying upon +// runtime code to accomplish the task. +// +// Absolute paths are, "absolute, including volume root." They are not +// portable between System and Unix. +// +// Relative paths are simply arbitrary path segments using a particular +// path delimiter. They are portable between System and Unix. +// +// Anchored paths are, "absolute, starting at a particular root." +// They are not aware of *what* their anchor is. It could be a repository, +// an `os.dirFS`, a package, `cwd`, or more. They are stored *without* +// a preceding delimiter for compatibility with `io/fs`. They are portable +// between System and Unix. +// +// In some future world everything in here can be optimized out at compile time. +// Everything is either `string` or `[]string` +// +// Much of this is dreadfully repetitive because of intentional +// limitations in the Go type system. +package turbopath + +// AnchoredUnixPathArray is a type used to enable transform operations on arrays of paths. +type AnchoredUnixPathArray []AnchoredUnixPath + +// RelativeSystemPathArray is a type used to enable transform operations on arrays of paths. +type RelativeSystemPathArray []RelativeSystemPath + +// RelativeUnixPathArray is a type used to enable transform operations on arrays of paths. +type RelativeUnixPathArray []RelativeUnixPath + +// ToStringArray enables ergonomic operations on arrays of RelativeSystemPath +func (source RelativeSystemPathArray) ToStringArray() []string { + output := make([]string, len(source)) + for index, path := range source { + output[index] = path.ToString() + } + return output +} + +// ToStringArray enables ergonomic operations on arrays of RelativeUnixPath +func (source RelativeUnixPathArray) ToStringArray() []string { + output := make([]string, len(source)) + for index, path := range source { + output[index] = path.ToString() + } + return output +} + +// ToSystemPathArray enables ergonomic operations on arrays of AnchoredUnixPath +func (source AnchoredUnixPathArray) ToSystemPathArray() []AnchoredSystemPath { + output := make([]AnchoredSystemPath, len(source)) + for index, path := range source { + output[index] = path.ToSystemPath() + } + return output +} + +// The following methods exist to import a path string and cast it to the appropriate +// type. They exist to communicate intent and make it explicit that this is an +// intentional action, not a "helpful" insertion by the IDE. +// +// This is intended to map closely to the `unsafe` keyword, without the denotative +// meaning of `unsafe` in English. These are "trust me, I've checkex it" places, and +// intend to mark the places where we smuggle paths from outside the world of safe +// path handling into the world where we carefully consider the path to ensure safety. + +// AbsoluteSystemPathFromUpstream takes a path string and casts it to an +// AbsoluteSystemPath without checking. If the input to this function is +// not an AbsoluteSystemPath it will result in downstream errors. +func AbsoluteSystemPathFromUpstream(path string) AbsoluteSystemPath { + return AbsoluteSystemPath(path) +} + +// AnchoredSystemPathFromUpstream takes a path string and casts it to an +// AnchoredSystemPath without checking. If the input to this function is +// not an AnchoredSystemPath it will result in downstream errors. +func AnchoredSystemPathFromUpstream(path string) AnchoredSystemPath { + return AnchoredSystemPath(path) +} + +// AnchoredUnixPathFromUpstream takes a path string and casts it to an +// AnchoredUnixPath without checking. If the input to this function is +// not an AnchoredUnixPath it will result in downstream errors. +func AnchoredUnixPathFromUpstream(path string) AnchoredUnixPath { + return AnchoredUnixPath(path) +} + +// RelativeSystemPathFromUpstream takes a path string and casts it to an +// RelativeSystemPath without checking. If the input to this function is +// not an RelativeSystemPath it will result in downstream errors. +func RelativeSystemPathFromUpstream(path string) RelativeSystemPath { + return RelativeSystemPath(path) +} + +// RelativeUnixPathFromUpstream takes a path string and casts it to an +// RelativeUnixPath without checking. If the input to this function is +// not an RelativeUnixPath it will result in downstream errors. +func RelativeUnixPathFromUpstream(path string) RelativeUnixPath { + return RelativeUnixPath(path) +} |
