nandi/gleanpublic⑂ Fork 0
⑂ 450d849
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Escape special characters in FTS5 search queriesUnverified

Julien Robert committed 2026-04-21T14:39:33+02:00 Browse files
450d849 parent: dc0dcf0
modified internal/db/article.go +18 -1
@@ -5,6 +5,7 @@ import (
55 "database/sql"
66 "strings"
77 "time"
8+ "unicode"
89 )
910
1011 type Article struct {
@@ -295,11 +296,27 @@ func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.T
295296 return count, err
296297 }
297298
299+func escapeFTS5(query string) string {
300+ var b strings.Builder
301+ b.Grow(len(query))
302+ for _, r := range query {
303+ if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) {
304+ b.WriteRune(r)
305+ }
306+ }
307+ return b.String()
308+}
309+
298310 func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) {
299311 if strings.TrimSpace(query) == "" {
300312 return nil, nil
301313 }
302314
315+ safeQuery := escapeFTS5(query)
316+ if strings.TrimSpace(safeQuery) == "" {
317+ return nil, nil
318+ }
319+
303320 rows, err := db.QueryContext(ctx, `
304321 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
305322 a.published, a.updated, a.fetched_at,
@@ -312,7 +329,7 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit,
312329 WHERE articles_fts MATCH ?
313330 ORDER BY ft.rank
314331 LIMIT ? OFFSET ?
315- `, userDID, userDID, query, limit, offset)
332+ `, userDID, userDID, safeQuery, limit, offset)
316333 if err != nil {
317334 return nil, err
318335 }
@@ -5,6 +5,7 @@ import (
5 "database/sql"5 "database/sql"
6 "strings"6 "strings"
7 "time"7 "time"
8+ "unicode"
8 )9 )
9 10
10 type Article struct {11 type Article struct {
@@ -295,11 +296,27 @@ func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.T
295 return count, err296 return count, err
296 }297 }
297 298
299+func escapeFTS5(query string) string {
300+ var b strings.Builder
301+ b.Grow(len(query))
302+ for _, r := range query {
303+ if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) {
304+ b.WriteRune(r)
305+ }
306+ }
307+ return b.String()
308+}
309+
298 func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) {310 func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) {
299 if strings.TrimSpace(query) == "" {311 if strings.TrimSpace(query) == "" {
300 return nil, nil312 return nil, nil
301 }313 }
302 314
315+ safeQuery := escapeFTS5(query)
316+ if strings.TrimSpace(safeQuery) == "" {
317+ return nil, nil
318+ }
319+
303 rows, err := db.QueryContext(ctx, `320 rows, err := db.QueryContext(ctx, `
304 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,321 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
305 a.published, a.updated, a.fetched_at,322 a.published, a.updated, a.fetched_at,
@@ -312,7 +329,7 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit,
312 WHERE articles_fts MATCH ?329 WHERE articles_fts MATCH ?
313 ORDER BY ft.rank330 ORDER BY ft.rank
314 LIMIT ? OFFSET ?331 LIMIT ? OFFSET ?
315- `, userDID, userDID, query, limit, offset)332+ `, userDID, userDID, safeQuery, limit, offset)
316 if err != nil {333 if err != nil {
317 return nil, err334 return nil, err
318 }335 }
modified internal/db/article_test.go +19 -0
@@ -324,3 +324,22 @@ func TestSearchArticles_EmptyQuery(t *testing.T) {
324324 assert.NilError(t, err)
325325 assert.Equal(t, len(results), 0)
326326 }
327+
328+func TestSearchArticles_SpecialCharactersNoError(t *testing.T) {
329+ ctx := context.Background()
330+ db := setupTestDB(t)
331+ userDID, _ := seedSearchData(t, ctx, db)
332+
333+ _, err := db.SearchArticles(ctx, userDID, "test.example.com/path?q=1&b=2", 10, 0)
334+ assert.NilError(t, err)
335+}
336+
337+func TestSearchArticles_OnlySpecialCharacters(t *testing.T) {
338+ ctx := context.Background()
339+ db := setupTestDB(t)
340+ userDID, _ := seedSearchData(t, ctx, db)
341+
342+ results, err := db.SearchArticles(ctx, userDID, "...///:::!!!", 10, 0)
343+ assert.NilError(t, err)
344+ assert.Equal(t, len(results), 0)
345+}
@@ -324,3 +324,22 @@ func TestSearchArticles_EmptyQuery(t *testing.T) {
324 assert.NilError(t, err)324 assert.NilError(t, err)
325 assert.Equal(t, len(results), 0)325 assert.Equal(t, len(results), 0)
326 }326 }
327+
328+func TestSearchArticles_SpecialCharactersNoError(t *testing.T) {
329+ ctx := context.Background()
330+ db := setupTestDB(t)
331+ userDID, _ := seedSearchData(t, ctx, db)
332+
333+ _, err := db.SearchArticles(ctx, userDID, "test.example.com/path?q=1&b=2", 10, 0)
334+ assert.NilError(t, err)
335+}
336+
337+func TestSearchArticles_OnlySpecialCharacters(t *testing.T) {
338+ ctx := context.Background()
339+ db := setupTestDB(t)
340+ userDID, _ := seedSearchData(t, ctx, db)
341+
342+ results, err := db.SearchArticles(ctx, userDID, "...///:::!!!", 10, 0)
343+ assert.NilError(t, err)
344+ assert.Equal(t, len(results), 0)
345+}