diff --git a/internal/db/sqlite/basedb.go b/internal/db/sqlite/basedb.go index 0a2a38036..b421bd9fa 100644 --- a/internal/db/sqlite/basedb.go +++ b/internal/db/sqlite/basedb.go @@ -10,6 +10,7 @@ import ( "database/sql" "embed" "io/fs" + "net/url" "path/filepath" "strconv" "strings" @@ -44,7 +45,12 @@ type baseDB struct { func openBase(path string, maxConns int, pragmas, schemaScripts, migrationScripts []string) (*baseDB, error) { // Open the database with options to enable foreign keys and recursive // triggers (needed for the delete+insert triggers on row replace). - sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions) + pathURL := url.URL{ + Scheme: "file", + Path: fileToUriPath(path), + RawQuery: commonOptions, + } + sqlDB, err := sqlx.Open(dbDriver, pathURL.String()) if err != nil { return nil, wrap(err) } @@ -110,6 +116,16 @@ func openBase(path string, maxConns int, pragmas, schemaScripts, migrationScript return db, nil } +func fileToUriPath(path string) string { + path = filepath.ToSlash(path) + if (build.IsWindows && len(path) >= 2 && path[1] == ':') || + (strings.HasPrefix(path, "//") && !strings.HasPrefix(path, "///")) { + // Add an extra leading slash for Windows drive letter or UNC path + path = "/" + path + } + return path +} + func (s *baseDB) Close() error { s.updateLock.Lock() s.statementsMut.Lock() diff --git a/internal/db/sqlite/db_test.go b/internal/db/sqlite/db_test.go index e1ccc44a3..6010d76b5 100644 --- a/internal/db/sqlite/db_test.go +++ b/internal/db/sqlite/db_test.go @@ -12,6 +12,8 @@ import ( "encoding/binary" "errors" "iter" + "os" + "path" "path/filepath" "sync" "testing" @@ -20,6 +22,7 @@ import ( "github.com/syncthing/syncthing/internal/db" "github.com/syncthing/syncthing/internal/itererr" "github.com/syncthing/syncthing/internal/timeutil" + "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/protocol" ) @@ -1157,6 +1160,47 @@ func TestStrangeDeletedGlobalBug(t *testing.T) { } } +func TestOpenSpecialName(t *testing.T) { + dir := t.TempDir() + + // Create a "base" dir that is in the way if the path becomes + // incorrectly truncated in the next steps. + base := path.Join(dir, "test") + if err := os.Mkdir(base, 0o755); err != nil { + t.Fatal(err) + } + + // Should be able to open a path with a hash sign in it. + p1 := base + "#foo" + db, err := Open(p1) + if err != nil { + t.Fatal(err) + } + t.Log(db.path) + db.Close() + + if !build.IsWindows { + // Should be able to open a path with something that looks like + // query params. + p2 := base + "?foo=bar" + db, err = Open(p2) + if err != nil { + t.Fatal(err) + } + t.Log(db.path) + db.Close() + } + + // Better not a have problem with a single ampersand either. + p2 := base + "&foo" + db, err = Open(p2) + if err != nil { + t.Fatal(err) + } + t.Log(db.path) + db.Close() +} + 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 {