From ef753e60d269623cd564ba1951e5f52dc1ba439d Mon Sep 17 00:00:00 2001 From: "Paul \"TBBle\" Hampson" Date: Sun, 13 Dec 2020 01:36:51 +1100 Subject: [PATCH] Implement winio.GetFileStandardInfo Signed-off-by: Paul "TBBle" Hampson --- fileinfo.go | 19 +++++++ fileinfo_test.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 fileinfo_test.go diff --git a/fileinfo.go b/fileinfo.go index 980dfbc..3ab6bff 100644 --- a/fileinfo.go +++ b/fileinfo.go @@ -36,6 +36,25 @@ func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error { return nil } +// FileStandardInfo contains extended information for the file. +// FILE_STANDARD_INFO in WinBase.h +// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info +type FileStandardInfo struct { + AllocationSize, EndOfFile int64 + NumberOfLinks uint32 + DeletePending, Directory bool +} + +// GetFileStandardInfo retrieves ended information for the file. +func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) { + si := &FileStandardInfo{} + if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), windows.FileStandardInfo, (*byte)(unsafe.Pointer(si)), uint32(unsafe.Sizeof(*si))); err != nil { + return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} + } + runtime.KeepAlive(f) + return si, nil +} + // FileIDInfo contains the volume serial number and file ID for a file. This pair should be // unique on a system. type FileIDInfo struct { diff --git a/fileinfo_test.go b/fileinfo_test.go new file mode 100644 index 0000000..e79700b --- /dev/null +++ b/fileinfo_test.go @@ -0,0 +1,134 @@ +package winio + +import ( + "io/ioutil" + "os" + "testing" + + "golang.org/x/sys/windows" +) + +// Checks if current matches expected. Note that AllocationSize is filesystem-specific, +// so we check that the current.AllocationSize is >= expected.AllocationSize. +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/5afa7f66-619c-48f3-955f-68c4ece704ae +func checkFileStandardInfo(t *testing.T, current, expected *FileStandardInfo) { + if current.AllocationSize < expected.AllocationSize { + t.Fatalf("FileStandardInfo unexpectedly had AllocationSize %d, expecting >=%d", current.AllocationSize, expected.AllocationSize) + } + + if current.EndOfFile != expected.EndOfFile { + t.Fatalf("FileStandardInfo unexpectedly had EndOfFile %d, expecting %d", current.EndOfFile, expected.EndOfFile) + } + + if current.NumberOfLinks != expected.NumberOfLinks { + t.Fatalf("FileStandardInfo unexpectedly had NumberOfLinks %d, expecting %d", current.NumberOfLinks, expected.NumberOfLinks) + } + + if current.DeletePending != expected.DeletePending { + if current.DeletePending { + t.Fatalf("FileStandardInfo unexpectedly DeletePending") + } else { + t.Fatalf("FileStandardInfo unexpectedly not DeletePending") + } + } + + if current.Directory != expected.Directory { + if current.Directory { + t.Fatalf("FileStandardInfo unexpectedly Directory") + } else { + t.Fatalf("FileStandardInfo unexpectedly not Directory") + } + } +} + +func TestGetFileStandardInfo_File(t *testing.T) { + f, err := ioutil.TempFile("", "tst") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.Remove(f.Name()) + + expectedFileInfo := &FileStandardInfo{ + AllocationSize: 0, + EndOfFile: 0, + NumberOfLinks: 1, + DeletePending: false, + Directory: false, + } + + info, err := GetFileStandardInfo(f) + if err != nil { + t.Fatal(err) + } + checkFileStandardInfo(t, info, expectedFileInfo) + + bytesWritten, err := f.Write([]byte("0123456789")) + if err != nil { + t.Fatal(err) + } + + expectedFileInfo.EndOfFile = int64(bytesWritten) + expectedFileInfo.AllocationSize = int64(bytesWritten) + + info, err = GetFileStandardInfo(f) + if err != nil { + t.Fatal(err) + } + checkFileStandardInfo(t, info, expectedFileInfo) + + linkName := f.Name() + ".link" + + if err = os.Link(f.Name(), linkName); err != nil { + t.Fatal(err) + } + defer os.Remove(linkName) + + expectedFileInfo.NumberOfLinks = 2 + + info, err = GetFileStandardInfo(f) + if err != nil { + t.Fatal(err) + } + checkFileStandardInfo(t, info, expectedFileInfo) + + os.Remove(linkName) + + expectedFileInfo.NumberOfLinks = 1 + + info, err = GetFileStandardInfo(f) + if err != nil { + t.Fatal(err) + } + checkFileStandardInfo(t, info, expectedFileInfo) +} + +func TestGetFileStandardInfo_Directory(t *testing.T) { + tempDir, err := ioutil.TempDir("", "tst") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + // os.Open returns the Search Handle, not the Directory Handle + // See https://github.com/golang/go/issues/13738 + f, err := OpenForBackup(tempDir, windows.GENERIC_READ, 0, windows.OPEN_EXISTING) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + expectedFileInfo := &FileStandardInfo{ + AllocationSize: 0, + EndOfFile: 0, + NumberOfLinks: 1, + DeletePending: false, + Directory: true, + } + + info, err := GetFileStandardInfo(f) + if err != nil { + t.Fatal(err) + } + checkFileStandardInfo(t, info, expectedFileInfo) +}