//go:build windows package winio import ( "os" "testing" "unsafe" "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) { t.Helper() 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 := os.CreateTemp("", "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 := t.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) } // TestFileInfoStructAlignment checks that the alignment of Go fileinfo structs // match what is expected by the Windows API. func TestFileInfoStructAlignment(t *testing.T) { //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. const ( // The alignment of various types, as named in the Windows APIs. When // deciding on an expectedAlignment for a struct's test case, use the // type of the largest field in the struct as written in the Windows // docs. This is intended to help reviewers by allowing them to first // check that a new align* const is correct, then independently check // that the test case is correct, rather than all at once. alignLARGE_INTEGER = unsafe.Alignof(uint64(0)) alignULONGLONG = unsafe.Alignof(uint64(0)) ) tests := []struct { name string actualAlign uintptr actualSize uintptr expectedAlignment uintptr }{ { // alignedFileBasicInfo is passed to the Windows API rather than FileBasicInfo. "alignedFileBasicInfo", unsafe.Alignof(alignedFileBasicInfo{}), unsafe.Sizeof(alignedFileBasicInfo{}), // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_basic_info alignLARGE_INTEGER, }, { "FileStandardInfo", unsafe.Alignof(FileStandardInfo{}), unsafe.Sizeof(FileStandardInfo{}), // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info alignLARGE_INTEGER, }, { "FileIDInfo", unsafe.Alignof(FileIDInfo{}), unsafe.Sizeof(FileIDInfo{}), // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_id_info alignULONGLONG, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.actualAlign != tt.expectedAlignment { t.Errorf("alignment mismatch: actual %d, expected %d", tt.actualAlign, tt.expectedAlignment) } if r := tt.actualSize % tt.expectedAlignment; r != 0 { t.Errorf( "size is not a multiple of alignment: size %% alignment (%d %% %d) is %d, expected 0", tt.actualSize, tt.expectedAlignment, r) } }) } }