From 5b40bf3d9367fb0ef52361c11c599cfd86dc0021 Mon Sep 17 00:00:00 2001 From: "John K. Luebs" Date: Tue, 3 Oct 2023 20:25:31 -0500 Subject: [PATCH] Fix handling of xattrs with symlinks Fix Linux, Darwin, and other BSD (untested) to allow proper handling of xattrs with symlinks. On Linux we cannot use the f* syscalls for symlinks because symlinks cannot be opened. File flags must be handled differently on darwin and other BSD due to the lack of the LCHFLAGS syscall on darwin, and the fact that it is emulated in libc. However, we do have O_SYMLINK on darwin. --- src/duplicacy_utils_bsd.go | 53 -------------- src/duplicacy_utils_bsd_common.go | 90 +++++++++++++++++++++++ src/duplicacy_utils_darwin.go | 19 +++++ src/duplicacy_utils_linux.go | 118 +++++++++++++++++++++++++++++- src/duplicacy_utils_others.go | 82 +++------------------ src/duplicacy_utils_xbsd.go | 30 ++++++++ 6 files changed, 265 insertions(+), 127 deletions(-) delete mode 100644 src/duplicacy_utils_bsd.go create mode 100644 src/duplicacy_utils_bsd_common.go create mode 100644 src/duplicacy_utils_xbsd.go diff --git a/src/duplicacy_utils_bsd.go b/src/duplicacy_utils_bsd.go deleted file mode 100644 index 79b6bd0..0000000 --- a/src/duplicacy_utils_bsd.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Acrosync LLC. All rights reserved. -// Free for personal use and commercial trial -// Commercial use requires per-user licenses available from https://duplicacy.com - -//go:build freebsd || netbsd || darwin -// +build freebsd netbsd darwin - -package duplicacy - -import ( - "encoding/binary" - "os" - "syscall" -) - -const bsdFileFlagsKey = "\x00bf" - -func (entry *Entry) ReadFileFlags(f *os.File) error { - fileInfo, err := f.Stat() - if err != nil { - return err - } - stat, ok := fileInfo.Sys().(*syscall.Stat_t) - if ok && stat.Flags != 0 { - if entry.Attributes == nil { - entry.Attributes = &map[string][]byte{} - } - v := make([]byte, 4) - binary.LittleEndian.PutUint32(v, stat.Flags) - (*entry.Attributes)[bsdFileFlagsKey] = v - LOG_DEBUG("ATTR_READ", "Read flags 0x%x for %s", stat.Flags, entry.Path) - } - return nil -} - -func (entry *Entry) RestoreEarlyDirFlags(path string) error { - return nil -} - -func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { - return nil -} - -func (entry *Entry) RestoreLateFileFlags(f *os.File) error { - if entry.Attributes == nil { - return nil - } - if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have { - LOG_DEBUG("ATTR_RESTORE", "Restore flags 0x%x for %s", binary.LittleEndian.Uint32(v), entry.Path) - return syscall.Fchflags(int(f.Fd()), int(binary.LittleEndian.Uint32(v))) - } - return nil -} diff --git a/src/duplicacy_utils_bsd_common.go b/src/duplicacy_utils_bsd_common.go new file mode 100644 index 0000000..8a2dbc4 --- /dev/null +++ b/src/duplicacy_utils_bsd_common.go @@ -0,0 +1,90 @@ +// Copyright (c) Acrosync LLC. All rights reserved. +// Free for personal use and commercial trial +// Commercial use requires per-user licenses available from https://duplicacy.com + +//go:build freebsd || netbsd || darwin +// +build freebsd netbsd darwin + +package duplicacy + +import ( + "encoding/binary" + "os" + "path/filepath" + "bytes" + "syscall" + + "github.com/pkg/xattr" +) + +const bsdFileFlagsKey = "\x00bf" + +func (entry *Entry) ReadAttributes(top string) { + fullPath := filepath.Join(top, entry.Path) + fileInfo, err := os.Lstat(fullPath) + if err != nil { + return + } + attributes, _ := xattr.LList(fullPath) + if len(attributes) > 0 { + entry.Attributes = &map[string][]byte{} + for _, name := range attributes { + attribute, err := xattr.LGet(fullPath, name) + if err == nil { + (*entry.Attributes)[name] = attribute + } + } + } + if err := entry.readFileFlags(fileInfo); err != nil { + LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) + } +} + +func (entry *Entry) SetAttributesToFile(fullPath string) { + names, _ := xattr.LList(fullPath) + for _, name := range names { + newAttribute, found := (*entry.Attributes)[name] + if found { + oldAttribute, _ := xattr.LGet(fullPath, name) + if !bytes.Equal(oldAttribute, newAttribute) { + xattr.LSet(fullPath, name, newAttribute) + } + delete(*entry.Attributes, name) + } else { + xattr.LRemove(fullPath, name) + } + } + + for name, attribute := range *entry.Attributes { + if len(name) > 0 && name[0] == '\x00' { + continue + } + xattr.LSet(fullPath, name, attribute) + } + if err := entry.restoreLateFileFlags(fullPath); err != nil { + LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) + } +} + +func (entry *Entry) readFileFlags(fileInfo os.FileInfo) error { + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if ok && stat.Flags != 0 { + if entry.Attributes == nil { + entry.Attributes = &map[string][]byte{} + } + v := make([]byte, 4) + binary.LittleEndian.PutUint32(v, stat.Flags) + (*entry.Attributes)[bsdFileFlagsKey] = v + LOG_DEBUG("ATTR_READ", "Read flags 0x%x for %s", stat.Flags, entry.Path) + } + return nil +} + +func (entry *Entry) RestoreEarlyDirFlags(path string) error { + return nil +} + +func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { + return nil +} + diff --git a/src/duplicacy_utils_darwin.go b/src/duplicacy_utils_darwin.go index 6c69d55..90bdeb1 100644 --- a/src/duplicacy_utils_darwin.go +++ b/src/duplicacy_utils_darwin.go @@ -5,10 +5,29 @@ package duplicacy import ( + "os" + "syscall" "strings" + "encoding/binary" ) func excludedByAttribute(attributes map[string][]byte) bool { value, ok := attributes["com.apple.metadata:com_apple_backup_excludeItem"] return ok && strings.Contains(string(value), "com.apple.backupd") } + +func (entry *Entry) restoreLateFileFlags(path string) error { + if entry.Attributes == nil { + return nil + } + if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have { + f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_SYMLINK, 0) + if err != nil { + return err + } + err = syscall.Fchflags(int(f.Fd()), int(binary.LittleEndian.Uint32(v))) + f.Close() + return err + } + return nil +} diff --git a/src/duplicacy_utils_linux.go b/src/duplicacy_utils_linux.go index ac6dff1..0399ab4 100644 --- a/src/duplicacy_utils_linux.go +++ b/src/duplicacy_utils_linux.go @@ -5,10 +5,14 @@ package duplicacy import ( + "bytes" "encoding/binary" "os" "syscall" "unsafe" + "path/filepath" + + "github.com/pkg/xattr" ) const ( @@ -47,10 +51,116 @@ func ioctl(f *os.File, request uintptr, attrp *uint32) error { return nil } -func (entry *Entry) ReadFileFlags(f *os.File) error { - if entry.IsSpecial() { - return nil +type xattrHandle struct { + f *os.File + fullPath string +} + +func (x xattrHandle) list() ([]string, error) { + if x.f != nil { + return xattr.FList(x.f) + } else { + return xattr.LList(x.fullPath) } +} + +func (x xattrHandle) get(name string) ([]byte, error) { + if x.f != nil { + return xattr.FGet(x.f, name) + } else { + return xattr.LGet(x.fullPath, name) + } +} + +func (x xattrHandle) set(name string, value []byte) error { + if x.f != nil { + return xattr.FSet(x.f, name, value) + } else { + return xattr.LSet(x.fullPath, name, value) + } +} + +func (x xattrHandle) remove(name string) error { + if x.f != nil { + return xattr.FRemove(x.f, name) + } else { + return xattr.LRemove(x.fullPath, name) + } +} + +func (entry *Entry) ReadAttributes(top string) { + fullPath := filepath.Join(top, entry.Path) + x := xattrHandle{nil, fullPath} + + if !entry.IsLink() { + var err error + x.f, err = os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) + if err != nil { + // FIXME: We really should return errors for failure to read + return + } + } + + attributes, _ := x.list() + + if len(attributes) > 0 { + entry.Attributes = &map[string][]byte{} + } + for _, name := range attributes { + attribute, err := x.get(name) + if err == nil { + (*entry.Attributes)[name] = attribute + } + } + + if entry.IsFile() || entry.IsDir() { + if err := entry.readFileFlags(x.f); err != nil { + LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) + } + } + x.f.Close() +} + +func (entry *Entry) SetAttributesToFile(fullPath string) { + x := xattrHandle{nil, fullPath} + if !entry.IsLink() { + var err error + x.f, err = os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { + return + } + } + + names, _ := x.list() + + for _, name := range names { + newAttribute, found := (*entry.Attributes)[name] + if found { + oldAttribute, _ := x.get(name) + if !bytes.Equal(oldAttribute, newAttribute) { + x.set(name, newAttribute) + } + delete(*entry.Attributes, name) + } else { + x.remove(name) + } + } + + for name, attribute := range *entry.Attributes { + if len(name) > 0 && name[0] == '\x00' { + continue + } + x.set(name, attribute) + } + if entry.IsFile() || entry.IsDir() { + if err := entry.restoreLateFileFlags(x.f); err != nil { + LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) + } + } + x.f.Close() +} + +func (entry *Entry) readFileFlags(f *os.File) error { var flags uint32 if err := ioctl(f, linux_FS_IOC_GETFLAGS, &flags); err != nil { return err @@ -97,7 +207,7 @@ func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { return nil } -func (entry *Entry) RestoreLateFileFlags(f *os.File) error { +func (entry *Entry) restoreLateFileFlags(f *os.File) error { if entry.Attributes == nil { return nil } diff --git a/src/duplicacy_utils_others.go b/src/duplicacy_utils_others.go index c202e65..4feae07 100644 --- a/src/duplicacy_utils_others.go +++ b/src/duplicacy_utils_others.go @@ -2,18 +2,15 @@ // Free for personal use and commercial trial // Commercial use requires per-user licenses available from https://duplicacy.com +//go:build !windows // +build !windows package duplicacy import ( - "bytes" "os" "path" - "path/filepath" "syscall" - - "github.com/pkg/xattr" ) func Readlink(path string) (isRegular bool, s string, err error) { @@ -47,60 +44,6 @@ func SetOwner(fullPath string, entry *Entry, fileInfo *os.FileInfo) bool { return true } -func (entry *Entry) ReadAttributes(top string) { - fullPath := filepath.Join(top, entry.Path) - f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) - if err != nil { - return - } - attributes, _ := xattr.FList(f) - if len(attributes) > 0 { - entry.Attributes = &map[string][]byte{} - for _, name := range attributes { - attribute, err := xattr.Get(fullPath, name) - if err == nil { - (*entry.Attributes)[name] = attribute - } - } - } - if err := entry.ReadFileFlags(f); err != nil { - LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) - } - f.Close() -} - -func (entry *Entry) SetAttributesToFile(fullPath string) { - f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) - if err != nil { - return - } - - names, _ := xattr.FList(f) - for _, name := range names { - newAttribute, found := (*entry.Attributes)[name] - if found { - oldAttribute, _ := xattr.FGet(f, name) - if !bytes.Equal(oldAttribute, newAttribute) { - xattr.FSet(f, name, newAttribute) - } - delete(*entry.Attributes, name) - } else { - xattr.FRemove(f, name) - } - } - - for name, attribute := range *entry.Attributes { - if len(name) > 0 && name[0] == '\x00' { - continue - } - xattr.FSet(f, name, attribute) - } - if err := entry.RestoreLateFileFlags(f); err != nil { - LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) - } - f.Close() -} - func (entry *Entry) ReadSpecial(fileInfo os.FileInfo) bool { if fileInfo.Mode() & (os.ModeDevice | os.ModeCharDevice) == 0 { return true @@ -117,19 +60,18 @@ func (entry *Entry) ReadSpecial(fileInfo os.FileInfo) bool { } func (entry *Entry) RestoreSpecial(fullPath string) error { - if entry.Mode & uint32(os.ModeDevice | os.ModeCharDevice) != 0 { - mode := entry.Mode & uint32(fileModeMask) - if entry.Mode & uint32(os.ModeCharDevice) != 0 { - mode |= syscall.S_IFCHR - } else { - mode |= syscall.S_IFBLK - } - rdev := uint64(entry.StartChunk) | uint64(entry.StartOffset) << 32 - return syscall.Mknod(fullPath, mode, int(rdev)) - } else if entry.Mode & uint32(os.ModeNamedPipe) != 0 { - return syscall.Mkfifo(fullPath, uint32(entry.Mode)) + mode := entry.Mode & uint32(fileModeMask) + + if entry.Mode & uint32(os.ModeNamedPipe) != 0 { + mode |= syscall.S_IFIFO + } else if entry.Mode & uint32(os.ModeCharDevice) != 0 { + mode |= syscall.S_IFCHR + } else if entry.Mode & uint32(os.ModeDevice) != 0 { + mode |= syscall.S_IFBLK + } else { + return nil } - return nil + return syscall.Mknod(fullPath, mode, int(uint64(entry.StartChunk) | uint64(entry.StartOffset) << 32)) } func joinPath(components ...string) string { diff --git a/src/duplicacy_utils_xbsd.go b/src/duplicacy_utils_xbsd.go new file mode 100644 index 0000000..2112d28 --- /dev/null +++ b/src/duplicacy_utils_xbsd.go @@ -0,0 +1,30 @@ +// Copyright (c) Acrosync LLC. All rights reserved. +// Free for personal use and commercial trial +// Commercial use requires per-user licenses available from https://duplicacy.com + +//go:build freebsd || netbsd +// +build freebsd netbsd + +package duplicacy + +import ( + "bytes" + "encoding/binary" + "os" + "path/filepath" + "syscall" + + "github.com/pkg/xattr" +) + +func (entry *Entry) restoreLateFileFlags(path string) error { + if entry.Attributes == nil { + return nil + } + if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have { + if _, _, errno := syscall.Syscall(syscall.SYS_LCHFLAGS, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), uintptr(v), 0); errno != 0 { + return os.NewSyscallError("lchflags", errno) + } + } + return nil +}