autofill/sync/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6pub mod address;
7mod bridge;
8pub use bridge::AddressesBridgedEngine;
9mod common;
10pub mod credit_card;
11pub mod engine;
12
13pub(crate) use crate::db::models::Metadata;
14use crate::error::Result;
15use error_support::{trace, warn};
16use interrupt_support::Interruptee;
17use rusqlite::Transaction;
18use sync15::bso::{IncomingBso, IncomingContent, IncomingEnvelope, IncomingKind, OutgoingBso};
19use sync15::ServerTimestamp;
20use sync_guid::Guid;
21use types::Timestamp;
22
23// This type is used as a snazzy way to capture all unknown fields from the payload
24// upon deserialization without having to work with a concrete type
25type UnknownFields = serde_json::Map<String, serde_json::Value>;
26
27// The fact that credit-card numbers are encrypted makes things a little tricky
28// for sync in various ways - and one non-obvious way is that the tables that
29// store sync payloads can't just store them directly as they are not encrypted
30// in that form.
31// ie, in the database, an address record's "payload" column looks like:
32// > '{"entry":{"address-level1":"VIC", "street-address":"2/25 Somewhere St","timeCreated":1497567116554, "version":1},"id":"29ac67adae7d"}'
33// or a tombstone: '{"deleted":true,"id":"6544992973e6"}'
34// > (Note a number of fields have been removed from 'entry' for clarity)
35// and in the database a credit-card's "payload" looks like:
36// > 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..<snip>-<snip>.<snip lots more>'
37// > while a tombstone here remains encrypted but has the 'deleted' entry after decryption.
38// (Note also that the address entry, and the decrypted credit-card json both have an "id" in
39// the JSON, but we ignore that when deserializing and will stop persisting that soon)
40
41// Some traits that help us abstract away much of the sync functionality.
42
43// A trait that abstracts the *storage* implementation of the specific record
44// types, and must be implemented by the concrete record owners.
45// Note that it doesn't assume a SQL database or anything concrete about the
46// storage, although objects implementing this trait will live only long enough
47// to perform the sync "incoming" steps - ie, a transaction is likely to live
48// exactly as long as this object.
49// XXX - *sob* - although each method has a `&Transaction` param, which in
50// theory could be avoided if the concrete impls could keep the ref (ie, if
51// it was held behind `self`), but markh failed to make this work due to
52// lifetime woes.
53pub trait ProcessIncomingRecordImpl {
54    type Record;
55
56    fn stage_incoming(
57        &self,
58        tx: &Transaction<'_>,
59        incoming: Vec<IncomingBso>,
60        signal: &dyn Interruptee,
61    ) -> Result<()>;
62
63    /// Finish the incoming phase. This will typically caused staged records
64    // to be written to the mirror.
65    fn finish_incoming(&self, tx: &Transaction<'_>) -> Result<()>;
66
67    fn fetch_incoming_states(
68        &self,
69        tx: &Transaction<'_>,
70    ) -> Result<Vec<IncomingState<Self::Record>>>;
71
72    /// Returns a local record that has the same values as the given incoming record (with the exception
73    /// of the `guid` values which should differ) that will be used as a local duplicate record for
74    /// syncing.
75    fn get_local_dupe(
76        &self,
77        tx: &Transaction<'_>,
78        incoming: &Self::Record,
79    ) -> Result<Option<Self::Record>>;
80
81    fn update_local_record(
82        &self,
83        tx: &Transaction<'_>,
84        record: Self::Record,
85        was_merged: bool,
86    ) -> Result<()>;
87
88    fn insert_local_record(&self, tx: &Transaction<'_>, record: Self::Record) -> Result<()>;
89
90    fn change_record_guid(
91        &self,
92        tx: &Transaction<'_>,
93        old_guid: &Guid,
94        new_guid: &Guid,
95    ) -> Result<()>;
96
97    fn remove_record(&self, tx: &Transaction<'_>, guid: &Guid) -> Result<()>;
98
99    fn remove_tombstone(&self, tx: &Transaction<'_>, guid: &Guid) -> Result<()>;
100}
101
102pub trait ProcessOutgoingRecordImpl {
103    type Record;
104
105    fn fetch_outgoing_records(&self, tx: &Transaction<'_>) -> anyhow::Result<Vec<OutgoingBso>>;
106
107    fn finish_synced_items(
108        &self,
109        tx: &Transaction<'_>,
110        records_synced: Vec<Guid>,
111    ) -> anyhow::Result<()>;
112}
113
114// A trait that abstracts the functionality in the record itself.
115pub trait SyncRecord {
116    fn record_name() -> &'static str; // "addresses" or similar, for logging/debuging.
117    fn id(&self) -> &Guid;
118    fn metadata(&self) -> &Metadata;
119    fn metadata_mut(&mut self) -> &mut Metadata;
120    // Merge or fork multiple copies of the same record. The resulting record
121    // might have the same guid as the inputs, meaning it was truly merged, or
122    // a different guid, in which case it was forked due to conflicting changes.
123    fn merge(incoming: &Self, local: &Self, mirror: &Option<Self>) -> MergeResult<Self>
124    where
125        Self: Sized;
126}
127
128impl Metadata {
129    /// Merge the metadata from `other`, and possibly `mirror`, into `self`
130    /// (which must already have valid metadata).
131    /// Note that mirror being None is an edge-case and typically means first
132    /// sync since a "reset" (eg, disconnecting and reconnecting.
133    pub fn merge(&mut self, other: &Metadata, mirror: Option<&Metadata>) {
134        match mirror {
135            Some(m) => {
136                fn get_latest_time(t1: Timestamp, t2: Timestamp, t3: Timestamp) -> Timestamp {
137                    std::cmp::max(t1, std::cmp::max(t2, t3))
138                }
139                fn get_earliest_time(t1: Timestamp, t2: Timestamp, t3: Timestamp) -> Timestamp {
140                    std::cmp::min(t1, std::cmp::min(t2, t3))
141                }
142                self.time_created =
143                    get_earliest_time(self.time_created, other.time_created, m.time_created);
144                self.time_last_used =
145                    get_latest_time(self.time_last_used, other.time_last_used, m.time_last_used);
146                self.time_last_modified = get_latest_time(
147                    self.time_last_modified,
148                    other.time_last_modified,
149                    m.time_last_modified,
150                );
151
152                self.times_used = m.times_used
153                    + std::cmp::max(other.times_used - m.times_used, 0)
154                    + std::cmp::max(self.times_used - m.times_used, 0);
155            }
156            None => {
157                fn get_latest_time(t1: Timestamp, t2: Timestamp) -> Timestamp {
158                    std::cmp::max(t1, t2)
159                }
160                fn get_earliest_time(t1: Timestamp, t2: Timestamp) -> Timestamp {
161                    std::cmp::min(t1, t2)
162                }
163                self.time_created = get_earliest_time(self.time_created, other.time_created);
164                self.time_last_used = get_latest_time(self.time_last_used, other.time_last_used);
165                self.time_last_modified =
166                    get_latest_time(self.time_last_modified, other.time_last_modified);
167                // No mirror is an edge-case that almost certainly means the
168                // client was disconnected and this is the first sync after
169                // reconnection. So we can't really do a simple sum() of the
170                // times_used values as if the disconnection was recent, it will
171                // be double the expected value.
172                // So we just take the largest.
173                self.times_used = std::cmp::max(other.times_used, self.times_used);
174            }
175        }
176    }
177}
178
179// A local record can be in any of these 5 states.
180#[derive(Debug)]
181enum LocalRecordInfo<T> {
182    Unmodified { record: T },
183    Modified { record: T },
184    // encrypted data was scrubbed from the local record and needs to be resynced from the server
185    Scrubbed { record: T },
186    Tombstone { guid: Guid },
187    Missing,
188}
189
190// An enum for the return value from our "merge" function, which might either
191// update the record, or might fork it.
192#[derive(Debug)]
193pub enum MergeResult<T> {
194    Merged { merged: T },
195    Forked { forked: T },
196}
197
198// This ties the 3 possible records together and is what we expect the
199// implementations to put together for us.
200#[derive(Debug)]
201pub struct IncomingState<T> {
202    incoming: IncomingContent<T>,
203    local: LocalRecordInfo<T>,
204    // We don't have an enum for the mirror - an Option<> is fine because
205    // although we do store tombstones there, we ignore them when reconciling
206    // (ie, we ignore tombstones in the mirror)
207    // don't store tombstones there.
208    mirror: Option<T>,
209}
210
211/// The distinct incoming sync actions to be performed for incoming records.
212#[derive(Debug, PartialEq)]
213enum IncomingAction<T> {
214    // Remove the local record with this GUID.
215    DeleteLocalRecord { guid: Guid },
216    // Insert a new record.
217    Insert { record: T },
218    // Update an existing record. If `was_merged` was true, then the updated
219    // record isn't identical to the incoming one, so needs to be flagged as
220    // dirty.
221    Update { record: T, was_merged: bool },
222    // We forked a record because we couldn't merge it. `forked` will have
223    // a new guid, while `incoming` is the unmodified version of the incoming
224    // record which we need to apply.
225    Fork { forked: T, incoming: T },
226    // An existing record with old_guid needs to be replaced with this record.
227    UpdateLocalGuid { old_guid: Guid, record: T },
228    // There's a remote tombstone, but our copy of the record is dirty. The
229    // remote tombstone should be replaced with this.
230    ResurrectRemoteTombstone { record: T },
231    // There's a local tombstone - it should be removed and replaced with this.
232    ResurrectLocalTombstone { record: T },
233    // Nothing to do.
234    DoNothing,
235}
236
237/// Convert a IncomingState to an IncomingAction - this is where the "policy"
238/// lives for when we resurrect, or merge etc.
239fn plan_incoming<T: std::fmt::Debug + SyncRecord>(
240    rec_impl: &dyn ProcessIncomingRecordImpl<Record = T>,
241    tx: &Transaction<'_>,
242    staged_info: IncomingState<T>,
243) -> Result<IncomingAction<T>> {
244    trace!("plan_incoming: {:?}", staged_info);
245    let IncomingState {
246        incoming,
247        local,
248        mirror,
249    } = staged_info;
250
251    let state = match incoming.kind {
252        IncomingKind::Tombstone => {
253            match local {
254                LocalRecordInfo::Unmodified { .. } | LocalRecordInfo::Scrubbed { .. } => {
255                    // Note: On desktop, when there's a local record for an incoming tombstone, a local tombstone
256                    // would created. But we don't actually need to create a local tombstone here. If we did it would
257                    // immediately be deleted after being uploaded to the server.
258                    IncomingAction::DeleteLocalRecord {
259                        guid: incoming.envelope.id,
260                    }
261                }
262                LocalRecordInfo::Modified { record } => {
263                    // Incoming tombstone with local changes should cause us to "resurrect" the local.
264                    // At a minimum, the implementation will need to ensure the record is marked as
265                    // dirty so it's uploaded, overwriting the server's tombstone.
266                    IncomingAction::ResurrectRemoteTombstone { record }
267                }
268                LocalRecordInfo::Tombstone {
269                    guid: tombstone_guid,
270                } => {
271                    assert_eq!(incoming.envelope.id, tombstone_guid);
272                    IncomingAction::DoNothing
273                }
274                LocalRecordInfo::Missing => IncomingAction::DoNothing,
275            }
276        }
277        IncomingKind::Content(mut incoming_record) => {
278            match local {
279                LocalRecordInfo::Unmodified {
280                    record: local_record,
281                }
282                | LocalRecordInfo::Scrubbed {
283                    record: local_record,
284                } => {
285                    // The local record was either unmodified, or scrubbed of its encrypted data.
286                    // Either way we want to:
287                    //   - Merge the metadata
288                    //   - Update the local record using data from the server
289                    //   - Don't flag the local item as dirty.  We don't want to reupload for just
290                    //     metadata changes.
291                    let metadata = incoming_record.metadata_mut();
292                    metadata.merge(
293                        local_record.metadata(),
294                        mirror.as_ref().map(|m| m.metadata()),
295                    );
296                    // a micro-optimization here would be to `::DoNothing` if
297                    // the metadata was actually identical and the local data wasn't scrubbed, but
298                    // this seems like an edge-case on an edge-case?
299                    IncomingAction::Update {
300                        record: incoming_record,
301                        was_merged: false,
302                    }
303                }
304                LocalRecordInfo::Modified {
305                    record: local_record,
306                } => {
307                    match SyncRecord::merge(&incoming_record, &local_record, &mirror) {
308                        MergeResult::Merged { merged } => {
309                            // The record we save locally has material differences
310                            // from the incoming one, so we are going to need to
311                            // reupload it.
312                            IncomingAction::Update {
313                                record: merged,
314                                was_merged: true,
315                            }
316                        }
317                        MergeResult::Forked { forked } => IncomingAction::Fork {
318                            forked,
319                            incoming: incoming_record,
320                        },
321                    }
322                }
323                LocalRecordInfo::Tombstone { .. } => IncomingAction::ResurrectLocalTombstone {
324                    record: incoming_record,
325                },
326                LocalRecordInfo::Missing => {
327                    match rec_impl.get_local_dupe(tx, &incoming_record)? {
328                        None => IncomingAction::Insert {
329                            record: incoming_record,
330                        },
331                        Some(local_dupe) => {
332                            // local record is missing but we found a dupe - so
333                            // the dupe must have a different guid (or we wouldn't
334                            // consider the local record missing!)
335                            assert_ne!(incoming_record.id(), local_dupe.id());
336                            // The existing item is identical except for the metadata, so
337                            // we still merge that metadata.
338                            let metadata = incoming_record.metadata_mut();
339                            metadata.merge(
340                                local_dupe.metadata(),
341                                mirror.as_ref().map(|m| m.metadata()),
342                            );
343                            IncomingAction::UpdateLocalGuid {
344                                old_guid: local_dupe.id().clone(),
345                                record: incoming_record,
346                            }
347                        }
348                    }
349                }
350            }
351        }
352        IncomingKind::Malformed => {
353            warn!("skipping incoming record: {}", incoming.envelope.id);
354            IncomingAction::DoNothing
355        }
356    };
357    trace!("plan_incoming resulted in {:?}", state);
358    Ok(state)
359}
360
361/// Apply the incoming action
362fn apply_incoming_action<T: std::fmt::Debug + SyncRecord>(
363    rec_impl: &dyn ProcessIncomingRecordImpl<Record = T>,
364    tx: &Transaction<'_>,
365    action: IncomingAction<T>,
366) -> Result<()> {
367    trace!("applying action: {:?}", action);
368    match action {
369        IncomingAction::Update { record, was_merged } => {
370            rec_impl.update_local_record(tx, record, was_merged)?;
371        }
372        IncomingAction::Fork { forked, incoming } => {
373            // `forked` exists in the DB with the same guid as `incoming`, so fix that.
374            // change_record_guid will also update the mirror (if it exists) to prevent
375            // the server from overriding the forked mirror record (and losing any unknown fields)
376            rec_impl.change_record_guid(tx, incoming.id(), forked.id())?;
377            // `incoming` has the correct new guid.
378            rec_impl.insert_local_record(tx, incoming)?;
379        }
380        IncomingAction::Insert { record } => {
381            rec_impl.insert_local_record(tx, record)?;
382        }
383        IncomingAction::UpdateLocalGuid { old_guid, record } => {
384            // expect record to have the new guid.
385            assert_ne!(old_guid, *record.id());
386            rec_impl.change_record_guid(tx, &old_guid, record.id())?;
387            // the item is identical with the item with the new guid
388            // *except* for the metadata - so we still need to update, but
389            // don't need to treat the item as dirty.
390            rec_impl.update_local_record(tx, record, false)?;
391        }
392        IncomingAction::ResurrectLocalTombstone { record } => {
393            rec_impl.remove_tombstone(tx, record.id())?;
394            rec_impl.insert_local_record(tx, record)?;
395        }
396        IncomingAction::ResurrectRemoteTombstone { record } => {
397            // This is just "ensure local record dirty", which
398            // update_local_record conveniently does.
399            rec_impl.update_local_record(tx, record, true)?;
400        }
401        IncomingAction::DeleteLocalRecord { guid } => {
402            rec_impl.remove_record(tx, &guid)?;
403        }
404        IncomingAction::DoNothing => {}
405    }
406    Ok(())
407}
408
409// Helpers for tests
410#[cfg(test)]
411mod tests; // pull in our integration tests
412
413// and a module for unit test utilities.
414#[cfg(test)]
415pub mod test {
416    use crate::db::{schema::create_empty_sync_temp_tables, test::new_mem_db, AutofillDb};
417
418    pub fn new_syncable_mem_db() -> AutofillDb {
419        error_support::init_for_tests();
420        let db = new_mem_db();
421        create_empty_sync_temp_tables(&db).expect("should work");
422        db
423    }
424}