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