diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index dc38cce05..138373402 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -31,6 +31,7 @@ import ( "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/fsutil" "k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/proto/hapi/chart" @@ -205,7 +206,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { return fmt.Errorf("%q is not a directory", destPath) } - if err := os.Rename(destPath, tmpPath); err != nil { + if err := fs.RenameWithFallback(destPath, tmpPath); err != nil { return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) } @@ -307,7 +308,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { if err := os.RemoveAll(destPath); err != nil { return fmt.Errorf("Failed to remove %v: %v", destPath, err) } - if err := os.Rename(tmpPath, destPath); err != nil { + if err := fs.RenameWithFallback(tmpPath, destPath); err != nil { return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) } return saveError @@ -686,7 +687,7 @@ func move(tmpPath, destPath string) error { filename := file.Name() tmpfile := filepath.Join(tmpPath, filename) destfile := filepath.Join(destPath, filename) - if err := os.Rename(tmpfile, destfile); err != nil { + if err := fs.RenameWithFallback(tmpfile, destfile); err != nil { return fmt.Errorf("Unable to move local charts to charts dir: %v", err) } } diff --git a/pkg/fsutil/fs.go b/pkg/fsutil/fs.go new file mode 100644 index 000000000..a1e44eee4 --- /dev/null +++ b/pkg/fsutil/fs.go @@ -0,0 +1,694 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "syscall" + "unicode" + + "github.com/pkg/errors" +) + +// HasFilepathPrefix will determine if "path" starts with "prefix" from +// the point of view of a filesystem. +// +// Unlike filepath.HasPrefix, this function is path-aware, meaning that +// it knows that two directories /foo and /foobar are not the same +// thing, and therefore HasFilepathPrefix("/foobar", "/foo") will return +// false. +// +// This function also handles the case where the involved filesystems +// are case-insensitive, meaning /foo/bar and /Foo/Bar correspond to the +// same file. In that situation HasFilepathPrefix("/Foo/Bar", "/foo") +// will return true. The implementation is *not* OS-specific, so a FAT32 +// filesystem mounted on Linux will be handled correctly. +func HasFilepathPrefix(path, prefix string) (bool, error) { + // this function is more convoluted then ideal due to need for special + // handling of volume name/drive letter on Windows. vnPath and vnPrefix + // are first compared, and then used to initialize initial values of p and + // d which will be appended to for incremental checks using + // IsCaseSensitiveFilesystem and then equality. + + // no need to check IsCaseSensitiveFilesystem because VolumeName return + // empty string on all non-Windows machines + vnPath := strings.ToLower(filepath.VolumeName(path)) + vnPrefix := strings.ToLower(filepath.VolumeName(prefix)) + if vnPath != vnPrefix { + return false, nil + } + + // Because filepath.Join("c:","dir") returns "c:dir", we have to manually + // add path separator to drive letters. Also, we need to set the path root + // on *nix systems, since filepath.Join("", "dir") returns a relative path. + vnPath += string(os.PathSeparator) + vnPrefix += string(os.PathSeparator) + + var dn string + + if isDir, err := IsDir(path); err != nil { + return false, errors.Wrap(err, "failed to check filepath prefix") + } else if isDir { + dn = path + } else { + dn = filepath.Dir(path) + } + + dn = filepath.Clean(dn) + prefix = filepath.Clean(prefix) + + // [1:] in the lines below eliminates empty string on *nix and volume name on Windows + dirs := strings.Split(dn, string(os.PathSeparator))[1:] + prefixes := strings.Split(prefix, string(os.PathSeparator))[1:] + + if len(prefixes) > len(dirs) { + return false, nil + } + + // d,p are initialized with "/" on *nix and volume name on Windows + d := vnPath + p := vnPrefix + + for i := range prefixes { + // need to test each component of the path for + // case-sensitiveness because on Unix we could have + // something like ext4 filesystem mounted on FAT + // mountpoint, mounted on ext4 filesystem, i.e. the + // problematic filesystem is not the last one. + caseSensitive, err := IsCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) + if err != nil { + return false, errors.Wrap(err, "failed to check filepath prefix") + } + if caseSensitive { + d = filepath.Join(d, dirs[i]) + p = filepath.Join(p, prefixes[i]) + } else { + d = filepath.Join(d, strings.ToLower(dirs[i])) + p = filepath.Join(p, strings.ToLower(prefixes[i])) + } + + if p != d { + return false, nil + } + } + + return true, nil +} + +// EquivalentPaths compares the paths passed to check if they are equivalent. +// It respects the case-sensitivity of the underlying filesysyems. +func EquivalentPaths(p1, p2 string) (bool, error) { + p1 = filepath.Clean(p1) + p2 = filepath.Clean(p2) + + fi1, err := os.Stat(p1) + if err != nil { + return false, errors.Wrapf(err, "could not check for path equivalence") + } + fi2, err := os.Stat(p2) + if err != nil { + return false, errors.Wrapf(err, "could not check for path equivalence") + } + + p1Filename, p2Filename := "", "" + + if !fi1.IsDir() { + p1, p1Filename = filepath.Split(p1) + } + if !fi2.IsDir() { + p2, p2Filename = filepath.Split(p2) + } + + if isPrefix1, err := HasFilepathPrefix(p1, p2); err != nil { + return false, errors.Wrap(err, "failed to check for path equivalence") + } else if isPrefix2, err := HasFilepathPrefix(p2, p1); err != nil { + return false, errors.Wrap(err, "failed to check for path equivalence") + } else if !isPrefix1 || !isPrefix2 { + return false, nil + } + + if p1Filename != "" || p2Filename != "" { + caseSensitive, err := IsCaseSensitiveFilesystem(filepath.Join(p1, p1Filename)) + if err != nil { + return false, errors.Wrap(err, "could not check for filesystem case-sensitivity") + } + if caseSensitive { + if p1Filename != p2Filename { + return false, nil + } + } else { + if strings.ToLower(p1Filename) != strings.ToLower(p2Filename) { + return false, nil + } + } + } + + return true, nil +} + +// RenameWithFallback attempts to rename a file or directory, but falls back to +// copying in the event of a cross-device link error. If the fallback copy +// succeeds, src is still removed, emulating normal rename behavior. +func RenameWithFallback(src, dst string) error { + _, err := os.Stat(src) + if err != nil { + return errors.Wrapf(err, "cannot stat %s", src) + } + + err = os.Rename(src, dst) + if err == nil { + return nil + } + + return renameFallback(err, src, dst) +} + +// renameByCopy attempts to rename a file or directory by copying it to the +// destination and then removing the src thus emulating the rename behavior. +func renameByCopy(src, dst string) error { + var cerr error + if dir, _ := IsDir(src); dir { + cerr = CopyDir(src, dst) + if cerr != nil { + cerr = errors.Wrap(cerr, "copying directory failed") + } + } else { + cerr = copyFile(src, dst) + if cerr != nil { + cerr = errors.Wrap(cerr, "copying file failed") + } + } + + if cerr != nil { + return errors.Wrapf(cerr, "rename fallback failed: cannot rename %s to %s", src, dst) + } + + return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src) +} + +// IsCaseSensitiveFilesystem determines if the filesystem where dir +// exists is case sensitive or not. +// +// CAVEAT: this function works by taking the last component of the given +// path and flipping the case of the first letter for which case +// flipping is a reversible operation (/foo/Bar → /foo/bar), then +// testing for the existence of the new filename. There are two +// possibilities: +// +// 1. The alternate filename does not exist. We can conclude that the +// filesystem is case sensitive. +// +// 2. The filename happens to exist. We have to test if the two files +// are the same file (case insensitive file system) or different ones +// (case sensitive filesystem). +// +// If the input directory is such that the last component is composed +// exclusively of case-less codepoints (e.g. numbers), this function will +// return false. +func IsCaseSensitiveFilesystem(dir string) (bool, error) { + alt := filepath.Join(filepath.Dir(dir), genTestFilename(filepath.Base(dir))) + + dInfo, err := os.Stat(dir) + if err != nil { + return false, errors.Wrap(err, "could not determine the case-sensitivity of the filesystem") + } + + aInfo, err := os.Stat(alt) + if err != nil { + // If the file doesn't exists, assume we are on a case-sensitive filesystem. + if os.IsNotExist(err) { + return true, nil + } + + return false, errors.Wrap(err, "could not determine the case-sensitivity of the filesystem") + } + + return !os.SameFile(dInfo, aInfo), nil +} + +// genTestFilename returns a string with at most one rune case-flipped. +// +// The transformation is applied only to the first rune that can be +// reversibly case-flipped, meaning: +// +// * A lowercase rune for which it's true that lower(upper(r)) == r +// * An uppercase rune for which it's true that upper(lower(r)) == r +// +// All the other runes are left intact. +func genTestFilename(str string) string { + flip := true + return strings.Map(func(r rune) rune { + if flip { + if unicode.IsLower(r) { + u := unicode.ToUpper(r) + if unicode.ToLower(u) == r { + r = u + flip = false + } + } else if unicode.IsUpper(r) { + l := unicode.ToLower(r) + if unicode.ToUpper(l) == r { + r = l + flip = false + } + } + } + return r + }, str) +} + +var errPathNotDir = errors.New("given path is not a directory") + +// ReadActualFilenames is used to determine the actual file names in given directory. +// +// On case sensitive file systems like ext4, it will check if those files exist using +// `os.Stat` and return a map with key and value as filenames which exist in the folder. +// +// Otherwise, it reads the contents of the directory and returns a map which has the +// given file name as the key and actual filename as the value(if it was found). +func ReadActualFilenames(dirPath string, names []string) (map[string]string, error) { + actualFilenames := make(map[string]string, len(names)) + if len(names) == 0 { + // This isn't expected to happen for current usage. Adding edge case handling, + // as it may be useful in future. + return actualFilenames, nil + } + // First, check that the given path is valid and it is a directory + dirStat, err := os.Stat(dirPath) + if err != nil { + return nil, errors.Wrap(err, "failed to read actual filenames") + } + + if !dirStat.IsDir() { + return nil, errPathNotDir + } + + // Ideally, we would use `os.Stat` for getting the actual file names but that returns + // the name we passed in as an argument and not the actual filename. So we are forced + // to list the directory contents and check against that. Since this check is costly, + // we do it only if absolutely necessary. + caseSensitive, err := IsCaseSensitiveFilesystem(dirPath) + if err != nil { + return nil, errors.Wrap(err, "failed to read actual filenames") + } + if caseSensitive { + // There will be no difference between actual filename and given filename. So + // just check if those files exist. + for _, name := range names { + _, err := os.Stat(filepath.Join(dirPath, name)) + if err == nil { + actualFilenames[name] = name + } else if !os.IsNotExist(err) { + // Some unexpected err, wrap and return it. + return nil, errors.Wrap(err, "failed to read actual filenames") + } + } + return actualFilenames, nil + } + + dir, err := os.Open(dirPath) + if err != nil { + return nil, errors.Wrap(err, "failed to read actual filenames") + } + defer dir.Close() + + // Pass -1 to read all filenames in directory + filenames, err := dir.Readdirnames(-1) + if err != nil { + return nil, errors.Wrap(err, "failed to read actual filenames") + } + + // namesMap holds the mapping from lowercase name to search name. Using this, we can + // avoid repeatedly looping through names. + namesMap := make(map[string]string, len(names)) + for _, name := range names { + namesMap[strings.ToLower(name)] = name + } + + for _, filename := range filenames { + searchName, ok := namesMap[strings.ToLower(filename)] + if ok { + // We are interested in this file, case insensitive match successful. + actualFilenames[searchName] = filename + if len(actualFilenames) == len(names) { + // We found all that we were looking for. + return actualFilenames, nil + } + } + } + return actualFilenames, nil +} + +var ( + errSrcNotDir = errors.New("source is not a directory") + errDstExist = errors.New("destination already exists") +) + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +func CopyDir(src, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + // We use os.Lstat() here to ensure we don't fall in a loop where a symlink + // actually links to a one of its parent directories. + fi, err := os.Lstat(src) + if err != nil { + return err + } + if !fi.IsDir() { + return errSrcNotDir + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + return errDstExist + } + + if err = os.MkdirAll(dst, fi.Mode()); err != nil { + return errors.Wrapf(err, "cannot mkdir %s", dst) + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return errors.Wrapf(err, "cannot read directory %s", dst) + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + if err = CopyDir(srcPath, dstPath); err != nil { + return errors.Wrap(err, "copying directory failed") + } + } else { + // This will include symlinks, which is what we want when + // copying things. + if err = copyFile(srcPath, dstPath); err != nil { + return errors.Wrap(err, "copying file failed") + } + } + } + + return nil +} + +// copyFile copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all its contents will be replaced by the contents +// of the source file. The file mode will be copied from the source. +func copyFile(src, dst string) (err error) { + if sym, err := IsSymlink(src); err != nil { + return errors.Wrap(err, "symlink check failed") + } else if sym { + if err := cloneSymlink(src, dst); err != nil { + if runtime.GOOS == "windows" { + // If cloning the symlink fails on Windows because the user + // does not have the required privileges, ignore the error and + // fall back to copying the file contents. + // + // ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522): + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx + if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) { + return err + } + } else { + return err + } + } else { + return nil + } + } + + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return + } + + if _, err = io.Copy(out, in); err != nil { + out.Close() + return + } + + // Check for write errors on Close + if err = out.Close(); err != nil { + return + } + + si, err := os.Stat(src) + if err != nil { + return + } + + // Temporary fix for Go < 1.9 + // + // See: https://github.com/golang/dep/issues/774 + // and https://github.com/golang/go/issues/20829 + if runtime.GOOS == "windows" { + dst = fixLongPath(dst) + } + err = os.Chmod(dst, si.Mode()) + + return +} + +// cloneSymlink will create a new symlink that points to the resolved path of sl. +// If sl is a relative symlink, dst will also be a relative symlink. +func cloneSymlink(sl, dst string) error { + resolved, err := os.Readlink(sl) + if err != nil { + return err + } + + return os.Symlink(resolved, dst) +} + +// EnsureDir tries to ensure that a directory is present at the given path. It first +// checks if the directory already exists at the given path. If there isn't one, it tries +// to create it with the given permissions. However, it does not try to create the +// directory recursively. +func EnsureDir(path string, perm os.FileMode) error { + _, err := IsDir(path) + + if os.IsNotExist(err) { + err = os.Mkdir(path, perm) + if err != nil { + return errors.Wrapf(err, "failed to ensure directory at %q", path) + } + } + + return err +} + +// IsDir determines is the path given is a directory or not. +func IsDir(name string) (bool, error) { + fi, err := os.Stat(name) + if err != nil { + return false, err + } + if !fi.IsDir() { + return false, errors.Errorf("%q is not a directory", name) + } + return true, nil +} + +// IsNonEmptyDir determines if the path given is a non-empty directory or not. +func IsNonEmptyDir(name string) (bool, error) { + isDir, err := IsDir(name) + if err != nil && !os.IsNotExist(err) { + return false, err + } else if !isDir { + return false, nil + } + + // Get file descriptor + f, err := os.Open(name) + if err != nil { + return false, err + } + defer f.Close() + + // Query only 1 child. EOF if no children. + _, err = f.Readdirnames(1) + switch err { + case io.EOF: + return false, nil + case nil: + return true, nil + default: + return false, err + } +} + +// IsRegular determines if the path given is a regular file or not. +func IsRegular(name string) (bool, error) { + fi, err := os.Stat(name) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + mode := fi.Mode() + if mode&os.ModeType != 0 { + return false, errors.Errorf("%q is a %v, expected a file", name, mode) + } + return true, nil +} + +// IsSymlink determines if the given path is a symbolic link. +func IsSymlink(path string) (bool, error) { + l, err := os.Lstat(path) + if err != nil { + return false, err + } + + return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil +} + +// fixLongPath returns the extended-length (\\?\-prefixed) form of +// path when needed, in order to avoid the default 260 character file +// path limit imposed by Windows. If path is not easily converted to +// the extended-length form (for example, if path is a relative path +// or contains .. elements), or is short enough, fixLongPath returns +// path unmodified. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath +func fixLongPath(path string) string { + // Do nothing (and don't allocate) if the path is "short". + // Empirically (at least on the Windows Server 2013 builder), + // the kernel is arbitrarily okay with < 248 bytes. That + // matches what the docs above say: + // "When using an API to create a directory, the specified + // path cannot be so long that you cannot append an 8.3 file + // name (that is, the directory name cannot exceed MAX_PATH + // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. + // + // The MSDN docs appear to say that a normal path that is 248 bytes long + // will work; empirically the path must be less then 248 bytes long. + if len(path) < 248 { + // Don't fix. (This is how Go 1.7 and earlier worked, + // not automatically generating the \\?\ form) + return path + } + + // The extended form begins with \\?\, as in + // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. + // The extended form disables evaluation of . and .. path + // elements and disables the interpretation of / as equivalent + // to \. The conversion here rewrites / to \ and elides + // . elements as well as trailing or duplicate separators. For + // simplicity it avoids the conversion entirely for relative + // paths or paths containing .. elements. For now, + // \\server\share paths are not converted to + // \\?\UNC\server\share paths because the rules for doing so + // are less well-specified. + if len(path) >= 2 && path[:2] == `\\` { + // Don't canonicalize UNC paths. + return path + } + if !isAbs(path) { + // Relative path + return path + } + + const prefix = `\\?` + + pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) + copy(pathbuf, prefix) + n := len(path) + r, w := 0, len(prefix) + for r < n { + switch { + case os.IsPathSeparator(path[r]): + // empty block + r++ + case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])): + // /./ + r++ + case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])): + // /../ is currently unhandled + return path + default: + pathbuf[w] = '\\' + w++ + for ; r < n && !os.IsPathSeparator(path[r]); r++ { + pathbuf[w] = path[r] + w++ + } + } + } + // A drive's root directory needs a trailing \ + if w == len(`\\?\c:`) { + pathbuf[w] = '\\' + w++ + } + return string(pathbuf[:w]) +} + +func isAbs(path string) (b bool) { + v := volumeName(path) + if v == "" { + return false + } + path = path[len(v):] + if path == "" { + return false + } + return os.IsPathSeparator(path[0]) +} + +func volumeName(path string) (v string) { + if len(path) < 2 { + return "" + } + // with drive letter + c := path[0] + if path[1] == ':' && + ('0' <= c && c <= '9' || 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z') { + return path[:2] + } + // is it UNC + if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) && + !os.IsPathSeparator(path[2]) && path[2] != '.' { + // first, leading `\\` and next shouldn't be `\`. its server name. + for n := 3; n < l-1; n++ { + // second, next '\' shouldn't be repeated. + if os.IsPathSeparator(path[n]) { + n++ + // third, following something characters. its share name. + if !os.IsPathSeparator(path[n]) { + if path[n] == '.' { + break + } + for ; n < l; n++ { + if os.IsPathSeparator(path[n]) { + break + } + } + return path[:n] + } + break + } + } + } + return "" +} diff --git a/pkg/fsutil/fs_test.go b/pkg/fsutil/fs_test.go new file mode 100644 index 000000000..9af0c0ec9 --- /dev/null +++ b/pkg/fsutil/fs_test.go @@ -0,0 +1,1132 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fs + +import ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/golang/dep/internal/test" + "github.com/pkg/errors" +) + +// This function tests HadFilepathPrefix. It should test it on both case +// sensitive and insensitive situations. However, the only reliable way to test +// case-insensitive behaviour is if using case-insensitive filesystem. This +// cannot be guaranteed in an automated test. Therefore, the behaviour of the +// tests is not to test case sensitivity on *nix and to assume that Windows is +// case-insensitive. Please see link below for some background. +// +// https://superuser.com/questions/266110/how-do-you-make-windows-7-fully-case-sensitive-with-respect-to-the-filesystem +// +// NOTE: NTFS can be made case-sensitive. However many Windows programs, +// including Windows Explorer do not handle gracefully multiple files that +// differ only in capitalization. It is possible that this can cause these tests +// to fail on some setups. +func TestHasFilepathPrefix(t *testing.T) { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + // dir2 is the same as dir but with different capitalization on Windows to + // test case insensitivity + var dir2 string + if runtime.GOOS == "windows" { + dir = strings.ToLower(dir) + dir2 = strings.ToUpper(dir) + } else { + dir2 = dir + } + + // For testing trailing and repeated separators + sep := string(os.PathSeparator) + + cases := []struct { + path string + prefix string + want bool + }{ + {filepath.Join(dir, "a", "b"), filepath.Join(dir2), true}, + {filepath.Join(dir, "a", "b"), dir2 + sep + sep + "a", true}, + {filepath.Join(dir, "a", "b"), filepath.Join(dir2, "a") + sep, true}, + {filepath.Join(dir, "a", "b") + sep, filepath.Join(dir2), true}, + {dir + sep + sep + filepath.Join("a", "b"), filepath.Join(dir2, "a"), true}, + {filepath.Join(dir, "a", "b"), filepath.Join(dir2, "a"), true}, + {filepath.Join(dir, "a", "b"), filepath.Join(dir2, "a", "b"), true}, + {filepath.Join(dir, "a", "b"), filepath.Join(dir2, "c"), false}, + {filepath.Join(dir, "a", "b"), filepath.Join(dir2, "a", "d", "b"), false}, + {filepath.Join(dir, "a", "b"), filepath.Join(dir2, "a", "b2"), false}, + {filepath.Join(dir), filepath.Join(dir2, "a", "b"), false}, + {filepath.Join(dir, "ab"), filepath.Join(dir2, "a", "b"), false}, + {filepath.Join(dir, "ab"), filepath.Join(dir2, "a"), false}, + {filepath.Join(dir, "123"), filepath.Join(dir2, "123"), true}, + {filepath.Join(dir, "123"), filepath.Join(dir2, "1"), false}, + {filepath.Join(dir, "⌘"), filepath.Join(dir2, "⌘"), true}, + {filepath.Join(dir, "a"), filepath.Join(dir2, "⌘"), false}, + {filepath.Join(dir, "⌘"), filepath.Join(dir2, "a"), false}, + } + + for _, c := range cases { + if err := os.MkdirAll(c.path, 0755); err != nil { + t.Fatal(err) + } + + if err = os.MkdirAll(c.prefix, 0755); err != nil { + t.Fatal(err) + } + + got, err := HasFilepathPrefix(c.path, c.prefix) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if c.want != got { + t.Fatalf("dir: %q, prefix: %q, expected: %v, got: %v", c.path, c.prefix, c.want, got) + } + } +} + +// This function tests HadFilepathPrefix. It should test it on both case +// sensitive and insensitive situations. However, the only reliable way to test +// case-insensitive behaviour is if using case-insensitive filesystem. This +// cannot be guaranteed in an automated test. Therefore, the behaviour of the +// tests is not to test case sensitivity on *nix and to assume that Windows is +// case-insensitive. Please see link below for some background. +// +// https://superuser.com/questions/266110/how-do-you-make-windows-7-fully-case-sensitive-with-respect-to-the-filesystem +// +// NOTE: NTFS can be made case-sensitive. However many Windows programs, +// including Windows Explorer do not handle gracefully multiple files that +// differ only in capitalization. It is possible that this can cause these tests +// to fail on some setups. +func TestHasFilepathPrefix_Files(t *testing.T) { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + // dir2 is the same as dir but with different capitalization on Windows to + // test case insensitivity + var dir2 string + if runtime.GOOS == "windows" { + dir = strings.ToLower(dir) + dir2 = strings.ToUpper(dir) + } else { + dir2 = dir + } + + existingFile := filepath.Join(dir, "exists") + if err = os.MkdirAll(existingFile, 0755); err != nil { + t.Fatal(err) + } + + nonExistingFile := filepath.Join(dir, "does_not_exists") + + cases := []struct { + path string + prefix string + want bool + err bool + }{ + {existingFile, filepath.Join(dir2), true, false}, + {nonExistingFile, filepath.Join(dir2), false, true}, + } + + for _, c := range cases { + got, err := HasFilepathPrefix(c.path, c.prefix) + if err != nil && !c.err { + t.Fatalf("unexpected error: %s", err) + } + if c.want != got { + t.Fatalf("dir: %q, prefix: %q, expected: %v, got: %v", c.path, c.prefix, c.want, got) + } + } +} + +func TestEquivalentPaths(t *testing.T) { + h := test.NewHelper(t) + h.TempDir("dir") + h.TempDir("dir2") + + h.TempFile("file", "") + h.TempFile("file2", "") + + h.TempDir("DIR") + h.TempFile("FILE", "") + + testcases := []struct { + p1, p2 string + caseSensitiveEquivalent bool + caseInensitiveEquivalent bool + err bool + }{ + {h.Path("dir"), h.Path("dir"), true, true, false}, + {h.Path("file"), h.Path("file"), true, true, false}, + {h.Path("dir"), h.Path("dir2"), false, false, false}, + {h.Path("file"), h.Path("file2"), false, false, false}, + {h.Path("dir"), h.Path("file"), false, false, false}, + {h.Path("dir"), h.Path("DIR"), false, true, false}, + {strings.ToLower(h.Path("dir")), strings.ToUpper(h.Path("dir")), false, true, true}, + } + + caseSensitive, err := IsCaseSensitiveFilesystem(h.Path("dir")) + if err != nil { + t.Fatal("unexpcted error:", err) + } + + for _, tc := range testcases { + got, err := EquivalentPaths(tc.p1, tc.p2) + if err != nil && !tc.err { + t.Error("unexpected error:", err) + } + if caseSensitive { + if tc.caseSensitiveEquivalent != got { + t.Errorf("expected EquivalentPaths(%q, %q) to be %t on case-sensitive filesystem, got %t", tc.p1, tc.p2, tc.caseSensitiveEquivalent, got) + } + } else { + if tc.caseInensitiveEquivalent != got { + t.Errorf("expected EquivalentPaths(%q, %q) to be %t on case-insensitive filesystem, got %t", tc.p1, tc.p2, tc.caseInensitiveEquivalent, got) + } + } + } +} + +func TestRenameWithFallback(t *testing.T) { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + if err = RenameWithFallback(filepath.Join(dir, "does_not_exists"), filepath.Join(dir, "dst")); err == nil { + t.Fatal("expected an error for non existing file, but got nil") + } + + srcpath := filepath.Join(dir, "src") + + if srcf, err := os.Create(srcpath); err != nil { + t.Fatal(err) + } else { + srcf.Close() + } + + if err = RenameWithFallback(srcpath, filepath.Join(dir, "dst")); err != nil { + t.Fatal(err) + } + + srcpath = filepath.Join(dir, "a") + if err = os.MkdirAll(srcpath, 0777); err != nil { + t.Fatal(err) + } + + dstpath := filepath.Join(dir, "b") + if err = os.MkdirAll(dstpath, 0777); err != nil { + t.Fatal(err) + } + + if err = RenameWithFallback(srcpath, dstpath); err == nil { + t.Fatal("expected an error if dst is an existing directory, but got nil") + } +} + +func TestIsCaseSensitiveFilesystem(t *testing.T) { + isLinux := runtime.GOOS == "linux" + isWindows := runtime.GOOS == "windows" + isMacOS := runtime.GOOS == "darwin" + + if !isLinux && !isWindows && !isMacOS { + t.Skip("Run this test on Windows, Linux and macOS only") + } + + dir, err := ioutil.TempDir("", "TestCaseSensitivity") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + var want bool + if isLinux { + want = true + } else { + want = false + } + + got, err := IsCaseSensitiveFilesystem(dir) + + if err != nil { + t.Fatalf("unexpected error message: \n\t(GOT) %+v", err) + } + + if want != got { + t.Fatalf("unexpected value returned: \n\t(GOT) %t\n\t(WNT) %t", got, want) + } +} + +func TestReadActualFilenames(t *testing.T) { + // We are trying to skip this test on file systems which are case-sensiive. We could + // have used `fs.IsCaseSensitiveFilesystem` for this check. However, the code we are + // testing also relies on `fs.IsCaseSensitiveFilesystem`. So a bug in + // `fs.IsCaseSensitiveFilesystem` could prevent this test from being run. This is the + // only scenario where we prefer the OS heuristic over doing the actual work of + // validating filesystem case sensitivity via `fs.IsCaseSensitiveFilesystem`. + if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { + t.Skip("skip this test on non-Windows, non-macOS") + } + + h := test.NewHelper(t) + defer h.Cleanup() + + h.TempDir("") + tmpPath := h.Path(".") + + // First, check the scenarios for which we expect an error. + _, err := ReadActualFilenames(filepath.Join(tmpPath, "does_not_exists"), []string{""}) + switch { + case err == nil: + t.Fatal("expected err for non-existing folder") + // use `errors.Cause` because the error is wrapped and returned + case !os.IsNotExist(errors.Cause(err)): + t.Fatalf("unexpected error: %+v", err) + } + h.TempFile("tmpFile", "") + _, err = ReadActualFilenames(h.Path("tmpFile"), []string{""}) + switch { + case err == nil: + t.Fatal("expected err for passing file instead of directory") + case err != errPathNotDir: + t.Fatalf("unexpected error: %+v", err) + } + + cases := []struct { + createFiles []string + names []string + want map[string]string + }{ + // If we supply no filenames to the function, it should return an empty map. + {nil, nil, map[string]string{}}, + // If the directory contains the given file with different case, it should return + // a map which has the given filename as the key and actual filename as the value. + { + []string{"test1.txt"}, + []string{"Test1.txt"}, + map[string]string{"Test1.txt": "test1.txt"}, + }, + // 1. If the given filename is same as the actual filename, map should have the + // same key and value for the file. + // 2. If the given filename is present with different case for file extension, + // it should return a map which has the given filename as the key and actual + // filename as the value. + // 3. If the given filename is not present even with a different case, the map + // returned should not have an entry for that filename. + { + []string{"test2.txt", "test3.TXT"}, + []string{"test2.txt", "Test3.txt", "Test4.txt"}, + map[string]string{ + "test2.txt": "test2.txt", + "Test3.txt": "test3.TXT", + }, + }, + } + for _, c := range cases { + for _, file := range c.createFiles { + h.TempFile(file, "") + } + got, err := ReadActualFilenames(tmpPath, c.names) + if err != nil { + t.Fatalf("unexpected error: %+v", err) + } + if !reflect.DeepEqual(c.want, got) { + t.Fatalf("returned value does not match expected: \n\t(GOT) %v\n\t(WNT) %v", + got, c.want) + } + } +} + +func TestGenTestFilename(t *testing.T) { + cases := []struct { + str string + want string + }{ + {"abc", "Abc"}, + {"ABC", "aBC"}, + {"AbC", "abC"}, + {"αβγ", "Αβγ"}, + {"123", "123"}, + {"1a2", "1A2"}, + {"12a", "12A"}, + {"⌘", "⌘"}, + } + + for _, c := range cases { + got := genTestFilename(c.str) + if c.want != got { + t.Fatalf("str: %q, expected: %q, got: %q", c.str, c.want, got) + } + } +} + +func BenchmarkGenTestFilename(b *testing.B) { + cases := []string{ + strings.Repeat("a", 128), + strings.Repeat("A", 128), + strings.Repeat("α", 128), + strings.Repeat("1", 128), + strings.Repeat("⌘", 128), + } + + for i := 0; i < b.N; i++ { + for _, str := range cases { + genTestFilename(str) + } + } +} + +func TestCopyDir(t *testing.T) { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir := filepath.Join(dir, "src") + if err := os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + files := []struct { + path string + contents string + fi os.FileInfo + }{ + {path: "myfile", contents: "hello world"}, + {path: filepath.Join("subdir", "file"), contents: "subdir file"}, + } + + // Create structure indicated in 'files' + for i, file := range files { + fn := filepath.Join(srcdir, file.path) + dn := filepath.Dir(fn) + if err = os.MkdirAll(dn, 0755); err != nil { + t.Fatal(err) + } + + fh, err := os.Create(fn) + if err != nil { + t.Fatal(err) + } + + if _, err = fh.Write([]byte(file.contents)); err != nil { + t.Fatal(err) + } + fh.Close() + + files[i].fi, err = os.Stat(fn) + if err != nil { + t.Fatal(err) + } + } + + destdir := filepath.Join(dir, "dest") + if err := CopyDir(srcdir, destdir); err != nil { + t.Fatal(err) + } + + // Compare copy against structure indicated in 'files' + for _, file := range files { + fn := filepath.Join(srcdir, file.path) + dn := filepath.Dir(fn) + dirOK, err := IsDir(dn) + if err != nil { + t.Fatal(err) + } + if !dirOK { + t.Fatalf("expected %s to be a directory", dn) + } + + got, err := ioutil.ReadFile(fn) + if err != nil { + t.Fatal(err) + } + + if file.contents != string(got) { + t.Fatalf("expected: %s, got: %s", file.contents, string(got)) + } + + gotinfo, err := os.Stat(fn) + if err != nil { + t.Fatal(err) + } + + if file.fi.Mode() != gotinfo.Mode() { + t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v", + file.path, file.fi.Mode(), fn, gotinfo.Mode()) + } + } +} + +func TestCopyDirFail_SrcInaccessible(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. Skipping this this until a + // compatible implementation is provided. + t.Skip("skipping on windows") + } + + var srcdir, dstdir string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + srcdir = filepath.Join(dir, "src") + return os.MkdirAll(srcdir, 0755) + }) + defer cleanup() + + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + dstdir = filepath.Join(dir, "dst") + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } +} + +func TestCopyDirFail_DstInaccessible(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. Skipping this this until a + // compatible implementation is provided. + t.Skip("skipping on windows") + } + + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if err = os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + cleanup := setupInaccessibleDir(t, func(dir string) error { + dstdir = filepath.Join(dir, "dst") + return nil + }) + defer cleanup() + + if err := CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } +} + +func TestCopyDirFail_SrcIsNotDir(t *testing.T) { + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if _, err = os.Create(srcdir); err != nil { + t.Fatal(err) + } + + dstdir = filepath.Join(dir, "dst") + + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } + + if err != errSrcNotDir { + t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errSrcNotDir, srcdir, dstdir, err) + } + +} + +func TestCopyDirFail_DstExists(t *testing.T) { + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if err = os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + dstdir = filepath.Join(dir, "dst") + if err = os.MkdirAll(dstdir, 0755); err != nil { + t.Fatal(err) + } + + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } + + if err != errDstExist { + t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errDstExist, srcdir, dstdir, err) + } +} + +func TestCopyDirFailOpen(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. os.Chmod(..., 0222) below is not + // enough for the file to be readonly, and os.Chmod(..., + // 0000) returns an invalid argument error. Skipping + // this this until a compatible implementation is + // provided. + t.Skip("skipping on windows") + } + + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if err = os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + srcfn := filepath.Join(srcdir, "file") + srcf, err := os.Create(srcfn) + if err != nil { + t.Fatal(err) + } + srcf.Close() + + // setup source file so that it cannot be read + if err = os.Chmod(srcfn, 0222); err != nil { + t.Fatal(err) + } + + dstdir = filepath.Join(dir, "dst") + + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } +} + +func TestCopyFile(t *testing.T) { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcf, err := os.Create(filepath.Join(dir, "srcfile")) + if err != nil { + t.Fatal(err) + } + + want := "hello world" + if _, err := srcf.Write([]byte(want)); err != nil { + t.Fatal(err) + } + srcf.Close() + + destf := filepath.Join(dir, "destf") + if err := copyFile(srcf.Name(), destf); err != nil { + t.Fatal(err) + } + + got, err := ioutil.ReadFile(destf) + if err != nil { + t.Fatal(err) + } + + if want != string(got) { + t.Fatalf("expected: %s, got: %s", want, string(got)) + } + + wantinfo, err := os.Stat(srcf.Name()) + if err != nil { + t.Fatal(err) + } + + gotinfo, err := os.Stat(destf) + if err != nil { + t.Fatal(err) + } + + if wantinfo.Mode() != gotinfo.Mode() { + t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v", srcf.Name(), wantinfo.Mode(), destf, gotinfo.Mode()) + } +} + +func TestCopyFileSymlink(t *testing.T) { + h := test.NewHelper(t) + defer h.Cleanup() + h.TempDir(".") + + testcases := map[string]string{ + filepath.Join("./testdata/symlinks/file-symlink"): filepath.Join(h.Path("."), "dst-file"), + filepath.Join("./testdata/symlinks/windows-file-symlink"): filepath.Join(h.Path("."), "windows-dst-file"), + filepath.Join("./testdata/symlinks/invalid-symlink"): filepath.Join(h.Path("."), "invalid-symlink"), + } + + for symlink, dst := range testcases { + t.Run(symlink, func(t *testing.T) { + var err error + if err = copyFile(symlink, dst); err != nil { + t.Fatalf("failed to copy symlink: %s", err) + } + + var want, got string + + if runtime.GOOS == "windows" { + // Creating symlinks on Windows require an additional permission + // regular users aren't granted usually. So we copy the file + // content as a fall back instead of creating a real symlink. + srcb, err := ioutil.ReadFile(symlink) + h.Must(err) + dstb, err := ioutil.ReadFile(dst) + h.Must(err) + + want = string(srcb) + got = string(dstb) + } else { + want, err = os.Readlink(symlink) + h.Must(err) + + got, err = os.Readlink(dst) + if err != nil { + t.Fatalf("could not resolve symlink: %s", err) + } + } + + if want != got { + t.Fatalf("resolved path is incorrect. expected %s, got %s", want, got) + } + }) + } +} + +func TestCopyFileLongFilePath(t *testing.T) { + if runtime.GOOS != "windows" { + // We want to ensure the temporary fix actually fixes the issue with + // os.Chmod and long file paths. This is only applicable on Windows. + t.Skip("skipping on non-windows") + } + + h := test.NewHelper(t) + h.TempDir(".") + defer h.Cleanup() + + tmpPath := h.Path(".") + + // Create a directory with a long-enough path name to cause the bug in #774. + dirName := "" + for len(tmpPath+string(os.PathSeparator)+dirName) <= 300 { + dirName += "directory" + } + + h.TempDir(dirName) + h.TempFile(dirName+string(os.PathSeparator)+"src", "") + + tmpDirPath := tmpPath + string(os.PathSeparator) + dirName + string(os.PathSeparator) + + err := copyFile(tmpDirPath+"src", tmpDirPath+"dst") + if err != nil { + t.Fatalf("unexpected error while copying file: %v", err) + } +} + +// C:\Users\appveyor\AppData\Local\Temp\1\gotest639065787\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890\dir4567890 + +func TestCopyFileFail(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. Skipping this this until a + // compatible implementation is provided. + t.Skip("skipping on windows") + } + + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcf, err := os.Create(filepath.Join(dir, "srcfile")) + if err != nil { + t.Fatal(err) + } + srcf.Close() + + var dstdir string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + dstdir = filepath.Join(dir, "dir") + return os.Mkdir(dstdir, 0777) + }) + defer cleanup() + + fn := filepath.Join(dstdir, "file") + if err := copyFile(srcf.Name(), fn); err == nil { + t.Fatalf("expected error for %s, got none", fn) + } +} + +// setupInaccessibleDir creates a temporary location with a single +// directory in it, in such a way that that directory is not accessible +// after this function returns. +// +// op is called with the directory as argument, so that it can create +// files or other test artifacts. +// +// If setupInaccessibleDir fails in its preparation, or op fails, t.Fatal +// will be invoked. +// +// This function returns a cleanup function that removes all the temporary +// files this function creates. It is the caller's responsibility to call +// this function before the test is done running, whether there's an error or not. +func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + return nil // keep compiler happy + } + + subdir := filepath.Join(dir, "dir") + + cleanup := func() { + if err := os.Chmod(subdir, 0777); err != nil { + t.Error(err) + } + if err := os.RemoveAll(dir); err != nil { + t.Error(err) + } + } + + if err := os.Mkdir(subdir, 0777); err != nil { + cleanup() + t.Fatal(err) + return nil + } + + if err := op(subdir); err != nil { + cleanup() + t.Fatal(err) + return nil + } + + if err := os.Chmod(subdir, 0666); err != nil { + cleanup() + t.Fatal(err) + return nil + } + + return cleanup +} + +func TestEnsureDir(t *testing.T) { + h := test.NewHelper(t) + defer h.Cleanup() + h.TempDir(".") + h.TempFile("file", "") + + tmpPath := h.Path(".") + + var dn string + cleanup := setupInaccessibleDir(t, func(dir string) error { + dn = filepath.Join(dir, "dir") + return os.Mkdir(dn, 0777) + }) + defer cleanup() + + tests := map[string]bool{ + // [success] A dir already exists for the given path. + tmpPath: true, + // [success] Dir does not exist but parent dir exists, so should get created. + filepath.Join(tmpPath, "testdir"): true, + // [failure] Dir and parent dir do not exist, should return an error. + filepath.Join(tmpPath, "notexist", "testdir"): false, + // [failure] Regular file present at given path. + h.Path("file"): false, + // [failure] Path inaccessible. + dn: false, + } + + if runtime.GOOS == "windows" { + // This test doesn't work on Microsoft Windows because + // of the differences in how file permissions are + // implemented. For this to work, the directory where + // the directory exists should be inaccessible. + delete(tests, dn) + } + + for path, shouldEnsure := range tests { + err := EnsureDir(path, 0777) + if shouldEnsure { + if err != nil { + t.Fatalf("unexpected error %q for %q", err, path) + } else if ok, err := IsDir(path); !ok { + t.Fatalf("expected directory to be preset at %q", path) + t.Fatal(err) + } + } else if err == nil { + t.Fatalf("expected error for path %q, got none", path) + } + } +} + +func TestIsRegular(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + var fn string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + fn = filepath.Join(dir, "file") + fh, err := os.Create(fn) + if err != nil { + return err + } + + return fh.Close() + }) + defer cleanup() + + tests := map[string]struct { + exists bool + err bool + }{ + wd: {false, true}, + filepath.Join(wd, "testdata"): {false, true}, + filepath.Join(wd, "testdata", "test.file"): {true, false}, + filepath.Join(wd, "this_file_does_not_exist.thing"): {false, false}, + fn: {false, true}, + } + + if runtime.GOOS == "windows" { + // This test doesn't work on Microsoft Windows because + // of the differences in how file permissions are + // implemented. For this to work, the directory where + // the file exists should be inaccessible. + delete(tests, fn) + } + + for f, want := range tests { + got, err := IsRegular(f) + if err != nil { + if want.exists != got { + t.Fatalf("expected %t for %s, got %t", want.exists, f, got) + } + if !want.err { + t.Fatalf("expected no error, got %v", err) + } + } else { + if want.err { + t.Fatalf("expected error for %s, got none", f) + } + } + + if got != want.exists { + t.Fatalf("expected %t for %s, got %t", want, f, got) + } + } + +} + +func TestIsDir(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + var dn string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + dn = filepath.Join(dir, "dir") + return os.Mkdir(dn, 0777) + }) + defer cleanup() + + tests := map[string]struct { + exists bool + err bool + }{ + wd: {true, false}, + filepath.Join(wd, "testdata"): {true, false}, + filepath.Join(wd, "main.go"): {false, true}, + filepath.Join(wd, "this_file_does_not_exist.thing"): {false, true}, + dn: {false, true}, + } + + if runtime.GOOS == "windows" { + // This test doesn't work on Microsoft Windows because + // of the differences in how file permissions are + // implemented. For this to work, the directory where + // the directory exists should be inaccessible. + delete(tests, dn) + } + + for f, want := range tests { + got, err := IsDir(f) + if err != nil && !want.err { + t.Fatalf("expected no error, got %v", err) + } + + if got != want.exists { + t.Fatalf("expected %t for %s, got %t", want.exists, f, got) + } + } +} + +func TestIsNonEmptyDir(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + h := test.NewHelper(t) + defer h.Cleanup() + + h.TempDir("empty") + + testCases := []struct { + path string + empty bool + err bool + }{ + {wd, true, false}, + {"testdata", true, false}, + {filepath.Join(wd, "fs.go"), false, true}, + {filepath.Join(wd, "this_file_does_not_exist.thing"), false, false}, + {h.Path("empty"), false, false}, + } + + // This test case doesn't work on Microsoft Windows because of the + // differences in how file permissions are implemented. + if runtime.GOOS != "windows" { + var inaccessibleDir string + cleanup := setupInaccessibleDir(t, func(dir string) error { + inaccessibleDir = filepath.Join(dir, "empty") + return os.Mkdir(inaccessibleDir, 0777) + }) + defer cleanup() + + testCases = append(testCases, struct { + path string + empty bool + err bool + }{inaccessibleDir, false, true}) + } + + for _, want := range testCases { + got, err := IsNonEmptyDir(want.path) + if want.err && err == nil { + if got { + t.Fatalf("wanted false with error for %v, but got true", want.path) + } + t.Fatalf("wanted an error for %v, but it was nil", want.path) + } + + if got != want.empty { + t.Fatalf("wanted %t for %v, but got %t", want.empty, want.path, got) + } + } +} + +func TestIsSymlink(t *testing.T) { + dir, err := ioutil.TempDir("", "dep") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + dirPath := filepath.Join(dir, "directory") + if err = os.MkdirAll(dirPath, 0777); err != nil { + t.Fatal(err) + } + + filePath := filepath.Join(dir, "file") + f, err := os.Create(filePath) + if err != nil { + t.Fatal(err) + } + f.Close() + + dirSymlink := filepath.Join(dir, "dirSymlink") + fileSymlink := filepath.Join(dir, "fileSymlink") + + if err = os.Symlink(dirPath, dirSymlink); err != nil { + t.Fatal(err) + } + if err = os.Symlink(filePath, fileSymlink); err != nil { + t.Fatal(err) + } + + var ( + inaccessibleFile string + inaccessibleSymlink string + ) + + cleanup := setupInaccessibleDir(t, func(dir string) error { + inaccessibleFile = filepath.Join(dir, "file") + if fh, err := os.Create(inaccessibleFile); err != nil { + return err + } else if err = fh.Close(); err != nil { + return err + } + + inaccessibleSymlink = filepath.Join(dir, "symlink") + return os.Symlink(inaccessibleFile, inaccessibleSymlink) + }) + defer cleanup() + + tests := map[string]struct{ expected, err bool }{ + dirPath: {false, false}, + filePath: {false, false}, + dirSymlink: {true, false}, + fileSymlink: {true, false}, + inaccessibleFile: {false, true}, + inaccessibleSymlink: {false, true}, + } + + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in Windows. Skipping + // these cases until a compatible implementation is provided. + delete(tests, inaccessibleFile) + delete(tests, inaccessibleSymlink) + } + + for path, want := range tests { + got, err := IsSymlink(path) + if err != nil { + if !want.err { + t.Errorf("expected no error, got %v", err) + } + } + + if got != want.expected { + t.Errorf("expected %t for %s, got %t", want.expected, path, got) + } + } +} diff --git a/pkg/fsutil/rename.go b/pkg/fsutil/rename.go new file mode 100644 index 000000000..c48f69f1a --- /dev/null +++ b/pkg/fsutil/rename.go @@ -0,0 +1,31 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !windows + +package fs + +import ( + "os" + "syscall" + + "github.com/pkg/errors" +) + +// renameFallback attempts to determine the appropriate fallback to failed rename +// operation depending on the resulting error. +func renameFallback(err error, src, dst string) error { + // Rename may fail if src and dst are on different devices; fall back to + // copy if we detect that case. syscall.EXDEV is the common name for the + // cross device link error which has varying output text across different + // operating systems. + terr, ok := err.(*os.LinkError) + if !ok { + return err + } else if terr.Err != syscall.EXDEV { + return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst) + } + + return renameByCopy(src, dst) +} diff --git a/pkg/fsutil/rename_windows.go b/pkg/fsutil/rename_windows.go new file mode 100644 index 000000000..50829a5cd --- /dev/null +++ b/pkg/fsutil/rename_windows.go @@ -0,0 +1,42 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package fs + +import ( + "os" + "syscall" + + "github.com/pkg/errors" +) + +// renameFallback attempts to determine the appropriate fallback to failed rename +// operation depending on the resulting error. +func renameFallback(err error, src, dst string) error { + // Rename may fail if src and dst are on different devices; fall back to + // copy if we detect that case. syscall.EXDEV is the common name for the + // cross device link error which has varying output text across different + // operating systems. + terr, ok := err.(*os.LinkError) + if !ok { + return err + } + + if terr.Err != syscall.EXDEV { + // In windows it can drop down to an operating system call that + // returns an operating system error with a different number and + // message. Checking for that as a fall back. + noerr, ok := terr.Err.(syscall.Errno) + + // 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error. + // See https://msdn.microsoft.com/en-us/library/cc231199.aspx + if ok && noerr != 0x11 { + return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst) + } + } + + return renameByCopy(src, dst) +} diff --git a/pkg/fsutil/testdata/symlinks/file-symlink b/pkg/fsutil/testdata/symlinks/file-symlink new file mode 120000 index 000000000..4c52274de --- /dev/null +++ b/pkg/fsutil/testdata/symlinks/file-symlink @@ -0,0 +1 @@ +../test.file \ No newline at end of file diff --git a/pkg/fsutil/testdata/symlinks/invalid-symlink b/pkg/fsutil/testdata/symlinks/invalid-symlink new file mode 120000 index 000000000..0edf4f301 --- /dev/null +++ b/pkg/fsutil/testdata/symlinks/invalid-symlink @@ -0,0 +1 @@ +/non/existing/file \ No newline at end of file diff --git a/pkg/fsutil/testdata/symlinks/windows-file-symlink b/pkg/fsutil/testdata/symlinks/windows-file-symlink new file mode 120000 index 000000000..af1d6c8f5 --- /dev/null +++ b/pkg/fsutil/testdata/symlinks/windows-file-symlink @@ -0,0 +1 @@ +C:/Users/ibrahim/go/src/github.com/golang/dep/internal/fs/testdata/test.file \ No newline at end of file diff --git a/pkg/fsutil/testdata/test.file b/pkg/fsutil/testdata/test.file new file mode 100644 index 000000000..e69de29bb