From 008bc6ea439f15884bef0b52f2772190c382bf46 Mon Sep 17 00:00:00 2001 From: Davis Goodin Date: Tue, 16 Jan 2024 11:46:45 -0800 Subject: [PATCH] fileinfo: internally fix FileBasicInfo memory alignment (#312) * fileinfo: internally fix FileBasicInfo memory alignment Signed-off-by: Davis Goodin * Update test with review feedback Remove unused winName. Extract more into Windows alignment consts to repeat less. Document reason for having multiple alignment consts for the same value. Signed-off-by: Davis Goodin --------- Signed-off-by: Davis Goodin --- fileinfo.go | 22 ++++++++++++++++---- fileinfo_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/fileinfo.go b/fileinfo.go index 702950e7..c860eb99 100644 --- a/fileinfo.go +++ b/fileinfo.go @@ -18,9 +18,18 @@ type FileBasicInfo struct { _ uint32 // padding } +// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing +// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64 +// alignment is necessary to pass this as FILE_BASIC_INFO. +type alignedFileBasicInfo struct { + CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64 + FileAttributes uint32 + _ uint32 // padding +} + // GetFileBasicInfo retrieves times and attributes for a file. func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) { - bi := &FileBasicInfo{} + bi := &alignedFileBasicInfo{} if err := windows.GetFileInformationByHandleEx( windows.Handle(f.Fd()), windows.FileBasicInfo, @@ -30,16 +39,21 @@ func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) { return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} } runtime.KeepAlive(f) - return bi, nil + // Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the + // public API of this module. The data may be unnecessarily aligned. + return (*FileBasicInfo)(unsafe.Pointer(bi)), nil } // SetFileBasicInfo sets times and attributes for a file. func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error { + // Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is + // suitable to pass to GetFileInformationByHandleEx. + biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi)) if err := windows.SetFileInformationByHandle( windows.Handle(f.Fd()), windows.FileBasicInfo, - (*byte)(unsafe.Pointer(bi)), - uint32(unsafe.Sizeof(*bi)), + (*byte)(unsafe.Pointer(&biAligned)), + uint32(unsafe.Sizeof(biAligned)), ); err != nil { return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err} } diff --git a/fileinfo_test.go b/fileinfo_test.go index 2e2e81bc..28b8f84a 100644 --- a/fileinfo_test.go +++ b/fileinfo_test.go @@ -6,6 +6,7 @@ package winio import ( "os" "testing" + "unsafe" "golang.org/x/sys/windows" ) @@ -131,3 +132,54 @@ func TestGetFileStandardInfo_Directory(t *testing.T) { } 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) + } + }) + } +}