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}