Skip to content

Commit 231aa9d

Browse files
committed
os: use extended-length paths on Windows when possible
Windows has a limit of 260 characters on normal paths, but it's possible to use longer paths by using "extended-length paths" that begin with `\\?\`. This commit attempts to transparently convert an absolute path to an extended-length path, following the subtly different rules those paths require. It does not attempt to handle relative paths, which continue to be passed to the operating system unmodified. This adds a new test, TestLongPath, to the os package. This test makes sure that it is possible to write a path at least 400 characters long and runs on every platform. It also tests symlinks and hardlinks, though symlinks are not testable with our builder configuration. HasLink is moved to internal/testenv so it can be used by multiple tests. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx has Microsoft's documentation on extended-length paths. Fixes #3358. Fixes #10577. Fixes #17500. Change-Id: I4ff6bb2ef9c9a4468d383d98379f65cf9c448218 Reviewed-on: https://go-review.googlesource.com/32451 Run-TryBot: Quentin Smith <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 2058511 commit 231aa9d

File tree

11 files changed

+196
-26
lines changed

11 files changed

+196
-26
lines changed

src/internal/testenv/testenv.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,22 @@ func MustHaveSymlink(t *testing.T) {
153153
}
154154
}
155155

156+
// HasLink reports whether the current system can use os.Link.
157+
func HasLink() bool {
158+
// From Android release M (Marshmallow), hard linking files is blocked
159+
// and an attempt to call link() on a file will return EACCES.
160+
// - https://code.google.com/p/android-developer-preview/issues/detail?id=3150
161+
return runtime.GOOS != "plan9" && runtime.GOOS != "android"
162+
}
163+
164+
// MustHaveLink reports whether the current system can use os.Link.
165+
// If not, MustHaveLink calls t.Skip with an explanation.
166+
func MustHaveLink(t *testing.T) {
167+
if !HasLink() {
168+
t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
169+
}
170+
}
171+
156172
var flaky = flag.Bool("flaky", false, "run known-flaky tests too")
157173

