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

Commit 40956b4

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 <[email protected]>
1 parent 310c2c8 commit 40956b4

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

internal/fs/fs.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io/ioutil"
1010
"os"
1111
"path/filepath"
12+
"runtime"
1213
"strings"
1314
"unicode"
1415

@@ -306,6 +307,13 @@ func copyFile(src, dst string) (err error) {
306307
if err != nil {
307308
return
308309
}
310+
311+
// Temporary fix for Go < 1.9
312+
//
313+
// See: https://github.com/golang/dep/issues/774 & https://github.com/golang/go/issues/20829
314+
if runtime.GOOS == "windows" {
315+
dst = fixLongPath(dst)
316+
}
309317
err = os.Chmod(dst, si.Mode())
310318
if err != nil {
311319
return
@@ -396,3 +404,133 @@ func IsSymlink(path string) (bool, error) {
396404

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

internal/fs/fs_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,33 @@ func TestCopyFileSymlinkToDirectory(t *testing.T) {
567567
}
568568
}
569569

570+
func TestCopyFile_LongFilePath(t *testing.T) {
571+
if runtime.GOOS != "windows" {
572+
// We want to ensure the temporary fix actually fixes the issue with
573+
// os.Chmod and long file paths. This is only applicable on Windows.
574+
t.Skip("skipping on non-windows")
575+
}
576+
577+
h := test.NewHelper(t)
578+
h.TempDir(".")
579+
580+
baseDir := h.Path(".")
581+
dir := ""
582+
583+
for len(baseDir)+len(dir) <= 300 {
584+
dir += string(os.PathSeparator) + "dir456789"
585+
}
586+
h.TempDir(dir)
587+
588+
tmpPath := h.Path(dir) + string(os.PathSeparator)
589+
590+
h.TempFile(tmpPath+"src", "")
591+
592+
if err := copyFile(tmpPath+"src", tmpPath+"dst"); err != nil {
593+
t.Fatalf("unexpected error while copying file: %v", err)
594+
}
595+
}
596+
570597
func TestCopyFileFail(t *testing.T) {
571598
if runtime.GOOS == "windows" {
572599
// XXX: setting permissions works differently in

0 commit comments

Comments
 (0)