places/bookmark_sync/
record.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 std::fmt;
6
7use crate::storage::bookmarks::BookmarkRootGuid;
8use crate::types::UnknownFields;
9use serde::{
10    de::{Deserialize, Deserializer, Visitor},
11    ser::{Serialize, Serializer},
12};
13use serde_derive::*;
14use sync_guid::Guid as SyncGuid;
15
16/// A bookmark record ID. Bookmark record IDs are the same as Places GUIDs,
17/// except for:
18///
19/// 1. The Places root, which is "places". Note that the Places root is not
20///    synced, but is still referenced in the user content roots' `parentid`s.
21/// 2. The four user content roots, which omit trailing underscores.
22///
23/// This wrapper helps avoid mix-ups like storing a record ID instead of a GUID,
24/// or uploading a GUID instead of a record ID.
25///
26/// Internally, we convert record IDs to GUIDs when applying incoming records,
27/// and only convert back to GUIDs during upload.
28#[derive(Clone, Debug, Hash, PartialEq, Eq)]
29pub struct BookmarkRecordId(SyncGuid);
30
31impl BookmarkRecordId {
32    /// Creates a bookmark record ID from a Sync record payload ID.
33    pub fn from_payload_id(payload_id: SyncGuid) -> BookmarkRecordId {
34        BookmarkRecordId(match payload_id.as_str() {
35            "places" => BookmarkRootGuid::Root.as_guid(),
36            "menu" => BookmarkRootGuid::Menu.as_guid(),
37            "toolbar" => BookmarkRootGuid::Toolbar.as_guid(),
38            "unfiled" => BookmarkRootGuid::Unfiled.as_guid(),
39            "mobile" => BookmarkRootGuid::Mobile.as_guid(),
40            _ => payload_id,
41        })
42    }
43
44    /// Returns a reference to the record payload ID. This is the borrowed
45    /// version of `into_payload_id`, and used for serialization.
46    #[inline]
47    pub fn as_payload_id(&self) -> &str {
48        self.root_payload_id().unwrap_or_else(|| self.0.as_ref())
49    }
50
51    /// Returns the record payload ID. This is the owned version of
52    /// `as_payload_id`, and exists to avoid copying strings when uploading
53    /// tombstones.
54    #[inline]
55    pub fn into_payload_id(self) -> SyncGuid {
56        self.root_payload_id().map(Into::into).unwrap_or(self.0)
57    }
58
59    /// Returns a reference to the GUID for this record ID.
60    #[inline]
61    pub fn as_guid(&self) -> &SyncGuid {
62        &self.0
63    }
64
65    fn root_payload_id(&self) -> Option<&str> {
66        Some(match BookmarkRootGuid::from_guid(self.as_guid()) {
67            Some(BookmarkRootGuid::Root) => "places",
68            Some(BookmarkRootGuid::Menu) => "menu",
69            Some(BookmarkRootGuid::Toolbar) => "toolbar",
70            Some(BookmarkRootGuid::Unfiled) => "unfiled",
71            Some(BookmarkRootGuid::Mobile) => "mobile",
72            None => return None,
73        })
74    }
75}
76
77/// Converts a Places GUID into a bookmark record ID.
78impl From<SyncGuid> for BookmarkRecordId {
79    #[inline]
80    fn from(guid: SyncGuid) -> BookmarkRecordId {
81        BookmarkRecordId(guid)
82    }
83}
84
85/// Converts a bookmark record ID into a Places GUID.
86impl From<BookmarkRecordId> for SyncGuid {
87    #[inline]
88    fn from(record_id: BookmarkRecordId) -> SyncGuid {
89        record_id.0
90    }
91}
92
93impl Serialize for BookmarkRecordId {
94    #[inline]
95    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
96        serializer.serialize_str(self.as_payload_id())
97    }
98}
99
100impl<'de> Deserialize<'de> for BookmarkRecordId {
101    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
102        struct V;
103
104        impl Visitor<'_> for V {
105            type Value = BookmarkRecordId;
106
107            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108                f.write_str("a bookmark record ID")
109            }
110
111            #[inline]
112            fn visit_string<E: serde::de::Error>(
113                self,
114                payload_id: String,
115            ) -> std::result::Result<BookmarkRecordId, E> {
116                // The JSON deserializer passes owned strings, so we can avoid
117                // cloning the payload ID in the common case...
118                Ok(BookmarkRecordId::from_payload_id(payload_id.into()))
119            }
120
121            #[inline]
122            fn visit_str<E: serde::de::Error>(
123                self,
124                payload_id: &str,
125            ) -> std::result::Result<BookmarkRecordId, E> {
126                // ...However, the Serde docs say we must implement
127                // `visit_str` if we implement `visit_string`, so we also
128                // provide an implementation that clones the ID.
129                Ok(BookmarkRecordId::from_payload_id(payload_id.into()))
130            }
131        }
132
133        deserializer.deserialize_string(V)
134    }
135}
136
137#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct BookmarkRecord {
140    // Note that `SyncGuid` does not check for validity, which is what we
141    // want. If the bookmark has an invalid GUID, we'll make a new one.
142    #[serde(rename = "id")]
143    pub record_id: BookmarkRecordId,
144
145    #[serde(rename = "parentid")]
146    pub parent_record_id: Option<BookmarkRecordId>,
147
148    #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
149    pub parent_title: Option<String>,
150
151    #[serde(skip_serializing_if = "Option::is_none")]
152    #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
153    pub date_added: Option<i64>,
154
155    #[serde(default)]
156    pub has_dupe: bool,
157
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub title: Option<String>,
160
161    #[serde(rename = "bmkUri", skip_serializing_if = "Option::is_none")]
162    pub url: Option<String>,
163
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub keyword: Option<String>,
166
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    pub tags: Vec<String>,
169
170    #[serde(flatten)]
171    pub unknown_fields: UnknownFields,
172}
173
174impl From<BookmarkRecord> for BookmarkItemRecord {
175    #[inline]
176    fn from(b: BookmarkRecord) -> BookmarkItemRecord {
177        BookmarkItemRecord::Bookmark(b)
178    }
179}
180
181#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
182#[serde(rename_all = "camelCase")]
183pub struct QueryRecord {
184    #[serde(rename = "id")]
185    pub record_id: BookmarkRecordId,
186
187    #[serde(rename = "parentid")]
188    pub parent_record_id: Option<BookmarkRecordId>,
189
190    #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
191    pub parent_title: Option<String>,
192
193    #[serde(skip_serializing_if = "Option::is_none")]
194    #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
195    pub date_added: Option<i64>,
196
197    #[serde(default)]
198    pub has_dupe: bool,
199
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub title: Option<String>,
202
203    #[serde(rename = "bmkUri", skip_serializing_if = "Option::is_none")]
204    pub url: Option<String>,
205
206    #[serde(rename = "folderName", skip_serializing_if = "Option::is_none")]
207    pub tag_folder_name: Option<String>,
208
209    #[serde(flatten)]
210    pub unknown_fields: UnknownFields,
211}
212
213impl From<QueryRecord> for BookmarkItemRecord {
214    #[inline]
215    fn from(q: QueryRecord) -> BookmarkItemRecord {
216        BookmarkItemRecord::Query(q)
217    }
218}
219
220#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
221#[serde(rename_all = "camelCase")]
222pub struct FolderRecord {
223    #[serde(rename = "id")]
224    pub record_id: BookmarkRecordId,
225
226    #[serde(rename = "parentid")]
227    pub parent_record_id: Option<BookmarkRecordId>,
228
229    #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
230    pub parent_title: Option<String>,
231
232    #[serde(skip_serializing_if = "Option::is_none")]
233    #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
234    pub date_added: Option<i64>,
235
236    #[serde(default)]
237    pub has_dupe: bool,
238
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub title: Option<String>,
241
242    #[serde(default)]
243    pub children: Vec<BookmarkRecordId>,
244
245    #[serde(flatten)]
246    pub unknown_fields: UnknownFields,
247}
248
249impl From<FolderRecord> for BookmarkItemRecord {
250    #[inline]
251    fn from(f: FolderRecord) -> BookmarkItemRecord {
252        BookmarkItemRecord::Folder(f)
253    }
254}
255
256#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
257#[serde(rename_all = "camelCase")]
258pub struct LivemarkRecord {
259    #[serde(rename = "id")]
260    pub record_id: BookmarkRecordId,
261
262    #[serde(rename = "parentid")]
263    pub parent_record_id: Option<BookmarkRecordId>,
264
265    #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
266    pub parent_title: Option<String>,
267
268    #[serde(skip_serializing_if = "Option::is_none")]
269    #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
270    pub date_added: Option<i64>,
271
272    #[serde(default)]
273    pub has_dupe: bool,
274
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub title: Option<String>,
277
278    #[serde(rename = "feedUri", skip_serializing_if = "Option::is_none")]
279    pub feed_url: Option<String>,
280
281    #[serde(rename = "siteUri", skip_serializing_if = "Option::is_none")]
282    pub site_url: Option<String>,
283
284    #[serde(flatten)]
285    pub unknown_fields: UnknownFields,
286}
287
288impl From<LivemarkRecord> for BookmarkItemRecord {
289    #[inline]
290    fn from(l: LivemarkRecord) -> BookmarkItemRecord {
291        BookmarkItemRecord::Livemark(l)
292    }
293}
294
295#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct SeparatorRecord {
298    #[serde(rename = "id")]
299    pub record_id: BookmarkRecordId,
300
301    #[serde(rename = "parentid")]
302    pub parent_record_id: Option<BookmarkRecordId>,
303
304    #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
305    pub parent_title: Option<String>,
306
307    #[serde(skip_serializing_if = "Option::is_none")]
308    #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
309    pub date_added: Option<i64>,
310
311    #[serde(default)]
312    pub has_dupe: bool,
313
314    // Not used on newer clients, but can be used to detect parent-child
315    // position disagreements. Older clients use this for deduping.
316    #[serde(rename = "pos", skip_serializing_if = "Option::is_none")]
317    pub position: Option<i64>,
318
319    #[serde(flatten)]
320    pub unknown_fields: UnknownFields,
321}
322
323impl From<SeparatorRecord> for BookmarkItemRecord {
324    #[inline]
325    fn from(s: SeparatorRecord) -> BookmarkItemRecord {
326        BookmarkItemRecord::Separator(s)
327    }
328}
329
330#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
331#[serde(tag = "type", rename_all = "camelCase")]
332pub enum BookmarkItemRecord {
333    Bookmark(BookmarkRecord),
334    Query(QueryRecord),
335    Folder(FolderRecord),
336    Livemark(LivemarkRecord),
337    Separator(SeparatorRecord),
338}
339
340impl BookmarkItemRecord {
341    pub fn record_id(&self) -> &BookmarkRecordId {
342        match self {
343            Self::Bookmark(b) => &b.record_id,
344            Self::Query(q) => &q.record_id,
345            Self::Folder(f) => &f.record_id,
346            Self::Livemark(l) => &l.record_id,
347            Self::Separator(s) => &s.record_id,
348        }
349    }
350
351    pub fn unknown_fields(&self) -> &UnknownFields {
352        match self {
353            Self::Bookmark(b) => &b.unknown_fields,
354            Self::Folder(f) => &f.unknown_fields,
355            Self::Separator(s) => &s.unknown_fields,
356            Self::Query(q) => &q.unknown_fields,
357            Self::Livemark(l) => &l.unknown_fields,
358        }
359    }
360}
361
362// dateAdded on a bookmark might be a string! See #1148.
363fn de_maybe_stringified_timestamp<'de, D>(
364    deserializer: D,
365) -> std::result::Result<Option<i64>, D::Error>
366where
367    D: serde::de::Deserializer<'de>,
368{
369    use std::marker::PhantomData;
370
371    struct StringOrInt(PhantomData<Option<i64>>);
372
373    impl Visitor<'_> for StringOrInt {
374        type Value = Option<i64>;
375
376        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
377            formatter.write_str("string or int")
378        }
379
380        fn visit_str<E>(self, value: &str) -> Result<Option<i64>, E>
381        where
382            E: serde::de::Error,
383        {
384            match value.parse::<i64>() {
385                Ok(v) => Ok(Some(v)),
386                Err(_) => Err(E::custom("invalid string literal")),
387            }
388        }
389
390        // all positive int literals
391        fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Option<i64>, E> {
392            Ok(Some(value.max(0)))
393        }
394
395        // all negative int literals
396        fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Option<i64>, E> {
397            Ok(Some((value as i64).max(0)))
398        }
399    }
400    deserializer.deserialize_any(StringOrInt(PhantomData))
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use serde_json::{json, Error};
407
408    #[test]
409    fn test_invalid_record_type() {
410        let r: std::result::Result<BookmarkItemRecord, Error> =
411            serde_json::from_value(json!({"id": "whatever", "type" : "unknown-type"}));
412        let e = r.unwrap_err();
413        assert!(e.is_data());
414        // I guess is good enough to check we are hitting what we expect.
415        assert!(e.to_string().contains("unknown-type"));
416    }
417
418    #[test]
419    fn test_id_rewriting() {
420        let j = json!({"id": "unfiled", "parentid": "menu", "type": "bookmark"});
421        let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
422        match &r {
423            BookmarkItemRecord::Bookmark(b) => {
424                assert_eq!(b.record_id.as_guid(), BookmarkRootGuid::Unfiled);
425                assert_eq!(
426                    b.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
427                    Some(&BookmarkRootGuid::Menu.as_guid())
428                );
429            }
430            _ => panic!("unexpected record type"),
431        };
432        let v = serde_json::to_value(r).expect("should serialize");
433        assert_eq!(
434            v,
435            json!({
436                "id": "unfiled",
437                "parentid": "menu",
438                "type": "bookmark",
439                "hasDupe": false,
440            })
441        );
442
443        let j = json!({"id": "unfiled", "parentid": "menu", "type": "query"});
444        let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
445        match &r {
446            BookmarkItemRecord::Query(q) => {
447                assert_eq!(q.record_id.as_guid(), BookmarkRootGuid::Unfiled);
448                assert_eq!(
449                    q.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
450                    Some(&BookmarkRootGuid::Menu.as_guid())
451                );
452            }
453            _ => panic!("unexpected record type"),
454        };
455        let v = serde_json::to_value(r).expect("should serialize");
456        assert_eq!(
457            v,
458            json!({
459                "id": "unfiled",
460                "parentid": "menu",
461                "type": "query",
462                "hasDupe": false,
463            })
464        );
465
466        let j = json!({"id": "unfiled", "parentid": "menu", "type": "folder"});
467        let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
468        match &r {
469            BookmarkItemRecord::Folder(f) => {
470                assert_eq!(f.record_id.as_guid(), BookmarkRootGuid::Unfiled);
471                assert_eq!(
472                    f.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
473                    Some(&BookmarkRootGuid::Menu.as_guid())
474                );
475            }
476            _ => panic!("unexpected record type"),
477        };
478        let v = serde_json::to_value(r).expect("should serialize");
479        assert_eq!(
480            v,
481            json!({
482                "id": "unfiled",
483                "parentid": "menu",
484                "type": "folder",
485                "hasDupe": false,
486                "children": [],
487            })
488        );
489
490        let j = json!({"id": "unfiled", "parentid": "menu", "type": "livemark"});
491        let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
492        match &r {
493            BookmarkItemRecord::Livemark(l) => {
494                assert_eq!(l.record_id.as_guid(), BookmarkRootGuid::Unfiled);
495                assert_eq!(
496                    l.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
497                    Some(&BookmarkRootGuid::Menu.as_guid())
498                );
499            }
500            _ => panic!("unexpected record type"),
501        };
502        let v = serde_json::to_value(r).expect("should serialize");
503        assert_eq!(
504            v,
505            json!({
506                "id": "unfiled",
507                "parentid": "menu",
508                "type": "livemark",
509                "hasDupe": false,
510            })
511        );
512
513        let j = json!({"id": "unfiled", "parentid": "menu", "type": "separator"});
514        let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
515        match &r {
516            BookmarkItemRecord::Separator(s) => {
517                assert_eq!(s.record_id.as_guid(), BookmarkRootGuid::Unfiled);
518                assert_eq!(
519                    s.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
520                    Some(&BookmarkRootGuid::Menu.as_guid())
521                );
522            }
523            _ => panic!("unexpected record type"),
524        };
525        let v = serde_json::to_value(r).expect("should serialize");
526        assert_eq!(
527            v,
528            json!({
529                "id": "unfiled",
530                "parentid": "menu",
531                "type": "separator",
532                "hasDupe": false,
533            })
534        );
535    }
536
537    // It's unfortunate that all below 'dateadded' tests only check the
538    // 'BookmarkItemRecord' variant, so it would be a problem if `date_added` on
539    // other variants forgot to do the `deserialize_with` dance. We could
540    // implement a new type to make that less likely, but that's not foolproof
541    // either and causes this hysterical raisin to leak out from this module.
542    fn check_date_added(j: serde_json::Value, expected: Option<i64>) {
543        let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
544        match &r {
545            BookmarkItemRecord::Bookmark(b) => assert_eq!(b.date_added, expected),
546            _ => panic!("unexpected record type"),
547        };
548    }
549
550    #[test]
551    fn test_dateadded_missing() {
552        check_date_added(
553            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark"}),
554            None,
555        )
556    }
557
558    #[test]
559    fn test_dateadded_int() {
560        check_date_added(
561            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": 123}),
562            Some(123),
563        )
564    }
565
566    #[test]
567    fn test_dateadded_negative() {
568        check_date_added(
569            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": -1}),
570            Some(0),
571        )
572    }
573
574    #[test]
575    fn test_dateadded_str() {
576        check_date_added(
577            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": "123"}),
578            Some(123),
579        )
580    }
581
582    // A kinda "policy" decision - like serde, 'type errors' fail rather than default.
583    #[test]
584    fn test_dateadded_null() {
585        // a literal `null` is insane (and note we already test it *missing* above)
586        serde_json::from_value::<BookmarkItemRecord>(
587            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": null}),
588        )
589        .expect_err("should fail, literal null");
590    }
591
592    #[test]
593    fn test_dateadded_invalid_str() {
594        serde_json::from_value::<BookmarkItemRecord>(
595            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": "foo"}),
596        )
597        .expect_err("should fail, bad string value");
598    }
599
600    #[test]
601    fn test_dateadded_invalid_type() {
602        serde_json::from_value::<BookmarkItemRecord>(
603            json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": []}),
604        )
605        .expect_err("should fail, invalid type");
606    }
607}