blob: cab9e842cb56e494adf1d585f8433f362d039ff7 [file] [edit]
// 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
}