Skip to main content

max / balanced_breakfast

3.8 KB · 96 lines History Blame Raw
1 //! Single source of truth for `user_config` keys and their sync posture.
2 //!
3 //! BalancedBreakfast used to sync every `user_config` key unconditionally: the
4 //! table's triggers enqueued a changelog row for any key, and the initial
5 //! snapshot swept the whole table. That is the same shape audiofiles was
6 //! hardened away from (fuzz-2026-07-06 #2, -07-20 #1, -07-21 #3): a config key
7 //! that names device state must never cross the sync boundary, and "sync
8 //! everything" cannot hold one back.
9 //!
10 //! [`CONFIG`] is the family's shared [`ConfigSpec`] for BB's `user_config`
11 //! table, declaring each known key's [`Posture`]. It drives two filters:
12 //!
13 //! - the SQL export filter (the `config_key_policy` table, seeded from this spec
14 //! by [`Database::seed_config_key_policy`](crate::Database::seed_config_key_policy)
15 //! and joined by the `user_config` triggers and the initial snapshot), and
16 //! - the Rust import filter ([`key_excluded_from_sync`]), applied when a remote
17 //! pull batch is written back.
18 //!
19 //! Storage stays sqlx (BB's `ConfigRepository`), not `synckit_config::ConfigStore`
20 //! (rusqlite): this crate takes only the pure-data half of synckit-config, the
21 //! spec and posture vocabulary, so a sqlx app shares the declaration without the
22 //! rusqlite store. Posture is fail-closed: an undeclared key is
23 //! [`Local`](Posture::Local) and never syncs.
24 //!
25 //! <!-- wiki: bb-config -->
26
27 use synckit_config::{ConfigSpec, Posture};
28
29 /// BB's `user_config` posture declaration.
30 ///
31 /// Only `theme` crosses the sync boundary. `bb-welcomed` is a per-device
32 /// first-run flag (each device shows the welcome once). `bb-theme` is the legacy
33 /// theme key read once to migrate the old value into `theme`; it must not sync
34 /// on its own. Every other key the generic `set_config` command might write is
35 /// undeclared and therefore `Local` (fail-closed).
36 pub const CONFIG: ConfigSpec = ConfigSpec::new(
37 "user_config",
38 &[
39 // Synced: user preferences carried across the user's devices.
40 ("theme", Posture::Synced),
41 // Local: per-device state and the legacy alias, never synced.
42 ("bb-welcomed", Posture::Local),
43 ("bb-theme", Posture::Local),
44 ],
45 );
46
47 /// Import-side filter: true when a raw remote `user_config` key must be dropped
48 /// rather than written back. A key the spec does not clear to sync is refused,
49 /// which by the spec's fail-closed default also refuses any unknown key: a
50 /// hostile server cannot set a device-local or unrecognized key by sending it.
51 #[must_use]
52 pub fn key_excluded_from_sync(key: &str) -> bool {
53 !CONFIG.is_synced(key)
54 }
55
56 #[cfg(test)]
57 mod tests {
58 use super::*;
59
60 #[test]
61 fn only_theme_crosses_the_boundary() {
62 assert!(CONFIG.is_synced("theme"), "theme is a shared preference");
63 assert!(!CONFIG.is_synced("bb-welcomed"), "welcome is per-device");
64 assert!(
65 !CONFIG.is_synced("bb-theme"),
66 "the legacy alias never syncs"
67 );
68 }
69
70 #[test]
71 fn the_import_filter_refuses_local_and_unknown_keys() {
72 assert!(!key_excluded_from_sync("theme"), "theme is admitted");
73 assert!(
74 key_excluded_from_sync("bb-welcomed"),
75 "per-device stays home"
76 );
77 assert!(
78 key_excluded_from_sync("totally_unknown"),
79 "an unknown key fails closed",
80 );
81 }
82
83 // The policy rows the export filter is seeded from must carry every declared
84 // key, marking only `theme` replicated. If this drifted, the SQL export and
85 // the Rust import would disagree.
86 #[test]
87 fn policy_rows_replicate_only_theme() {
88 let replicated: Vec<&str> = CONFIG
89 .policy_rows()
90 .filter(|row| row.replicated)
91 .map(|row| row.key)
92 .collect();
93 assert_eq!(replicated, ["theme"]);
94 }
95 }
96