mirror of
https://github.com/rwinkhart/sys.git
synced 2026-08-28 04:46:44 -04:00
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>
This commit is contained in:
committed by
Alex Brainman
parent
04396e85d4
commit
e4444cbaaa
@@ -452,6 +452,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys RtlInitString(destinationString *NTString, sourceString *byte) = ntdll.RtlInitString
|
||||
//sys NtCreateFile(handle *Handle, access uint32, oa *OBJECT_ATTRIBUTES, iosb *IO_STATUS_BLOCK, allocationSize *int64, attributes uint32, share uint32, disposition uint32, options uint32, eabuffer uintptr, ealength uint32) (ntstatus error) = ntdll.NtCreateFile
|
||||
//sys NtCreateNamedPipeFile(pipe *Handle, access uint32, oa *OBJECT_ATTRIBUTES, iosb *IO_STATUS_BLOCK, share uint32, disposition uint32, options uint32, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (ntstatus error) = ntdll.NtCreateNamedPipeFile
|
||||
//sys NtQueryInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtQueryInformationFile
|
||||
//sys NtSetInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtSetInformationFile
|
||||
//sys RtlDosPathNameToNtPathName(dosName *uint16, ntName *NTUnicodeString, ntFileNamePart *uint16, relativeName *RTL_RELATIVE_NAME) (ntstatus error) = ntdll.RtlDosPathNameToNtPathName_U_WithStatus
|
||||
//sys RtlDosPathNameToRelativeNtPathName(dosName *uint16, ntName *NTUnicodeString, ntFileNamePart *uint16, relativeName *RTL_RELATIVE_NAME) (ntstatus error) = ntdll.RtlDosPathNameToRelativeNtPathName_U_WithStatus
|
||||
@@ -460,6 +461,8 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys NtSetInformationProcess(proc Handle, procInfoClass int32, procInfo unsafe.Pointer, procInfoLen uint32) (ntstatus error) = ntdll.NtSetInformationProcess
|
||||
//sys NtQuerySystemInformation(sysInfoClass int32, sysInfo unsafe.Pointer, sysInfoLen uint32, retLen *uint32) (ntstatus error) = ntdll.NtQuerySystemInformation
|
||||
//sys NtSetSystemInformation(sysInfoClass int32, sysInfo unsafe.Pointer, sysInfoLen uint32) (ntstatus error) = ntdll.NtSetSystemInformation
|
||||
//sys NtQueryEaFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, returnSingleEntry bool, eaList *byte, eaListLen uint32, eaIndex *uint32, restartScan bool) (ntstatus error) = ntdll.NtQueryEaFile
|
||||
//sys NtSetEaFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32) (ntstatus error) = ntdll.NtSetEaFile
|
||||
//sys RtlAddFunctionTable(functionTable *RUNTIME_FUNCTION, entryCount uint32, baseAddress uintptr) (ret bool) = ntdll.RtlAddFunctionTable
|
||||
//sys RtlDeleteFunctionTable(functionTable *RUNTIME_FUNCTION) (ret bool) = ntdll.RtlDeleteFunctionTable
|
||||
|
||||
|
||||
@@ -3043,8 +3043,10 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
// FileInformationClass for NtSetInformationFile
|
||||
// FileInformationClass for NtSetInformationFile/NtQueryInformationFile, see
|
||||
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-_file_information_class
|
||||
FileBasicInformation = 4
|
||||
FileEaInformation = 7
|
||||
FileRenameInformation = 10
|
||||
FileDispositionInformation = 13
|
||||
FilePositionInformation = 14
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// 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
|
||||
}
|
||||
@@ -428,8 +428,11 @@ var (
|
||||
procNetUserGetInfo = modnetapi32.NewProc("NetUserGetInfo")
|
||||
procNtCreateFile = modntdll.NewProc("NtCreateFile")
|
||||
procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile")
|
||||
procNtQueryEaFile = modntdll.NewProc("NtQueryEaFile")
|
||||
procNtQueryInformationFile = modntdll.NewProc("NtQueryInformationFile")
|
||||
procNtQueryInformationProcess = modntdll.NewProc("NtQueryInformationProcess")
|
||||
procNtQuerySystemInformation = modntdll.NewProc("NtQuerySystemInformation")
|
||||
procNtSetEaFile = modntdll.NewProc("NtSetEaFile")
|
||||
procNtSetInformationFile = modntdll.NewProc("NtSetInformationFile")
|
||||
procNtSetInformationProcess = modntdll.NewProc("NtSetInformationProcess")
|
||||
procNtSetSystemInformation = modntdll.NewProc("NtSetSystemInformation")
|
||||
@@ -3740,6 +3743,30 @@ func NtCreateNamedPipeFile(pipe *Handle, access uint32, oa *OBJECT_ATTRIBUTES, i
|
||||
return
|
||||
}
|
||||
|
||||
func NtQueryEaFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, returnSingleEntry bool, eaList *byte, eaListLen uint32, eaIndex *uint32, restartScan bool) (ntstatus error) {
|
||||
var _p0 uint32
|
||||
if returnSingleEntry {
|
||||
_p0 = 1
|
||||
}
|
||||
var _p1 uint32
|
||||
if restartScan {
|
||||
_p1 = 1
|
||||
}
|
||||
r0, _, _ := syscall.SyscallN(procNtQueryEaFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(outBuffer)), uintptr(outBufferLen), uintptr(_p0), uintptr(unsafe.Pointer(eaList)), uintptr(eaListLen), uintptr(unsafe.Pointer(eaIndex)), uintptr(_p1))
|
||||
if r0 != 0 {
|
||||
ntstatus = NTStatus(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NtQueryInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, outBuffer *byte, outBufferLen uint32, class uint32) (ntstatus error) {
|
||||
r0, _, _ := syscall.SyscallN(procNtQueryInformationFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(outBuffer)), uintptr(outBufferLen), uintptr(class))
|
||||
if r0 != 0 {
|
||||
ntstatus = NTStatus(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NtQueryInformationProcess(proc Handle, procInfoClass int32, procInfo unsafe.Pointer, procInfoLen uint32, retLen *uint32) (ntstatus error) {
|
||||
r0, _, _ := syscall.SyscallN(procNtQueryInformationProcess.Addr(), uintptr(proc), uintptr(procInfoClass), uintptr(procInfo), uintptr(procInfoLen), uintptr(unsafe.Pointer(retLen)))
|
||||
if r0 != 0 {
|
||||
@@ -3756,6 +3783,14 @@ func NtQuerySystemInformation(sysInfoClass int32, sysInfo unsafe.Pointer, sysInf
|
||||
return
|
||||
}
|
||||
|
||||
func NtSetEaFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32) (ntstatus error) {
|
||||
r0, _, _ := syscall.SyscallN(procNtSetEaFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(inBuffer)), uintptr(inBufferLen))
|
||||
if r0 != 0 {
|
||||
ntstatus = NTStatus(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NtSetInformationFile(handle Handle, iosb *IO_STATUS_BLOCK, inBuffer *byte, inBufferLen uint32, class uint32) (ntstatus error) {
|
||||
r0, _, _ := syscall.SyscallN(procNtSetInformationFile.Addr(), uintptr(handle), uintptr(unsafe.Pointer(iosb)), uintptr(unsafe.Pointer(inBuffer)), uintptr(inBufferLen), uintptr(class))
|
||||
if r0 != 0 {
|
||||
|
||||
Reference in New Issue
Block a user