places/
types.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 rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
6use rusqlite::Result as RusqliteResult;
7use serde::ser::{Serialize, Serializer};
8use std::fmt;
9
10mod visit_transition_set;
11pub use visit_transition_set::VisitTransitionSet;
12
13#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14#[error("Invalid visit type")]
15pub struct InvalidVisitType;
16
17// NOTE: These discriminator values are the same as those used by Desktop
18// Firefox and are what is written to the database. We also duplicate them
19// as a set of flags in visit_transition_set.rs
20#[repr(u8)]
21#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
22pub enum VisitType {
23    // This transition type means the user followed a link.
24    Link = 1,
25
26    // The user typed the page's URL in the
27    // URL bar or selected it from UI (URL bar autocomplete results, etc)
28    Typed = 2,
29
30    // The user followed a bookmark to get to the page.
31    Bookmark = 3,
32    /*
33     * This transition type is set when some inner content is loaded. This is
34     * true of all images on a page, and the contents of the iframe. It is also
35     * true of any content in a frame if the user did not explicitly follow
36     * a link to get there.
37     */
38    Embed = 4,
39
40    // Transition was a permanent redirect.
41    RedirectPermanent = 5,
42
43    // Transition was a temporary redirect.
44    RedirectTemporary = 6,
45
46    // Transition is a download.
47    Download = 7,
48
49    // The user followed a link and got a visit in a frame.
50    FramedLink = 8,
51
52    // The page has been reloaded.
53    Reload = 9,
54
55    // Internal visit type used for meta data updates. Doesn't represent an actual page visit
56    UpdatePlace = 10,
57}
58
59impl ToSql for VisitType {
60    fn to_sql(&self) -> RusqliteResult<ToSqlOutput<'_>> {
61        Ok(ToSqlOutput::from(*self as u8))
62    }
63}
64
65impl VisitType {
66    pub fn from_primitive(p: u8) -> Option<Self> {
67        match p {
68            1 => Some(VisitType::Link),
69            2 => Some(VisitType::Typed),
70            3 => Some(VisitType::Bookmark),
71            4 => Some(VisitType::Embed),
72            5 => Some(VisitType::RedirectPermanent),
73            6 => Some(VisitType::RedirectTemporary),
74            7 => Some(VisitType::Download),
75            8 => Some(VisitType::FramedLink),
76            9 => Some(VisitType::Reload),
77            10 => Some(VisitType::UpdatePlace),
78            _ => None,
79        }
80    }
81}
82
83impl TryFrom<u8> for VisitType {
84    type Error = InvalidVisitType;
85    fn try_from(p: u8) -> Result<Self, Self::Error> {
86        VisitType::from_primitive(p).ok_or(InvalidVisitType)
87    }
88}
89
90struct VisitTransitionSerdeVisitor;
91
92impl serde::de::Visitor<'_> for VisitTransitionSerdeVisitor {
93    type Value = VisitType;
94
95    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        formatter.write_str("positive integer representing VisitType")
97    }
98
99    fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<VisitType, E> {
100        if value > u64::from(u8::MAX) {
101            // In practice this is *way* out of the valid range of VisitType, but
102            // serde requires us to implement this as visit_u64 so...
103            return Err(E::custom(format!("value out of u8 range: {}", value)));
104        }
105        VisitType::from_primitive(value as u8)
106            .ok_or_else(|| E::custom(format!("unknown VisitType value: {}", value)))
107    }
108}
109
110impl serde::Serialize for VisitType {
111    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112        serializer.serialize_u64(*self as u64)
113    }
114}
115
116impl<'de> serde::Deserialize<'de> for VisitType {
117    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118        deserializer.deserialize_u64(VisitTransitionSerdeVisitor)
119    }
120}
121
122/// Bookmark types.
123#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
124#[repr(u8)]
125pub enum BookmarkType {
126    Bookmark = 1, // TYPE_BOOKMARK
127    Folder = 2,   // TYPE_FOLDER
128    Separator = 3, // TYPE_SEPARATOR;
129                  // On desktop, TYPE_DYNAMIC_CONTAINER = 4 but is deprecated - so please
130                  // avoid using this value in the future.
131}
132
133impl FromSql for BookmarkType {
134    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
135        let v = value.as_i64()?;
136        if v < 0 || v > i64::from(u8::MAX) {
137            return Err(FromSqlError::OutOfRange(v));
138        }
139        BookmarkType::from_u8(v as u8).ok_or(FromSqlError::OutOfRange(v))
140    }
141}
142
143impl BookmarkType {
144    #[inline]
145    pub fn from_u8(v: u8) -> Option<Self> {
146        match v {
147            1 => Some(BookmarkType::Bookmark),
148            2 => Some(BookmarkType::Folder),
149            3 => Some(BookmarkType::Separator),
150            _ => None,
151        }
152    }
153
154    pub fn from_u8_with_valid_url<F: Fn() -> bool>(v: u8, has_valid_url: F) -> Self {
155        match BookmarkType::from_u8(v) {
156            Some(BookmarkType::Bookmark) | None => {
157                if has_valid_url() {
158                    // Even if the node says it is a bookmark it still must have a
159                    // valid url.
160                    BookmarkType::Bookmark
161                } else {
162                    BookmarkType::Folder
163                }
164            }
165            Some(t) => t,
166        }
167    }
168}
169
170impl ToSql for BookmarkType {
171    fn to_sql(&self) -> RusqliteResult<ToSqlOutput<'_>> {
172        Ok(ToSqlOutput::from(*self as u8))
173    }
174}
175
176impl Serialize for BookmarkType {
177    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
178    where
179        S: Serializer,
180    {
181        serializer.serialize_u8(*self as u8)
182    }
183}
184
185/// Re SyncStatus - note that:
186/// * logins has synced=0, changed=1, new=2
187/// * desktop bookmarks has unknown=0, new=1, normal=2
188///
189/// This is "places", so eventually bookmarks will have a status - should history
190/// and bookmarks share this enum?
191/// Note that history specifically needs neither (a) login's "changed" (the
192/// changeCounter works there), nor (b) bookmark's "unknown" (as that's only
193/// used after a restore).
194/// History only needs a distinction between "synced" and "new" so it doesn't
195/// accumulate never-to-be-synced tombstones - so we basically copy bookmarks
196/// and treat unknown as new.
197/// Which means we get the "bonus side-effect" ;) of ::Unknown replacing Option<>!
198///
199/// Note that some of these values are in schema.rs
200#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
201#[repr(u8)]
202pub enum SyncStatus {
203    Unknown = 0,
204    New = 1,
205    Normal = 2,
206}
207
208impl SyncStatus {
209    #[inline]
210    pub fn from_u8(v: u8) -> Self {
211        match v {
212            1 => SyncStatus::New,
213            2 => SyncStatus::Normal,
214            _ => SyncStatus::Unknown,
215        }
216    }
217}
218
219impl FromSql for SyncStatus {
220    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
221        let v = value.as_i64()?;
222        if v < 0 || v > i64::from(u8::MAX) {
223            return Err(FromSqlError::OutOfRange(v));
224        }
225        Ok(SyncStatus::from_u8(v as u8))
226    }
227}
228
229impl ToSql for SyncStatus {
230    fn to_sql(&self) -> RusqliteResult<ToSqlOutput<'_>> {
231        Ok(ToSqlOutput::from(*self as u8))
232    }
233}
234
235// This type is used as a snazzy way to capture all unknown fields from the payload
236// upon deserialization without having to work with a concrete type
237pub type UnknownFields = serde_json::Map<String, serde_json::Value>;
238
239pub(crate) fn serialize_unknown_fields(
240    unknown_fields: &UnknownFields,
241) -> crate::Result<Option<String>> {
242    if unknown_fields.is_empty() {
243        Ok(None)
244    } else {
245        Ok(Some(serde_json::to_string(unknown_fields)?))
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_primitive() {
255        assert_eq!(Some(VisitType::Link), VisitType::from_primitive(1));
256        assert_eq!(None, VisitType::from_primitive(99));
257    }
258}