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>
This commit is contained in:
Jakob Borg 2022-09-14 09:50:55 +02:00 committed by GitHub
parent 8065cf7e97
commit 6cac308bcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2720 additions and 527 deletions

View File

@ -284,6 +284,45 @@
</p> </p>
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-6 form-group">
<p>
<label translate>Ownership</label>
&nbsp;<a href="{{docsURL('advanced/folder-sync-ownership')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'sendonly' || currentFolder.type == 'receiveencrypted'" ng-model="currentFolder.syncOwnership" /> <span translate>Sync Ownership</span>
</label>
<p translate class="help-block">
Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'receiveonly' || currentFolder.type == 'receiveencrypted' || currentFolder.syncOwnership" ng-checked="currentFolder.sendOwnership || currentFolder.syncOwnership" ng-model="currentFolder.sendOwnership" /> <span translate>Send Ownership</span>
</label>
<p translate class="help-block">
Enables sending ownership to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when "Sync Ownership" is enabled.
</p>
</div>
<div class="col-md-6 form-group">
<p>
<label translate>Extended Attributes</label>
&nbsp;<a href="{{docsURL('advanced/folder-sync-xattrs')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'sendonly' || currentFolder.type == 'receiveencrypted'" ng-model="currentFolder.syncXattrs" /> <span translate>Sync Extended Attributes</span>
</label>
<p translate class="help-block">
Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'receiveonly' || currentFolder.type == 'receiveencrypted' || currentFolder.syncXattrs" ng-checked="currentFolder.sendXattrs || currentFolder.syncXattrs" ng-model="currentFolder.sendXattrs" /> <span translate>Send Extended Attributes</span>
</label>
<p translate class="help-block">
Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when "Sync Extended Attributes" is enabled.
</p>
</div>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1820,6 +1820,8 @@ func fileIntfJSONMap(f protocol.FileIntf) map[string]interface{} {
"sequence": f.SequenceNo(), "sequence": f.SequenceNo(),
"version": jsonVersionVector(f.FileVersion()), "version": jsonVersionVector(f.FileVersion()),
"localFlags": f.FileLocalFlags(), "localFlags": f.FileLocalFlags(),
"platform": f.PlatformData(),
"inodeChange": f.InodeChangeTime(),
} }
if f.HasPermissionBits() { if f.HasPermissionBits() {
out["permissions"] = fmt.Sprintf("%#o", f.FilePermissions()) out["permissions"] = fmt.Sprintf("%#o", f.FilePermissions())

View File

@ -28,7 +28,7 @@ import (
const ( const (
OldestHandledVersion = 10 OldestHandledVersion = 10
CurrentVersion = 36 CurrentVersion = 37
MaxRescanIntervalS = 365 * 24 * 60 * 60 MaxRescanIntervalS = 365 * 24 * 60 * 60
) )

View File

@ -106,6 +106,11 @@ func TestDefaultValues(t *testing.T) {
WeakHashThresholdPct: 25, WeakHashThresholdPct: 25,
MarkerName: ".stfolder", MarkerName: ".stfolder",
MaxConcurrentWrites: 2, MaxConcurrentWrites: 2,
XattrFilter: XattrFilter{
Entries: []XattrFilterEntry{},
MaxSingleEntrySize: 1024,
MaxTotalSize: 4096,
},
}, },
Device: DeviceConfiguration{ Device: DeviceConfiguration{
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
@ -177,6 +182,9 @@ func TestDeviceConfig(t *testing.T) {
MarkerName: DefaultMarkerName, MarkerName: DefaultMarkerName,
JunctionsAsDirs: true, JunctionsAsDirs: true,
MaxConcurrentWrites: maxConcurrentWritesDefault, MaxConcurrentWrites: maxConcurrentWritesDefault,
XattrFilter: XattrFilter{
Entries: []XattrFilterEntry{},
},
}, },
} }
@ -1420,3 +1428,43 @@ func TestReceiveEncryptedFolderFixed(t *testing.T) {
t.Error("IgnorePerms should be true") t.Error("IgnorePerms should be true")
} }
} }
func TestXattrFilter(t *testing.T) {
cases := []struct {
in []string
filter []XattrFilterEntry
out []string
}{
{in: nil, filter: nil, out: nil},
{in: []string{"foo", "bar", "baz"}, filter: nil, out: []string{"foo", "bar", "baz"}},
{
in: []string{"foo", "bar", "baz"},
filter: []XattrFilterEntry{{Match: "b*", Permit: true}},
out: []string{"bar", "baz"},
},
{
in: []string{"foo", "bar", "baz"},
filter: []XattrFilterEntry{{Match: "b*", Permit: false}, {Match: "*", Permit: true}},
out: []string{"foo"},
},
{
in: []string{"foo", "bar", "baz"},
filter: []XattrFilterEntry{{Match: "yoink", Permit: true}},
out: []string{},
},
}
for _, tc := range cases {
f := XattrFilter{Entries: tc.filter}
var out []string
for _, s := range tc.in {
if f.Permit(s) {
out = append(out, s)
}
}
if fmt.Sprint(out) != fmt.Sprint(tc.out) {
t.Errorf("Filter.Apply(%v, %v) == %v, expected %v", tc.in, tc.filter, out, tc.out)
}
}
}

View File

@ -9,6 +9,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"path"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -272,3 +273,24 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
} }
return nil return nil
} }
func (f XattrFilter) Permit(s string) bool {
if len(f.Entries) == 0 {
return true
}
for _, entry := range f.Entries {
if ok, _ := path.Match(entry.Match, s); ok {
return entry.Permit
}
}
return false
}
func (f XattrFilter) GetMaxSingleEntrySize() int {
return f.MaxSingleEntrySize
}
func (f XattrFilter) GetMaxTotalSize() int {
return f.MaxTotalSize
}

View File

