places/storage/
bookmarks.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use super::RowId;
6use super::{delete_meta, put_meta};
7use super::{fetch_page_info, new_page_info};
8use crate::bookmark_sync::engine::{
9    COLLECTION_SYNCID_META_KEY, GLOBAL_SYNCID_META_KEY, LAST_SYNC_META_KEY,
10};
11use crate::db::PlacesDb;
12use crate::error::*;
13use crate::types::{BookmarkType, SyncStatus};
14use rusqlite::{self, Connection, Row};
15#[cfg(test)]
16use serde_json::{self, json};
17use sql_support::{self, repeat_sql_vars, ConnExt};
18use std::cmp::{max, min};
19use sync15::engine::EngineSyncAssociation;
20use sync_guid::Guid as SyncGuid;
21use types::Timestamp;
22use url::Url;
23
24pub use root_guid::{BookmarkRootGuid, USER_CONTENT_ROOTS};
25
26mod conversions;
27pub mod fetch;
28pub mod json_tree;
29mod root_guid;
30
31fn create_root(
32    db: &Connection,
33    title: &str,
34    guid: &SyncGuid,
35    position: u32,
36    when: Timestamp,
37) -> rusqlite::Result<()> {
38    let sql = format!(
39        "
40        INSERT INTO moz_bookmarks
41            (type, position, title, dateAdded, lastModified, guid, parent,
42             syncChangeCounter, syncStatus)
43        VALUES
44            (:item_type, :item_position, :item_title, :date_added, :last_modified, :guid,
45             (SELECT id FROM moz_bookmarks WHERE guid = {:?}),
46             1, :sync_status)
47        ",
48        BookmarkRootGuid::Root.as_guid().as_str()
49    );
50    let params = rusqlite::named_params! {
51        ":item_type": &BookmarkType::Folder,
52        ":item_position": &position,
53        ":item_title": &title,
54        ":date_added": &when,
55        ":last_modified": &when,
56        ":guid": guid,
57        ":sync_status": &SyncStatus::New,
58    };
59    db.execute_cached(&sql, params)?;
60    Ok(())
61}
62
63pub fn create_bookmark_roots(db: &Connection) -> rusqlite::Result<()> {
64    let now = Timestamp::now();
65    create_root(db, "root", &BookmarkRootGuid::Root.into(), 0, now)?;
66    create_root(db, "menu", &BookmarkRootGuid::Menu.into(), 0, now)?;
67    create_root(db, "toolbar", &BookmarkRootGuid::Toolbar.into(), 1, now)?;
68    create_root(db, "unfiled", &BookmarkRootGuid::Unfiled.into(), 2, now)?;
69    create_root(db, "mobile", &BookmarkRootGuid::Mobile.into(), 3, now)?;
70    Ok(())
71}
72
73#[derive(Debug, Copy, Clone)]
74pub enum BookmarkPosition {
75    Specific { pos: u32 },
76    Append,
77}
78
79/// Helpers to deal with managing the position correctly.
80///
81/// Updates the position of existing items so that the insertion of a child in
82/// the position specified leaves all siblings with the correct position.
83/// Returns the index the item should be inserted at.
84fn resolve_pos_for_insert(
85    db: &PlacesDb,
86    pos: BookmarkPosition,
87    parent: &RawBookmark,
88) -> Result<u32> {
89    Ok(match pos {
90        BookmarkPosition::Specific { pos } => {
91            let actual = min(pos, parent.child_count);
92            // must reorder existing children.
93            db.execute_cached(
94                "UPDATE moz_bookmarks SET position = position + 1
95                 WHERE parent = :parent_id
96                 AND position >= :position",
97                &[
98                    (":parent_id", &parent.row_id as &dyn rusqlite::ToSql),
99                    (":position", &actual),
100                ],
101            )?;
102            actual
103        }
104        BookmarkPosition::Append => parent.child_count,
105    })
106}
107
108/// Updates the position of existing items so that the deletion of a child
109/// from the position specified leaves all siblings with the correct position.
110fn update_pos_for_deletion(db: &PlacesDb, pos: u32, parent_id: RowId) -> Result<()> {
111    db.execute_cached(
112        "UPDATE moz_bookmarks SET position = position - 1
113         WHERE parent = :parent
114         AND position >= :position",
115        &[
116            (":parent", &parent_id as &dyn rusqlite::ToSql),
117            (":position", &pos),
118        ],
119    )?;
120    Ok(())
121}
122
123/// Updates the position of existing items when an item is being moved in the
124/// same folder.
125/// Returns what the position should be updated to.
126fn update_pos_for_move(
127    db: &PlacesDb,
128    pos: BookmarkPosition,
129    bm: &RawBookmark,
130    parent: &RawBookmark,
131) -> Result<u32> {
132    assert_eq!(bm.parent_id.unwrap(), parent.row_id);
133    // Note the additional -1's below are to account for the item already being
134    // in the folder.
135    let new_index = match pos {
136        BookmarkPosition::Specific { pos } => min(pos, parent.child_count - 1),
137        BookmarkPosition::Append => parent.child_count - 1,
138    };
139    db.execute_cached(
140        "UPDATE moz_bookmarks
141         SET position = CASE WHEN :new_index < :cur_index
142            THEN position + 1
143            ELSE position - 1
144         END
145         WHERE parent = :parent_id
146         AND position BETWEEN :low_index AND :high_index",
147        &[
148            (":new_index", &new_index as &dyn rusqlite::ToSql),
149            (":cur_index", &bm.position),
150            (":parent_id", &parent.row_id),
151            (":low_index", &min(bm.position, new_index)),
152            (":high_index", &max(bm.position, new_index)),
153        ],
154    )?;
155    Ok(new_index)
156}
157
158/// Structures which can be used to insert a bookmark, folder or separator.
159#[derive(Debug, Clone)]
160pub struct InsertableBookmark {
161    pub parent_guid: SyncGuid,
162    pub position: BookmarkPosition,
163    pub date_added: Option<Timestamp>,
164    pub last_modified: Option<Timestamp>,
165    pub guid: Option<SyncGuid>,
166    pub url: Url,
167    pub title: Option<String>,
168}
169
170impl From<InsertableBookmark> for InsertableItem {
171    fn from(b: InsertableBookmark) -> Self {
172        InsertableItem::Bookmark { b }
173    }
174}
175
176#[derive(Debug, Clone)]
177pub struct InsertableSeparator {
178    pub parent_guid: SyncGuid,
179    pub position: BookmarkPosition,
180    pub date_added: Option<Timestamp>,
181    pub last_modified: Option<Timestamp>,
182    pub guid: Option<SyncGuid>,
183}
184
185impl From<InsertableSeparator> for InsertableItem {
186    fn from(s: InsertableSeparator) -> Self {
187        InsertableItem::Separator { s }
188    }
189}
190
191#[derive(Debug, Clone)]
192pub struct InsertableFolder {
193    pub parent_guid: SyncGuid,
194    pub position: BookmarkPosition,
195    pub date_added: Option<Timestamp>,
196    pub last_modified: Option<Timestamp>,
197    pub guid: Option<SyncGuid>,
198    pub title: Option<String>,
199    pub children: Vec<InsertableItem>,
200}
201
202impl From<InsertableFolder> for InsertableItem {
203    fn from(f: InsertableFolder) -> Self {
204        InsertableItem::Folder { f }
205    }
206}
207
208// The type used to insert the actual item.
209#[derive(Debug, Clone)]
210pub enum InsertableItem {
211    Bookmark { b: InsertableBookmark },
212    Separator { s: InsertableSeparator },
213    Folder { f: InsertableFolder },
214}
215
216// We allow all "common" fields from the sub-types to be getters on the
217// InsertableItem type.
218macro_rules! impl_common_bookmark_getter {
219    ($getter_name:ident, $T:ty) => {
220        fn $getter_name(&self) -> &$T {
221            match self {
222                InsertableItem::Bookmark { b } => &b.$getter_name,
223                InsertableItem::Separator { s } => &s.$getter_name,
224                InsertableItem::Folder { f } => &f.$getter_name,
225            }
226        }
227    };
228}
229
230impl InsertableItem {
231    fn bookmark_type(&self) -> BookmarkType {
232        match self {
233            InsertableItem::Bookmark { .. } => BookmarkType::Bookmark,
234            InsertableItem::Separator { .. } => BookmarkType::Separator,
235            InsertableItem::Folder { .. } => BookmarkType::Folder,
236        }
237    }
238    impl_common_bookmark_getter!(parent_guid, SyncGuid);
239    impl_common_bookmark_getter!(position, BookmarkPosition);
240    impl_common_bookmark_getter!(date_added, Option<Timestamp>);
241    impl_common_bookmark_getter!(last_modified, Option<Timestamp>);
242    impl_common_bookmark_getter!(guid, Option<SyncGuid>);
243
244    // We allow a setter for parent_guid and timestamps to help when inserting a tree.
245    fn set_parent_guid(&mut self, guid: SyncGuid) {
246        match self {
247            InsertableItem::Bookmark { b } => b.parent_guid = guid,
248            InsertableItem::Separator { s } => s.parent_guid = guid,
249            InsertableItem::Folder { f } => f.parent_guid = guid,
250        }
251    }
252
253    fn set_last_modified(&mut self, ts: Timestamp) {
254        match self {
255            InsertableItem::Bookmark { b } => b.last_modified = Some(ts),
256            InsertableItem::Separator { s } => s.last_modified = Some(ts),
257            InsertableItem::Folder { f } => f.last_modified = Some(ts),
258        }
259    }
260
261    fn set_date_added(&mut self, ts: Timestamp) {
262        match self {
263            InsertableItem::Bookmark { b } => b.date_added = Some(ts),
264            InsertableItem::Separator { s } => s.date_added = Some(ts),
265            InsertableItem::Folder { f } => f.date_added = Some(ts),
266        }
267    }
268}
269
270pub fn insert_bookmark(db: &PlacesDb, bm: InsertableItem) -> Result<SyncGuid> {
271    let tx = db.begin_transaction()?;
272    let result = insert_bookmark_in_tx(db, bm);
273    super::delete_pending_temp_tables(db)?;
274    match result {
275        Ok(_) => tx.commit()?,
276        Err(_) => tx.rollback()?,
277    }
278    result
279}
280
281pub fn maybe_truncate_title<'a>(t: &Option<&'a str>) -> Option<&'a str> {
282    use super::TITLE_LENGTH_MAX;
283    use crate::util::slice_up_to;
284    t.map(|title| slice_up_to(title, TITLE_LENGTH_MAX))
285}
286
287fn insert_bookmark_in_tx(db: &PlacesDb, bm: InsertableItem) -> Result<SyncGuid> {
288    // find the row ID of the parent.
289    if bm.parent_guid() == BookmarkRootGuid::Root {
290        return Err(InvalidPlaceInfo::CannotUpdateRoot(BookmarkRootGuid::Root).into());
291    }
292    let parent_guid = bm.parent_guid();
293    let parent = get_raw_bookmark(db, parent_guid)?
294        .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(parent_guid.to_string()))?;
295    if parent.bookmark_type != BookmarkType::Folder {
296        return Err(InvalidPlaceInfo::InvalidParent(parent_guid.to_string()).into());
297    }
298    // Do the "position" dance.
299    let position = resolve_pos_for_insert(db, *bm.position(), &parent)?;
300
301    // Note that we could probably do this 'fk' work as a sub-query (although
302    // markh isn't clear how we could perform the insert) - it probably doesn't
303    // matter in practice though...
304    let fk = match bm {
305        InsertableItem::Bookmark { ref b } => {
306            let page_info = match fetch_page_info(db, &b.url)? {
307                Some(info) => info.page,
308                None => new_page_info(db, &b.url, None)?,
309            };
310            Some(page_info.row_id)
311        }
312        _ => None,
313    };
314    let sql = "INSERT INTO moz_bookmarks
315              (fk, type, parent, position, title, dateAdded, lastModified,
316               guid, syncStatus, syncChangeCounter) VALUES
317              (:fk, :type, :parent, :position, :title, :dateAdded, :lastModified,
318               :guid, :syncStatus, :syncChangeCounter)";
319
320    let guid = bm.guid().clone().unwrap_or_else(SyncGuid::random);
321    if !guid.is_valid_for_places() || !guid.is_valid_for_sync_server() {
322        return Err(InvalidPlaceInfo::InvalidGuid.into());
323    }
324    let date_added = bm.date_added().unwrap_or_else(Timestamp::now);
325    // last_modified can't be before date_added
326    let last_modified = max(
327        bm.last_modified().unwrap_or_else(Timestamp::now),
328        date_added,
329    );
330
331    let bookmark_type = bm.bookmark_type();
332    match bm {
333        InsertableItem::Bookmark { ref b } => {
334            let title = maybe_truncate_title(&b.title.as_deref());
335            db.execute_cached(
336                sql,
337                &[
338                    (":fk", &fk as &dyn rusqlite::ToSql),
339                    (":type", &bookmark_type),
340                    (":parent", &parent.row_id),
341                    (":position", &position),
342                    (":title", &title),
343                    (":dateAdded", &date_added),
344                    (":lastModified", &last_modified),
345                    (":guid", &guid),
346                    (":syncStatus", &SyncStatus::New),
347                    (":syncChangeCounter", &1),
348                ],
349            )?;
350        }
351        InsertableItem::Separator { .. } => {
352            db.execute_cached(
353                sql,
354                &[
355                    (":type", &bookmark_type as &dyn rusqlite::ToSql),
356                    (":parent", &parent.row_id),
357                    (":position", &position),
358                    (":dateAdded", &date_added),
359                    (":lastModified", &last_modified),
360                    (":guid", &guid),
361                    (":syncStatus", &SyncStatus::New),
362                    (":syncChangeCounter", &1),
363                ],
364            )?;
365        }
366        InsertableItem::Folder { f } => {
367            let title = maybe_truncate_title(&f.title.as_deref());
368            db.execute_cached(
369                sql,
370                &[
371                    (":type", &bookmark_type as &dyn rusqlite::ToSql),
372                    (":parent", &parent.row_id),
373                    (":title", &title),
374                    (":position", &position),
375                    (":dateAdded", &date_added),
376                    (":lastModified", &last_modified),
377                    (":guid", &guid),
378                    (":syncStatus", &SyncStatus::New),
379                    (":syncChangeCounter", &1),
380                ],
381            )?;
382            // now recurse for children
383            for mut child in f.children.into_iter() {
384                // As a special case for trees, each child in a folder can specify
385                // Guid::Empty as the parent.
386                let specified_parent_guid = child.parent_guid();
387                if specified_parent_guid.is_empty() {
388                    child.set_parent_guid(guid.clone());
389                } else if *specified_parent_guid != guid {
390                    return Err(
391                        InvalidPlaceInfo::InvalidParent(specified_parent_guid.to_string()).into(),
392                    );
393                }
394                // If children have defaults for last_modified and date_added we use the parent
395                if child.last_modified().is_none() {
396                    child.set_last_modified(last_modified);
397                }
398                if child.date_added().is_none() {
399                    child.set_date_added(date_added);
400                }
401                insert_bookmark_in_tx(db, child)?;
402            }
403        }
404    };
405
406    // Bump the parent's change counter.
407    let sql_counter = "
408        UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1
409        WHERE id = :parent_id";
410    db.execute_cached(sql_counter, &[(":parent_id", &parent.row_id)])?;
411
412    Ok(guid)
413}
414
415/// Delete the specified bookmark. Returns true if a bookmark with the guid
416/// existed and was deleted, false otherwise.
417pub fn delete_bookmark(db: &PlacesDb, guid: &SyncGuid) -> Result<bool> {
418    let tx = db.begin_transaction()?;
419    let result = delete_bookmark_in_tx(db, guid);
420    match result {
421        Ok(_) => tx.commit()?,
422        Err(_) => tx.rollback()?,
423    }
424    result
425}
426
427fn delete_bookmark_in_tx(db: &PlacesDb, guid: &SyncGuid) -> Result<bool> {
428    // Can't delete a root.
429    if let Some(root) = BookmarkRootGuid::well_known(guid.as_str()) {
430        return Err(InvalidPlaceInfo::CannotUpdateRoot(root).into());
431    }
432    let record = match get_raw_bookmark(db, guid)? {
433        Some(r) => r,
434        None => {
435            debug!("Can't delete bookmark '{:?}' as it doesn't exist", guid);
436            return Ok(false);
437        }
438    };
439    // There's an argument to be made here that we should still honor the
440    // deletion in the case of this corruption, since it would be fixed by
441    // performing the deletion, and the user wants it gone...
442    let record_parent_id = record
443        .parent_id
444        .ok_or_else(|| Corruption::NonRootWithoutParent(guid.to_string()))?;
445    // must reorder existing children.
446    update_pos_for_deletion(db, record.position, record_parent_id)?;
447    // and delete - children are recursively deleted.
448    db.execute_cached(
449        "DELETE from moz_bookmarks WHERE id = :id",
450        &[(":id", &record.row_id)],
451    )?;
452    super::delete_pending_temp_tables(db)?;
453    Ok(true)
454}
455
456/// Support for modifying bookmarks, including changing the location in
457/// the tree.
458
459// Used to specify how the location of the item in the tree should be updated.
460#[derive(Debug, Default, Clone)]
461pub enum UpdateTreeLocation {
462    #[default]
463    None, // no change
464    Position {
465        pos: BookmarkPosition,
466    }, // new position in the same folder.
467    Parent {
468        guid: SyncGuid,
469        pos: BookmarkPosition,
470    }, // new parent
471}
472
473/// Structures which can be used to update a bookmark, folder or separator.
474/// Almost all fields are Option<>-like, with None meaning "do not change".
475/// Many fields which can't be changed by our public API are omitted (eg,
476/// guid, date_added, last_modified, etc)
477#[derive(Debug, Clone, Default)]
478pub struct UpdatableBookmark {
479    pub location: UpdateTreeLocation,
480    pub url: Option<Url>,
481    pub title: Option<String>,
482}
483
484impl From<UpdatableBookmark> for UpdatableItem {
485    fn from(b: UpdatableBookmark) -> Self {
486        UpdatableItem::Bookmark { b }
487    }
488}
489
490#[derive(Debug, Clone)]
491pub struct UpdatableSeparator {
492    pub location: UpdateTreeLocation,
493}
494
495impl From<UpdatableSeparator> for UpdatableItem {
496    fn from(s: UpdatableSeparator) -> Self {
497        UpdatableItem::Separator { s }
498    }
499}
500
501#[derive(Debug, Clone, Default)]
502pub struct UpdatableFolder {
503    pub location: UpdateTreeLocation,
504    pub title: Option<String>,
505}
506
507impl From<UpdatableFolder> for UpdatableItem {
508    fn from(f: UpdatableFolder) -> Self {
509        UpdatableItem::Folder { f }
510    }
511}
512
513// The type used to update the actual item.
514#[derive(Debug, Clone)]
515pub enum UpdatableItem {
516    Bookmark { b: UpdatableBookmark },
517    Separator { s: UpdatableSeparator },
518    Folder { f: UpdatableFolder },
519}
520
521impl UpdatableItem {
522    fn bookmark_type(&self) -> BookmarkType {
523        match self {
524            UpdatableItem::Bookmark { .. } => BookmarkType::Bookmark,
525            UpdatableItem::Separator { .. } => BookmarkType::Separator,
526            UpdatableItem::Folder { .. } => BookmarkType::Folder,
527        }
528    }
529
530    pub fn location(&self) -> &UpdateTreeLocation {
531        match self {
532            UpdatableItem::Bookmark { b } => &b.location,
533            UpdatableItem::Separator { s } => &s.location,
534            UpdatableItem::Folder { f } => &f.location,
535        }
536    }
537}
538
539/// We don't require bookmark type for updates on the other side of the FFI,
540/// since the type is immutable, and iOS wants to be able to move bookmarks by
541/// GUID. We also don't/can't enforce as much in the Kotlin/Swift type system
542/// as we can/do in Rust.
543///
544/// This is a type that represents the data we get from the FFI, which we then
545/// turn into a `UpdatableItem` that we can actually use (we do this by
546/// reading the type out of the DB, but we can do that transactionally, so it's
547/// not a problem).
548///
549/// It's basically an intermediate between the protobuf message format and
550/// `UpdatableItem`, used to avoid needing to pass in the `type` to update, and
551/// to give us a place to check things that we can't enforce in Swift/Kotlin's
552/// type system, but that we do in Rust's.
553#[derive(Debug, Clone, PartialEq, Eq)]
554pub struct BookmarkUpdateInfo {
555    pub guid: SyncGuid,
556    pub title: Option<String>,
557    pub url: Option<String>,
558    pub parent_guid: Option<SyncGuid>,
559    pub position: Option<u32>,
560}
561
562pub fn update_bookmark_from_info(db: &PlacesDb, info: BookmarkUpdateInfo) -> Result<()> {
563    let tx = db.begin_transaction()?;
564    let existing = get_raw_bookmark(db, &info.guid)?
565        .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(info.guid.to_string()))?;
566    let (guid, updatable) = info.into_updatable(existing.bookmark_type)?;
567
568    update_bookmark_in_tx(db, &guid, &updatable, existing)?;
569    tx.commit()?;
570    Ok(())
571}
572
573pub fn update_bookmark(db: &PlacesDb, guid: &SyncGuid, item: &UpdatableItem) -> Result<()> {
574    let tx = db.begin_transaction()?;
575    let existing = get_raw_bookmark(db, guid)?
576        .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(guid.to_string()))?;
577    let result = update_bookmark_in_tx(db, guid, item, existing);
578    super::delete_pending_temp_tables(db)?;
579    // Note: `tx` automatically rolls back on drop if we don't commit
580    tx.commit()?;
581    result
582}
583
584fn update_bookmark_in_tx(
585    db: &PlacesDb,
586    guid: &SyncGuid,
587    item: &UpdatableItem,
588    raw: RawBookmark,
589) -> Result<()> {
590    // if guid is root
591    if BookmarkRootGuid::well_known(guid.as_str()).is_some() {
592        return Err(InvalidPlaceInfo::CannotUpdateRoot(BookmarkRootGuid::Root).into());
593    }
594    let existing_parent_guid = raw
595        .parent_guid
596        .as_ref()
597        .ok_or_else(|| Corruption::NonRootWithoutParent(guid.to_string()))?;
598
599    let existing_parent_id = raw
600        .parent_id
601        .ok_or_else(|| Corruption::NoParent(guid.to_string(), existing_parent_guid.to_string()))?;
602
603    if raw.bookmark_type != item.bookmark_type() {
604        return Err(InvalidPlaceInfo::MismatchedBookmarkType(
605            raw.bookmark_type as u8,
606            item.bookmark_type() as u8,
607        )
608        .into());
609    }
610
611    let update_old_parent_status;
612    let update_new_parent_status;
613    // to make our life easier we update every field, using existing when
614    // no value is specified.
615    let parent_id;
616    let position;
617    match item.location() {
618        UpdateTreeLocation::None => {
619            parent_id = existing_parent_id;
620            position = raw.position;
621            update_old_parent_status = false;
622            update_new_parent_status = false;
623        }
624        UpdateTreeLocation::Position { pos } => {
625            parent_id = existing_parent_id;
626            update_old_parent_status = true;
627            update_new_parent_status = false;
628            let parent = get_raw_bookmark(db, existing_parent_guid)?.ok_or_else(|| {
629                Corruption::NoParent(guid.to_string(), existing_parent_guid.to_string())
630            })?;
631            position = update_pos_for_move(db, *pos, &raw, &parent)?;
632        }
633        UpdateTreeLocation::Parent {
634            guid: new_parent_guid,
635            pos,
636        } => {
637            if new_parent_guid == BookmarkRootGuid::Root {
638                return Err(InvalidPlaceInfo::CannotUpdateRoot(BookmarkRootGuid::Root).into());
639            }
640            let new_parent = get_raw_bookmark(db, new_parent_guid)?
641                .ok_or_else(|| InvalidPlaceInfo::NoSuchGuid(new_parent_guid.to_string()))?;
642            if new_parent.bookmark_type != BookmarkType::Folder {
643                return Err(InvalidPlaceInfo::InvalidParent(new_parent_guid.to_string()).into());
644            }
645            parent_id = new_parent.row_id;
646            update_old_parent_status = true;
647            update_new_parent_status = true;
648            let existing_parent = get_raw_bookmark(db, existing_parent_guid)?.ok_or_else(|| {
649                Corruption::NoParent(guid.to_string(), existing_parent_guid.to_string())
650            })?;
651            update_pos_for_deletion(db, raw.position, existing_parent.row_id)?;
652            position = resolve_pos_for_insert(db, *pos, &new_parent)?;
653        }
654    };
655    let place_id = match item {
656        UpdatableItem::Bookmark { b } => match &b.url {
657            None => raw.place_id,
658            Some(url) => {
659                let page_info = match fetch_page_info(db, url)? {
660                    Some(info) => info.page,
661                    None => new_page_info(db, url, None)?,
662                };
663                Some(page_info.row_id)
664            }
665        },
666        _ => {
667            // Updating a non-bookmark item, so the existing item must not
668            // have a place_id
669            assert_eq!(raw.place_id, None);
670            None
671        }
672    };
673    // While we could let the SQL take care of being clever about the update
674    // via, say `title = NULLIF(IFNULL(:title, title), '')`, this code needs
675    // to know if it changed so the sync counter can be managed correctly.
676    let update_title = match item {
677        UpdatableItem::Bookmark { b } => &b.title,
678        UpdatableItem::Folder { f } => &f.title,
679        UpdatableItem::Separator { .. } => &None,
680    };
681
682    let title: Option<String> = match update_title {
683        None => raw.title.clone(),
684        // We don't differentiate between null and the empty string for title,
685        // just like desktop doesn't post bug 1360872, hence an empty string
686        // means "set to null".
687        Some(val) => {
688            if val.is_empty() {
689                None
690            } else {
691                Some(val.clone())
692            }
693        }
694    };
695
696    let change_incr = title != raw.title || place_id != raw.place_id;
697
698    let now = Timestamp::now();
699
700    let sql = "
701        UPDATE moz_bookmarks SET
702            fk = :fk,
703            parent = :parent,
704            position = :position,
705            title = :title,
706            lastModified = :now,
707            syncChangeCounter = syncChangeCounter + :change_incr
708        WHERE id = :id";
709
710    db.execute_cached(
711        sql,
712        &[
713            (":fk", &place_id as &dyn rusqlite::ToSql),
714            (":parent", &parent_id),
715            (":position", &position),
716            (":title", &maybe_truncate_title(&title.as_deref())),
717            (":now", &now),
718            (":change_incr", &(change_incr as u32)),
719            (":id", &raw.row_id),
720        ],
721    )?;
722
723    let sql_counter = "
724        UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1
725        WHERE id = :parent_id";
726
727    // The lastModified of the existing parent ancestors (which may still be
728    // the current parent) is always updated, even if the change counter for it
729    // isn't.
730    set_ancestors_last_modified(db, existing_parent_id, now)?;
731    if update_old_parent_status {
732        db.execute_cached(sql_counter, &[(":parent_id", &existing_parent_id)])?;
733    }
734    if update_new_parent_status {
735        set_ancestors_last_modified(db, parent_id, now)?;
736        db.execute_cached(sql_counter, &[(":parent_id", &parent_id)])?;
737    }
738    Ok(())
739}
740
741fn set_ancestors_last_modified(db: &PlacesDb, parent_id: RowId, time: Timestamp) -> Result<()> {
742    let sql = "
743        WITH RECURSIVE
744        ancestors(aid) AS (
745            SELECT :parent_id
746            UNION ALL
747            SELECT parent FROM moz_bookmarks
748            JOIN ancestors ON id = aid
749            WHERE type = :type
750        )
751        UPDATE moz_bookmarks SET lastModified = :time
752        WHERE id IN ancestors
753    ";
754    db.execute_cached(
755        sql,
756        &[
757            (":parent_id", &parent_id as &dyn rusqlite::ToSql),
758            (":type", &(BookmarkType::Folder as u8)),
759            (":time", &time),
760        ],
761    )?;
762    Ok(())
763}
764
765/// Get the URL of the bookmark matching a keyword
766pub fn bookmarks_get_url_for_keyword(db: &PlacesDb, keyword: &str) -> Result<Option<Url>> {
767    let bookmark_url = db.try_query_row(
768        "SELECT h.url FROM moz_keywords k
769         JOIN moz_places h ON h.id = k.place_id
770         WHERE k.keyword = :keyword",
771        &[(":keyword", &keyword)],
772        |row| row.get::<_, String>("url"),
773        true,
774    )?;
775
776    match bookmark_url {
777        Some(b) => match Url::parse(&b) {
778            Ok(u) => Ok(Some(u)),
779            Err(e) => {
780                // We don't have the guid to log and the keyword is PII...
781                warn!("ignoring invalid url: {:?}", e);
782                Ok(None)
783            }
784        },
785        None => Ok(None),
786    }
787}
788
789// Counts the number of bookmark items in the bookmark trees under the specified GUIDs.
790// Does not count folder items, separators. A set of empty folders will return zero, as will
791// a set of non-existing GUIDs or guids of a non-folder item.
792// The result is implementation dependant if the trees overlap.
793pub fn count_bookmarks_in_trees(db: &PlacesDb, item_guids: &[SyncGuid]) -> Result<u32> {
794    if item_guids.is_empty() {
795        return Ok(0);
796    }
797    let vars = repeat_sql_vars(item_guids.len());
798    let sql = format!(
799        r#"
800        WITH RECURSIVE bookmark_tree(id, parent, type)
801        AS (
802            SELECT id, parent, type FROM moz_bookmarks
803            WHERE parent IN (SELECT id from moz_bookmarks WHERE guid IN ({vars}))
804            UNION ALL
805            SELECT bm.id, bm.parent, bm.type FROM moz_bookmarks bm, bookmark_tree bt WHERE bm.parent = bt.id
806        )
807    SELECT COUNT(*) from bookmark_tree WHERE type = {};
808    "#,
809        BookmarkType::Bookmark as u8
810    );
811    let params = rusqlite::params_from_iter(item_guids);
812    Ok(db.try_query_one(&sql, params, true)?.unwrap_or_default())
813}
814
815/// Erases all bookmarks and resets all Sync metadata.
816pub fn delete_everything(db: &PlacesDb) -> Result<()> {
817    let tx = db.begin_transaction()?;
818    db.execute_batch(&format!(
819        "DELETE FROM moz_bookmarks
820         WHERE guid NOT IN ('{}', '{}', '{}', '{}', '{}');",
821        BookmarkRootGuid::Root.as_str(),
822        BookmarkRootGuid::Menu.as_str(),
823        BookmarkRootGuid::Mobile.as_str(),
824        BookmarkRootGuid::Toolbar.as_str(),
825        BookmarkRootGuid::Unfiled.as_str(),
826    ))?;
827    reset_in_tx(db, &EngineSyncAssociation::Disconnected)?;
828    tx.commit()?;
829    Ok(())
830}
831
832/// A "raw" bookmark - a representation of the row and some summary fields.
833#[derive(Debug)]
834pub(crate) struct RawBookmark {
835    pub place_id: Option<RowId>,
836    pub row_id: RowId,
837    pub bookmark_type: BookmarkType,
838    pub parent_id: Option<RowId>,
839    pub parent_guid: Option<SyncGuid>,
840    pub position: u32,
841    pub title: Option<String>,
842    pub url: Option<Url>,
843    pub date_added: Timestamp,
844    pub date_modified: Timestamp,
845    pub guid: SyncGuid,
846    pub _sync_status: SyncStatus,
847    pub _sync_change_counter: u32,
848    pub child_count: u32,
849    pub _grandparent_id: Option<RowId>,
850}
851
852impl RawBookmark {
853    pub fn from_row(row: &Row<'_>) -> Result<Self> {
854        let place_id = row.get::<_, Option<RowId>>("fk")?;
855        Ok(Self {
856            row_id: row.get("_id")?,
857            place_id,
858            bookmark_type: BookmarkType::from_u8_with_valid_url(row.get::<_, u8>("type")?, || {
859                place_id.is_some()
860            }),
861            parent_id: row.get("_parentId")?,
862            parent_guid: row.get("parentGuid")?,
863            position: row.get("position")?,
864            title: row.get::<_, Option<String>>("title")?,
865            url: match row.get::<_, Option<String>>("url")? {
866                Some(s) => Some(Url::parse(&s)?),
867                None => None,
868            },
869            date_added: row.get("dateAdded")?,
870            date_modified: row.get("lastModified")?,
871            guid: row.get::<_, String>("guid")?.into(),
872            _sync_status: SyncStatus::from_u8(row.get::<_, u8>("_syncStatus")?),
873            _sync_change_counter: row
874                .get::<_, Option<u32>>("syncChangeCounter")?
875                .unwrap_or_default(),
876            child_count: row.get("_childCount")?,
877            _grandparent_id: row.get("_grandparentId")?,
878        })
879    }
880}
881
882/// sql is based on fetchBookmark() in Desktop's Bookmarks.jsm, with 'fk' added
883/// and title's NULLIF handling.
884const RAW_BOOKMARK_SQL: &str = "
885    SELECT
886        b.guid,
887        p.guid AS parentGuid,
888        b.position,
889        b.dateAdded,
890        b.lastModified,
891        b.type,
892        -- Note we return null for titles with an empty string.
893        NULLIF(b.title, '') AS title,
894        h.url AS url,
895        b.id AS _id,
896        b.parent AS _parentId,
897        (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
898        p.parent AS _grandParentId,
899        b.syncStatus AS _syncStatus,
900        -- the columns below don't appear in the desktop query
901        b.fk,
902        b.syncChangeCounter
903    FROM moz_bookmarks b
904    LEFT JOIN moz_bookmarks p ON p.id = b.parent
905    LEFT JOIN moz_places h ON h.id = b.fk
906";
907
908pub(crate) fn get_raw_bookmark(db: &PlacesDb, guid: &SyncGuid) -> Result<Option<RawBookmark>> {
909    // sql is based on fetchBookmark() in Desktop's Bookmarks.jsm, with 'fk' added
910    // and title's NULLIF handling.
911    db.try_query_row(
912        &format!("{} WHERE b.guid = :guid", RAW_BOOKMARK_SQL),
913        &[(":guid", guid)],
914        RawBookmark::from_row,
915        true,
916    )
917}
918
919fn get_raw_bookmarks_for_url(db: &PlacesDb, url: &Url) -> Result<Vec<RawBookmark>> {
920    db.query_rows_into_cached(
921        &format!(
922            "{} WHERE h.url_hash = hash(:url) AND h.url = :url",
923            RAW_BOOKMARK_SQL
924        ),
925        &[(":url", &url.as_str())],
926        RawBookmark::from_row,
927    )
928}
929
930fn reset_in_tx(db: &PlacesDb, assoc: &EngineSyncAssociation) -> Result<()> {
931    // Remove all synced bookmarks and pending tombstones, and mark all
932    // local bookmarks as new.
933    db.execute_batch(&format!(
934        "DELETE FROM moz_bookmarks_synced;
935
936        DELETE FROM moz_bookmarks_deleted;
937
938        UPDATE moz_bookmarks
939        SET syncChangeCounter = 1,
940            syncStatus = {}",
941        (SyncStatus::New as u8)
942    ))?;
943
944    // Recreate the set of synced roots, since we just removed all synced
945    // bookmarks.
946    bookmark_sync::create_synced_bookmark_roots(db)?;
947
948    // Reset the last sync time, so that the next sync fetches fresh records
949    // from the server.
950    put_meta(db, LAST_SYNC_META_KEY, &0)?;
951
952    // Clear the sync ID if we're signing out, or set it to whatever the
953    // server gave us if we're signing in.
954    match assoc {
955        EngineSyncAssociation::Disconnected => {
956            delete_meta(db, GLOBAL_SYNCID_META_KEY)?;
957            delete_meta(db, COLLECTION_SYNCID_META_KEY)?;
958        }
959        EngineSyncAssociation::Connected(ids) => {
960            put_meta(db, GLOBAL_SYNCID_META_KEY, &ids.global)?;
961            put_meta(db, COLLECTION_SYNCID_META_KEY, &ids.coll)?;
962        }
963    }
964
965    Ok(())
966}
967
968pub mod bookmark_sync {
969    use super::*;
970    use crate::bookmark_sync::SyncedBookmarkKind;
971
972    /// Removes all sync metadata, including synced bookmarks, pending tombstones,
973    /// change counters, sync statuses, the last sync time, and sync ID. This
974    /// should be called when the user signs out of Sync.
975    pub(crate) fn reset(db: &PlacesDb, assoc: &EngineSyncAssociation) -> Result<()> {
976        let tx = db.begin_transaction()?;
977        reset_in_tx(db, assoc)?;
978        tx.commit()?;
979        Ok(())
980    }
981
982    /// Sets up the syncable roots. All items in `moz_bookmarks_synced` descend
983    /// from these roots.
984    pub fn create_synced_bookmark_roots(db: &Connection) -> rusqlite::Result<()> {
985        // NOTE: This is called in a transaction.
986        fn maybe_insert(
987            db: &Connection,
988            guid: &SyncGuid,
989            parent_guid: &SyncGuid,
990            pos: u32,
991        ) -> rusqlite::Result<()> {
992            db.execute_batch(&format!(
993                "INSERT OR IGNORE INTO moz_bookmarks_synced(guid, parentGuid, kind)
994                VALUES('{guid}', '{parent_guid}', {kind});
995
996                INSERT OR IGNORE INTO moz_bookmarks_synced_structure(
997                    guid, parentGuid, position)
998                VALUES('{guid}', '{parent_guid}', {pos});",
999                guid = guid.as_str(),
1000                parent_guid = parent_guid.as_str(),
1001                kind = SyncedBookmarkKind::Folder as u8,
1002                pos = pos
1003            ))?;
1004            Ok(())
1005        }
1006
1007        // The Places root is its own parent, to ensure it's always in
1008        // `moz_bookmarks_synced_structure`.
1009        maybe_insert(
1010            db,
1011            &BookmarkRootGuid::Root.as_guid(),
1012            &BookmarkRootGuid::Root.as_guid(),
1013            0,
1014        )?;
1015        for (pos, user_root) in USER_CONTENT_ROOTS.iter().enumerate() {
1016            maybe_insert(
1017                db,
1018                &user_root.as_guid(),
1019                &BookmarkRootGuid::Root.as_guid(),
1020                pos as u32,
1021            )?;
1022        }
1023        Ok(())
1024    }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030    use crate::api::places_api::test::new_mem_connection;
1031    use crate::db::PlacesDb;
1032    use crate::storage::get_meta;
1033    use crate::tests::{append_invalid_bookmark, assert_json_tree, insert_json_tree};
1034    use json_tree::*;
1035    use serde_json::Value;
1036    use std::collections::HashSet;
1037
1038    fn get_pos(conn: &PlacesDb, guid: &SyncGuid) -> u32 {
1039        get_raw_bookmark(conn, guid)
1040            .expect("should work")
1041            .unwrap()
1042            .position
1043    }
1044
1045    #[test]
1046    fn test_bookmark_url_for_keyword() -> Result<()> {
1047        let conn = new_mem_connection();
1048
1049        let url = Url::parse("http://example.com").expect("valid url");
1050
1051        conn.execute_cached(
1052            "INSERT INTO moz_places (guid, url, url_hash) VALUES ('fake_guid___', :url, hash(:url))",
1053            &[(":url", &String::from(url))],
1054        )
1055        .expect("should work");
1056        let place_id = conn.last_insert_rowid();
1057
1058        // create a bookmark with keyword 'donut' pointing at it.
1059        conn.execute_cached(
1060            "INSERT INTO moz_keywords
1061                (keyword, place_id)
1062            VALUES
1063                ('donut', :place_id)",
1064            &[(":place_id", &place_id)],
1065        )
1066        .expect("should work");
1067
1068        assert_eq!(
1069            bookmarks_get_url_for_keyword(&conn, "donut")?,
1070            Some(Url::parse("http://example.com")?)
1071        );
1072        assert_eq!(bookmarks_get_url_for_keyword(&conn, "juice")?, None);
1073
1074        // now change the keyword to 'ice cream'
1075        conn.execute_cached(
1076            "REPLACE INTO moz_keywords
1077                (keyword, place_id)
1078            VALUES
1079                ('ice cream', :place_id)",
1080            &[(":place_id", &place_id)],
1081        )
1082        .expect("should work");
1083
1084        assert_eq!(
1085            bookmarks_get_url_for_keyword(&conn, "ice cream")?,
1086            Some(Url::parse("http://example.com")?)
1087        );
1088        assert_eq!(bookmarks_get_url_for_keyword(&conn, "donut")?, None);
1089        assert_eq!(bookmarks_get_url_for_keyword(&conn, "ice")?, None);
1090
1091        Ok(())
1092    }
1093
1094    #[test]
1095    fn test_bookmark_invalid_url_for_keyword() -> Result<()> {
1096        let conn = new_mem_connection();
1097
1098        let place_id =
1099            append_invalid_bookmark(&conn, BookmarkRootGuid::Unfiled.guid(), "invalid", "badurl")
1100                .place_id;
1101
1102        // create a bookmark with keyword 'donut' pointing at it.
1103        conn.execute_cached(
1104            "INSERT INTO moz_keywords
1105                (keyword, place_id)
1106            VALUES
1107                ('donut', :place_id)",
1108            &[(":place_id", &place_id)],
1109        )
1110        .expect("should work");
1111
1112        assert_eq!(bookmarks_get_url_for_keyword(&conn, "donut")?, None);
1113
1114        Ok(())
1115    }
1116
1117    #[test]
1118    fn test_insert() -> Result<()> {
1119        let conn = new_mem_connection();
1120        let url = Url::parse("https://www.example.com")?;
1121
1122        conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1123            .expect("should work");
1124
1125        let global_change_tracker = conn.global_bookmark_change_tracker();
1126        assert!(!global_change_tracker.changed(), "can't start as changed!");
1127        let bm = InsertableItem::Bookmark {
1128            b: InsertableBookmark {
1129                parent_guid: BookmarkRootGuid::Unfiled.into(),
1130                position: BookmarkPosition::Append,
1131                date_added: None,
1132                last_modified: None,
1133                guid: None,
1134                url: url.clone(),
1135                title: Some("the title".into()),
1136            },
1137        };
1138        let guid = insert_bookmark(&conn, bm)?;
1139
1140        // re-fetch it.
1141        let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1142
1143        assert!(rb.place_id.is_some());
1144        assert_eq!(rb.bookmark_type, BookmarkType::Bookmark);
1145        assert_eq!(rb.parent_guid.unwrap(), BookmarkRootGuid::Unfiled);
1146        assert_eq!(rb.position, 0);
1147        assert_eq!(rb.title, Some("the title".into()));
1148        assert_eq!(rb.url, Some(url));
1149        assert_eq!(rb._sync_status, SyncStatus::New);
1150        assert_eq!(rb._sync_change_counter, 1);
1151        assert!(global_change_tracker.changed());
1152        assert_eq!(rb.child_count, 0);
1153
1154        let unfiled = get_raw_bookmark(&conn, &BookmarkRootGuid::Unfiled.as_guid())?
1155            .expect("should get unfiled");
1156        assert_eq!(unfiled._sync_change_counter, 1);
1157
1158        Ok(())
1159    }
1160
1161    #[test]
1162    fn test_insert_titles() -> Result<()> {
1163        let conn = new_mem_connection();
1164        let url = Url::parse("https://www.example.com")?;
1165
1166        let bm = InsertableItem::Bookmark {
1167            b: InsertableBookmark {
1168                parent_guid: BookmarkRootGuid::Unfiled.into(),
1169                position: BookmarkPosition::Append,
1170                date_added: None,
1171                last_modified: None,
1172                guid: None,
1173                url: url.clone(),
1174                title: Some("".into()),
1175            },
1176        };
1177        let guid = insert_bookmark(&conn, bm)?;
1178        let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1179        assert_eq!(rb.title, None);
1180
1181        let bm2 = InsertableItem::Bookmark {
1182            b: InsertableBookmark {
1183                parent_guid: BookmarkRootGuid::Unfiled.into(),
1184                position: BookmarkPosition::Append,
1185                date_added: None,
1186                last_modified: None,
1187                guid: None,
1188                url,
1189                title: None,
1190            },
1191        };
1192        let guid2 = insert_bookmark(&conn, bm2)?;
1193        let rb2 = get_raw_bookmark(&conn, &guid2)?.expect("should get the bookmark");
1194        assert_eq!(rb2.title, None);
1195        Ok(())
1196    }
1197
1198    #[test]
1199    fn test_delete() -> Result<()> {
1200        let conn = new_mem_connection();
1201
1202        let guid1 = SyncGuid::random();
1203        let guid2 = SyncGuid::random();
1204        let guid2_1 = SyncGuid::random();
1205        let guid3 = SyncGuid::random();
1206
1207        let jtree = json!({
1208            "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1209            "children": [
1210                {
1211                    "guid": &guid1,
1212                    "title": "the bookmark",
1213                    "url": "https://www.example.com/"
1214                },
1215                {
1216                    "guid": &guid2,
1217                    "title": "A folder",
1218                    "children": [
1219                        {
1220                            "guid": &guid2_1,
1221                            "title": "bookmark in A folder",
1222                            "url": "https://www.example2.com/"
1223                        }
1224                    ]
1225                },
1226                {
1227                    "guid": &guid3,
1228                    "title": "the last bookmark",
1229                    "url": "https://www.example3.com/"
1230                },
1231            ]
1232        });
1233
1234        insert_json_tree(&conn, jtree);
1235
1236        conn.execute(
1237            &format!(
1238                "UPDATE moz_bookmarks SET syncChangeCounter = 1, syncStatus = {}",
1239                SyncStatus::Normal as u8
1240            ),
1241            [],
1242        )
1243        .expect("should work");
1244
1245        // Make sure the positions are correct now.
1246        assert_eq!(get_pos(&conn, &guid1), 0);
1247        assert_eq!(get_pos(&conn, &guid2), 1);
1248        assert_eq!(get_pos(&conn, &guid3), 2);
1249
1250        let global_change_tracker = conn.global_bookmark_change_tracker();
1251
1252        // Delete the middle folder.
1253        delete_bookmark(&conn, &guid2)?;
1254        // Should no longer exist.
1255        assert!(get_raw_bookmark(&conn, &guid2)?.is_none());
1256        // Neither should the child.
1257        assert!(get_raw_bookmark(&conn, &guid2_1)?.is_none());
1258        // Positions of the remaining should be correct.
1259        assert_eq!(get_pos(&conn, &guid1), 0);
1260        assert_eq!(get_pos(&conn, &guid3), 1);
1261        assert!(global_change_tracker.changed());
1262
1263        assert_eq!(
1264            conn.conn_ext_query_one::<i64>(
1265                "SELECT COUNT(*) FROM moz_origins WHERE host='www.example2.com';"
1266            )?,
1267            0
1268        );
1269
1270        Ok(())
1271    }
1272
1273    #[test]
1274    fn test_delete_roots() {
1275        let conn = new_mem_connection();
1276
1277        delete_bookmark(&conn, &BookmarkRootGuid::Root.into()).expect_err("can't delete root");
1278        delete_bookmark(&conn, &BookmarkRootGuid::Unfiled.into())
1279            .expect_err("can't delete any root");
1280    }
1281
1282    #[test]
1283    fn test_insert_pos_too_large() -> Result<()> {
1284        let conn = new_mem_connection();
1285        let url = Url::parse("https://www.example.com")?;
1286
1287        let bm = InsertableItem::Bookmark {
1288            b: InsertableBookmark {
1289                parent_guid: BookmarkRootGuid::Unfiled.into(),
1290                position: BookmarkPosition::Specific { pos: 100 },
1291                date_added: None,
1292                last_modified: None,
1293                guid: None,
1294                url,
1295                title: Some("the title".into()),
1296            },
1297        };
1298        let guid = insert_bookmark(&conn, bm)?;
1299
1300        // re-fetch it.
1301        let rb = get_raw_bookmark(&conn, &guid)?.expect("should get the bookmark");
1302
1303        assert_eq!(rb.position, 0, "large value should have been ignored");
1304        Ok(())
1305    }
1306
1307    #[test]
1308    fn test_update_move_same_parent() {
1309        let conn = new_mem_connection();
1310        let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1311
1312        // A helper to make the moves below more concise.
1313        let do_move = |guid: &str, pos: BookmarkPosition| {
1314            let global_change_tracker = conn.global_bookmark_change_tracker();
1315            update_bookmark(
1316                &conn,
1317                &guid.into(),
1318                &UpdatableBookmark {
1319                    location: UpdateTreeLocation::Position { pos },
1320                    ..Default::default()
1321                }
1322                .into(),
1323            )
1324            .expect("update should work");
1325            assert!(global_change_tracker.changed(), "should be tracked");
1326        };
1327
1328        // A helper to make the checks below more concise.
1329        let check_tree = |children: Value| {
1330            assert_json_tree(
1331                &conn,
1332                unfiled,
1333                json!({
1334                    "guid": unfiled,
1335                    "children": children
1336                }),
1337            );
1338        };
1339
1340        insert_json_tree(
1341            &conn,
1342            json!({
1343                "guid": unfiled,
1344                "children": [
1345                    {
1346                        "guid": "bookmark1___",
1347                        "url": "https://www.example1.com/"
1348                    },
1349                    {
1350                        "guid": "bookmark2___",
1351                        "url": "https://www.example2.com/"
1352                    },
1353                    {
1354                        "guid": "bookmark3___",
1355                        "url": "https://www.example3.com/"
1356                    },
1357
1358                ]
1359            }),
1360        );
1361
1362        // Move a bookmark to the end.
1363        do_move("bookmark2___", BookmarkPosition::Append);
1364        check_tree(json!([
1365            {"url": "https://www.example1.com/"},
1366            {"url": "https://www.example3.com/"},
1367            {"url": "https://www.example2.com/"},
1368        ]));
1369
1370        // Move a bookmark to its existing position
1371        do_move("bookmark3___", BookmarkPosition::Specific { pos: 1 });
1372        check_tree(json!([
1373            {"url": "https://www.example1.com/"},
1374            {"url": "https://www.example3.com/"},
1375            {"url": "https://www.example2.com/"},
1376        ]));
1377
1378        // Move a bookmark back 1 position.
1379        do_move("bookmark2___", BookmarkPosition::Specific { pos: 1 });
1380        check_tree(json!([
1381            {"url": "https://www.example1.com/"},
1382            {"url": "https://www.example2.com/"},
1383            {"url": "https://www.example3.com/"},
1384        ]));
1385
1386        // Move a bookmark forward 1 position.
1387        do_move("bookmark2___", BookmarkPosition::Specific { pos: 2 });
1388        check_tree(json!([
1389            {"url": "https://www.example1.com/"},
1390            {"url": "https://www.example3.com/"},
1391            {"url": "https://www.example2.com/"},
1392        ]));
1393
1394        // Move a bookmark beyond the end.
1395        do_move("bookmark1___", BookmarkPosition::Specific { pos: 10 });
1396        check_tree(json!([
1397            {"url": "https://www.example3.com/"},
1398            {"url": "https://www.example2.com/"},
1399            {"url": "https://www.example1.com/"},
1400        ]));
1401    }
1402
1403    #[test]
1404    fn test_update() -> Result<()> {
1405        let conn = new_mem_connection();
1406        let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1407
1408        insert_json_tree(
1409            &conn,
1410            json!({
1411                "guid": unfiled,
1412                "children": [
1413                    {
1414                        "guid": "bookmark1___",
1415                        "title": "the bookmark",
1416                        "url": "https://www.example.com/"
1417                    },
1418                    {
1419                        "guid": "bookmark2___",
1420                        "title": "another bookmark",
1421                        "url": "https://www.example2.com/"
1422                    },
1423                    {
1424                        "guid": "folder1_____",
1425                        "title": "A folder",
1426                        "children": [
1427                            {
1428                                "guid": "bookmark3___",
1429                                "title": "bookmark in A folder",
1430                                "url": "https://www.example3.com/"
1431                            },
1432                            {
1433                                "guid": "bookmark4___",
1434                                "title": "next bookmark in A folder",
1435                                "url": "https://www.example4.com/"
1436                            },
1437                            {
1438                                "guid": "bookmark5___",
1439                                "title": "next next bookmark in A folder",
1440                                "url": "https://www.example5.com/"
1441                            }
1442                        ]
1443                    },
1444                    {
1445                        "guid": "bookmark6___",
1446                        "title": "yet another bookmark",
1447                        "url": "https://www.example6.com/"
1448                    },
1449
1450                ]
1451            }),
1452        );
1453
1454        update_bookmark(
1455            &conn,
1456            &"folder1_____".into(),
1457            &UpdatableFolder {
1458                title: Some("new name".to_string()),
1459                ..Default::default()
1460            }
1461            .into(),
1462        )?;
1463        update_bookmark(
1464            &conn,
1465            &"bookmark1___".into(),
1466            &UpdatableBookmark {
1467                url: Some(Url::parse("https://www.example3.com/")?),
1468                title: None,
1469                ..Default::default()
1470            }
1471            .into(),
1472        )?;
1473
1474        // A move in the same folder.
1475        update_bookmark(
1476            &conn,
1477            &"bookmark6___".into(),
1478            &UpdatableBookmark {
1479                location: UpdateTreeLocation::Position {
1480                    pos: BookmarkPosition::Specific { pos: 2 },
1481                },
1482                ..Default::default()
1483            }
1484            .into(),
1485        )?;
1486
1487        // A move across folders.
1488        update_bookmark(
1489            &conn,
1490            &"bookmark2___".into(),
1491            &UpdatableBookmark {
1492                location: UpdateTreeLocation::Parent {
1493                    guid: "folder1_____".into(),
1494                    pos: BookmarkPosition::Specific { pos: 1 },
1495                },
1496                ..Default::default()
1497            }
1498            .into(),
1499        )?;
1500
1501        assert_json_tree(
1502            &conn,
1503            unfiled,
1504            json!({
1505                "guid": unfiled,
1506                "children": [
1507                    {
1508                        // We updated the url and title of this.
1509                        "guid": "bookmark1___",
1510                        "title": null,
1511                        "url": "https://www.example3.com/"
1512                    },
1513                        // We moved bookmark6 to position=2 (ie, 3rd) of the same
1514                        // parent, but then moved the existing 2nd item to the
1515                        // folder, so this ends up second.
1516                    {
1517                        "guid": "bookmark6___",
1518                        "url": "https://www.example6.com/"
1519                    },
1520                    {
1521                        // We changed the name of the folder.
1522                        "guid": "folder1_____",
1523                        "title": "new name",
1524                        "children": [
1525                            {
1526                                "guid": "bookmark3___",
1527                                "url": "https://www.example3.com/"
1528                            },
1529                            {
1530                                // This was moved from the parent to position 1
1531                                "guid": "bookmark2___",
1532                                "url": "https://www.example2.com/"
1533                            },
1534                            {
1535                                "guid": "bookmark4___",
1536                                "url": "https://www.example4.com/"
1537                            },
1538                            {
1539                                "guid": "bookmark5___",
1540                                "url": "https://www.example5.com/"
1541                            }
1542                        ]
1543                    },
1544
1545                ]
1546            }),
1547        );
1548
1549        Ok(())
1550    }
1551
1552    #[test]
1553    fn test_update_titles() -> Result<()> {
1554        let conn = new_mem_connection();
1555        let guid: SyncGuid = "bookmark1___".into();
1556
1557        insert_json_tree(
1558            &conn,
1559            json!({
1560                "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1561                "children": [
1562                    {
1563                        "guid": "bookmark1___",
1564                        "title": "the bookmark",
1565                        "url": "https://www.example.com/"
1566                    },
1567                ],
1568            }),
1569        );
1570
1571        conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1572            .expect("should work");
1573
1574        // Update of None means no change.
1575        update_bookmark(
1576            &conn,
1577            &guid,
1578            &UpdatableBookmark {
1579                title: None,
1580                ..Default::default()
1581            }
1582            .into(),
1583        )?;
1584        let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1585        assert_eq!(bm.title, Some("the bookmark".to_string()));
1586        assert_eq!(bm._sync_change_counter, 0);
1587
1588        // Update to the same value is still not a change.
1589        update_bookmark(
1590            &conn,
1591            &guid,
1592            &UpdatableBookmark {
1593                title: Some("the bookmark".to_string()),
1594                ..Default::default()
1595            }
1596            .into(),
1597        )?;
1598        let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1599        assert_eq!(bm.title, Some("the bookmark".to_string()));
1600        assert_eq!(bm._sync_change_counter, 0);
1601
1602        // Update to an empty string sets it to null
1603        update_bookmark(
1604            &conn,
1605            &guid,
1606            &UpdatableBookmark {
1607                title: Some("".to_string()),
1608                ..Default::default()
1609            }
1610            .into(),
1611        )?;
1612        let bm = get_raw_bookmark(&conn, &guid)?.expect("should exist");
1613        assert_eq!(bm.title, None);
1614        assert_eq!(bm._sync_change_counter, 1);
1615
1616        Ok(())
1617    }
1618
1619    #[test]
1620    fn test_update_statuses() -> Result<()> {
1621        let conn = new_mem_connection();
1622        let unfiled = &BookmarkRootGuid::Unfiled.as_guid();
1623
1624        let check_change_counters = |guids: Vec<&str>| {
1625            let sql = "SELECT guid FROM moz_bookmarks WHERE syncChangeCounter != 0";
1626            let mut stmt = conn.prepare(sql).expect("sql is ok");
1627            let got_guids: HashSet<String> = stmt
1628                .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, String>(0) })
1629                .expect("should work")
1630                .map(std::result::Result::unwrap)
1631                .collect();
1632
1633            assert_eq!(
1634                got_guids,
1635                guids.into_iter().map(ToString::to_string).collect()
1636            );
1637            // reset them all back
1638            conn.execute("UPDATE moz_bookmarks SET syncChangeCounter = 0", [])
1639                .expect("should work");
1640        };
1641
1642        let check_last_modified = |guids: Vec<&str>| {
1643            let sql = "SELECT guid FROM moz_bookmarks
1644                       WHERE lastModified >= 1000 AND guid != 'root________'";
1645
1646            let mut stmt = conn.prepare(sql).expect("sql is ok");
1647            let got_guids: HashSet<String> = stmt
1648                .query_and_then([], |row| -> rusqlite::Result<_> { row.get::<_, String>(0) })
1649                .expect("should work")
1650                .map(std::result::Result::unwrap)
1651                .collect();
1652
1653            assert_eq!(
1654                got_guids,
1655                guids.into_iter().map(ToString::to_string).collect()
1656            );
1657            // reset them all back
1658            conn.execute("UPDATE moz_bookmarks SET lastModified = 123", [])
1659                .expect("should work");
1660        };
1661
1662        insert_json_tree(
1663            &conn,
1664            json!({
1665                "guid": unfiled,
1666                "children": [
1667                    {
1668                        "guid": "folder1_____",
1669                        "title": "A folder",
1670                        "children": [
1671                            {
1672                                "guid": "bookmark1___",
1673                                "title": "bookmark in A folder",
1674                                "url": "https://www.example2.com/"
1675                            },
1676                            {
1677                                "guid": "bookmark2___",
1678                                "title": "next bookmark in A folder",
1679                                "url": "https://www.example3.com/"
1680                            },
1681                        ]
1682                    },
1683                    {
1684                        "guid": "folder2_____",
1685                        "title": "folder 2",
1686                    },
1687                ]
1688            }),
1689        );
1690
1691        // reset all statuses and timestamps.
1692        conn.execute(
1693            "UPDATE moz_bookmarks SET syncChangeCounter = 0, lastModified = 123",
1694            [],
1695        )?;
1696
1697        // update a title - should get a change counter.
1698        update_bookmark(
1699            &conn,
1700            &"bookmark1___".into(),
1701            &UpdatableBookmark {
1702                title: Some("new name".to_string()),
1703                ..Default::default()
1704            }
1705            .into(),
1706        )?;
1707        check_change_counters(vec!["bookmark1___"]);
1708        // last modified should be all the way up the tree.
1709        check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1710
1711        // update the position in the same folder.
1712        update_bookmark(
1713            &conn,
1714            &"bookmark1___".into(),
1715            &UpdatableBookmark {
1716                location: UpdateTreeLocation::Position {
1717                    pos: BookmarkPosition::Append,
1718                },
1719                ..Default::default()
1720            }
1721            .into(),
1722        )?;
1723        // parent should be the only thing with a change counter.
1724        check_change_counters(vec!["folder1_____"]);
1725        // last modified should be all the way up the tree.
1726        check_last_modified(vec!["unfiled_____", "folder1_____", "bookmark1___"]);
1727
1728        // update the position to a different folder.
1729        update_bookmark(
1730            &conn,
1731            &"bookmark1___".into(),
1732            &UpdatableBookmark {
1733                location: UpdateTreeLocation::Parent {
1734                    guid: "folder2_____".into(),
1735                    pos: BookmarkPosition::Append,
1736                },
1737                ..Default::default()
1738            }
1739            .into(),
1740        )?;
1741        // Both parents should have a change counter.
1742        check_change_counters(vec!["folder1_____", "folder2_____"]);
1743        // last modified should be all the way up the tree and include both parents.
1744        check_last_modified(vec![
1745            "unfiled_____",
1746            "folder1_____",
1747            "folder2_____",
1748            "bookmark1___",
1749        ]);
1750
1751        Ok(())
1752    }
1753
1754    #[test]
1755    fn test_update_errors() {
1756        let conn = new_mem_connection();
1757
1758        insert_json_tree(
1759            &conn,
1760            json!({
1761                "guid": &BookmarkRootGuid::Unfiled.as_guid(),
1762                "children": [
1763                    {
1764                        "guid": "bookmark1___",
1765                        "title": "the bookmark",
1766                        "url": "https://www.example.com/"
1767                    },
1768                    {
1769                        "guid": "folder1_____",
1770                        "title": "A folder",
1771                        "children": [
1772                            {
1773                                "guid": "bookmark2___",
1774                                "title": "bookmark in A folder",
1775                                "url": "https://www.example2.com/"
1776                            },
1777                        ]
1778                    },
1779                ]
1780            }),
1781        );
1782        // Update an item that doesn't exist.
1783        update_bookmark(
1784            &conn,
1785            &"bookmark9___".into(),
1786            &UpdatableBookmark {
1787                ..Default::default()
1788            }
1789            .into(),
1790        )
1791        .expect_err("should fail to update an item that doesn't exist");
1792
1793        // A move across to a non-folder
1794        update_bookmark(
1795            &conn,
1796            &"bookmark1___".into(),
1797            &UpdatableBookmark {
1798                location: UpdateTreeLocation::Parent {
1799                    guid: "bookmark2___".into(),
1800                    pos: BookmarkPosition::Specific { pos: 1 },
1801                },
1802                ..Default::default()
1803            }
1804            .into(),
1805        )
1806        .expect_err("can't move to a bookmark");
1807
1808        // A move to the root
1809        update_bookmark(
1810            &conn,
1811            &"bookmark1___".into(),
1812            &UpdatableBookmark {
1813                location: UpdateTreeLocation::Parent {
1814                    guid: BookmarkRootGuid::Root.as_guid(),
1815                    pos: BookmarkPosition::Specific { pos: 1 },
1816                },
1817                ..Default::default()
1818            }
1819            .into(),
1820        )
1821        .expect_err("can't move to the root");
1822    }
1823
1824    #[test]
1825    fn test_delete_everything() -> Result<()> {
1826        let conn = new_mem_connection();
1827
1828        insert_bookmark(
1829            &conn,
1830            InsertableFolder {
1831                parent_guid: BookmarkRootGuid::Unfiled.into(),
1832                position: BookmarkPosition::Append,
1833                date_added: None,
1834                last_modified: None,
1835                guid: Some("folderAAAAAA".into()),
1836                title: Some("A".into()),
1837                children: vec![],
1838            }
1839            .into(),
1840        )?;
1841        insert_bookmark(
1842            &conn,
1843            InsertableBookmark {
1844                parent_guid: BookmarkRootGuid::Unfiled.into(),
1845                position: BookmarkPosition::Append,
1846                date_added: None,
1847                last_modified: None,
1848                guid: Some("bookmarkBBBB".into()),
1849                url: Url::parse("http://example.com/b")?,
1850                title: Some("B".into()),
1851            }
1852            .into(),
1853        )?;
1854        insert_bookmark(
1855            &conn,
1856            InsertableBookmark {
1857                parent_guid: "folderAAAAAA".into(),
1858                position: BookmarkPosition::Append,
1859                date_added: None,
1860                last_modified: None,
1861                guid: Some("bookmarkCCCC".into()),
1862                url: Url::parse("http://example.com/c")?,
1863                title: Some("C".into()),
1864            }
1865            .into(),
1866        )?;
1867
1868        delete_everything(&conn)?;
1869
1870        let (tree, _, _) =
1871            fetch_tree(&conn, &BookmarkRootGuid::Root.into(), &FetchDepth::Deepest)?.unwrap();
1872        if let BookmarkTreeNode::Folder { f: root } = tree {
1873            assert_eq!(root.children.len(), 4);
1874            let unfiled = root
1875                .children
1876                .iter()
1877                .find(|c| c.guid() == BookmarkRootGuid::Unfiled.guid())
1878                .expect("Should return unfiled root");
1879            if let BookmarkTreeNode::Folder { f: unfiled } = unfiled {
1880                assert!(unfiled.children.is_empty());
1881            } else {
1882                panic!("The unfiled root should be a folder");
1883            }
1884        } else {
1885            panic!("`fetch_tree` should return the Places root folder");
1886        }
1887
1888        Ok(())
1889    }
1890
1891    #[test]
1892    fn test_sync_reset() -> Result<()> {
1893        let conn = new_mem_connection();
1894
1895        // Add Sync metadata keys, to ensure they're reset.
1896        put_meta(&conn, GLOBAL_SYNCID_META_KEY, &"syncAAAAAAAA")?;
1897        put_meta(&conn, COLLECTION_SYNCID_META_KEY, &"syncBBBBBBBB")?;
1898        put_meta(&conn, LAST_SYNC_META_KEY, &12345)?;
1899
1900        insert_bookmark(
1901            &conn,
1902            InsertableBookmark {
1903                parent_guid: BookmarkRootGuid::Unfiled.into(),
1904                position: BookmarkPosition::Append,
1905                date_added: None,
1906                last_modified: None,
1907                guid: Some("bookmarkAAAA".into()),
1908                url: Url::parse("http://example.com/a")?,
1909                title: Some("A".into()),
1910            }
1911            .into(),
1912        )?;
1913
1914        // Mark all items as synced.
1915        conn.execute(
1916            &format!(
1917                "UPDATE moz_bookmarks SET
1918                     syncChangeCounter = 0,
1919                     syncStatus = {}",
1920                (SyncStatus::Normal as u8)
1921            ),
1922            [],
1923        )?;
1924
1925        let bmk = get_raw_bookmark(&conn, &"bookmarkAAAA".into())?
1926            .expect("Should fetch A before resetting");
1927        assert_eq!(bmk._sync_change_counter, 0);
1928        assert_eq!(bmk._sync_status, SyncStatus::Normal);
1929
1930        bookmark_sync::reset(&conn, &EngineSyncAssociation::Disconnected)?;
1931
1932        let bmk = get_raw_bookmark(&conn, &"bookmarkAAAA".into())?
1933            .expect("Should fetch A after resetting");
1934        assert_eq!(bmk._sync_change_counter, 1);
1935        assert_eq!(bmk._sync_status, SyncStatus::New);
1936
1937        // Ensure we reset Sync metadata, too.
1938        let global = get_meta::<SyncGuid>(&conn, GLOBAL_SYNCID_META_KEY)?;
1939        assert!(global.is_none());
1940        let coll = get_meta::<SyncGuid>(&conn, COLLECTION_SYNCID_META_KEY)?;
1941        assert!(coll.is_none());
1942        let since = get_meta::<i64>(&conn, LAST_SYNC_META_KEY)?;
1943        assert_eq!(since, Some(0));
1944
1945        Ok(())
1946    }
1947
1948    #[test]
1949    fn test_count_tree() -> Result<()> {
1950        let conn = new_mem_connection();
1951        let unfiled = BookmarkRootGuid::Unfiled.as_guid();
1952
1953        insert_json_tree(
1954            &conn,
1955            json!({
1956                "guid": &unfiled,
1957                "children": [
1958                    {
1959                        "guid": "folder1_____",
1960                        "title": "A folder",
1961                        "children": [
1962                            {
1963                                "guid": "bookmark1___",
1964                                "title": "bookmark in A folder",
1965                                "url": "https://www.example2.com/"
1966                            },
1967                            {
1968                                "guid": "separator1__",
1969                                "type": BookmarkType::Separator,
1970                            },
1971                            {
1972                                "guid": "bookmark2___",
1973                                "title": "next bookmark in A folder",
1974                                "url": "https://www.example3.com/"
1975                            },
1976                        ]
1977                    },
1978                    {
1979                        "guid": "folder2_____",
1980                        "title": "folder 2",
1981                    },
1982                    {
1983                        "guid": "folder3_____",
1984                        "title": "Another folder",
1985                        "children": [
1986                            {
1987                                "guid": "bookmark3___",
1988                                "title": "bookmark in folder 3",
1989                                "url": "https://www.example2.com/"
1990                            },
1991                            {
1992                                "guid": "separator2__",
1993                                "type": BookmarkType::Separator,
1994                            },
1995                            {
1996                                "guid": "bookmark4___",
1997                                "title": "next bookmark in folder 3",
1998                                "url": "https://www.example3.com/"
1999                            },
2000                        ]
2001                    },
2002                ]
2003            }),
2004        );
2005        assert_eq!(count_bookmarks_in_trees(&conn, &[])?, 0);
2006        // A folder with sub-folders
2007        assert_eq!(count_bookmarks_in_trees(&conn, &[unfiled])?, 4);
2008        // A folder with items but no folders.
2009        assert_eq!(
2010            count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder1_____")])?,
2011            2
2012        );
2013        // Asking for a bookmark (ie, not a folder) or an invalid guid gives zero.
2014        assert_eq!(
2015            count_bookmarks_in_trees(&conn, &[SyncGuid::from("bookmark1___")])?,
2016            0
2017        );
2018        assert_eq!(
2019            count_bookmarks_in_trees(&conn, &[SyncGuid::from("no_such_guid")])?,
2020            0
2021        );
2022        // empty folder also zero.
2023        assert_eq!(
2024            count_bookmarks_in_trees(&conn, &[SyncGuid::from("folder2_____")])?,
2025            0
2026        );
2027        // multiple folders
2028        assert_eq!(
2029            count_bookmarks_in_trees(
2030                &conn,
2031                &[
2032                    SyncGuid::from("folder1_____"),
2033                    SyncGuid::from("folder3_____")
2034                ]
2035            )?,
2036            4
2037        );
2038        Ok(())
2039    }
2040}