syncthing/lib/fs/basicfs_xattr_bsdish.go
Jakob Borg 6cac308bcd
all: Support syncing extended attributes (fixes #2698) (#8513)
This adds support for syncing extended attributes on supported
filesystem on Linux, macOS, FreeBSD and NetBSD. Windows is currently
excluded because the APIs seem onerous and annoying and frankly the uses
cases seem few and far between. On Unixes this also covers ACLs as those
are stored as extended attributes.

Similar to ownership syncing this will optional & opt-in, which two
settings controlling the main behavior: one to "sync" xattrs (read &
write) and another one to "scan" xattrs (only read them so other devices
can "sync" them, but not apply any locally).

Co-authored-by: Tomasz Wilczyński <twilczynski@naver.com>
2022-09-14 09:50:55 +02:00

102 lines
2.8 KiB
Go

// Copyright (C) 2022 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build freebsd || netbsd
// +build freebsd netbsd
package fs
import (
"errors"
"fmt"
"sort"
"unsafe"
"golang.org/x/sys/unix"
)
var (
namespaces = [...]int{unix.EXTATTR_NAMESPACE_USER, unix.EXTATTR_NAMESPACE_SYSTEM}
namespacePrefixes = [...]string{unix.EXTATTR_NAMESPACE_USER: "user.", unix.EXTATTR_NAMESPACE_SYSTEM: "system."}
)
func listXattr(path string) ([]string, error) {
var attrs []string
// List the two namespaces explicitly and prefix any results with the
// namespace name.
for _, nsid := range namespaces {
buf := make([]byte, 1024)
size, err := unixLlistxattr(path, buf, nsid)
if errors.Is(err, unix.ERANGE) || size == len(buf) {
// Buffer is too small. Try again with a zero sized buffer to
// get the size, then allocate a buffer of the correct size. We
// inlude the size == len(buf) because apparently macOS doesn't
// return ERANGE as it should -- no harm done, just an extra
// read if we happened to need precisely 1024 bytes on the first
// pass.
size, err = unixLlistxattr(path, nil, nsid)
if err != nil {
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
}
buf = make([]byte, size)
size, err = unixLlistxattr(path, buf, nsid)
}
if err != nil {
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
}
buf = buf[:size]
// "Each list entry consists of a single byte containing the length
// of the attribute name, followed by the attribute name. The
// attribute name is not terminated by ASCII 0 (nul)."
i := 0
for i < len(buf) {
l := int(buf[i])
i++
if i+l > len(buf) {
// uh-oh
return nil, fmt.Errorf("get xattr %s: attribute length %d at offset %d exceeds buffer length %d", path, l, i, len(buf))
}
if l > 0 {
attrs = append(attrs, namespacePrefixes[nsid]+string(buf[i:i+l]))
i += l
}
}
}
sort.Strings(attrs)
return attrs, nil
}
// This is unix.Llistxattr except taking a namespace parameter to dodge
// https://github.com/golang/go/issues/54357 ("Listxattr on FreeBSD loses
// namespace info")
func unixLlistxattr(link string, dest []byte, nsid int) (sz int, err error) {
d := initxattrdest(dest, 0)
destsiz := len(dest)
s, e := unix.ExtattrListLink(link, nsid, uintptr(d), destsiz)
if e != nil && e == unix.EPERM && nsid != unix.EXTATTR_NAMESPACE_USER {
return 0, nil
} else if e != nil {
return s, e
}
return s, nil
}
var _zero uintptr
func initxattrdest(dest []byte, idx int) (d unsafe.Pointer) {
if len(dest) > idx {
return unsafe.Pointer(&dest[idx])
} else {
return unsafe.Pointer(_zero)
}
}