]> Cypherpunks repositories - gostls13.git/commitdiff
os: support Stat and LStat for CON device on Windows
authorqmuntal <quimmuntal@gmail.com>
Fri, 2 Feb 2024 15:30:54 +0000 (16:30 +0100)
committerQuim Muntal <quimmuntal@gmail.com>
Wed, 7 Feb 2024 20:53:05 +0000 (20:53 +0000)
\\.\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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Reviewed-by: David Chase <drchase@google.com>
src/os/stat_test.go
src/os/stat_windows.go

index 96019699aa7bb59c3618b3b5a1fcf6e8a699f598..e79f6a90c643bf5a5671c0d49dc7513f090aac51 100644 (file)
@@ -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)
+       }
+}
index 668255f74adf3f0b82b8f846fbd27fedb01d7b51..f7cf5275a530446a09ec0c2265744f3ffa899754 100644 (file)
@@ -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