Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

path/filepath: Clean strips trailing slash on some Windows device paths #67880

Open
Hakkin opened this issue Jun 7, 2024 · 2 comments
Open
Labels
NeedsFix The path to resolution is known, but the work has not been done. OS-Windows

Comments

@Hakkin
Copy link

Hakkin commented Jun 7, 2024

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:

case pathHasPrefixFold(path, `\\.`) ||
pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
// Path starts with \\.\, and is a Local Device path; or
// path starts with \\?\ or \??\ and is a Root Local Device path.
//
// We treat the next component after the \\.\ prefix as
// part of the volume name, which means Clean(`\\?\c:\`)
// won't remove the trailing \. (See #64028.)
if len(path) == 3 {
return 3 // exactly \\.
}
_, rest, ok := cutPath(path[4:])
if !ok {
return len(path)
}
return len(path) - len(rest) - 1

then Clean uses this here:

originalPath := path
volLen := volumeNameLen(path)
path = path[volLen:]
if path == "" {
if volLen > 1 && IsPathSeparator(originalPath[0]) && IsPathSeparator(originalPath[1]) {
// should be UNC
return FromSlash(originalPath)
}
return originalPath + "."
}

So filepath.Clean does maintain the trailing slash for paths like \\.\C:\, but doesn't for paths like \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\, since it treats GLOBALROOT as the volume name instead of the full device path.

@qmuntal
Copy link
Contributor

qmuntal commented Jun 7, 2024

@golang/windows

@qmuntal qmuntal added NeedsFix The path to resolution is known, but the work has not been done. OS-Windows labels Jun 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsFix The path to resolution is known, but the work has not been done. OS-Windows
Projects
None yet
Development

No branches or pull requests

3 participants