Skip to main content

max / balanced_breakfast

2.8 KB · 80 lines History Blame Raw
1 //! Database repositories for feeds, items, and busser state.
2 //!
3 //! Each repository wraps an SQLite connection pool and provides typed
4 //! CRUD operations for a single table.
5
6 use chrono::{DateTime, Utc};
7 use sqlx::SqlitePool;
8
9 use bb_interface::{ErrorCategory, StructuredError};
10
11 use crate::TIMESTAMP_FMT;
12 use crate::id_types::{BookmarkId, BusserStateId, FeedId, ItemId, QueryFeedId};
13 use crate::models::{
14 CreateBookmark, CreateFeed, CreateFeedItem, CreateQueryFeed, DbBookmark, DbBusserState, DbFeed,
15 DbFeedItem, DbQueryFeed, QueryCondition, UpdateBookmark,
16 };
17
18 /// Sanitize a user-provided search string for FTS5 MATCH syntax.
19 ///
20 /// Wraps each word in double quotes to prevent FTS5 syntax injection
21 /// (e.g. a user typing `AND`, `OR`, `NOT`, `NEAR`, or `NEAR/N` won't be
22 /// interpreted as FTS5 operators). Words are joined with spaces (implicit AND).
23 ///
24 /// Inside quoted strings FTS5 still honours two special characters:
25 /// - `*` at the end of the last token triggers prefix matching
26 /// - `^` at the start of the first token triggers beginning-of-column matching
27 ///
28 /// We strip those so user input like `^hello` or `world*` is treated literally.
29 /// The `column:` prefix syntax (e.g. `title:word`) is neutralised by the quoting
30 /// itself, colons inside double-quoted strings are treated as literal characters.
31 fn sanitize_fts_query(query: &str) -> String {
32 query
33 .split_whitespace()
34 .map(|word| {
35 // Escape embedded double quotes (FTS5 uses "" to represent a literal ")
36 let escaped = word.replace('"', "\"\"");
37 // Strip `^` prefix and `*` suffix, both are special inside FTS5 quotes
38 let escaped = escaped.trim_start_matches('^');
39 let escaped = escaped.trim_end_matches('*');
40 format!("\"{escaped}\"")
41 })
42 // Drop tokens that became empty after stripping (e.g. bare `^`, `*`, `^*`)
43 .filter(|token| token != "\"\"")
44 .collect::<Vec<_>>()
45 .join(" ")
46 }
47
48 /// Maximum allowed length for a search query string.
49 ///
50 /// Queries longer than this are rejected early (returning an empty result set)
51 /// to prevent excessively large FTS5 MATCH expressions from consuming memory
52 /// or CPU in SQLite.
53 const MAX_SEARCH_QUERY_LENGTH: usize = 500;
54
55 /// Number of consecutive failures before a feed is automatically disabled.
56 ///
57 /// Once a feed accumulates this many failures without a successful fetch,
58 /// the circuit breaker trips: the feed is marked `circuit_broken = 1` and
59 /// excluded from automatic fetch scheduling until manually reset.
60 pub const CIRCUIT_BREAKER_THRESHOLD: i64 = 10;
61
62 mod bookmarks;
63 mod config;
64 mod feeds;
65 mod items;
66 mod query_feeds;
67 mod state;
68 mod tags;
69
70 pub use bookmarks::*;
71 pub use config::*;
72 pub use feeds::*;
73 pub use items::*;
74 pub use query_feeds::*;
75 pub use state::*;
76 pub use tags::*;
77
78 #[cfg(test)]
79 mod tests;
80