Description
Go version
go version go1.22.4 windows/amd64
Output of go env
in your module/workspace:
set GO111MODULE=
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\Hakkin\AppData\Local\go-build
set GOENV=C:\Users\Hakkin\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=C:\Users\Hakkin\Desktop\Programs\Misc\Projects\Go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\Hakkin\Desktop\Programs\Misc\Projects\Go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.22.4
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=NUL
set GOWORK=
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\Hakkin\AppData\Local\Temp\go-build1491048773=/tmp/go-build -gno-record-gcc-switches
What did you do?
Opening this issue in response to #67834 (comment)
Trying to use filepath.Clean
on Windows device paths that are more than 1 level deep results in the trailing backslash being removed from the path.
Example code:
package main
import (
"log"
"os"
"path/filepath"
)
func main() {
// Example paths, theoretically all point to the same device/volume
devicePaths := []string{
`\\?\C:\`,
`\\?\Volume{00000000-0000-0000-0000-000000000000}\`,
`\\?\GLOBALROOT\Device\HarddiskVolume1\`,
}
for _, devicePath := range devicePaths {
cleanPath := filepath.Clean(devicePath)
hasBackslash := devicePath[len(cleanPath)-1] == os.PathSeparator
log.Printf("has backslash: %v \tcleaned path: %s", hasBackslash, cleanPath)
}
}
What did you see happen?
$ go run main.go
has backslash: true cleaned path: \\?\C:\
has backslash: true cleaned path: \\?\Volume{00000000-0000-0000-0000-000000000000}\
has backslash: false cleaned path: \\?\GLOBALROOT\Device\HarddiskVolume1
What did you expect to see?
The path \\?\GLOBALROOT\Device\HarddiskVolume1\
should not have the trailing backslash trimmed, since it is a root device path and is equivalent to the other two paths (in fact, internally, the first two paths are symbolic object links to the third path, so it is the true canonical device path). The first two forms in the example code are already handled by a special case in the Go code, but it only handles paths that are "top-level" directories.
Windows offers a variety of ways to access device paths like this, you can read more here and here.
Paths in the form of \\?\GLOBALROOT\Device\...
are returned by some Windows APIs, notably the Windows Shadow Copy API (device paths are returned in the form of \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy...
).
Because the Windows Object Namespace supports symbolic linking, there are many (infinite?) paths to access the same device, for example, the Volume{...}
path in the above example can also be accessed using the path \\?\GLOBALROOT\GLOBAL??\Volume{00000000-0000-0000-0000-000000000000}\
. GLOBALROOT
is a symlink to the root of the NT Object namespace, GLOBAL??
is a directory for the Win32 namespace, and then Volume{...}
is a symlink to \Device\HarddiskVolume...
.
I think properly resolving all paths of this kind would be difficult, but at the very least, adding the canonical \Device\...
paths to the special handling should probably be fairly straightforward.
Related issues #64028 #67834
and copying my comment from #67834 (comment) here:
filepath.Clean does actually have special handling for these paths, but only in specific circumstances. This mostly seems to be handled in
volumeNameLen
here:go/src/internal/filepathlite/path_windows.go
Lines 228 to 243 in 45967bb
then
Clean
uses this here:go/src/internal/filepathlite/path.go
Lines 66 to 75 in 45967bb
So filepath.Clean does maintain the trailing slash for paths like
\\.\C:\
, but doesn't for paths like\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\
, since it treatsGLOBALROOT
as the volume name instead of the full device path.