1use super::request::InfoConfiguration;
6use super::{CollectionKeys, GlobalState};
7use crate::engine::{CollSyncIds, EngineSyncAssociation, SyncEngine};
8use crate::error;
9use crate::error::{info, trace, warn};
10use crate::KeyBundle;
11use crate::ServerTimestamp;
12
13#[derive(Debug, Clone)]
16pub struct CollState {
17 pub config: InfoConfiguration,
19 pub last_modified: ServerTimestamp,
21 pub key: KeyBundle,
22}
23
24#[derive(Debug)]
26pub enum LocalCollState {
27 Unknown { assoc: EngineSyncAssociation },
30
31 Declined,
33
34 NoSuchCollection,
38
39 SyncIdChanged { ids: CollSyncIds },
41
42 Ready { coll_state: CollState },
44}
45
46pub struct LocalCollStateMachine<'state> {
47 global_state: &'state GlobalState,
48 root_key: &'state KeyBundle,
49}
50
51impl<'state> LocalCollStateMachine<'state> {
52 fn advance(
53 &self,
54 from: LocalCollState,
55 engine: &dyn SyncEngine,
56 ) -> error::Result<LocalCollState> {
57 let name = &engine.collection_name().to_string();
58 let meta_global = &self.global_state.global;
59 match from {
60 LocalCollState::Unknown { assoc } => {
61 if meta_global.declined.contains(name) {
62 return Ok(LocalCollState::Declined);
63 }
64 match meta_global.engines.get(name) {
65 Some(engine_meta) => match assoc {
66 EngineSyncAssociation::Disconnected => Ok(LocalCollState::SyncIdChanged {
67 ids: CollSyncIds {
68 global: meta_global.sync_id.clone(),
69 coll: engine_meta.sync_id.clone(),
70 },
71 }),
72 EngineSyncAssociation::Connected(ref ids)
73 if ids.global == meta_global.sync_id
74 && ids.coll == engine_meta.sync_id =>
75 {
76 let coll_keys = CollectionKeys::from_encrypted_payload(
78 self.global_state.keys.clone(),
79 self.global_state.keys_timestamp,
80 self.root_key,
81 )?;
82 let key = coll_keys.key_for_collection(name).clone();
83 let name = engine.collection_name();
84 let config = self.global_state.config.clone();
85 let last_modified = self
86 .global_state
87 .collections
88 .get(name.as_ref())
89 .cloned()
90 .unwrap_or_default();
91 let coll_state = CollState {
92 config,
93 last_modified,
94 key,
95 };
96 Ok(LocalCollState::Ready { coll_state })
97 }
98 _ => Ok(LocalCollState::SyncIdChanged {
99 ids: CollSyncIds {
100 global: meta_global.sync_id.clone(),
101 coll: engine_meta.sync_id.clone(),
102 },
103 }),
104 },
105 None => Ok(LocalCollState::NoSuchCollection),
106 }
107 }
108
109 LocalCollState::Declined => unreachable!("can't advance from declined"),
110
111 LocalCollState::NoSuchCollection => unreachable!("the collection is unknown"),
112
113 LocalCollState::SyncIdChanged { ids } => {
114 let assoc = EngineSyncAssociation::Connected(ids);
115 info!("Resetting {} engine", engine.collection_name());
116 engine.reset(&assoc)?;
117 Ok(LocalCollState::Unknown { assoc })
118 }
119
120 LocalCollState::Ready { .. } => unreachable!("can't advance from ready"),
121 }
122 }
123
124 fn run_and_run_as_farst_as_you_can(
126 &mut self,
127 engine: &dyn SyncEngine,
128 ) -> error::Result<Option<CollState>> {
129 let mut s = LocalCollState::Unknown {
130 assoc: engine.get_sync_assoc()?,
131 };
132 let mut count = 0;
135 loop {
136 trace!("LocalCollState in {:?}", s);
137 match s {
138 LocalCollState::Ready { coll_state } => return Ok(Some(coll_state)),
139 LocalCollState::Declined | LocalCollState::NoSuchCollection => return Ok(None),
140 _ => {
141 count += 1;
142 if count > 10 {
143 warn!("LocalCollStateMachine appears to be looping");
144 return Ok(None);
145 }
146 s = self.advance(s, engine)?;
149 }
150 };
151 }
152 }
153
154 pub fn get_state(
155 engine: &dyn SyncEngine,
156 global_state: &'state GlobalState,
157 root_key: &'state KeyBundle,
158 ) -> error::Result<Option<CollState>> {
159 let mut gingerbread_man = Self {
160 global_state,
161 root_key,
162 };
163 gingerbread_man.run_and_run_as_farst_as_you_can(engine)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::super::request::{InfoCollections, InfoConfiguration};
170 use super::super::CollectionKeys;
171 use super::*;
172 use crate::bso::{IncomingBso, OutgoingBso};
173 use crate::engine::CollectionRequest;
174 use crate::record_types::{MetaGlobalEngine, MetaGlobalRecord};
175 use crate::{telemetry, CollectionName};
176 use anyhow::Result;
177 use nss::ensure_initialized;
178 use std::cell::{Cell, RefCell};
179 use std::collections::HashMap;
180 use sync_guid::Guid;
181
182 fn get_global_state(root_key: &KeyBundle) -> GlobalState {
183 let keys = CollectionKeys::new_random()
184 .unwrap()
185 .to_encrypted_payload(root_key)
186 .unwrap();
187 GlobalState {
188 config: InfoConfiguration::default(),
189 collections: InfoCollections::new(HashMap::new()),
190 global: MetaGlobalRecord {
191 sync_id: "syncIDAAAAAA".into(),
192 storage_version: 5usize,
193 engines: vec![(
194 "bookmarks",
195 MetaGlobalEngine {
196 version: 1usize,
197 sync_id: "syncIDBBBBBB".into(),
198 },
199 )]
200 .into_iter()
201 .map(|(key, value)| (key.to_owned(), value))
202 .collect(),
203 declined: vec![],
204 },
205 global_timestamp: ServerTimestamp::default(),
206 keys,
207 keys_timestamp: ServerTimestamp::default(),
208 }
209 }
210
211 struct TestSyncEngine {
212 collection_name: &'static str,
213 assoc: Cell<EngineSyncAssociation>,
214 num_resets: RefCell<usize>,
215 }
216
217 impl TestSyncEngine {
218 fn new(collection_name: &'static str, assoc: EngineSyncAssociation) -> Self {
219 Self {
220 collection_name,
221 assoc: Cell::new(assoc),
222 num_resets: RefCell::new(0),
223 }
224 }
225 fn get_num_resets(&self) -> usize {
226 *self.num_resets.borrow()
227 }
228 }
229
230 impl SyncEngine for TestSyncEngine {
231 fn collection_name(&self) -> CollectionName {
232 self.collection_name.into()
233 }
234
235 fn stage_incoming(
236 &self,
237 _inbound: Vec<IncomingBso>,
238 _telem: &mut telemetry::Engine,
239 ) -> Result<()> {
240 unreachable!("these tests shouldn't call these");
241 }
242
243 fn apply(
244 &self,
245 _timestamp: ServerTimestamp,
246 _telem: &mut telemetry::Engine,
247 ) -> Result<Vec<OutgoingBso>> {
248 unreachable!("these tests shouldn't call these");
249 }
250
251 fn set_uploaded(&self, _new_timestamp: ServerTimestamp, _ids: Vec<Guid>) -> Result<()> {
252 unreachable!("these tests shouldn't call these");
253 }
254
255 fn sync_finished(&self) -> Result<()> {
256 unreachable!("these tests shouldn't call these");
257 }
258
259 fn get_collection_request(
260 &self,
261 _server_timestamp: ServerTimestamp,
262 ) -> Result<Option<CollectionRequest>> {
263 unreachable!("these tests shouldn't call these");
264 }
265
266 fn get_sync_assoc(&self) -> Result<EngineSyncAssociation> {
267 Ok(self.assoc.replace(EngineSyncAssociation::Disconnected))
268 }
269
270 fn reset(&self, new_assoc: &EngineSyncAssociation) -> Result<()> {
271 self.assoc.replace(new_assoc.clone());
272 *self.num_resets.borrow_mut() += 1;
273 Ok(())
274 }
275
276 fn wipe(&self) -> Result<()> {
277 unreachable!("these tests shouldn't call these");
278 }
279 }
280
281 #[test]
282 fn test_unknown() {
283 ensure_initialized();
284 let root_key = KeyBundle::new_random().expect("should work");
285 let gs = get_global_state(&root_key);
286 let engine = TestSyncEngine::new("unknown", EngineSyncAssociation::Disconnected);
287 let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
288 assert!(cs.is_none(), "unknown collection name can't sync");
289 assert_eq!(engine.get_num_resets(), 0);
290 }
291
292 #[test]
293 fn test_known_no_state() {
294 ensure_initialized();
295 let root_key = KeyBundle::new_random().expect("should work");
296 let gs = get_global_state(&root_key);
297 let engine = TestSyncEngine::new("bookmarks", EngineSyncAssociation::Disconnected);
298 let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
299 assert!(cs.is_some(), "collection can sync");
300 assert_eq!(
301 engine.assoc.replace(EngineSyncAssociation::Disconnected),
302 EngineSyncAssociation::Connected(CollSyncIds {
303 global: "syncIDAAAAAA".into(),
304 coll: "syncIDBBBBBB".into(),
305 })
306 );
307 assert_eq!(engine.get_num_resets(), 1);
308 }
309
310 #[test]
311 fn test_known_wrong_state() {
312 ensure_initialized();
313 let root_key = KeyBundle::new_random().expect("should work");
314 let gs = get_global_state(&root_key);
315 let engine = TestSyncEngine::new(
316 "bookmarks",
317 EngineSyncAssociation::Connected(CollSyncIds {
318 global: "syncIDXXXXXX".into(),
319 coll: "syncIDYYYYYY".into(),
320 }),
321 );
322 let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
323 assert!(cs.is_some(), "collection can sync");
324 assert_eq!(
325 engine.assoc.replace(EngineSyncAssociation::Disconnected),
326 EngineSyncAssociation::Connected(CollSyncIds {
327 global: "syncIDAAAAAA".into(),
328 coll: "syncIDBBBBBB".into(),
329 })
330 );
331 assert_eq!(engine.get_num_resets(), 1);
332 }
333
334 #[test]
335 fn test_known_good_state() {
336 ensure_initialized();
337 let root_key = KeyBundle::new_random().expect("should work");
338 let gs = get_global_state(&root_key);
339 let engine = TestSyncEngine::new(
340 "bookmarks",
341 EngineSyncAssociation::Connected(CollSyncIds {
342 global: "syncIDAAAAAA".into(),
343 coll: "syncIDBBBBBB".into(),
344 }),
345 );
346 let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
347 assert!(cs.is_some(), "collection can sync");
348 assert_eq!(engine.get_num_resets(), 0);
349 }
350
351 #[test]
352 fn test_declined() {
353 ensure_initialized();
354 let root_key = KeyBundle::new_random().expect("should work");
355 let mut gs = get_global_state(&root_key);
356 gs.global.declined.push("bookmarks".to_string());
357 let engine = TestSyncEngine::new(
358 "bookmarks",
359 EngineSyncAssociation::Connected(CollSyncIds {
360 global: "syncIDAAAAAA".into(),
361 coll: "syncIDBBBBBB".into(),
362 }),
363 );
364 let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
365 assert!(cs.is_none(), "declined collection can sync");
366 assert_eq!(engine.get_num_resets(), 0);
367 }
368}