diff --git a/internal/db/sqlite/db_test.go b/internal/db/sqlite/db_test.go index c6e2528e9..44c9a5d68 100644 --- a/internal/db/sqlite/db_test.go +++ b/internal/db/sqlite/db_test.go @@ -1109,6 +1109,54 @@ func TestErrorWrap(t *testing.T) { } } +func TestStrangeDeletedGlobalBug(t *testing.T) { + // This exercises an edge case with serialisation and ordering of + // version vectors. It does not need to make sense, it just needs to + // pass. + + t.Parallel() + + sdb, err := OpenTemp() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := sdb.Close(); err != nil { + t.Fatal(err) + } + }) + + // One remote device announces the original version of the file + + file := genFile("test", 1, 1) + file.Version = protocol.Vector{Counters: []protocol.Counter{{ID: 35494436325452, Value: 1742900373}}} + t.Log("orig", file.Version) + sdb.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{file}) + + // Another one announces a newer one that is deleted + + del := file + del.SetDeleted(43) + del.Version = protocol.Vector{Counters: []protocol.Counter{{ID: 55445057455644, Value: 1742918457}, {ID: 35494436325452, Value: 1742900373}}} + t.Log("del", del.Version) + sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{del}) + + // We have an instance of the original file + + sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}) + + // Which one is the global? It should be the deleted one, clearly. + + g, _, err := sdb.GetGlobalFile(folderID, "test") + if err != nil { + t.Fatal(err) + } + if !g.Deleted { + t.Log(g) + t.Fatal("should be deleted") + } +} + func mustCollect[T any](t *testing.T) func(it iter.Seq[T], errFn func() error) []T { t.Helper() return func(it iter.Seq[T], errFn func() error) []T { diff --git a/internal/db/sqlite/util.go b/internal/db/sqlite/util.go index 3d734d711..dfed2857c 100644 --- a/internal/db/sqlite/util.go +++ b/internal/db/sqlite/util.go @@ -7,9 +7,11 @@ package sqlite import ( + "cmp" "database/sql/driver" "errors" "iter" + "slices" "github.com/jmoiron/sqlx" "github.com/syncthing/syncthing/internal/gen/bep" @@ -71,6 +73,11 @@ func (v *dbVector) Scan(value any) error { if err != nil { return wrap(err) } + + // This is only necessary because I messed up counter serialisation and + // thereby ordering in 2.0.0 betas, and can be removed in the future. + slices.SortFunc(vec.Counters, func(a, b protocol.Counter) int { return cmp.Compare(a.ID, b.ID) }) + v.Vector = vec return nil diff --git a/internal/db/sqlite/util_test.go b/internal/db/sqlite/util_test.go new file mode 100644 index 000000000..eb78a5782 --- /dev/null +++ b/internal/db/sqlite/util_test.go @@ -0,0 +1,33 @@ +// Copyright (C) 2025 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package sqlite + +import ( + "testing" + + "github.com/syncthing/syncthing/lib/protocol" +) + +func TestDbvector(t *testing.T) { + vec := protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 7}, {ID: 123456789, Value: 42424242}}} + dbVec := dbVector{vec} + val, err := dbVec.Value() + if err != nil { + t.Fatal(val) + } + + var dbVec2 dbVector + if err := dbVec2.Scan(val); err != nil { + t.Fatal(err) + } + + if !dbVec2.Vector.Equal(vec) { + t.Log(vec) + t.Log(dbVec2.Vector) + t.Fatal("should match") + } +} diff --git a/lib/protocol/vector.go b/lib/protocol/vector.go index 91ee274f7..adaaa7494 100644 --- a/lib/protocol/vector.go +++ b/lib/protocol/vector.go @@ -31,7 +31,9 @@ func (v *Vector) String() string { if i > 0 { buf.WriteRune(',') } - fmt.Fprintf(&buf, "%x:%d", c.ID, c.Value) + var idbs [8]byte + binary.BigEndian.PutUint64(idbs[:], uint64(c.ID)) + fmt.Fprintf(&buf, "%x:%d", idbs, c.Value) } return buf.String() }