| // Copyright 2026 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build windows |
| |
| package windows_test |
| |
| import ( |
| "bytes" |
| "encoding/binary" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| "unsafe" |
| |
| "golang.org/x/sys/windows" |
| ) |
| |
| func TestFdXattr(t *testing.T) { |
| fp := filepath.Join(t.TempDir(), "test_fd_xattr.txt") |
| wantContent := "I am an xattr testing file. I will get some xattrs attached." |
| wantXa := map[string]string{ |
| "xattr-key-1": "Value for xattr-key-1", |
| "xattr-key-2": "Also value, but for xattr-key-2", |
| "xattr-key-3": "xattr-key-3 needs a value too", |
| "xattr-key-4": "xattr-key-4 never wanted any value but got one anyway", |
| } |
| |
| xattrSet(t, fp, wantContent, wantXa) |
| |
| fr, err := os.Open(fp) |
| if err != nil { |
| t.Fatalf("Open for read error: %v", err) |
| } |
| defer fr.Close() |
| |
| haveContent, err := io.ReadAll(fr) |
| |
| if err != nil { |
| t.Fatalf("Read error: %v", err) |
| } |
| if string(haveContent) != wantContent { |
| t.Fatalf("File content mismatch: want %q, have %q", wantContent, string(haveContent)) |
| } |
| |
| haveXa, err := winGetEa(fr) |
| if err != nil { |
| t.Fatalf("Windows get EA error: %v", err) |
| } |
| |
| for k, v := range wantXa { |
| if haveXa[k] != v { |
| t.Fatalf("XAttr mismatch for key %q: want %q, have %q", k, v, haveXa[k]) |
| } |
| } |
| } |
| |
| func xattrSet(t *testing.T, fp string, content string, xa map[string]string) { |
| fw, err := os.OpenFile(fp, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o600) |
| if err != nil { |
| t.Fatalf("Open for write error: %v", err) |
| } |
| defer fw.Close() |
| |
| _, err = fw.WriteString(content) |
| if err != nil { |
| t.Fatalf("Write error: %v", err) |
| } |
| |
| err = winSetEa(fw, xa) |
| if err != nil { |
| if err == windows.STATUS_EAS_NOT_SUPPORTED { |
| t.Skip("filesystem does not support extended attributes, skipping test") |
| } |
| t.Fatalf("Windows set EA error: %v", err) |
| } |
| } |
| |
| // ExtendedAttribute represents a single Windows EA. |
| type extendedAttribute struct { |
| Name string |
| Value []byte |
| Flags uint8 |
| } |
| |
| type fileFullEaInformation struct { |
| NextEntryOffset uint32 |
| Flags uint8 |
| NameLength uint8 |
| ValueLength uint16 |
| } |
| |
| var fileFullEaInformationSize = binary.Size(&fileFullEaInformation{}) |
| |
| // Windows just cannot keep its hands off letter case |
| func keyToAttrName(k string) string { |
| return strings.ToUpper(k) |
| } |
| |
| func attrNameToKey(k string) string { |
| return strings.ToLower(k) |
| } |
| |
| func winSetEa(f *os.File, xattrs map[string]string) error { |
| eas := make([]extendedAttribute, 0, len(xattrs)) |
| |
| for k, v := range xattrs { |
| eas = append(eas, extendedAttribute{ |
| Name: keyToAttrName(k), |
| Value: []byte(v), |
| }) |
| } |
| |
| eaBuf, err := encodeExtendedAttributes(eas) |
| if err != nil { |
| return err |
| } |
| |
| var iosb windows.IO_STATUS_BLOCK |
| err = windows.NtSetEaFile( |
| windows.Handle(f.Fd()), &iosb, &eaBuf[0], uint32(len(eaBuf)), |
| ) |
| |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func winGetEa(f *os.File) (map[string]string, error) { |
| sz, err := getEaBlockSize(f) |
| if err != nil || sz == 0 { |
| return nil, err |
| } |
| |
| var iosb windows.IO_STATUS_BLOCK |
| eaBuf := make([]byte, int(sz)) |
| |
| err = windows.NtQueryEaFile( |
| windows.Handle(f.Fd()), |
| &iosb, |
| &eaBuf[0], |
| uint32(len(eaBuf)), |
| false, |
| nil, |
| 0, |
| nil, |
| true, |
| ) |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| eas, err := decodeExtendedAttributes(eaBuf) |
| if err != nil || len(eas) == 0 { |
| return nil, err |
| } |
| |
| m := make(map[string]string, len(eas)) |
| for _, ea := range eas { |
| m[attrNameToKey(ea.Name)] = string(ea.Value) |
| } |
| |
| return m, nil |
| } |
| |
| func getEaBlockSize(f *os.File) (uint, error) { |
| var iosb windows.IO_STATUS_BLOCK |
| var rv uint32 |
| |
| err := windows.NtQueryInformationFile( |
| windows.Handle(f.Fd()), |
| &iosb, |
| (*byte)(unsafe.Pointer(&rv)), |
| uint32(unsafe.Sizeof(rv)), |
| windows.FileEaInformation, |
| ) |
| |
| if err != nil { |
| return 0, err |
| } |
| |
| return uint(rv), nil |
| } |
| |
| func encodeExtendedAttributes(eas []extendedAttribute) ([]byte, error) { |
| var buf bytes.Buffer |
| for i := range eas { |
| last := false |
| if i == len(eas)-1 { |
| last = true |
| } |
| |
| err := writeExtendedAttributes(&buf, &eas[i], last) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return buf.Bytes(), nil |
| } |
| |
| func decodeExtendedAttributes(b []byte) (eas []extendedAttribute, err error) { |
| for len(b) != 0 { |
| ea, nb, err := parseExtendedAttributes(b) |
| if err != nil { |
| return nil, err |
| } |
| |
| eas = append(eas, ea) |
| b = nb |
| } |
| return |
| } |
| |
| func writeExtendedAttributes(buf *bytes.Buffer, ea *extendedAttribute, last bool) error { |
| if int(uint8(len(ea.Name))) != len(ea.Name) { |
| return fmt.Errorf( |
| "Extended attribute name is too long (limited to 255 bytes): name %q, value %q", |
| ea.Name, string(ea.Value), |
| ) |
| } |
| |
| if int(uint16(len(ea.Value))) != len(ea.Value) { |
| return fmt.Errorf( |
| "Extended attribute value is too long (limited to 65535 bytes): name %q, value %q", |
| ea.Name, string(ea.Value), |
| ) |
| } |
| |
| entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value)) |
| withPadding := (entrySize + 3) &^ 3 |
| nextOffset := uint32(0) |
| if !last { |
| nextOffset = withPadding |
| } |
| info := fileFullEaInformation{ |
| NextEntryOffset: nextOffset, |
| Flags: ea.Flags, |
| NameLength: uint8(len(ea.Name)), |
| ValueLength: uint16(len(ea.Value)), |
| } |
| |
| err := binary.Write(buf, binary.LittleEndian, &info) |
| if err != nil { |
| return err |
| } |
| |
| _, err = buf.Write([]byte(ea.Name)) |
| if err != nil { |
| return err |
| } |
| |
| err = buf.WriteByte(0) |
| if err != nil { |
| return err |
| } |
| |
| _, err = buf.Write(ea.Value) |
| if err != nil { |
| return err |
| } |
| |
| _, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize]) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func parseExtendedAttributes(b []byte) (ea extendedAttribute, nb []byte, err error) { |
| var info fileFullEaInformation |
| err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info) |
| if err != nil { |
| return |
| } |
| |
| nameOffset := fileFullEaInformationSize |
| nameLen := int(info.NameLength) |
| valueOffset := nameOffset + int(info.NameLength) + 1 |
| valueLen := int(info.ValueLength) |
| nextOffset := int(info.NextEntryOffset) |
| if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) { |
| err = fmt.Errorf("Invalid extended attribute buffer offset") |
| return |
| } |
| |
| ea.Name = string(b[nameOffset : nameOffset+nameLen]) |
| ea.Value = b[valueOffset : valueOffset+valueLen] |
| ea.Flags = info.Flags |
| if info.NextEntryOffset != 0 { |
| nb = b[info.NextEntryOffset:] |
| } |
| return |
| } |