// Copyright (C) 2014 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/. package protocol import ( "bytes" "crypto/sha256" "encoding/binary" "fmt" "time" "github.com/syncthing/syncthing/internal/gen/bep" "github.com/syncthing/syncthing/lib/build" ) // FileInfo.LocalFlags flags const ( FlagLocalUnsupported = 1 << 0 // The kind is unsupported, e.g. symlinks on Windows FlagLocalIgnored = 1 << 1 // Matches local ignore patterns FlagLocalMustRescan = 1 << 2 // Doesn't match content on disk, must be rechecked fully FlagLocalReceiveOnly = 1 << 3 // Change detected on receive only folder // Flags that should result in the Invalid bit on outgoing updates LocalInvalidFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly // Flags that should result in a file being in conflict with its // successor, due to us not having an up to date picture of its state on // disk. LocalConflictFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalReceiveOnly LocalAllFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly ) // BlockSizes is the list of valid block sizes, from min to max var BlockSizes []int func init() { for blockSize := MinBlockSize; blockSize <= MaxBlockSize; blockSize *= 2 { BlockSizes = append(BlockSizes, blockSize) if _, ok := sha256OfEmptyBlock[blockSize]; !ok { panic("missing hard coded value for sha256 of empty block") } } BufferPool = newBufferPool() // must happen after BlockSizes is initialized } type FileInfoType = bep.FileInfoType const ( FileInfoTypeFile = bep.FileInfoType_FILE_INFO_TYPE_FILE FileInfoTypeDirectory = bep.FileInfoType_FILE_INFO_TYPE_DIRECTORY FileInfoTypeSymlinkFile = bep.FileInfoType_FILE_INFO_TYPE_SYMLINK_FILE FileInfoTypeSymlinkDirectory = bep.FileInfoType_FILE_INFO_TYPE_SYMLINK_DIRECTORY FileInfoTypeSymlink = bep.FileInfoType_FILE_INFO_TYPE_SYMLINK ) type FileInfo struct { Name string Size int64 ModifiedS int64 ModifiedBy ShortID Version Vector Sequence int64 Blocks []BlockInfo SymlinkTarget string BlocksHash []byte Encrypted []byte Platform PlatformData Type FileInfoType Permissions uint32 ModifiedNs int32 RawBlockSize int32 // The local_flags fields stores flags that are relevant to the local // host only. It is not part of the protocol, doesn't get sent or // received (we make sure to zero it), nonetheless we need it on our // struct and to be able to serialize it to/from the database. LocalFlags uint32 // The version_hash is an implementation detail and not part of the wire // format. VersionHash []byte // The time when the inode was last changed (i.e., permissions, xattrs // etc changed). This is host-local, not sent over the wire. InodeChangeNs int64 // The size of the data appended to the encrypted file on disk. This is // host-local, not sent over the wire. EncryptionTrailerSize int Deleted bool RawInvalid bool NoPermissions bool truncated bool // was created from a truncated file info without blocks } func (f *FileInfo) ToWire(withInternalFields bool) *bep.FileInfo { if f.truncated && !(f.IsDeleted() || f.IsInvalid() || f.IsIgnored()) { panic("bug: must not serialize truncated file info") } blocks := make([]*bep.BlockInfo, len(f.Blocks)) for j, b := range f.Blocks { blocks[j] = b.ToWire() } w := &bep.FileInfo{ Name: f.Name, Size: f.Size, ModifiedS: f.ModifiedS, ModifiedBy: uint64(f.ModifiedBy), Version: f.Version.ToWire(), Sequence: f.Sequence, Blocks: blocks, SymlinkTarget: f.SymlinkTarget, BlocksHash: f.BlocksHash, Encrypted: f.Encrypted, Type: f.Type, Permissions: f.Permissions, ModifiedNs: f.ModifiedNs, BlockSize: f.RawBlockSize, Platform: f.Platform.toWire(), Deleted: f.Deleted, Invalid: f.RawInvalid, NoPermissions: f.NoPermissions, } if withInternalFields { w.LocalFlags = f.LocalFlags w.VersionHash = f.VersionHash w.InodeChangeNs = f.InodeChangeNs w.EncryptionTrailerSize = int32(f.EncryptionTrailerSize) } return w } // WinsConflict returns true if "f" is the one to choose when it is in // conflict with "other". func (f *FileInfo) WinsConflict(other FileInfo) bool { // If only one of the files is invalid, that one loses. if f.IsInvalid() != other.IsInvalid() { return !f.IsInvalid() } // If a modification is in conflict with a delete, we pick the // modification. if !f.IsDeleted() && other.IsDeleted() { return true } if f.IsDeleted() && !other.IsDeleted() { return false } // The one with the newer modification time wins. if f.ModTime().After(other.ModTime()) { return true } if f.ModTime().Before(other.ModTime()) { return false } // The modification times were equal. Use the device ID in the version // vector as tie breaker. return f.FileVersion().Compare(other.FileVersion()) == ConcurrentGreater } func FileInfoFromWire(w *bep.FileInfo) FileInfo { var blocks []BlockInfo if len(w.Blocks) > 0 { blocks = make([]BlockInfo, len(w.Blocks)) for j, b := range w.Blocks { blocks[j] = BlockInfoFromWire(b) } } return fileInfoFromWireWithBlocks(w, blocks) } type FileInfoWithoutBlocks interface { GetName() string GetSize() int64 GetModifiedS() int64 GetModifiedBy() uint64 GetVersion() *bep.Vector GetSequence() int64 // GetBlocks() []*bep.BlockInfo // not included GetSymlinkTarget() string GetBlocksHash() []byte GetEncrypted() []byte GetType() FileInfoType GetPermissions() uint32 GetModifiedNs() int32 GetBlockSize() int32 GetPlatform() *bep.PlatformData GetLocalFlags() uint32 GetVersionHash() []byte GetInodeChangeNs() int64 GetEncryptionTrailerSize() int32 GetDeleted() bool GetInvalid() bool GetNoPermissions() bool } func fileInfoFromWireWithBlocks(w FileInfoWithoutBlocks, blocks []BlockInfo) FileInfo { return FileInfo{ Name: w.GetName(), Size: w.GetSize(), ModifiedS: w.GetModifiedS(), ModifiedBy: ShortID(w.GetModifiedBy()), Version: VectorFromWire(w.GetVersion()), Sequence: w.GetSequence(), Blocks: blocks, SymlinkTarget: w.GetSymlinkTarget(), BlocksHash: w.GetBlocksHash(), Encrypted: w.GetEncrypted(), Type: w.GetType(), Permissions: w.GetPermissions(), ModifiedNs: w.GetModifiedNs(), RawBlockSize: w.GetBlockSize(), Platform: platformDataFromWire(w.GetPlatform()), Deleted: w.GetDeleted(), RawInvalid: w.GetInvalid(), NoPermissions: w.GetNoPermissions(), } } func FileInfoFromDB(w *bep.FileInfo) FileInfo { f := FileInfoFromWire(w) f.LocalFlags = w.LocalFlags f.VersionHash = w.VersionHash f.InodeChangeNs = w.InodeChangeNs f.EncryptionTrailerSize = int(w.EncryptionTrailerSize) return f } func FileInfoFromDBTruncated(w FileInfoWithoutBlocks) FileInfo { f := fileInfoFromWireWithBlocks(w, nil) f.LocalFlags = w.GetLocalFlags() f.VersionHash = w.GetVersionHash() f.InodeChangeNs = w.GetInodeChangeNs() f.EncryptionTrailerSize = int(w.GetEncryptionTrailerSize()) f.truncated = true return f } func (f FileInfo) String() string { switch f.Type { case FileInfoTypeDirectory: return fmt.Sprintf("Directory{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, VersionHash:%x, Deleted:%v, Invalid:%v, LocalFlags:0x%x, NoPermissions:%v, Platform:%v, InodeChangeTime:%v}", f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.VersionHash, f.Deleted, f.RawInvalid, f.LocalFlags, f.NoPermissions, f.Platform, f.InodeChangeTime()) case FileInfoTypeFile: return fmt.Sprintf("File{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, VersionHash:%x, Length:%d, Deleted:%v, Invalid:%v, LocalFlags:0x%x, NoPermissions:%v, BlockSize:%d, NumBlocks:%d, BlocksHash:%x, Platform:%v, InodeChangeTime:%v}", f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.VersionHash, f.Size, f.Deleted, f.RawInvalid, f.LocalFlags, f.NoPermissions, f.RawBlockSize, len(f.Blocks), f.BlocksHash, f.Platform, f.InodeChangeTime()) case FileInfoTypeSymlink, FileInfoTypeSymlinkDirectory, FileInfoTypeSymlinkFile: return fmt.Sprintf("Symlink{Name:%q, Type:%v, Sequence:%d, Version:%v, VersionHash:%x, Deleted:%v, Invalid:%v, LocalFlags:0x%x, NoPermissions:%v, SymlinkTarget:%q, Platform:%v, InodeChangeTime:%v}", f.Name, f.Type, f.Sequence, f.Version, f.VersionHash, f.Deleted, f.RawInvalid, f.LocalFlags, f.NoPermissions, f.SymlinkTarget, f.Platform, f.InodeChangeTime()) default: panic("mystery file type detected") } } func (f FileInfo) IsDeleted() bool { return f.Deleted } func (f FileInfo) IsInvalid() bool { return f.RawInvalid || f.LocalFlags&LocalInvalidFlags != 0 } func (f FileInfo) IsUnsupported() bool { return f.LocalFlags&FlagLocalUnsupported != 0 } func (f FileInfo) IsIgnored() bool { return f.LocalFlags&FlagLocalIgnored != 0 } func (f FileInfo) MustRescan() bool { return f.LocalFlags&FlagLocalMustRescan != 0 } func (f FileInfo) IsReceiveOnlyChanged() bool { return f.LocalFlags&FlagLocalReceiveOnly != 0 } func (f FileInfo) IsDirectory() bool { return f.Type == FileInfoTypeDirectory } func (f FileInfo) ShouldConflict() bool { return f.LocalFlags&LocalConflictFlags != 0 } func (f FileInfo) IsSymlink() bool { switch f.Type { case FileInfoTypeSymlink, FileInfoTypeSymlinkDirectory, FileInfoTypeSymlinkFile: return true default: return false } } func (f FileInfo) HasPermissionBits() bool { return !f.NoPermissions } func (f FileInfo) FileSize() int64 { if f.Deleted { return 0 } if f.IsDirectory() || f.IsSymlink() { return SyntheticDirectorySize } return f.Size } func (f FileInfo) BlockSize() int { if f.RawBlockSize < MinBlockSize { return MinBlockSize } return int(f.RawBlockSize) } // BlockSize returns the block size to use for the given file size func BlockSize(fileSize int64) int { var blockSize int for _, blockSize = range BlockSizes { if fileSize < DesiredPerFileBlocks*int64(blockSize) { break } } return blockSize } func (f FileInfo) FileName() string { return f.Name } func (f FileInfo) FileLocalFlags() uint32 { return f.LocalFlags } func (f FileInfo) ModTime() time.Time { return time.Unix(f.ModifiedS, int64(f.ModifiedNs)) } func (f FileInfo) SequenceNo() int64 { return f.Sequence } func (f FileInfo) FileVersion() Vector { return f.Version } func (f FileInfo) FileType() FileInfoType { return f.Type } func (f FileInfo) FilePermissions() uint32 { return f.Permissions } func (f FileInfo) FileModifiedBy() ShortID { return f.ModifiedBy } func (f FileInfo) PlatformData() PlatformData { return f.Platform } func (f FileInfo) InodeChangeTime() time.Time { return time.Unix(0, f.InodeChangeNs) } func (f FileInfo) FileBlocksHash() []byte { return f.BlocksHash } type FileInfoComparison struct { ModTimeWindow time.Duration IgnorePerms bool IgnoreBlocks bool IgnoreFlags uint32 IgnoreOwnership bool IgnoreXattrs bool } func (f FileInfo) IsEquivalent(other FileInfo, modTimeWindow time.Duration) bool { return f.isEquivalent(other, FileInfoComparison{ModTimeWindow: modTimeWindow}) } func (f FileInfo) IsEquivalentOptional(other FileInfo, comp FileInfoComparison) bool { return f.isEquivalent(other, comp) } // isEquivalent checks that the two file infos represent the same actual file content, // i.e. it does purposely not check only selected (see below) struct members. // Permissions (config) and blocks (scanning) can be excluded from the comparison. // Any file info is not "equivalent", if it has different // - type // - deleted flag // - invalid flag // - permissions, unless they are ignored // // A file is not "equivalent", if it has different // - modification time (difference bigger than modTimeWindow) // - size // - blocks, unless there are no blocks to compare (scanning) // - os data // // A symlink is not "equivalent", if it has different // - target // // A directory does not have anything specific to check. func (f FileInfo) isEquivalent(other FileInfo, comp FileInfoComparison) bool { if f.MustRescan() || other.MustRescan() { // These are per definition not equivalent because they don't // represent a valid state, even if both happen to have the // MustRescan bit set. return false } // If we care about either ownership or xattrs, are recording inode change // times and it changed, they are not equal. if !(comp.IgnoreOwnership && comp.IgnoreXattrs) && f.InodeChangeNs != 0 && other.InodeChangeNs != 0 && f.InodeChangeNs != other.InodeChangeNs { return false } // Mask out the ignored local flags before checking IsInvalid() below f.LocalFlags &^= comp.IgnoreFlags other.LocalFlags &^= comp.IgnoreFlags if f.Name != other.Name || f.Type != other.Type || f.Deleted != other.Deleted || f.IsInvalid() != other.IsInvalid() { return false } if !comp.IgnoreOwnership && f.Platform != other.Platform { if !unixOwnershipEqual(f.Platform.Unix, other.Platform.Unix) { return false } if !windowsOwnershipEqual(f.Platform.Windows, other.Platform.Windows) { return false } } if !comp.IgnoreXattrs && f.Platform != other.Platform { if !xattrsEqual(f.Platform.Linux, other.Platform.Linux) { return false } if !xattrsEqual(f.Platform.Darwin, other.Platform.Darwin) { return false } if !xattrsEqual(f.Platform.FreeBSD, other.Platform.FreeBSD) { return false } if !xattrsEqual(f.Platform.NetBSD, other.Platform.NetBSD) { return false } } if !comp.IgnorePerms && !f.NoPermissions && !other.NoPermissions && !PermsEqual(f.Permissions, other.Permissions) { return false } switch f.Type { case FileInfoTypeFile: return f.Size == other.Size && ModTimeEqual(f.ModTime(), other.ModTime(), comp.ModTimeWindow) && (comp.IgnoreBlocks || f.BlocksEqual(other)) case FileInfoTypeSymlink: return f.SymlinkTarget == other.SymlinkTarget case FileInfoTypeDirectory: return true } return false } func ModTimeEqual(a, b time.Time, modTimeWindow time.Duration) bool { if a.Equal(b) { return true } diff := a.Sub(b) if diff < 0 { diff *= -1 } return diff < modTimeWindow } func PermsEqual(a, b uint32) bool { if build.IsWindows { // There is only writeable and read only, represented for user, group // and other equally. We only compare against user. return a&0o600 == b&0o600 } // All bits count return a&0o777 == b&0o777 } // BlocksEqual returns true when the two files have identical block lists. func (f FileInfo) BlocksEqual(other FileInfo) bool { // If both sides have blocks hashes and they match, we are good. If they // don't match still check individual block hashes to catch differences // in weak hashes only (e.g. after switching weak hash algo). if len(f.BlocksHash) > 0 && len(other.BlocksHash) > 0 && bytes.Equal(f.BlocksHash, other.BlocksHash) { return true } // Actually compare the block lists in full. return blocksEqual(f.Blocks, other.Blocks) } func (f *FileInfo) SetMustRescan() { f.setLocalFlags(FlagLocalMustRescan) } func (f *FileInfo) SetIgnored() { f.setLocalFlags(FlagLocalIgnored) } func (f *FileInfo) SetUnsupported() { f.setLocalFlags(FlagLocalUnsupported) } func (f *FileInfo) SetDeleted(by ShortID) { f.ModifiedBy = by f.Deleted = true f.Version = f.Version.Update(by) f.ModifiedS = time.Now().Unix() f.setNoContent() } func (f *FileInfo) setLocalFlags(flags uint32) { f.RawInvalid = false f.LocalFlags = flags f.setNoContent() } func (f *FileInfo) setNoContent() { f.Blocks = nil f.BlocksHash = nil f.Size = 0 } type BlockInfo struct { Hash []byte Offset int64 Size int WeakHash uint32 } func (b BlockInfo) ToWire() *bep.BlockInfo { return &bep.BlockInfo{ Hash: b.Hash, Offset: b.Offset, Size: int32(b.Size), WeakHash: b.WeakHash, } } func BlockInfoFromWire(w *bep.BlockInfo) BlockInfo { return BlockInfo{ Hash: w.Hash, Offset: w.Offset, Size: int(w.Size), WeakHash: w.WeakHash, } } func (b BlockInfo) String() string { return fmt.Sprintf("Block{%d/%d/%d/%x}", b.Offset, b.Size, b.WeakHash, b.Hash) } // For each block size, the hash of a block of all zeroes var sha256OfEmptyBlock = map[int][sha256.Size]byte{ 128 << KiB: {0xfa, 0x43, 0x23, 0x9b, 0xce, 0xe7, 0xb9, 0x7c, 0xa6, 0x2f, 0x0, 0x7c, 0xc6, 0x84, 0x87, 0x56, 0xa, 0x39, 0xe1, 0x9f, 0x74, 0xf3, 0xdd, 0xe7, 0x48, 0x6d, 0xb3, 0xf9, 0x8d, 0xf8, 0xe4, 0x71}, 256 << KiB: {0x8a, 0x39, 0xd2, 0xab, 0xd3, 0x99, 0x9a, 0xb7, 0x3c, 0x34, 0xdb, 0x24, 0x76, 0x84, 0x9c, 0xdd, 0xf3, 0x3, 0xce, 0x38, 0x9b, 0x35, 0x82, 0x68, 0x50, 0xf9, 0xa7, 0x0, 0x58, 0x9b, 0x4a, 0x90}, 512 << KiB: {0x7, 0x85, 0x4d, 0x2f, 0xef, 0x29, 0x7a, 0x6, 0xba, 0x81, 0x68, 0x5e, 0x66, 0xc, 0x33, 0x2d, 0xe3, 0x6d, 0x5d, 0x18, 0xd5, 0x46, 0x92, 0x7d, 0x30, 0xda, 0xad, 0x6d, 0x7f, 0xda, 0x15, 0x41}, 1 << MiB: {0x30, 0xe1, 0x49, 0x55, 0xeb, 0xf1, 0x35, 0x22, 0x66, 0xdc, 0x2f, 0xf8, 0x6, 0x7e, 0x68, 0x10, 0x46, 0x7, 0xe7, 0x50, 0xab, 0xb9, 0xd3, 0xb3, 0x65, 0x82, 0xb8, 0xaf, 0x90, 0x9f, 0xcb, 0x58}, 2 << MiB: {0x56, 0x47, 0xf0, 0x5e, 0xc1, 0x89, 0x58, 0x94, 0x7d, 0x32, 0x87, 0x4e, 0xeb, 0x78, 0x8f, 0xa3, 0x96, 0xa0, 0x5d, 0xb, 0xab, 0x7c, 0x1b, 0x71, 0xf1, 0x12, 0xce, 0xb7, 0xe9, 0xb3, 0x1e, 0xee}, 4 << MiB: {0xbb, 0x9f, 0x8d, 0xf6, 0x14, 0x74, 0xd2, 0x5e, 0x71, 0xfa, 0x0, 0x72, 0x23, 0x18, 0xcd, 0x38, 0x73, 0x96, 0xca, 0x17, 0x36, 0x60, 0x5e, 0x12, 0x48, 0x82, 0x1c, 0xc0, 0xde, 0x3d, 0x3a, 0xf8}, 8 << MiB: {0x2d, 0xae, 0xb1, 0xf3, 0x60, 0x95, 0xb4, 0x4b, 0x31, 0x84, 0x10, 0xb3, 0xf4, 0xe8, 0xb5, 0xd9, 0x89, 0xdc, 0xc7, 0xbb, 0x2, 0x3d, 0x14, 0x26, 0xc4, 0x92, 0xda, 0xb0, 0xa3, 0x5, 0x3e, 0x74}, 16 << MiB: {0x8, 0xa, 0xcf, 0x35, 0xa5, 0x7, 0xac, 0x98, 0x49, 0xcf, 0xcb, 0xa4, 0x7d, 0xc2, 0xad, 0x83, 0xe0, 0x1b, 0x75, 0x66, 0x3a, 0x51, 0x62, 0x79, 0xc8, 0xb9, 0xd2, 0x43, 0xb7, 0x19, 0x64, 0x3e}, } // IsEmpty returns true if the block is a full block of zeroes. func (b BlockInfo) IsEmpty() bool { if v, ok := sha256OfEmptyBlock[b.Size]; ok { return bytes.Equal(b.Hash, v[:]) } return false } func BlocksHash(bs []BlockInfo) []byte { h := sha256.New() for _, b := range bs { _, _ = h.Write(b.Hash) _ = binary.Write(h, binary.BigEndian, b.WeakHash) } return h.Sum(nil) } func VectorHash(v Vector) []byte { h := sha256.New() for _, c := range v.Counters { if err := binary.Write(h, binary.BigEndian, c.ID); err != nil { panic("impossible: failed to write c.ID to hash function: " + err.Error()) } if err := binary.Write(h, binary.BigEndian, c.Value); err != nil { panic("impossible: failed to write c.Value to hash function: " + err.Error()) } } return h.Sum(nil) } // Xattrs is a convenience method to return the extended attributes of the // file for the current platform. func (p *PlatformData) Xattrs() []Xattr { switch { case build.IsLinux && p.Linux != nil: return p.Linux.Xattrs case build.IsDarwin && p.Darwin != nil: return p.Darwin.Xattrs case build.IsFreeBSD && p.FreeBSD != nil: return p.FreeBSD.Xattrs case build.IsNetBSD && p.NetBSD != nil: return p.NetBSD.Xattrs default: return nil } } // SetXattrs is a convenience method to set the extended attributes of the // file for the current platform. func (p *PlatformData) SetXattrs(xattrs []Xattr) { switch { case build.IsLinux: if p.Linux == nil { p.Linux = &XattrData{} } p.Linux.Xattrs = xattrs case build.IsDarwin: if p.Darwin == nil { p.Darwin = &XattrData{} } p.Darwin.Xattrs = xattrs case build.IsFreeBSD: if p.FreeBSD == nil { p.FreeBSD = &XattrData{} } p.FreeBSD.Xattrs = xattrs case build.IsNetBSD: if p.NetBSD == nil { p.NetBSD = &XattrData{} } p.NetBSD.Xattrs = xattrs } } // MergeWith copies platform data from other, for platforms where it's not // already set on p. func (p *PlatformData) MergeWith(other *PlatformData) { if p.Unix == nil { p.Unix = other.Unix } if p.Windows == nil { p.Windows = other.Windows } if p.Linux == nil { p.Linux = other.Linux } if p.Darwin == nil { p.Darwin = other.Darwin } if p.FreeBSD == nil { p.FreeBSD = other.FreeBSD } if p.NetBSD == nil { p.NetBSD = other.NetBSD } } // blocksEqual returns whether two slices of blocks are exactly the same hash // and index pair wise. func blocksEqual(a, b []BlockInfo) bool { if len(b) != len(a) { return false } for i, sblk := range a { if !bytes.Equal(sblk.Hash, b[i].Hash) { return false } } return true } type PlatformData struct { Unix *UnixData Windows *WindowsData Linux *XattrData Darwin *XattrData FreeBSD *XattrData NetBSD *XattrData } func (p *PlatformData) toWire() *bep.PlatformData { return &bep.PlatformData{ Unix: p.Unix.toWire(), Windows: p.Windows, Linux: p.Linux.toWire(), Darwin: p.Darwin.toWire(), Freebsd: p.FreeBSD.toWire(), Netbsd: p.NetBSD.toWire(), } } func platformDataFromWire(w *bep.PlatformData) PlatformData { if w == nil { return PlatformData{} } return PlatformData{ Unix: unixDataFromWire(w.Unix), Windows: w.Windows, Linux: xattrDataFromWire(w.Linux), Darwin: xattrDataFromWire(w.Darwin), FreeBSD: xattrDataFromWire(w.Freebsd), NetBSD: xattrDataFromWire(w.Netbsd), } } type UnixData struct { // The owner name and group name are set when known (i.e., could be // resolved on the source device), while the UID and GID are always set // as they come directly from the stat() call. OwnerName string GroupName string UID int GID int } func (u *UnixData) toWire() *bep.UnixData { if u == nil { return nil } return &bep.UnixData{ OwnerName: u.OwnerName, GroupName: u.GroupName, Uid: int32(u.UID), Gid: int32(u.GID), } } func unixDataFromWire(w *bep.UnixData) *UnixData { if w == nil { return nil } return &UnixData{ OwnerName: w.OwnerName, GroupName: w.GroupName, UID: int(w.Uid), GID: int(w.Gid), } } type WindowsData = bep.WindowsData type XattrData struct { Xattrs []Xattr } func (x *XattrData) toWire() *bep.XattrData { if x == nil { return nil } xattrs := make([]*bep.Xattr, len(x.Xattrs)) for i, a := range x.Xattrs { xattrs[i] = a.toWire() } return &bep.XattrData{ Xattrs: xattrs, } } func xattrDataFromWire(w *bep.XattrData) *XattrData { if w == nil { return nil } x := &XattrData{} x.Xattrs = make([]Xattr, len(w.Xattrs)) for i, a := range w.Xattrs { x.Xattrs[i] = xattrFromWire(a) } return x } type Xattr struct { Name string Value []byte } func (a Xattr) toWire() *bep.Xattr { return &bep.Xattr{ Name: a.Name, Value: a.Value, } } func xattrFromWire(w *bep.Xattr) Xattr { return Xattr{ Name: w.Name, Value: w.Value, } } func xattrsEqual(a, b *XattrData) bool { aEmpty := a == nil || len(a.Xattrs) == 0 bEmpty := b == nil || len(b.Xattrs) == 0 if aEmpty && bEmpty { return true } if aEmpty || bEmpty { // Only one side is empty, so they can't be equal. return false } if len(a.Xattrs) != len(b.Xattrs) { return false } for i := range a.Xattrs { if a.Xattrs[i].Name != b.Xattrs[i].Name { return false } if !bytes.Equal(a.Xattrs[i].Value, b.Xattrs[i].Value) { return false } } return true } func unixOwnershipEqual(a, b *UnixData) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } ownerEqual := a.OwnerName == "" || b.OwnerName == "" || a.OwnerName == b.OwnerName groupEqual := a.GroupName == "" || b.GroupName == "" || a.GroupName == b.GroupName return a.UID == b.UID && a.GID == b.GID && ownerEqual && groupEqual } func windowsOwnershipEqual(a, b *WindowsData) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } return a.OwnerName == b.OwnerName && a.OwnerIsGroup == b.OwnerIsGroup }