Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit 496e2a0

Browse files
committed
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 <ibra.sho@gmail.com>
1 parent 44a454b commit 496e2a0

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

internal/fs/fs.go

+137
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ func copyFile(src, dst string) (err error) {
318318
return
319319
}
320320

321+
// Temporary fix for Go < 1.9
322+
//
323+
// See: https://github.com/golang/dep/issues/774
324+
// and https://github.com/golang/go/issues/20829
325+
if runtime.GOOS == "windows" {
326+
dst = fixLongPath(dst)
327+
}
321328
err = os.Chmod(dst, si.Mode())
322329

323330
return
@@ -400,3 +407,133 @@ func IsSymlink(path string) (bool, error) {
400407

401408
return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil
402409
}
410+
411+
// fixLongPath returns the extended-length (\\?\-prefixed) form of
412+
// path when needed, in order to avoid the default 260 character file
413+
// path limit imposed by Windows. If path is not easily converted to
414+
// the extended-length form (for example, if path is a relative path
415+
// or contains .. elements), or is short enough, fixLongPath returns
416+
// path unmodified.
417+
//
418+
// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
419+
func fixLongPath(path string) string {
420+
// Do nothing (and don't allocate) if the path is "short".
421+
// Empirically (at least on the Windows Server 2013 builder),
422+
// the kernel is arbitrarily okay with < 248 bytes. That
423+
// matches what the docs above say:
424+
// "When using an API to create a directory, the specified
425+
// path cannot be so long that you cannot append an 8.3 file
426+
// name (that is, the directory name cannot exceed MAX_PATH
427+
// minus 12)." Since MAX_PATH is 260, 260 - 12 = 248.
428+
//
429+
// The MSDN docs appear to say that a normal path that is 248 bytes long
430+
// will work; empirically the path must be less then 248 bytes long.
431+
if len(path) < 248 {
432+
// Don't fix. (This is how Go 1.7 and earlier worked,
433+
// not automatically generating the \\?\ form)
434+
return path
435+
}
436+
437+
// The extended form begins with \\?\, as in
438+
// \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt.
439+
// The extended form disables evaluation of . and .. path
440+
// elements and disables the interpretation of / as equivalent
441+
// to \. The conversion here rewrites / to \ and elides
442+
// . elements as well as trailing or duplicate separators. For
443+
// simplicity it avoids the conversion entirely for relative
444+
// paths or paths containing .. elements. For now,
445+
// \\server\share paths are not converted to
446+
// \\?\UNC\server\share paths because the rules for doing so
447+
// are less well-specified.
448+
if len(path) >= 2 && path[:2] == `\\` {
449+
// Don't canonicalize UNC paths.
450+
return path
451+
}
452+
if !isAbs(path) {
453+
// Relative path
454+
return path
455+
}
456+
457+
const prefix = `\\?`
458+
459+
pathbuf := make([]byte, len(prefix)+len(path)+len(`\`))
460+
copy(pathbuf, prefix)
461+
n := len(path)
462+
r, w := 0, len(prefix)
463+
for r < n {
464+
switch {
465+
case os.IsPathSeparator(path[r]):
466+
// empty block
467+
r++
468+
case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
469+
// /./
470+
r++
471+
case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
472+
// /../ is currently unhandled
473+
return path
474+
default:
475+
pathbuf[w] = '\\'
476+
w++
477+
for ; r < n && !os.IsPathSeparator(path[r]); r++ {
478+
pathbuf[w] = path[r]
479+
w++
480+
}
481+
}
482+
}
483+
// A drive's root directory needs a trailing \
484+
if w == len(`\\?\c:`) {
485+
pathbuf[w] = '\\'
486+
w++
487+
}
488+
return string(pathbuf[:w])
489+
}
490+
491+
func isAbs(path string) (b bool) {
492+
v := volumeName(path)
493+
if v == "" {
494+
return false
495+
}
496+
path = path[len(v):]
497+
if path == "" {
498+
return false
499+
}
500+
return os.IsPathSeparator(path[0])
501+
}
502+
503+
func volumeName(path string) (v string) {
504+
if len(path) < 2 {
505+
return ""
506+
}
507+
// with drive letter
508+
c := path[0]
509+
if path[1] == ':' &&
510+
('0' <= c && c <= '9' || 'a' <= c && c <= 'z' ||
511+
'A' <= c && c <= 'Z') {
512+
return path[:2]
513+
}
514+
// is it UNC
515+
if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) &&
516+
!os.IsPathSeparator(path[2]) && path[2] != '.' {
517+
// first, leading `\\` and next shouldn't be `\`. its server name.
518+
for n := 3; n < l-1; n++ {
519+
// second, next '\' shouldn't be repeated.
520+
if os.IsPathSeparator(path[n]) {
521+
n++
522+
// third, following something characters. its share name.
523+
if !os.IsPathSeparator(path[n]) {
524+
if path[n] == '.' {
525+
break
526+
}
527+
for ; n < l; n++ {
528+
if os.IsPathSeparator(path[n]) {
529+
break
530+
}
531+
}
532+
return path[:n]
533+
}
534+
break
535+
}
536+
}
537+
}
538+
return ""
539+
}

internal/fs/fs_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,37 @@ func TestCopyFileSymlink(t *testing.T) {
547547
}
548548
}
549549

550+
func TestCopyFileLongFilePath(t *testing.T) {
551+
if runtime.GOOS != "windows" {
552+
// We want to ensure the temporary fix actually fixes the issue with
553+
// os.Chmod and long file paths. This is only applicable on Windows.
554+
t.Skip("skipping on non-windows")
555+
}
556+
557+
h := test.NewHelper(t)
558+
h.TempDir(".")
559+
560+
tmpPath := h.Path(".")
561+
562+
// Create a directory with a long-enough path name to cause the bug in #774.
563+
dirName := ""
564+
for len(tmpPath+string(os.PathSeparator)+dirName) <= 300 {
565+
dirName += "directory"
566+
}
567+
568+
h.TempDir(dirName)
569+
h.TempFile(dirName+string(os.PathSeparator)+"src", "")
570+
571+
tmpDirPath := tmpPath + string(os.PathSeparator) + dirName + string(os.PathSeparator)
572+
573+
err := copyFile(tmpDirPath+"src", tmpDirPath+"dst")
574+
if err != nil {
575+
t.Fatalf("unexpected error while copying file: %v", err)
576+
}
577+
}
578+
579+
// 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
580+
550581
func TestCopyFileFail(t *testing.T) {
551582
if runtime.GOOS == "windows" {
552583
// XXX: setting permissions works differently in

0 commit comments

Comments
 (0)