Compare commits

..
3 Commits
Author SHA1 Message Date
Jakob Borg aa65c6f249 build: Dockerfile that expects ready baked binaries 2020-09-15 19:38:23 +02:00
Simon FreiandGitHub 8210466b03 lib/model: Consider case conflicts when checking to be deleted items (#6986) 2020-09-12 07:45:50 +02:00
greatroarandGitHub 3e24d82513 lib/fs: Optimize UnicodeLowercase (#6979)
Most notably, it now detects all-lowercase files and returns these
as-is. The tests have been expanded with two cases and are now used
as a benchmark (admittedly a rather trivial one).

name                           old time/op    new time/op    delta
UnicodeLowercaseMaybeChange-8    4.59µs ± 2%    4.57µs ± 1%    ~     (p=0.197 n=10+10)
UnicodeLowercaseNoChange-8       3.26µs ± 1%    3.09µs ± 1%  -5.27%  (p=0.000 n=9+10)
2020-09-11 09:16:10 +02:00
5 changed files with 158 additions and 62 deletions
+19
View File
@@ -0,0 +1,19 @@
FROM alpine
ARG TARGETARCH
EXPOSE 8384 22000 21027/udp
VOLUME ["/var/syncthing"]
RUN apk add --no-cache ca-certificates su-exec tzdata
COPY ./syncthing-linux-$TARGETARCH /bin/syncthing
COPY ./script/docker-entrypoint.sh /bin/entrypoint.sh
ENV PUID=1000 PGID=1000 HOME=/var/syncthing
HEALTHCHECK --interval=1m --timeout=10s \
CMD nc -z 127.0.0.1 8384 || exit 1
ENV STGUIADDRESS=0.0.0.0:8384
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing", "-home", "/var/syncthing/config"]
+30 -4
View File
@@ -7,13 +7,39 @@
package fs
import (
"strings"
"unicode"
"unicode/utf8"
)
func UnicodeLowercase(s string) string {
rs := []rune(s)
for i, r := range rs {
rs[i] = unicode.ToLower(unicode.ToUpper(r))
i := firstCaseChange(s)
if i == -1 {
return s
}
return string(rs)
var rs strings.Builder
// WriteRune always reserves utf8.UTFMax bytes for non-ASCII runes,
// even if it doesn't need all that space. Overallocate now to prevent
// it from ever triggering a reallocation.
rs.Grow(utf8.UTFMax - 1 + len(s))
rs.WriteString(s[:i])
for _, r := range s[i:] {
rs.WriteRune(unicode.ToLower(unicode.ToUpper(r)))
}
return rs.String()
}
// Byte index of the first rune r s.t. lower(upper(r)) != r.
func firstCaseChange(s string) int {
for i, r := range s {
if r <= unicode.MaxASCII && (r < 'A' || r > 'Z') {
continue
}
if unicode.ToLower(unicode.ToUpper(r)) != r {
return i
}
}
return -1
}
+60 -34
View File
@@ -6,45 +6,71 @@
package fs
import "testing"
import (
"testing"
)
var caseCases = [][2]string{
{"", ""},
{"hej", "hej"},
{"HeJ!@#", "hej!@#"},
// Western Europe diacritical stuff is trivial.
{"ÜBERRÄKSMÖRGÅS", "überräksmörgås"},
// As are ligatures.
{"Æglefinus", "æglefinus"},
{"IJssel", "ijssel"},
// Cyrillic seems regular as well.
{"Привет", "привет"},
// Greek has multiple lower case characters for things depending on
// context; we should always choose the same one.
{"Ὀδυσσεύς", "ὀδυσσεύσ"},
{"ὈΔΥΣΣΕΎΣ", "ὀδυσσεύσ"},
// German ß doesn't really have an upper case variant, and we
// shouldn't mess things up when lower casing it either. We don't
// attempt to make ß equivalent to "ss".
{"Reichwaldstraße", "reichwaldstraße"},
// The Turks do their thing with the Is.... Like the Greek example
// we pick just the one canonicalized "i" although you can argue
// with this... From what I understand most operating systems don't
// get this right anyway.
{"İI", "ii"},
// Arabic doesn't do case folding.
{"العَرَبِيَّة", "العَرَبِيَّة"},
// Neither does Hebrew.
{"עברית", "עברית"},
// Nor Chinese, in any variant.
{"汉语/漢語 or 中文", "汉语/漢語 or 中文"},
// Nor katakana, as far as I can tell.
{"チャーハン", "チャーハン"},
// Some special Unicode characters, however, are folded by OSes.
{"\u212A", "k"},
}
func TestUnicodeLowercase(t *testing.T) {
cases := [][2]string{
{"", ""},
{"hej", "hej"},
{"HeJ!@#", "hej!@#"},
// Western Europe diacritical stuff is trivial
{"ÜBERRÄKSMÖRGÅS", "überräksmörgås"},
// Cyrillic seems regular as well
{"Привет", "привет"},
// Greek has multiple lower case characters for things depending on
// context; we should always choose the right one.
{"Ὀδυσσεύς", "ὀδυσσεύσ"},
{"ὈΔΥΣΣΕΎΣ", "ὀδυσσεύσ"},
// German ß doesn't really have an upper case variant, and we
// shouldn't mess things up when lower casing it either. We don't
// attempt to make ß equivalent to "ss".
{"Reichwaldstraße", "reichwaldstraße"},
// The Turks do their thing with the Is.... Like the Greek example
// we pick just the one canonicalized "i" although you can argue
// with this... From what I understand most operating systems don't
// get this right anyway.
{"İI", "ii"},
// Arabic doesn't do case folding.
{"العَرَبِيَّة", "العَرَبِيَّة"},
// Neither does Hebrew.
{"עברית", "עברית"},
// Nor Chinese, in any variant.
{"汉语/漢語 or 中文", "汉语/漢語 or 中文"},
// Niether katakana as far as I can tell.
{"チャーハン", "チャーハン"},
// Some special unicode characters, however, are folded by OSes
{"\u212A", "k"},
}
for _, tc := range cases {
for _, tc := range caseCases {
res := UnicodeLowercase(tc[0])
if res != tc[1] {
t.Errorf("UnicodeLowercase(%q) => %q, expected %q", tc[0], res, tc[1])
}
}
}
func BenchmarkUnicodeLowercaseMaybeChange(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, s := range caseCases {
UnicodeLowercase(s[0])
}
}
}
func BenchmarkUnicodeLowercaseNoChange(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, s := range caseCases {
UnicodeLowercase(s[1])
}
}
}
+13 -12
View File
@@ -813,9 +813,10 @@ func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, snap *db.Snapshot,
cur, hasCur := snap.Get(protocol.LocalDeviceID, file.Name)
if err = f.checkToBeDeleted(file, cur, hasCur, dbUpdateDeleteDir, dbUpdateChan, scanChan); err != nil {
if fs.IsNotExist(err) {
if err = f.checkToBeDeleted(file, cur, hasCur, scanChan); err != nil {
if fs.IsNotExist(err) || fs.IsErrCaseConflict(err) {
err = nil
dbUpdateChan <- dbUpdateJob{file, dbUpdateDeleteDir}
}
return
}
@@ -860,9 +861,10 @@ func (f *sendReceiveFolder) deleteFileWithCurrent(file, cur protocol.FileInfo, h
})
}()
if err = f.checkToBeDeleted(file, cur, hasCur, dbUpdateDeleteFile, dbUpdateChan, scanChan); err != nil {
if fs.IsNotExist(err) {
if err = f.checkToBeDeleted(file, cur, hasCur, scanChan); err != nil {
if fs.IsNotExist(err) || fs.IsErrCaseConflict(err) {
err = nil
dbUpdateChan <- dbUpdateJob{file, dbUpdateDeleteFile}
}
return
}
@@ -945,7 +947,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, sn
l.Debugln(f, "taking rename shortcut", source.Name, "->", target.Name)
// Check that source is compatible with what we have in the DB
if err = f.checkToBeDeleted(source, cur, true, dbUpdateDeleteFile, dbUpdateChan, scanChan); err != nil {
if err = f.checkToBeDeleted(source, cur, true, scanChan); err != nil {
return err
}
// Check that the target corresponds to what we have in the DB
@@ -1979,26 +1981,25 @@ func (f *sendReceiveFolder) scanIfItemChanged(name string, stat fs.FileInfo, ite
// checkToBeDeleted makes sure the file on disk is compatible with what there is
// in the DB before the caller proceeds with actually deleting it.
// I.e. non-nil error status means "Do not delete!".
func (f *sendReceiveFolder) checkToBeDeleted(file, cur protocol.FileInfo, hasCur bool, updateType dbUpdateType, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
// I.e. non-nil error status means "Do not delete!" or "is already deleted".
func (f *sendReceiveFolder) checkToBeDeleted(file, cur protocol.FileInfo, hasCur bool, scanChan chan<- string) error {
if err := osutil.TraversesSymlink(f.fs, filepath.Dir(file.Name)); err != nil {
l.Debugln(f, "not deleting item behind symlink on disk, but update db", file.Name)
dbUpdateChan <- dbUpdateJob{file, updateType}
return fs.ErrNotExist
}
stat, err := f.fs.Lstat(file.Name)
if !fs.IsNotExist(err) && err != nil {
deleted := fs.IsNotExist(err) || fs.IsErrCaseConflict(err)
if !deleted && err != nil {
return err
}
if fs.IsNotExist(err) {
if deleted {
if hasCur && !cur.Deleted && !cur.IsUnsupported() {
scanChan <- file.Name
return errModified
}
l.Debugln(f, "not deleting item we don't have, but update db", file.Name)
dbUpdateChan <- dbUpdateJob{file, updateType}
return fs.ErrNotExist
return err
}
return f.scanIfItemChanged(file.Name, stat, cur, hasCur, scanChan)
+36 -12
View File
@@ -781,18 +781,8 @@ func TestDeleteIgnorePerms(t *testing.T) {
fi, err := scanner.CreateFileInfo(stat, name, ffs)
must(t, err)
ffs.Chmod(name, 0600)
scanChan := make(chan string)
dbUpdateChan := make(chan dbUpdateJob)
finished := make(chan struct{})
go func() {
err = f.checkToBeDeleted(fi, fi, true, dbUpdateDeleteFile, dbUpdateChan, scanChan)
close(finished)
}()
select {
case <-scanChan:
<-finished
case <-finished:
}
scanChan := make(chan string, 1)
err = f.checkToBeDeleted(fi, fi, true, scanChan)
must(t, err)
}
@@ -1325,6 +1315,40 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
}
}
func TestPullDeleteCaseConflict(t *testing.T) {
m, f := setupSendReceiveFolder()
defer cleanupSRFolder(f, m)
name := "foo"
fi := protocol.FileInfo{Name: "Foo"}
dbUpdateChan := make(chan dbUpdateJob, 1)
scanChan := make(chan string)
if fd, err := f.fs.Create(name); err != nil {
t.Fatal(err)
} else {
if _, err := fd.Write([]byte("data")); err != nil {
t.Fatal(err)
}
fd.Close()
}
f.deleteFileWithCurrent(fi, protocol.FileInfo{}, false, dbUpdateChan, scanChan)
select {
case <-dbUpdateChan:
default:
t.Error("Missing db update for file")
}
snap := f.fset.Snapshot()
defer snap.Release()
f.deleteDir(fi, snap, dbUpdateChan, scanChan)
select {
case <-dbUpdateChan:
default:
t.Error("Missing db update for dir")
}
}
func cleanupSharedPullerState(s *sharedPullerState) {
s.mut.Lock()
defer s.mut.Unlock()