diff --git a/test/winfsp/deleteonclose.go b/test/winfsp/deleteonclose.go new file mode 100644 index 000000000..967d733e6 --- /dev/null +++ b/test/winfsp/deleteonclose.go @@ -0,0 +1,7 @@ +package winfsp + +import "errors" + +// errWindowsOnly marks the platforms with no way to ask for delete-on-close, +// so the test can tell that apart from the flag being refused. +var errWindowsOnly = errors.New("delete-on-close is a windows flag") diff --git a/test/winfsp/deleteonclose_other.go b/test/winfsp/deleteonclose_other.go new file mode 100644 index 000000000..0d328e7cb --- /dev/null +++ b/test/winfsp/deleteonclose_other.go @@ -0,0 +1,9 @@ +//go:build !windows + +package winfsp + +type handle uintptr + +func createDeleteOnClose(path string) (handle, error) { return 0, errWindowsOnly } + +func closeHandle(h handle) error { return errWindowsOnly } diff --git a/test/winfsp/deleteonclose_windows.go b/test/winfsp/deleteonclose_windows.go new file mode 100644 index 000000000..dc93a971a --- /dev/null +++ b/test/winfsp/deleteonclose_windows.go @@ -0,0 +1,24 @@ +package winfsp + +import "golang.org/x/sys/windows" + +// createDeleteOnClose opens a new file that Windows removes when the last +// handle to it closes. Installers and editors use this for temporaries, and +// os has no way to ask for it. +func createDeleteOnClose(path string) (windows.Handle, error) { + p, err := windows.UTF16PtrFromString(path) + if err != nil { + return windows.InvalidHandle, err + } + return windows.CreateFile( + p, + windows.GENERIC_READ|windows.GENERIC_WRITE, + windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, + nil, + windows.CREATE_NEW, + windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_DELETE_ON_CLOSE, + 0, + ) +} + +func closeHandle(h windows.Handle) error { return windows.CloseHandle(h) } diff --git a/test/winfsp/semantics_test.go b/test/winfsp/semantics_test.go index 67c36b8b7..7620ca0ba 100644 --- a/test/winfsp/semantics_test.go +++ b/test/winfsp/semantics_test.go @@ -2,6 +2,7 @@ package winfsp import ( "bytes" + "errors" "fmt" "os" "path/filepath" @@ -461,3 +462,41 @@ func TestStatfsIsSelfConsistent(t *testing.T) { t.Fatalf("total free (%d) exceeds total (%d)", totalFree, total) } } + +// TestDeleteOnClose covers the pattern Windows software uses for temporaries: +// the file goes away when the last handle closes, with no explicit delete. A +// filesystem that ignores the flag leaves the name behind, and the next +// program to create it gets a collision. +func TestDeleteOnClose(t *testing.T) { + dir := testRoot(t) + path := filepath.Join(dir, "ephemeral.tmp") + + h, err := createDeleteOnClose(path) + if err != nil { + if errors.Is(err, errWindowsOnly) { + t.Skip("delete-on-close needs the Win32 create call") + } + t.Fatalf("open with delete-on-close: %v", err) + } + if _, err := os.Stat(path); err != nil { + closeHandle(h) + t.Fatalf("file is not visible while its handle is open: %v", err) + } + if err := closeHandle(h); err != nil { + t.Fatalf("close: %v", err) + } + if _, err := os.Stat(path); err == nil { + t.Fatal("file outlived its last handle") + } else if !os.IsNotExist(err) { + t.Fatalf("stat after close: %v", err) + } + // The name has to be free again, which is what the conformance suite + // tripped over when an aborted test left its file behind. + h, err = createDeleteOnClose(path) + if err != nil { + t.Fatalf("recreating the same name failed: %v", err) + } + if err := closeHandle(h); err != nil { + t.Fatalf("close after recreate: %v", err) + } +}