places/
ffi.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5// This module implement the traits that make the FFI code easier to manage.
6
7use crate::api::matcher::{self, search_frecent, SearchParams};
8pub use crate::api::places_api::places_api_new;
9pub use crate::error::{warn, Result};
10pub use crate::error::{ApiResult, PlacesApiError};
11#[cfg(all(feature = "glean-sym", target_os = "android"))]
12use crate::glean_metrics::places_manager;
13pub use crate::import::common::HistoryMigrationResult;
14use crate::import::import_ios_history;
15use crate::storage;
16use crate::storage::bookmarks;
17pub use crate::storage::bookmarks::BookmarkPosition;
18pub use crate::storage::history_metadata::{
19    DocumentType, HistoryHighlight, HistoryHighlightWeights, HistoryMetadata,
20    HistoryMetadataObservation, HistoryMetadataPageMissingBehavior,
21    NoteHistoryMetadataObservationOptions,
22};
23pub use crate::storage::RunMaintenanceMetrics;
24use crate::storage::{history, history_metadata};
25use crate::types::VisitTransitionSet;
26use crate::ConnectionType;
27use crate::VisitObservation;
28use crate::VisitType;
29use crate::{PlacesApi, PlacesDb};
30use error_support::handle_error;
31use interrupt_support::register_interrupt;
32pub use interrupt_support::SqlInterruptHandle;
33use parking_lot::Mutex;
34use std::sync::{Arc, Weak};
35pub use sync_guid::Guid;
36pub use types::Timestamp as PlacesTimestamp;
37pub use url::Url;
38
39// From https://searchfox.org/mozilla-central/rev/1674b86019a96f076e0f98f1d0f5f3ab9d4e9020/browser/components/newtab/lib/TopSitesFeed.jsm#87
40const SKIP_ONE_PAGE_FRECENCY_THRESHOLD: i64 = 101 + 1;
41
42// `bookmarks::InsertableItem` is clear for Rust code, but just `InsertableItem` is less
43// clear in the UDL - so change some of the type names.
44pub type InsertableBookmarkItem = crate::storage::bookmarks::InsertableItem;
45pub type InsertableBookmarkFolder = crate::storage::bookmarks::InsertableFolder;
46pub type InsertableBookmarkSeparator = crate::storage::bookmarks::InsertableSeparator;
47pub use crate::storage::bookmarks::InsertableBookmark;
48
49pub use crate::storage::bookmarks::BookmarkUpdateInfo;
50
51// And types used when fetching items.
52pub type BookmarkItem = crate::storage::bookmarks::fetch::Item;
53pub type BookmarkFolder = crate::storage::bookmarks::fetch::Folder;
54pub type BookmarkSeparator = crate::storage::bookmarks::fetch::Separator;
55pub use crate::storage::bookmarks::fetch::BookmarkData;
56
57uniffi::custom_type!(Url, String, {
58    remote,
59    try_lift: |val| {
60        match Url::parse(val.as_str()) {
61            Ok(url) => Ok(url),
62            Err(e) => Err(PlacesApiError::UrlParseFailed {
63                reason: e.to_string(),
64            }
65            .into()),
66        }
67    },
68    lower: |obj| obj.into(),
69});
70
71uniffi::custom_type!(PlacesTimestamp, i64, {
72    remote,
73    try_lift: |val| Ok(PlacesTimestamp(val as u64)),
74    lower: |obj| obj.as_millis() as i64,
75});
76
77uniffi::custom_type!(VisitTransitionSet, i32, {
78    try_lift: |val| {
79        Ok(VisitTransitionSet::from_u16(val as u16).expect("Bug: Invalid VisitTransitionSet"))
80    },
81    lower: |obj| VisitTransitionSet::into_u16(obj) as i32,
82});
83
84uniffi::custom_type!(Guid, String, {
85    remote,
86    try_lift: |val| Ok(Guid::new(val.as_str())),
87    lower: |obj| obj.into(),
88});
89
90// Check for multiple write connections open at the same time
91//
92// One potential cause of #5040 is that Fenix is somehow opening multiiple write connections to
93// the places DB.  This code tests if that's happening and reports an error if so.
94lazy_static::lazy_static! {
95    static ref READ_WRITE_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
96    static ref SYNC_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
97}
98
99impl PlacesApi {
100    #[handle_error(crate::Error)]
101    pub fn new_connection(&self, conn_type: ConnectionType) -> ApiResult<Arc<PlacesConnection>> {
102        let db = self.open_connection(conn_type)?;
103        let connection = Arc::new(PlacesConnection::new(db));
104        register_interrupt(Arc::<PlacesConnection>::downgrade(&connection));
105        Ok(connection)
106    }
107}
108
109pub struct PlacesConnection {
110    db: Mutex<PlacesDb>,
111    interrupt_handle: Arc<SqlInterruptHandle>,
112}
113
114impl PlacesConnection {
115    pub fn new(db: PlacesDb) -> Self {
116        #[cfg(all(feature = "glean-sym", target_os = "android"))]
117        places_manager::connection_initialized.add(1);
118
119        Self {
120            interrupt_handle: db.new_interrupt_handle(),
121            db: Mutex::new(db),
122        }
123    }
124
125    // A helper that gets the connection from the mutex and converts errors.
126    fn with_conn<F, T>(&self, f: F) -> Result<T>
127    where
128        F: FnOnce(&PlacesDb) -> crate::error::Result<T>,
129    {
130        let conn = self.db.lock();
131        f(&conn)
132    }
133
134    // pass the SqlInterruptHandle as an object through Uniffi
135    pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
136        Arc::clone(&self.interrupt_handle)
137    }
138
139    #[handle_error(crate::Error)]
140    pub fn get_latest_history_metadata_for_url(
141        &self,
142        url: Url,
143    ) -> ApiResult<Option<HistoryMetadata>> {
144        self.with_conn(|conn| history_metadata::get_latest_for_url(conn, &url))
145    }
146
147    #[handle_error(crate::Error)]
148    pub fn get_history_metadata_between(
149        &self,
150        start: PlacesTimestamp,
151        end: PlacesTimestamp,
152    ) -> ApiResult<Vec<HistoryMetadata>> {
153        self.with_conn(|conn| {
154            history_metadata::get_between(conn, start.as_millis_i64(), end.as_millis_i64())
155        })
156    }
157
158    #[handle_error(crate::Error)]
159    pub fn get_history_metadata_since(
160        &self,
161        start: PlacesTimestamp,
162    ) -> ApiResult<Vec<HistoryMetadata>> {
163        self.with_conn(|conn| history_metadata::get_since(conn, start.as_millis_i64()))
164    }
165
166    #[handle_error(crate::Error)]
167    pub fn get_most_recent_history_metadata(&self, limit: i32) -> ApiResult<Vec<HistoryMetadata>> {
168        self.with_conn(|conn| history_metadata::get_most_recent(conn, limit))
169    }
170
171    #[handle_error(crate::Error)]
172    pub fn get_most_recent_search_entries_in_history_metadata(
173        &self,
174        limit: i32,
175    ) -> ApiResult<Vec<HistoryMetadata>> {
176        self.with_conn(|conn| history_metadata::get_most_recent_search_entries(conn, limit))
177    }
178
179    #[handle_error(crate::Error)]
180    pub fn query_history_metadata(
181        &self,
182        query: String,
183        limit: i32,
184    ) -> ApiResult<Vec<HistoryMetadata>> {
185        self.with_conn(|conn| history_metadata::query(conn, query.as_str(), limit))
186    }
187
188    #[handle_error(crate::Error)]
189    pub fn get_history_highlights(
190        &self,
191        weights: HistoryHighlightWeights,
192        limit: i32,
193    ) -> ApiResult<Vec<HistoryHighlight>> {
194        self.with_conn(|conn| history_metadata::get_highlights(conn, weights, limit))
195    }
196
197    #[handle_error(crate::Error)]
198    pub fn note_history_metadata_observation(
199        &self,
200        data: HistoryMetadataObservation,
201        options: NoteHistoryMetadataObservationOptions,
202    ) -> ApiResult<()> {
203        // odd historical naming discrepancy - public function is "note_*", impl is "apply_*"
204        self.with_conn(|conn| history_metadata::apply_metadata_observation(conn, data, options))
205    }
206
207    #[handle_error(crate::Error)]
208    pub fn metadata_delete_older_than(&self, older_than: PlacesTimestamp) -> ApiResult<()> {
209        self.with_conn(|conn| history_metadata::delete_older_than(conn, older_than.as_millis_i64()))
210    }
211
212    #[handle_error(crate::Error)]
213    pub fn metadata_delete(
214        &self,
215        url: Url,
216        referrer_url: Option<Url>,
217        search_term: Option<String>,
218    ) -> ApiResult<()> {
219        self.with_conn(|conn| {
220            history_metadata::delete_metadata(
221                conn,
222                &url,
223                referrer_url.as_ref(),
224                search_term.as_deref(),
225            )
226        })
227    }
228
229    #[handle_error(crate::Error)]
230    pub fn metadata_delete_search_terms(&self) -> ApiResult<()> {
231        self.with_conn(history_metadata::delete_all_metadata_for_search)
232    }
233
234    /// Add an observation to the database.
235    #[handle_error(crate::Error)]
236    pub fn apply_observation(&self, visit: VisitObservation) -> ApiResult<()> {
237        self.with_conn(|conn| history::apply_observation(conn, visit))?;
238        Ok(())
239    }
240
241    #[handle_error(crate::Error)]
242    pub fn get_visited_urls_in_range(
243        &self,
244        start: PlacesTimestamp,
245        end: PlacesTimestamp,
246        include_remote: bool,
247    ) -> ApiResult<Vec<Url>> {
248        self.with_conn(|conn| {
249            let urls = history::get_visited_urls(conn, start, end, include_remote)?
250                .iter()
251                // Turn the list of strings into valid Urls
252                .filter_map(|s| Url::parse(s).ok())
253                .collect::<Vec<_>>();
254            Ok(urls)
255        })
256    }
257
258    #[handle_error(crate::Error)]
259    pub fn get_visit_infos(
260        &self,
261        start_date: PlacesTimestamp,
262        end_date: PlacesTimestamp,
263        exclude_types: VisitTransitionSet,
264    ) -> ApiResult<Vec<HistoryVisitInfo>> {
265        self.with_conn(|conn| history::get_visit_infos(conn, start_date, end_date, exclude_types))
266    }
267
268    #[handle_error(crate::Error)]
269    pub fn get_visit_count(&self, exclude_types: VisitTransitionSet) -> ApiResult<i64> {
270        self.with_conn(|conn| history::get_visit_count(conn, exclude_types))
271    }
272
273    #[handle_error(crate::Error)]
274    pub fn get_visit_count_for_host(
275        &self,
276        host: String,
277        before: PlacesTimestamp,
278        exclude_types: VisitTransitionSet,
279    ) -> ApiResult<i64> {
280        self.with_conn(|conn| {
281            history::get_visit_count_for_host(conn, host.as_str(), before, exclude_types)
282        })
283    }
284
285    #[handle_error(crate::Error)]
286    pub fn get_visit_page(
287        &self,
288        offset: i64,
289        count: i64,
290        exclude_types: VisitTransitionSet,
291    ) -> ApiResult<Vec<HistoryVisitInfo>> {
292        self.with_conn(|conn| history::get_visit_page(conn, offset, count, exclude_types))
293    }
294
295    #[handle_error(crate::Error)]
296    pub fn get_visit_page_with_bound(
297        &self,
298        bound: i64,
299        offset: i64,
300        count: i64,
301        exclude_types: VisitTransitionSet,
302    ) -> ApiResult<HistoryVisitInfosWithBound> {
303        self.with_conn(|conn| {
304            history::get_visit_page_with_bound(conn, bound, offset, count, exclude_types)
305        })
306    }
307
308    // This is identical to get_visited in history.rs but takes a list of strings instead of urls
309    // This is necessary b/c we still need to return 'false' for bad URLs which prevents us from
310    // parsing/filtering them before reaching the history layer
311    #[handle_error(crate::Error)]
312    pub fn get_visited(&self, urls: Vec<String>) -> ApiResult<Vec<bool>> {
313        let iter = urls.into_iter();
314        let mut result = vec![false; iter.len()];
315        let url_idxs = iter
316            .enumerate()
317            .filter_map(|(idx, s)| Url::parse(&s).ok().map(|url| (idx, url)))
318            .collect::<Vec<_>>();
319        self.with_conn(|conn| history::get_visited_into(conn, &url_idxs, &mut result))?;
320        Ok(result)
321    }
322
323    #[handle_error(crate::Error)]
324    pub fn delete_visits_for(&self, url: String) -> ApiResult<()> {
325        self.with_conn(|conn| {
326            let guid = match Url::parse(&url) {
327                Ok(url) => history::url_to_guid(conn, &url)?,
328                Err(e) => {
329                    warn!("Invalid URL passed to places_delete_visits_for, {}", e);
330                    history::href_to_guid(conn, url.clone().as_str())?
331                }
332            };
333            if let Some(guid) = guid {
334                history::delete_visits_for(conn, &guid)?;
335            }
336            Ok(())
337        })
338    }
339
340    #[handle_error(crate::Error)]
341    pub fn delete_visits_between(
342        &self,
343        start: PlacesTimestamp,
344        end: PlacesTimestamp,
345    ) -> ApiResult<()> {
346        self.with_conn(|conn| history::delete_visits_between(conn, start, end))
347    }
348
349    #[handle_error(crate::Error)]
350    pub fn delete_visit(&self, url: String, timestamp: PlacesTimestamp) -> ApiResult<()> {
351        self.with_conn(|conn| {
352            match Url::parse(&url) {
353                Ok(url) => {
354                    history::delete_place_visit_at_time(conn, &url, timestamp)?;
355                }
356                Err(e) => {
357                    warn!("Invalid URL passed to places_delete_visit, {}", e);
358                    history::delete_place_visit_at_time_by_href(conn, url.as_str(), timestamp)?;
359                }
360            };
361            Ok(())
362        })
363    }
364
365    #[handle_error(crate::Error)]
366    pub fn get_top_frecent_site_infos(
367        &self,
368        num_items: i32,
369        threshold_option: FrecencyThresholdOption,
370    ) -> ApiResult<Vec<TopFrecentSiteInfo>> {
371        self.with_conn(|conn| {
372            crate::storage::history::get_top_frecent_site_infos(
373                conn,
374                num_items,
375                threshold_option.value(),
376            )
377        })
378    }
379    // deletes all history and updates the sync metadata to only sync after
380    // most recent visit to prevent further syncing of older data
381    #[handle_error(crate::Error)]
382    pub fn delete_everything_history(&self) -> ApiResult<()> {
383        history::delete_everything(&self.db.lock())
384    }
385
386    #[handle_error(crate::Error)]
387    pub fn run_maintenance_prune(
388        &self,
389        db_size_limit: u32,
390        prune_limit: u32,
391    ) -> ApiResult<RunMaintenanceMetrics> {
392        #[cfg(all(feature = "glean-sym", target_os = "android"))]
393        let timer_id = places_manager::run_maintenance_prune_time_temp.start();
394        let res =
395            self.with_conn(|conn| storage::run_maintenance_prune(conn, db_size_limit, prune_limit));
396
397        #[cfg(all(feature = "glean-sym", target_os = "android"))]
398        places_manager::run_maintenance_prune_time_temp.stop_and_accumulate(timer_id);
399
400        res
401    }
402
403    #[handle_error(crate::Error)]
404    pub fn run_maintenance_vacuum(&self) -> ApiResult<()> {
405        #[cfg(all(feature = "glean-sym", target_os = "android"))]
406        let timer_id = places_manager::run_maintenance_vacuum_time_temp.start();
407        let res = self.with_conn(storage::run_maintenance_vacuum);
408
409        #[cfg(all(feature = "glean-sym", target_os = "android"))]
410        places_manager::run_maintenance_vacuum_time_temp.stop_and_accumulate(timer_id);
411
412        res
413    }
414
415    #[handle_error(crate::Error)]
416    pub fn run_maintenance_optimize(&self) -> ApiResult<()> {
417        #[cfg(all(feature = "glean-sym", target_os = "android"))]
418        let timer_id = places_manager::run_maintenance_optimize_time_temp.start();
419        let res = self.with_conn(storage::run_maintenance_optimize);
420
421        #[cfg(all(feature = "glean-sym", target_os = "android"))]
422        places_manager::run_maintenance_optimize_time_temp.stop_and_accumulate(timer_id);
423
424        res
425    }
426
427    #[handle_error(crate::Error)]
428    pub fn run_maintenance_checkpoint(&self) -> ApiResult<()> {
429        #[cfg(all(feature = "glean-sym", target_os = "android"))]
430        let timer_id = places_manager::run_maintenance_chk_pnt_time_temp.start();
431        let res = self.with_conn(storage::run_maintenance_checkpoint);
432
433        #[cfg(all(feature = "glean-sym", target_os = "android"))]
434        places_manager::run_maintenance_chk_pnt_time_temp.stop_and_accumulate(timer_id);
435
436        res
437    }
438
439    #[handle_error(crate::Error)]
440    pub fn query_autocomplete(&self, search: String, limit: i32) -> ApiResult<Vec<SearchResult>> {
441        self.with_conn(|conn| {
442            search_frecent(
443                conn,
444                SearchParams {
445                    search_string: search,
446                    limit: limit as u32,
447                },
448            )
449            .map(|search_results| search_results.into_iter().map(Into::into).collect())
450        })
451    }
452
453    #[handle_error(crate::Error)]
454    pub fn accept_result(&self, search_string: String, url: String) -> ApiResult<()> {
455        self.with_conn(|conn| {
456            match Url::parse(&url) {
457                Ok(url) => {
458                    matcher::accept_result(conn, &search_string, &url)?;
459                }
460                Err(_) => {
461                    warn!("Ignoring invalid URL in places_accept_result");
462                    return Ok(());
463                }
464            };
465            Ok(())
466        })
467    }
468
469    #[handle_error(crate::Error)]
470    pub fn match_url(&self, query: String) -> ApiResult<Option<Url>> {
471        self.with_conn(|conn| matcher::match_url(conn, query))
472    }
473
474    #[handle_error(crate::Error)]
475    pub fn bookmarks_get_tree(&self, item_guid: &Guid) -> ApiResult<Option<BookmarkItem>> {
476        self.with_conn(|conn| bookmarks::fetch::fetch_tree(conn, item_guid))
477    }
478
479    #[handle_error(crate::Error)]
480    pub fn bookmarks_get_by_guid(
481        &self,
482        guid: &Guid,
483        get_direct_children: bool,
484    ) -> ApiResult<Option<BookmarkItem>> {
485        self.with_conn(|conn| {
486            let bookmark = bookmarks::fetch::fetch_bookmark(conn, guid, get_direct_children)?;
487            Ok(bookmark)
488        })
489    }
490
491    #[handle_error(crate::Error)]
492    pub fn bookmarks_get_all_with_url(&self, url: String) -> ApiResult<Vec<BookmarkItem>> {
493        self.with_conn(|conn| {
494            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
495            match Url::parse(&url) {
496                Ok(url) => Ok(bookmarks::fetch::fetch_bookmarks_by_url(conn, &url)?
497                    .into_iter()
498                    .map(|b| BookmarkItem::Bookmark { b })
499                    .collect::<Vec<BookmarkItem>>()),
500                Err(e) => {
501                    // There are no bookmarks with the URL if it's invalid.
502                    warn!("Invalid URL passed to bookmarks_get_all_with_url, {}", e);
503                    Ok(Vec::<BookmarkItem>::new())
504                }
505            }
506        })
507    }
508
509    #[handle_error(crate::Error)]
510    pub fn bookmarks_search(&self, query: String, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
511        self.with_conn(|conn| {
512            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
513            Ok(
514                bookmarks::fetch::search_bookmarks(conn, query.as_str(), limit as u32)?
515                    .into_iter()
516                    .map(|b| BookmarkItem::Bookmark { b })
517                    .collect(),
518            )
519        })
520    }
521
522    #[handle_error(crate::Error)]
523    pub fn bookmarks_get_recent(&self, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
524        self.with_conn(|conn| {
525            // XXX - We should return the exact type - ie, BookmarkData rather than BookmarkItem.
526            Ok(bookmarks::fetch::recent_bookmarks(conn, limit as u32)?
527                .into_iter()
528                .map(|b| BookmarkItem::Bookmark { b })
529                .collect())
530        })
531    }
532
533    #[handle_error(crate::Error)]
534    pub fn bookmarks_delete(&self, id: Guid) -> ApiResult<bool> {
535        self.with_conn(|conn| bookmarks::delete_bookmark(conn, &id))
536    }
537
538    #[handle_error(crate::Error)]
539    pub fn bookmarks_delete_everything(&self) -> ApiResult<()> {
540        self.with_conn(bookmarks::delete_everything)
541    }
542
543    #[handle_error(crate::Error)]
544    pub fn bookmarks_get_url_for_keyword(&self, keyword: String) -> ApiResult<Option<Url>> {
545        self.with_conn(|conn| bookmarks::bookmarks_get_url_for_keyword(conn, keyword.as_str()))
546    }
547
548    #[handle_error(crate::Error)]
549    pub fn bookmarks_insert(&self, data: InsertableBookmarkItem) -> ApiResult<Guid> {
550        self.with_conn(|conn| bookmarks::insert_bookmark(conn, data))
551    }
552
553    #[handle_error(crate::Error)]
554    pub fn bookmarks_update(&self, item: BookmarkUpdateInfo) -> ApiResult<()> {
555        self.with_conn(|conn| bookmarks::update_bookmark_from_info(conn, item))
556    }
557
558    #[handle_error(crate::Error)]
559    pub fn bookmarks_count_bookmarks_in_trees(&self, guids: &[Guid]) -> ApiResult<u32> {
560        self.with_conn(|conn| bookmarks::count_bookmarks_in_trees(conn, guids))
561    }
562
563    #[handle_error(crate::Error)]
564    pub fn places_history_import_from_ios(
565        &self,
566        db_path: String,
567        last_sync_timestamp: i64,
568    ) -> ApiResult<HistoryMigrationResult> {
569        self.with_conn(|conn| import_ios_history(conn, &db_path, last_sync_timestamp))
570    }
571}
572
573impl AsRef<SqlInterruptHandle> for PlacesConnection {
574    fn as_ref(&self) -> &SqlInterruptHandle {
575        &self.interrupt_handle
576    }
577}
578
579#[derive(Clone, PartialEq, Eq)]
580pub struct HistoryVisitInfo {
581    pub url: Url,
582    pub title: Option<String>,
583    pub timestamp: PlacesTimestamp,
584    pub visit_type: VisitType,
585    pub is_hidden: bool,
586    pub preview_image_url: Option<Url>,
587    pub is_remote: bool,
588}
589#[derive(Clone, PartialEq, Eq)]
590pub struct HistoryVisitInfosWithBound {
591    pub infos: Vec<HistoryVisitInfo>,
592    pub bound: i64,
593    pub offset: i64,
594}
595
596pub struct TopFrecentSiteInfo {
597    pub url: Url,
598    pub title: Option<String>,
599}
600
601pub enum FrecencyThresholdOption {
602    None,
603    SkipOneTimePages,
604}
605
606impl FrecencyThresholdOption {
607    fn value(&self) -> i64 {
608        match self {
609            FrecencyThresholdOption::None => 0,
610            FrecencyThresholdOption::SkipOneTimePages => SKIP_ONE_PAGE_FRECENCY_THRESHOLD,
611        }
612    }
613}
614
615pub struct SearchResult {
616    pub url: Url,
617    pub title: String,
618    pub frecency: i64,
619}
620
621// Exists just to convince uniffi to generate `liftSequence*` helpers!
622pub struct Dummy {
623    pub md: Option<Vec<HistoryMetadata>>,
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use crate::test::new_mem_connection;
630
631    #[test]
632    fn test_accept_result_with_invalid_url() {
633        let conn = PlacesConnection::new(new_mem_connection());
634        let invalid_url = "http://1234.56.78.90".to_string();
635        assert!(PlacesConnection::accept_result(&conn, "ample".to_string(), invalid_url).is_ok());
636    }
637
638    #[test]
639    fn test_bookmarks_get_all_with_url_with_invalid_url() {
640        let conn = PlacesConnection::new(new_mem_connection());
641        let invalid_url = "http://1234.56.78.90".to_string();
642        assert!(PlacesConnection::bookmarks_get_all_with_url(&conn, invalid_url).is_ok());
643    }
644}