// 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 }