From 220d94037bb1672142e5638ac443e491af8d308b Mon Sep 17 00:00:00 2001 From: Ibrahim AshShohail Date: Sun, 30 Jul 2017 22:46:43 +0300 Subject: [PATCH] internal/fs: fix os.Chmod on Windows with long paths copy the same functions used in os to convert long paths on Windows to the extended-length form. Fixes #774 Signed-off-by: Ibrahim AshShohail --- internal/fs/fs.go | 137 +++++++++++++++++++++++++++++++++++++++++ internal/fs/fs_test.go | 29 +++++++++ 2 files changed, 166 insertions(+) diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 84f0346856..90d6f87908 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -318,6 +318,13 @@ func copyFile(src, dst string) (err error) { 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 @@ -400,3 +407,133 @@ func IsSymlink(path string) (bool, error) { 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/internal/fs/fs_test.go b/internal/fs/fs_test.go index 163b26e647..b4a0bd541e 100644 --- a/internal/fs/fs_test.go +++ b/internal/fs/fs_test.go @@ -547,6 +547,35 @@ func TestCopyFileSymlink(t *testing.T) { } } +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(".") + + // Create a directory with a long-enough path name to cause the bug in #774. + dirName := "" + for len(h.Path(dirName)) <= 300 { + dirName += "directory1" + } + + h.TempDir(dirName) + tmpDirPath := dirName + string(os.PathSeparator) + + h.TempFile(tmpDirPath+"src", "") + + 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