Files
sys/windows/xattr_test.go
T
Alex Dubov e4444cbaaa windows: add NtSetEaFile, NtQueryEaFile and NtQueryInformationFile
unix package already provides support for working with file extended
attributes via Fsetxattr and Fgetxattr syscalls. Windows offers a similar
feature implemented by means of NtSetEaFile and NtQueryEaFile syscalls.
Those syscalls are provided by this patch.

Additionally, NtQueryInformationFile syscall is added. It is required
for correct implementation of extended attribute support in library
code, as correct invocation of NtQueryEaFile relies on file information
returned by said syscall. It also amends the previously accepted
NtSetInformationFile syscall.

Fixes golang/go#78672

Change-Id: Ia4ba037eb2777ba02a953a57ec1d365aad17cac5
Reviewed-on: https://go-review.googlesource.com/c/sys/+/766800
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2026-05-13 22:59:11 -07:00

302 lines
6.3 KiB
Go

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