mirror of
https://github.com/rwinkhart/sys.git
synced 2026-08-28 12:56:32 -04:00
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>
302 lines
6.3 KiB
Go
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
|
|
}
|