@ -101,11 +101,15 @@ type FolderConfiguration struct {
CaseSensitiveFS bool `protobuf:"varint,33,opt,name=case_sensitive_fs,json=caseSensitiveFs,proto3" json:"caseSensitiveFS" xml:"caseSensitiveFS"` CaseSensitiveFS bool `protobuf:"varint,33,opt,name=case_sensitive_fs,json=caseSensitiveFs,proto3" json:"caseSensitiveFS" xml:"caseSensitiveFS"`
JunctionsAsDirs bool `protobuf:"varint,34,opt,name=follow_junctions,json=followJunctions,proto3" json:"junctionsAsDirs" xml:"junctionsAsDirs"` JunctionsAsDirs bool `protobuf:"varint,34,opt,name=follow_junctions,json=followJunctions,proto3" json:"junctionsAsDirs" xml:"junctionsAsDirs"`
SyncOwnership bool `protobuf:"varint,35,opt,name=sync_ownership,json=syncOwnership,proto3" json:"syncOwnership" xml:"syncOwnership"` SyncOwnership bool `protobuf:"varint,35,opt,name=sync_ownership,json=syncOwnership,proto3" json:"syncOwnership" xml:"syncOwnership"`
ScanOwnership bool `protobuf:"varint,36,opt,name=scan_ownership,json=scanOwnership,proto3" json:"scanOwnership" xml:"scanOwnership"` SendOwnership bool `protobuf:"varint,36,opt,name=send_ownership,json=sendOwnership,proto3" json:"sendOwnership" xml:"sendOwnership"`
SyncXattrs bool `protobuf:"varint,37,opt,name=sync_xattrs,json=syncXattrs,proto3" json:"syncXattrs" xml:"syncXattrs"`
SendXattrs bool `protobuf:"varint,38,opt,name=send_xattrs,json=sendXattrs,proto3" json:"sendXattrs" xml:"sendXattrs"`
XattrFilter XattrFilter `protobuf:"bytes,39,opt,name=xattr_filter,json=xattrFilter,proto3" json:"xattrFilter" xml:"xattrFilter"`
// Legacy deprecated // Legacy deprecated
DeprecatedReadOnly bool `protobuf:"varint,9000,opt,name=read_only,json=readOnly,proto3" json:"-" xml:"ro,attr,omitempty"` // Deprecated: Do not use. DeprecatedReadOnly bool `protobuf:"varint,9000,opt,name=read_only,json=readOnly,proto3" json:"-" xml:"ro,attr,omitempty"` // Deprecated: Do not use.
DeprecatedMinDiskFreePct float64 `protobuf:"fixed64,9001,opt,name=min_disk_free_pct,json=minDiskFreePct,proto3" json:"-" xml:"minDiskFreePct,omitempty"` // Deprecated: Do not use. DeprecatedMinDiskFreePct float64 `protobuf:"fixed64,9001,opt,name=min_disk_free_pct,json=minDiskFreePct,proto3" json:"-" xml:"minDiskFreePct,omitempty"` // Deprecated: Do not use.
DeprecatedPullers int `protobuf:"varint,9002,opt,name=pullers,proto3,casttype=int" json:"-" xml:"pullers,omitempty"` // Deprecated: Do not use. DeprecatedPullers int `protobuf:"varint,9002,opt,name=pullers,proto3,casttype=int" json:"-" xml:"pullers,omitempty"` // Deprecated: Do not use.
DeprecatedScanOwnership bool `protobuf:"varint,9003,opt,name=scan_ownership,json=scanOwnership,proto3" json:"-" xml:"scanOwnership,omitempty"` // Deprecated: Do not use.
} }
func (m *FolderConfiguration) Reset() { *m = FolderConfiguration{} } func (m *FolderConfiguration) Reset() { *m = FolderConfiguration{} }
@ -141,9 +145,94 @@ func (m *FolderConfiguration) XXX_DiscardUnknown() {
var xxx_messageInfo_FolderConfiguration proto.InternalMessageInfo var xxx_messageInfo_FolderConfiguration proto.InternalMessageInfo
// Extended attribute filter. This is a list of patterns to match (glob
// style), each with an action (permit or deny). First match is used. If the
// filter is empty, all strings are permitted. If the filter is non-empty,
// the default action becomes deny. To counter this, you can use the "*"
// pattern to match all strings at the end of the filter. There are also
// limits on the size of accepted attributes.
type XattrFilter struct {
Entries []XattrFilterEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries" xml:"entry"`
MaxSingleEntrySize int `protobuf:"varint,2,opt,name=max_single_entry_size,json=maxSingleEntrySize,proto3,casttype=int" json:"maxSingleEntrySize" xml:"maxSingleEntrySize" default:"1024"`
MaxTotalSize int `protobuf:"varint,3,opt,name=max_total_size,json=maxTotalSize,proto3,casttype=int" json:"maxTotalSize" xml:"maxTotalSize" default:"4096"`
}
func (m *XattrFilter) Reset() { *m = XattrFilter{} }
func (m *XattrFilter) String() string { return proto.CompactTextString(m) }
func (*XattrFilter) ProtoMessage() {}
func (*XattrFilter) Descriptor() ([]byte, []int) {
return fileDescriptor_44a9785876ed3afa, []int{2}
}
func (m *XattrFilter) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *XattrFilter) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_XattrFilter.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *XattrFilter) XXX_Merge(src proto.Message) {
xxx_messageInfo_XattrFilter.Merge(m, src)
}
func (m *XattrFilter) XXX_Size() int {
return m.ProtoSize()
}
func (m *XattrFilter) XXX_DiscardUnknown() {
xxx_messageInfo_XattrFilter.DiscardUnknown(m)
}
var xxx_messageInfo_XattrFilter proto.InternalMessageInfo
type XattrFilterEntry struct {
Match string `protobuf:"bytes,1,opt,name=match,proto3" json:"match" xml:"match,attr"`
Permit bool `protobuf:"varint,2,opt,name=permit,proto3" json:"permit" xml:"permit,attr"`
}
func (m *XattrFilterEntry) Reset() { *m = XattrFilterEntry{} }
func (m *XattrFilterEntry) String() string { return proto.CompactTextString(m) }
func (*XattrFilterEntry) ProtoMessage() {}
func (*XattrFilterEntry) Descriptor() ([]byte, []int) {
return fileDescriptor_44a9785876ed3afa, []int{3}
}
func (m *XattrFilterEntry) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *XattrFilterEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_XattrFilterEntry.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *XattrFilterEntry) XXX_Merge(src proto.Message) {
xxx_messageInfo_XattrFilterEntry.Merge(m, src)
}
func (m *XattrFilterEntry) XXX_Size() int {
return m.ProtoSize()
}
func (m *XattrFilterEntry) XXX_DiscardUnknown() {
xxx_messageInfo_XattrFilterEntry.DiscardUnknown(m)
}
var xxx_messageInfo_XattrFilterEntry proto.InternalMessageInfo
func init() { func init() {
proto.RegisterType((*FolderDeviceConfiguration)(nil), "config.FolderDeviceConfiguration") proto.RegisterType((*FolderDeviceConfiguration)(nil), "config.FolderDeviceConfiguration")
proto.RegisterType((*FolderConfiguration)(nil), "config.FolderConfiguration") proto.RegisterType((*FolderConfiguration)(nil), "config.FolderConfiguration")
proto.RegisterType((*XattrFilter)(nil), "config.XattrFilter")
proto.RegisterType((*XattrFilterEntry)(nil), "config.XattrFilterEntry")
} }
func init() { func init() {
@ -151,138 +240,158 @@ func init() {
} }
var fileDescriptor_44a9785876ed3afa = []byte{ var fileDescriptor_44a9785876ed3afa = []byte{
// 2093 bytes of a gzipped FileDescriptorProto // 2407 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x58, 0xcf, 0x6f, 0xdc, 0xc6, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x58, 0xcd, 0x6f, 0x1b, 0xc7,
0xf5, 0x17, 0xe5, 0x5f, 0xd2, 0xe8, 0xf7, 0xc8, 0xb2, 0xc7, 0x72, 0xb2, 0xb3, 0x66, 0xd6, 0xf9, 0x15, 0xd7, 0x4a, 0xb6, 0x25, 0x8d, 0xbe, 0x47, 0x96, 0xbd, 0x51, 0x12, 0x8d, 0xb2, 0xa1, 0x63,
0x2a, 0x41, 0x22, 0xdb, 0xca, 0x17, 0x05, 0x6a, 0xd4, 0x6d, 0xb3, 0x52, 0x84, 0xba, 0xae, 0xe2, 0x25, 0x4d, 0x64, 0x5b, 0x31, 0x02, 0xc4, 0xa8, 0xdb, 0x86, 0x92, 0x85, 0xba, 0xae, 0x62, 0x61,
0x05, 0xe5, 0xd6, 0x68, 0x5a, 0x80, 0xe5, 0x92, 0xb3, 0xbb, 0x8c, 0xf8, 0xab, 0x33, 0x5c, 0x4b, 0xa9, 0xd6, 0x6d, 0x52, 0x60, 0xbb, 0xda, 0x1d, 0x92, 0x1b, 0xed, 0x07, 0xbb, 0xb3, 0xb2, 0x44,
0xeb, 0x43, 0xe0, 0x5e, 0x8a, 0x16, 0xcd, 0xa1, 0x50, 0x0f, 0xbd, 0x06, 0x68, 0x51, 0xb4, 0xf9, 0x1f, 0x02, 0xb7, 0x87, 0xa2, 0x45, 0x73, 0x28, 0xd4, 0x43, 0x91, 0x43, 0x81, 0x00, 0x2d, 0x8a,
0x07, 0x0a, 0xf4, 0xd4, 0xa3, 0x2f, 0x85, 0xf6, 0x54, 0x14, 0x3d, 0x0c, 0x10, 0xf9, 0xb6, 0x47, 0x36, 0xfd, 0x03, 0x0a, 0xf4, 0x2f, 0xf0, 0xa5, 0x90, 0x4e, 0x45, 0xd1, 0xc3, 0x00, 0x91, 0x6f,
0x1e, 0x7d, 0x2a, 0x66, 0x86, 0xe4, 0x92, 0xdc, 0x0d, 0x50, 0xa0, 0x37, 0xce, 0xe7, 0xf3, 0xe6, 0x3c, 0xf2, 0xe8, 0x53, 0xf1, 0xde, 0x7e, 0x70, 0x96, 0x64, 0x80, 0x02, 0xbd, 0x71, 0x7e, 0xbf,
0xbd, 0x0f, 0xdf, 0xcc, 0xbc, 0x79, 0x24, 0x68, 0x78, 0x6e, 0xfb, 0x8e, 0x1d, 0x06, 0x1d, 0xb7, 0x37, 0xef, 0xfd, 0xf6, 0xcd, 0xcc, 0x9b, 0x37, 0x24, 0x15, 0xdf, 0xdb, 0xbf, 0xe1, 0x44, 0x61,
0x7b, 0xa7, 0x13, 0x7a, 0x0e, 0xa1, 0x6a, 0xd0, 0xa7, 0x56, 0xec, 0x86, 0xc1, 0x76, 0x44, 0xc3, 0xdd, 0x6b, 0xdc, 0xa8, 0x47, 0xbe, 0xcb, 0xe3, 0x74, 0x70, 0x18, 0xdb, 0x89, 0x17, 0x85, 0xeb,
0x38, 0x84, 0x97, 0x15, 0xb8, 0x79, 0x73, 0xc2, 0x3a, 0x1e, 0x44, 0x44, 0x19, 0x6d, 0x6e, 0x14, 0xad, 0x38, 0x4a, 0x22, 0x7a, 0x29, 0x05, 0x97, 0x5f, 0x1e, 0xb0, 0x4e, 0xda, 0x2d, 0x9e, 0x1a,
0x48, 0xe6, 0x3e, 0xcf, 0xe0, 0xcd, 0x02, 0x1c, 0xf5, 0x3d, 0x2f, 0xa4, 0x0e, 0xa1, 0x29, 0xb7, 0x2d, 0x2f, 0x29, 0xa4, 0xf0, 0x9e, 0xe4, 0xf0, 0xb2, 0x02, 0xb7, 0x0e, 0x7d, 0x3f, 0x8a, 0x5d,
0x55, 0xe0, 0x9e, 0x11, 0xca, 0xdc, 0x30, 0x70, 0x83, 0xee, 0x14, 0x05, 0x9b, 0xb8, 0x60, 0xd9, 0x1e, 0x67, 0xdc, 0x9a, 0xc2, 0x3d, 0xe6, 0xb1, 0xf0, 0xa2, 0xd0, 0x0b, 0x1b, 0x43, 0x14, 0x2c,
0xf6, 0x42, 0xfb, 0xa8, 0xea, 0x0a, 0x0a, 0x83, 0x0e, 0xbb, 0x23, 0x04, 0xb1, 0x14, 0x7b, 0x23, 0x33, 0xc5, 0x72, 0xdf, 0x8f, 0x9c, 0x83, 0x7e, 0x57, 0x14, 0x0c, 0xea, 0xe2, 0x06, 0x08, 0x12,
0xc5, 0xec, 0x30, 0x1a, 0x50, 0x2b, 0xe8, 0x12, 0x9f, 0xc4, 0xbd, 0xd0, 0x49, 0xd9, 0x79, 0x72, 0x19, 0xf6, 0x4a, 0x86, 0x39, 0x51, 0xab, 0x1d, 0xdb, 0x61, 0x83, 0x07, 0x3c, 0x69, 0x46, 0x6e,
0x12, 0xab, 0x47, 0xfd, 0x9f, 0x17, 0xc0, 0x8d, 0x7d, 0xf9, 0x3e, 0x7b, 0xe4, 0x99, 0x6b, 0x93, 0xc6, 0x4e, 0xf2, 0xe3, 0x24, 0xfd, 0x69, 0xfc, 0x6b, 0x8c, 0xbc, 0xb4, 0x8d, 0xdf, 0xb3, 0xc5,
0xdd, 0xa2, 0x02, 0xf8, 0xa5, 0x06, 0xe6, 0x1d, 0x89, 0x9b, 0xae, 0x83, 0xb4, 0xba, 0xb6, 0xb5, 0x1f, 0x7b, 0x0e, 0xdf, 0x54, 0x15, 0xd0, 0x2f, 0x35, 0x32, 0xe9, 0x22, 0x6e, 0x79, 0xae, 0xae,
0xd8, 0xfc, 0x5c, 0x7b, 0xc9, 0xf1, 0xcc, 0xbf, 0x39, 0xfe, 0xff, 0xae, 0x1b, 0xf7, 0xfa, 0xed, 0xad, 0x6a, 0x6b, 0xd3, 0xd5, 0xcf, 0xb4, 0x67, 0x92, 0x8d, 0xfc, 0x47, 0xb2, 0xdb, 0x0d, 0x2f,
0x6d, 0x3b, 0xf4, 0xef, 0xb0, 0x41, 0x60, 0xc7, 0x3d, 0x37, 0xe8, 0x16, 0x9e, 0x84, 0x04, 0x19, 0x69, 0x1e, 0xee, 0xaf, 0x3b, 0x51, 0x70, 0x43, 0xb4, 0x43, 0x27, 0x69, 0x7a, 0x61, 0x43, 0xf9,
0xc4, 0x0e, 0xbd, 0x6d, 0xe5, 0xfd, 0xe1, 0xde, 0x39, 0xc7, 0x73, 0xd9, 0xf3, 0x88, 0xe3, 0x39, 0x05, 0x12, 0x30, 0x88, 0x13, 0xf9, 0xeb, 0xa9, 0xf7, 0xfb, 0x5b, 0xe7, 0x92, 0x4d, 0xe4, 0xbf,
0x27, 0x7d, 0x4e, 0x38, 0x5e, 0x3a, 0xf1, 0xbd, 0xfb, 0xba, 0xeb, 0xbc, 0x67, 0xc5, 0x31, 0xd5, 0x3b, 0x92, 0x4d, 0xb8, 0xd9, 0xef, 0xae, 0x64, 0x33, 0xc7, 0x81, 0x7f, 0xc7, 0xf0, 0xdc, 0xb7,
0x47, 0x67, 0x8d, 0x2b, 0xe9, 0x73, 0x72, 0xd6, 0xc8, 0xed, 0x7e, 0x35, 0x6c, 0x68, 0xa7, 0xc3, 0xed, 0x24, 0x89, 0x8d, 0xce, 0x69, 0x65, 0x3c, 0xfb, 0xdd, 0x3d, 0xad, 0x14, 0x76, 0xbf, 0x3a,
0x46, 0xee, 0xc3, 0xc8, 0x18, 0x07, 0xfe, 0x49, 0x03, 0x4b, 0x6e, 0x10, 0xd3, 0xd0, 0xe9, 0xdb, 0xab, 0x68, 0x27, 0x67, 0x95, 0xc2, 0x87, 0x99, 0x33, 0x2e, 0xfd, 0xb3, 0x46, 0x66, 0xbc, 0x30,
0xc4, 0x31, 0xdb, 0x03, 0x34, 0x2b, 0x05, 0xbf, 0xf8, 0x9f, 0x04, 0x8f, 0x38, 0x5e, 0x1c, 0x7b, 0x89, 0x23, 0xf7, 0xd0, 0xe1, 0xae, 0xb5, 0xdf, 0xd6, 0x47, 0x51, 0xf0, 0xd3, 0xff, 0x4b, 0x70,
0x6d, 0x0e, 0x12, 0x8e, 0xaf, 0x2b, 0xa1, 0x05, 0x30, 0x97, 0xbc, 0x36, 0x81, 0x0a, 0xc1, 0x46, 0x47, 0xb2, 0xe9, 0x9e, 0xd7, 0x6a, 0xbb, 0x2b, 0xd9, 0xd5, 0x54, 0xa8, 0x02, 0x16, 0x92, 0x17,
0xc9, 0x03, 0xb4, 0xc1, 0x3a, 0x09, 0x6c, 0x3a, 0x88, 0x44, 0x8e, 0xcd, 0xc8, 0x62, 0xec, 0x38, 0x06, 0x50, 0x10, 0x6c, 0x96, 0x3c, 0x50, 0x87, 0x2c, 0xf2, 0xd0, 0x89, 0xdb, 0x2d, 0xc8, 0xb1,
0xa4, 0x0e, 0xba, 0x50, 0xd7, 0xb6, 0xe6, 0x9b, 0x3b, 0x23, 0x8e, 0xe1, 0x98, 0x6e, 0xa5, 0x6c, 0xd5, 0xb2, 0x85, 0x38, 0x8a, 0x62, 0x57, 0x1f, 0x5b, 0xd5, 0xd6, 0x26, 0xab, 0x1b, 0x1d, 0xc9,
0xc2, 0x31, 0x92, 0x61, 0x27, 0x29, 0xdd, 0x98, 0x62, 0xaf, 0xff, 0xfd, 0x16, 0x58, 0x57, 0x0b, 0x68, 0x8f, 0xde, 0xcd, 0xd8, 0xae, 0x64, 0x3a, 0x86, 0x1d, 0xa4, 0x0c, 0x73, 0x88, 0xbd, 0xf1,
0x5b, 0x5e, 0xd2, 0x43, 0x30, 0x9b, 0x2e, 0xe5, 0x7c, 0x73, 0xf7, 0x9c, 0xe3, 0x59, 0xf9, 0x8a, 0x79, 0x85, 0x2c, 0xa6, 0x0b, 0x5b, 0x5e, 0xd2, 0x1a, 0x19, 0xcd, 0x96, 0x72, 0xb2, 0xba, 0x79,
0xb3, 0xae, 0x88, 0x50, 0x2b, 0xad, 0x40, 0x3d, 0x08, 0x1d, 0xd2, 0xb1, 0xfa, 0x5e, 0x7c, 0x5f, 0x2e, 0xd9, 0x28, 0x7e, 0xe2, 0xa8, 0x07, 0x11, 0x56, 0x4a, 0x2b, 0xb0, 0x1a, 0x46, 0x2e, 0xaf,
0x8f, 0x69, 0x9f, 0x14, 0x97, 0xe4, 0x74, 0xd8, 0x98, 0x7d, 0xb8, 0xf7, 0x85, 0x78, 0xb7, 0x59, 0xdb, 0x87, 0x7e, 0x72, 0xc7, 0x48, 0xe2, 0x43, 0xae, 0x2e, 0xc9, 0xc9, 0x59, 0x65, 0xf4, 0xfe,
0xd7, 0x81, 0x3f, 0x04, 0x97, 0x3c, 0xab, 0x4d, 0x3c, 0x99, 0xf1, 0xf9, 0xe6, 0x77, 0x46, 0x1c, 0xd6, 0x17, 0xf0, 0x6d, 0xa3, 0x9e, 0x4b, 0x7f, 0x40, 0x2e, 0xfa, 0xf6, 0x3e, 0xf7, 0x31, 0xe3,
0x2b, 0x20, 0xe1, 0xb8, 0x2e, 0x9d, 0xca, 0x51, 0xea, 0x97, 0x12, 0x16, 0x5b, 0x34, 0xbe, 0xaf, 0x93, 0xd5, 0x6f, 0x77, 0x24, 0x4b, 0x81, 0xae, 0x64, 0xab, 0xe8, 0x14, 0x47, 0x99, 0xdf, 0x98,
0x77, 0x2c, 0x8f, 0x49, 0xb7, 0x60, 0x4c, 0xbf, 0x18, 0x36, 0x66, 0x0c, 0x35, 0x19, 0x76, 0xc1, 0x8b, 0xc4, 0x8e, 0x93, 0x3b, 0x46, 0xdd, 0xf6, 0x05, 0xba, 0x25, 0x3d, 0xfa, 0xe9, 0x59, 0x65,
0x4a, 0xc7, 0xf5, 0x08, 0x1b, 0xb0, 0x98, 0xf8, 0xa6, 0xd8, 0xdf, 0x32, 0x49, 0xcb, 0x3b, 0x70, 0xc4, 0x4c, 0x27, 0xd3, 0x06, 0x99, 0xab, 0x7b, 0x3e, 0x17, 0x6d, 0x91, 0xf0, 0xc0, 0x82, 0xfd,
0xbb, 0xc3, 0xb6, 0xf7, 0x73, 0xea, 0xc9, 0x20, 0x22, 0xcd, 0x77, 0x47, 0x1c, 0x2f, 0x77, 0x4a, 0x8d, 0x49, 0x9a, 0xdd, 0xa0, 0xeb, 0x75, 0xb1, 0xbe, 0x5d, 0x50, 0x7b, 0xed, 0x16, 0xaf, 0xbe,
0x58, 0xc2, 0xf1, 0x55, 0x19, 0xbd, 0x0c, 0xeb, 0x46, 0xc5, 0x0e, 0x1e, 0x80, 0x8b, 0x91, 0x15, 0xd5, 0x91, 0x6c, 0xb6, 0x5e, 0xc2, 0xba, 0x92, 0x5d, 0xc6, 0xe8, 0x65, 0xd8, 0x30, 0xfb, 0xec,
0xf7, 0xd0, 0x45, 0x29, 0xff, 0x9b, 0x23, 0x8e, 0xe5, 0x38, 0xe1, 0xf8, 0xa6, 0x9c, 0x2f, 0x06, 0xe8, 0x0e, 0xb9, 0xd0, 0xb2, 0x93, 0xa6, 0x7e, 0x01, 0xe5, 0xbf, 0xdf, 0x91, 0x0c, 0xc7, 0x5d,
0xa9, 0xf8, 0x3c, 0x25, 0x9f, 0x09, 0xe1, 0xf3, 0x39, 0xf3, 0xfa, 0xac, 0xa1, 0x7d, 0x66, 0xc8, 0xc9, 0x5e, 0xc6, 0xf9, 0x30, 0xc8, 0xc4, 0x17, 0x29, 0xf9, 0x14, 0x84, 0x4f, 0x16, 0xcc, 0x8b,
0x69, 0xb0, 0x05, 0x2e, 0x4a, 0xb1, 0x97, 0x52, 0xb1, 0xea, 0xf4, 0x6e, 0xab, 0xe5, 0x90, 0x62, 0xd3, 0x8a, 0xf6, 0xa9, 0x89, 0xd3, 0xe8, 0x2e, 0xb9, 0x80, 0x62, 0x2f, 0x66, 0x62, 0xd3, 0xd3,
0xb7, 0x44, 0x88, 0x58, 0x49, 0x5c, 0x91, 0x21, 0xc4, 0x20, 0xdf, 0x46, 0xf3, 0xf9, 0xc8, 0x90, 0xbb, 0x9e, 0x2e, 0x07, 0x8a, 0x5d, 0x83, 0x10, 0x49, 0x2a, 0x71, 0x0e, 0x43, 0xc0, 0xa0, 0xd8,
0x56, 0xf0, 0xa7, 0xe0, 0x8a, 0xda, 0xe7, 0x0c, 0x5d, 0xae, 0x5f, 0xd8, 0x5a, 0xd8, 0xb9, 0x55, 0x46, 0x93, 0xc5, 0xc8, 0x44, 0x2b, 0xfa, 0x13, 0x32, 0x9e, 0xee, 0x73, 0xa1, 0x5f, 0x5a, 0x1d,
0x76, 0x3a, 0xe5, 0xf0, 0x36, 0xb1, 0xd8, 0xf6, 0x23, 0x8e, 0xb3, 0x99, 0x09, 0xc7, 0x8b, 0x32, 0x5b, 0x9b, 0xda, 0x78, 0xad, 0xec, 0x74, 0xc8, 0xe1, 0xad, 0x32, 0xd8, 0xf6, 0x1d, 0xc9, 0xf2,
0x94, 0x1a, 0xeb, 0x46, 0x46, 0xc0, 0xdf, 0x69, 0x60, 0x8d, 0x12, 0x66, 0x5b, 0x81, 0xe9, 0x06, 0x99, 0x5d, 0xc9, 0xa6, 0x31, 0x54, 0x3a, 0x36, 0xcc, 0x9c, 0xa0, 0xbf, 0xd3, 0xc8, 0x42, 0xcc,
0x31, 0xa1, 0xcf, 0x2c, 0xcf, 0x64, 0xe8, 0x4a, 0x5d, 0xdb, 0xba, 0xd4, 0xec, 0x8e, 0x38, 0x5e, 0x85, 0x63, 0x87, 0x96, 0x17, 0x26, 0x3c, 0x7e, 0x6c, 0xfb, 0x96, 0xd0, 0xc7, 0x57, 0xb5, 0xb5,
0x51, 0xe4, 0xc3, 0x94, 0x3b, 0x4c, 0x38, 0x7e, 0x47, 0x7a, 0xaa, 0xe0, 0xd5, 0x14, 0x7d, 0xf0, 0x8b, 0xd5, 0x46, 0x47, 0xb2, 0xb9, 0x94, 0xbc, 0x9f, 0x71, 0xb5, 0xae, 0x64, 0x6f, 0xa2, 0xa7,
0x8d, 0xbb, 0x77, 0xf5, 0xd7, 0x1c, 0x5f, 0x70, 0x83, 0x78, 0x74, 0xd6, 0xb8, 0x3a, 0xcd, 0xfc, 0x3e, 0xbc, 0x3f, 0x45, 0xef, 0xbe, 0x77, 0xf3, 0xa6, 0xf1, 0x42, 0xb2, 0x31, 0x2f, 0x4c, 0x3a,
0xf5, 0x59, 0xe3, 0xa2, 0xb0, 0x33, 0xaa, 0x41, 0xe0, 0xdf, 0x34, 0x00, 0x3b, 0xcc, 0x3c, 0xb6, 0xa7, 0x95, 0xcb, 0xc3, 0xcc, 0x5f, 0x9c, 0x56, 0x2e, 0x80, 0x9d, 0xd9, 0x1f, 0x84, 0xfe, 0x43,
0x62, 0xbb, 0x47, 0xa8, 0x49, 0x02, 0xab, 0xed, 0x11, 0x07, 0xcd, 0xd5, 0xb5, 0xad, 0xb9, 0xe6, 0x23, 0xb4, 0x2e, 0xac, 0x23, 0x3b, 0x71, 0x9a, 0x3c, 0xb6, 0x78, 0x68, 0xef, 0xfb, 0xdc, 0xd5,
0x6f, 0xb4, 0x73, 0x8e, 0x57, 0xf7, 0x0f, 0x9f, 0x2a, 0xf6, 0x23, 0x45, 0x8e, 0x38, 0x5e, 0xed, 0x27, 0x56, 0xb5, 0xb5, 0x89, 0xea, 0x6f, 0xb4, 0x73, 0xc9, 0xe6, 0xb7, 0x6b, 0x8f, 0x52, 0xf6,
0xb0, 0x32, 0x96, 0x70, 0xfc, 0xae, 0xda, 0x04, 0x15, 0xa2, 0xaa, 0x36, 0xdb, 0xe3, 0x1b, 0x53, 0x5e, 0x4a, 0x76, 0x24, 0x9b, 0xaf, 0x8b, 0x32, 0xd6, 0x95, 0xec, 0xad, 0x74, 0x13, 0xf4, 0x11,
0x0d, 0x85, 0x4e, 0x61, 0x71, 0x3a, 0x6c, 0x4c, 0x84, 0x35, 0x26, 0x82, 0xc2, 0xbf, 0x96, 0xc5, 0xfd, 0x6a, 0xf3, 0x3d, 0xbe, 0x34, 0xd4, 0x10, 0x74, 0x82, 0xc5, 0xc9, 0x59, 0x65, 0x20, 0xac,
0x3b, 0xc4, 0xb3, 0x06, 0x26, 0x43, 0xf3, 0x32, 0xa7, 0xbf, 0x16, 0xe2, 0x57, 0x72, 0x2f, 0x7b, 0x39, 0x10, 0x94, 0xfe, 0xbd, 0x2c, 0xde, 0xe5, 0xbe, 0xdd, 0xb6, 0x84, 0x3e, 0x89, 0x39, 0xfd,
0x82, 0x3c, 0x14, 0x79, 0xce, 0xdd, 0x28, 0x28, 0xe1, 0xf8, 0xff, 0xca, 0xd2, 0x15, 0x5e, 0x55, 0x35, 0x88, 0x9f, 0x2b, 0xbc, 0x6c, 0x01, 0x59, 0x83, 0x3c, 0x17, 0x6e, 0x52, 0xa8, 0x2b, 0xd9,
0x7e, 0xaf, 0x94, 0xe5, 0x69, 0xc6, 0xaf, 0xcf, 0x1a, 0xb3, 0xf7, 0xee, 0x9e, 0x0e, 0x1b, 0xd5, 0xf5, 0xb2, 0xf4, 0x14, 0xef, 0x57, 0x7e, 0xab, 0x94, 0xe5, 0x61, 0xc6, 0x2f, 0x4e, 0x2b, 0xa3,
0xa8, 0x46, 0x35, 0x26, 0xfc, 0x19, 0x58, 0x74, 0xbb, 0x41, 0x48, 0x89, 0x19, 0x11, 0xea, 0x33, 0xb7, 0x6e, 0x9e, 0x9c, 0x55, 0xfa, 0xa3, 0x9a, 0xfd, 0x31, 0xe9, 0x4f, 0xc9, 0xb4, 0xd7, 0x08,
0x04, 0x64, 0xbe, 0x1f, 0x8c, 0x38, 0x5e, 0x50, 0x78, 0x4b, 0xc0, 0x09, 0xc7, 0xd7, 0x54, 0xb5, 0xa3, 0x98, 0x5b, 0x2d, 0x1e, 0x07, 0x42, 0x27, 0x98, 0xef, 0xbb, 0x1d, 0xc9, 0xa6, 0x52, 0x7c,
0x18, 0x63, 0xf9, 0xf6, 0x5d, 0xad, 0x82, 0x46, 0x71, 0x2a, 0xfc, 0x85, 0x06, 0x96, 0xad, 0x7e, 0x17, 0xe0, 0xae, 0x64, 0x57, 0xd2, 0x6a, 0xd1, 0xc3, 0x8a, 0xed, 0x3b, 0xdf, 0x0f, 0x9a, 0xea,
0x1c, 0x9a, 0x41, 0x48, 0x7d, 0xcb, 0x73, 0x9f, 0x13, 0xb4, 0x20, 0x83, 0x7c, 0x32, 0xe2, 0x78, 0x54, 0xfa, 0x73, 0x8d, 0xcc, 0xda, 0x87, 0x49, 0x64, 0x85, 0x51, 0x1c, 0xd8, 0xbe, 0xf7, 0x84,
0x49, 0x30, 0x1f, 0x67, 0x44, 0x9e, 0x81, 0x12, 0xfa, 0x75, 0x2b, 0x07, 0x27, 0xad, 0xb2, 0x65, 0xeb, 0x53, 0x18, 0xe4, 0xa3, 0x8e, 0x64, 0x33, 0xc0, 0x7c, 0x98, 0x13, 0x45, 0x06, 0x4a, 0xe8,
0x33, 0xca, 0x7e, 0x61, 0x08, 0x96, 0x7c, 0x37, 0x30, 0x1d, 0x97, 0x1d, 0x99, 0x1d, 0x4a, 0x08, 0xd7, 0xad, 0x1c, 0x1d, 0xb4, 0xca, 0x97, 0xcd, 0x2c, 0xfb, 0xa5, 0x11, 0x99, 0x09, 0xbc, 0xd0,
0x5a, 0xac, 0x6b, 0x5b, 0x0b, 0x3b, 0x8b, 0xd9, 0xb1, 0x3a, 0x74, 0x9f, 0x93, 0xe6, 0x83, 0xf4, 0x72, 0x3d, 0x71, 0x60, 0xd5, 0x63, 0xce, 0xf5, 0xe9, 0x55, 0x6d, 0x6d, 0x6a, 0x63, 0x3a, 0x3f,
0x04, 0x2d, 0xf8, 0x6e, 0xb0, 0xe7, 0xb2, 0xa3, 0x7d, 0x4a, 0x84, 0x22, 0x2c, 0x15, 0x15, 0xb0, 0x56, 0x35, 0xef, 0x09, 0xaf, 0xde, 0xcd, 0x4e, 0xd0, 0x54, 0xe0, 0x85, 0x5b, 0x9e, 0x38, 0xd8,
0xe2, 0x52, 0xd4, 0x6f, 0xeb, 0xaf, 0xcf, 0x1a, 0x17, 0xee, 0xd5, 0x6f, 0x1b, 0xc5, 0x69, 0xb0, 0x8e, 0x39, 0x28, 0x62, 0xa8, 0x48, 0xc1, 0xd4, 0xa5, 0x58, 0xbd, 0x66, 0xbc, 0x38, 0xad, 0x8c,
0x0b, 0xc0, 0xf8, 0x9e, 0x47, 0x4b, 0x32, 0x1a, 0xce, 0xa2, 0xfd, 0x28, 0x67, 0xca, 0x47, 0xf8, 0xdd, 0x5a, 0xbd, 0x66, 0xaa, 0xd3, 0x68, 0x83, 0x90, 0xde, 0x3d, 0xaf, 0xcf, 0x60, 0x34, 0x96,
0xed, 0x54, 0x40, 0x61, 0x6a, 0xc2, 0xf1, 0xaa, 0x8c, 0x3f, 0x86, 0x74, 0xa3, 0xc0, 0xc3, 0x07, 0x47, 0xfb, 0x61, 0xc1, 0x94, 0x8f, 0xf0, 0x1b, 0x99, 0x00, 0x65, 0x6a, 0x57, 0xb2, 0x79, 0x8c,
0xe0, 0x8a, 0x1d, 0x46, 0x2e, 0xa1, 0x0c, 0x2d, 0xcb, 0xdd, 0xf6, 0x96, 0xa8, 0x01, 0x29, 0x94, 0xdf, 0x83, 0x0c, 0x53, 0xe1, 0xe9, 0x5d, 0x32, 0xee, 0x44, 0x2d, 0x8f, 0xc7, 0x42, 0x9f, 0xc5,
0x5f, 0xb3, 0xe9, 0x38, 0xdb, 0x37, 0x46, 0x66, 0x00, 0xff, 0xa1, 0x81, 0x6b, 0xa2, 0xc3, 0x20, 0xdd, 0xf6, 0x3a, 0xd4, 0x80, 0x0c, 0x2a, 0xae, 0xd9, 0x6c, 0x9c, 0xef, 0x1b, 0x33, 0x37, 0xa0,
0xd4, 0xf4, 0xad, 0x13, 0x33, 0x22, 0x81, 0xe3, 0x06, 0x5d, 0xf3, 0xc8, 0x6d, 0xa3, 0x15, 0xe9, 0xff, 0xd4, 0xc8, 0x15, 0xe8, 0x30, 0x78, 0x6c, 0x05, 0xf6, 0xb1, 0xd5, 0xe2, 0xa1, 0xeb, 0x85,
0xee, 0xf7, 0x62, 0xf3, 0xae, 0xb7, 0xa4, 0xc9, 0x81, 0x75, 0xd2, 0x52, 0x06, 0x8f, 0xdc, 0xe6, 0x0d, 0xeb, 0xc0, 0xdb, 0xd7, 0xe7, 0xd0, 0xdd, 0xef, 0x61, 0xf3, 0x2e, 0xee, 0xa2, 0xc9, 0x8e,
0x88, 0xe3, 0xf5, 0x68, 0x12, 0x4e, 0x38, 0xbe, 0xa1, 0x8a, 0xe8, 0x24, 0x57, 0xd8, 0xb6, 0x53, 0x7d, 0xbc, 0x9b, 0x1a, 0x3c, 0xf0, 0xaa, 0x1d, 0xc9, 0x16, 0x5b, 0x83, 0x70, 0x57, 0xb2, 0x97,
0xa7, 0x4e, 0x87, 0x4f, 0x87, 0x8d, 0x69, 0xf1, 0x8d, 0x29, 0xb6, 0x6d, 0x91, 0x8e, 0x9e, 0xc5, 0xd2, 0x22, 0x3a, 0xc8, 0x29, 0xdb, 0x76, 0xe8, 0xd4, 0xe1, 0xf0, 0xc9, 0x59, 0x65, 0x58, 0x7c,
0x7a, 0x22, 0x1d, 0xab, 0xe3, 0x74, 0xa4, 0x50, 0x9e, 0x8e, 0x74, 0x3c, 0x4e, 0x47, 0x0a, 0xc0, 0x73, 0x88, 0xed, 0x3e, 0xa4, 0xa3, 0x69, 0x8b, 0x26, 0xa4, 0x63, 0xbe, 0x97, 0x8e, 0x0c, 0x2a,
0x0f, 0xc1, 0x25, 0xd9, 0x6b, 0xa1, 0x35, 0x59, 0xcb, 0xd7, 0xb2, 0x15, 0x13, 0xf1, 0x1f, 0x0b, 0xd2, 0x91, 0x8d, 0x7b, 0xe9, 0xc8, 0x00, 0xfa, 0x01, 0xb9, 0x88, 0xbd, 0x96, 0xbe, 0x80, 0xb5,
0xa2, 0x89, 0xc4, 0x65, 0x27, 0x6d, 0x12, 0x8e, 0x17, 0xa4, 0x37, 0x39, 0xd2, 0x0d, 0x85, 0xc2, 0x7c, 0x21, 0x5f, 0x31, 0x88, 0xff, 0x10, 0x88, 0xaa, 0x0e, 0x97, 0x1d, 0xda, 0x74, 0x25, 0x9b,
0x47, 0x60, 0x29, 0x3d, 0x50, 0x0e, 0xf1, 0x48, 0x4c, 0x10, 0x94, 0x9b, 0xfd, 0x6d, 0xd9, 0x59, 0x42, 0x6f, 0x38, 0x32, 0xcc, 0x14, 0xa5, 0x0f, 0xc8, 0x4c, 0x76, 0xa0, 0x5c, 0xee, 0xf3, 0x84,
0x48, 0x62, 0x4f, 0xe2, 0x09, 0xc7, 0xb0, 0x70, 0xa4, 0x14, 0xa8, 0x1b, 0x25, 0x1b, 0x78, 0x02, 0xeb, 0x14, 0x37, 0xfb, 0x1b, 0xd8, 0x59, 0x20, 0xb1, 0x85, 0x78, 0x57, 0x32, 0xaa, 0x1c, 0xa9,
0x90, 0xac, 0xd3, 0x11, 0x0d, 0xbb, 0x94, 0x30, 0x56, 0x2c, 0xd8, 0xeb, 0xf2, 0xfd, 0xc4, 0xe5, 0x14, 0x34, 0xcc, 0x92, 0x0d, 0x3d, 0x26, 0x3a, 0xd6, 0xe9, 0x56, 0x1c, 0x35, 0x62, 0x2e, 0x84,
0xbb, 0x21, 0x6c, 0x5a, 0xa9, 0x49, 0xb1, 0x6c, 0xab, 0xeb, 0x6c, 0x2a, 0x9b, 0xbf, 0xfb, 0xf4, 0x5a, 0xb0, 0x17, 0xf1, 0xfb, 0xe0, 0xf2, 0x5d, 0x02, 0x9b, 0xdd, 0xcc, 0x44, 0x2d, 0xdb, 0xe9,
0xc9, 0xf0, 0x10, 0x2c, 0xa7, 0xfb, 0x22, 0xb2, 0xfa, 0x8c, 0x98, 0x0c, 0x5d, 0x95, 0xf1, 0xde, 0x75, 0x36, 0x94, 0x2d, 0xbe, 0x7d, 0xf8, 0x64, 0x5a, 0x23, 0xb3, 0xd9, 0xbe, 0x68, 0xd9, 0x87,
0x17, 0xef, 0xa1, 0x98, 0x96, 0x20, 0x0e, 0xf3, 0xf7, 0x28, 0x82, 0xb9, 0xf7, 0x92, 0x29, 0x24, 0x82, 0x5b, 0x42, 0xbf, 0x8c, 0xf1, 0xde, 0x81, 0xef, 0x48, 0x99, 0x5d, 0x20, 0x6a, 0xc5, 0x77,
0x60, 0x49, 0xec, 0x32, 0x91, 0x54, 0xcf, 0xb5, 0x63, 0x86, 0x36, 0xa4, 0xcf, 0xef, 0x0a, 0x9f, 0xa8, 0x60, 0xe1, 0xbd, 0x64, 0x4a, 0x39, 0x99, 0x81, 0x5d, 0x06, 0x49, 0xf5, 0x3d, 0x27, 0x11,
0xbe, 0x75, 0xb2, 0x9b, 0xe1, 0xe3, 0x53, 0x57, 0x00, 0xa7, 0x56, 0x40, 0x55, 0xe9, 0x8c, 0xd2, 0xfa, 0x12, 0xfa, 0xfc, 0x0e, 0xf8, 0x0c, 0xec, 0xe3, 0xcd, 0x1c, 0xef, 0x9d, 0x3a, 0x05, 0x1c,
0x6c, 0xe8, 0x80, 0xab, 0x8e, 0xcb, 0x44, 0x65, 0x36, 0x59, 0x64, 0x51, 0x46, 0x4c, 0xd9, 0x00, 0x5a, 0x01, 0xd3, 0x4a, 0x67, 0x96, 0x66, 0x53, 0x97, 0x5c, 0x76, 0x3d, 0x01, 0x95, 0xd9, 0x12,
0xa0, 0x6b, 0x72, 0x25, 0x64, 0xcb, 0x95, 0xf2, 0x87, 0x92, 0x96, 0xad, 0x45, 0xde, 0x72, 0x4d, 0x2d, 0x3b, 0x16, 0xdc, 0xc2, 0x06, 0x40, 0xbf, 0x82, 0x2b, 0x81, 0x2d, 0x57, 0xc6, 0xd7, 0x90,
0x52, 0xba, 0x31, 0xc5, 0xbe, 0x18, 0x25, 0x26, 0x7e, 0x64, 0xba, 0x81, 0x43, 0x4e, 0x08, 0x43, 0xc6, 0xd6, 0xa2, 0x68, 0xb9, 0x06, 0x29, 0xc3, 0x1c, 0x62, 0xaf, 0x46, 0x49, 0x78, 0xd0, 0xb2,
0xd7, 0x27, 0xa2, 0x3c, 0x21, 0x7e, 0xf4, 0x50, 0xb1, 0xd5, 0x28, 0x05, 0x6a, 0x1c, 0xa5, 0x00, 0xbc, 0xd0, 0xe5, 0xc7, 0x5c, 0xe8, 0x57, 0x07, 0xa2, 0xec, 0xf1, 0xa0, 0x75, 0x3f, 0x65, 0xfb,
0xc2, 0x1d, 0x70, 0x59, 0x2e, 0x80, 0x83, 0x90, 0xf4, 0xbb, 0x39, 0xe2, 0x38, 0x45, 0xf2, 0x1b, 0xa3, 0x28, 0x54, 0x2f, 0x8a, 0x02, 0xd2, 0x0d, 0x72, 0x09, 0x17, 0xc0, 0xd5, 0x75, 0xf4, 0xbb,
0x5e, 0x0d, 0x75, 0x23, 0xc5, 0x61, 0x0c, 0xae, 0x1f, 0x13, 0xeb, 0xc8, 0x14, 0xbb, 0xda, 0x8c, 0xdc, 0x91, 0x2c, 0x43, 0x8a, 0x1b, 0x3e, 0x1d, 0x1a, 0x66, 0x86, 0xd3, 0x84, 0x5c, 0x3d, 0xe2,
0x7b, 0x94, 0xb0, 0x5e, 0xe8, 0x39, 0x66, 0x64, 0xc7, 0xe8, 0x86, 0x4c, 0xb8, 0x28, 0xef, 0x57, 0xf6, 0x81, 0x05, 0xbb, 0xda, 0x4a, 0x9a, 0x31, 0x17, 0xcd, 0xc8, 0x77, 0xad, 0x96, 0x93, 0xe8,
0x85, 0xc9, 0xf7, 0x2c, 0xd6, 0x7b, 0x92, 0x19, 0xb4, 0xec, 0x38, 0xe1, 0x78, 0x53, 0xba, 0x9c, 0x2f, 0x61, 0xc2, 0xa1, 0xbc, 0x5f, 0x06, 0x93, 0xef, 0xda, 0xa2, 0xb9, 0x97, 0x1b, 0xec, 0x3a,
0x46, 0xe6, 0x8b, 0x3a, 0x75, 0x2a, 0xdc, 0x05, 0x0b, 0xbe, 0x45, 0x8f, 0x08, 0x35, 0x03, 0xcb, 0x49, 0x57, 0xb2, 0x65, 0x74, 0x39, 0x8c, 0x2c, 0x16, 0x75, 0xe8, 0x54, 0xba, 0x49, 0xa6, 0x02,
0x27, 0x68, 0x53, 0x36, 0x57, 0xba, 0x28, 0x67, 0x0a, 0xfe, 0xd8, 0xf2, 0x49, 0x5e, 0xce, 0xc6, 0x3b, 0x3e, 0xe0, 0xb1, 0x15, 0xda, 0x01, 0xd7, 0x97, 0xb1, 0xb9, 0x32, 0xa0, 0x9c, 0xa5, 0xf0,
0x90, 0x6e, 0x14, 0x78, 0x38, 0x00, 0x9b, 0xe2, 0x23, 0xc6, 0x0c, 0x8f, 0x03, 0x42, 0x59, 0xcf, 0x87, 0x76, 0xc0, 0x8b, 0x72, 0xd6, 0x83, 0x0c, 0x53, 0xe1, 0x69, 0x9b, 0x2c, 0xc3, 0x23, 0xc6,
0x8d, 0xcc, 0x0e, 0x0d, 0x7d, 0x33, 0xb2, 0x28, 0x09, 0x62, 0x74, 0x53, 0xa6, 0xe0, 0x5b, 0x23, 0x8a, 0x8e, 0x42, 0x1e, 0x8b, 0xa6, 0xd7, 0xb2, 0xea, 0x71, 0x14, 0x58, 0x2d, 0x3b, 0xe6, 0x61,
0x8e, 0xaf, 0x0b, 0xab, 0xc7, 0x99, 0xd1, 0x3e, 0x0d, 0xfd, 0x96, 0x34, 0x49, 0x38, 0x7e, 0x33, 0xa2, 0xbf, 0x8c, 0x29, 0xf8, 0x66, 0x47, 0xb2, 0xab, 0x60, 0xf5, 0x30, 0x37, 0xda, 0x8e, 0xa3,
0xab, 0x78, 0xd3, 0x78, 0xdd, 0xf8, 0xba, 0x99, 0xf0, 0x97, 0x1a, 0x58, 0xf3, 0x43, 0xc7, 0x8c, 0x60, 0x17, 0x4d, 0xba, 0x92, 0xbd, 0x9a, 0x57, 0xbc, 0x61, 0xbc, 0x61, 0x7e, 0xdd, 0x4c, 0xfa,
0x5d, 0x9f, 0x98, 0xc7, 0x6e, 0xe0, 0x84, 0xc7, 0x26, 0x43, 0x6f, 0xc8, 0x84, 0xfd, 0xe4, 0x9c, 0x4b, 0x8d, 0x2c, 0x04, 0x91, 0x6b, 0x25, 0x5e, 0xc0, 0xad, 0x23, 0x2f, 0x74, 0xa3, 0x23, 0x4b,
0xe3, 0x35, 0xc3, 0x3a, 0x3e, 0x08, 0x9d, 0x27, 0xae, 0x4f, 0x9e, 0x4a, 0x56, 0xdc, 0xe1, 0xcb, 0xe8, 0xaf, 0x60, 0xc2, 0x3e, 0x3e, 0x97, 0x6c, 0xc1, 0xb4, 0x8f, 0x76, 0x22, 0x77, 0xcf, 0x0b,
0x7e, 0x09, 0xc9, 0x5b, 0xd0, 0x32, 0x9c, 0x65, 0xee, 0x74, 0xd8, 0x98, 0xf4, 0x62, 0x54, 0x7c, 0xf8, 0x23, 0x64, 0xe1, 0x0e, 0x9f, 0x0d, 0x4a, 0x48, 0xd1, 0x82, 0x96, 0xe1, 0x3c, 0x73, 0x27,
0xc0, 0x17, 0x1a, 0xd8, 0x48, 0x8f, 0x89, 0xdd, 0xa7, 0x42, 0x9b, 0x79, 0x4c, 0xdd, 0x98, 0x30, 0x67, 0x95, 0x41, 0x2f, 0x66, 0x9f, 0x0f, 0xfa, 0x54, 0x23, 0x4b, 0xd9, 0x31, 0x71, 0x0e, 0x63,
0xf4, 0xa6, 0x14, 0xf3, 0x03, 0x51, 0x7a, 0xd5, 0x86, 0x4f, 0xf9, 0xa7, 0x92, 0x4e, 0x38, 0xbe, 0xd0, 0x66, 0x1d, 0xc5, 0x5e, 0xc2, 0x85, 0xfe, 0x2a, 0x8a, 0xf9, 0x3e, 0x94, 0xde, 0x74, 0xc3,
0x5d, 0x38, 0x35, 0x25, 0xae, 0x70, 0x78, 0x76, 0x0a, 0x67, 0x47, 0xdb, 0x31, 0xa6, 0x79, 0x12, 0x67, 0xfc, 0x23, 0xa4, 0xbb, 0x92, 0x5d, 0x53, 0x4e, 0x4d, 0x89, 0x53, 0x0e, 0xcf, 0x86, 0x72,
0x45, 0x2c, 0xdb, 0xdb, 0x1d, 0xf1, 0xc5, 0x84, 0x6a, 0xe3, 0x22, 0x96, 0x12, 0xfb, 0x02, 0xcf, 0x76, 0xb4, 0x0d, 0x73, 0x98, 0x27, 0x28, 0x62, 0xf9, 0xde, 0xae, 0xc3, 0x8b, 0x49, 0x5f, 0xe9,
0x0f, 0x7f, 0x11, 0xd4, 0x8d, 0x92, 0x0d, 0xf4, 0xc0, 0xaa, 0xfc, 0x92, 0x35, 0x45, 0x2d, 0x30, 0x15, 0xb1, 0x8c, 0xd8, 0x06, 0xbc, 0x38, 0xfc, 0x2a, 0x68, 0x98, 0x25, 0x1b, 0xea, 0x93, 0x79,
0x55, 0x7d, 0xc5, 0xb2, 0xbe, 0x5e, 0xcb, 0xea, 0x6b, 0x53, 0xf0, 0xe3, 0x22, 0x2b, 0x9b, 0xfb, 0x7c, 0xc9, 0x5a, 0x50, 0x0b, 0xac, 0xb4, 0xbe, 0x32, 0xac, 0xaf, 0x57, 0xf2, 0xfa, 0x5a, 0x05,
0x76, 0x09, 0xcb, 0x33, 0x5b, 0x86, 0x75, 0xa3, 0x62, 0x07, 0x3f, 0xd7, 0xc0, 0x9a, 0xdc, 0x42, 0xbe, 0x57, 0x64, 0xb1, 0xb9, 0xdf, 0x2f, 0x61, 0x45, 0x66, 0xcb, 0xb0, 0x61, 0xf6, 0xd9, 0xd1,
0xf2, 0x43, 0xd8, 0x54, 0x5f, 0xc2, 0xa8, 0x2e, 0xe3, 0xad, 0x8b, 0x0f, 0x89, 0xdd, 0x30, 0x1a, 0xcf, 0x34, 0xb2, 0x80, 0x5b, 0x08, 0x1f, 0xc2, 0x56, 0xfa, 0x12, 0xd6, 0x57, 0x31, 0xde, 0x22,
0x18, 0x82, 0x3b, 0x90, 0x54, 0xf3, 0x91, 0x68, 0xc5, 0xec, 0x32, 0x98, 0x70, 0xbc, 0x95, 0x6f, 0x3c, 0x24, 0x36, 0xa3, 0x56, 0xdb, 0x04, 0x6e, 0x07, 0xa9, 0xea, 0x03, 0x68, 0xc5, 0x9c, 0x32,
0xa3, 0x02, 0x5e, 0x48, 0x23, 0x8b, 0xad, 0xc0, 0xb1, 0xa8, 0x23, 0xee, 0xff, 0xb9, 0x6c, 0x60, 0xd8, 0x95, 0x6c, 0xad, 0xd8, 0x46, 0x0a, 0xae, 0xa4, 0x51, 0x24, 0x76, 0xe8, 0xda, 0xb1, 0x0b,
0x54, 0x1d, 0xc1, 0x3f, 0x0a, 0x39, 0x96, 0x28, 0xa0, 0x24, 0x60, 0x6e, 0xec, 0x3e, 0x13, 0x19, 0xf7, 0xff, 0x44, 0x3e, 0x30, 0xfb, 0x1d, 0xd1, 0x3f, 0x81, 0x1c, 0x1b, 0x0a, 0x28, 0x0f, 0x85,
0x45, 0xb7, 0x64, 0x3a, 0x4f, 0x44, 0x5f, 0xb8, 0x6b, 0x31, 0x72, 0x98, 0x71, 0xfb, 0xb2, 0x2f, 0x97, 0x78, 0x8f, 0x21, 0xa3, 0xfa, 0x6b, 0x98, 0xce, 0x63, 0xe8, 0x0b, 0x37, 0x6d, 0xc1, 0x6b,
0xb4, 0xcb, 0x50, 0xc2, 0xf1, 0x86, 0x12, 0x53, 0xc6, 0x45, 0x0f, 0x34, 0x61, 0x3b, 0x09, 0x89, 0x39, 0xb7, 0x8d, 0x7d, 0xa1, 0x53, 0x86, 0xba, 0x92, 0x2d, 0xa5, 0x62, 0xca, 0x38, 0xf4, 0x40,
0x36, 0xb0, 0x12, 0xc4, 0xa8, 0xd8, 0x30, 0xf8, 0x07, 0x0d, 0xac, 0x76, 0x42, 0xcf, 0x0b, 0x8f, 0x03, 0xb6, 0x83, 0x10, 0xb4, 0x81, 0x7d, 0x41, 0xcc, 0x3e, 0x1b, 0x41, 0xff, 0xa8, 0x91, 0xf9,
0xcd, 0x4f, 0xfb, 0x81, 0x2d, 0xda, 0x11, 0x86, 0xf4, 0xb1, 0xca, 0xef, 0x67, 0xe0, 0x87, 0x6c, 0x7a, 0xe4, 0xfb, 0xd1, 0x91, 0xf5, 0xc9, 0x61, 0xe8, 0x40, 0x3b, 0x22, 0x74, 0xa3, 0xa7, 0xf2,
0xcf, 0xa5, 0x4c, 0xa8, 0xfc, 0xb4, 0x0c, 0xe5, 0x2a, 0x2b, 0xb8, 0x54, 0x59, 0xb5, 0x9d, 0x84, 0x7b, 0x39, 0xf8, 0x81, 0xd8, 0xf2, 0x62, 0x01, 0x2a, 0x3f, 0x29, 0x43, 0x85, 0xca, 0x3e, 0x1c,
0x84, 0xca, 0x4a, 0x10, 0x63, 0x45, 0x29, 0xca, 0x61, 0xf8, 0x18, 0x2c, 0x8b, 0x1d, 0x35, 0xae, 0x55, 0xf6, 0xdb, 0x0e, 0x42, 0xa0, 0xb2, 0x2f, 0x88, 0x39, 0x97, 0x2a, 0x2a, 0x60, 0xfa, 0x90,
0x0e, 0xe8, 0x2d, 0x29, 0x51, 0x7c, 0x5f, 0x2d, 0x09, 0x26, 0x3f, 0xd7, 0x09, 0xc7, 0xeb, 0xea, 0xcc, 0xc2, 0x8e, 0xea, 0x55, 0x07, 0xfd, 0x75, 0x94, 0x08, 0xef, 0xab, 0x19, 0x60, 0x8a, 0x73,
0xf2, 0x2b, 0xa2, 0xba, 0x51, 0xb6, 0x92, 0x0e, 0xc5, 0xfd, 0x3a, 0x76, 0xd8, 0x28, 0x38, 0xb4, 0xdd, 0x95, 0x6c, 0x31, 0xbd, 0xfc, 0x54, 0xd4, 0x30, 0xcb, 0x56, 0xe8, 0x90, 0x87, 0xae, 0xe2,
0xad, 0x60, 0x8a, 0xc3, 0x22, 0x2a, 0x1c, 0x16, 0xc7, 0xf0, 0x08, 0xcc, 0x53, 0x62, 0x39, 0x66, 0xb0, 0xa2, 0x38, 0xe4, 0xa1, 0x3b, 0xc4, 0xa1, 0x8a, 0x82, 0x43, 0x75, 0x0c, 0x45, 0x10, 0x15,
0x18, 0x78, 0x03, 0xf4, 0xe7, 0x7d, 0xe9, 0xec, 0xe0, 0x9c, 0x63, 0xb8, 0x47, 0x22, 0x4a, 0x6c, 0x1e, 0x43, 0x37, 0x2a, 0xf4, 0x6b, 0xe8, 0x0d, 0x8b, 0x20, 0xc0, 0x3f, 0x42, 0xb4, 0x28, 0x82,
0x2b, 0x26, 0x8e, 0x41, 0x2c, 0xe7, 0x71, 0xe0, 0x0d, 0x46, 0x1c, 0x6b, 0xef, 0xe7, 0xff, 0x17, 0x3d, 0xc8, 0x30, 0x15, 0x1e, 0x9d, 0x80, 0xaa, 0xcc, 0xc9, 0x1b, 0x8a, 0x13, 0x1e, 0xba, 0xfd,
0x68, 0x28, 0x1b, 0xd8, 0xf7, 0x42, 0xdf, 0x15, 0xb7, 0x49, 0x3c, 0x90, 0xff, 0x17, 0x26, 0x50, 0x4e, 0x0a, 0x08, 0x9c, 0x14, 0x03, 0x68, 0xec, 0x71, 0x3e, 0xdc, 0x7d, 0x09, 0x8f, 0xf5, 0xeb,
0xa4, 0x19, 0x73, 0x34, 0x75, 0x00, 0x7f, 0x0e, 0xd6, 0x4a, 0x5d, 0xad, 0xac, 0xf0, 0x7f, 0x11, 0xd8, 0x83, 0x2e, 0xe6, 0x27, 0x0e, 0xad, 0xb6, 0x91, 0xaa, 0xae, 0xe5, 0x8d, 0xef, 0x71, 0x0f,
0x41, 0xb5, 0xe6, 0x47, 0xe7, 0x1c, 0xa3, 0x71, 0xd0, 0x83, 0x71, 0x6f, 0xda, 0xb2, 0xe3, 0x2c, 0xec, 0x4a, 0xb6, 0x80, 0xfe, 0x15, 0xcc, 0x30, 0x55, 0x0b, 0x7a, 0x40, 0x26, 0x63, 0x6e, 0xbb,
0x74, 0xad, 0xda, 0xda, 0xb6, 0xec, 0xb8, 0xa0, 0x00, 0x69, 0xc6, 0x72, 0x99, 0x84, 0x3f, 0x06, 0x56, 0x14, 0xfa, 0x6d, 0xfd, 0x2f, 0xdb, 0xa8, 0x72, 0xe7, 0x5c, 0x32, 0xba, 0xc5, 0x5b, 0x31,
0x57, 0xd4, 0x8d, 0xce, 0xd0, 0x97, 0xfb, 0xb2, 0x1a, 0x7d, 0x5b, 0x94, 0xc6, 0x71, 0x20, 0xd5, 0x77, 0xec, 0x84, 0xbb, 0x26, 0xb7, 0xdd, 0x87, 0xa1, 0xdf, 0xee, 0x48, 0xa6, 0xbd, 0x53, 0xfc,
0xa9, 0xb1, 0xf2, 0xcb, 0xa5, 0x53, 0x0a, 0xae, 0xd3, 0x12, 0x84, 0x34, 0x23, 0xf3, 0xd7, 0x7c, 0x97, 0x12, 0x47, 0xd8, 0xac, 0xbf, 0x1d, 0x05, 0x1e, 0xdc, 0x9c, 0x49, 0x1b, 0xff, 0x4b, 0x19,
0xf4, 0xf2, 0xab, 0xda, 0xcc, 0xf0, 0xab, 0xda, 0xcc, 0xcb, 0xf3, 0x9a, 0x36, 0x3c, 0xaf, 0x69, 0x40, 0x75, 0xcd, 0x9c, 0x88, 0x33, 0x07, 0xf4, 0x67, 0x64, 0xa1, 0xd4, 0xc1, 0xe3, 0x6d, 0xf6,
0xbf, 0x7d, 0x55, 0x9b, 0xf9, 0xe2, 0x55, 0x4d, 0x1b, 0xbe, 0xaa, 0xcd, 0xfc, 0xeb, 0x55, 0x6d, 0x57, 0x08, 0xaa, 0x55, 0xef, 0x9d, 0x4b, 0xa6, 0xf7, 0x82, 0xee, 0xf4, 0xfa, 0xf0, 0x5d, 0x27,
0xe6, 0x93, 0x77, 0xfe, 0x8b, 0x3f, 0x3a, 0xaa, 0xa0, 0xb4, 0x2f, 0xcb, 0x3f, 0x3b, 0x1f, 0xfc, 0xc9, 0x43, 0xaf, 0xf4, 0xb7, 0xf1, 0xbb, 0x4e, 0xa2, 0x28, 0xd0, 0x35, 0x73, 0xb6, 0x4c, 0xd2,
0x27, 0x00, 0x00, 0xff, 0xff, 0xd1, 0xc6, 0xcc, 0xd3, 0xf7, 0x13, 0x00, 0x00, 0x1f, 0x93, 0xf1, 0xb4, 0x7b, 0x11, 0xfa, 0x97, 0xdb, 0x58, 0x79, 0xbf, 0x05, 0xd7, 0x40, 0x2f,
0x50, 0xda, 0x95, 0x8a, 0xf2, 0xc7, 0x65, 0x53, 0x14, 0xd7, 0x59, 0xb9, 0xd5, 0x35, 0x33, 0xf7,
0x47, 0x0f, 0xc8, 0x2c, 0xf6, 0x75, 0xbd, 0x7d, 0xf7, 0xb7, 0x34, 0x7f, 0x9b, 0xe7, 0x92, 0x5d,
0xed, 0x45, 0xa8, 0x39, 0x76, 0x58, 0x6c, 0xae, 0x3c, 0xce, 0xab, 0x45, 0x57, 0x57, 0x50, 0xe5,
0x0f, 0x99, 0x29, 0x71, 0xc6, 0x2f, 0xc6, 0xc8, 0x94, 0xb2, 0xdc, 0xf4, 0x63, 0x32, 0xce, 0xc3,
0x24, 0xf6, 0xb8, 0xd0, 0x35, 0xfc, 0x77, 0x41, 0x1f, 0xb2, 0x29, 0xee, 0x85, 0x49, 0xdc, 0xae,
0x5e, 0xcf, 0xff, 0x54, 0xc8, 0x26, 0x14, 0x3d, 0x2f, 0x8c, 0x71, 0xd9, 0x2e, 0xe2, 0x2f, 0x33,
0x37, 0xa0, 0x9f, 0x67, 0x97, 0x97, 0xf0, 0xc2, 0x86, 0xcf, 0x2d, 0x64, 0x2d, 0x01, 0x8f, 0xbe,
0x51, 0x4c, 0x61, 0x1d, 0xfa, 0xa2, 0xc0, 0x3e, 0xae, 0x21, 0x8f, 0x51, 0x6a, 0xea, 0xcb, 0x6f,
0x90, 0x2a, 0xf5, 0x7d, 0x1b, 0xb7, 0x95, 0x47, 0xc4, 0x10, 0x3f, 0xf0, 0x00, 0x04, 0x2b, 0x73,
0x08, 0x47, 0x9f, 0x90, 0x59, 0x90, 0x96, 0x44, 0x09, 0x34, 0xd0, 0xa0, 0x69, 0x0c, 0x35, 0xed,
0x65, 0xfd, 0xe7, 0x1e, 0x10, 0x99, 0x9a, 0xd7, 0x72, 0x35, 0x05, 0xa8, 0xe8, 0xb8, 0x7d, 0xf3,
0xfd, 0xf7, 0x14, 0x1d, 0xa5, 0xb9, 0xa0, 0x00, 0x78, 0xb3, 0x84, 0x1a, 0x7f, 0xd0, 0xc8, 0x7c,
0x7f, 0x7a, 0xe1, 0xb9, 0x11, 0xc0, 0x6b, 0x3c, 0xfb, 0x83, 0xee, 0x1b, 0xf0, 0xb6, 0x40, 0x40,
0xe9, 0x93, 0x12, 0xa7, 0x59, 0xbc, 0xb4, 0x49, 0x6f, 0x68, 0xa6, 0x86, 0x74, 0x9b, 0x5c, 0x82,
0x87, 0xbb, 0x97, 0x60, 0x7e, 0x27, 0xaa, 0xeb, 0xd8, 0x1f, 0x22, 0x52, 0x1c, 0xe1, 0x74, 0x58,
0x78, 0x99, 0x52, 0xc6, 0x66, 0x66, 0x5b, 0x7d, 0xf0, 0xec, 0xab, 0x95, 0x91, 0xb3, 0xaf, 0x56,
0x46, 0x9e, 0x9d, 0xaf, 0x68, 0x67, 0xe7, 0x2b, 0xda, 0x6f, 0x9f, 0xaf, 0x8c, 0x7c, 0xf1, 0x7c,
0x45, 0x3b, 0x7b, 0xbe, 0x32, 0xf2, 0xef, 0xe7, 0x2b, 0x23, 0x1f, 0xbd, 0xf9, 0x3f, 0xfc, 0x9f,
0x9a, 0xee, 0xa3, 0xfd, 0x4b, 0xf8, 0xbf, 0xea, 0xbb, 0xff, 0x0d, 0x00, 0x00, 0xff, 0xff, 0x9a,
0x47, 0x7c, 0xbb, 0x75, 0x17, 0x00, 0x00,
} }
func (m *FolderDeviceConfiguration) Marshal() (dAtA []byte, err error) { func (m *FolderDeviceConfiguration) Marshal() (dAtA []byte, err error) {
@ -355,6 +464,20 @@ func (m *FolderConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = l
if m.DeprecatedScanOwnership {
i--
if m.DeprecatedScanOwnership {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x4
i--
dAtA[i] = 0xb2
i--
dAtA[i] = 0xd8
}
if m.DeprecatedPullers != 0 { if m.DeprecatedPullers != 0 {
i = encodeVarintFolderconfiguration(dAtA, i, uint64(m.DeprecatedPullers)) i = encodeVarintFolderconfiguration(dAtA, i, uint64(m.DeprecatedPullers))
i-- i--
@ -388,9 +511,45 @@ func (m *FolderConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i-- i--
dAtA[i] = 0xc0 dAtA[i] = 0xc0
} }
if m.ScanOwnership { {
size, err := m.XattrFilter.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintFolderconfiguration(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x2
i--
dAtA[i] = 0xba
if m.SendXattrs {
i-- i--
if m.ScanOwnership { if m.SendXattrs {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x2
i--
dAtA[i] = 0xb0
}
if m.SyncXattrs {
i--
if m.SyncXattrs {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x2
i--
dAtA[i] = 0xa8
}
if m.SendOwnership {
i--
if m.SendOwnership {
dAtA[i] = 1 dAtA[i] = 1
} else { } else {
dAtA[i] = 0 dAtA[i] = 0
@ -705,6 +864,93 @@ func (m *FolderConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
return len(dAtA) - i, nil return len(dAtA) - i, nil
} }
func (m *XattrFilter) Marshal() (dAtA []byte, err error) {
size := m.ProtoSize()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *XattrFilter) MarshalTo(dAtA []byte) (int, error) {
size := m.ProtoSize()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *XattrFilter) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.MaxTotalSize != 0 {
i = encodeVarintFolderconfiguration(dAtA, i, uint64(m.MaxTotalSize))
i--
dAtA[i] = 0x18
}
if m.MaxSingleEntrySize != 0 {
i = encodeVarintFolderconfiguration(dAtA, i, uint64(m.MaxSingleEntrySize))
i--
dAtA[i] = 0x10
}
if len(m.Entries) > 0 {
for iNdEx := len(m.Entries) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Entries[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintFolderconfiguration(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0xa
}
}
return len(dAtA) - i, nil
}
func (m *XattrFilterEntry) Marshal() (dAtA []byte, err error) {
size := m.ProtoSize()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *XattrFilterEntry) MarshalTo(dAtA []byte) (int, error) {
size := m.ProtoSize()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *XattrFilterEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.Permit {
i--
if m.Permit {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x10
}
if len(m.Match) > 0 {
i -= len(m.Match)
copy(dAtA[i:], m.Match)
i = encodeVarintFolderconfiguration(dAtA, i, uint64(len(m.Match)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintFolderconfiguration(dAtA []byte, offset int, v uint64) int { func encodeVarintFolderconfiguration(dAtA []byte, offset int, v uint64) int {
offset -= sovFolderconfiguration(v) offset -= sovFolderconfiguration(v)
base := offset base := offset
@ -849,9 +1095,17 @@ func (m *FolderConfiguration) ProtoSize() (n int) {
if m.SyncOwnership { if m.SyncOwnership {
n += 3 n += 3
} }
if m.ScanOwnership { if m.SendOwnership {
n += 3 n += 3
} }
if m.SyncXattrs {
n += 3
}
if m.SendXattrs {
n += 3
}
l = m.XattrFilter.ProtoSize()
n += 2 + l + sovFolderconfiguration(uint64(l))
if m.DeprecatedReadOnly { if m.DeprecatedReadOnly {
n += 4 n += 4
} }
@ -861,6 +1115,46 @@ func (m *FolderConfiguration) ProtoSize() (n int) {
if m.DeprecatedPullers != 0 { if m.DeprecatedPullers != 0 {
n += 3 + sovFolderconfiguration(uint64(m.DeprecatedPullers)) n += 3 + sovFolderconfiguration(uint64(m.DeprecatedPullers))
} }
if m.DeprecatedScanOwnership {
n += 4
}
return n
}
func (m *XattrFilter) ProtoSize() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if len(m.Entries) > 0 {
for _, e := range m.Entries {
l = e.ProtoSize()
n += 1 + l + sovFolderconfiguration(uint64(l))
}
}
if m.MaxSingleEntrySize != 0 {
n += 1 + sovFolderconfiguration(uint64(m.MaxSingleEntrySize))
}
if m.MaxTotalSize != 0 {
n += 1 + sovFolderconfiguration(uint64(m.MaxTotalSize))
}
return n
}
func (m *XattrFilterEntry) ProtoSize() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Match)
if l > 0 {
n += 1 + l + sovFolderconfiguration(uint64(l))
}
if m.Permit {
n += 2
}
return n return n
} }
@ -1821,7 +2115,7 @@ func (m *FolderConfiguration) Unmarshal(dAtA []byte) error {
m.SyncOwnership = bool(v != 0) m.SyncOwnership = bool(v != 0)
case 36: case 36:
if wireType != 0 { if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field ScanOwnership", wireType) return fmt.Errorf("proto: wrong wireType = %d for field SendOwnership", wireType)
} }
var v int var v int
for shift := uint(0); ; shift += 7 { for shift := uint(0); ; shift += 7 {
@ -1838,7 +2132,80 @@ func (m *FolderConfiguration) Unmarshal(dAtA []byte) error {
break break
} }
} }
m.ScanOwnership = bool(v != 0) m.SendOwnership = bool(v != 0)
case 37:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field SyncXattrs", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.SyncXattrs = bool(v != 0)
case 38:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field SendXattrs", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.SendXattrs = bool(v != 0)
case 39:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field XattrFilter", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthFolderconfiguration
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthFolderconfiguration
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := m.XattrFilter.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 9000: case 9000:
if wireType != 0 { if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field DeprecatedReadOnly", wireType) return fmt.Errorf("proto: wrong wireType = %d for field DeprecatedReadOnly", wireType)
@ -1889,6 +2256,250 @@ func (m *FolderConfiguration) Unmarshal(dAtA []byte) error {
break break
} }
} }
case 9003:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field DeprecatedScanOwnership", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.DeprecatedScanOwnership = bool(v != 0)
default:
iNdEx = preIndex
skippy, err := skipFolderconfiguration(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthFolderconfiguration
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *XattrFilter) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: XattrFilter: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: XattrFilter: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Entries", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthFolderconfiguration
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthFolderconfiguration
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Entries = append(m.Entries, XattrFilterEntry{})
if err := m.Entries[len(m.Entries)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field MaxSingleEntrySize", wireType)
}
m.MaxSingleEntrySize = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.MaxSingleEntrySize |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field MaxTotalSize", wireType)
}
m.MaxTotalSize = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.MaxTotalSize |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipFolderconfiguration(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthFolderconfiguration
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *XattrFilterEntry) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: XattrFilterEntry: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: XattrFilterEntry: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Match", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthFolderconfiguration
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthFolderconfiguration
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Match = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Permit", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowFolderconfiguration
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.Permit = bool(v != 0)
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipFolderconfiguration(dAtA[iNdEx:]) skippy, err := skipFolderconfiguration(dAtA[iNdEx:])

View File

@ -27,6 +27,7 @@ import (
// put the newest on top for readability. // put the newest on top for readability.
var ( var (
migrations = migrationSet{ migrations = migrationSet{
{37, migrateToConfigV37},
{36, migrateToConfigV36}, {36, migrateToConfigV36},
{35, migrateToConfigV35}, {35, migrateToConfigV35},
{34, migrateToConfigV34}, {34, migrateToConfigV34},
@ -95,6 +96,14 @@ func (m migration) apply(cfg *Configuration) {
cfg.Version = m.targetVersion cfg.Version = m.targetVersion
} }
func migrateToConfigV37(cfg *Configuration) {
// "scan ownership" changed name to "send ownership"
for i := range cfg.Folders {
cfg.Folders[i].SendOwnership = cfg.Folders[i].DeprecatedScanOwnership
cfg.Folders[i].DeprecatedScanOwnership = false
}
}
func migrateToConfigV36(cfg *Configuration) { func migrateToConfigV36(cfg *Configuration) {
for i := range cfg.Folders { for i := range cfg.Folders {
delete(cfg.Folders[i].Versioning.Params, "cleanInterval") delete(cfg.Folders[i].Versioning.Params, "cleanInterval")

View File

@ -125,6 +125,14 @@ func (f FileInfoTruncated) FileModifiedBy() protocol.ShortID {
return f.ModifiedBy return f.ModifiedBy
} }
func (f FileInfoTruncated) PlatformData() protocol.PlatformData {
return f.Platform
}
func (f FileInfoTruncated) InodeChangeTime() time.Time {
return time.Unix(0, f.InodeChangeNs)
}
func (f FileInfoTruncated) ConvertToIgnoredFileInfo() protocol.FileInfo { func (f FileInfoTruncated) ConvertToIgnoredFileInfo() protocol.FileInfo {
file := f.copyToFileInfo() file := f.copyToFileInfo()
file.SetIgnored() file.SetIgnored()

View File

@ -126,6 +126,7 @@ type FileInfoTruncated struct {
// see bep.proto // see bep.proto
LocalFlags uint32 `protobuf:"varint,1000,opt,name=local_flags,json=localFlags,proto3" json:"localFlags" xml:"localFlags"` LocalFlags uint32 `protobuf:"varint,1000,opt,name=local_flags,json=localFlags,proto3" json:"localFlags" xml:"localFlags"`
VersionHash []byte `protobuf:"bytes,1001,opt,name=version_hash,json=versionHash,proto3" json:"versionHash" xml:"versionHash"` VersionHash []byte `protobuf:"bytes,1001,opt,name=version_hash,json=versionHash,proto3" json:"versionHash" xml:"versionHash"`
InodeChangeNs int64 `protobuf:"varint,1002,opt,name=inode_change_ns,json=inodeChangeNs,proto3" json:"inodeChangeNs" xml:"inodeChangeNs"`
Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted" xml:"deleted"` Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted" xml:"deleted"`
RawInvalid bool `protobuf:"varint,7,opt,name=invalid,proto3" json:"invalid" xml:"invalid"` RawInvalid bool `protobuf:"varint,7,opt,name=invalid,proto3" json:"invalid" xml:"invalid"`
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"noPermissions" xml:"noPermissions"` NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"noPermissions" xml:"noPermissions"`
@ -496,102 +497,104 @@ func init() {
func init() { proto.RegisterFile("lib/db/structs.proto", fileDescriptor_5465d80e8cba02e3) } func init() { proto.RegisterFile("lib/db/structs.proto", fileDescriptor_5465d80e8cba02e3) }
var fileDescriptor_5465d80e8cba02e3 = []byte{ var fileDescriptor_5465d80e8cba02e3 = []byte{
// 1510 bytes of a gzipped FileDescriptorProto // 1543 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xcd, 0x6f, 0xdb, 0x46, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xcd, 0x6f, 0xdb, 0x46,
0x16, 0x37, 0x2d, 0xd9, 0x96, 0x46, 0xf2, 0x17, 0xb3, 0x36, 0xb8, 0xde, 0x5d, 0x8d, 0x76, 0xe2, 0x16, 0x37, 0x2d, 0xd9, 0x92, 0x46, 0xf2, 0x17, 0xb3, 0x36, 0xb4, 0xde, 0x5d, 0x51, 0x3b, 0x71,
0x00, 0xda, 0x0f, 0xc8, 0x80, 0x83, 0x18, 0x8b, 0x00, 0xdb, 0x20, 0x8c, 0xeb, 0xc4, 0x41, 0x9a, 0x00, 0xed, 0x07, 0x64, 0xc0, 0x41, 0x8c, 0x45, 0x80, 0x6d, 0x10, 0xc6, 0x75, 0xe2, 0x20, 0x75,
0x04, 0xe3, 0x20, 0x29, 0xda, 0x83, 0xc0, 0x8f, 0xb1, 0x4c, 0x84, 0x22, 0x55, 0x92, 0xb6, 0xa3, 0xd2, 0x71, 0x90, 0x14, 0xed, 0x41, 0xe0, 0xc7, 0x58, 0x26, 0x42, 0x91, 0x2a, 0x49, 0xdb, 0x51,
0xdc, 0x7a, 0x29, 0xd0, 0x5b, 0x10, 0xf4, 0x50, 0x14, 0x45, 0x91, 0x53, 0xff, 0x84, 0xfe, 0x05, 0x6e, 0xbd, 0x14, 0xe8, 0x2d, 0x08, 0x7a, 0x28, 0x8a, 0xa2, 0x08, 0x50, 0xa0, 0x7f, 0x42, 0xff,
0x45, 0x91, 0xa3, 0x8f, 0x45, 0x0f, 0x2c, 0x62, 0x5f, 0x5a, 0x1d, 0x75, 0xec, 0xa9, 0x98, 0x37, 0x82, 0xa2, 0xc8, 0xd1, 0xc7, 0xa2, 0x07, 0x16, 0xb1, 0x2f, 0xad, 0x8e, 0x3a, 0xf6, 0x54, 0xcc,
0xc3, 0x21, 0x65, 0x23, 0x45, 0x92, 0xfa, 0xc6, 0xf7, 0x7b, 0xbf, 0xf7, 0x24, 0xbe, 0xf9, 0xbd, 0x9b, 0xe1, 0x70, 0x64, 0x23, 0x45, 0x92, 0xfa, 0xc6, 0xf7, 0x7b, 0xbf, 0x79, 0x12, 0xdf, 0xfc,
0x37, 0x8f, 0xe8, 0x2f, 0xbe, 0x67, 0xaf, 0xb9, 0xf6, 0x5a, 0x9c, 0x44, 0xfb, 0x4e, 0x12, 0xb7, 0xde, 0x07, 0xd1, 0x5f, 0x7c, 0xcf, 0x5e, 0x75, 0xed, 0xd5, 0x38, 0x89, 0xf6, 0x9d, 0x24, 0x6e,
0xfb, 0x51, 0x98, 0x84, 0xfa, 0xa4, 0x6b, 0xaf, 0x5c, 0x8c, 0x58, 0x3f, 0x8c, 0xd7, 0x00, 0xb0, 0xf7, 0xa3, 0x30, 0x09, 0xf5, 0x49, 0xd7, 0x5e, 0xbe, 0x18, 0xd1, 0x7e, 0x18, 0xaf, 0x02, 0x60,
0xf7, 0x77, 0xd7, 0xba, 0x61, 0x37, 0x04, 0x03, 0x9e, 0x04, 0x71, 0x05, 0x77, 0xc3, 0xb0, 0xeb, 0xef, 0xef, 0xae, 0x76, 0xc3, 0x6e, 0x08, 0x06, 0x3c, 0x71, 0xe2, 0xb2, 0xd1, 0x0d, 0xc3, 0xae,
0xb3, 0x9c, 0x95, 0x78, 0x3d, 0x16, 0x27, 0x56, 0xaf, 0x2f, 0x09, 0xcb, 0x3c, 0x3f, 0x3c, 0x3a, 0x4f, 0x73, 0x56, 0xe2, 0xf5, 0x68, 0x9c, 0x58, 0xbd, 0xbe, 0x20, 0x2c, 0xb1, 0xf8, 0xf0, 0xe8,
0xa1, 0xbf, 0x66, 0xb3, 0x0c, 0xaf, 0xb2, 0x27, 0x89, 0x78, 0x24, 0xdf, 0x4c, 0xa2, 0xda, 0x96, 0x84, 0xfe, 0xaa, 0x4d, 0x33, 0xbc, 0x42, 0x1f, 0x27, 0xfc, 0x11, 0x7f, 0x3d, 0x89, 0xaa, 0x9b,
0xe7, 0xb3, 0x87, 0x2c, 0x8a, 0xbd, 0x30, 0xd0, 0xef, 0xa0, 0x99, 0x03, 0xf1, 0x68, 0x68, 0x4d, 0x9e, 0x4f, 0x1f, 0xd0, 0x28, 0xf6, 0xc2, 0x40, 0xbf, 0x83, 0x4a, 0x07, 0xfc, 0xb1, 0xae, 0x35,
0xad, 0x55, 0x5b, 0x5f, 0x68, 0x67, 0x09, 0xda, 0x0f, 0x99, 0x93, 0x84, 0x91, 0xd9, 0x7c, 0x99, 0xb5, 0x56, 0x75, 0x6d, 0xbe, 0x9d, 0x05, 0x68, 0x3f, 0xa0, 0x4e, 0x12, 0x46, 0x66, 0xf3, 0x45,
0xe2, 0x89, 0x61, 0x8a, 0x33, 0xe2, 0x28, 0xc5, 0xb3, 0x4f, 0x7a, 0xfe, 0x55, 0x22, 0x6d, 0x42, 0x6a, 0x4c, 0x0c, 0x53, 0x23, 0x23, 0x8e, 0x52, 0x63, 0xe6, 0x71, 0xcf, 0xbf, 0x8a, 0x85, 0x8d,
0x33, 0x8f, 0xbe, 0x81, 0x66, 0x5c, 0xe6, 0xb3, 0x84, 0xb9, 0xc6, 0x64, 0x53, 0x6b, 0x55, 0xcc, 0x49, 0xe6, 0xd1, 0xd7, 0x51, 0xc9, 0xa5, 0x3e, 0x4d, 0xa8, 0x5b, 0x9f, 0x6c, 0x6a, 0xad, 0xb2,
0xbf, 0xf3, 0x38, 0x09, 0xa9, 0x38, 0x69, 0x13, 0x9a, 0x79, 0xf4, 0x2b, 0x3c, 0xee, 0xc0, 0x73, 0xf9, 0x77, 0x76, 0x4e, 0x40, 0xf2, 0x9c, 0xb0, 0x31, 0xc9, 0x3c, 0xfa, 0x15, 0x76, 0xee, 0xc0,
0x58, 0x6c, 0x94, 0x9a, 0xa5, 0x56, 0xdd, 0xfc, 0x9b, 0x88, 0x03, 0x68, 0x94, 0xe2, 0xba, 0x8c, 0x73, 0x68, 0x5c, 0x2f, 0x34, 0x0b, 0xad, 0x9a, 0xf9, 0x37, 0x7e, 0x0e, 0xa0, 0x51, 0x6a, 0xd4,
0xe3, 0x36, 0x84, 0x81, 0x43, 0xa7, 0x68, 0xde, 0x0b, 0x0e, 0x2c, 0xdf, 0x73, 0x3b, 0x59, 0x78, 0xc4, 0x39, 0x66, 0xc3, 0x31, 0x70, 0xe8, 0x04, 0xcd, 0x79, 0xc1, 0x81, 0xe5, 0x7b, 0x6e, 0x27,
0x19, 0xc2, 0xff, 0x35, 0x4c, 0xf1, 0x9c, 0x74, 0x6d, 0xaa, 0x2c, 0x17, 0x20, 0xcb, 0x18, 0x4c, 0x3b, 0x5e, 0x84, 0xe3, 0xff, 0x1a, 0xa6, 0xc6, 0xac, 0x70, 0x6d, 0xc8, 0x28, 0x17, 0x20, 0xca,
0xe8, 0x29, 0x1a, 0xf9, 0x54, 0x43, 0x35, 0x59, 0x9c, 0x3b, 0x5e, 0x9c, 0xe8, 0x3e, 0xaa, 0xc8, 0x18, 0x8c, 0xc9, 0x29, 0x1a, 0xfe, 0x44, 0x43, 0x55, 0x91, 0x9c, 0x3b, 0x5e, 0x9c, 0xe8, 0x3e,
0xb7, 0x8b, 0x0d, 0xad, 0x59, 0x6a, 0xd5, 0xd6, 0xe7, 0xdb, 0xae, 0xdd, 0x2e, 0xd4, 0xd0, 0xbc, 0x2a, 0x8b, 0xb7, 0x8b, 0xeb, 0x5a, 0xb3, 0xd0, 0xaa, 0xae, 0xcd, 0xb5, 0x5d, 0xbb, 0xad, 0xe4,
0xc6, 0x0b, 0x74, 0x9c, 0xe2, 0x1a, 0xb5, 0x0e, 0x25, 0x16, 0x0f, 0x53, 0xac, 0xe2, 0xce, 0x14, 0xd0, 0xbc, 0xc6, 0x12, 0x74, 0x9c, 0x1a, 0x55, 0x62, 0x1d, 0x0a, 0x2c, 0x1e, 0xa6, 0x86, 0x3c,
0xec, 0xf9, 0xd1, 0x6a, 0x91, 0x4b, 0x15, 0xf3, 0x6a, 0xf9, 0xcb, 0x17, 0x78, 0x82, 0x1c, 0xd5, 0x77, 0x26, 0x61, 0xcf, 0x8e, 0x56, 0x54, 0x2e, 0x91, 0xcc, 0xab, 0xc5, 0x2f, 0x9e, 0x1b, 0x13,
0xd0, 0x22, 0xff, 0x81, 0xed, 0x60, 0x37, 0x7c, 0x10, 0xed, 0x07, 0x8e, 0xc5, 0x8b, 0xf4, 0x6f, 0xf8, 0x9b, 0x1a, 0x5a, 0x60, 0x3f, 0xb0, 0x15, 0xec, 0x86, 0xf7, 0xa3, 0xfd, 0xc0, 0xb1, 0x58,
0x54, 0x0e, 0xac, 0x1e, 0x83, 0x73, 0xaa, 0x9a, 0xcb, 0xc3, 0x14, 0x83, 0x3d, 0x4a, 0x31, 0x82, 0x92, 0xfe, 0x8d, 0x8a, 0x81, 0xd5, 0xa3, 0x70, 0x4f, 0x15, 0x73, 0x69, 0x98, 0x1a, 0x60, 0x8f,
0xec, 0xdc, 0x20, 0x14, 0x30, 0xce, 0x8d, 0xbd, 0xa7, 0xcc, 0x28, 0x35, 0xb5, 0x56, 0x49, 0x70, 0x52, 0x03, 0x41, 0x74, 0x66, 0x60, 0x02, 0x18, 0xe3, 0xc6, 0xde, 0x13, 0x5a, 0x2f, 0x34, 0xb5,
0xb9, 0xad, 0xb8, 0xdc, 0x20, 0x14, 0x30, 0xfd, 0x1a, 0x42, 0xbd, 0xd0, 0xf5, 0x76, 0x3d, 0xe6, 0x56, 0x81, 0x73, 0x99, 0x2d, 0xb9, 0xcc, 0xc0, 0x04, 0x30, 0xfd, 0x1a, 0x42, 0xbd, 0xd0, 0xf5,
0x76, 0x62, 0x63, 0x0a, 0x22, 0x9a, 0xc3, 0x14, 0x57, 0x33, 0x74, 0x67, 0x94, 0xe2, 0x79, 0x08, 0x76, 0x3d, 0xea, 0x76, 0xe2, 0xfa, 0x14, 0x9c, 0x68, 0x0e, 0x53, 0xa3, 0x92, 0xa1, 0x3b, 0xa3,
0x53, 0x08, 0xa1, 0xb9, 0x57, 0xff, 0x4e, 0x43, 0x35, 0x95, 0xc1, 0x1e, 0x18, 0xf5, 0xa6, 0xd6, 0xd4, 0x98, 0x83, 0x63, 0x12, 0xc1, 0x24, 0xf7, 0xea, 0xdf, 0x69, 0xa8, 0x2a, 0x23, 0xd8, 0x83,
0x2a, 0x9b, 0x5f, 0x68, 0xbc, 0x2c, 0x3f, 0xa5, 0xf8, 0x72, 0xd7, 0x4b, 0xf6, 0xf6, 0xed, 0xb6, 0x7a, 0xad, 0xa9, 0xb5, 0x8a, 0xe6, 0xe7, 0x1a, 0x4b, 0xcb, 0x4f, 0xa9, 0x71, 0xb9, 0xeb, 0x25,
0x13, 0xf6, 0xd6, 0xe2, 0x41, 0xe0, 0x24, 0x7b, 0x5e, 0xd0, 0x2d, 0x3c, 0x15, 0x45, 0xdb, 0xde, 0x7b, 0xfb, 0x76, 0xdb, 0x09, 0x7b, 0xab, 0xf1, 0x20, 0x70, 0x92, 0x3d, 0x2f, 0xe8, 0x2a, 0x4f,
0xd9, 0x0b, 0xa3, 0x64, 0x7b, 0x73, 0x98, 0x62, 0xf5, 0xa7, 0xcc, 0xc1, 0x28, 0xc5, 0x0b, 0x63, 0xaa, 0x68, 0xdb, 0x3b, 0x7b, 0x61, 0x94, 0x6c, 0x6d, 0x0c, 0x53, 0x43, 0xfe, 0x29, 0x73, 0x30,
0xbf, 0x6f, 0x0e, 0xc8, 0x57, 0x47, 0xab, 0xef, 0x92, 0x98, 0x16, 0xd2, 0x16, 0xc5, 0x5f, 0xfd, 0x4a, 0x8d, 0xf9, 0xb1, 0xdf, 0x37, 0x07, 0xf8, 0xcb, 0xa3, 0x95, 0xb7, 0x09, 0x4c, 0x94, 0xb0,
0xf3, 0xe2, 0xbf, 0x8a, 0x2a, 0x31, 0xfb, 0x64, 0x9f, 0x05, 0x0e, 0x33, 0x10, 0x54, 0xb1, 0xc1, 0xaa, 0xf8, 0x2b, 0x7f, 0x5e, 0xfc, 0x57, 0x51, 0x39, 0xa6, 0x1f, 0xef, 0xd3, 0xc0, 0xa1, 0x75,
0x55, 0x90, 0x61, 0xa3, 0x14, 0xcf, 0x89, 0xda, 0x4b, 0x80, 0x50, 0xe5, 0xd3, 0xef, 0xa1, 0xb9, 0x04, 0x59, 0x6c, 0x30, 0x15, 0x64, 0xd8, 0x28, 0x35, 0x66, 0x79, 0xee, 0x05, 0x80, 0x89, 0xf4,
0x78, 0xd0, 0xf3, 0xbd, 0xe0, 0x71, 0x27, 0xb1, 0xa2, 0x2e, 0x4b, 0x8c, 0x45, 0x38, 0xe5, 0xd6, 0xe9, 0x77, 0xd1, 0x6c, 0x3c, 0xe8, 0xf9, 0x5e, 0xf0, 0xa8, 0x93, 0x58, 0x51, 0x97, 0x26, 0xf5,
0x30, 0xc5, 0xb3, 0xd2, 0xf3, 0x00, 0x1c, 0x4a, 0xc7, 0x63, 0x28, 0xa1, 0xe3, 0x2c, 0xfd, 0x06, 0x05, 0xb8, 0xe5, 0xd6, 0x30, 0x35, 0x66, 0x84, 0xe7, 0x3e, 0x38, 0xa4, 0x8e, 0xc7, 0x50, 0x4c,
0xaa, 0xd9, 0x7e, 0xe8, 0x3c, 0x8e, 0x3b, 0x7b, 0x56, 0xbc, 0x67, 0xe8, 0x4d, 0xad, 0x55, 0x37, 0xc6, 0x59, 0xfa, 0x0d, 0x54, 0xb5, 0xfd, 0xd0, 0x79, 0x14, 0x77, 0xf6, 0xac, 0x78, 0xaf, 0xae,
0x09, 0x2f, 0xab, 0x80, 0x6f, 0x59, 0xf1, 0x9e, 0x2a, 0x6b, 0x0e, 0x11, 0x5a, 0xf0, 0xeb, 0xef, 0x37, 0xb5, 0x56, 0xcd, 0xc4, 0x2c, 0xad, 0x1c, 0xbe, 0x65, 0xc5, 0x7b, 0x32, 0xad, 0x39, 0x84,
0xa1, 0x2a, 0x0b, 0x9c, 0x68, 0xd0, 0xe7, 0x0d, 0x7d, 0x01, 0x52, 0x80, 0x30, 0x14, 0xa8, 0x84, 0x89, 0xe2, 0xd7, 0xdf, 0x41, 0x15, 0x1a, 0x38, 0xd1, 0xa0, 0xcf, 0x0a, 0xfa, 0x02, 0x84, 0x00,
0xa1, 0x10, 0x42, 0x73, 0xaf, 0x6e, 0xa2, 0x72, 0x32, 0xe8, 0x33, 0x98, 0x05, 0x73, 0xeb, 0xcb, 0x61, 0x48, 0x50, 0x0a, 0x43, 0x22, 0x98, 0xe4, 0x5e, 0xdd, 0x44, 0xc5, 0x64, 0xd0, 0xa7, 0xd0,
0x79, 0x71, 0x95, 0xb8, 0x07, 0x7d, 0x26, 0xd4, 0xc9, 0x79, 0x4a, 0x9d, 0xdc, 0x20, 0x14, 0x30, 0x0b, 0x66, 0xd7, 0x96, 0xf2, 0xe4, 0x4a, 0x71, 0x0f, 0xfa, 0x94, 0xab, 0x93, 0xf1, 0xa4, 0x3a,
0x7d, 0x0b, 0xd5, 0xfa, 0x2c, 0xea, 0x79, 0xb1, 0x68, 0xc1, 0x72, 0x53, 0x6b, 0xcd, 0x9a, 0xab, 0x99, 0x81, 0x09, 0x60, 0xfa, 0x26, 0xaa, 0xf6, 0x69, 0xd4, 0xf3, 0x62, 0x5e, 0x82, 0xc5, 0xa6,
0xc3, 0x14, 0x17, 0xe1, 0x51, 0x8a, 0x17, 0x21, 0xb2, 0x80, 0x11, 0x5a, 0x64, 0xe8, 0xb7, 0x0b, 0xd6, 0x9a, 0x31, 0x57, 0x86, 0xa9, 0xa1, 0xc2, 0xa3, 0xd4, 0x58, 0x80, 0x93, 0x0a, 0x86, 0x89,
0x1a, 0x0d, 0x62, 0xa3, 0xd6, 0xd4, 0x5a, 0x53, 0x30, 0x27, 0x94, 0x20, 0xee, 0xc6, 0x67, 0x74, 0xca, 0xd0, 0x6f, 0x2b, 0x1a, 0x0d, 0xe2, 0x7a, 0xb5, 0xa9, 0xb5, 0xa6, 0xa0, 0x4f, 0x48, 0x41,
0x76, 0x37, 0x26, 0xbf, 0xa5, 0xb8, 0xe4, 0x05, 0x09, 0x2d, 0xd0, 0xf4, 0x5d, 0x24, 0xaa, 0xd4, 0x6c, 0xc7, 0x67, 0x74, 0xb6, 0x1d, 0xe3, 0xdf, 0x52, 0xa3, 0xe0, 0x05, 0x09, 0x51, 0x68, 0xfa,
0x81, 0x1e, 0x9b, 0x85, 0x54, 0x37, 0x8f, 0x53, 0x5c, 0xa7, 0xd6, 0xa1, 0xc9, 0x1d, 0x3b, 0xde, 0x2e, 0xe2, 0x59, 0xea, 0x40, 0x8d, 0xcd, 0x40, 0xa8, 0x9b, 0xc7, 0xa9, 0x51, 0x23, 0xd6, 0xa1,
0x53, 0xc6, 0x0b, 0x65, 0x67, 0x86, 0x2a, 0x94, 0x42, 0xb2, 0xc4, 0xcf, 0x8f, 0x56, 0xc7, 0xc2, 0xc9, 0x1c, 0x3b, 0xde, 0x13, 0xca, 0x12, 0x65, 0x67, 0x86, 0x4c, 0x94, 0x44, 0xb2, 0xc0, 0xcf,
0x68, 0x1e, 0xa4, 0x3f, 0x44, 0x95, 0xbe, 0x6f, 0x25, 0xbb, 0x61, 0xd4, 0x33, 0xe6, 0x40, 0xa0, 0x8e, 0x56, 0xc6, 0x8e, 0x91, 0xfc, 0x90, 0xfe, 0x00, 0x95, 0xfb, 0xbe, 0x95, 0xec, 0x86, 0x51,
0x85, 0x1a, 0xde, 0x97, 0x9e, 0x4d, 0x2b, 0xb1, 0x4c, 0x22, 0x65, 0xaa, 0xf8, 0x4a, 0x6d, 0x19, 0xaf, 0x3e, 0x0b, 0x02, 0x55, 0x72, 0x78, 0x4f, 0x78, 0x36, 0xac, 0xc4, 0x32, 0xb1, 0x90, 0xa9,
0x40, 0xa8, 0xf2, 0xe9, 0x9b, 0xa8, 0xe6, 0x87, 0x8e, 0xe5, 0x77, 0x76, 0x7d, 0xab, 0x1b, 0x1b, 0xe4, 0x4b, 0xb5, 0x65, 0x00, 0x26, 0xd2, 0xa7, 0x6f, 0xa0, 0xaa, 0x1f, 0x3a, 0x96, 0xdf, 0xd9,
0xbf, 0xcc, 0x40, 0x51, 0x41, 0x1d, 0x80, 0x6f, 0x71, 0x58, 0x15, 0x23, 0x87, 0x08, 0x2d, 0xf8, 0xf5, 0xad, 0x6e, 0x5c, 0xff, 0xa5, 0x04, 0x49, 0x05, 0x75, 0x00, 0xbe, 0xc9, 0x60, 0x99, 0x8c,
0xf5, 0x5b, 0xa8, 0x2e, 0xa5, 0x2f, 0x34, 0xf6, 0xeb, 0x0c, 0x28, 0x04, 0xce, 0x46, 0x3a, 0xa4, 0x1c, 0xc2, 0x44, 0xf1, 0xeb, 0xb7, 0x50, 0x4d, 0x48, 0x9f, 0x6b, 0xec, 0xd7, 0x12, 0x28, 0x04,
0xca, 0x16, 0x8b, 0x1d, 0x23, 0x64, 0x56, 0x64, 0x14, 0xaf, 0x8d, 0xe9, 0xb7, 0xb9, 0x36, 0x28, 0xee, 0x46, 0x38, 0x84, 0xca, 0x16, 0xd4, 0x8a, 0xe1, 0x32, 0x53, 0x19, 0xfa, 0xfb, 0xac, 0x8f,
0x9a, 0x91, 0xd3, 0xdb, 0x98, 0x81, 0xb8, 0xff, 0x1d, 0xa7, 0x18, 0x51, 0xeb, 0x70, 0x5b, 0xa0, 0x87, 0x2e, 0xed, 0x38, 0x7b, 0x56, 0xd0, 0xa5, 0xec, 0x7e, 0x86, 0x25, 0xa8, 0x20, 0xd0, 0x3f,
0x3c, 0x8b, 0x24, 0xa8, 0x2c, 0xd2, 0xe6, 0x33, 0xb8, 0xc0, 0xa4, 0x19, 0x8f, 0x77, 0x62, 0x10, 0xf8, 0x6e, 0x80, 0x6b, 0x5b, 0xed, 0xe3, 0x0a, 0x8a, 0xc9, 0x38, 0x4b, 0x9d, 0x44, 0xd3, 0x6f,
0x76, 0x8a, 0x92, 0xab, 0x40, 0x6a, 0xe8, 0xc4, 0x20, 0xbc, 0x3f, 0x26, 0x3a, 0xd1, 0x89, 0x63, 0x32, 0x89, 0x08, 0x2a, 0x89, 0x81, 0x50, 0x2f, 0xc1, 0xb9, 0xff, 0x1d, 0xa7, 0x06, 0x22, 0xd6,
0x28, 0xa1, 0xe3, 0x2c, 0x39, 0xd2, 0x1f, 0xa1, 0x2a, 0x1c, 0x31, 0xdc, 0x29, 0xb7, 0xd1, 0xb4, 0xe1, 0x16, 0x47, 0x59, 0x14, 0x41, 0x90, 0x51, 0x84, 0xcd, 0xda, 0xba, 0xc2, 0x24, 0x19, 0x8f,
0xe8, 0x32, 0x79, 0xa3, 0x5c, 0xc8, 0x4f, 0x15, 0x48, 0xbc, 0x35, 0xcc, 0x7f, 0xc8, 0x23, 0x95, 0x15, 0x77, 0x10, 0x76, 0x54, 0x15, 0x97, 0x21, 0x34, 0xbc, 0x5c, 0x10, 0xde, 0x1b, 0xd3, 0x31,
0xd4, 0x51, 0x8a, 0x6b, 0xb9, 0x82, 0x08, 0x95, 0x30, 0xf9, 0x56, 0x43, 0x4b, 0xdb, 0x81, 0xeb, 0x7f, 0xb9, 0x31, 0x14, 0x93, 0x71, 0x96, 0x98, 0x12, 0x0f, 0x51, 0x05, 0x54, 0x03, 0x63, 0xea,
0x45, 0xcc, 0x49, 0x64, 0x3d, 0x59, 0x7c, 0x2f, 0xf0, 0x07, 0xe7, 0x33, 0x02, 0xce, 0xed, 0x90, 0x36, 0x9a, 0xe6, 0x85, 0x2b, 0x86, 0xd4, 0x85, 0x5c, 0x28, 0x40, 0x62, 0xd5, 0x66, 0xfe, 0x43,
0xc9, 0xd7, 0x65, 0x34, 0x7d, 0x23, 0xdc, 0x0f, 0x92, 0x58, 0xbf, 0x82, 0xa6, 0x76, 0x3d, 0x9f, 0xa8, 0x44, 0x50, 0x47, 0xa9, 0x51, 0xcd, 0x45, 0x89, 0x89, 0x80, 0xf1, 0xb7, 0x1a, 0x5a, 0xdc,
0xc5, 0x70, 0x95, 0x4d, 0x99, 0x78, 0x98, 0x62, 0x01, 0xa8, 0x97, 0x04, 0x4b, 0xf5, 0x9e, 0x70, 0x0a, 0x5c, 0x2f, 0xa2, 0x4e, 0x22, 0xae, 0x88, 0xc6, 0x77, 0x03, 0x7f, 0x70, 0x3e, 0x5d, 0xe5,
0xea, 0x1f, 0xa0, 0x9a, 0x78, 0xcf, 0x30, 0xf2, 0x58, 0x0c, 0x53, 0x65, 0xca, 0xfc, 0x0f, 0xff, 0xdc, 0x74, 0x83, 0xbf, 0x2a, 0xa2, 0xe9, 0x1b, 0xe1, 0x7e, 0x90, 0xc4, 0xfa, 0x15, 0x34, 0xb5,
0x27, 0x05, 0x58, 0xfd, 0x93, 0x02, 0xa6, 0x12, 0x15, 0x89, 0xfa, 0x75, 0x54, 0x91, 0x33, 0x33, 0xeb, 0xf9, 0x34, 0x86, 0xe9, 0x38, 0x65, 0x1a, 0xc3, 0xd4, 0xe0, 0x80, 0x7c, 0x49, 0xb0, 0x64,
0x86, 0x7b, 0x72, 0xca, 0xbc, 0x04, 0xf3, 0x5a, 0x62, 0xf9, 0xbc, 0x96, 0x80, 0xca, 0xa2, 0x28, 0x39, 0x73, 0xa7, 0xfe, 0x1e, 0xaa, 0xf2, 0xf7, 0x0c, 0x23, 0x8f, 0xc6, 0xd0, 0xa8, 0xa6, 0xcc,
0xfa, 0xff, 0x73, 0xe1, 0x96, 0x21, 0xc3, 0xc5, 0x3f, 0x12, 0x6e, 0x16, 0xaf, 0xf4, 0xdb, 0x46, 0xff, 0xb0, 0x7f, 0xa2, 0xc0, 0xf2, 0x9f, 0x28, 0x98, 0x0c, 0xa4, 0x12, 0xf5, 0xeb, 0xa8, 0x2c,
0x53, 0xf6, 0x20, 0x61, 0xd9, 0xa5, 0x6b, 0xf0, 0x3a, 0x00, 0x90, 0x1f, 0x36, 0xb7, 0x08, 0x15, 0xda, 0x70, 0x0c, 0xa3, 0x77, 0xca, 0xbc, 0x04, 0x23, 0x40, 0x60, 0xf9, 0x08, 0x10, 0x80, 0x8c,
0xe8, 0xd8, 0x0d, 0x33, 0xfd, 0x96, 0x37, 0xcc, 0x0e, 0xaa, 0x8a, 0x1d, 0xa9, 0xe3, 0xb9, 0x70, 0x22, 0x29, 0xfa, 0xff, 0x73, 0xe1, 0x16, 0x21, 0xc2, 0xc5, 0x3f, 0x12, 0x6e, 0x76, 0x5e, 0xea,
0xb9, 0xd4, 0xcd, 0x8d, 0xe3, 0x14, 0x57, 0xc4, 0xde, 0x03, 0x37, 0x6e, 0x45, 0x10, 0xb6, 0x5d, 0xb7, 0x8d, 0xa6, 0xec, 0x41, 0x42, 0xb3, 0x39, 0x5e, 0x67, 0x79, 0x00, 0x20, 0xbf, 0x6c, 0x66,
0x95, 0x28, 0x03, 0x78, 0xb7, 0x28, 0x26, 0x55, 0x3c, 0x2e, 0xb1, 0xe2, 0x20, 0xd1, 0xdf, 0x65, 0x61, 0xc2, 0xd1, 0xb1, 0xa1, 0x35, 0xfd, 0x86, 0x43, 0x6b, 0x07, 0x55, 0xf8, 0xda, 0xd5, 0xf1,
0x8e, 0xc8, 0x06, 0xf9, 0x4c, 0x43, 0x55, 0x21, 0x8f, 0x1d, 0x96, 0xe8, 0xd7, 0xd1, 0xb4, 0x03, 0x5c, 0x98, 0x57, 0x35, 0x73, 0xfd, 0x38, 0x35, 0xca, 0x7c, 0x95, 0x82, 0x21, 0x5e, 0xe6, 0x84,
0x86, 0xec, 0x10, 0xc4, 0x77, 0x2e, 0xe1, 0xce, 0x1b, 0x43, 0x30, 0x54, 0xad, 0xc0, 0x24, 0x54, 0x2d, 0x57, 0x06, 0xca, 0x00, 0x56, 0x2d, 0x92, 0x49, 0x24, 0x8f, 0x49, 0x4c, 0xed, 0x4d, 0xfa,
0xc2, 0x7c, 0xa8, 0x38, 0x11, 0xb3, 0xb2, 0x5d, 0xb4, 0x24, 0x86, 0x8a, 0x84, 0xd4, 0xd9, 0x48, 0xdb, 0xb4, 0x26, 0x51, 0x20, 0x9f, 0x6a, 0xa8, 0xc2, 0xe5, 0xb1, 0x43, 0x13, 0xfd, 0x3a, 0x9a,
0x9b, 0xd0, 0xcc, 0x43, 0x3e, 0x9f, 0x44, 0x4b, 0x85, 0xed, 0x6e, 0x93, 0xf5, 0x23, 0x26, 0x16, 0x76, 0xc0, 0x10, 0x15, 0x82, 0xd8, 0x1a, 0xc7, 0xdd, 0x79, 0x61, 0x70, 0x86, 0xcc, 0x15, 0x98,
0xb0, 0xf3, 0xdd, 0x95, 0xd7, 0xd1, 0xb4, 0xa8, 0x23, 0xfc, 0xbd, 0xba, 0xb9, 0xc2, 0x5f, 0x49, 0x98, 0x08, 0x98, 0x35, 0x15, 0x27, 0xa2, 0x56, 0xb6, 0xde, 0x16, 0x78, 0x53, 0x11, 0x90, 0xbc,
0x20, 0x67, 0x36, 0x5e, 0x89, 0xf3, 0x77, 0xca, 0x06, 0x5e, 0x29, 0x1f, 0x94, 0xaf, 0x1b, 0x71, 0x1b, 0x61, 0x63, 0x92, 0x79, 0xf0, 0x67, 0x93, 0x68, 0x51, 0x59, 0x18, 0x37, 0x68, 0x3f, 0xa2,
0xf9, 0x50, 0xdb, 0x18, 0xd7, 0xe9, 0x9b, 0x0e, 0x58, 0x72, 0x88, 0x96, 0x0a, 0xbb, 0x70, 0xa1, 0x7c, 0xa7, 0x3b, 0xdf, 0xf5, 0x7b, 0x0d, 0x4d, 0xf3, 0x3c, 0xc2, 0xdf, 0xab, 0x99, 0xcb, 0xec,
0x14, 0x1f, 0x9e, 0xd9, 0x8a, 0xff, 0x7a, 0x6a, 0x2b, 0xce, 0xc9, 0xe6, 0x3f, 0xb3, 0xcb, 0xe9, 0x95, 0x38, 0x72, 0x66, 0x89, 0x16, 0x38, 0x7b, 0xa7, 0xac, 0xe1, 0x15, 0xf2, 0x46, 0xf9, 0xaa,
0xb5, 0x0b, 0xf1, 0x99, 0x0d, 0xf8, 0x87, 0x49, 0x34, 0x77, 0xcf, 0x8e, 0x59, 0x74, 0xc0, 0xdc, 0x16, 0x97, 0x37, 0xb5, 0xf5, 0x71, 0x9d, 0xbe, 0x6e, 0x83, 0xc5, 0x87, 0x68, 0x51, 0x59, 0xaf,
0xad, 0xd0, 0x77, 0x59, 0xa4, 0xdf, 0x45, 0x65, 0xfe, 0xbd, 0x23, 0x4b, 0xbf, 0xd2, 0x16, 0x1f, 0x95, 0x54, 0x7c, 0x70, 0x66, 0xd1, 0xfe, 0xeb, 0xa9, 0x45, 0x3b, 0x27, 0x9b, 0xff, 0xcc, 0xe6,
0x43, 0xed, 0xec, 0x63, 0xa8, 0xfd, 0x20, 0xfb, 0x18, 0x32, 0x1b, 0xf2, 0xf7, 0x80, 0x9f, 0x2f, 0xdd, 0x2b, 0x77, 0xec, 0x33, 0x4b, 0xf5, 0x0f, 0x93, 0x68, 0xf6, 0xae, 0x1d, 0xd3, 0xe8, 0x80,
0x15, 0x5e, 0x8f, 0x91, 0x67, 0x3f, 0x63, 0x8d, 0x02, 0xce, 0x9b, 0xcf, 0xb7, 0x6c, 0xe6, 0x43, 0xba, 0x9b, 0xa1, 0xef, 0xd2, 0x48, 0xdf, 0x46, 0x45, 0xf6, 0x09, 0x25, 0x52, 0xbf, 0xdc, 0xe6,
0xf9, 0xab, 0xa2, 0xf9, 0x00, 0x50, 0x82, 0x02, 0x8b, 0x50, 0x81, 0xea, 0x1f, 0xa3, 0xc5, 0x88, 0xdf, 0x57, 0xed, 0xec, 0xfb, 0xaa, 0x7d, 0x3f, 0xfb, 0xbe, 0x32, 0x1b, 0xe2, 0xf7, 0x80, 0x9f,
0x39, 0xcc, 0x3b, 0x60, 0x9d, 0x7c, 0x29, 0x12, 0xa7, 0xd0, 0x1e, 0xa6, 0x78, 0x41, 0x3a, 0xdf, 0xef, 0x29, 0x5e, 0x8f, 0xe2, 0xa7, 0x3f, 0x1b, 0x1a, 0x01, 0x9c, 0x15, 0x9f, 0x6f, 0xd9, 0xd4,
0x2f, 0xec, 0x46, 0xcb, 0x90, 0xe6, 0xb4, 0x83, 0xd0, 0x33, 0x5c, 0xfd, 0x11, 0x5a, 0x88, 0x58, 0x87, 0xf4, 0x57, 0x78, 0xf1, 0x01, 0x20, 0x05, 0x05, 0x16, 0x26, 0x1c, 0xd5, 0x3f, 0x42, 0x0b,
0x2f, 0x4c, 0x8a, 0xb9, 0xc5, 0x49, 0xfd, 0x77, 0x98, 0xe2, 0x79, 0xe1, 0x2b, 0xa6, 0x5e, 0x92, 0x11, 0x75, 0xa8, 0x77, 0x40, 0x3b, 0xf9, 0x9e, 0xc5, 0x6f, 0xa1, 0x3d, 0x4c, 0x8d, 0x79, 0xe1,
0xa9, 0xc7, 0x70, 0x42, 0x4f, 0x33, 0xc9, 0xf7, 0x5a, 0x5e, 0x48, 0xd1, 0xc0, 0xe7, 0x5e, 0xc8, 0x7c, 0x57, 0x59, 0xb7, 0x96, 0x20, 0xcc, 0x69, 0x07, 0x26, 0x67, 0xb8, 0xfa, 0x43, 0x34, 0x1f,
0xec, 0xbb, 0x64, 0xf2, 0x0d, 0xbe, 0x4b, 0x36, 0xd0, 0x8c, 0xe5, 0xba, 0x11, 0x8b, 0xc5, 0xc8, 0xd1, 0x5e, 0x98, 0xa8, 0xb1, 0xf9, 0x4d, 0xfd, 0x77, 0x98, 0x1a, 0x73, 0xdc, 0xa7, 0x86, 0x5e,
0xad, 0x0a, 0x21, 0x4a, 0x48, 0xc9, 0x42, 0xda, 0x84, 0x66, 0x1e, 0xf3, 0xe6, 0xcb, 0x57, 0x8d, 0x14, 0xa1, 0xc7, 0x70, 0x4c, 0x4e, 0x33, 0xf1, 0xf7, 0x5a, 0x9e, 0x48, 0x5e, 0xc0, 0xe7, 0x9e,
0x89, 0xa3, 0x57, 0x8d, 0x89, 0x97, 0xc7, 0x0d, 0xed, 0xe8, 0xb8, 0xa1, 0x3d, 0x3b, 0x69, 0x4c, 0xc8, 0xec, 0x53, 0x67, 0xf2, 0x35, 0x3e, 0x75, 0xd6, 0x51, 0xc9, 0x72, 0xdd, 0x88, 0xc6, 0xbc,
0xbc, 0x38, 0x69, 0x68, 0x47, 0x27, 0x8d, 0x89, 0x1f, 0x4f, 0x1a, 0x13, 0x1f, 0x5d, 0x7a, 0x83, 0xe5, 0x56, 0xb8, 0x10, 0x05, 0x24, 0x65, 0x21, 0x6c, 0x4c, 0x32, 0x8f, 0x79, 0xf3, 0xc5, 0xcb,
0x8f, 0x01, 0xd7, 0xb6, 0xa7, 0xe1, 0x35, 0x2f, 0xff, 0x1e, 0x00, 0x00, 0xff, 0xff, 0x95, 0x1d, 0xc6, 0xc4, 0xd1, 0xcb, 0xc6, 0xc4, 0x8b, 0xe3, 0x86, 0x76, 0x74, 0xdc, 0xd0, 0x9e, 0x9e, 0x34,
0x77, 0x00, 0x8b, 0x0f, 0x00, 0x00, 0x26, 0x9e, 0x9f, 0x34, 0xb4, 0xa3, 0x93, 0xc6, 0xc4, 0x8f, 0x27, 0x8d, 0x89, 0x0f, 0x2f, 0xbd,
0xc6, 0xf7, 0x85, 0x6b, 0xdb, 0xd3, 0xf0, 0x9a, 0x97, 0x7f, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x52,
0x5f, 0x14, 0xfe, 0xde, 0x0f, 0x00, 0x00,
} }
func (m *FileVersion) Marshal() (dAtA []byte, err error) { func (m *FileVersion) Marshal() (dAtA []byte, err error) {
@ -712,6 +715,13 @@ func (m *FileInfoTruncated) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = l
if m.InodeChangeNs != 0 {
i = encodeVarintStructs(dAtA, i, uint64(m.InodeChangeNs))
i--
dAtA[i] = 0x3e
i--
dAtA[i] = 0xd0
}
if len(m.VersionHash) > 0 { if len(m.VersionHash) > 0 {
i -= len(m.VersionHash) i -= len(m.VersionHash)
copy(dAtA[i:], m.VersionHash) copy(dAtA[i:], m.VersionHash)
@ -1362,6 +1372,9 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
if l > 0 { if l > 0 {
n += 2 + l + sovStructs(uint64(l)) n += 2 + l + sovStructs(uint64(l))
} }
if m.InodeChangeNs != 0 {
n += 2 + sovStructs(uint64(m.InodeChangeNs))
}
return n return n
} }
@ -2274,6 +2287,25 @@ func (m *FileInfoTruncated) Unmarshal(dAtA []byte) error {
m.VersionHash = []byte{} m.VersionHash = []byte{}
} }
iNdEx = postIndex iNdEx = postIndex
case 1002:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field InodeChangeNs", wireType)
}
m.InodeChangeNs = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowStructs
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.InodeChangeNs |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipStructs(dAtA[iNdEx:]) skippy, err := skipStructs(dAtA[iNdEx:])

View File

@ -0,0 +1,22 @@
// 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 darwin || freebsd || netbsd
// +build darwin freebsd netbsd
package fs
import (
"syscall"
"time"
)
func (fi basicFileInfo) InodeChangeTime() time.Time {
if sys, ok := fi.FileInfo.Sys().(*syscall.Stat_t); ok {
return time.Unix(0, sys.Ctimespec.Nano())
}
return time.Time{}
}

View File

@ -0,0 +1,22 @@
// 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 aix || dragonfly || linux || openbsd || solaris || illumos
// +build aix dragonfly linux openbsd solaris illumos
package fs
import (
"syscall"
"time"
)
func (fi basicFileInfo) InodeChangeTime() time.Time {
if sys, ok := fi.FileInfo.Sys().(*syscall.Stat_t); ok {
return time.Unix(0, sys.Ctim.Nano())
}
return time.Time{}
}

View File

@ -10,6 +10,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
var execExts map[string]bool var execExts map[string]bool
@ -57,6 +58,10 @@ func (e basicFileInfo) Group() int {
return -1 return -1
} }
func (basicFileInfo) InodeChangeTime() time.Time {
return time.Time{}
}
// osFileInfo converts e to os.FileInfo that is suitable // osFileInfo converts e to os.FileInfo that is suitable
// to be passed to os.SameFile. // to be passed to os.SameFile.
func (e *basicFileInfo) osFileInfo() os.FileInfo { func (e *basicFileInfo) osFileInfo() os.FileInfo {

View File

@ -13,6 +13,6 @@ import (
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) { func (f *BasicFilesystem) PlatformData(name string, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
return unixPlatformData(f, name, f.userCache, f.groupCache) return unixPlatformData(f, name, f.userCache, f.groupCache, scanOwnership, scanXattrs, xattrFilter)
} }

View File

@ -13,7 +13,12 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) { func (f *BasicFilesystem) PlatformData(name string, scanOwnership, _ bool, _ XattrFilter) (protocol.PlatformData, error) {
if !scanOwnership {
// That's the only thing we do, currently
return protocol.PlatformData{}, nil
}
rootedName, err := f.rooted(name) rootedName, err := f.rooted(name)
if err != nil { if err != nil {
return protocol.PlatformData{}, fmt.Errorf("rooted for %s: %w", name, err) return protocol.PlatformData{}, fmt.Errorf("rooted for %s: %w", name, err)

View File

@ -7,6 +7,9 @@
package fs package fs
import ( import (
"bytes"
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -16,6 +19,7 @@ import (
"time" "time"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/rand"
) )
@ -565,6 +569,87 @@ func TestRel(t *testing.T) {
} }
} }
func TestXattr(t *testing.T) {
tfs, _ := setup(t)
if err := tfs.Mkdir("/test", 0755); err != nil {
t.Fatal(err)
}
xattrSize := func() int { return 20 + rand.Intn(20) }
// Create a set of random attributes that we will set and read back
var attrs []protocol.Xattr
for i := 0; i < 10; i++ {
key := fmt.Sprintf("user.test-%d", i)
value := make([]byte, xattrSize())
rand.Read(value)
attrs = append(attrs, protocol.Xattr{
Name: key,
Value: value,
})
}
// Set the xattrs, read them back and compare
if err := tfs.SetXattr("/test", attrs, noopXattrFilter{}); errors.Is(err, ErrXattrsNotSupported) {
t.Skip("xattrs not supported")
} else if err != nil {
t.Fatal(err)
}
res, err := tfs.GetXattr("/test", noopXattrFilter{})
if err != nil {
t.Fatal(err)
}
if len(res) != len(attrs) {
t.Fatalf("length of returned xattrs does not match (%d != %d)", len(res), len(attrs))
}
for i, xa := range res {
if xa.Name != attrs[i].Name {
t.Errorf("xattr name %q != %q", xa.Name, attrs[i].Name)
}
if !bytes.Equal(xa.Value, attrs[i].Value) {
t.Errorf("xattr value %q != %q", xa.Value, attrs[i].Value)
}
}
// Remove a couple, change a couple, and add another couple of
// attributes. Replacing the xattrs again should work.
attrs = attrs[2:]
attrs[1].Value = make([]byte, xattrSize())
rand.Read(attrs[1].Value)
attrs[3].Value = make([]byte, xattrSize())
rand.Read(attrs[3].Value)
for i := 10; i < 12; i++ {
key := fmt.Sprintf("user.test-%d", i)
value := make([]byte, xattrSize())
rand.Read(value)
attrs = append(attrs, protocol.Xattr{
Name: key,
Value: value,
})
}
sort.Slice(attrs, func(i, j int) bool { return attrs[i].Name < attrs[j].Name })
// Set the xattrs, read them back and compare
if err := tfs.SetXattr("/test", attrs, noopXattrFilter{}); err != nil {
t.Fatal(err)
}
res, err = tfs.GetXattr("/test", noopXattrFilter{})
if err != nil {
t.Fatal(err)
}
if len(res) != len(attrs) {
t.Fatalf("length of returned xattrs does not match (%d != %d)", len(res), len(attrs))
}
for i, xa := range res {
if xa.Name != attrs[i].Name {
t.Errorf("xattr name %q != %q", xa.Name, attrs[i].Name)
}
if !bytes.Equal(xa.Value, attrs[i].Value) {
t.Errorf("xattr value %q != %q", xa.Value, attrs[i].Value)
}
}
}
func TestBasicWalkSkipSymlink(t *testing.T) { func TestBasicWalkSkipSymlink(t *testing.T) {
_, dir := setup(t) _, dir := setup(t)
testWalkSkipSymlink(t, FilesystemTypeBasic, dir) testWalkSkipSymlink(t, FilesystemTypeBasic, dir)
@ -579,3 +664,9 @@ func TestWalkInfiniteRecursion(t *testing.T) {
_, dir := setup(t) _, dir := setup(t)
testWalkInfiniteRecursion(t, FilesystemTypeBasic, dir) testWalkInfiniteRecursion(t, FilesystemTypeBasic, dir)
} }
type noopXattrFilter struct{}
func (noopXattrFilter) Permit(string) bool { return true }
func (noopXattrFilter) GetMaxSingleEntrySize() int { return 0 }
func (noopXattrFilter) GetMaxTotalSize() int { return 0 }

View File

@ -0,0 +1,101 @@
// 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)
}
}

View File

@ -0,0 +1,43 @@
// 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 linux || darwin
// +build linux darwin
package fs
import (
"errors"
"fmt"
"sort"
"strings"
"golang.org/x/sys/unix"
)
func listXattr(path string) ([]string, error) {
buf := make([]byte, 1024)
size, err := unix.Llistxattr(path, buf)
if errors.Is(err, unix.ERANGE) {
// Buffer is too small. Try again with a zero sized buffer to get
// the size, then allocate a buffer of the correct size.
size, err = unix.Llistxattr(path, nil)
if err != nil {
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
}
buf = make([]byte, size)
size, err = unix.Llistxattr(path, buf)
}
if err != nil {
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
}
buf = buf[:size]
attrs := compact(strings.Split(string(buf), "\x00"))
sort.Strings(attrs)
return attrs, nil
}

View File

@ -0,0 +1,143 @@
// 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 !windows && !dragonfly && !illumos && !solaris && !openbsd
// +build !windows,!dragonfly,!illumos,!solaris,!openbsd
package fs
import (
"bytes"
"errors"
"fmt"
"syscall"
"github.com/syncthing/syncthing/lib/protocol"
"golang.org/x/sys/unix"
)
func (f *BasicFilesystem) GetXattr(path string, xattrFilter XattrFilter) ([]protocol.Xattr, error) {
path, err := f.rooted(path)
if err != nil {
return nil, fmt.Errorf("get xattr %s: %w", path, err)
}
attrs, err := listXattr(path)
if err != nil {
return nil, fmt.Errorf("get xattr %s: %w", path, err)
}
res := make([]protocol.Xattr, 0, len(attrs))
var val, buf []byte
var totSize int
for _, attr := range attrs {
if !xattrFilter.Permit(attr) {
l.Debugf("get xattr %s: skipping attribute %q denied by filter", path, attr)
continue
}
val, buf, err = getXattr(path, attr, buf)
var errNo syscall.Errno
if errors.As(err, &errNo) && errNo == 0x5d {
// ENOATTR, returned on BSD when asking for an attribute that
// doesn't exist (any more?)
continue
} else if err != nil {
return nil, fmt.Errorf("get xattr %s: %w", path, err)
}
if max := xattrFilter.GetMaxSingleEntrySize(); max > 0 && len(attr)+len(val) > max {
l.Debugf("get xattr %s: attribute %q exceeds max size", path, attr)
continue
}
totSize += len(attr) + len(val)
if max := xattrFilter.GetMaxTotalSize(); max > 0 && totSize > max {
l.Debugf("get xattr %s: attribute %q would cause max size to be exceeded", path, attr)
continue
}
res = append(res, protocol.Xattr{
Name: attr,
Value: val,
})
}
return res, nil
}
func getXattr(path, name string, buf []byte) (val []byte, rest []byte, err error) {
if len(buf) == 0 {
buf = make([]byte, 1024)
}
size, err := unix.Lgetxattr(path, name, buf)
if errors.Is(err, unix.ERANGE) {
// Buffer was too small. Figure out how large it needs to be, and
// allocate.
size, err = unix.Lgetxattr(path, name, nil)
if err != nil {
return nil, nil, fmt.Errorf("Lgetxattr %s %q: %w", path, name, err)
}
if size > len(buf) {
buf = make([]byte, size)
}
size, err = unix.Lgetxattr(path, name, buf)
}
if err != nil {
return nil, buf, fmt.Errorf("Lgetxattr %s %q: %w", path, name, err)
}
return buf[:size], buf[size:], nil
}
func (f *BasicFilesystem) SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error {
// Index the new attribute set.
xattrsIdx := make(map[string]int)
for i, xa := range xattrs {
xattrsIdx[xa.Name] = i
}
// Get and index the existing attribute set
current, err := f.GetXattr(path, xattrFilter)
if err != nil {
return fmt.Errorf("set xattrs %s: GetXattr: %w", path, err)
}
currentIdx := make(map[string]int)
for i, xa := range current {
currentIdx[xa.Name] = i
}
path, err = f.rooted(path)
if err != nil {
return fmt.Errorf("set xattrs %s: %w", path, err)
}
// Remove all existing xattrs that are not in the new set
for _, xa := range current {
if _, ok := xattrsIdx[xa.Name]; !ok {
if err := unix.Lremovexattr(path, xa.Name); err != nil {
return fmt.Errorf("set xattrs %s: Removexattr %q: %w", path, xa.Name, err)
}
}
}
// Set all xattrs that are different in the new set
for _, xa := range xattrs {
if old, ok := currentIdx[xa.Name]; ok && bytes.Equal(xa.Value, current[old].Value) {
continue
}
if err := unix.Lsetxattr(path, xa.Name, xa.Value, 0); err != nil {
return fmt.Errorf("set xattrs %s: Setxattr %q: %w", path, xa.Name, err)
}
}
return nil
}
func compact(ss []string) []string {
i := 0
for _, s := range ss {
if s != "" {
ss[i] = s
i++
}
}
return ss[:i]
}

View File

@ -0,0 +1,22 @@
// 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 windows || dragonfly || illumos || solaris || openbsd
// +build windows dragonfly illumos solaris openbsd
package fs
import (
"github.com/syncthing/syncthing/lib/protocol"
)
func (f *BasicFilesystem) GetXattr(path string, xattrFilter XattrFilter) ([]protocol.Xattr, error) {
return nil, ErrXattrsNotSupported
}
func (f *BasicFilesystem) SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error {
return ErrXattrsNotSupported
}

View File

@ -24,9 +24,15 @@ func (fs *errorFilesystem) Lchown(_, _, _ string) error { return fs.err }
func (fs *errorFilesystem) Chtimes(_ string, _ time.Time, _ time.Time) error { func (fs *errorFilesystem) Chtimes(_ string, _ time.Time, _ time.Time) error {
return fs.err return fs.err
} }
func (fs *errorFilesystem) Create(_ string) (File, error) { return nil, fs.err } func (fs *errorFilesystem) Create(_ string) (File, error) { return nil, fs.err }
func (fs *errorFilesystem) CreateSymlink(_, _ string) error { return fs.err } func (fs *errorFilesystem) CreateSymlink(_, _ string) error { return fs.err }
func (fs *errorFilesystem) DirNames(_ string) ([]string, error) { return nil, fs.err } func (fs *errorFilesystem) DirNames(_ string) ([]string, error) { return nil, fs.err }
func (fs *errorFilesystem) GetXattr(_ string, _ XattrFilter) ([]protocol.Xattr, error) {
return nil, fs.err
}
func (fs *errorFilesystem) SetXattr(_ string, _ []protocol.Xattr, _ XattrFilter) error {
return fs.err
}
func (fs *errorFilesystem) Lstat(_ string) (FileInfo, error) { return nil, fs.err } func (fs *errorFilesystem) Lstat(_ string) (FileInfo, error) { return nil, fs.err }
func (fs *errorFilesystem) Mkdir(_ string, _ FileMode) error { return fs.err } func (fs *errorFilesystem) Mkdir(_ string, _ FileMode) error { return fs.err }
func (fs *errorFilesystem) MkdirAll(_ string, _ FileMode) error { return fs.err } func (fs *errorFilesystem) MkdirAll(_ string, _ FileMode) error { return fs.err }
@ -54,7 +60,7 @@ func (*errorFilesystem) SameFile(_, _ FileInfo) bool { return false }
func (fs *errorFilesystem) Watch(_ string, _ Matcher, _ context.Context, _ bool) (<-chan Event, <-chan error, error) { func (fs *errorFilesystem) Watch(_ string, _ Matcher, _ context.Context, _ bool) (<-chan Event, <-chan error, error) {
return nil, nil, fs.err return nil, nil, fs.err
} }
func (fs *errorFilesystem) PlatformData(_ string) (protocol.PlatformData, error) { func (fs *errorFilesystem) PlatformData(_ string, _, _ bool, _ XattrFilter) (protocol.PlatformData, error) {
return protocol.PlatformData{}, fs.err return protocol.PlatformData{}, fs.err
} }

View File

@ -622,6 +622,14 @@ func (*fakeFS) Unhide(_ string) error {
return nil return nil
} }
func (*fakeFS) GetXattr(_ string, _ XattrFilter) ([]protocol.Xattr, error) {
return nil, nil
}
func (*fakeFS) SetXattr(_ string, _ []protocol.Xattr, _ XattrFilter) error {
return nil
}
func (*fakeFS) Glob(_ string) ([]string, error) { func (*fakeFS) Glob(_ string) ([]string, error) {
// gnnh we don't seem to actually require this in practice // gnnh we don't seem to actually require this in practice
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
@ -662,8 +670,8 @@ func (fs *fakeFS) SameFile(fi1, fi2 FileInfo) bool {
return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group() return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group()
} }
func (fs *fakeFS) PlatformData(name string) (protocol.PlatformData, error) { func (fs *fakeFS) PlatformData(name string, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
return unixPlatformData(fs, name, fs.userCache, fs.groupCache) return unixPlatformData(fs, name, fs.userCache, fs.groupCache, scanOwnership, scanXattrs, xattrFilter)
} }
func (*fakeFS) underlying() (Filesystem, bool) { func (*fakeFS) underlying() (Filesystem, bool) {
@ -961,3 +969,11 @@ func (f *fakeFileInfo) Owner() int {
func (f *fakeFileInfo) Group() int { func (f *fakeFileInfo) Group() int {
return f.gid return f.gid
} }
func (*fakeFileInfo) Sys() interface{} {
return nil
}
func (*fakeFileInfo) InodeChangeTime() time.Time {
return time.Time{}
}

View File

@ -29,6 +29,12 @@ const (
filesystemWrapperTypeLog filesystemWrapperTypeLog
) )
type XattrFilter interface {
Permit(string) bool
GetMaxSingleEntrySize() int
GetMaxTotalSize() int
}
// The Filesystem interface abstracts access to the file system. // The Filesystem interface abstracts access to the file system.
type Filesystem interface { type Filesystem interface {
Chmod(name string, mode FileMode) error Chmod(name string, mode FileMode) error
@ -62,7 +68,9 @@ type Filesystem interface {
URI() string URI() string
Options() []Option Options() []Option
SameFile(fi1, fi2 FileInfo) bool SameFile(fi1, fi2 FileInfo) bool
PlatformData(name string) (protocol.PlatformData, error) PlatformData(name string, withOwnership, withXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error)
GetXattr(name string, xattrFilter XattrFilter) ([]protocol.Xattr, error)
SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error
// Used for unwrapping things // Used for unwrapping things
underlying() (Filesystem, bool) underlying() (Filesystem, bool)
@ -94,11 +102,13 @@ type FileInfo interface {
Size() int64 Size() int64
ModTime() time.Time ModTime() time.Time
IsDir() bool IsDir() bool
Sys() interface{}
// Extensions // Extensions
IsRegular() bool IsRegular() bool
IsSymlink() bool IsSymlink() bool
Owner() int Owner() int
Group() int Group() int
InodeChangeTime() time.Time // may be zero if not supported
} }
// FileMode is similar to os.FileMode // FileMode is similar to os.FileMode
@ -154,7 +164,10 @@ func (evType EventType) String() string {
} }
} }
var ErrWatchNotSupported = errors.New("watching is not supported") var (
ErrWatchNotSupported = errors.New("watching is not supported")
ErrXattrsNotSupported = errors.New("extended attributes are not supported on this platform")
)
// Equivalents from os package. // Equivalents from os package.

View File

@ -17,42 +17,48 @@ import (
// unixPlatformData is used on all platforms, because apart from being the // unixPlatformData is used on all platforms, because apart from being the
// implementation for BasicFilesystem on Unixes it's also the implementation // implementation for BasicFilesystem on Unixes it's also the implementation
// in fakeFS. // in fakeFS.
func unixPlatformData(fs Filesystem, name string, userCache *userCache, groupCache *groupCache) (protocol.PlatformData, error) { func unixPlatformData(fs Filesystem, name string, userCache *userCache, groupCache *groupCache, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
stat, err := fs.Lstat(name) var pd protocol.PlatformData
if err != nil { if scanOwnership {
return protocol.PlatformData{}, err var ud protocol.UnixData
stat, err := fs.Lstat(name)
if err != nil {
return protocol.PlatformData{}, err
}
ud.UID = stat.Owner()
if user := userCache.lookup(strconv.Itoa(ud.UID)); user != nil {
ud.OwnerName = user.Username
} else if ud.UID == 0 {
// We couldn't look up a name, but UID zero should be "root". This
// fixup works around the (unlikely) situation where the ownership
// is 0:0 but we can't look up a name for either uid zero or gid
// zero. If that were the case we'd return a zero PlatformData which
// wouldn't get serialized over the wire and the other side would
// assume a lack of ownership info...
ud.OwnerName = "root"
}
ud.GID = stat.Group()
if group := groupCache.lookup(strconv.Itoa(ud.GID)); group != nil {
ud.GroupName = group.Name
} else if ud.GID == 0 {
ud.GroupName = "root"
}
pd.Unix = &ud
} }
ownerUID := stat.Owner() if scanXattrs {
ownerName := "" xattrs, err := fs.GetXattr(name, xattrFilter)
if user := userCache.lookup(strconv.Itoa(ownerUID)); user != nil { if err != nil {
ownerName = user.Username return protocol.PlatformData{}, err
} else if ownerUID == 0 { }
// We couldn't look up a name, but UID zero should be "root". This pd.SetXattrs(xattrs)
// fixup works around the (unlikely) situation where the ownership
// is 0:0 but we can't look up a name for either uid zero or gid
// zero. If that were the case we'd return a zero PlatformData which
// wouldn't get serialized over the wire and the other side would
// assume a lack of ownership info...
ownerName = "root"
} }
groupID := stat.Group() return pd, nil
groupName := ""
if group := groupCache.lookup(strconv.Itoa(ownerUID)); group != nil {
groupName = group.Name
} else if groupID == 0 {
groupName = "root"
}
return protocol.PlatformData{
Unix: &protocol.UnixData{
OwnerName: ownerName,
GroupName: groupName,
UID: ownerUID,
GID: groupID,
},
}, nil
} }
type valueCache[K comparable, V any] struct { type valueCache[K comparable, V any] struct {

View File

@ -607,6 +607,7 @@ func (b *scanBatch) Update(fi protocol.FileInfo, snap *db.Snapshot) bool {
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: protocol.FlagLocalReceiveOnly, IgnoreFlags: protocol.FlagLocalReceiveOnly,
IgnoreOwnership: !b.f.SyncOwnership, IgnoreOwnership: !b.f.SyncOwnership,
IgnoreXattrs: !b.f.SyncXattrs,
}): }):
// What we have locally is equivalent to the global file. // What we have locally is equivalent to the global file.
l.Debugf("%v scanning: Merging identical locally changed item with global", b.f, fi) l.Debugf("%v scanning: Merging identical locally changed item with global", b.f, fi)
@ -637,7 +638,6 @@ func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (i
CurrentFiler: cFiler{snap}, CurrentFiler: cFiler{snap},
Filesystem: f.mtimefs, Filesystem: f.mtimefs,
IgnorePerms: f.IgnorePerms, IgnorePerms: f.IgnorePerms,
IgnoreOwnership: !f.SyncOwnership,
AutoNormalize: f.AutoNormalize, AutoNormalize: f.AutoNormalize,
Hashers: f.model.numHashers(f.ID), Hashers: f.model.numHashers(f.ID),
ShortID: f.shortID, ShortID: f.shortID,
@ -645,7 +645,9 @@ func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (i
LocalFlags: f.localFlags, LocalFlags: f.localFlags,
ModTimeWindow: f.modTimeWindow, ModTimeWindow: f.modTimeWindow,
EventLogger: f.evLogger, EventLogger: f.evLogger,
ScanOwnership: f.ScanOwnership || f.SyncOwnership, ScanOwnership: f.SendOwnership || f.SyncOwnership,
ScanXattrs: f.SendXattrs || f.SyncXattrs,
XattrFilter: f.XattrFilter,
} }
var fchan chan scanner.ScanResult var fchan chan scanner.ScanResult
if f.Type == config.FolderTypeReceiveEncrypted { if f.Type == config.FolderTypeReceiveEncrypted {

View File

@ -130,6 +130,7 @@ func (f *receiveOnlyFolder) revert() error {
ModTimeWindow: f.modTimeWindow, ModTimeWindow: f.modTimeWindow,
IgnoreFlags: protocol.FlagLocalReceiveOnly, IgnoreFlags: protocol.FlagLocalReceiveOnly,
IgnoreOwnership: !f.SyncOwnership, IgnoreOwnership: !f.SyncOwnership,
IgnoreXattrs: !f.SyncXattrs,
}): }):
// What we have locally is equivalent to the global file. // What we have locally is equivalent to the global file.
fi = gf fi = gf

View File

@ -76,6 +76,7 @@ func (f *sendOnlyFolder) pull() (bool, error) {
ModTimeWindow: f.modTimeWindow, ModTimeWindow: f.modTimeWindow,
IgnorePerms: f.IgnorePerms, IgnorePerms: f.IgnorePerms,
IgnoreOwnership: !f.SyncOwnership, IgnoreOwnership: !f.SyncOwnership,
IgnoreXattrs: !f.SyncXattrs,
}) { }) {
return true return true
} }

View File

@ -592,8 +592,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, snap *db.Snapshot,
// Check that it is what we have in the database. // Check that it is what we have in the database.
curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name) curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name)
if err := f.scanIfItemChanged(file.Name, info, curFile, hasCurFile, scanChan); err != nil { if err := f.scanIfItemChanged(file.Name, info, curFile, hasCurFile, scanChan); err != nil {
err = fmt.Errorf("handling dir: %w", err) f.newPullError(file.Name, fmt.Errorf("handling dir: %w", err))
f.newPullError(file.Name, err)
return return
} }
@ -661,7 +660,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, snap *db.Snapshot,
// It's OK to change mode bits on stuff within non-writable directories. // It's OK to change mode bits on stuff within non-writable directories.
if !f.IgnorePerms && !file.NoPermissions { if !f.IgnorePerms && !file.NoPermissions {
if err := f.mtimefs.Chmod(file.Name, mode|(info.Mode()&retainBits)); err != nil { if err := f.mtimefs.Chmod(file.Name, mode|(info.Mode()&retainBits)); err != nil {
f.newPullError(file.Name, err) f.newPullError(file.Name, fmt.Errorf("handling dir (setting permissions): %w", err))
return return
} }
} }
@ -988,13 +987,14 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, sn
err = errModified err = errModified
default: default:
var fi protocol.FileInfo var fi protocol.FileInfo
if fi, err = scanner.CreateFileInfo(stat, target.Name, f.mtimefs, f.SyncOwnership); err == nil { if fi, err = scanner.CreateFileInfo(stat, target.Name, f.mtimefs, f.SyncOwnership, f.SyncXattrs, f.XattrFilter); err == nil {
if !fi.IsEquivalentOptional(curTarget, protocol.FileInfoComparison{ if !fi.IsEquivalentOptional(curTarget, protocol.FileInfoComparison{
ModTimeWindow: f.modTimeWindow, ModTimeWindow: f.modTimeWindow,
IgnorePerms: f.IgnorePerms, IgnorePerms: f.IgnorePerms,
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: protocol.LocalAllFlags, IgnoreFlags: protocol.LocalAllFlags,
IgnoreOwnership: !f.SyncOwnership, IgnoreOwnership: !f.SyncOwnership,
IgnoreXattrs: !f.SyncXattrs,
}) { }) {
// Target changed // Target changed
scanChan <- target.Name scanChan <- target.Name
@ -1203,8 +1203,8 @@ func populateOffsets(blocks []protocol.BlockInfo) {
} }
} }
// shortcutFile sets file mode and modification time, when that's the only // shortcutFile sets file metadata, when that's the only thing that has
// thing that has changed. // changed.
func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob) { func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob) {
l.Debugln(f, "taking shortcut on", file.Name) l.Debugln(f, "taking shortcut on", file.Name)
@ -1228,13 +1228,22 @@ func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo, dbUpdateChan ch
if !f.IgnorePerms && !file.NoPermissions { if !f.IgnorePerms && !file.NoPermissions {
if err = f.mtimefs.Chmod(file.Name, fs.FileMode(file.Permissions&0777)); err != nil { if err = f.mtimefs.Chmod(file.Name, fs.FileMode(file.Permissions&0777)); err != nil {
f.newPullError(file.Name, err) f.newPullError(file.Name, fmt.Errorf("shortcut file (setting permissions): %w", err))
return
}
}
if f.SyncXattrs {
if err = f.mtimefs.SetXattr(file.Name, file.Platform.Xattrs(), f.XattrFilter); errors.Is(err, fs.ErrXattrsNotSupported) {
l.Debugf("Cannot set xattrs on %q: %v", file.Name, err)
} else if err != nil {
f.newPullError(file.Name, fmt.Errorf("shortcut file (setting xattrs): %w", err))
return return
} }
} }
if err := f.maybeAdjustOwnership(&file, file.Name); err != nil { if err := f.maybeAdjustOwnership(&file, file.Name); err != nil {
f.newPullError(file.Name, err) f.newPullError(file.Name, fmt.Errorf("shortcut file (setting ownership): %w", err))
return return
} }
@ -1253,7 +1262,7 @@ func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo, dbUpdateChan ch
return fd.Truncate(file.Size + trailerSize) return fd.Truncate(file.Size + trailerSize)
}, f.mtimefs, file.Name, true) }, f.mtimefs, file.Name, true)
if err != nil { if err != nil {
f.newPullError(file.Name, err) f.newPullError(file.Name, fmt.Errorf("writing encrypted file trailer: %w", err))
return return
} }
} }
@ -1599,13 +1608,22 @@ func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCu
// Set the correct permission bits on the new file // Set the correct permission bits on the new file
if !f.IgnorePerms && !file.NoPermissions { if !f.IgnorePerms && !file.NoPermissions {
if err := f.mtimefs.Chmod(tempName, fs.FileMode(file.Permissions&0777)); err != nil { if err := f.mtimefs.Chmod(tempName, fs.FileMode(file.Permissions&0777)); err != nil {
return err return fmt.Errorf("setting permissions: %w", err)
}
}
// Set extended attributes
if f.SyncXattrs {
if err := f.mtimefs.SetXattr(tempName, file.Platform.Xattrs(), f.XattrFilter); errors.Is(err, fs.ErrXattrsNotSupported) {
l.Debugf("Cannot set xattrs on %q: %v", file.Name, err)
} else if err != nil {
return fmt.Errorf("setting xattrs: %w", err)
} }
} }
// Set ownership based on file metadata or parent, maybe. // Set ownership based on file metadata or parent, maybe.
if err := f.maybeAdjustOwnership(&file, tempName); err != nil { if err := f.maybeAdjustOwnership(&file, tempName); err != nil {
return err return fmt.Errorf("setting ownership: %w", err)
} }
if stat, err := f.mtimefs.Lstat(file.Name); err == nil { if stat, err := f.mtimefs.Lstat(file.Name); err == nil {
@ -1613,7 +1631,7 @@ func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCu
// handle that. // handle that.
if err := f.scanIfItemChanged(file.Name, stat, curFile, hasCurFile, scanChan); err != nil { if err := f.scanIfItemChanged(file.Name, stat, curFile, hasCurFile, scanChan); err != nil {
return err return fmt.Errorf("checking existing file: %w", err)
} }
if !curFile.IsDirectory() && !curFile.IsSymlink() && f.inConflict(curFile.Version, file.Version) { if !curFile.IsDirectory() && !curFile.IsSymlink() && f.inConflict(curFile.Version, file.Version) {
@ -1629,16 +1647,16 @@ func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCu
err = f.deleteItemOnDisk(curFile, snap, scanChan) err = f.deleteItemOnDisk(curFile, snap, scanChan)
} }
if err != nil { if err != nil {
return err return fmt.Errorf("moving for conflict: %w", err)
} }
} else if !fs.IsNotExist(err) { } else if !fs.IsNotExist(err) {
return err return fmt.Errorf("checking existing file: %w", err)
} }
// Replace the original content with the new one. If it didn't work, // Replace the original content with the new one. If it didn't work,
// leave the temp file in place for reuse. // leave the temp file in place for reuse.
if err := osutil.RenameOrCopy(f.CopyRangeMethod, f.mtimefs, f.mtimefs, tempName, file.Name); err != nil { if err := osutil.RenameOrCopy(f.CopyRangeMethod, f.mtimefs, f.mtimefs, tempName, file.Name); err != nil {
return err return fmt.Errorf("replacing file: %w", err)
} }
// Set the correct timestamp on the new file // Set the correct timestamp on the new file
@ -1661,7 +1679,7 @@ func (f *sendReceiveFolder) finisherRoutine(snap *db.Snapshot, in <-chan *shared
} }
if err != nil { if err != nil {
f.newPullError(state.file.Name, err) f.newPullError(state.file.Name, fmt.Errorf("finishing: %w", err))
} else { } else {
minBlocksPerBlock := state.file.BlockSize() / protocol.MinBlockSize minBlocksPerBlock := state.file.BlockSize() / protocol.MinBlockSize
blockStatsMut.Lock() blockStatsMut.Lock()
@ -1768,6 +1786,15 @@ loop:
lastFile = job.file lastFile = job.file
} }
if !job.file.IsDeleted() && !job.file.IsInvalid() {
// Now that the file is finalized, grab possibly updated
// inode change time from disk into the local FileInfo. We
// use this change time to check for changes to xattrs etc
// on next scan.
if err := f.updateFileInfoChangeTime(&job.file); err != nil {
l.Warnln("Error updating metadata for %q at database commit: %v", job.file.Name, err)
}
}
job.file.Sequence = 0 job.file.Sequence = 0
batch.Append(job.file) batch.Append(job.file)
@ -1878,7 +1905,7 @@ func (f *sendReceiveFolder) newPullError(path string, err error) {
// Establish context to differentiate from errors while scanning. // Establish context to differentiate from errors while scanning.
// Use "syncing" as opposed to "pulling" as the latter might be used // Use "syncing" as opposed to "pulling" as the latter might be used
// for errors occurring specifically in the puller routine. // for errors occurring specifically in the puller routine.
errStr := fmt.Sprintln("syncing:", err) errStr := fmt.Sprintf("syncing: %s", err)
f.tempPullErrors[path] = errStr f.tempPullErrors[path] = errStr
l.Debugf("%v new error for %v: %v", f, path, err) l.Debugf("%v new error for %v: %v", f, path, err)
@ -1978,7 +2005,7 @@ func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, snap *db.S
hasReceiveOnlyChanged = true hasReceiveOnlyChanged = true
return nil return nil
} }
diskFile, err := scanner.CreateFileInfo(info, path, f.mtimefs, f.SyncOwnership) diskFile, err := scanner.CreateFileInfo(info, path, f.mtimefs, f.SyncOwnership, f.SyncXattrs, f.XattrFilter)
if err != nil { if err != nil {
// Lets just assume the file has changed. // Lets just assume the file has changed.
scanChan <- path scanChan <- path
@ -1991,6 +2018,7 @@ func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, snap *db.S
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: protocol.LocalAllFlags, IgnoreFlags: protocol.LocalAllFlags,
IgnoreOwnership: !f.SyncOwnership, IgnoreOwnership: !f.SyncOwnership,
IgnoreXattrs: !f.SyncXattrs,
}) { }) {
// File on disk changed compared to what we have in db // File on disk changed compared to what we have in db
// -> schedule scan. // -> schedule scan.
@ -2055,7 +2083,7 @@ func (f *sendReceiveFolder) scanIfItemChanged(name string, stat fs.FileInfo, ite
// to the database. If there's a mismatch here, there might be local // to the database. If there's a mismatch here, there might be local
// changes that we don't know about yet and we should scan before // changes that we don't know about yet and we should scan before
// touching the item. // touching the item.
statItem, err := scanner.CreateFileInfo(stat, item.Name, f.mtimefs, f.SyncOwnership) statItem, err := scanner.CreateFileInfo(stat, item.Name, f.mtimefs, f.SyncOwnership, f.SyncXattrs, f.XattrFilter)
if err != nil { if err != nil {
return fmt.Errorf("comparing item on disk to db: %w", err) return fmt.Errorf("comparing item on disk to db: %w", err)
} }
@ -2066,6 +2094,7 @@ func (f *sendReceiveFolder) scanIfItemChanged(name string, stat fs.FileInfo, ite
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: protocol.LocalAllFlags, IgnoreFlags: protocol.LocalAllFlags,
IgnoreOwnership: !f.SyncOwnership, IgnoreOwnership: !f.SyncOwnership,
IgnoreXattrs: !f.SyncXattrs,
}) { }) {
return errModified return errModified
} }
@ -2150,6 +2179,22 @@ func (f *sendReceiveFolder) withLimiter(fn func() error) error {
return fn() return fn()
} }
// updateFileInfoChangeTime updates the inode change time in the FileInfo,
// because that depends on the current, new, state of the file on disk.
func (f *sendReceiveFolder) updateFileInfoChangeTime(file *protocol.FileInfo) error {
info, err := f.mtimefs.Lstat(file.Name)
if err != nil {
return err
}
if ct := info.InodeChangeTime(); !ct.IsZero() {
file.InodeChangeNs = ct.UnixNano()
} else {
file.InodeChangeNs = 0
}
return nil
}
// A []FileError is sent as part of an event and will be JSON serialized. // A []FileError is sent as part of an event and will be JSON serialized.
type FileError struct { type FileError struct {
Path string `json:"path"` Path string `json:"path"`

View File

@ -21,6 +21,7 @@ import (
"time" "time"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore" "github.com/syncthing/syncthing/lib/ignore"
@ -83,7 +84,7 @@ func createEmptyFileInfo(t *testing.T, name string, fs fs.Filesystem) protocol.F
writeFile(t, fs, name, nil) writeFile(t, fs, name, nil)
fi, err := fs.Stat(name) fi, err := fs.Stat(name)
must(t, err) must(t, err)
file, err := scanner.CreateFileInfo(fi, name, fs, false) file, err := scanner.CreateFileInfo(fi, name, fs, false, false, config.XattrFilter{})
must(t, err) must(t, err)
return file return file
} }
@ -777,9 +778,12 @@ func TestDeleteIgnorePerms(t *testing.T) {
stat, err := file.Stat() stat, err := file.Stat()
must(t, err) must(t, err)
fi, err := scanner.CreateFileInfo(stat, name, ffs, false) fi, err := scanner.CreateFileInfo(stat, name, ffs, false, false, config.XattrFilter{})
must(t, err) must(t, err)
ffs.Chmod(name, 0600) ffs.Chmod(name, 0600)
if info, err := ffs.Stat(name); err == nil {
fi.InodeChangeNs = info.InodeChangeTime().UnixNano()
}
scanChan := make(chan string, 1) scanChan := make(chan string, 1)
err = f.checkToBeDeleted(fi, fi, true, scanChan) err = f.checkToBeDeleted(fi, fi, true, scanChan)
must(t, err) must(t, err)

View File

@ -360,6 +360,7 @@ func prepareFileInfoForIndex(f protocol.FileInfo) protocol.FileInfo {
// never sent externally // never sent externally
f.LocalFlags = 0 f.LocalFlags = 0
f.VersionHash = nil f.VersionHash = nil
f.InodeChangeNs = 0
return f return f
} }

View File

@ -3216,10 +3216,28 @@ func TestConnCloseOnRestart(t *testing.T) {
} }
func TestModTimeWindow(t *testing.T) { func TestModTimeWindow(t *testing.T) {
// This test doesn't work any more, because changing the file like we do
// in the test below changes the inode time, which we detect
// (correctly). The test could be fixed by having a filesystem wrapper
// around fakeFs or basicFs that lies in the returned modtime (like FAT
// does...), but injecting such a wrapper isn't trivial. The filesystem
// is created by FolderConfiguration, so it would require a new
// filesystem type, which is really ugly, or creating a
// FilesystemFactory object that would create filesystems based on
// configs and would be injected into the model. But that's a major
// refactor. Adding a test-only override of the filesystem to the
// FolderConfiguration could be neat, but the FolderConfiguration is
// generated by protobuf so this is also a little tricky or at least
// ugly. I'm leaving it like this for now.
t.Skip("this test is currently broken")
w, fcfg, wCancel := tmpDefaultWrapper(t) w, fcfg, wCancel := tmpDefaultWrapper(t)
defer wCancel() defer wCancel()
tfs := fcfg.Filesystem(nil) tfs := modtimeTruncatingFS{
fcfg.RawModTimeWindowS = 2 trunc: 0,
Filesystem: fcfg.Filesystem(nil),
}
// fcfg.RawModTimeWindowS = 2
setFolder(t, w, fcfg) setFolder(t, w, fcfg)
m := setupModel(t, w) m := setupModel(t, w)
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -3243,10 +3261,12 @@ func TestModTimeWindow(t *testing.T) {
} }
v := fi.Version v := fi.Version
// Update time on disk 1s // Change the filesystem to only return modtimes to the closest two
// seconds, like FAT.
err = tfs.Chtimes(name, time.Now(), modTime.Add(time.Second)) tfs.trunc = 2 * time.Second
must(t, err)
// Scan again
m.ScanFolders() m.ScanFolders()
@ -4305,3 +4325,41 @@ func equalStringsInAnyOrder(a, b []string) bool {
} }
return true return true
} }
// modtimeTruncatingFS is a FileSystem that returns modification times only
// to the closest two `trunc` interval.
type modtimeTruncatingFS struct {
trunc time.Duration
fs.Filesystem
}
func (f modtimeTruncatingFS) Lstat(name string) (fs.FileInfo, error) {
fmt.Println("lstat", name)
info, err := f.Filesystem.Lstat(name)
return modtimeTruncatingFileInfo{trunc: f.trunc, FileInfo: info}, err
}
func (f modtimeTruncatingFS) Stat(name string) (fs.FileInfo, error) {
fmt.Println("stat", name)
info, err := f.Filesystem.Stat(name)
return modtimeTruncatingFileInfo{trunc: f.trunc, FileInfo: info}, err
}
func (f modtimeTruncatingFS) Walk(root string, walkFn fs.WalkFunc) error {
return f.Filesystem.Walk(root, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return walkFn(path, nil, err)
}
fmt.Println("walk", info.Name())
return walkFn(path, modtimeTruncatingFileInfo{trunc: f.trunc, FileInfo: info}, nil)
})
}
type modtimeTruncatingFileInfo struct {
trunc time.Duration
fs.FileInfo
}
func (fi modtimeTruncatingFileInfo) ModTime() time.Time {
return fi.FileInfo.ModTime().Truncate(fi.trunc)
}

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,8 @@ type FileIntf interface {
FilePermissions() uint32 FilePermissions() uint32
FileModifiedBy() ShortID FileModifiedBy() ShortID
ModTime() time.Time ModTime() time.Time
PlatformData() PlatformData
InodeChangeTime() time.Time
} }
func (Hello) Magic() uint32 { func (Hello) Magic() uint32 {
@ -160,6 +162,14 @@ func (f FileInfo) FileModifiedBy() ShortID {
return f.ModifiedBy return f.ModifiedBy
} }
func (f FileInfo) PlatformData() PlatformData {
return f.Platform
}
func (f FileInfo) InodeChangeTime() time.Time {
return time.Unix(0, f.InodeChangeNs)
}
// WinsConflict returns true if "f" is the one to choose when it is in // WinsConflict returns true if "f" is the one to choose when it is in
// conflict with "other". // conflict with "other".
func WinsConflict(f, other FileIntf) bool { func WinsConflict(f, other FileIntf) bool {
@ -196,6 +206,7 @@ type FileInfoComparison struct {
IgnoreBlocks bool IgnoreBlocks bool
IgnoreFlags uint32 IgnoreFlags uint32
IgnoreOwnership bool IgnoreOwnership bool
IgnoreXattrs bool
} }
func (f FileInfo) IsEquivalent(other FileInfo, modTimeWindow time.Duration) bool { func (f FileInfo) IsEquivalent(other FileInfo, modTimeWindow time.Duration) bool {
@ -233,6 +244,12 @@ func (f FileInfo) isEquivalent(other FileInfo, comp FileInfoComparison) bool {
return false return false
} }
// If we are recording inode change times and it changed, they are not
// equal.
if f.InodeChangeNs != 0 && other.InodeChangeNs != 0 && f.InodeChangeNs != other.InodeChangeNs {
return false
}
// Mask out the ignored local flags before checking IsInvalid() below // Mask out the ignored local flags before checking IsInvalid() below
f.LocalFlags &^= comp.IgnoreFlags f.LocalFlags &^= comp.IgnoreFlags
other.LocalFlags &^= comp.IgnoreFlags other.LocalFlags &^= comp.IgnoreFlags
@ -252,11 +269,26 @@ func (f FileInfo) isEquivalent(other FileInfo, comp FileInfoComparison) bool {
} }
} }
if f.Platform.Windows != nil && other.Platform.Windows != nil { if f.Platform.Windows != nil && other.Platform.Windows != nil {
if *f.Platform.Windows != *other.Platform.Windows { if f.Platform.Windows.OwnerName != other.Platform.Windows.OwnerName ||
f.Platform.Windows.OwnerIsGroup != other.Platform.Windows.OwnerIsGroup {
return false 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) { if !comp.IgnorePerms && !f.NoPermissions && !other.NoPermissions && !PermsEqual(f.Permissions, other.Permissions) {
return false return false
@ -308,6 +340,76 @@ func (f FileInfo) BlocksEqual(other FileInfo) bool {
return blocksEqual(f.Blocks, other.Blocks) return blocksEqual(f.Blocks, other.Blocks)
} }
// Xattrs is a convenience method to return the extended attributes of the
// file for the current platform.
func (f *PlatformData) Xattrs() []Xattr {
switch {
case build.IsLinux && f.Linux != nil:
return f.Linux.Xattrs
case build.IsDarwin && f.Darwin != nil:
return f.Darwin.Xattrs
case build.IsFreeBSD && f.FreeBSD != nil:
return f.FreeBSD.Xattrs
case build.IsNetBSD && f.NetBSD != nil:
return f.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 // blocksEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise. // and index pair wise.
func blocksEqual(a, b []BlockInfo) bool { func blocksEqual(a, b []BlockInfo) bool {
@ -438,3 +540,23 @@ func (x *FileInfoType) UnmarshalJSON(data []byte) error {
*x = FileInfoType(n) *x = FileInfoType(n)
return nil return nil
} }
func xattrsEqual(a, b *XattrData) bool {
if a == nil || b == nil {
// Having no data on either side means we have nothing to compare
// to, and we consider that equal.
return true
}
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
}

View File

@ -55,7 +55,7 @@ func (i infiniteFS) Open(name string) (fs.File, error) {
return &fakeFile{name, i.filesize, 0}, nil return &fakeFile{name, i.filesize, 0}, nil
} }
func (infiniteFS) PlatformData(_ string) (protocol.PlatformData, error) { func (infiniteFS) PlatformData(_ string, _, _ bool, _ fs.XattrFilter) (protocol.PlatformData, error) {
return protocol.PlatformData{}, nil return protocol.PlatformData{}, nil
} }
@ -105,7 +105,7 @@ func (singleFileFS) Options() []fs.Option {
return nil return nil
} }
func (singleFileFS) PlatformData(_ string) (protocol.PlatformData, error) { func (singleFileFS) PlatformData(_ string, _, _ bool, _ fs.XattrFilter) (protocol.PlatformData, error) {
return protocol.PlatformData{}, nil return protocol.PlatformData{}, nil
} }
@ -121,10 +121,12 @@ func (fakeInfo) ModTime() time.Time { return time.Unix(1234567890, 0) }
func (f fakeInfo) IsDir() bool { func (f fakeInfo) IsDir() bool {
return strings.Contains(filepath.Base(f.name), "dir") || f.name == "." return strings.Contains(filepath.Base(f.name), "dir") || f.name == "."
} }
func (f fakeInfo) IsRegular() bool { return !f.IsDir() } func (f fakeInfo) IsRegular() bool { return !f.IsDir() }
func (fakeInfo) IsSymlink() bool { return false } func (fakeInfo) IsSymlink() bool { return false }
func (fakeInfo) Owner() int { return 0 } func (fakeInfo) Owner() int { return 0 }
func (fakeInfo) Group() int { return 0 } func (fakeInfo) Group() int { return 0 }
func (fakeInfo) Sys() interface{} { return nil }
func (fakeInfo) InodeChangeTime() time.Time { return time.Time{} }
type fakeFile struct { type fakeFile struct {
name string name string

View File

@ -42,8 +42,6 @@ type Config struct {
// If IgnorePerms is true, changes to permission bits will not be // If IgnorePerms is true, changes to permission bits will not be
// detected. // detected.
IgnorePerms bool IgnorePerms bool
// If IgnoreOwnership is true, changes to ownership will not be detected.
IgnoreOwnership bool
// When AutoNormalize is set, file names that are in UTF8 but incorrect // When AutoNormalize is set, file names that are in UTF8 but incorrect
// normalization form will be corrected. // normalization form will be corrected.
AutoNormalize bool AutoNormalize bool
@ -62,6 +60,10 @@ type Config struct {
EventLogger events.Logger EventLogger events.Logger
// If ScanOwnership is true, we pick up ownership information on files while scanning. // If ScanOwnership is true, we pick up ownership information on files while scanning.
ScanOwnership bool ScanOwnership bool
// If ScanXattrs is true, we pick up extended attributes on files while scanning.
ScanXattrs bool
// Filter for extended attributes
XattrFilter XattrFilter
} }
type CurrentFiler interface { type CurrentFiler interface {
@ -69,6 +71,12 @@ type CurrentFiler interface {
CurrentFile(name string) (protocol.FileInfo, bool) CurrentFile(name string) (protocol.FileInfo, bool)
} }
type XattrFilter interface {
Permit(string) bool
GetMaxSingleEntrySize() int
GetMaxTotalSize() int
}
type ScanResult struct { type ScanResult struct {
File protocol.FileInfo File protocol.FileInfo
Err error Err error
@ -384,7 +392,7 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
} }
} }
f, err := CreateFileInfo(info, relPath, w.Filesystem, w.ScanOwnership) f, err := CreateFileInfo(info, relPath, w.Filesystem, w.ScanOwnership, w.ScanXattrs, w.XattrFilter)
if err != nil { if err != nil {
return err return err
} }
@ -398,7 +406,8 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
IgnorePerms: w.IgnorePerms, IgnorePerms: w.IgnorePerms,
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: w.LocalFlags, IgnoreFlags: w.LocalFlags,
IgnoreOwnership: w.IgnoreOwnership, IgnoreOwnership: !w.ScanOwnership,
IgnoreXattrs: !w.ScanXattrs,
}) { }) {
l.Debugln(w, "unchanged:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm) l.Debugln(w, "unchanged:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
return nil return nil
@ -428,7 +437,7 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo, finishedChan chan<- ScanResult) error { func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo, finishedChan chan<- ScanResult) error {
curFile, hasCurFile := w.CurrentFiler.CurrentFile(relPath) curFile, hasCurFile := w.CurrentFiler.CurrentFile(relPath)
f, err := CreateFileInfo(info, relPath, w.Filesystem, w.ScanOwnership) f, err := CreateFileInfo(info, relPath, w.Filesystem, w.ScanOwnership, w.ScanXattrs, w.XattrFilter)
if err != nil { if err != nil {
return err return err
} }
@ -441,7 +450,8 @@ func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo,
IgnorePerms: w.IgnorePerms, IgnorePerms: w.IgnorePerms,
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: w.LocalFlags, IgnoreFlags: w.LocalFlags,
IgnoreOwnership: w.IgnoreOwnership, IgnoreOwnership: !w.ScanOwnership,
IgnoreXattrs: !w.ScanXattrs,
}) { }) {
l.Debugln(w, "unchanged:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm) l.Debugln(w, "unchanged:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
return nil return nil
@ -476,9 +486,9 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileIn
return nil return nil
} }
f, err := CreateFileInfo(info, relPath, w.Filesystem, w.ScanOwnership) f, err := CreateFileInfo(info, relPath, w.Filesystem, w.ScanOwnership, w.ScanXattrs, w.XattrFilter)
if err != nil { if err != nil {
handleError(ctx, "reading link:", relPath, err, finishedChan) handleError(ctx, "reading link", relPath, err, finishedChan)
return nil return nil
} }
@ -492,7 +502,8 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileIn
IgnorePerms: w.IgnorePerms, IgnorePerms: w.IgnorePerms,
IgnoreBlocks: true, IgnoreBlocks: true,
IgnoreFlags: w.LocalFlags, IgnoreFlags: w.LocalFlags,
IgnoreOwnership: w.IgnoreOwnership, IgnoreOwnership: !w.ScanOwnership,
IgnoreXattrs: !w.ScanXattrs,
}) { }) {
l.Debugln(w, "unchanged:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm) l.Debugln(w, "unchanged:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
return nil return nil
@ -591,12 +602,7 @@ func (w *walker) updateFileInfo(dst, src protocol.FileInfo) protocol.FileInfo {
dst.LocalFlags = w.LocalFlags dst.LocalFlags = w.LocalFlags
// Copy OS data from src to dst, unless it was already set on dst. // Copy OS data from src to dst, unless it was already set on dst.
if dst.Platform.Unix == nil { dst.Platform.MergeWith(&src.Platform)
dst.Platform.Unix = src.Platform.Unix
}
if dst.Platform.Windows == nil {
dst.Platform.Windows = src.Platform.Windows
}
return dst return dst
} }
@ -668,10 +674,10 @@ func (noCurrentFiler) CurrentFile(_ string) (protocol.FileInfo, bool) {
return protocol.FileInfo{}, false return protocol.FileInfo{}, false
} }
func CreateFileInfo(fi fs.FileInfo, name string, filesystem fs.Filesystem, scanOwnership bool) (protocol.FileInfo, error) { func CreateFileInfo(fi fs.FileInfo, name string, filesystem fs.Filesystem, scanOwnership bool, scanXattrs bool, xattrFilter XattrFilter) (protocol.FileInfo, error) {
f := protocol.FileInfo{Name: name} f := protocol.FileInfo{Name: name}
if scanOwnership { if scanOwnership || scanXattrs {
if plat, err := filesystem.PlatformData(name); err == nil { if plat, err := filesystem.PlatformData(name, scanOwnership, scanXattrs, xattrFilter); err == nil {
f.Platform = plat f.Platform = plat
} else { } else {
return protocol.FileInfo{}, fmt.Errorf("reading platform data: %w", err) return protocol.FileInfo{}, fmt.Errorf("reading platform data: %w", err)
@ -696,5 +702,10 @@ func CreateFileInfo(fi fs.FileInfo, name string, filesystem fs.Filesystem, scanO
} }
f.Size = fi.Size() f.Size = fi.Size()
f.Type = protocol.FileInfoTypeFile f.Type = protocol.FileInfoTypeFile
if ct := fi.InodeChangeTime(); !ct.IsZero() {
f.InodeChangeNs = ct.UnixNano()
} else {
f.InodeChangeNs = 0
}
return f, nil return f, nil
} }

View File

@ -55,10 +55,31 @@ message FolderConfiguration {
bool case_sensitive_fs = 33 [(ext.goname) = "CaseSensitiveFS", (ext.xml) = "caseSensitiveFS", (ext.json) = "caseSensitiveFS"]; bool case_sensitive_fs = 33 [(ext.goname) = "CaseSensitiveFS", (ext.xml) = "caseSensitiveFS", (ext.json) = "caseSensitiveFS"];
bool follow_junctions = 34 [(ext.goname) = "JunctionsAsDirs", (ext.xml) = "junctionsAsDirs", (ext.json) = "junctionsAsDirs"]; bool follow_junctions = 34 [(ext.goname) = "JunctionsAsDirs", (ext.xml) = "junctionsAsDirs", (ext.json) = "junctionsAsDirs"];
bool sync_ownership = 35; bool sync_ownership = 35;
bool scan_ownership = 36; bool send_ownership = 36;
bool sync_xattrs = 37;
bool send_xattrs = 38;
XattrFilter xattr_filter = 39;
// Legacy deprecated // Legacy deprecated
bool read_only = 9000 [deprecated=true, (ext.xml) = "ro,attr,omitempty"]; bool read_only = 9000 [deprecated=true, (ext.xml) = "ro,attr,omitempty"];
double min_disk_free_pct = 9001 [deprecated=true]; double min_disk_free_pct = 9001 [deprecated=true];
int32 pullers = 9002 [deprecated=true]; int32 pullers = 9002 [deprecated=true];
bool scan_ownership = 9003 [deprecated=true];
}
// Extended attribute filter. This is a list of patterns to match (glob
// style), each with an action (permit or deny). First match is used. If the
// filter is empty, all strings are permitted. If the filter is non-empty,
// the default action becomes deny. To counter this, you can use the "*"
// pattern to match all strings at the end of the filter. There are also
// limits on the size of accepted attributes.
message XattrFilter {
repeated XattrFilterEntry entries = 1 [(ext.xml) = "entry"];
int32 max_single_entry_size = 2 [(ext.xml) = "maxSingleEntrySize", (ext.default) = "1024"];
int32 max_total_size = 3 [(ext.xml) = "maxTotalSize", (ext.default) = "4096"];
}
message XattrFilterEntry {
string match = 1 [(ext.xml) = "match,attr"];
bool permit = 2 [(ext.xml) = "permit,attr"];
} }

View File

@ -39,8 +39,9 @@ message FileInfoTruncated {
protocol.PlatformData platform = 14; protocol.PlatformData platform = 14;
// see bep.proto // see bep.proto
uint32 local_flags = 1000; uint32 local_flags = 1000;
bytes version_hash = 1001; bytes version_hash = 1001;
int64 inode_change_ns = 1002;
bool deleted = 6; bool deleted = 6;
bool invalid = 7 [(ext.goname) = "RawInvalid"]; bool invalid = 7 [(ext.goname) = "RawInvalid"];

View File

@ -114,10 +114,15 @@ message FileInfo {
// received (we make sure to zero it), nonetheless we need it on our // 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. // struct and to be able to serialize it to/from the database.
uint32 local_flags = 1000; uint32 local_flags = 1000;
// The version_hash is an implementation detail and not part of the wire // The version_hash is an implementation detail and not part of the wire
// format. // format.
bytes version_hash = 1001; bytes version_hash = 1001;
// The time when the inode was last changed (i.e., permissions, xattrs
// etc changed). This is host-local, not sent over the wire.
int64 inode_change_ns = 1002;
bool deleted = 6; bool deleted = 6;
bool invalid = 7 [(ext.goname) = "RawInvalid"]; bool invalid = 7 [(ext.goname) = "RawInvalid"];
bool no_permissions = 8; bool no_permissions = 8;
@ -151,6 +156,10 @@ message Counter {
message PlatformData { message PlatformData {
UnixData unix = 1 [(gogoproto.nullable) = true]; UnixData unix = 1 [(gogoproto.nullable) = true];
WindowsData windows = 2 [(gogoproto.nullable) = true]; WindowsData windows = 2 [(gogoproto.nullable) = true];
XattrData linux = 3 [(gogoproto.nullable) = true];
XattrData darwin = 4 [(gogoproto.nullable) = true];
XattrData freebsd = 5 [(gogoproto.nullable) = true, (ext.goname) = "FreeBSD"];
XattrData netbsd = 6 [(gogoproto.nullable) = true, (ext.goname) = "NetBSD"];
} }
message UnixData { message UnixData {
@ -171,6 +180,15 @@ message WindowsData {
bool owner_is_group = 2; bool owner_is_group = 2;
} }
message XattrData {
repeated Xattr xattrs = 1;
}
message Xattr {
string name = 1;
bytes value = 2;
}
// Request // Request
message Request { message Request {