Skip to content

Commit 008bc6e

Browse files
authored
fileinfo: internally fix FileBasicInfo memory alignment (#312)
* fileinfo: internally fix FileBasicInfo memory alignment Signed-off-by: Davis Goodin <[email protected]> * 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 <[email protected]> --------- Signed-off-by: Davis Goodin <[email protected]>
1 parent bc421d9 commit 008bc6e

File tree

2 files changed

+70
-4
lines changed

2 files changed

+70
-4
lines changed

fileinfo.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@ type FileBasicInfo struct {
1818
_ uint32 // padding
1919
}
2020

21+
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
22+
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
23+
// alignment is necessary to pass this as FILE_BASIC_INFO.
24+
type alignedFileBasicInfo struct {
25+
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
26+
FileAttributes uint32
27+
_ uint32 // padding
28+
}
29+
2130
// GetFileBasicInfo retrieves times and attributes for a file.
2231
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
23-
bi := &FileBasicInfo{}
32+
bi := &alignedFileBasicInfo{}
2433
if err := windows.GetFileInformationByHandleEx(
2534
windows.Handle(f.Fd()),
2635
windows.FileBasicInfo,
@@ -30,16 +39,21 @@ func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
3039
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
3140
}
3241
runtime.KeepAlive(f)
33-
return bi, nil
42+
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
43+
// public API of this module. The data may be unnecessarily aligned.
44+
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
3445
}
3546

3647
// SetFileBasicInfo sets times and attributes for a file.
3748
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
49+
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
50+
// suitable to pass to GetFileInformationByHandleEx.
51+
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
3852
if err := windows.SetFileInformationByHandle(
3953
windows.Handle(f.Fd()),
4054
windows.FileBasicInfo,
41-
(*byte)(unsafe.Pointer(bi)),
42-
uint32(unsafe.Sizeof(*bi)),
55+
(*byte)(unsafe.Pointer(&biAligned)),
56+
uint32(unsafe.Sizeof(biAligned)),
4357
); err != nil {
4458
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
4559
}

fileinfo_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package winio
66
import (
77
"os"
88
"testing"
9+
"unsafe"
910

1011
"golang.org/x/sys/windows"
1112
)
@@ -131,3 +132,54 @@ func TestGetFileStandardInfo_Directory(t *testing.T) {
131132
}
132133
checkFileStandardInfo(t, info, expectedFileInfo)
133134
}
135+
136+
// TestFileInfoStructAlignment checks that the alignment of Go fileinfo structs
137+
// match what is expected by the Windows API.
138+
func TestFileInfoStructAlignment(t *testing.T) {
139+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
140+
const (
141+
// The alignment of various types, as named in the Windows APIs. When
142+
// deciding on an expectedAlignment for a struct's test case, use the
143+
// type of the largest field in the struct as written in the Windows
144+
// docs. This is intended to help reviewers by allowing them to first
145+
// check that a new align* const is correct, then independently check
146+
// that the test case is correct, rather than all at once.
147+
alignLARGE_INTEGER = unsafe.Alignof(uint64(0))
148+
alignULONGLONG = unsafe.Alignof(uint64(0))
149+
)
150+
tests := []struct {
151+
name string
152+
actualAlign uintptr
153+
actualSize uintptr
154+
expectedAlignment uintptr
155+
}{
156+
{
157+
// alignedFileBasicInfo is passed to the Windows API rather than FileBasicInfo.
158+
"alignedFileBasicInfo", unsafe.Alignof(alignedFileBasicInfo{}), unsafe.Sizeof(alignedFileBasicInfo{}),
159+
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_basic_info
160+
alignLARGE_INTEGER,
161+
},
162+
{
163+
"FileStandardInfo", unsafe.Alignof(FileStandardInfo{}), unsafe.Sizeof(FileStandardInfo{}),
164+
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
165+
alignLARGE_INTEGER,
166+
},
167+
{
168+
"FileIDInfo", unsafe.Alignof(FileIDInfo{}), unsafe.Sizeof(FileIDInfo{}),
169+
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_id_info
170+
alignULONGLONG,
171+
},
172+
}
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
if tt.actualAlign != tt.expectedAlignment {
176+
t.Errorf("alignment mismatch: actual %d, expected %d", tt.actualAlign, tt.expectedAlignment)
177+
}
178+
if r := tt.actualSize % tt.expectedAlignment; r != 0 {
179+
t.Errorf(
180+
"size is not a multiple of alignment: size %% alignment (%d %% %d) is %d, expected 0",
181+
tt.actualSize, tt.expectedAlignment, r)
182+
}
183+
})
184+
}
185+
}

0 commit comments

Comments
 (0)