golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.19k stars 17.46k forks source link

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

Open Hakkin opened 2 months ago

Hakkin commented 2 months ago

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 https://github.com/golang/go/issues/67834#issuecomment-2154302340

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 https://github.com/golang/go/issues/64028 https://github.com/golang/go/issues/67834 and copying my comment from https://github.com/golang/go/issues/67834#issuecomment-2151928869 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:

https://github.com/golang/go/blob/45967bb18e04fa6dc62c2786c87ce120443c64f6/src/internal/filepathlite/path_windows.go#L228-L243

then Clean uses this here:

https://github.com/golang/go/blob/45967bb18e04fa6dc62c2786c87ce120443c64f6/src/internal/filepathlite/path.go#L66-L75

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 commented 2 months ago

@golang/windows

gabyhelp commented 2 months ago

Similar Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)