1use super::incoming::IncomingApplicator;
6use super::record::{
7 BookmarkItemRecord, BookmarkRecord, BookmarkRecordId, FolderRecord, QueryRecord,
8 SeparatorRecord,
9};
10use super::{SyncedBookmarkKind, SyncedBookmarkValidity};
11use crate::db::{GlobalChangeCounterTracker, PlacesDb, SharedPlacesDb};
12use crate::error::*;
13use crate::frecency::{calculate_frecency, DEFAULT_FRECENCY_SETTINGS};
14use crate::storage::{
15 bookmarks::{
16 bookmark_sync::{create_synced_bookmark_roots, reset},
17 BookmarkRootGuid,
18 },
19 delete_pending_temp_tables, get_meta, put_meta,
20};
21use crate::types::{BookmarkType, SyncStatus, UnknownFields};
22use dogear::{
23 self, AbortSignal, CompletionOps, Content, Item, MergedRoot, TelemetryEvent, Tree, UploadItem,
24 UploadTombstone,
25};
26use interrupt_support::SqlInterruptScope;
27use rusqlite::ErrorCode;
28use rusqlite::Row;
29use sql_support::ConnExt;
30use std::cell::RefCell;
31use std::collections::HashMap;
32use std::fmt;
33use std::sync::Arc;
34use sync15::bso::{IncomingBso, OutgoingBso};
35use sync15::engine::{CollSyncIds, CollectionRequest, EngineSyncAssociation, SyncEngine};
36use sync15::{telemetry, CollectionName, ServerTimestamp};
37use sync_guid::Guid as SyncGuid;
38use types::Timestamp;
39pub const LAST_SYNC_META_KEY: &str = "bookmarks_last_sync_time";
40pub const GLOBAL_SYNCID_META_KEY: &str = "bookmarks_global_sync_id";
43pub const COLLECTION_SYNCID_META_KEY: &str = "bookmarks_sync_id";
44pub const COLLECTION_NAME: &str = "bookmarks";
45
46const MAX_FRECENCIES_TO_RECALCULATE_PER_CHUNK: usize = 400;
51
52struct MergeInterruptee<'a>(&'a SqlInterruptScope);
54
55impl AbortSignal for MergeInterruptee<'_> {
56 #[inline]
57 fn aborted(&self) -> bool {
58 self.0.was_interrupted()
59 }
60}
61
62fn stage_incoming(
63 db: &PlacesDb,
64 scope: &SqlInterruptScope,
65 inbound: Vec<IncomingBso>,
66 incoming_telemetry: &mut telemetry::EngineIncoming,
67) -> Result<()> {
68 let mut tx = db.begin_transaction()?;
69
70 let applicator = IncomingApplicator::new(db);
71
72 for incoming in inbound {
73 applicator.apply_bso(incoming)?;
74 incoming_telemetry.applied(1);
75 if tx.should_commit() {
76 debug!("Updating origins for new synced URLs since last commit");
78 delete_pending_temp_tables(db)?;
79 }
80 tx.maybe_commit()?;
81 scope.err_if_interrupted()?;
82 }
83
84 debug!("Updating origins for new synced URLs in last chunk");
85 delete_pending_temp_tables(db)?;
86
87 tx.commit()?;
88 Ok(())
89}
90
91fn db_has_changes(db: &PlacesDb) -> Result<bool> {
92 let sql = format!(
96 "SELECT
97 EXISTS (
98 SELECT 1
99 FROM moz_bookmarks_synced v
100 LEFT JOIN moz_bookmarks b ON v.guid = b.guid
101 WHERE v.needsMerge AND
102 (NOT v.isDeleted OR b.guid NOT NULL)
103 ) OR EXISTS (
104 WITH RECURSIVE
105 {}
106 SELECT 1
107 FROM localItems
108 WHERE syncChangeCounter > 0
109 ) OR EXISTS (
110 SELECT 1
111 FROM moz_bookmarks_deleted
112 )
113 AS hasChanges",
114 LocalItemsFragment("localItems")
115 );
116 Ok(db
117 .try_query_row(
118 &sql,
119 [],
120 |row| -> rusqlite::Result<_> { row.get::<_, bool>(0) },
121 false,
122 )?
123 .unwrap_or(false))
124}
125
126fn update_local_items_in_places(
133 db: &PlacesDb,
134 scope: &SqlInterruptScope,
135 now: Timestamp,
136 ops: &CompletionOps<'_>,
137) -> Result<()> {
138 debug!(
140 "Staging apply {} remote item ops",
141 ops.apply_remote_items.len()
142 );
143 sql_support::each_sized_chunk(
144 &ops.apply_remote_items,
145 sql_support::default_max_variable_number() / 3,
146 |chunk, _| -> Result<()> {
147 let sql = format!(
153 "WITH ops(mergedGuid, localGuid, remoteGuid, remoteType,
154 level) AS (
155 VALUES {ops}
156 )
157 INSERT INTO itemsToApply(mergedGuid, localId, remoteId,
158 remoteGuid, newLevel, newKind,
159 localDateAdded, remoteDateAdded,
160 lastModified, oldTitle, newTitle,
161 oldPlaceId, newPlaceId,
162 newKeyword)
163 SELECT n.mergedGuid, b.id, v.id,
164 v.guid, n.level, n.remoteType,
165 b.dateAdded, v.dateAdded,
166 v.serverModified, b.title, v.title,
167 b.fk, v.placeId,
168 v.keyword
169 FROM ops n
170 JOIN moz_bookmarks_synced v ON v.guid = n.remoteGuid
171 LEFT JOIN moz_bookmarks b ON b.guid = n.localGuid",
172 ops = sql_support::repeat_display(chunk.len(), ",", |index, f| {
173 let op = &chunk[index];
174 write!(
175 f,
176 "(?, ?, ?, {}, {})",
177 SyncedBookmarkKind::from(op.remote_node().kind) as u8,
178 op.level
179 )
180 }),
181 );
182
183 let mut params = Vec::with_capacity(chunk.len() * 3);
188 for op in chunk.iter() {
189 scope.err_if_interrupted()?;
190
191 let merged_guid = op.merged_node.guid.as_str();
192 params.push(Some(merged_guid));
193
194 let local_guid = op
195 .merged_node
196 .merge_state
197 .local_node()
198 .map(|node| node.guid.as_str());
199 params.push(local_guid);
200
201 let remote_guid = op.remote_node().guid.as_str();
202 params.push(Some(remote_guid));
203 }
204
205 db.execute(&sql, rusqlite::params_from_iter(params))?;
206 Ok(())
207 },
208 )?;
209
210 debug!("Staging {} change GUID ops", ops.change_guids.len());
211 sql_support::each_sized_chunk(
212 &ops.change_guids,
213 sql_support::default_max_variable_number() / 2,
214 |chunk, _| -> Result<()> {
215 let sql = format!(
216 "INSERT INTO changeGuidOps(localGuid, mergedGuid,
217 syncStatus, level, lastModified)
218 VALUES {}",
219 sql_support::repeat_display(chunk.len(), ",", |index, f| {
220 let op = &chunk[index];
221 let sync_status = if op.merged_node.remote_guid_changed() {
226 None
227 } else {
228 Some(SyncStatus::Normal as u8)
229 };
230 write!(
231 f,
232 "(?, ?, {}, {}, {})",
233 NullableFragment(sync_status),
234 op.level,
235 now
236 )
237 }),
238 );
239
240 let mut params = Vec::with_capacity(chunk.len() * 2);
241 for op in chunk.iter() {
242 scope.err_if_interrupted()?;
243
244 let local_guid = op.local_node().guid.as_str();
245 params.push(local_guid);
246
247 let merged_guid = op.merged_node.guid.as_str();
248 params.push(merged_guid);
249 }
250
251 db.execute(&sql, rusqlite::params_from_iter(params))?;
252 Ok(())
253 },
254 )?;
255
256 debug!(
257 "Staging apply {} new local structure ops",
258 ops.apply_new_local_structure.len()
259 );
260 sql_support::each_sized_chunk(
261 &ops.apply_new_local_structure,
262 sql_support::default_max_variable_number() / 2,
263 |chunk, _| -> Result<()> {
264 let sql = format!(
265 "INSERT INTO applyNewLocalStructureOps(
266 mergedGuid, mergedParentGuid, position, level
267 )
268 VALUES {}",
269 sql_support::repeat_display(chunk.len(), ",", |index, f| {
270 let op = &chunk[index];
271 write!(f, "(?, ?, {}, {})", op.position, op.level)
272 }),
273 );
274
275 let mut params = Vec::with_capacity(chunk.len() * 2);
276 for op in chunk.iter() {
277 scope.err_if_interrupted()?;
278
279 let merged_guid = op.merged_node.guid.as_str();
280 params.push(merged_guid);
281
282 let merged_parent_guid = op.merged_parent_node.guid.as_str();
283 params.push(merged_parent_guid);
284 }
285
286 db.execute(&sql, rusqlite::params_from_iter(params))?;
287 Ok(())
288 },
289 )?;
290
291 debug!(
292 "Removing {} tombstones for revived items",
293 ops.delete_local_tombstones.len()
294 );
295 sql_support::each_chunk_mapped(
296 &ops.delete_local_tombstones,
297 |op| op.guid().as_str(),
298 |chunk, _| -> Result<()> {
299 scope.err_if_interrupted()?;
300 db.execute(
301 &format!(
302 "DELETE FROM moz_bookmarks_deleted
303 WHERE guid IN ({})",
304 sql_support::repeat_sql_vars(chunk.len())
305 ),
306 rusqlite::params_from_iter(chunk),
307 )?;
308 Ok(())
309 },
310 )?;
311
312 debug!(
313 "Inserting {} new tombstones for non-syncable and invalid items",
314 ops.insert_local_tombstones.len()
315 );
316 sql_support::each_chunk_mapped(
317 &ops.insert_local_tombstones,
318 |op| op.remote_node().guid.as_str().to_owned(),
319 |chunk, _| -> Result<()> {
320 scope.err_if_interrupted()?;
321 db.execute(
322 &format!(
323 "INSERT INTO moz_bookmarks_deleted(guid, dateRemoved)
324 VALUES {}",
325 sql_support::repeat_display(chunk.len(), ",", |_, f| write!(f, "(?, {})", now)),
326 ),
327 rusqlite::params_from_iter(chunk),
328 )?;
329 Ok(())
330 },
331 )?;
332
333 debug!(
334 "Flag frecencies for {} removed bookmark URLs as stale",
335 ops.delete_local_items.len()
336 );
337 sql_support::each_chunk_mapped(
338 &ops.delete_local_items,
339 |op| op.local_node().guid.as_str().to_owned(),
340 |chunk, _| -> Result<()> {
341 scope.err_if_interrupted()?;
342 db.execute(
343 &format!(
344 "REPLACE INTO moz_places_stale_frecencies(
345 place_id, stale_at
346 )
347 SELECT b.fk, {now}
348 FROM moz_bookmarks b
349 WHERE b.guid IN ({vars})
350 AND b.fk NOT NULL",
351 now = now,
352 vars = sql_support::repeat_sql_vars(chunk.len())
353 ),
354 rusqlite::params_from_iter(chunk),
355 )?;
356 Ok(())
357 },
358 )?;
359
360 debug!(
361 "Removing {} deleted items from Places",
362 ops.delete_local_items.len()
363 );
364 sql_support::each_chunk_mapped(
365 &ops.delete_local_items,
366 |op| op.local_node().guid.as_str().to_owned(),
367 |chunk, _| -> Result<()> {
368 scope.err_if_interrupted()?;
369 db.execute(
370 &format!(
371 "DELETE FROM moz_bookmarks
372 WHERE guid IN ({})",
373 sql_support::repeat_sql_vars(chunk.len())
374 ),
375 rusqlite::params_from_iter(chunk),
376 )?;
377 Ok(())
378 },
379 )?;
380
381 debug!("Changing GUIDs");
382 scope.err_if_interrupted()?;
383 db.execute_batch("DELETE FROM changeGuidOps")?;
384
385 debug!("Applying remote items");
386 apply_remote_items(db, scope, now)?;
387
388 debug!("Applying new local structure");
390 scope.err_if_interrupted()?;
391 db.execute_batch("DELETE FROM applyNewLocalStructureOps")?;
392
393 let orphaned_count: i64 = db.query_row(
396 "WITH RECURSIVE orphans(id) AS (
397 SELECT b.id
398 FROM moz_bookmarks b
399 WHERE b.parent IS NOT NULL
400 AND NOT EXISTS (
401 SELECT 1 FROM moz_bookmarks p WHERE p.id = b.parent
402 )
403 UNION
404 SELECT c.id
405 FROM moz_bookmarks c
406 JOIN orphans o ON c.parent = o.id
407 )
408 SELECT COUNT(*) FROM orphans;",
409 [],
410 |row| row.get(0),
411 )?;
412
413 if orphaned_count > 0 {
414 warn!("Found {} orphaned bookmarks after sync", orphaned_count);
415 error_support::report_error!(
416 "places-sync-bookmarks-orphaned",
417 "found local orphaned bookmarks after we applied new local structure ops: {}",
418 orphaned_count,
419 );
420 }
421
422 debug!(
423 "Resetting change counters for {} items that shouldn't be uploaded",
424 ops.set_local_merged.len()
425 );
426 sql_support::each_chunk_mapped(
427 &ops.set_local_merged,
428 |op| op.merged_node.guid.as_str(),
429 |chunk, _| -> Result<()> {
430 scope.err_if_interrupted()?;
431 db.execute(
432 &format!(
433 "UPDATE moz_bookmarks SET
434 syncChangeCounter = 0
435 WHERE guid IN ({})",
436 sql_support::repeat_sql_vars(chunk.len()),
437 ),
438 rusqlite::params_from_iter(chunk),
439 )?;
440 Ok(())
441 },
442 )?;
443
444 debug!(
445 "Bumping change counters for {} items that should be uploaded",
446 ops.set_local_unmerged.len()
447 );
448 sql_support::each_chunk_mapped(
449 &ops.set_local_unmerged,
450 |op| op.merged_node.guid.as_str(),
451 |chunk, _| -> Result<()> {
452 scope.err_if_interrupted()?;
453 db.execute(
454 &format!(
455 "UPDATE moz_bookmarks SET
456 syncChangeCounter = 1
457 WHERE guid IN ({})",
458 sql_support::repeat_sql_vars(chunk.len()),
459 ),
460 rusqlite::params_from_iter(chunk),
461 )?;
462 Ok(())
463 },
464 )?;
465
466 debug!(
467 "Flagging applied {} remote items as merged",
468 ops.set_remote_merged.len()
469 );
470 sql_support::each_chunk_mapped(
471 &ops.set_remote_merged,
472 |op| op.guid().as_str(),
473 |chunk, _| -> Result<()> {
474 scope.err_if_interrupted()?;
475 db.execute(
476 &format!(
477 "UPDATE moz_bookmarks_synced SET
478 needsMerge = 0
479 WHERE guid IN ({})",
480 sql_support::repeat_sql_vars(chunk.len()),
481 ),
482 rusqlite::params_from_iter(chunk),
483 )?;
484 Ok(())
485 },
486 )?;
487
488 Ok(())
489}
490
491fn apply_remote_items(db: &PlacesDb, scope: &SqlInterruptScope, now: Timestamp) -> Result<()> {
492 debug!("Removing old keywords");
497 scope.err_if_interrupted()?;
498 db.execute_batch(
499 "DELETE FROM moz_keywords
500 WHERE place_id IN (SELECT oldPlaceId FROM itemsToApply
501 WHERE oldPlaceId NOT NULL) OR
502 place_id IN (SELECT newPlaceId FROM itemsToApply
503 WHERE newPlaceId NOT NULL) OR
504 keyword IN (SELECT newKeyword FROM itemsToApply
505 WHERE newKeyword NOT NULL)",
506 )?;
507
508 debug!("Removing old tags");
509 scope.err_if_interrupted()?;
510 db.execute_batch(
511 "DELETE FROM moz_tags_relation
512 WHERE place_id IN (SELECT oldPlaceId FROM itemsToApply
513 WHERE oldPlaceId NOT NULL) OR
514 place_id IN (SELECT newPlaceId FROM itemsToApply
515 WHERE newPlaceId NOT NULL)",
516 )?;
517
518 debug!("Checking for potential GUID collisions before upserting items");
521 let collision_check_sql = "
522 SELECT ia.localId, ia.mergedGuid, ia.remoteGuid, b.id, b.guid
523 FROM itemsToApply ia
524 JOIN moz_bookmarks b ON ia.mergedGuid = b.guid
525 WHERE (ia.localId IS NULL OR ia.localId != b.id)
526 ";
527
528 let potential_collisions: Vec<(Option<i64>, String, String, i64, String)> = db
529 .prepare(collision_check_sql)?
530 .query_map([], |row| {
531 let ia_local_id: Option<i64> = row.get(0)?;
532 let ia_merged_guid: String = row.get(1)?;
533 let ia_remote_guid: String = row.get(2)?;
534 let bmk_id: i64 = row.get(3)?;
535 let bmk_guid: String = row.get(4)?;
536 Ok((
537 ia_local_id,
538 ia_merged_guid,
539 ia_remote_guid,
540 bmk_id,
541 bmk_guid,
542 ))
543 })?
544 .filter_map(|entry| entry.ok())
545 .collect();
546
547 if !potential_collisions.is_empty() {
548 for (ia_local_id, ia_merged_guid, ia_remote_guid, bmk_id, bmk_guid) in &potential_collisions
550 {
551 error_support::breadcrumb!(
552 "Found GUID collision: ia_localId={:?}, ia_mergedGuid={}, ia_remoteGuid={}, mb_id={}, mb_guid={}",
553 ia_local_id,
554 ia_merged_guid,
555 ia_remote_guid,
556 bmk_id,
557 bmk_guid
558 );
559 }
560 }
561
562 let orphaned_count: i64 = db.query_row(
565 "WITH RECURSIVE orphans(id) AS (
566 SELECT b.id
567 FROM moz_bookmarks b
568 WHERE b.parent IS NOT NULL
569 AND NOT EXISTS (
570 SELECT 1 FROM moz_bookmarks p WHERE p.id = b.parent
571 )
572 UNION
573 SELECT c.id
574 FROM moz_bookmarks c
575 JOIN orphans o ON c.parent = o.id
576 )
577 SELECT COUNT(*) FROM orphans;",
578 [],
579 |row| row.get(0),
580 )?;
581
582 if orphaned_count > 0 {
583 warn!("Found {} orphaned bookmarks during sync", orphaned_count);
584 error_support::breadcrumb!(
585 "places-sync-bookmarks-orphaned: found local orphans before upsert {}",
586 orphaned_count
587 );
588 }
589
590 debug!("Upserting new items");
596 let upsert_sql = format!(
597 "INSERT INTO moz_bookmarks(id, guid, parent,
598 position, type, fk, title,
599 dateAdded,
600 lastModified,
601 syncStatus, syncChangeCounter)
602 SELECT localId, mergedGuid, (SELECT id FROM moz_bookmarks
603 WHERE guid = '{root_guid}'),
604 -1, {type_fragment}, newPlaceId, newTitle,
605 /* Pick the older of the local and remote date added. We'll
606 weakly reupload any items with an older local date. */
607 MIN(IFNULL(localDateAdded, remoteDateAdded), remoteDateAdded),
608 /* The last modified date should always be newer than the date
609 added, so we pick the newer of the two here. */
610 MAX(lastModified, remoteDateAdded),
611 {sync_status}, 0
612 FROM itemsToApply
613 WHERE 1
614 ON CONFLICT(id) DO UPDATE SET
615 title = excluded.title,
616 dateAdded = excluded.dateAdded,
617 lastModified = excluded.lastModified,
618 fk = excluded.fk,
619 syncStatus = {sync_status}
620 /* Due to bug 1935797, we found scenarios where users had bookmarks with GUIDs that matched
621 * incoming records BUT for one reason or another dogear doesn't believe it exists locally
622 * This handles the case where we try to insert a new bookmark with a GUID that already exists,
623 * updating the existing record instead of failing with a constraint violation.
624 * Usually the above conflict will catch most of these scenarios and there's no issue of
625 * any dupes being added here since users that hit this before would've just failed the bookmark sync
626 */
627 ON CONFLICT(guid) DO UPDATE SET
628 title = excluded.title,
629 dateAdded = excluded.dateAdded,
630 lastModified = excluded.lastModified,
631 fk = excluded.fk,
632 syncStatus = {sync_status}",
633 root_guid = BookmarkRootGuid::Root.as_guid().as_str(),
634 type_fragment = ItemTypeFragment("newKind"),
635 sync_status = SyncStatus::Normal as u8,
636 );
637
638 scope.err_if_interrupted()?;
639 let result = db.execute_batch(&upsert_sql);
640
641 if let Err(rusqlite::Error::SqliteFailure(e, _)) = &result {
645 if e.code == ErrorCode::ConstraintViolation {
646 error_support::report_error!(
647 "places-sync-bookmarks-constraint-violation",
648 "Hit a constraint violation {:?}",
649 result
650 );
651 }
652 }
653 result?;
655
656 debug!("Flagging frecencies for recalculation");
657 scope.err_if_interrupted()?;
658 db.execute_batch(&format!(
659 "REPLACE INTO moz_places_stale_frecencies(place_id, stale_at)
660 SELECT oldPlaceId, {now} FROM itemsToApply
661 WHERE newKind = {bookmark_kind} AND (
662 oldPlaceId IS NULL <> newPlaceId IS NULL OR
663 oldPlaceId <> newPlaceId
664 )
665 UNION ALL
666 SELECT newPlaceId, {now} FROM itemsToApply
667 WHERE newKind = {bookmark_kind} AND (
668 newPlaceId IS NULL <> oldPlaceId IS NULL OR
669 newPlaceId <> oldPlaceId
670 )",
671 now = now,
672 bookmark_kind = SyncedBookmarkKind::Bookmark as u8,
673 ))?;
674
675 debug!("Inserting new keywords for new URLs");
676 scope.err_if_interrupted()?;
677 db.execute_batch(
678 "INSERT OR IGNORE INTO moz_keywords(keyword, place_id)
679 SELECT newKeyword, newPlaceId
680 FROM itemsToApply
681 WHERE newKeyword NOT NULL",
682 )?;
683
684 debug!("Inserting new tags for new URLs");
685 scope.err_if_interrupted()?;
686 db.execute_batch(
687 "INSERT OR IGNORE INTO moz_tags_relation(tag_id, place_id)
688 SELECT r.tagId, n.newPlaceId
689 FROM itemsToApply n
690 JOIN moz_bookmarks_synced_tag_relation r ON r.itemId = n.remoteId",
691 )?;
692
693 Ok(())
694}
695
696fn stage_items_to_upload(
706 db: &PlacesDb,
707 scope: &SqlInterruptScope,
708 upload_items: &[UploadItem<'_>],
709 upload_tombstones: &[UploadTombstone<'_>],
710) -> Result<()> {
711 debug!("Cleaning up staged items left from last sync");
712 scope.err_if_interrupted()?;
713 db.execute_batch("DELETE FROM itemsToUpload")?;
714
715 debug!("Staging items with older local dates added");
719 scope.err_if_interrupted()?;
720 db.execute_batch(&format!(
721 "INSERT OR IGNORE INTO itemsToUpload(id, guid, syncChangeCounter,
722 parentGuid, parentTitle, dateAdded,
723 kind, title, placeId, url,
724 keyword, position)
725 {}
726 JOIN itemsToApply n ON n.mergedGuid = b.guid
727 WHERE n.localDateAdded < n.remoteDateAdded",
728 UploadItemsFragment("b")
729 ))?;
730
731 debug!(
732 "Staging {} remaining locally changed items for upload",
733 upload_items.len()
734 );
735 sql_support::each_chunk_mapped(
736 upload_items,
737 |op| op.merged_node.guid.as_str(),
738 |chunk, _| -> Result<()> {
739 let sql = format!(
740 "INSERT OR IGNORE INTO itemsToUpload(id, guid, syncChangeCounter,
741 parentGuid, parentTitle,
742 dateAdded, kind, title,
743 placeId, url, keyword,
744 position)
745 {upload_items_fragment}
746 WHERE b.guid IN ({vars})",
747 vars = sql_support::repeat_sql_vars(chunk.len()),
748 upload_items_fragment = UploadItemsFragment("b")
749 );
750
751 db.execute(&sql, rusqlite::params_from_iter(chunk))?;
752 Ok(())
753 },
754 )?;
755
756 debug!("Staging structure to upload");
759 scope.err_if_interrupted()?;
760 db.execute_batch(
761 "INSERT INTO structureToUpload(guid, parentId, position)
762 SELECT b.guid, b.parent, b.position
763 FROM moz_bookmarks b
764 JOIN itemsToUpload o ON o.id = b.parent",
765 )?;
766
767 debug!("Staging tags to upload");
769 scope.err_if_interrupted()?;
770 db.execute_batch(
771 "INSERT INTO tagsToUpload(id, tag)
772 SELECT o.id, t.tag
773 FROM itemsToUpload o
774 JOIN moz_tags_relation r ON r.place_id = o.placeId
775 JOIN moz_tags t ON t.id = r.tag_id",
776 )?;
777
778 debug!("Staging {} tombstones to upload", upload_tombstones.len());
780 sql_support::each_chunk_mapped(
781 upload_tombstones,
782 |op| op.guid().as_str(),
783 |chunk, _| -> Result<()> {
784 scope.err_if_interrupted()?;
785 db.execute(
786 &format!(
787 "INSERT OR IGNORE INTO itemsToUpload(
788 guid, syncChangeCounter, isDeleted
789 )
790 VALUES {}",
791 sql_support::repeat_display(chunk.len(), ",", |_, f| write!(f, "(?, 1, 1)")),
792 ),
793 rusqlite::params_from_iter(chunk),
794 )?;
795 Ok(())
796 },
797 )?;
798
799 Ok(())
800}
801
802fn fetch_outgoing_records(db: &PlacesDb, scope: &SqlInterruptScope) -> Result<Vec<OutgoingBso>> {
804 let mut changes = Vec::new();
805 let mut child_record_ids_by_local_parent_id: HashMap<i64, Vec<BookmarkRecordId>> =
806 HashMap::new();
807 let mut tags_by_local_id: HashMap<i64, Vec<String>> = HashMap::new();
808
809 let mut stmt = db.prepare(
810 "SELECT parentId, guid FROM structureToUpload
811 ORDER BY parentId, position",
812 )?;
813 let mut results = stmt.query([])?;
814 while let Some(row) = results.next()? {
815 scope.err_if_interrupted()?;
816 let local_parent_id = row.get::<_, i64>("parentId")?;
817 let child_guid = row.get::<_, SyncGuid>("guid")?;
818 let child_record_ids = child_record_ids_by_local_parent_id
819 .entry(local_parent_id)
820 .or_default();
821 child_record_ids.push(child_guid.into());
822 }
823
824 let mut stmt = db.prepare("SELECT id, tag FROM tagsToUpload")?;
825 let mut results = stmt.query([])?;
826 while let Some(row) = results.next()? {
827 scope.err_if_interrupted()?;
828 let local_id = row.get::<_, i64>("id")?;
829 let tag = row.get::<_, String>("tag")?;
830 let tags = tags_by_local_id.entry(local_id).or_default();
831 tags.push(tag);
832 }
833
834 let mut stmt = db.prepare(
835 "SELECT i.id, i.syncChangeCounter, i.guid, i.isDeleted, i.kind, i.keyword,
836 i.url, IFNULL(i.title, '') AS title, i.position, i.parentGuid,
837 IFNULL(i.parentTitle, '') AS parentTitle, i.dateAdded, m.unknownFields
838 FROM itemsToUpload i
839 LEFT JOIN moz_bookmarks_synced m ON i.guid == m.guid
840 ",
841 )?;
842 let mut results = stmt.query([])?;
843 while let Some(row) = results.next()? {
844 scope.err_if_interrupted()?;
845 let guid = row.get::<_, SyncGuid>("guid")?;
846 let is_deleted = row.get::<_, bool>("isDeleted")?;
847 if is_deleted {
848 changes.push(OutgoingBso::new_tombstone(
849 BookmarkRecordId::from(guid).as_guid().clone().into(),
850 ));
851 continue;
852 }
853 let parent_guid = row.get::<_, SyncGuid>("parentGuid")?;
854 let parent_title = row.get::<_, String>("parentTitle")?;
855 let date_added = row.get::<_, i64>("dateAdded")?;
856 let unknown_fields = match row.get::<_, Option<String>>("unknownFields")? {
857 None => UnknownFields::new(),
858 Some(s) => serde_json::from_str(&s)?,
859 };
860 let record: BookmarkItemRecord = match SyncedBookmarkKind::from_u8(row.get("kind")?)? {
861 SyncedBookmarkKind::Bookmark => {
862 let local_id = row.get::<_, i64>("id")?;
863 let title = row.get::<_, String>("title")?;
864 let url = row.get::<_, String>("url")?;
865 BookmarkRecord {
866 record_id: guid.into(),
867 parent_record_id: Some(parent_guid.into()),
868 parent_title: Some(parent_title),
869 date_added: Some(date_added),
870 has_dupe: true,
871 title: Some(title),
872 url: Some(url),
873 keyword: row.get::<_, Option<String>>("keyword")?,
874 tags: tags_by_local_id.remove(&local_id).unwrap_or_default(),
875 unknown_fields,
876 }
877 .into()
878 }
879 SyncedBookmarkKind::Query => {
880 let title = row.get::<_, String>("title")?;
881 let url = row.get::<_, String>("url")?;
882 QueryRecord {
883 record_id: guid.into(),
884 parent_record_id: Some(parent_guid.into()),
885 parent_title: Some(parent_title),
886 date_added: Some(date_added),
887 has_dupe: true,
888 title: Some(title),
889 url: Some(url),
890 tag_folder_name: None,
891 unknown_fields,
892 }
893 .into()
894 }
895 SyncedBookmarkKind::Folder => {
896 let title = row.get::<_, String>("title")?;
897 let local_id = row.get::<_, i64>("id")?;
898 let children = child_record_ids_by_local_parent_id
899 .remove(&local_id)
900 .unwrap_or_default();
901 FolderRecord {
902 record_id: guid.into(),
903 parent_record_id: Some(parent_guid.into()),
904 parent_title: Some(parent_title),
905 date_added: Some(date_added),
906 has_dupe: true,
907 title: Some(title),
908 children,
909 unknown_fields,
910 }
911 .into()
912 }
913 SyncedBookmarkKind::Livemark => continue,
914 SyncedBookmarkKind::Separator => {
915 let position = row.get::<_, i64>("position")?;
916 SeparatorRecord {
917 record_id: guid.into(),
918 parent_record_id: Some(parent_guid.into()),
919 parent_title: Some(parent_title),
920 date_added: Some(date_added),
921 has_dupe: true,
922 position: Some(position),
923 unknown_fields,
924 }
925 .into()
926 }
927 };
928 changes.push(OutgoingBso::from_content_with_id(record)?);
929 }
930
931 Ok(changes)
932}
933
934fn push_synced_items(
938 db: &PlacesDb,
939 scope: &SqlInterruptScope,
940 uploaded_at: ServerTimestamp,
941 records_synced: Vec<SyncGuid>,
942) -> Result<()> {
943 let mut tx = db.begin_transaction()?;
947
948 let guids = records_synced
949 .into_iter()
950 .map(|id| BookmarkRecordId::from_payload_id(id).into())
951 .collect::<Vec<SyncGuid>>();
952 sql_support::each_chunk(&guids, |chunk, _| -> Result<()> {
953 db.execute(
954 &format!(
955 "UPDATE itemsToUpload SET
956 uploadedAt = {uploaded_at}
957 WHERE guid IN ({values})",
958 uploaded_at = uploaded_at.as_millis(),
959 values = sql_support::repeat_sql_values(chunk.len())
960 ),
961 rusqlite::params_from_iter(chunk),
962 )?;
963 tx.maybe_commit()?;
964 scope.err_if_interrupted()?;
965 Ok(())
966 })?;
967
968 put_meta(db, LAST_SYNC_META_KEY, &uploaded_at.as_millis())?;
971
972 db.execute_batch("DELETE FROM itemsToUpload")?;
974 tx.commit()?;
975
976 Ok(())
977}
978
979pub(crate) fn update_frecencies(db: &PlacesDb, scope: &SqlInterruptScope) -> Result<()> {
980 let mut tx = db.begin_transaction()?;
981
982 let mut frecencies = Vec::with_capacity(MAX_FRECENCIES_TO_RECALCULATE_PER_CHUNK);
983 loop {
984 let sql = format!(
985 "SELECT place_id FROM moz_places_stale_frecencies
986 ORDER BY stale_at DESC
987 LIMIT {}",
988 MAX_FRECENCIES_TO_RECALCULATE_PER_CHUNK
989 );
990 let mut stmt = db.prepare_maybe_cached(&sql, true)?;
991 let mut results = stmt.query([])?;
992 while let Some(row) = results.next()? {
993 let place_id = row.get("place_id")?;
994 scope.err_if_interrupted()?;
997 let frecency =
998 calculate_frecency(db, &DEFAULT_FRECENCY_SETTINGS, place_id, Some(false))?;
999 frecencies.push((place_id, frecency));
1000 }
1001 if frecencies.is_empty() {
1002 break;
1003 }
1004
1005 db.execute_batch(&format!(
1007 "WITH frecencies(id, frecency) AS (
1008 VALUES {}
1009 )
1010 UPDATE moz_places SET
1011 frecency = (SELECT frecency FROM frecencies f
1012 WHERE f.id = id)
1013 WHERE id IN (SELECT f.id FROM frecencies f)",
1014 sql_support::repeat_display(frecencies.len(), ",", |index, f| {
1015 let (id, frecency) = frecencies[index];
1016 write!(f, "({}, {})", id, frecency)
1017 })
1018 ))?;
1019 tx.maybe_commit()?;
1020 scope.err_if_interrupted()?;
1021
1022 db.execute_batch(&format!(
1024 "DELETE FROM moz_places_stale_frecencies
1025 WHERE place_id IN ({})",
1026 sql_support::repeat_display(frecencies.len(), ",", |index, f| {
1027 let (id, _) = frecencies[index];
1028 write!(f, "{}", id)
1029 })
1030 ))?;
1031 tx.maybe_commit()?;
1032 scope.err_if_interrupted()?;
1033
1034 if frecencies.len() < MAX_FRECENCIES_TO_RECALCULATE_PER_CHUNK {
1038 break;
1039 }
1040 frecencies.clear();
1041 }
1042
1043 tx.commit()?;
1044
1045 Ok(())
1046}
1047
1048pub struct BookmarksSyncEngine {
1050 db: Arc<SharedPlacesDb>,
1051 pub(crate) scope: SqlInterruptScope,
1054}
1055
1056impl BookmarksSyncEngine {
1057 pub fn new(db: Arc<SharedPlacesDb>) -> Result<Self> {
1058 Ok(Self {
1059 scope: db.begin_interrupt_scope()?,
1060 db,
1061 })
1062 }
1063}
1064
1065impl SyncEngine for BookmarksSyncEngine {
1066 #[inline]
1067 fn collection_name(&self) -> CollectionName {
1068 COLLECTION_NAME.into()
1069 }
1070
1071 fn stage_incoming(
1072 &self,
1073 inbound: Vec<IncomingBso>,
1074 telem: &mut telemetry::Engine,
1075 ) -> anyhow::Result<()> {
1076 let conn = self.db.lock();
1077 let mut incoming_telemetry = telemetry::EngineIncoming::new();
1079 stage_incoming(&conn, &self.scope, inbound, &mut incoming_telemetry)?;
1080 telem.incoming(incoming_telemetry);
1081 Ok(())
1082 }
1083
1084 fn apply(
1085 &self,
1086 timestamp: ServerTimestamp,
1087 telem: &mut telemetry::Engine,
1088 ) -> anyhow::Result<Vec<OutgoingBso>> {
1089 let conn = self.db.lock();
1090 put_meta(&conn, LAST_SYNC_META_KEY, ×tamp.as_millis())?;
1094
1095 let mut merger = Merger::with_telemetry(&conn, &self.scope, timestamp, telem);
1097 merger.merge()?;
1098 Ok(fetch_outgoing_records(&conn, &self.scope)?)
1099 }
1100
1101 fn set_uploaded(
1102 &self,
1103 new_timestamp: ServerTimestamp,
1104 ids: Vec<SyncGuid>,
1105 ) -> anyhow::Result<()> {
1106 let conn = self.db.lock();
1107 push_synced_items(&conn, &self.scope, new_timestamp, ids)?;
1108 Ok(update_frecencies(&conn, &self.scope)?)
1109 }
1110
1111 fn sync_finished(&self) -> anyhow::Result<()> {
1112 let conn = self.db.lock();
1113 conn.pragma_update(None, "wal_checkpoint", "PASSIVE")?;
1114 Ok(())
1115 }
1116
1117 fn get_collection_request(
1118 &self,
1119 server_timestamp: ServerTimestamp,
1120 ) -> anyhow::Result<Option<CollectionRequest>> {
1121 let conn = self.db.lock();
1122 let since =
1123 ServerTimestamp(get_meta::<i64>(&conn, LAST_SYNC_META_KEY)?.unwrap_or_default());
1124 Ok(if since == server_timestamp {
1125 None
1126 } else {
1127 Some(
1128 CollectionRequest::new(self.collection_name())
1129 .full()
1130 .newer_than(since),
1131 )
1132 })
1133 }
1134
1135 fn get_sync_assoc(&self) -> anyhow::Result<EngineSyncAssociation> {
1136 let conn = self.db.lock();
1137 let global = get_meta(&conn, GLOBAL_SYNCID_META_KEY)?;
1138 let coll = get_meta(&conn, COLLECTION_SYNCID_META_KEY)?;
1139 Ok(if let (Some(global), Some(coll)) = (global, coll) {
1140 EngineSyncAssociation::Connected(CollSyncIds { global, coll })
1141 } else {
1142 EngineSyncAssociation::Disconnected
1143 })
1144 }
1145
1146 fn reset(&self, assoc: &EngineSyncAssociation) -> anyhow::Result<()> {
1147 let conn = self.db.lock();
1148 reset(&conn, assoc)?;
1149 Ok(())
1150 }
1151
1152 fn wipe(&self) -> anyhow::Result<()> {
1159 let conn = self.db.lock();
1160 let tx = conn.begin_transaction()?;
1161 let sql = format!(
1162 "INSERT INTO moz_bookmarks_deleted(guid, dateRemoved)
1163 SELECT guid, now()
1164 FROM moz_bookmarks
1165 WHERE guid NOT IN {roots} AND
1166 syncStatus = {sync_status};
1167
1168 UPDATE moz_bookmarks SET
1169 syncChangeCounter = syncChangeCounter + 1
1170 WHERE guid IN {roots};
1171
1172 DELETE FROM moz_bookmarks
1173 WHERE guid NOT IN {roots};",
1174 roots = RootsFragment(&[
1175 BookmarkRootGuid::Root,
1176 BookmarkRootGuid::Menu,
1177 BookmarkRootGuid::Mobile,
1178 BookmarkRootGuid::Toolbar,
1179 BookmarkRootGuid::Unfiled
1180 ]),
1181 sync_status = SyncStatus::Normal as u8
1182 );
1183 conn.execute_batch(&sql)?;
1184 create_synced_bookmark_roots(&conn)?;
1185 tx.commit()?;
1186 Ok(())
1187 }
1188}
1189
1190#[derive(Default)]
1191struct Driver {
1192 validation: RefCell<telemetry::Validation>,
1193}
1194
1195impl dogear::Driver for Driver {
1196 fn generate_new_guid(&self, _invalid_guid: &dogear::Guid) -> dogear::Result<dogear::Guid> {
1197 Ok(SyncGuid::random().as_str().into())
1198 }
1199
1200 fn record_telemetry_event(&self, event: TelemetryEvent) {
1201 if let TelemetryEvent::FetchRemoteTree(stats) = event {
1203 self.validation
1204 .borrow_mut()
1205 .problem("orphans", stats.problems.orphans)
1206 .problem("misparentedRoots", stats.problems.misparented_roots)
1207 .problem(
1208 "multipleParents",
1209 stats.problems.multiple_parents_by_children,
1210 )
1211 .problem("missingParents", stats.problems.missing_parent_guids)
1212 .problem("nonFolderParents", stats.problems.non_folder_parent_guids)
1213 .problem(
1214 "parentChildDisagreements",
1215 stats.problems.parent_child_disagreements,
1216 )
1217 .problem("missingChildren", stats.problems.missing_children);
1218 }
1219 }
1220}
1221
1222pub(crate) struct Merger<'a> {
1224 db: &'a PlacesDb,
1225 scope: &'a SqlInterruptScope,
1226 remote_time: ServerTimestamp,
1227 local_time: Timestamp,
1228 external_transaction: bool,
1233 telem: Option<&'a mut telemetry::Engine>,
1234 global_change_tracker: GlobalChangeCounterTracker,
1237}
1238
1239impl<'a> Merger<'a> {
1240 #[cfg(test)]
1241 pub(crate) fn new(
1242 db: &'a PlacesDb,
1243 scope: &'a SqlInterruptScope,
1244 remote_time: ServerTimestamp,
1245 ) -> Self {
1246 Self {
1247 db,
1248 scope,
1249 remote_time,
1250 local_time: Timestamp::now(),
1251 external_transaction: false,
1252 telem: None,
1253 global_change_tracker: db.global_bookmark_change_tracker(),
1254 }
1255 }
1256
1257 pub(crate) fn with_telemetry(
1258 db: &'a PlacesDb,
1259 scope: &'a SqlInterruptScope,
1260 remote_time: ServerTimestamp,
1261 telem: &'a mut telemetry::Engine,
1262 ) -> Self {
1263 Self {
1264 db,
1265 scope,
1266 remote_time,
1267 local_time: Timestamp::now(),
1268 external_transaction: false,
1269 telem: Some(telem),
1270 global_change_tracker: db.global_bookmark_change_tracker(),
1271 }
1272 }
1273
1274 #[cfg(test)]
1275 fn with_localtime(
1276 db: &'a PlacesDb,
1277 scope: &'a SqlInterruptScope,
1278 remote_time: ServerTimestamp,
1279 local_time: Timestamp,
1280 ) -> Self {
1281 Self {
1282 db,
1283 scope,
1284 remote_time,
1285 local_time,
1286 external_transaction: false,
1287 telem: None,
1288 global_change_tracker: db.global_bookmark_change_tracker(),
1289 }
1290 }
1291
1292 pub(crate) fn merge(&mut self) -> Result<()> {
1293 use dogear::Store;
1294 if !db_has_changes(self.db)? {
1295 return Ok(());
1296 }
1297 let driver = Driver::default();
1299 self.prepare()?;
1300 let result = self.merge_with_driver(&driver, &MergeInterruptee(self.scope));
1301 debug!("merge completed: {:?}", result);
1302
1303 if let Some(ref mut telem) = self.telem {
1305 telem.validation(driver.validation.into_inner());
1306 }
1307 result
1308 }
1309
1310 fn prepare(&self) -> Result<()> {
1312 self.scope.err_if_interrupted()?;
1322 debug!("Flagging bookmarks with mismatched keywords for reupload");
1323 let sql = format!(
1324 "UPDATE moz_bookmarks_synced SET
1325 validity = {reupload}
1326 WHERE validity = {valid} AND (
1327 placeId IN (
1328 /* Same URL, different keywords. `COUNT` ignores NULLs, so
1329 we need to count them separately. This handles cases where
1330 a keyword was removed from one, but not all bookmarks with
1331 the same URL. */
1332 SELECT placeId FROM moz_bookmarks_synced
1333 GROUP BY placeId
1334 HAVING COUNT(DISTINCT keyword) +
1335 COUNT(DISTINCT CASE WHEN keyword IS NULL
1336 THEN 1 END) > 1
1337 ) OR keyword IN (
1338 /* Different URLs, same keyword. Bookmarks with keywords but
1339 without URLs are already invalid, so we don't need to handle
1340 NULLs here. */
1341 SELECT keyword FROM moz_bookmarks_synced
1342 WHERE keyword NOT NULL
1343 GROUP BY keyword
1344 HAVING COUNT(DISTINCT placeId) > 1
1345 )
1346 )",
1347 reupload = SyncedBookmarkValidity::Reupload as u8,
1348 valid = SyncedBookmarkValidity::Valid as u8,
1349 );
1350 self.db.execute_batch(&sql)?;
1351
1352 self.scope.err_if_interrupted()?;
1373 debug!("Flagging bookmarks with mismatched tags for reupload");
1374 let sql = format!(
1375 "WITH
1376 tagsByPlaceId(placeId, tagIds) AS (
1377 /* For multiple bookmarks with the same URL, each group will
1378 have one tag per bookmark. So, if bookmarks A1, A2, and A3
1379 have the same URL A with tag T, T will be in the group three
1380 times. But we only want to count each tag once per URL, so
1381 we use `SUM(DISTINCT)`. */
1382 SELECT v.placeId, SUM(DISTINCT t.tagId)
1383 FROM moz_bookmarks_synced v
1384 JOIN moz_bookmarks_synced_tag_relation t ON t.itemId = v.id
1385 WHERE v.placeId NOT NULL
1386 GROUP BY v.placeId
1387 ),
1388 tagsByItemId(itemId, tagIds) AS (
1389 /* But here, we can use a plain `SUM`, since we're grouping by
1390 item ID, and an item can't have duplicate tags thanks to the
1391 primary key on the relation table. */
1392 SELECT t.itemId, SUM(t.tagId)
1393 FROM moz_bookmarks_synced_tag_relation t
1394 GROUP BY t.itemId
1395 )
1396 UPDATE moz_bookmarks_synced SET
1397 validity = {reupload}
1398 WHERE validity = {valid} AND id IN (
1399 SELECT v.id FROM moz_bookmarks_synced v
1400 JOIN tagsByPlaceId u ON v.placeId = u.placeId
1401 /* This left join is important: if A1 has tags and A2 doesn't,
1402 we want to flag A2 for reupload. */
1403 LEFT JOIN tagsByItemId t ON t.itemId = v.id
1404 /* Unlike `<>`, `IS NOT` compares NULLs. */
1405 WHERE t.tagIds IS NOT u.tagIds
1406 )",
1407 reupload = SyncedBookmarkValidity::Reupload as u8,
1408 valid = SyncedBookmarkValidity::Valid as u8,
1409 );
1410 self.db.execute_batch(&sql)?;
1411
1412 Ok(())
1413 }
1414
1415 fn local_row_to_item(&self, row: &Row<'_>) -> Result<(Item, Option<Content>)> {
1417 let guid = row.get::<_, SyncGuid>("guid")?;
1418 let url_href = row.get::<_, Option<String>>("url")?;
1419 let kind = match row.get::<_, BookmarkType>("type")? {
1420 BookmarkType::Bookmark => match url_href.as_ref() {
1421 Some(u) if u.starts_with("place:") => SyncedBookmarkKind::Query,
1422 _ => SyncedBookmarkKind::Bookmark,
1423 },
1424 BookmarkType::Folder => SyncedBookmarkKind::Folder,
1425 BookmarkType::Separator => SyncedBookmarkKind::Separator,
1426 };
1427 let mut item = Item::new(guid.as_str().into(), kind.into());
1428 let age = self
1430 .local_time
1431 .duration_since(row.get::<_, Timestamp>("localModified")?)
1432 .unwrap_or_default();
1433 item.age = age.as_secs() as i64 * 1000 + i64::from(age.subsec_millis());
1434 item.needs_merge = row.get::<_, u32>("syncChangeCounter")? > 0;
1435
1436 let content = if item.guid == dogear::ROOT_GUID {
1437 None
1438 } else {
1439 match row.get::<_, SyncStatus>("syncStatus")? {
1440 SyncStatus::Normal => None,
1441 _ => match kind {
1442 SyncedBookmarkKind::Bookmark | SyncedBookmarkKind::Query => {
1443 let title = row.get::<_, String>("title")?;
1444 url_href.map(|url_href| Content::Bookmark { title, url_href })
1445 }
1446 SyncedBookmarkKind::Folder | SyncedBookmarkKind::Livemark => {
1447 let title = row.get::<_, String>("title")?;
1448 Some(Content::Folder { title })
1449 }
1450 SyncedBookmarkKind::Separator => Some(Content::Separator),
1451 },
1452 }
1453 };
1454
1455 Ok((item, content))
1456 }
1457
1458 fn remote_row_to_item(&self, row: &Row<'_>) -> Result<(Item, Option<Content>)> {
1460 let guid = row.get::<_, SyncGuid>("guid")?;
1461 let kind = SyncedBookmarkKind::from_u8(row.get("kind")?)?;
1462 let mut item = Item::new(guid.as_str().into(), kind.into());
1463 let age = self
1467 .remote_time
1468 .duration_since(ServerTimestamp(row.get::<_, i64>("serverModified")?))
1469 .unwrap_or_default();
1470 item.age = age.as_secs() as i64 * 1000 + i64::from(age.subsec_millis());
1471 item.needs_merge = row.get("needsMerge")?;
1472 item.validity = SyncedBookmarkValidity::from_u8(row.get("validity")?)?.into();
1473
1474 let content = if item.guid == dogear::ROOT_GUID || !item.needs_merge {
1475 None
1476 } else {
1477 match kind {
1478 SyncedBookmarkKind::Bookmark | SyncedBookmarkKind::Query => {
1479 let title = row.get::<_, String>("title")?;
1480 let url_href = row.get::<_, Option<String>>("url")?;
1481 url_href.map(|url_href| Content::Bookmark { title, url_href })
1482 }
1483 SyncedBookmarkKind::Folder | SyncedBookmarkKind::Livemark => {
1484 let title = row.get::<_, String>("title")?;
1485 Some(Content::Folder { title })
1486 }
1487 SyncedBookmarkKind::Separator => Some(Content::Separator),
1488 }
1489 };
1490
1491 Ok((item, content))
1492 }
1493}
1494
1495impl dogear::Store for Merger<'_> {
1496 type Ok = ();
1497 type Error = Error;
1498
1499 fn fetch_local_tree(&self) -> Result<Tree> {
1502 let mut stmt = self.db.prepare(&format!(
1503 "SELECT guid, type, syncChangeCounter, syncStatus,
1504 lastModified AS localModified,
1505 NULL AS url
1506 FROM moz_bookmarks
1507 WHERE guid = '{root_guid}'",
1508 root_guid = BookmarkRootGuid::Root.as_guid().as_str(),
1509 ))?;
1510 let mut results = stmt.query([])?;
1511 let mut builder = match results.next()? {
1512 Some(row) => {
1513 let (item, _) = self.local_row_to_item(row)?;
1514 Tree::with_root(item)
1515 }
1516 None => return Err(Error::Corruption(Corruption::InvalidLocalRoots)),
1517 };
1518
1519 let mut child_guids_by_parent_guid: HashMap<SyncGuid, Vec<dogear::Guid>> = HashMap::new();
1526 let mut stmt = self.db.prepare(&format!(
1527 "SELECT b.guid, p.guid AS parentGuid, b.type, b.syncChangeCounter,
1528 b.syncStatus, b.lastModified AS localModified,
1529 IFNULL(b.title, '') AS title,
1530 {url_fragment} AS url
1531 FROM moz_bookmarks b
1532 JOIN moz_bookmarks p ON p.id = b.parent
1533 WHERE b.guid <> '{root_guid}'
1534 ORDER BY b.parent, b.position",
1535 url_fragment = UrlOrPlaceIdFragment::PlaceId("b.fk"),
1536 root_guid = BookmarkRootGuid::Root.as_guid().as_str(),
1537 ))?;
1538 let mut results = stmt.query([])?;
1539
1540 while let Some(row) = results.next()? {
1541 self.scope.err_if_interrupted()?;
1542
1543 let (item, content) = self.local_row_to_item(row)?;
1544
1545 let parent_guid = row.get::<_, SyncGuid>("parentGuid")?;
1546 child_guids_by_parent_guid
1547 .entry(parent_guid)
1548 .or_default()
1549 .push(item.guid.clone());
1550
1551 let mut p = builder.item(item)?;
1552 if let Some(content) = content {
1553 p.content(content);
1554 }
1555 }
1556
1557 for (parent_guid, child_guids) in &child_guids_by_parent_guid {
1560 for child_guid in child_guids {
1561 self.scope.err_if_interrupted()?;
1562 builder
1563 .parent_for(child_guid)
1564 .by_structure(&parent_guid.as_str().into())?;
1565 }
1566 }
1567
1568 let mut stmt = self.db.prepare("SELECT guid FROM moz_bookmarks_deleted")?;
1570 let mut results = stmt.query([])?;
1571 while let Some(row) = results.next()? {
1572 self.scope.err_if_interrupted()?;
1573 let guid = row.get::<_, SyncGuid>("guid")?;
1574 builder.deletion(guid.as_str().into());
1575 }
1576
1577 let tree = Tree::try_from(builder)?;
1578 Ok(tree)
1579 }
1580
1581 fn fetch_remote_tree(&self) -> Result<Tree> {
1583 let sql = format!(
1587 "SELECT guid, serverModified, kind, needsMerge, validity
1588 FROM moz_bookmarks_synced
1589 WHERE NOT isDeleted AND
1590 guid = '{root_guid}'",
1591 root_guid = BookmarkRootGuid::Root.as_guid().as_str()
1592 );
1593 let mut builder = self
1594 .db
1595 .try_query_row(
1596 &sql,
1597 [],
1598 |row| -> Result<_> {
1599 let (root, _) = self.remote_row_to_item(row)?;
1600 Ok(Tree::with_root(root))
1601 },
1602 false,
1603 )?
1604 .ok_or(Error::Corruption(Corruption::InvalidSyncedRoots))?;
1605 builder.reparent_orphans_to(&dogear::UNFILED_GUID);
1606
1607 let sql = format!(
1608 "SELECT v.guid, v.parentGuid, v.serverModified, v.kind,
1609 IFNULL(v.title, '') AS title, v.needsMerge, v.validity,
1610 v.isDeleted, {url_fragment} AS url
1611 FROM moz_bookmarks_synced v
1612 WHERE v.guid <> '{root_guid}'
1613 ORDER BY v.guid",
1614 url_fragment = UrlOrPlaceIdFragment::PlaceId("v.placeId"),
1615 root_guid = BookmarkRootGuid::Root.as_guid().as_str()
1616 );
1617 let mut stmt = self.db.prepare(&sql)?;
1618 let mut results = stmt.query([])?;
1619 while let Some(row) = results.next()? {
1620 self.scope.err_if_interrupted()?;
1621
1622 let is_deleted = row.get::<_, bool>("isDeleted")?;
1623 if is_deleted {
1624 let needs_merge = row.get::<_, bool>("needsMerge")?;
1625 if !needs_merge {
1626 continue;
1629 }
1630 let guid = row.get::<_, SyncGuid>("guid")?;
1631 builder.deletion(guid.as_str().into());
1632 } else {
1633 let (item, content) = self.remote_row_to_item(row)?;
1634 let mut p = builder.item(item)?;
1635 if let Some(content) = content {
1636 p.content(content);
1637 }
1638 if let Some(parent_guid) = row.get::<_, Option<SyncGuid>>("parentGuid")? {
1639 p.by_parent_guid(parent_guid.as_str().into())?;
1640 }
1641 }
1642 }
1643
1644 let sql = format!(
1645 "SELECT guid, parentGuid FROM moz_bookmarks_synced_structure
1646 WHERE guid <> '{root_guid}'
1647 ORDER BY parentGuid, position",
1648 root_guid = BookmarkRootGuid::Root.as_guid().as_str()
1649 );
1650 let mut stmt = self.db.prepare(&sql)?;
1651 let mut results = stmt.query([])?;
1652 while let Some(row) = results.next()? {
1653 self.scope.err_if_interrupted()?;
1654 let guid = row.get::<_, SyncGuid>("guid")?;
1655 let parent_guid = row.get::<_, SyncGuid>("parentGuid")?;
1656 builder
1657 .parent_for(&guid.as_str().into())
1658 .by_children(&parent_guid.as_str().into())?;
1659 }
1660
1661 let tree = Tree::try_from(builder)?;
1662 Ok(tree)
1663 }
1664
1665 fn apply(&mut self, root: MergedRoot<'_>) -> Result<()> {
1666 let ops = root.completion_ops_with_signal(&MergeInterruptee(self.scope))?;
1667
1668 if ops.is_empty() {
1669 return Ok(());
1672 }
1673
1674 let tx = if !self.external_transaction {
1675 Some(self.db.begin_transaction()?)
1676 } else {
1677 None
1678 };
1679
1680 if self.global_change_tracker.changed() {
1683 info!("Aborting update of local items as local tree changed while merging");
1684 if let Some(tx) = tx {
1685 tx.rollback()?;
1686 }
1687 return Ok(());
1688 }
1689
1690 debug!("Updating local items in Places");
1691 update_local_items_in_places(self.db, self.scope, self.local_time, &ops)?;
1692
1693 debug!(
1694 "Staging {} items and {} tombstones to upload",
1695 ops.upload_items.len(),
1696 ops.upload_tombstones.len()
1697 );
1698 stage_items_to_upload(
1699 self.db,
1700 self.scope,
1701 &ops.upload_items,
1702 &ops.upload_tombstones,
1703 )?;
1704
1705 self.db.execute_batch("DELETE FROM itemsToApply;")?;
1706 if let Some(tx) = tx {
1707 tx.commit()?;
1708 }
1709 Ok(())
1710 }
1711}
1712
1713struct NullableFragment<T>(Option<T>);
1716
1717impl<T> fmt::Display for NullableFragment<T>
1718where
1719 T: fmt::Display,
1720{
1721 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1722 match &self.0 {
1723 Some(v) => v.fmt(f),
1724 None => write!(f, "NULL"),
1725 }
1726 }
1727}
1728
1729struct ItemTypeFragment(&'static str);
1733
1734impl fmt::Display for ItemTypeFragment {
1735 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1736 write!(
1737 f,
1738 "(CASE WHEN {col} IN ({bookmark_kind}, {query_kind})
1739 THEN {bookmark_type}
1740 WHEN {col} IN ({folder_kind}, {livemark_kind})
1741 THEN {folder_type}
1742 WHEN {col} = {separator_kind}
1743 THEN {separator_type}
1744 END)",
1745 col = self.0,
1746 bookmark_kind = SyncedBookmarkKind::Bookmark as u8,
1747 query_kind = SyncedBookmarkKind::Query as u8,
1748 bookmark_type = BookmarkType::Bookmark as u8,
1749 folder_kind = SyncedBookmarkKind::Folder as u8,
1750 livemark_kind = SyncedBookmarkKind::Livemark as u8,
1751 folder_type = BookmarkType::Folder as u8,
1752 separator_kind = SyncedBookmarkKind::Separator as u8,
1753 separator_type = BookmarkType::Separator as u8,
1754 )
1755 }
1756}
1757
1758struct UploadItemsFragment(&'static str);
1761
1762impl fmt::Display for UploadItemsFragment {
1763 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1764 write!(
1765 f,
1766 "SELECT {alias}.id, {alias}.guid, {alias}.syncChangeCounter,
1767 p.guid AS parentGuid, p.title AS parentTitle,
1768 {alias}.dateAdded, {kind_fragment} AS kind,
1769 {alias}.title, h.id AS placeId, h.url,
1770 (SELECT k.keyword FROM moz_keywords k
1771 WHERE k.place_id = h.id) AS keyword,
1772 {alias}.position
1773 FROM moz_bookmarks {alias}
1774 JOIN moz_bookmarks p ON p.id = {alias}.parent
1775 LEFT JOIN moz_places h ON h.id = {alias}.fk",
1776 alias = self.0,
1777 kind_fragment = item_kind_fragment(self.0, "type", UrlOrPlaceIdFragment::Url("h.url")),
1778 )
1779 }
1780}
1781
1782struct LocalItemsFragment<'a>(&'a str);
1785
1786impl fmt::Display for LocalItemsFragment<'_> {
1787 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1788 write!(
1789 f,
1790 "{name}(id, guid, parentId, parentGuid, position, type, title, parentTitle,
1791 placeId, dateAdded, lastModified, syncChangeCounter, level) AS (
1792 SELECT b.id, b.guid, 0, NULL, b.position, b.type, b.title, NULL,
1793 b.fk, b.dateAdded, b.lastModified, b.syncChangeCounter, 0
1794 FROM moz_bookmarks b
1795 WHERE b.guid = '{root_guid}'
1796 UNION ALL
1797 SELECT b.id, b.guid, s.id, s.guid, b.position, b.type, b.title, s.title,
1798 b.fk, b.dateAdded, b.lastModified, b.syncChangeCounter, s.level + 1
1799 FROM moz_bookmarks b
1800 JOIN {name} s ON s.id = b.parent)",
1801 name = self.0,
1802 root_guid = BookmarkRootGuid::Root.as_guid().as_str()
1803 )
1804 }
1805}
1806
1807fn item_kind_fragment(
1808 table_name: &'static str,
1809 type_column_name: &'static str,
1810 url_or_place_id_fragment: UrlOrPlaceIdFragment,
1811) -> ItemKindFragment {
1812 ItemKindFragment {
1813 table_name,
1814 type_column_name,
1815 url_or_place_id_fragment,
1816 }
1817}
1818
1819struct ItemKindFragment {
1823 table_name: &'static str,
1825 type_column_name: &'static str,
1827 url_or_place_id_fragment: UrlOrPlaceIdFragment,
1829}
1830
1831impl fmt::Display for ItemKindFragment {
1832 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1833 write!(
1834 f,
1835 "(CASE {table_name}.{type_column_name}
1836 WHEN {bookmark_type} THEN (
1837 CASE substr({url}, 1, 6)
1838 /* Queries are bookmarks with a 'place:' URL scheme. */
1839 WHEN 'place:' THEN {query_kind}
1840 ELSE {bookmark_kind}
1841 END
1842 )
1843 WHEN {folder_type} THEN {folder_kind}
1844 WHEN {separator_type} THEN {separator_kind}
1845 END)",
1846 table_name = self.table_name,
1847 type_column_name = self.type_column_name,
1848 bookmark_type = BookmarkType::Bookmark as u8,
1849 url = self.url_or_place_id_fragment,
1850 query_kind = SyncedBookmarkKind::Query as u8,
1851 bookmark_kind = SyncedBookmarkKind::Bookmark as u8,
1852 folder_type = BookmarkType::Folder as u8,
1853 folder_kind = SyncedBookmarkKind::Folder as u8,
1854 separator_type = BookmarkType::Separator as u8,
1855 separator_kind = SyncedBookmarkKind::Separator as u8,
1856 )
1857 }
1858}
1859
1860enum UrlOrPlaceIdFragment {
1864 Url(&'static str),
1867 PlaceId(&'static str),
1870}
1871
1872impl fmt::Display for UrlOrPlaceIdFragment {
1873 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1874 match self {
1875 UrlOrPlaceIdFragment::Url(s) => write!(f, "{}", s),
1876 UrlOrPlaceIdFragment::PlaceId(s) => {
1877 write!(f, "(SELECT h.url FROM moz_places h WHERE h.id = {})", s)
1878 }
1879 }
1880 }
1881}
1882
1883struct RootsFragment<'a>(&'a [BookmarkRootGuid]);
1886
1887impl fmt::Display for RootsFragment<'_> {
1888 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1889 f.write_str("(")?;
1890 for (i, guid) in self.0.iter().enumerate() {
1891 if i != 0 {
1892 f.write_str(",")?;
1893 }
1894 write!(f, "'{}'", guid.as_str())?;
1895 }
1896 f.write_str(")")
1897 }
1898}
1899
1900#[cfg(test)]
1901mod tests {
1902 use super::*;
1903 use crate::api::places_api::{test::new_mem_api, ConnectionType, PlacesApi};
1904 use crate::bookmark_sync::tests::SyncedBookmarkItem;
1905 use crate::db::PlacesDb;
1906 use crate::storage::{
1907 bookmarks::{
1908 get_raw_bookmark, insert_bookmark, update_bookmark, BookmarkPosition,
1909 InsertableBookmark, UpdatableBookmark, USER_CONTENT_ROOTS,
1910 },
1911 history::frecency_stale_at,
1912 tags,
1913 };
1914 use crate::tests::{
1915 assert_json_tree as assert_local_json_tree, insert_json_tree as insert_local_json_tree,
1916 };
1917 use dogear::{Store as DogearStore, Validity};
1918 use rusqlite::{Error as RusqlError, ErrorCode};
1919 use serde_json::{json, Value};
1920 use std::{
1921 borrow::Cow,
1922 time::{Duration, SystemTime},
1923 };
1924 use sync15::bso::{IncomingBso, IncomingKind};
1925 use sync15::engine::CollSyncIds;
1926 use sync_guid::Guid;
1927 use url::Url;
1928
1929 struct ExpectedSyncedItem<'a>(SyncGuid, Cow<'a, SyncedBookmarkItem>);
1931
1932 impl<'a> ExpectedSyncedItem<'a> {
1933 fn new(
1934 guid: impl Into<SyncGuid>,
1935 expected: &'a SyncedBookmarkItem,
1936 ) -> ExpectedSyncedItem<'a> {
1937 ExpectedSyncedItem(guid.into(), Cow::Borrowed(expected))
1938 }
1939
1940 fn with_properties(
1941 guid: impl Into<SyncGuid>,
1942 expected: &'a SyncedBookmarkItem,
1943 f: impl FnOnce(&mut SyncedBookmarkItem) -> &mut SyncedBookmarkItem + 'static,
1944 ) -> ExpectedSyncedItem<'a> {
1945 let mut expected = expected.clone();
1946 f(&mut expected);
1947 ExpectedSyncedItem(guid.into(), Cow::Owned(expected))
1948 }
1949
1950 fn check(&self, conn: &PlacesDb) -> Result<()> {
1951 let actual =
1952 SyncedBookmarkItem::get(conn, &self.0)?.expect("Expected synced item should exist");
1953 assert_eq!(&actual, &*self.1);
1954 Ok(())
1955 }
1956 }
1957
1958 fn create_sync_engine(api: &PlacesApi) -> BookmarksSyncEngine {
1959 BookmarksSyncEngine::new(api.get_sync_connection().unwrap()).unwrap()
1960 }
1961
1962 fn engine_apply_incoming(
1963 engine: &BookmarksSyncEngine,
1964 incoming: Vec<IncomingBso>,
1965 ) -> Vec<OutgoingBso> {
1966 let mut telem = telemetry::Engine::new(engine.collection_name());
1967 engine
1968 .stage_incoming(incoming, &mut telem)
1969 .expect("Should stage incoming");
1970 engine
1971 .apply(ServerTimestamp(0), &mut telem)
1972 .expect("Should apply")
1973 }
1974
1975 fn apply_incoming(
1979 api: &PlacesApi,
1980 remote_time: ServerTimestamp,
1981 records_json: Value,
1982 ) -> Vec<Guid> {
1983 let engine = create_sync_engine(api);
1985
1986 let incoming = match records_json {
1987 Value::Array(records) => records
1988 .into_iter()
1989 .map(|record| IncomingBso::from_test_content_ts(record, remote_time))
1990 .collect(),
1991 Value::Object(_) => {
1992 vec![IncomingBso::from_test_content_ts(records_json, remote_time)]
1993 }
1994 _ => panic!("unexpected json value"),
1995 };
1996
1997 engine_apply_incoming(&engine, incoming);
1998
1999 let sync_db = api.get_sync_connection().unwrap();
2000 let syncer = sync_db.lock();
2001 let mut stmt = syncer
2002 .prepare("SELECT guid FROM itemsToUpload")
2003 .expect("Should prepare statement to fetch uploaded GUIDs");
2004 let uploaded_guids: Vec<Guid> = stmt
2005 .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, Guid>(0) })
2006 .expect("Should fetch uploaded GUIDs")
2007 .map(std::result::Result::unwrap)
2008 .collect();
2009
2010 push_synced_items(&syncer, &engine.scope, remote_time, uploaded_guids.clone())
2011 .expect("Should push synced changes back to the engine");
2012 uploaded_guids
2013 }
2014
2015 fn assert_incoming_creates_local_tree(
2016 api: &PlacesApi,
2017 records_json: Value,
2018 local_folder: &SyncGuid,
2019 local_tree: Value,
2020 ) {
2021 apply_incoming(api, ServerTimestamp(0), records_json);
2022 assert_local_json_tree(
2023 &api.get_sync_connection().unwrap().lock(),
2024 local_folder,
2025 local_tree,
2026 );
2027 }
2028
2029 #[test]
2030 fn test_fetch_remote_tree() -> Result<()> {
2031 let records = vec![
2032 json!({
2033 "id": "qqVTRWhLBOu3",
2034 "type": "bookmark",
2035 "parentid": "unfiled",
2036 "parentName": "Unfiled Bookmarks",
2037 "dateAdded": 1_381_542_355_843u64,
2038 "title": "The title",
2039 "bmkUri": "https://example.com",
2040 "tags": [],
2041 }),
2042 json!({
2043 "id": "unfiled",
2044 "type": "folder",
2045 "parentid": "places",
2046 "parentName": "",
2047 "dateAdded": 0,
2048 "title": "Unfiled Bookmarks",
2049 "children": ["qqVTRWhLBOu3"],
2050 "tags": [],
2051 }),
2052 ];
2053
2054 let api = new_mem_api();
2055 let db = api.get_sync_connection().unwrap();
2056 let conn = db.lock();
2057
2058 let interrupt_scope = conn.begin_interrupt_scope()?;
2060
2061 let incoming = records
2062 .into_iter()
2063 .map(IncomingBso::from_test_content)
2064 .collect();
2065
2066 stage_incoming(
2067 &conn,
2068 &interrupt_scope,
2069 incoming,
2070 &mut telemetry::EngineIncoming::new(),
2071 )
2072 .expect("Should apply incoming and stage outgoing records");
2073
2074 let merger = Merger::new(&conn, &interrupt_scope, ServerTimestamp(0));
2075
2076 let tree = merger.fetch_remote_tree()?;
2077
2078 assert_eq!(tree.guids().count(), USER_CONTENT_ROOTS.len() + 2);
2080
2081 let node = tree
2082 .node_for_guid(&"qqVTRWhLBOu3".into())
2083 .expect("should exist");
2084 assert!(node.needs_merge);
2085 assert_eq!(node.validity, Validity::Valid);
2086 assert_eq!(node.level(), 2);
2087 assert!(node.is_syncable());
2088
2089 let node = tree
2090 .node_for_guid(&BookmarkRootGuid::Unfiled.as_guid().as_str().into())
2091 .expect("should exist");
2092 assert!(node.needs_merge);
2093 assert_eq!(node.validity, Validity::Valid);
2094 assert_eq!(node.level(), 1);
2095 assert!(node.is_syncable());
2096
2097 let node = tree
2098 .node_for_guid(&BookmarkRootGuid::Menu.as_guid().as_str().into())
2099 .expect("should exist");
2100 assert!(!node.needs_merge);
2101 assert_eq!(node.validity, Validity::Valid);
2102 assert_eq!(node.level(), 1);
2103 assert!(node.is_syncable());
2104
2105 let node = tree
2106 .node_for_guid(&BookmarkRootGuid::Root.as_guid().as_str().into())
2107 .expect("should exist");
2108 assert_eq!(node.validity, Validity::Valid);
2109 assert_eq!(node.level(), 0);
2110 assert!(!node.is_syncable());
2111
2112 assert!(db_has_changes(&conn).unwrap());
2114 Ok(())
2115 }
2116
2117 #[test]
2118 fn test_fetch_local_tree() -> Result<()> {
2119 let now = SystemTime::now();
2120 let previously_ts: Timestamp = (now - Duration::new(10, 0)).into();
2121 let api = new_mem_api();
2122 let writer = api.open_connection(ConnectionType::ReadWrite)?;
2123 let sync_db = api.get_sync_connection().unwrap();
2124 let syncer = sync_db.lock();
2125
2126 writer
2127 .execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
2128 .expect("should work");
2129
2130 insert_local_json_tree(
2131 &writer,
2132 json!({
2133 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
2134 "children": [
2135 {
2136 "guid": "bookmark1___",
2137 "title": "the bookmark",
2138 "url": "https://www.example.com/",
2139 "last_modified": previously_ts,
2140 "date_added": previously_ts,
2141 },
2142 ]
2143 }),
2144 );
2145
2146 let interrupt_scope = syncer.begin_interrupt_scope()?;
2147 let merger =
2148 Merger::with_localtime(&syncer, &interrupt_scope, ServerTimestamp(0), now.into());
2149
2150 let tree = merger.fetch_local_tree()?;
2151
2152 assert_eq!(tree.guids().count(), USER_CONTENT_ROOTS.len() + 2);
2154
2155 let node = tree
2156 .node_for_guid(&"bookmark1___".into())
2157 .expect("should exist");
2158 assert!(node.needs_merge);
2159 assert_eq!(node.level(), 2);
2160 assert!(node.is_syncable());
2161 assert_eq!(node.age, 10000);
2162
2163 let node = tree
2164 .node_for_guid(&BookmarkRootGuid::Unfiled.as_guid().as_str().into())
2165 .expect("should exist");
2166 assert!(node.needs_merge);
2167 assert_eq!(node.level(), 1);
2168 assert!(node.is_syncable());
2169
2170 let node = tree
2171 .node_for_guid(&BookmarkRootGuid::Menu.as_guid().as_str().into())
2172 .expect("should exist");
2173 assert!(!node.needs_merge);
2174 assert_eq!(node.level(), 1);
2175 assert!(node.is_syncable());
2176
2177 let node = tree
2178 .node_for_guid(&BookmarkRootGuid::Root.as_guid().as_str().into())
2179 .expect("should exist");
2180 assert!(!node.needs_merge);
2181 assert_eq!(node.level(), 0);
2182 assert!(!node.is_syncable());
2183 let max_dur = SystemTime::now().duration_since(now).unwrap();
2185 let max_age = max_dur.as_secs() as i64 * 1000 + i64::from(max_dur.subsec_millis());
2186 assert!(node.age <= max_age);
2187
2188 assert!(db_has_changes(&syncer).unwrap());
2190 Ok(())
2191 }
2192
2193 #[test]
2194 fn test_apply_bookmark() {
2195 let api = new_mem_api();
2196 assert_incoming_creates_local_tree(
2197 &api,
2198 json!([{
2199 "id": "bookmark1___",
2200 "type": "bookmark",
2201 "parentid": "unfiled",
2202 "parentName": "Unfiled Bookmarks",
2203 "dateAdded": 1_381_542_355_843u64,
2204 "title": "Some bookmark",
2205 "bmkUri": "http://example.com",
2206 },
2207 {
2208 "id": "unfiled",
2209 "type": "folder",
2210 "parentid": "places",
2211 "dateAdded": 1_381_542_355_843u64,
2212 "title": "Unfiled",
2213 "children": ["bookmark1___"],
2214 }]),
2215 &BookmarkRootGuid::Unfiled.as_guid(),
2216 json!({"children" : [{"guid": "bookmark1___", "url": "http://example.com"}]}),
2217 );
2218 let reader = api
2219 .open_connection(ConnectionType::ReadOnly)
2220 .expect("Should open read-only connection");
2221 assert!(
2222 frecency_stale_at(&reader, &Url::parse("http://example.com").unwrap())
2223 .expect("Should check stale frecency")
2224 .is_some(),
2225 "Should mark frecency for bookmark URL as stale"
2226 );
2227
2228 let writer = api
2229 .open_connection(ConnectionType::ReadWrite)
2230 .expect("Should open read-write connection");
2231 insert_local_json_tree(
2232 &writer,
2233 json!({
2234 "guid": &BookmarkRootGuid::Menu.as_guid(),
2235 "children": [
2236 {
2237 "guid": "bookmark2___",
2238 "title": "2",
2239 "url": "http://example.com/2",
2240 }
2241 ],
2242 }),
2243 );
2244 assert_incoming_creates_local_tree(
2245 &api,
2246 json!([{
2247 "id": "menu",
2248 "type": "folder",
2249 "parentid": "places",
2250 "parentName": "",
2251 "dateAdded": 0,
2252 "title": "menu",
2253 "children": ["bookmark2___"],
2254 }, {
2255 "id": "bookmark2___",
2256 "type": "bookmark",
2257 "parentid": "menu",
2258 "parentName": "menu",
2259 "dateAdded": 1_381_542_355_843u64,
2260 "title": "2",
2261 "bmkUri": "http://example.com/2-remote",
2262 }]),
2263 &BookmarkRootGuid::Menu.as_guid(),
2264 json!({"children" : [{"guid": "bookmark2___", "url": "http://example.com/2-remote"}]}),
2265 );
2266 assert!(
2267 frecency_stale_at(&reader, &Url::parse("http://example.com/2").unwrap())
2268 .expect("Should check stale frecency for old URL")
2269 .is_some(),
2270 "Should mark frecency for old URL as stale"
2271 );
2272 assert!(
2273 frecency_stale_at(&reader, &Url::parse("http://example.com/2-remote").unwrap())
2274 .expect("Should check stale frecency for new URL")
2275 .is_some(),
2276 "Should mark frecency for new URL as stale"
2277 );
2278
2279 let sync_db = api.get_sync_connection().unwrap();
2280 let syncer = sync_db.lock();
2281 let interrupt_scope = syncer.begin_interrupt_scope().unwrap();
2282
2283 update_frecencies(&syncer, &interrupt_scope).expect("Should update frecencies");
2284
2285 assert!(
2286 frecency_stale_at(&reader, &Url::parse("http://example.com").unwrap())
2287 .expect("Should check stale frecency")
2288 .is_none(),
2289 "Should recalculate frecency for first bookmark"
2290 );
2291 assert!(
2292 frecency_stale_at(&reader, &Url::parse("http://example.com/2").unwrap())
2293 .expect("Should check stale frecency for old URL")
2294 .is_none(),
2295 "Should recalculate frecency for old URL"
2296 );
2297 assert!(
2298 frecency_stale_at(&reader, &Url::parse("http://example.com/2-remote").unwrap())
2299 .expect("Should check stale frecency for new URL")
2300 .is_none(),
2301 "Should recalculate frecency for new URL"
2302 );
2303 }
2304
2305 #[test]
2306 fn test_apply_complex_bookmark_tags() -> Result<()> {
2307 let api = new_mem_api();
2308 let writer = api.open_connection(ConnectionType::ReadWrite)?;
2309
2310 let local_bookmarks = vec![
2314 InsertableBookmark {
2315 parent_guid: BookmarkRootGuid::Unfiled.as_guid(),
2316 position: BookmarkPosition::Append,
2317 date_added: None,
2318 last_modified: None,
2319 guid: Some("bookmarkAAA1".into()),
2320 url: Url::parse("http://example.com/a").unwrap(),
2321 title: Some("A1".into()),
2322 }
2323 .into(),
2324 InsertableBookmark {
2325 parent_guid: BookmarkRootGuid::Menu.as_guid(),
2326 position: BookmarkPosition::Append,
2327 date_added: None,
2328 last_modified: None,
2329 guid: Some("bookmarkAAA2".into()),
2330 url: Url::parse("http://example.com/a").unwrap(),
2331 title: Some("A2".into()),
2332 }
2333 .into(),
2334 InsertableBookmark {
2335 parent_guid: BookmarkRootGuid::Unfiled.as_guid(),
2336 position: BookmarkPosition::Append,
2337 date_added: None,
2338 last_modified: None,
2339 guid: Some("bookmarkBBBB".into()),
2340 url: Url::parse("http://example.com/b").unwrap(),
2341 title: Some("B".into()),
2342 }
2343 .into(),
2344 ];
2345 let local_tags = &[
2346 ("http://example.com/a", vec!["one", "two"]),
2347 (
2348 "http://example.com/b",
2349 vec!["two", "three", "three", "four"],
2351 ),
2352 ];
2353 for bm in local_bookmarks.into_iter() {
2354 insert_bookmark(&writer, bm)?;
2355 }
2356 for (url, tags) in local_tags {
2357 let url = Url::parse(url)?;
2358 for t in tags.iter() {
2359 tags::tag_url(&writer, &url, t)?;
2360 }
2361 }
2362
2363 let remote_records = json!([{
2366 "id": "bookmarkBBBB",
2369 "type": "bookmark",
2370 "parentid": "unfiled",
2371 "parentName": "Unfiled",
2372 "dateAdded": 1_381_542_355_843u64,
2373 "title": "B",
2374 "bmkUri": "http://example.com/b",
2375 "tags": ["two", "two", "three", "eight"],
2376 }, {
2377 "id": "bookmarkCCC1",
2381 "type": "bookmark",
2382 "parentid": "unfiled",
2383 "parentName": "Unfiled",
2384 "dateAdded": 1_381_542_355_843u64,
2385 "title": "C1",
2386 "bmkUri": "http://example.com/c",
2387 "tags": ["four", "five", "six"],
2388 }, {
2389 "id": "bookmarkCCC2",
2390 "type": "bookmark",
2391 "parentid": "menu",
2392 "parentName": "Menu",
2393 "dateAdded": 1_381_542_355_843u64,
2394 "title": "C2",
2395 "bmkUri": "http://example.com/c",
2396 "tags": ["four", "five", "six"],
2397 }, {
2398 "id": "bookmarkCCC3",
2399 "type": "bookmark",
2400 "parentid": "menu",
2401 "parentName": "Menu",
2402 "dateAdded": 1_381_542_355_843u64,
2403 "title": "C3",
2404 "bmkUri": "http://example.com/c",
2405 "tags": ["six", "six", "seven"],
2406 }, {
2407 "id": "bookmarkDDDD",
2412 "type": "bookmark",
2413 "parentid": "unfiled",
2414 "parentName": "Unfiled",
2415 "dateAdded": 1_381_542_355_843u64,
2416 "title": "D",
2417 "bmkUri": "http://example.com/d",
2418 "tags": ["four", "five", "six"],
2419 }, {
2420 "id": "bookmarkEEE1",
2423 "type": "bookmark",
2424 "parentid": "toolbar",
2425 "parentName": "Toolbar",
2426 "dateAdded": 1_381_542_355_843u64,
2427 "title": "E1",
2428 "bmkUri": "http://example.com/e",
2429 "tags": ["nine", "ten", "eleven"],
2430 }, {
2431 "id": "bookmarkEEE2",
2432 "type": "bookmark",
2433 "parentid": "mobile",
2434 "parentName": "Mobile",
2435 "dateAdded": 1_381_542_355_843u64,
2436 "title": "E2",
2437 "bmkUri": "http://example.com/e",
2438 "tags": ["nine", "ten", "eleven"],
2439 }, {
2440 "id": "bookmarkFFF1",
2443 "type": "bookmark",
2444 "parentid": "toolbar",
2445 "parentName": "Toolbar",
2446 "dateAdded": 1_381_542_355_843u64,
2447 "title": "F1",
2448 "bmkUri": "http://example.com/f",
2449 "tags": ["twelve"],
2450 }, {
2451 "id": "bookmarkFFF2",
2452 "type": "bookmark",
2453 "parentid": "mobile",
2454 "parentName": "Mobile",
2455 "dateAdded": 1_381_542_355_843u64,
2456 "title": "F2",
2457 "bmkUri": "http://example.com/f",
2458 }, {
2459 "id": "unfiled",
2460 "type": "folder",
2461 "parentid": "places",
2462 "dateAdded": 1_381_542_355_843u64,
2463 "title": "Unfiled",
2464 "children": ["bookmarkBBBB", "bookmarkCCC1", "bookmarkDDDD"],
2465 }, {
2466 "id": "menu",
2467 "type": "folder",
2468 "parentid": "places",
2469 "dateAdded": 1_381_542_355_843u64,
2470 "title": "Menu",
2471 "children": ["bookmarkCCC2", "bookmarkCCC3"],
2472 }, {
2473 "id": "toolbar",
2474 "type": "folder",
2475 "parentid": "places",
2476 "dateAdded": 1_381_542_355_843u64,
2477 "title": "Toolbar",
2478 "children": ["bookmarkEEE1", "bookmarkFFF1"],
2479 }, {
2480 "id": "mobile",
2481 "type": "folder",
2482 "parentid": "places",
2483 "dateAdded": 1_381_542_355_843u64,
2484 "title": "Mobile",
2485 "children": ["bookmarkEEE2", "bookmarkFFF2"],
2486 }]);
2487
2488 let engine = create_sync_engine(&api);
2491 let incoming = if let Value::Array(records) = remote_records {
2492 records
2493 .into_iter()
2494 .map(IncomingBso::from_test_content)
2495 .collect()
2496 } else {
2497 unreachable!("JSON records must be an array");
2498 };
2499 let mut outgoing = engine_apply_incoming(&engine, incoming);
2500 outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
2501
2502 assert_local_json_tree(
2504 &writer,
2505 &BookmarkRootGuid::Root.as_guid(),
2506 json!({
2507 "guid": &BookmarkRootGuid::Root.as_guid(),
2508 "children": [{
2509 "guid": &BookmarkRootGuid::Menu.as_guid(),
2510 "children": [{
2511 "guid": "bookmarkCCC2",
2512 "title": "C2",
2513 "url": "http://example.com/c",
2514 }, {
2515 "guid": "bookmarkCCC3",
2516 "title": "C3",
2517 "url": "http://example.com/c",
2518 }, {
2519 "guid": "bookmarkAAA2",
2520 "title": "A2",
2521 "url": "http://example.com/a",
2522 }],
2523 }, {
2524 "guid": &BookmarkRootGuid::Toolbar.as_guid(),
2525 "children": [{
2526 "guid": "bookmarkEEE1",
2527 "title": "E1",
2528 "url": "http://example.com/e",
2529 }, {
2530 "guid": "bookmarkFFF1",
2531 "title": "F1",
2532 "url": "http://example.com/f",
2533 }],
2534 }, {
2535 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
2536 "children": [{
2537 "guid": "bookmarkBBBB",
2538 "title": "B",
2539 "url": "http://example.com/b",
2540 }, {
2541 "guid": "bookmarkCCC1",
2542 "title": "C1",
2543 "url": "http://example.com/c",
2544 }, {
2545 "guid": "bookmarkDDDD",
2546 "title": "D",
2547 "url": "http://example.com/d",
2548 }, {
2549 "guid": "bookmarkAAA1",
2550 "title": "A1",
2551 "url": "http://example.com/a",
2552 }],
2553 }, {
2554 "guid": &BookmarkRootGuid::Mobile.as_guid(),
2555 "children": [{
2556 "guid": "bookmarkEEE2",
2557 "title": "E2",
2558 "url": "http://example.com/e",
2559 }, {
2560 "guid": "bookmarkFFF2",
2561 "title": "F2",
2562 "url": "http://example.com/f",
2563 }],
2564 }],
2565 }),
2566 );
2567 let expected_local_tags = &[
2569 ("http://example.com/a", vec!["one", "two"]),
2570 ("http://example.com/b", vec!["eight", "three", "two"]),
2571 ("http://example.com/c", vec!["five", "four", "seven", "six"]),
2572 ("http://example.com/d", vec!["five", "four", "six"]),
2573 ("http://example.com/e", vec!["eleven", "nine", "ten"]),
2574 ("http://example.com/f", vec!["twelve"]),
2575 ];
2576 for (href, expected) in expected_local_tags {
2577 let mut actual = tags::get_tags_for_url(&writer, &Url::parse(href).unwrap())?;
2578 actual.sort();
2579 assert_eq!(&actual, expected);
2580 }
2581
2582 let expected_outgoing_ids = &[
2583 "bookmarkAAA1", "bookmarkAAA2",
2585 "bookmarkBBBB", "bookmarkCCC1", "bookmarkCCC2",
2588 "bookmarkCCC3",
2589 "bookmarkFFF2", "menu", "mobile",
2592 "toolbar",
2593 "unfiled",
2594 ];
2595 assert_eq!(
2596 outgoing
2597 .iter()
2598 .map(|p| p.envelope.id.as_str())
2599 .collect::<Vec<_>>(),
2600 expected_outgoing_ids,
2601 "Should upload new bookmarks and fix up tags",
2602 );
2603
2604 engine
2607 .set_uploaded(
2608 ServerTimestamp(0),
2609 expected_outgoing_ids.iter().map(SyncGuid::from).collect(),
2610 )
2611 .expect("Should push synced changes back to the engine");
2612 engine.sync_finished().expect("should work");
2613
2614 let mut synced_item_for_a = SyncedBookmarkItem::new();
2619 synced_item_for_a
2620 .validity(SyncedBookmarkValidity::Valid)
2621 .kind(SyncedBookmarkKind::Bookmark)
2622 .url(Some("http://example.com/a"))
2623 .tags(["one", "two"].iter().map(|&tag| tag.into()).collect());
2624 let mut synced_item_for_b = SyncedBookmarkItem::new();
2625 synced_item_for_b
2626 .validity(SyncedBookmarkValidity::Valid)
2627 .kind(SyncedBookmarkKind::Bookmark)
2628 .url(Some("http://example.com/b"))
2629 .tags(
2630 ["eight", "three", "two"]
2631 .iter()
2632 .map(|&tag| tag.into())
2633 .collect(),
2634 )
2635 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
2636 .title(Some("B"));
2637 let mut synced_item_for_c = SyncedBookmarkItem::new();
2638 synced_item_for_c
2639 .validity(SyncedBookmarkValidity::Valid)
2640 .kind(SyncedBookmarkKind::Bookmark)
2641 .url(Some("http://example.com/c"))
2642 .tags(
2643 ["five", "four", "seven", "six"]
2644 .iter()
2645 .map(|&tag| tag.into())
2646 .collect(),
2647 );
2648 let mut synced_item_for_f = SyncedBookmarkItem::new();
2649 synced_item_for_f
2650 .validity(SyncedBookmarkValidity::Valid)
2651 .kind(SyncedBookmarkKind::Bookmark)
2652 .url(Some("http://example.com/f"))
2653 .tags(vec!["twelve".into()]);
2654 let expected_synced_items = &[
2658 ExpectedSyncedItem::with_properties("bookmarkAAA1", &synced_item_for_a, |a| {
2659 a.parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
2660 .title(Some("A1"))
2661 }),
2662 ExpectedSyncedItem::with_properties("bookmarkAAA2", &synced_item_for_a, |a| {
2663 a.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
2664 .title(Some("A2"))
2665 }),
2666 ExpectedSyncedItem::new("bookmarkBBBB", &synced_item_for_b),
2667 ExpectedSyncedItem::with_properties("bookmarkCCC1", &synced_item_for_c, |c| {
2668 c.parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
2669 .title(Some("C1"))
2670 }),
2671 ExpectedSyncedItem::with_properties("bookmarkCCC2", &synced_item_for_c, |c| {
2672 c.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
2673 .title(Some("C2"))
2674 }),
2675 ExpectedSyncedItem::with_properties("bookmarkCCC3", &synced_item_for_c, |c| {
2676 c.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
2677 .title(Some("C3"))
2678 }),
2679 ExpectedSyncedItem::with_properties(
2680 "bookmarkFFF1",
2682 &synced_item_for_f,
2683 |f| {
2684 f.parent_guid(Some(&BookmarkRootGuid::Toolbar.as_guid()))
2685 .title(Some("F1"))
2686 },
2687 ),
2688 ExpectedSyncedItem::with_properties("bookmarkFFF2", &synced_item_for_f, |f| {
2689 f.parent_guid(Some(&BookmarkRootGuid::Mobile.as_guid()))
2690 .title(Some("F2"))
2691 }),
2692 ];
2693 for item in expected_synced_items {
2694 item.check(&writer)?;
2695 }
2696
2697 Ok(())
2698 }
2699
2700 #[test]
2701 fn test_apply_bookmark_tags() -> Result<()> {
2702 let api = new_mem_api();
2703 let writer = api.open_connection(ConnectionType::ReadWrite)?;
2704
2705 insert_bookmark(
2707 &writer,
2708 InsertableBookmark {
2709 parent_guid: BookmarkRootGuid::Unfiled.as_guid(),
2710 position: BookmarkPosition::Append,
2711 date_added: None,
2712 last_modified: None,
2713 guid: Some("bookmarkAAAA".into()),
2714 url: Url::parse("http://example.com/a").unwrap(),
2715 title: Some("A".into()),
2716 }
2717 .into(),
2718 )?;
2719 tags::tag_url(&writer, &Url::parse("http://example.com/a").unwrap(), "one")?;
2720
2721 let mut tags_for_a =
2722 tags::get_tags_for_url(&writer, &Url::parse("http://example.com/a").unwrap())?;
2723 tags_for_a.sort();
2724 assert_eq!(tags_for_a, vec!["one".to_owned()]);
2725
2726 assert_incoming_creates_local_tree(
2727 &api,
2728 json!([{
2729 "id": "bookmarkBBBB",
2730 "type": "bookmark",
2731 "parentid": "unfiled",
2732 "parentName": "Unfiled",
2733 "dateAdded": 1_381_542_355_843u64,
2734 "title": "B",
2735 "bmkUri": "http://example.com/b",
2736 "tags": ["one", "two"],
2737 }, {
2738 "id": "bookmarkCCCC",
2739 "type": "bookmark",
2740 "parentid": "unfiled",
2741 "parentName": "Unfiled",
2742 "dateAdded": 1_381_542_355_843u64,
2743 "title": "C",
2744 "bmkUri": "http://example.com/c",
2745 "tags": ["three"],
2746 }, {
2747 "id": "unfiled",
2748 "type": "folder",
2749 "parentid": "places",
2750 "dateAdded": 1_381_542_355_843u64,
2751 "title": "Unfiled",
2752 "children": ["bookmarkBBBB", "bookmarkCCCC"],
2753 }]),
2754 &BookmarkRootGuid::Unfiled.as_guid(),
2755 json!({"children" : [
2756 {"guid": "bookmarkBBBB", "url": "http://example.com/b"},
2757 {"guid": "bookmarkCCCC", "url": "http://example.com/c"},
2758 {"guid": "bookmarkAAAA", "url": "http://example.com/a"},
2759 ]}),
2760 );
2761
2762 let mut tags_for_a =
2763 tags::get_tags_for_url(&writer, &Url::parse("http://example.com/a").unwrap())?;
2764 tags_for_a.sort();
2765 assert_eq!(tags_for_a, vec!["one".to_owned()]);
2766
2767 let mut tags_for_b =
2768 tags::get_tags_for_url(&writer, &Url::parse("http://example.com/b").unwrap())?;
2769 tags_for_b.sort();
2770 assert_eq!(tags_for_b, vec!["one".to_owned(), "two".to_owned()]);
2771
2772 let mut tags_for_c =
2773 tags::get_tags_for_url(&writer, &Url::parse("http://example.com/c").unwrap())?;
2774 tags_for_c.sort();
2775 assert_eq!(tags_for_c, vec!["three".to_owned()]);
2776
2777 let synced_item_for_a = SyncedBookmarkItem::get(&writer, &"bookmarkAAAA".into())
2778 .expect("Should fetch A")
2779 .expect("A should exist");
2780 assert_eq!(
2781 synced_item_for_a,
2782 *SyncedBookmarkItem::new()
2783 .validity(SyncedBookmarkValidity::Valid)
2784 .kind(SyncedBookmarkKind::Bookmark)
2785 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
2786 .title(Some("A"))
2787 .url(Some("http://example.com/a"))
2788 .tags(vec!["one".into()])
2789 );
2790
2791 let synced_item_for_b = SyncedBookmarkItem::get(&writer, &"bookmarkBBBB".into())
2792 .expect("Should fetch B")
2793 .expect("B should exist");
2794 assert_eq!(
2795 synced_item_for_b,
2796 *SyncedBookmarkItem::new()
2797 .validity(SyncedBookmarkValidity::Valid)
2798 .kind(SyncedBookmarkKind::Bookmark)
2799 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
2800 .title(Some("B"))
2801 .url(Some("http://example.com/b"))
2802 .tags(vec!["one".into(), "two".into()])
2803 );
2804
2805 Ok(())
2806 }
2807
2808 #[test]
2809 fn test_apply_bookmark_keyword() -> Result<()> {
2810 let api = new_mem_api();
2811
2812 let records = json!([{
2813 "id": "bookmarkAAAA",
2814 "type": "bookmark",
2815 "parentid": "unfiled",
2816 "parentName": "Unfiled",
2817 "dateAdded": 1_381_542_355_843u64,
2818 "title": "A",
2819 "bmkUri": "http://example.com/a?b=c&d=%s",
2820 "keyword": "ex",
2821 },
2822 {
2823 "id": "unfiled",
2824 "type": "folder",
2825 "parentid": "places",
2826 "dateAdded": 1_381_542_355_843u64,
2827 "title": "Unfiled",
2828 "children": ["bookmarkAAAA"],
2829 }]);
2830
2831 let db_mutex = api.get_sync_connection().unwrap();
2832 let db = db_mutex.lock();
2833 let tx = db.begin_transaction()?;
2834 let applicator = IncomingApplicator::new(&db);
2835
2836 if let Value::Array(records) = records {
2837 for record in records {
2838 applicator.apply_bso(IncomingBso::from_test_content(record))?;
2839 }
2840 } else {
2841 unreachable!("JSON records must be an array");
2842 }
2843
2844 tx.commit()?;
2845
2846 db.execute(
2849 "UPDATE moz_bookmarks_synced SET
2850 validity = :validity
2851 WHERE guid = :guid",
2852 rusqlite::named_params! {
2853 ":validity": SyncedBookmarkValidity::Reupload,
2854 ":guid": SyncGuid::from("bookmarkAAAA"),
2855 },
2856 )?;
2857
2858 let interrupt_scope = db.begin_interrupt_scope()?;
2859
2860 let mut merger = Merger::new(&db, &interrupt_scope, ServerTimestamp(0));
2861 merger.merge()?;
2862
2863 assert_local_json_tree(
2864 &db,
2865 &BookmarkRootGuid::Unfiled.as_guid(),
2866 json!({"children" : [{"guid": "bookmarkAAAA", "url": "http://example.com/a?b=c&d=%s"}]}),
2867 );
2868
2869 let outgoing = fetch_outgoing_records(&db, &interrupt_scope)?;
2870 let record_for_a = outgoing
2871 .iter()
2872 .find(|payload| payload.envelope.id == "bookmarkAAAA")
2873 .expect("Should reupload A");
2874 let bk = record_for_a.to_test_incoming_t::<BookmarkRecord>();
2875 assert_eq!(bk.url.unwrap(), "http://example.com/a?b=c&d=%s");
2876 assert_eq!(bk.keyword.unwrap(), "ex");
2877
2878 Ok(())
2879 }
2880
2881 #[test]
2882 fn test_apply_query() {
2883 let api = new_mem_api();
2885 assert_incoming_creates_local_tree(
2886 &api,
2887 json!([{
2888 "id": "query1______",
2889 "type": "query",
2890 "parentid": "unfiled",
2891 "parentName": "Unfiled Bookmarks",
2892 "dateAdded": 1_381_542_355_843u64,
2893 "title": "Some query",
2894 "bmkUri": "place:tag=foo",
2895 },
2896 {
2897 "id": "unfiled",
2898 "type": "folder",
2899 "parentid": "places",
2900 "dateAdded": 1_381_542_355_843u64,
2901 "title": "Unfiled",
2902 "children": ["query1______"],
2903 }]),
2904 &BookmarkRootGuid::Unfiled.as_guid(),
2905 json!({"children" : [{"guid": "query1______", "url": "place:tag=foo"}]}),
2906 );
2907 let reader = api
2908 .open_connection(ConnectionType::ReadOnly)
2909 .expect("Should open read-only connection");
2910 assert!(
2911 frecency_stale_at(&reader, &Url::parse("place:tag=foo").unwrap())
2912 .expect("Should check stale frecency")
2913 .is_none(),
2914 "Should not mark frecency for queries as stale"
2915 );
2916 }
2917
2918 #[test]
2919 fn test_apply() -> Result<()> {
2920 let api = new_mem_api();
2921 let writer = api.open_connection(ConnectionType::ReadWrite)?;
2922 let db = api.get_sync_connection().unwrap();
2923 let syncer = db.lock();
2924
2925 syncer
2926 .execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
2927 .expect("should work");
2928
2929 insert_local_json_tree(
2930 &writer,
2931 json!({
2932 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
2933 "children": [
2934 {
2935 "guid": "bookmarkAAAA",
2936 "title": "A",
2937 "url": "http://example.com/a",
2938 },
2939 {
2940 "guid": "bookmarkBBBB",
2941 "title": "B",
2942 "url": "http://example.com/b",
2943 },
2944 ]
2945 }),
2946 );
2947 tags::tag_url(
2948 &writer,
2949 &Url::parse("http://example.com/a").expect("Should parse URL for A"),
2950 "baz",
2951 )
2952 .expect("Should tag A");
2953
2954 let records = vec![
2955 json!({
2956 "id": "bookmarkCCCC",
2957 "type": "bookmark",
2958 "parentid": "menu",
2959 "parentName": "menu",
2960 "dateAdded": 1_552_183_116_885u64,
2961 "title": "C",
2962 "bmkUri": "http://example.com/c",
2963 "tags": ["foo", "bar"],
2964 }),
2965 json!({
2966 "id": "menu",
2967 "type": "folder",
2968 "parentid": "places",
2969 "parentName": "",
2970 "dateAdded": 0,
2971 "title": "menu",
2972 "children": ["bookmarkCCCC"],
2973 }),
2974 ];
2975
2976 drop(syncer);
2978 let engine = create_sync_engine(&api);
2979
2980 let incoming = records
2981 .into_iter()
2982 .map(IncomingBso::from_test_content)
2983 .collect();
2984
2985 let mut outgoing = engine_apply_incoming(&engine, incoming);
2986 outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
2987 assert_eq!(
2988 outgoing
2989 .iter()
2990 .map(|p| p.envelope.id.as_str())
2991 .collect::<Vec<_>>(),
2992 vec!["bookmarkAAAA", "bookmarkBBBB", "unfiled",]
2993 );
2994 let record_for_a = outgoing
2995 .iter()
2996 .find(|p| p.envelope.id == "bookmarkAAAA")
2997 .expect("Should upload A");
2998 let content_for_a = record_for_a.to_test_incoming_t::<BookmarkRecord>();
2999 assert_eq!(content_for_a.tags, vec!["baz".to_string()]);
3000
3001 assert_local_json_tree(
3002 &writer,
3003 &BookmarkRootGuid::Root.as_guid(),
3004 json!({
3005 "guid": &BookmarkRootGuid::Root.as_guid(),
3006 "children": [
3007 {
3008 "guid": &BookmarkRootGuid::Menu.as_guid(),
3009 "children": [
3010 {
3011 "guid": "bookmarkCCCC",
3012 "title": "C",
3013 "url": "http://example.com/c",
3014 "date_added": Timestamp(1_552_183_116_885),
3015 },
3016 ],
3017 },
3018 {
3019 "guid": &BookmarkRootGuid::Toolbar.as_guid(),
3020 "children": [],
3021 },
3022 {
3023 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
3024 "children": [
3025 {
3026 "guid": "bookmarkAAAA",
3027 "title": "A",
3028 "url": "http://example.com/a",
3029 },
3030 {
3031 "guid": "bookmarkBBBB",
3032 "title": "B",
3033 "url": "http://example.com/b",
3034 },
3035 ],
3036 },
3037 {
3038 "guid": &BookmarkRootGuid::Mobile.as_guid(),
3039 "children": [],
3040 },
3041 ],
3042 }),
3043 );
3044
3045 let guid_for_a: SyncGuid = "bookmarkAAAA".into();
3048 let info_for_a = get_raw_bookmark(&writer, &guid_for_a)
3049 .expect("Should fetch info for A")
3050 .unwrap();
3051 assert_eq!(info_for_a._sync_change_counter, 2);
3052 let info_for_unfiled = get_raw_bookmark(&writer, &BookmarkRootGuid::Unfiled.as_guid())
3053 .expect("Should fetch info for unfiled")
3054 .unwrap();
3055 assert_eq!(info_for_unfiled._sync_change_counter, 2);
3056
3057 engine
3058 .set_uploaded(
3059 ServerTimestamp(0),
3060 vec![
3061 "bookmarkAAAA".into(),
3062 "bookmarkBBBB".into(),
3063 "unfiled".into(),
3064 ],
3065 )
3066 .expect("Should push synced changes back to the engine");
3067 engine.sync_finished().expect("finish always works");
3068
3069 let info_for_a = get_raw_bookmark(&writer, &guid_for_a)
3070 .expect("Should fetch info for A")
3071 .unwrap();
3072 assert_eq!(info_for_a._sync_change_counter, 0);
3073 let info_for_unfiled = get_raw_bookmark(&writer, &BookmarkRootGuid::Unfiled.as_guid())
3074 .expect("Should fetch info for unfiled")
3075 .unwrap();
3076 assert_eq!(info_for_unfiled._sync_change_counter, 0);
3077
3078 let mut tags_for_c = tags::get_tags_for_url(
3079 &writer,
3080 &Url::parse("http://example.com/c").expect("Should parse URL for C"),
3081 )
3082 .expect("Should return tags for C");
3083 tags_for_c.sort();
3084 assert_eq!(tags_for_c, &["bar", "foo"]);
3085
3086 Ok(())
3087 }
3088
3089 #[test]
3090 fn test_apply_invalid_url() -> Result<()> {
3091 let api = new_mem_api();
3092 let db = api.get_sync_connection().unwrap();
3093 let syncer = db.lock();
3094
3095 syncer
3096 .execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
3097 .expect("should work");
3098
3099 let records = vec![
3100 json!({
3101 "id": "bookmarkXXXX",
3102 "type": "bookmark",
3103 "parentid": "menu",
3104 "parentName": "menu",
3105 "dateAdded": 1_552_183_116_885u64,
3106 "title": "Invalid",
3107 "bmkUri": "invalid url",
3108 }),
3109 json!({
3110 "id": "menu",
3111 "type": "folder",
3112 "parentid": "places",
3113 "parentName": "",
3114 "dateAdded": 0,
3115 "title": "menu",
3116 "children": ["bookmarkXXXX"],
3117 }),
3118 ];
3119
3120 drop(syncer);
3122 let engine = create_sync_engine(&api);
3123
3124 let incoming = records
3125 .into_iter()
3126 .map(IncomingBso::from_test_content)
3127 .collect();
3128
3129 let mut outgoing = engine_apply_incoming(&engine, incoming);
3130 outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
3131 assert_eq!(
3132 outgoing
3133 .iter()
3134 .map(|p| p.envelope.id.as_str())
3135 .collect::<Vec<_>>(),
3136 vec!["bookmarkXXXX", "menu",]
3137 );
3138
3139 let record_for_invalid = outgoing
3140 .iter()
3141 .find(|p| p.envelope.id == "bookmarkXXXX")
3142 .expect("Should re-upload the invalid record");
3143
3144 assert!(
3145 matches!(
3146 record_for_invalid
3147 .to_test_incoming()
3148 .into_content::<BookmarkRecord>()
3149 .kind,
3150 IncomingKind::Tombstone
3151 ),
3152 "is invalid record"
3153 );
3154
3155 let record_for_menu = outgoing
3156 .iter()
3157 .find(|p| p.envelope.id == "menu")
3158 .expect("Should upload menu");
3159 let content_for_menu = record_for_menu.to_test_incoming_t::<FolderRecord>();
3160 assert!(
3161 content_for_menu.children.is_empty(),
3162 "should have been removed from the parent"
3163 );
3164 Ok(())
3165 }
3166
3167 #[test]
3168 fn test_apply_tombstones() -> Result<()> {
3169 let local_modified = Timestamp::now();
3170 let api = new_mem_api();
3171 let writer = api.open_connection(ConnectionType::ReadWrite)?;
3172 insert_local_json_tree(
3173 &writer,
3174 json!({
3175 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
3176 "children": [{
3177 "guid": "bookmarkAAAA",
3178 "title": "A",
3179 "url": "http://example.com/a",
3180 "date_added": local_modified,
3181 "last_modified": local_modified,
3182 }, {
3183 "guid": "separatorAAA",
3184 "type": BookmarkType::Separator as u8,
3185 "date_added": local_modified,
3186 "last_modified": local_modified,
3187 }, {
3188 "guid": "folderAAAAAA",
3189 "children": [{
3190 "guid": "bookmarkBBBB",
3191 "title": "b",
3192 "url": "http://example.com/b",
3193 "date_added": local_modified,
3194 "last_modified": local_modified,
3195 }],
3196 }],
3197 }),
3198 );
3199 let engine = create_sync_engine(&api);
3201 let outgoing = engine_apply_incoming(&engine, vec![]);
3202 let outgoing_ids = outgoing
3203 .iter()
3204 .map(|p| p.envelope.id.clone())
3205 .collect::<Vec<_>>();
3206 assert_eq!(outgoing_ids.len(), 8, "{:?}", outgoing_ids);
3208
3209 engine
3210 .set_uploaded(ServerTimestamp(0), outgoing_ids)
3211 .expect("should work");
3212 engine.sync_finished().expect("should work");
3213
3214 let remote_unfiled = json!({
3216 "id": "unfiled",
3217 "type": "folder",
3218 "parentid": "places",
3219 "title": "Unfiled",
3220 "children": [],
3221 });
3222
3223 let incoming = vec![
3224 IncomingBso::new_test_tombstone(Guid::new("bookmarkAAAA")),
3225 IncomingBso::new_test_tombstone(Guid::new("separatorAAA")),
3226 IncomingBso::new_test_tombstone(Guid::new("folderAAAAAA")),
3227 IncomingBso::new_test_tombstone(Guid::new("bookmarkBBBB")),
3228 IncomingBso::from_test_content(remote_unfiled),
3229 ];
3230
3231 let outgoing = engine_apply_incoming(&engine, incoming);
3232 let outgoing_ids = outgoing
3233 .iter()
3234 .map(|p| p.envelope.id.clone())
3235 .collect::<Vec<_>>();
3236 assert_eq!(outgoing_ids.len(), 0, "{:?}", outgoing_ids);
3237
3238 engine
3239 .set_uploaded(ServerTimestamp(0), outgoing_ids)
3240 .expect("should work");
3241 engine.sync_finished().expect("should work");
3242
3243 assert_local_json_tree(
3245 &api.get_sync_connection().unwrap().lock(),
3246 &BookmarkRootGuid::Unfiled.as_guid(),
3247 json!({"children" : []}),
3248 );
3249 Ok(())
3250 }
3251
3252 #[test]
3253 fn test_keywords() -> Result<()> {
3254 use crate::storage::bookmarks::bookmarks_get_url_for_keyword;
3255
3256 let api = new_mem_api();
3257 let writer = api.open_connection(ConnectionType::ReadWrite)?;
3258
3259 let records = vec![
3260 json!({
3261 "id": "toolbar",
3262 "type": "folder",
3263 "parentid": "places",
3264 "parentName": "",
3265 "dateAdded": 0,
3266 "title": "toolbar",
3267 "children": ["bookmarkAAAA"],
3268 }),
3269 json!({
3270 "id": "bookmarkAAAA",
3271 "type": "bookmark",
3272 "parentid": "toolbar",
3273 "parentName": "toolbar",
3274 "dateAdded": 1_552_183_116_885u64,
3275 "title": "A",
3276 "bmkUri": "http://example.com/a/%s",
3277 "keyword": "a",
3278 }),
3279 ];
3280
3281 let engine = create_sync_engine(&api);
3282
3283 let incoming = records
3284 .into_iter()
3285 .map(IncomingBso::from_test_content)
3286 .collect();
3287
3288 let outgoing = engine_apply_incoming(&engine, incoming);
3289 let mut outgoing_ids = outgoing
3290 .iter()
3291 .map(|p| p.envelope.id.clone())
3292 .collect::<Vec<_>>();
3293 outgoing_ids.sort();
3294 assert_eq!(outgoing_ids, &["menu", "mobile", "toolbar", "unfiled"],);
3295
3296 assert_eq!(
3297 bookmarks_get_url_for_keyword(&writer, "a")?,
3298 Some(Url::parse("http://example.com/a/%s")?)
3299 );
3300
3301 engine
3302 .set_uploaded(ServerTimestamp(0), outgoing_ids)
3303 .expect("Should push synced changes back to the engine");
3304 engine.sync_finished().expect("should work");
3305
3306 update_bookmark(
3307 &writer,
3308 &"bookmarkAAAA".into(),
3309 &UpdatableBookmark {
3310 title: Some("A (local)".into()),
3311 ..UpdatableBookmark::default()
3312 }
3313 .into(),
3314 )?;
3315
3316 let outgoing = engine_apply_incoming(&engine, vec![]);
3317 assert_eq!(outgoing.len(), 1);
3318 let bk = outgoing[0].to_test_incoming_t::<BookmarkRecord>();
3319 assert_eq!(bk.record_id.as_guid(), "bookmarkAAAA");
3320 assert_eq!(bk.keyword.unwrap(), "a");
3321 assert_eq!(bk.url.unwrap(), "http://example.com/a/%s");
3322
3323 let foreign_count = writer
3328 .try_query_row(
3329 "SELECT foreign_count FROM moz_places
3330 WHERE url_hash = hash(:url) AND
3331 url = :url",
3332 &[(":url", &"http://example.com/a/%s")],
3333 |row| -> rusqlite::Result<_> { row.get::<_, i64>(0) },
3334 false,
3335 )?
3336 .expect("Should fetch foreign count for URL A");
3337 assert_eq!(foreign_count, 3);
3338 let err = writer
3339 .execute(
3340 "DELETE FROM moz_places
3341 WHERE url_hash = hash(:url) AND
3342 url = :url",
3343 rusqlite::named_params! {
3344 ":url": "http://example.com/a/%s",
3345 },
3346 )
3347 .expect_err("Should fail to delete URL A with keyword");
3348 match err {
3349 RusqlError::SqliteFailure(e, _) => assert_eq!(e.code, ErrorCode::ConstraintViolation),
3350 _ => panic!("Wanted constraint violation error; got {:?}", err),
3351 }
3352
3353 Ok(())
3354 }
3355
3356 #[test]
3357 fn test_apply_complex_bookmark_keywords() -> Result<()> {
3358 use crate::storage::bookmarks::bookmarks_get_url_for_keyword;
3359
3360 let api = new_mem_api();
3364 let writer = api.open_connection(ConnectionType::ReadWrite)?;
3365
3366 let remote_records = json!([{
3368 "id": "bookmarkAAA1",
3371 "type": "bookmark",
3372 "parentid": "unfiled",
3373 "parentName": "Unfiled",
3374 "title": "A1",
3375 "bmkUri": "http://example.com/a",
3376 "keyword": "one",
3377 }, {
3378 "id": "bookmarkAAA2",
3379 "type": "bookmark",
3380 "parentid": "menu",
3381 "parentName": "Menu",
3382 "title": "A2",
3383 "bmkUri": "http://example.com/a",
3384 "keyword": "one",
3385 }, {
3386 "id": "bookmarkBBB1",
3390 "type": "bookmark",
3391 "parentid": "unfiled",
3392 "parentName": "Unfiled",
3393 "title": "B1",
3394 "bmkUri": "http://example.com/b",
3395 "keyword": "two",
3396 }, {
3397 "id": "bookmarkBBB2",
3398 "type": "bookmark",
3399 "parentid": "menu",
3400 "parentName": "Menu",
3401 "title": "B2",
3402 "bmkUri": "http://example.com/b",
3403 "keyword": "three",
3404 }, {
3405 "id": "bookmarkCCC1",
3409 "type": "bookmark",
3410 "parentid": "unfiled",
3411 "parentName": "Unfiled",
3412 "title": "C1",
3413 "bmkUri": "http://example.com/c",
3414 "keyword": "four",
3415 }, {
3416 "id": "bookmarkCCC2",
3417 "type": "bookmark",
3418 "parentid": "menu",
3419 "parentName": "Menu",
3420 "title": "C2",
3421 "bmkUri": "http://example.com/c",
3422 }, {
3423 "id": "bookmarkDDDD",
3426 "type": "bookmark",
3427 "parentid": "unfiled",
3428 "parentName": "Unfiled",
3429 "title": "D",
3430 "bmkUri": "http://example.com/d",
3431 "keyword": " FIVE ",
3432 }, {
3433 "id": "unfiled",
3434 "type": "folder",
3435 "parentid": "places",
3436 "title": "Unfiled",
3437 "children": ["bookmarkAAA1", "bookmarkBBB1", "bookmarkCCC1", "bookmarkDDDD"],
3438 }, {
3439 "id": "menu",
3440 "type": "folder",
3441 "parentid": "places",
3442 "title": "Menu",
3443 "children": ["bookmarkAAA2", "bookmarkBBB2", "bookmarkCCC2"],
3444 }]);
3445
3446 let engine = create_sync_engine(&api);
3447 let incoming = if let Value::Array(records) = remote_records {
3448 records
3449 .into_iter()
3450 .map(IncomingBso::from_test_content)
3451 .collect()
3452 } else {
3453 unreachable!("JSON records must be an array");
3454 };
3455 let mut outgoing = engine_apply_incoming(&engine, incoming);
3456 outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
3457
3458 assert_local_json_tree(
3459 &writer,
3460 &BookmarkRootGuid::Root.as_guid(),
3461 json!({
3462 "guid": &BookmarkRootGuid::Root.as_guid(),
3463 "children": [{
3464 "guid": &BookmarkRootGuid::Menu.as_guid(),
3465 "children": [{
3466 "guid": "bookmarkAAA2",
3467 "title": "A2",
3468 "url": "http://example.com/a",
3469 }, {
3470 "guid": "bookmarkBBB2",
3471 "title": "B2",
3472 "url": "http://example.com/b",
3473 }, {
3474 "guid": "bookmarkCCC2",
3475 "title": "C2",
3476 "url": "http://example.com/c",
3477 }],
3478 }, {
3479 "guid": &BookmarkRootGuid::Toolbar.as_guid(),
3480 "children": [],
3481 }, {
3482 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
3483 "children": [{
3484 "guid": "bookmarkAAA1",
3485 "title": "A1",
3486 "url": "http://example.com/a",
3487 }, {
3488 "guid": "bookmarkBBB1",
3489 "title": "B1",
3490 "url": "http://example.com/b",
3491 }, {
3492 "guid": "bookmarkCCC1",
3493 "title": "C1",
3494 "url": "http://example.com/c",
3495 }, {
3496 "guid": "bookmarkDDDD",
3497 "title": "D",
3498 "url": "http://example.com/d",
3499 }],
3500 }, {
3501 "guid": &BookmarkRootGuid::Mobile.as_guid(),
3502 "children": [],
3503 }],
3504 }),
3505 );
3506 let url_for_one = bookmarks_get_url_for_keyword(&writer, "one")?
3508 .expect("Should have URL for keyword `one`");
3509 assert_eq!(url_for_one.as_str(), "http://example.com/a");
3510
3511 let keyword_for_b = match (
3512 bookmarks_get_url_for_keyword(&writer, "two")?,
3513 bookmarks_get_url_for_keyword(&writer, "three")?,
3514 ) {
3515 (Some(url), None) => {
3516 assert_eq!(url.as_str(), "http://example.com/b");
3517 "two".to_string()
3518 }
3519 (None, Some(url)) => {
3520 assert_eq!(url.as_str(), "http://example.com/b");
3521 "three".to_string()
3522 }
3523 (Some(_), Some(_)) => panic!("Should pick `two` or `three`, not both"),
3524 (None, None) => panic!("Should have URL for either `two` or `three`"),
3525 };
3526
3527 let keyword_for_c = match bookmarks_get_url_for_keyword(&writer, "four")? {
3528 Some(url) => {
3529 assert_eq!(url.as_str(), "http://example.com/c");
3530 Some("four".to_string())
3531 }
3532 None => None,
3533 };
3534
3535 let url_for_five = bookmarks_get_url_for_keyword(&writer, "five")?
3536 .expect("Should have URL for keyword `five`");
3537 assert_eq!(url_for_five.as_str(), "http://example.com/d");
3538
3539 let expected_outgoing_keywords = &[
3540 ("bookmarkBBB1", Some(keyword_for_b.clone())),
3541 ("bookmarkBBB2", Some(keyword_for_b.clone())),
3542 ("bookmarkCCC1", keyword_for_c.clone()),
3543 ("bookmarkCCC2", keyword_for_c.clone()),
3544 ("menu", None), ("mobile", None),
3546 ("toolbar", None),
3547 ("unfiled", None),
3548 ];
3549 assert_eq!(
3550 outgoing
3551 .iter()
3552 .map(|p| (
3553 p.envelope.id.as_str(),
3554 p.to_test_incoming_t::<BookmarkRecord>().keyword
3555 ))
3556 .collect::<Vec<_>>(),
3557 expected_outgoing_keywords,
3558 "Should upload new bookmarks and fix up keywords",
3559 );
3560
3561 engine
3564 .set_uploaded(
3565 ServerTimestamp(0),
3566 expected_outgoing_keywords
3567 .iter()
3568 .map(|(id, _)| SyncGuid::from(id))
3569 .collect(),
3570 )
3571 .expect("Should push synced changes back to the engine");
3572 engine.sync_finished().expect("should work");
3573
3574 let mut synced_item_for_b = SyncedBookmarkItem::new();
3575 synced_item_for_b
3576 .validity(SyncedBookmarkValidity::Valid)
3577 .kind(SyncedBookmarkKind::Bookmark)
3578 .url(Some("http://example.com/b"))
3579 .keyword(Some(&keyword_for_b));
3580 let mut synced_item_for_c = SyncedBookmarkItem::new();
3581 synced_item_for_c
3582 .validity(SyncedBookmarkValidity::Valid)
3583 .kind(SyncedBookmarkKind::Bookmark)
3584 .url(Some("http://example.com/c"))
3585 .keyword(Some(keyword_for_c.unwrap().as_str()));
3586 let expected_synced_items = &[
3587 ExpectedSyncedItem::with_properties("bookmarkBBB1", &synced_item_for_b, |a| {
3588 a.parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
3589 .title(Some("B1"))
3590 }),
3591 ExpectedSyncedItem::with_properties("bookmarkBBB2", &synced_item_for_b, |a| {
3592 a.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
3593 .title(Some("B2"))
3594 }),
3595 ExpectedSyncedItem::with_properties("bookmarkCCC1", &synced_item_for_c, |a| {
3596 a.parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
3597 .title(Some("C1"))
3598 }),
3599 ExpectedSyncedItem::with_properties("bookmarkCCC2", &synced_item_for_c, |a| {
3600 a.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
3601 .title(Some("C2"))
3602 }),
3603 ];
3604 for item in expected_synced_items {
3605 item.check(&writer)?;
3606 }
3607
3608 Ok(())
3609 }
3610
3611 #[test]
3612 fn test_wipe() -> Result<()> {
3613 let api = new_mem_api();
3614 let writer = api.open_connection(ConnectionType::ReadWrite)?;
3615
3616 let records = vec![
3617 json!({
3618 "id": "toolbar",
3619 "type": "folder",
3620 "parentid": "places",
3621 "parentName": "",
3622 "dateAdded": 0,
3623 "title": "toolbar",
3624 "children": ["folderAAAAAA"],
3625 }),
3626 json!({
3627 "id": "folderAAAAAA",
3628 "type": "folder",
3629 "parentid": "toolbar",
3630 "parentName": "toolbar",
3631 "dateAdded": 0,
3632 "title": "A",
3633 "children": ["bookmarkBBBB"],
3634 }),
3635 json!({
3636 "id": "bookmarkBBBB",
3637 "type": "bookmark",
3638 "parentid": "folderAAAAAA",
3639 "parentName": "A",
3640 "dateAdded": 0,
3641 "title": "A",
3642 "bmkUri": "http://example.com/a",
3643 }),
3644 json!({
3645 "id": "menu",
3646 "type": "folder",
3647 "parentid": "places",
3648 "parentName": "",
3649 "dateAdded": 0,
3650 "title": "menu",
3651 "children": ["folderCCCCCC"],
3652 }),
3653 json!({
3654 "id": "folderCCCCCC",
3655 "type": "folder",
3656 "parentid": "menu",
3657 "parentName": "menu",
3658 "dateAdded": 0,
3659 "title": "A",
3660 "children": ["bookmarkDDDD", "folderEEEEEE"],
3661 }),
3662 json!({
3663 "id": "bookmarkDDDD",
3664 "type": "bookmark",
3665 "parentid": "folderCCCCCC",
3666 "parentName": "C",
3667 "dateAdded": 0,
3668 "title": "D",
3669 "bmkUri": "http://example.com/d",
3670 }),
3671 json!({
3672 "id": "folderEEEEEE",
3673 "type": "folder",
3674 "parentid": "folderCCCCCC",
3675 "parentName": "C",
3676 "dateAdded": 0,
3677 "title": "E",
3678 "children": ["bookmarkFFFF"],
3679 }),
3680 json!({
3681 "id": "bookmarkFFFF",
3682 "type": "bookmark",
3683 "parentid": "folderEEEEEE",
3684 "parentName": "E",
3685 "dateAdded": 0,
3686 "title": "F",
3687 "bmkUri": "http://example.com/f",
3688 }),
3689 ];
3690
3691 let engine = create_sync_engine(&api);
3692
3693 let incoming = records
3694 .into_iter()
3695 .map(IncomingBso::from_test_content)
3696 .collect();
3697
3698 let outgoing = engine_apply_incoming(&engine, incoming);
3699 let mut outgoing_ids = outgoing
3700 .iter()
3701 .map(|p| p.envelope.id.clone())
3702 .collect::<Vec<_>>();
3703 outgoing_ids.sort();
3704 assert_eq!(outgoing_ids, &["menu", "mobile", "toolbar", "unfiled"],);
3705
3706 engine
3707 .set_uploaded(ServerTimestamp(0), outgoing_ids)
3708 .expect("Should push synced changes back to the engine");
3709 engine.sync_finished().expect("should work");
3710
3711 engine.wipe().expect("Should wipe the store");
3712
3713 assert_local_json_tree(
3715 &writer,
3716 &BookmarkRootGuid::Root.as_guid(),
3717 json!({
3718 "guid": &BookmarkRootGuid::Root.as_guid(),
3719 "children": [
3720 {
3721 "guid": &BookmarkRootGuid::Menu.as_guid(),
3722 "children": [],
3723 },
3724 {
3725 "guid": &BookmarkRootGuid::Toolbar.as_guid(),
3726 "children": [],
3727 },
3728 {
3729 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
3730 "children": [],
3731 },
3732 {
3733 "guid": &BookmarkRootGuid::Mobile.as_guid(),
3734 "children": [],
3735 },
3736 ],
3737 }),
3738 );
3739
3740 let record_for_f = json!({
3743 "id": "bookmarkFFFF",
3744 "type": "bookmark",
3745 "parentid": "folderEEEEEE",
3746 "parentName": "E",
3747 "dateAdded": 0,
3748 "title": "F (remote)",
3749 "bmkUri": "http://example.com/f-remote",
3750 });
3751
3752 let incoming = vec![IncomingBso::from_test_content_ts(
3753 record_for_f,
3754 ServerTimestamp(1000),
3755 )];
3756
3757 let outgoing = engine_apply_incoming(&engine, incoming);
3758 let (outgoing_tombstones, outgoing_records): (Vec<_>, Vec<_>) =
3759 outgoing.iter().partition(|record| {
3760 matches!(
3761 record
3762 .to_test_incoming()
3763 .into_content::<BookmarkRecord>()
3764 .kind,
3765 IncomingKind::Tombstone
3766 )
3767 });
3768 let mut outgoing_record_ids = outgoing_records
3769 .iter()
3770 .map(|p| p.envelope.id.as_str())
3771 .collect::<Vec<_>>();
3772 outgoing_record_ids.sort_unstable();
3773 assert_eq!(
3774 outgoing_record_ids,
3775 &["bookmarkFFFF", "menu", "mobile", "toolbar", "unfiled"],
3776 );
3777 let mut outgoing_tombstone_ids = outgoing_tombstones
3778 .iter()
3779 .map(|p| p.envelope.id.clone())
3780 .collect::<Vec<_>>();
3781 outgoing_tombstone_ids.sort();
3782 assert_eq!(
3783 outgoing_tombstone_ids,
3784 &[
3785 "bookmarkBBBB",
3786 "bookmarkDDDD",
3787 "folderAAAAAA",
3788 "folderCCCCCC",
3789 "folderEEEEEE"
3790 ]
3791 );
3792
3793 assert_local_json_tree(
3796 &writer,
3797 &BookmarkRootGuid::Root.as_guid(),
3798 json!({
3799 "guid": &BookmarkRootGuid::Root.as_guid(),
3800 "children": [
3801 {
3802 "guid": &BookmarkRootGuid::Menu.as_guid(),
3803 "children": [
3804 {
3805 "guid": "bookmarkFFFF",
3806 "title": "F (remote)",
3807 "url": "http://example.com/f-remote",
3808 },
3809 ],
3810 },
3811 {
3812 "guid": &BookmarkRootGuid::Toolbar.as_guid(),
3813 "children": [],
3814 },
3815 {
3816 "guid": &BookmarkRootGuid::Unfiled.as_guid(),
3817 "children": [],
3818 },
3819 {
3820 "guid": &BookmarkRootGuid::Mobile.as_guid(),
3821 "children": [],
3822 },
3823 ],
3824 }),
3825 );
3826
3827 Ok(())
3828 }
3829
3830 #[test]
3831 fn test_reset() -> anyhow::Result<()> {
3832 let api = new_mem_api();
3833 let writer = api.open_connection(ConnectionType::ReadWrite)?;
3834
3835 insert_local_json_tree(
3836 &writer,
3837 json!({
3838 "guid": &BookmarkRootGuid::Menu.as_guid(),
3839 "children": [
3840 {
3841 "guid": "bookmark2___",
3842 "title": "2",
3843 "url": "http://example.com/2",
3844 }
3845 ],
3846 }),
3847 );
3848
3849 {
3850 let engine = create_sync_engine(&api);
3852
3853 assert_eq!(
3854 engine.get_sync_assoc()?,
3855 EngineSyncAssociation::Disconnected
3856 );
3857
3858 let outgoing = engine_apply_incoming(&engine, vec![]);
3859 let synced_ids: Vec<Guid> = outgoing.into_iter().map(|c| c.envelope.id).collect();
3860 assert_eq!(synced_ids.len(), 5, "should be 4 roots + 1 outgoing item");
3861 engine.set_uploaded(ServerTimestamp(2_000), synced_ids)?;
3862 engine.sync_finished().expect("should work");
3863
3864 let db = api.get_sync_connection().unwrap();
3865 let syncer = db.lock();
3866 assert_eq!(get_meta::<i64>(&syncer, LAST_SYNC_META_KEY)?, Some(2_000));
3867
3868 let sync_ids = CollSyncIds {
3869 global: Guid::random(),
3870 coll: Guid::random(),
3871 };
3872 drop(syncer);
3875 engine.reset(&EngineSyncAssociation::Connected(sync_ids.clone()))?;
3876 let syncer = db.lock();
3877 assert_eq!(
3878 get_meta::<Guid>(&syncer, GLOBAL_SYNCID_META_KEY)?,
3879 Some(sync_ids.global)
3880 );
3881 assert_eq!(
3882 get_meta::<Guid>(&syncer, COLLECTION_SYNCID_META_KEY)?,
3883 Some(sync_ids.coll)
3884 );
3885 assert_eq!(get_meta::<i64>(&syncer, LAST_SYNC_META_KEY)?, Some(0));
3886 }
3887 {
3889 let engine = create_sync_engine(&api);
3890
3891 let outgoing = engine_apply_incoming(&engine, vec![]);
3892 let synced_ids: Vec<Guid> = outgoing.into_iter().map(|c| c.envelope.id).collect();
3893 assert_eq!(synced_ids.len(), 5, "should be 4 roots + 1 outgoing item");
3894 engine.set_uploaded(ServerTimestamp(2_000), synced_ids)?;
3895 engine.sync_finished().expect("should work");
3896
3897 let db = api.get_sync_connection().unwrap();
3898 let syncer = db.lock();
3899 assert_eq!(get_meta::<i64>(&syncer, LAST_SYNC_META_KEY)?, Some(2_000));
3900
3901 drop(syncer);
3904 engine.reset(&EngineSyncAssociation::Disconnected)?;
3905 let syncer = db.lock();
3906 assert_eq!(
3907 get_meta::<Option<String>>(&syncer, GLOBAL_SYNCID_META_KEY)?,
3908 None
3909 );
3910 assert_eq!(
3911 get_meta::<Option<String>>(&syncer, COLLECTION_SYNCID_META_KEY)?,
3912 None
3913 );
3914 assert_eq!(get_meta::<i64>(&syncer, LAST_SYNC_META_KEY)?, Some(0));
3915 }
3916
3917 Ok(())
3918 }
3919
3920 #[test]
3921 fn test_incoming_timestamps() -> anyhow::Result<()> {
3922 let api = new_mem_api();
3923 let reader = api.open_connection(ConnectionType::ReadOnly)?;
3924
3925 let remote_modified = ServerTimestamp::from_millis(1770000000000);
3927
3928 let records = vec![
3929 json!({
3930 "id": "toolbar",
3931 "type": "folder",
3932 "parentid": "places",
3933 "parentName": "",
3934 "dateAdded": 0,
3935 "title": "toolbar",
3936 "children": ["folderAAAAAA"],
3937 }),
3938 json!({
3939 "id": "folderAAAAAA",
3940 "type": "folder",
3941 "parentid": "toolbar",
3942 "parentName": "toolbar",
3943 "dateAdded": 0,
3944 "title": "A",
3945 "children": ["bookmarkBBBB"],
3946 }),
3947 json!({
3948 "id": "bookmarkBBBB",
3949 "type": "bookmark",
3950 "parentid": "folderAAAAAA",
3951 "parentName": "A",
3952 "dateAdded": 0,
3953 "title": "A",
3954 "bmkUri": "http://example.com/a",
3955 }),
3956 ];
3957
3958 let engine = create_sync_engine(&api);
3959
3960 let incoming = records
3961 .clone()
3962 .into_iter()
3963 .map(|json| IncomingBso::from_test_content_ts(json, remote_modified))
3964 .collect();
3965
3966 engine_apply_incoming(&engine, incoming);
3967
3968 let bm = get_raw_bookmark(&reader, &SyncGuid::new("bookmarkBBBB"))
3970 .expect("must work")
3971 .expect("must exist");
3972
3973 assert_eq!(
3974 bm.date_modified.as_millis_i64(),
3975 remote_modified.as_millis()
3976 );
3977
3978 engine.reset(&EngineSyncAssociation::Disconnected)?;
3980 let incoming = records
3981 .into_iter()
3982 .map(|json| IncomingBso::from_test_content_ts(json, remote_modified))
3983 .collect();
3984 engine_apply_incoming(&engine, incoming);
3985 let bm = get_raw_bookmark(&reader, &SyncGuid::new("bookmarkBBBB"))
3986 .expect("must work")
3987 .expect("must exist");
3988
3989 assert_eq!(
3990 bm.date_modified.as_millis_i64(),
3991 remote_modified.as_millis()
3992 );
3993
3994 let new_records = vec![json!({
3996 "id": "bookmarkBBBB",
3997 "type": "bookmark",
3998 "parentid": "folderAAAAAA",
3999 "parentName": "A",
4000 "dateAdded": 0,
4001 "title": "A",
4002 "bmkUri": "http://example.com/a",
4003 })];
4004 let new_remote_modified = ServerTimestamp::from_millis(1780000000000);
4005 let new_incoming = new_records
4006 .into_iter()
4007 .map(|json| IncomingBso::from_test_content_ts(json, new_remote_modified))
4008 .collect();
4009 engine_apply_incoming(&engine, new_incoming);
4010 let bm = get_raw_bookmark(&reader, &SyncGuid::new("bookmarkBBBB"))
4011 .expect("must work")
4012 .expect("must exist");
4013
4014 assert_eq!(
4015 bm.date_modified.as_millis_i64(),
4016 new_remote_modified.as_millis()
4017 );
4018 Ok(())
4019 }
4020
4021 #[test]
4022 fn test_dedupe_local_newer() -> anyhow::Result<()> {
4023 let api = new_mem_api();
4024 let writer = api.open_connection(ConnectionType::ReadWrite)?;
4025
4026 let local_modified = Timestamp::now();
4027 let remote_modified = local_modified.as_millis() as f64 / 1000f64 - 5f64;
4028
4029 apply_incoming(
4031 &api,
4032 ServerTimestamp::from_float_seconds(remote_modified),
4033 json!([{
4034 "id": "menu",
4035 "type": "folder",
4036 "parentid": "places",
4037 "parentName": "",
4038 "title": "menu",
4039 "children": ["bookmarkAAA5"],
4040 }, {
4041 "id": "bookmarkAAA5",
4042 "type": "bookmark",
4043 "parentid": "menu",
4044 "parentName": "menu",
4045 "title": "A",
4046 "bmkUri": "http://example.com/a",
4047 }]),
4048 );
4049
4050 insert_local_json_tree(
4052 &writer,
4053 json!({
4054 "guid": &BookmarkRootGuid::Menu.as_guid(),
4055 "children": [{
4056 "guid": "bookmarkAAA1",
4057 "title": "A",
4058 "url": "http://example.com/a",
4059 "date_added": local_modified,
4060 "last_modified": local_modified,
4061 }, {
4062 "guid": "bookmarkAAA2",
4063 "title": "A",
4064 "url": "http://example.com/a",
4065 "date_added": local_modified,
4066 "last_modified": local_modified,
4067 }, {
4068 "guid": "bookmarkAAA3",
4069 "title": "A",
4070 "url": "http://example.com/a",
4071 "date_added": local_modified,
4072 "last_modified": local_modified,
4073 }],
4074 }),
4075 );
4076
4077 apply_incoming(
4079 &api,
4080 ServerTimestamp(local_modified.as_millis() as i64),
4081 json!([{
4082 "id": "menu",
4083 "type": "folder",
4084 "parentid": "places",
4085 "parentName": "",
4086 "title": "menu",
4087 "children": ["bookmarkAAAA", "bookmarkAAA4", "bookmarkAAA5"],
4088 }, {
4089 "id": "bookmarkAAAA",
4090 "type": "bookmark",
4091 "parentid": "menu",
4092 "parentName": "menu",
4093 "title": "A",
4094 "bmkUri": "http://example.com/a",
4095 }, {
4096 "id": "bookmarkAAA4",
4097 "type": "bookmark",
4098 "parentid": "menu",
4099 "parentName": "menu",
4100 "title": "A",
4101 "bmkUri": "http://example.com/a",
4102 }]),
4103 );
4104
4105 assert_local_json_tree(
4106 &writer,
4107 &BookmarkRootGuid::Menu.as_guid(),
4108 json!({
4109 "guid": &BookmarkRootGuid::Menu.as_guid(),
4110 "children": [{
4111 "guid": "bookmarkAAAA",
4112 "title": "A",
4113 "url": "http://example.com/a",
4114 }, {
4115 "guid": "bookmarkAAA4",
4116 "title": "A",
4117 "url": "http://example.com/a",
4118 }, {
4119 "guid": "bookmarkAAA5",
4120 "title": "A",
4121 "url": "http://example.com/a",
4122 }, {
4123 "guid": "bookmarkAAA3",
4124 "title": "A",
4125 "url": "http://example.com/a",
4126 }],
4127 }),
4128 );
4129
4130 Ok(())
4131 }
4132
4133 #[test]
4134 fn test_deduping_remote_newer() -> anyhow::Result<()> {
4135 let api = new_mem_api();
4136 let writer = api.open_connection(ConnectionType::ReadWrite)?;
4137
4138 let local_modified = Timestamp::from(Timestamp::now().as_millis() - 5000);
4139 let remote_modified = local_modified.as_millis() as f64 / 1000f64;
4140
4141 apply_incoming(
4143 &api,
4144 ServerTimestamp::from_float_seconds(remote_modified),
4145 json!([{
4146 "id": "menu",
4147 "type": "folder",
4148 "parentid": "places",
4149 "parentName": "",
4150 "title": "menu",
4151 "children": ["folderAAAAAA"],
4152 }, {
4153 "id": "folderAAAAAA",
4155 "type": "folder",
4156 "parentid": "menu",
4157 "parentName": "menu",
4158 "title": "A",
4159 "children": ["bookmarkGGGG"],
4160 }, {
4161 "id": "bookmarkGGGG",
4163 "type": "bookmark",
4164 "parentid": "folderAAAAAA",
4165 "parentName": "A",
4166 "title": "G",
4167 "bmkUri": "http://example.com/g",
4168 }]),
4169 );
4170
4171 insert_local_json_tree(
4173 &writer,
4174 json!({
4175 "guid": "folderAAAAAA",
4176 "children": [{
4177 "guid": "bookmarkHHHH",
4179 "title": "H",
4180 "url": "http://example.com/h",
4181 "date_added": local_modified,
4182 "last_modified": local_modified,
4183 }]
4184 }),
4185 );
4186 insert_local_json_tree(
4187 &writer,
4188 json!({
4189 "guid": &BookmarkRootGuid::Menu.as_guid(),
4190 "children": [{
4191 "guid": "folderBBBBBB",
4193 "type": BookmarkType::Folder as u8,
4194 "title": "B",
4195 "date_added": local_modified,
4196 "last_modified": local_modified,
4197 "children": [{
4198 "guid": "bookmarkC111",
4200 "title": "C",
4201 "url": "http://example.com/c",
4202 "date_added": local_modified,
4203 "last_modified": local_modified,
4204 }, {
4205 "guid": "separatorFFF",
4207 "type": BookmarkType::Separator as u8,
4208 "date_added": local_modified,
4209 "last_modified": local_modified,
4210 }],
4211 }, {
4212 "guid": "separatorEEE",
4214 "type": BookmarkType::Separator as u8,
4215 "date_added": local_modified,
4216 "last_modified": local_modified,
4217 }, {
4218 "guid": "bookmarkCCCC",
4220 "title": "C",
4221 "url": "http://example.com/c",
4222 "date_added": local_modified,
4223 "last_modified": local_modified,
4224 }, {
4225 "guid": "queryDDDDDDD",
4227 "title": "Most Visited",
4228 "url": "place:maxResults=10&sort=8",
4229 "date_added": local_modified,
4230 "last_modified": local_modified,
4231 }],
4232 }),
4233 );
4234
4235 apply_incoming(
4237 &api,
4238 ServerTimestamp::from_float_seconds(remote_modified),
4239 json!([{
4240 "id": "menu",
4241 "type": "folder",
4242 "parentid": "places",
4243 "parentName": "",
4244 "title": "menu",
4245 "children": ["folderAAAAAA", "folderB11111", "folderA11111", "separatorE11", "queryD111111"],
4246 "dateAdded": local_modified.as_millis(),
4247 }, {
4248 "id": "folderB11111",
4249 "type": "folder",
4250 "parentid": "menu",
4251 "parentName": "menu",
4252 "title": "B",
4253 "children": ["bookmarkC222", "separatorF11"],
4254 "dateAdded": local_modified.as_millis(),
4255 }, {
4256 "id": "bookmarkC222",
4257 "type": "bookmark",
4258 "parentid": "folderB11111",
4259 "parentName": "B",
4260 "title": "C",
4261 "bmkUri": "http://example.com/c",
4262 "dateAdded": local_modified.as_millis(),
4263 }, {
4264 "id": "separatorF11",
4265 "type": "separator",
4266 "parentid": "folderB11111",
4267 "parentName": "B",
4268 "dateAdded": local_modified.as_millis(),
4269 }, {
4270 "id": "folderA11111",
4271 "type": "folder",
4272 "parentid": "menu",
4273 "parentName": "menu",
4274 "title": "A",
4275 "children": ["bookmarkG111"],
4276 "dateAdded": local_modified.as_millis(),
4277 }, {
4278 "id": "bookmarkG111",
4279 "type": "bookmark",
4280 "parentid": "folderA11111",
4281 "parentName": "A",
4282 "title": "G",
4283 "bmkUri": "http://example.com/g",
4284 "dateAdded": local_modified.as_millis(),
4285 }, {
4286 "id": "separatorE11",
4287 "type": "separator",
4288 "parentid": "folderB11111",
4289 "parentName": "B",
4290 "dateAdded": local_modified.as_millis(),
4291 }, {
4292 "id": "queryD111111",
4293 "type": "query",
4294 "parentid": "menu",
4295 "parentName": "menu",
4296 "title": "Most Visited",
4297 "bmkUri": "place:maxResults=10&sort=8",
4298 "dateAdded": local_modified.as_millis(),
4299 }]),
4300 );
4301
4302 assert_local_json_tree(
4303 &writer,
4304 &BookmarkRootGuid::Menu.as_guid(),
4305 json!({
4306 "guid": &BookmarkRootGuid::Menu.as_guid(),
4307 "children": [{
4308 "guid": "folderAAAAAA",
4309 "children": [{
4310 "guid": "bookmarkGGGG",
4311 "title": "G",
4312 "url": "http://example.com/g",
4313 }, {
4314 "guid": "bookmarkHHHH",
4315 "title": "H",
4316 "url": "http://example.com/h",
4317 }]
4318 }, {
4319 "guid": "folderB11111",
4320 "children": [{
4321 "guid": "bookmarkC222",
4322 "title": "C",
4323 "url": "http://example.com/c",
4324 }, {
4325 "guid": "separatorF11",
4326 "type": BookmarkType::Separator as u8,
4327 }],
4328 }, {
4329 "guid": "folderA11111",
4330 "children": [{
4331 "guid": "bookmarkG111",
4332 "title": "G",
4333 "url": "http://example.com/g",
4334 }]
4335 }, {
4336 "guid": "separatorE11",
4337 "type": BookmarkType::Separator as u8,
4338 }, {
4339 "guid": "queryD111111",
4340 "title": "Most Visited",
4341 "url": "place:maxResults=10&sort=8",
4342 }, {
4343 "guid": "separatorEEE",
4344 "type": BookmarkType::Separator as u8,
4345 }, {
4346 "guid": "bookmarkCCCC",
4347 "title": "C",
4348 "url": "http://example.com/c",
4349 }],
4350 }),
4351 );
4352
4353 Ok(())
4354 }
4355
4356 #[test]
4357 fn test_reconcile_sync_metadata() -> anyhow::Result<()> {
4358 let api = new_mem_api();
4359 let writer = api.open_connection(ConnectionType::ReadWrite)?;
4360
4361 let local_modified = Timestamp::from(Timestamp::now().as_millis() - 5000);
4362 let remote_modified = local_modified.as_millis() as f64 / 1000f64;
4363
4364 insert_local_json_tree(
4365 &writer,
4366 json!({
4367 "guid": &BookmarkRootGuid::Menu.as_guid(),
4368 "children": [{
4369 "guid": "folderAAAAAA",
4371 "type": BookmarkType::Folder as u8,
4372 "title": "A",
4373 "date_added": local_modified,
4374 "last_modified": local_modified,
4375 "children": [{
4376 "guid": "bookmarkBBBB",
4377 "title": "B",
4378 "url": "http://example.com/b",
4379 "date_added": local_modified,
4380 "last_modified": local_modified,
4381 }]
4382 }, {
4383 "guid": "folderCCCCCC",
4386 "type": BookmarkType::Folder as u8,
4387 "title": "C",
4388 "date_added": local_modified,
4389 "last_modified": local_modified,
4390 "children": [{
4391 "guid": "bookmarkEEEE",
4392 "title": "E",
4393 "url": "http://example.com/e",
4394 "date_added": local_modified,
4395 "last_modified": local_modified,
4396 }]
4397 }, {
4398 "guid": "bookmarkFFFF",
4400 "title": "f",
4401 "url": "http://example.com/f",
4402 "date_added": local_modified,
4403 "last_modified": local_modified,
4404 }],
4405 }),
4406 );
4407
4408 let outgoing = apply_incoming(
4409 &api,
4410 ServerTimestamp::from_float_seconds(remote_modified),
4411 json!([{
4412 "id": "menu",
4413 "type": "folder",
4414 "parentid": "places",
4415 "parentName": "",
4416 "title": "menu",
4417 "children": ["folderAAAAAA", "folderCCCCCC", "bookmarkFFFF"],
4418 "dateAdded": local_modified.as_millis(),
4419 }, {
4420 "id": "folderAAAAAA",
4421 "type": "folder",
4422 "parentid": "menu",
4423 "parentName": "menu",
4424 "title": "A",
4425 "children": ["bookmarkBBBB"],
4426 "dateAdded": local_modified.as_millis(),
4427 }, {
4428 "id": "bookmarkBBBB",
4429 "type": "bookmark",
4430 "parentid": "folderAAAAAA",
4431 "parentName": "A",
4432 "title": "B",
4433 "bmkUri": "http://example.com/b",
4434 "dateAdded": local_modified.as_millis(),
4435 }, {
4436 "id": "folderCCCCCC",
4437 "type": "folder",
4438 "parentid": "menu",
4439 "parentName": "menu",
4440 "title": "C",
4441 "children": ["bookmarkDDDD"],
4442 "dateAdded": local_modified.as_millis(),
4443 }, {
4444 "id": "bookmarkDDDD",
4445 "type": "bookmark",
4446 "parentid": "folderCCCCCC",
4447 "parentName": "C",
4448 "title": "D",
4449 "bmkUri": "http://example.com/d",
4450 "dateAdded": local_modified.as_millis(),
4451 }, {
4452 "id": "bookmarkFFFF",
4453 "type": "bookmark",
4454 "parentid": "menu",
4455 "parentName": "menu",
4456 "title": "F",
4457 "bmkUri": "http://example.com/f",
4458 "dateAdded": local_modified.as_millis(),
4459 },]),
4460 );
4461
4462 assert_local_json_tree(
4465 &writer,
4466 &BookmarkRootGuid::Menu.as_guid(),
4467 json!({
4468 "guid": &BookmarkRootGuid::Menu.as_guid(),
4469 "children": [{
4470 "guid": "folderAAAAAA",
4472 "type": BookmarkType::Folder as u8,
4473 "title": "A",
4474 "children": [{
4475 "guid": "bookmarkBBBB",
4476 "title": "B",
4477 "url": "http://example.com/b",
4478 }]
4479 }, {
4480 "guid": "folderCCCCCC",
4481 "type": BookmarkType::Folder as u8,
4482 "title": "C",
4483 "children": [{
4484 "guid": "bookmarkDDDD",
4485 "title": "D",
4486 "url": "http://example.com/d",
4487 },{
4488 "guid": "bookmarkEEEE",
4489 "title": "E",
4490 "url": "http://example.com/e",
4491 }]
4492 }, {
4493 "guid": "bookmarkFFFF",
4494 "title": "F",
4495 "url": "http://example.com/f",
4496 }],
4497 }),
4498 );
4499
4500 for guid in &[
4503 "folderAAAAAA",
4504 "bookmarkBBBB",
4505 "folderCCCCCC",
4506 "bookmarkDDDD",
4507 "bookmarkFFFF",
4508 ] {
4509 let bm = get_raw_bookmark(&writer, &guid.into())
4510 .expect("must work")
4511 .expect("must exist");
4512 assert_eq!(bm._sync_status, SyncStatus::Normal, "{}", guid);
4513 assert_eq!(bm._sync_change_counter, 0, "{}", guid);
4514 }
4515 assert!(outgoing.contains(&"bookmarkEEEE".into()));
4518 assert!(outgoing.contains(&"folderCCCCCC".into()));
4519 Ok(())
4520 }
4521
4522 #[test]
4529 fn test_handle_unique_guid_violation() -> Result<()> {
4530 let api = new_mem_api();
4531 let db = api.get_sync_connection().unwrap();
4532 let conn = db.lock();
4533
4534 conn.execute_batch(
4535 r#"
4536 INSERT INTO moz_places(url, guid, title, frecency)
4537 VALUES
4538 ('http://example.com/', 'testPlaceGuidAAAA', 'Example site', 0)
4539 "#,
4540 )?;
4541
4542 conn.execute_batch(&format!(
4545 r#"
4546 INSERT INTO moz_bookmarks(guid, parent, fk, position, type)
4547 VALUES (
4548 'collisionGUI',
4549 (SELECT id FROM moz_bookmarks WHERE guid = '{menu}'),
4550 (SELECT id FROM moz_places WHERE guid = 'testPlaceGuidAAAA'),
4551 0,
4552 1 -- type=1 => bookmark
4553 );
4554 "#,
4555 menu = BookmarkRootGuid::Menu.as_guid(),
4556 ))?;
4557
4558 conn.execute(
4563 r#"
4564 INSERT INTO itemsToApply(
4565 mergedGuid,
4566 localId,
4567 remoteId,
4568 remoteGuid,
4569 newKind,
4570 newLevel,
4571 newTitle,
4572 newPlaceId,
4573 oldPlaceId,
4574 localDateAdded,
4575 remoteDateAdded,
4576 lastModified
4577 )
4578 VALUES (
4579 ?1, -- mergedGuid
4580 NULL, -- localId => so it doesn't unify
4581 999, -- remoteId => arbitrary
4582 ?1, -- remoteGuid
4583 1, -- newKind=1 => bookmark
4584 0, -- level
4585 'New Title', -- newTitle
4586 1, -- newPlaceId
4587 NULL, -- oldPlaceId
4588 1000, -- localDateAdded
4589 2000, -- remoteDateAdded
4590 2000 -- lastModified
4591 )
4592 "#,
4593 [&"collisionGUI"],
4594 )?;
4595
4596 let scope = conn.begin_interrupt_scope()?;
4600 apply_remote_items(&conn, &scope, Timestamp(999))?;
4601
4602 assert_local_json_tree(
4604 &conn,
4605 &BookmarkRootGuid::Menu.as_guid(),
4606 json!({
4607 "guid": &BookmarkRootGuid::Menu.as_guid(),
4608 "children": [{
4610 "guid": "collisionGUI",
4611 "title": "New Title", "url": "http://example.com/",
4613 }],
4614 }),
4615 );
4616 Ok(())
4617 }
4618}