158174
func SkipFlaky(t *testing.T, issue int) {

src/os/export_windows_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ var (
1111
GetCPP = &getCP
1212
ReadFileP = &readFile
1313
ResetGetConsoleCPAndReadFileFuncs = resetGetConsoleCPAndReadFileFuncs
14+
FixLongPath = fixLongPath
1415
)

src/os/file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func (f *File) WriteString(s string) (n int, err error) {
203203
// Mkdir creates a new directory with the specified name and permission bits.
204204
// If there is an error, it will be of type *PathError.
205205
func Mkdir(name string, perm FileMode) error {
206-
e := syscall.Mkdir(name, syscallMode(perm))
206+
e := syscall.Mkdir(fixLongPath(name), syscallMode(perm))
207207

208208
if e != nil {
209209
return &PathError{"mkdir", name, e}

src/os/file_plan9.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import (
1111
"time"
1212
)
1313

14+
// fixLongPath is a noop on non-Windows platforms.
15+
func fixLongPath(path string) string {
16+
return path
17+
}
18+
1419
// file is the real representation of *File.
1520
// The extra level of indirection ensures that no clients of os
1621
// can overwrite this data, which could cause the finalizer

src/os/file_posix.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func sigpipe() // implemented in package runtime
1818
func Readlink(name string) (string, error) {
1919
for len := 128; ; len *= 2 {
2020
b := make([]byte, len)
21-
n, e := fixCount(syscall.Readlink(name, b))
21+
n, e := fixCount(syscall.Readlink(fixLongPath(name), b))
2222
if e != nil {
2323
return "", &PathError{"readlink", name, e}
2424
}
@@ -134,7 +134,7 @@ func Chtimes(name string, atime time.Time, mtime time.Time) error {
134134
var utimes [2]syscall.Timespec
135135
utimes[0] = syscall.NsecToTimespec(atime.UnixNano())
136136
utimes[1] = syscall.NsecToTimespec(mtime.UnixNano())
137-
if e := syscall.UtimesNano(name, utimes[0:]); e != nil {
137+
if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
138138
return &PathError{"chtimes", name, e}
139139
}
140140
return nil

src/os/file_unix.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import (
1111
"syscall"
1212
)
1313

14+
// fixLongPath is a noop on non-Windows platforms.
15+
func fixLongPath(path string) string {
16+
return path
17+
}
18+
1419
func rename(oldname, newname string) error {
1520
fi, err := Lstat(newname)
1621
if err == nil && fi.IsDir() {

src/os/file_windows.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const DevNull = "NUL"
8686
func (f *file) isdir() bool { return f != nil && f.dirinfo != nil }
8787

8888
func openFile(name string, flag int, perm FileMode) (file *File, err error) {
89-
r, e := syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
89+
r, e := syscall.Open(fixLongPath(name), flag|syscall.O_CLOEXEC, syscallMode(perm))
9090
if e != nil {
9191
return nil, e
9292
}
@@ -95,10 +95,13 @@ func openFile(name string, flag int, perm FileMode) (file *File, err error) {
9595

9696
func openDir(name string) (file *File, err error) {
9797
var mask string
98-
if len(name) == 2 && name[1] == ':' { // it is a drive letter, like C:
99-
mask = name + `*`
98+
99+
path := fixLongPath(name)
100+
101+
if len(path) == 2 && path[1] == ':' || (len(path) > 0 && path[len(path)-1] == '\\') { // it is a drive letter, like C:
102+
mask = path + `*`
100103
} else {
101-
mask = name + `\*`
104+
mask = path + `\*`
102105
}
103106
maskp, e := syscall.UTF16PtrFromString(mask)
104107
if e != nil {
@@ -114,11 +117,11 @@ func openDir(name string) (file *File, err error) {
114117
return nil, e
115118
}
116119
var fa syscall.Win32FileAttributeData
117-
namep, e := syscall.UTF16PtrFromString(name)
120+
pathp, e := syscall.UTF16PtrFromString(path)
118121
if e != nil {
119122
return nil, e
120123
}
121-
e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
124+
e = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
122125
if e != nil {
123126
return nil, e
124127
}
@@ -127,7 +130,7 @@ func openDir(name string) (file *File, err error) {
127130
}
128131
d.isempty = true
129132
}
130-
d.path = name
133+
d.path = path
131134
if !isAbs(d.path) {
132135
d.path, e = syscall.FullPath(d.path)
133136
if e != nil {
@@ -439,7 +442,7 @@ func Truncate(name string, size int64) error {
439442
// Remove removes the named file or directory.
440443
// If there is an error, it will be of type *PathError.
441444
func Remove(name string) error {
442-
p, e := syscall.UTF16PtrFromString(name)
445+
p, e := syscall.UTF16PtrFromString(fixLongPath(name))
443446
if e != nil {
444447
return &PathError{"remove", name, e}
445448
}
@@ -476,7 +479,7 @@ func Remove(name string) error {
476479
}
477480

478481
func rename(oldname, newname string) error {
479-
e := windows.Rename(oldname, newname)
482+
e := windows.Rename(fixLongPath(oldname), fixLongPath(newname))
480483
if e != nil {
481484
return &LinkError{"rename", oldname, newname, e}
482485
}
@@ -521,11 +524,11 @@ func TempDir() string {
521524
// Link creates newname as a hard link to the oldname file.
522525
// If there is an error, it will be of type *LinkError.
523526
func Link(oldname, newname string) error {
524-
n, err := syscall.UTF16PtrFromString(newname)
527+
n, err := syscall.UTF16PtrFromString(fixLongPath(newname))
525528
if err != nil {
526529
return &LinkError{"link", oldname, newname, err}
527530
}
528-
o, err := syscall.UTF16PtrFromString(oldname)
531+
o, err := syscall.UTF16PtrFromString(fixLongPath(oldname))
529532
if err != nil {
530533
return &LinkError{"link", oldname, newname, err}
531534
}
@@ -556,11 +559,11 @@ func Symlink(oldname, newname string) error {
556559
fi, err := Lstat(destpath)
557560
isdir := err == nil && fi.IsDir()
558561

559-
n, err := syscall.UTF16PtrFromString(newname)
562+
n, err := syscall.UTF16PtrFromString(fixLongPath(newname))
560563
if err != nil {
561564
return &LinkError{"symlink", oldname, newname, err}
562565
}
563-
o, err := syscall.UTF16PtrFromString(oldname)
566+
o, err := syscall.UTF16PtrFromString(fixLongPath(oldname))
564567
if err != nil {
565568
return &LinkError{"symlink", oldname, newname, err}
566569
}

src/os/os_test.go

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -600,15 +600,8 @@ func TestReaddirOfFile(t *testing.T) {
600600
}
601601

602602
func TestHardLink(t *testing.T) {
603-
if runtime.GOOS == "plan9" {
604-
t.Skip("skipping on plan9, hardlinks not supported")
605-
}
606-
// From Android release M (Marshmallow), hard linking files is blocked
607-
// and an attempt to call link() on a file will return EACCES.
608-
// - https://code.google.com/p/android-developer-preview/issues/detail?id=3150
609-
if runtime.GOOS == "android" {
610-
t.Skip("skipping on android, hardlinks not supported")
611-
}
603+
testenv.MustHaveLink(t)
604+
612605
defer chtmpdir(t)()
613606
from, to := "hardlinktestfrom", "hardlinktestto"
614607
Remove(from) // Just in case.
@@ -1708,6 +1701,61 @@ func TestReadAtEOF(t *testing.T) {
17081701
}
17091702
}
17101703

1704+
func TestLongPath(t *testing.T) {
1705+
tmpdir := newDir("TestLongPath", t)
1706+
defer func() {
1707+
if err := RemoveAll(tmpdir); err != nil {
1708+
t.Fatalf("RemoveAll failed: %v", err)
1709+
}
1710+
}()
1711+
for len(tmpdir) < 400 {
1712+
tmpdir += "/dir3456789"
1713+
}
1714+
if err := MkdirAll(tmpdir, 0755); err != nil {
1715+
t.Fatalf("MkdirAll failed: %v", err)
1716+
}
1717+
data := []byte("hello world\n")
1718+
if err := ioutil.WriteFile(tmpdir+"/foo.txt", data, 0644); err != nil {
1719+
t.Fatalf("ioutil.WriteFile() failed: %v", err)
1720+
}
1721+
if err := Rename(tmpdir+"/foo.txt", tmpdir+"/bar.txt"); err != nil {
1722+
t.Fatalf("Rename failed: %v", err)
1723+
}
1724+
mtime := time.Now().Truncate(time.Minute)
1725+
if err := Chtimes(tmpdir+"/bar.txt", mtime, mtime); err != nil {
1726+
t.Fatalf("Chtimes failed: %v", err)
1727+
}
1728+
names := []string{"bar.txt"}
1729+
if testenv.HasSymlink() {
1730+
if err := Symlink(tmpdir+"/bar.txt", tmpdir+"/symlink.txt"); err != nil {
1731+
t.Fatalf("Symlink failed: %v", err)
1732+
}
1733+
names = append(names, "symlink.txt")
1734+
}
1735+
if testenv.HasLink() {
1736+
if err := Link(tmpdir+"/bar.txt", tmpdir+"/link.txt"); err != nil {
1737+
t.Fatalf("Link failed: %v", err)
1738+
}
1739+
names = append(names, "link.txt")
1740+
}
1741+
for _, wantSize := range []int64{int64(len(data)), 0} {
1742+
for _, name := range names {
1743+
path := tmpdir + "/" + name
1744+
dir, err := Stat(path)
1745+
if err != nil {
1746+
t.Fatalf("Stat(%q) failed: %v", path, err)
1747+
}
1748+
filesize := size(path, t)
1749+
if dir.Size() != filesize || filesize != wantSize {
1750+
t.Errorf("Size(%q) is %d, len(ReadFile()) is %d, want %d", path, dir.Size(), filesize, wantSize)
1751+
}
1752+
}
1753+
if err := Truncate(tmpdir+"/bar.txt", 0); err != nil {
1754+
t.Fatalf("Truncate failed: %v")
1755+
}
1756+
}
1757+
}
1758+
17111759
func testKillProcess(t *testing.T, processKiller func(p *Process)) {
17121760
testenv.MustHaveExec(t)
17131761

src/os/path_windows.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,66 @@ func dirname(path string) string {
127127
}
128128
return vol + dir
129129
}
130+
131+
// fixLongPath returns the extended-length (\\?\-prefixed) form of
132+
// path if possible, in order to avoid the default 260 character file
133+
// path limit imposed by Windows. If path is not easily converted to
134+
// the extended-length form (for example, if path is a relative path
135+
// or contains .. elements), fixLongPath returns path unmodified.
136+
func fixLongPath(path string) string {
137+
// The extended form begins with \\?\, as in
138+
// \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt.
139+
// The extended form disables evaluation of . and .. path
140+
// elements and disables the interpretation of / as equivalent
141+
// to \. The conversion here rewrites / to \ and elides
142+
// . elements as well as trailing or duplicate separators. For
143+
// simplicity it avoids the conversion entirely for relative
144+
// paths or paths containing .. elements. For now,
145+
// \\server\share paths are not converted to
146+
// \\?\UNC\server\share paths because the rules for doing so
147+
// are less well-specified.
148+
//
149+
// For details of \\?\ paths, see:
150+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
151+
if len(path) == 0 || (len(path) >= 2 && path[:2] == `\\`) {
152+
// Don't canonicalize UNC paths.
153+
return path
154+
}
155+
if !isAbs(path) {
156+
// Relative path
157+
return path
158+
}
159+
160+
const prefix = `\\?`
161+
162+
pathbuf := make([]byte, len(prefix)+len(path)+len(`\`))
163+
copy(pathbuf, prefix)
164+
n := len(path)
165+
r, w := 0, len(prefix)
166+
for r < n {
167+
switch {
168+
case IsPathSeparator(path[r]):
169+
// empty block
170+
r++
171+
case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])):
172+
// /./
173+
r++
174+
case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])):
175+
// /../ is currently unhandled
176+
return path
177+
default:
178+
pathbuf[w] = '\\'
179+
w++
180+
for ; r < n && !IsPathSeparator(path[r]); r++ {
181+
pathbuf[w] = path[r]
182+
w++
183+
}
184+
}
185+
}
186+
// A drive's root directory needs a trailing \
187+
if w == len(`\\?\c:`) {
188+
pathbuf[w] = '\\'
189+
w++
190+
}
191+
return string(pathbuf[:w])
192+
}

src/os/path_windows_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package os_test
6+
7+
import (
8+
"os"
9+
"testing"
10+
)
11+
12+
func TestFixLongPath(t *testing.T) {
13+
for _, test := range []struct{ in, want string }{
14+
{`C:\foo.txt`, `\\?\C:\foo.txt`},
15+
{`C:/foo.txt`, `\\?\C:\foo.txt`},
16+
{`C:\foo\\bar\.\baz\\`, `\\?\C:\foo\bar\baz`},
17+
{`C:\`, `\\?\C:\`}, // drives must have a trailing slash
18+
{`\\unc\path`, `\\unc\path`},
19+
{`foo.txt`, `foo.txt`},
20+
{`C:foo.txt`, `C:foo.txt`},
21+
{`c:\foo\..\bar\baz`, `c:\foo\..\bar\baz`},
22+
{`\\?\c:\windows\foo.txt`, `\\?\c:\windows\foo.txt`},
23+
{`\\?\c:\windows/foo.txt`, `\\?\c:\windows/foo.txt`},
24+
} {
25+
if got := os.FixLongPath(test.in); got != test.want {
26+
t.Errorf("fixLongPath(%q) = %q; want %q", test.in, got, test.want)
27+
}
28+
}
29+
}

src/os/stat_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func Lstat(name string) (FileInfo, error) {
9090
return &devNullStat, nil
9191
}
9292
fs := &fileStat{name: basename(name)}
93-
namep, e := syscall.UTF16PtrFromString(name)
93+
namep, e := syscall.UTF16PtrFromString(fixLongPath(name))
9494
if e != nil {
9595
return nil, &PathError{"Lstat", name, e}
9696
}

0 commit comments

Comments
 (0)