From: qmuntal Date: Fri, 2 Feb 2024 15:30:54 +0000 (+0100) Subject: os: support Stat and LStat for CON device on Windows X-Git-Tag: go1.23rc1~1288 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=28b8851671a0254ed0e46ce8dbec43ebe73e7132;p=gostls13.git os: support Stat and LStat for CON device on Windows \\.\con and CON need to be opened with GENERIC_READ access, else CreateFile will fail with ERROR_INVALID_PARAMETER. Special-case ERROR_INVALID_PARAMETER in os.[L]Stat so it retries with GENERIC_READ access. Fixes #34900. Change-Id: I5010e736d0189c8ada4fc0eca98d71a438c41426 Reviewed-on: https://go-review.googlesource.com/c/go/+/560755 LUCI-TryBot-Result: Go LUCI Reviewed-by: Bryan Mills Reviewed-by: David Chase --- diff --git a/src/os/stat_test.go b/src/os/stat_test.go index 96019699aa..e79f6a90c6 100644 --- a/src/os/stat_test.go +++ b/src/os/stat_test.go @@ -9,18 +9,25 @@ import ( "io/fs" "os" "path/filepath" + "runtime" "testing" ) +type testStatAndLstatParams struct { + isLink bool + statCheck func(*testing.T, string, fs.FileInfo) + lstatCheck func(*testing.T, string, fs.FileInfo) +} + // testStatAndLstat verifies that all os.Stat, os.Lstat os.File.Stat and os.Readdir work. -func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCheck func(*testing.T, string, fs.FileInfo)) { +func testStatAndLstat(t *testing.T, path string, params testStatAndLstatParams) { // test os.Stat sfi, err := os.Stat(path) if err != nil { t.Error(err) return } - statCheck(t, path, sfi) + params.statCheck(t, path, sfi) // test os.Lstat lsfi, err := os.Lstat(path) @@ -28,9 +35,9 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh t.Error(err) return } - lstatCheck(t, path, lsfi) + params.lstatCheck(t, path, lsfi) - if isLink { + if params.isLink { if os.SameFile(sfi, lsfi) { t.Errorf("stat and lstat of %q should not be the same", path) } @@ -53,13 +60,13 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh t.Error(err) return } - statCheck(t, path, sfi2) + params.statCheck(t, path, sfi2) if !os.SameFile(sfi, sfi2) { t.Errorf("stat of open %q file and stat of %q should be the same", path, path) } - if isLink { + if params.isLink { if os.SameFile(sfi2, lsfi) { t.Errorf("stat of opened %q file and lstat of %q should not be the same", path, path) } @@ -69,12 +76,13 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh } } - // test fs.FileInfo returned by os.Readdir - if len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) { - // skip os.Readdir test of directories with slash at the end + parentdir, base := filepath.Split(path) + if parentdir == "" || base == "" { + // skip os.Readdir test of files without directory or file name component, + // such as directories with slash at the end or Windows device names. return } - parentdir := filepath.Dir(path) + parent, err := os.Open(parentdir) if err != nil { t.Error(err) @@ -88,7 +96,6 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh return } var lsfi2 fs.FileInfo - base := filepath.Base(path) for _, fi2 := range fis { if fi2.Name() == base { lsfi2 = fi2 @@ -99,7 +106,7 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh t.Errorf("failed to find %q in its parent", path) return } - lstatCheck(t, path, lsfi2) + params.lstatCheck(t, path, lsfi2) if !os.SameFile(lsfi, lsfi2) { t.Errorf("lstat of %q file in %q directory and %q should be the same", lsfi2.Name(), parentdir, path) @@ -140,19 +147,34 @@ func testIsFile(t *testing.T, path string, fi fs.FileInfo) { } func testDirStats(t *testing.T, path string) { - testStatAndLstat(t, path, false, testIsDir, testIsDir) + params := testStatAndLstatParams{ + isLink: false, + statCheck: testIsDir, + lstatCheck: testIsDir, + } + testStatAndLstat(t, path, params) } func testFileStats(t *testing.T, path string) { - testStatAndLstat(t, path, false, testIsFile, testIsFile) + params := testStatAndLstatParams{ + isLink: false, + statCheck: testIsFile, + lstatCheck: testIsFile, + } + testStatAndLstat(t, path, params) } func testSymlinkStats(t *testing.T, path string, isdir bool) { + params := testStatAndLstatParams{ + isLink: true, + lstatCheck: testIsSymlink, + } if isdir { - testStatAndLstat(t, path, true, testIsDir, testIsSymlink) + params.statCheck = testIsDir } else { - testStatAndLstat(t, path, true, testIsFile, testIsSymlink) + params.statCheck = testIsFile } + testStatAndLstat(t, path, params) } func testSymlinkSameFile(t *testing.T, path, link string) { @@ -294,3 +316,24 @@ func TestSymlinkWithTrailingSlash(t *testing.T) { t.Errorf("os.Stat(%q) and os.Stat(%q) are not the same file", dir, dirlinkWithSlash) } } + +func TestStatConsole(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("skipping on non-Windows") + } + t.Parallel() + consoleNames := []string{ + "CONIN$", + "CONOUT$", + "CON", + } + for _, name := range consoleNames { + params := testStatAndLstatParams{ + isLink: false, + statCheck: testIsFile, + lstatCheck: testIsFile, + } + testStatAndLstat(t, name, params) + testStatAndLstat(t, `\\.\`+name, params) + } +} diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go index 668255f74a..f7cf5275a5 100644 --- a/src/os/stat_windows.go +++ b/src/os/stat_windows.go @@ -74,7 +74,16 @@ func stat(funcname, name string, followSurrogates bool) (FileInfo, error) { // save information about the link target. // Set FILE_FLAG_BACKUP_SEMANTICS so that CreateFile will create the handle // even if name refers to a directory. - h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT, 0) + var flags uint32 = syscall.FILE_FLAG_BACKUP_SEMANTICS | syscall.FILE_FLAG_OPEN_REPARSE_POINT + h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, flags, 0) + + if err == windows.ERROR_INVALID_PARAMETER { + // Console handles, like "\\.\con", require generic read access. See + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#consoles. + // We haven't set it previously because it is normally not required + // to read attributes and some files may not allow it. + h, err = syscall.CreateFile(namep, syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING, flags, 0) + } if err != nil { // Since CreateFile failed, we can't determine whether name refers to a // name surrogate, or some other kind of reparse point. Since we can't return a