Skip to content

Commit a662740

Browse files
committed
tools: add a function to properly canonicalize paths
Git consistently uses canonicalized paths internally. This is for many reasons, but mostly to verify that a single path is within a repository. In order to interoperate properly with Git, we need to canonicalize paths and do it in the same way as Git. On Unix systems, to canonicalize a path, it is sufficient to make the path absolute and then resolve any symlinks. Go provides two functions to do these two steps, filepath.Abs and filepath.EvalSymlinks, and they work as advertised. Windows, however, has much more complex path handling and these functions do not handle all cases. The typical way to canonicalize paths on Windows is using GetFinalPathNameByHandle, and this is the technique Git uses. Go, however, does not provide a general API to canonicalize paths, unlike Rust's std::fs::canonicalize and similar functionality in myriad other languages. Therefore, in order to get this working on Windows, let's add a function to canonicalize paths in the appropriate system way, one for Unix systems and one for Windows. The code comes from Go's standard library, so update the copyright and license accordingly. Update the CanonicalizePath function to use the new function. We duplicate the Abs call because we an absolute path in CanonicalizePath in some cases even if the path is missing, whereas the new function needs to do it in all cases because we will use it other situations in the future. This should be a simple string check, so it should not involve an extra system call or other overhead.
1 parent 39dcf53 commit a662740

File tree

5 files changed

+70
-3
lines changed

5 files changed

+70
-3
lines changed

LICENSE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
SOFTWARE.
2222

23-
Portions of the subprocess directory are copied from Go and are under the
24-
following license:
23+
Portions of the subprocess and tools directories are copied from Go and are
24+
under the following license:
2525

2626
Copyright (c) 2010 The Go Authors. All rights reserved.
2727

debian/copyright

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Files: vendor/github.com/alexbrainman/sspi/*
3131
subprocess/path.go
3232
subprocess/path_nix.go
3333
subprocess/path_windows.go
34+
tools/filetools_windows.go
3435
Copyright: 2009, 2010, 2012, 2017 The Go Authors
3536
License: Go
3637
Redistribution and use in source and binary forms, with or without

tools/filetools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ func CanonicalizePath(path string, missingOk bool) (string, error) {
494494
if err != nil {
495495
return "", err
496496
}
497-
result, err := filepath.EvalSymlinks(path)
497+
result, err := CanonicalizeSystemPath(path)
498498
if err != nil && os.IsNotExist(err) && missingOk {
499499
return path, nil
500500
}

tools/filetools_nix.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// +build !windows
2+
3+
package tools
4+
5+
import "path/filepath"
6+
7+
func CanonicalizeSystemPath(path string) (string, error) {
8+
path, err := filepath.Abs(path)
9+
if err != nil {
10+
return "", err
11+
}
12+
return filepath.EvalSymlinks(path)
13+
}

tools/filetools_windows.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// +build windows
2+
3+
package tools
4+
5+
import (
6+
"golang.org/x/sys/windows"
7+
)
8+
9+
func openSymlink(path string) (windows.Handle, error) {
10+
p, err := windows.UTF16PtrFromString(path)
11+
if err != nil {
12+
return 0, err
13+
}
14+
15+
attrs := uint32(windows.FILE_FLAG_BACKUP_SEMANTICS)
16+
h, err := windows.CreateFile(p, 0, 0, nil, windows.OPEN_EXISTING, attrs, 0)
17+
if err != nil {
18+
return 0, err
19+
}
20+
21+
return h, nil
22+
}
23+
24+
func CanonicalizeSystemPath(path string) (string, error) {
25+
h, err := openSymlink(path)
26+
if err != nil {
27+
return "", err
28+
}
29+
defer windows.CloseHandle(h)
30+
31+
buf := make([]uint16, 100)
32+
for {
33+
n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), 0)
34+
if err != nil {
35+
return "", err
36+
}
37+
if n < uint32(len(buf)) {
38+
break
39+
}
40+
buf = make([]uint16, n)
41+
}
42+
43+
s := windows.UTF16ToString(buf)
44+
if len(s) > 4 && s[:4] == `\\?\` {
45+
s = s[4:]
46+
if len(s) > 3 && s[:3] == `UNC` {
47+
// return path like \\server\share\...
48+
return `\` + s[3:], nil
49+
}
50+
return s, nil
51+
}
52+
return s, nil
53+
}

0 commit comments

Comments
 (0)