1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::storage::bookmarks::BookmarkRootGuid;
use crate::types::BookmarkType;
use error_support::{ErrorHandling, GetErrorHandling};
use interrupt_support::Interrupted;

// Result type used internally
pub type Result<T> = std::result::Result<T, Error>;
// Functions which are part of the public API should use this Result.
pub type ApiResult<T> = std::result::Result<T, PlacesApiError>;

// Errors we return via the public interface.
#[derive(Debug, thiserror::Error)]
pub enum PlacesApiError {
    #[error("Unexpected error: {reason}")]
    UnexpectedPlacesException { reason: String },

    /// Thrown for invalid URLs
    ///
    /// This includes attempting to insert a URL greater than 65536 bytes
    /// (after punycoding and percent encoding).
    #[error("UrlParseFailed: {reason}")]
    UrlParseFailed { reason: String },

    #[error("PlacesConnectionBusy error: {reason}")]
    PlacesConnectionBusy { reason: String },

    #[error("Operation Interrupted: {reason}")]
    OperationInterrupted { reason: String },

    /// Thrown when providing a guid to a create or update function
    /// which does not refer to a known bookmark.
    #[error("Unknown bookmark: {reason}")]
    UnknownBookmarkItem { reason: String },

    /// Attempt to create/update/delete a bookmark item in an illegal way.
    ///
    /// Some examples:
    ///  - Attempting to change the URL of a bookmark folder
    ///  - Referring to a non-folder as the parentGUID parameter to a create or update
    ///  - Attempting to insert a child under BookmarkRoot.Root,
    #[error("Invalid bookmark operation: {reason}")]
    InvalidBookmarkOperation { reason: String },
}

/// Error enum used internally
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Invalid place info: {0}")]
    InvalidPlaceInfo(#[from] InvalidPlaceInfo),

    #[error("The store is corrupt: {0}")]
    Corruption(#[from] Corruption),

    #[error("Error synchronizing: {0}")]
    SyncAdapterError(#[from] sync15::Error),

    #[error("Error merging: {0}")]
    MergeError(#[from] dogear::Error),

    #[error("Error parsing JSON data: {0}")]
    JsonError(#[from] serde_json::Error),

    #[error("Error executing SQL: {0}")]
    SqlError(#[from] rusqlite::Error),

    #[error("Error parsing URL: {0}")]
    UrlParseError(#[from] url::ParseError),

    #[error("A connection of this type is already open")]
    ConnectionAlreadyOpen,

    #[error("An invalid connection type was specified")]
    InvalidConnectionType,

    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),

    #[error("Operation interrupted")]
    InterruptedError(#[from] Interrupted),

    #[error("Tried to close connection on wrong PlacesApi instance")]
    WrongApiForClose,

    #[error("Incoming bookmark missing type")]
    MissingBookmarkKind,

    #[error("Synced bookmark has unsupported kind {0}")]
    UnsupportedSyncedBookmarkKind(u8),

    #[error("Synced bookmark has unsupported validity {0}")]
    UnsupportedSyncedBookmarkValidity(u8),

    // This will happen if you provide something absurd like
    // "/" or "" as your database path. For more subtley broken paths,
    // we'll likely return an IoError.
    #[error("Illegal database path: {0:?}")]
    IllegalDatabasePath(std::path::PathBuf),

    #[error("UTF8 Error: {0}")]
    Utf8Error(#[from] std::str::Utf8Error),

    // This error is saying an old Fennec or iOS version isn't supported - it's never used for
    // our specific version.
    #[error("Can not import from database version {0}")]
    UnsupportedDatabaseVersion(i64),

    #[error("Error opening database: {0}")]
    OpenDatabaseError(#[from] sql_support::open_database::Error),

    #[error("Invalid metadata observation: {0}")]
    InvalidMetadataObservation(#[from] InvalidMetadataObservation),
}

#[derive(Debug, thiserror::Error)]
pub enum InvalidPlaceInfo {
    #[error("No url specified")]
    NoUrl,
    #[error("Invalid guid")]
    InvalidGuid,
    #[error("Invalid parent: {0}")]
    InvalidParent(String),
    #[error("Invalid child guid")]
    InvalidChildGuid,

    // NoSuchGuid is used for guids, which aren't considered private information,
    // so it's fine if this error, including the guid, is in the logs.
    #[error("No such item: {0}")]
    NoSuchGuid(String),

    // NoSuchUrl is used for URLs, which are private information, so the URL
    // itself is not included in the error.
    #[error("No such url")]
    NoSuchUrl,

    #[error("Can't update a bookmark of type {0} with one of type {1}")]
    MismatchedBookmarkType(u8, u8),

    // Only returned when attempting to insert a bookmark --
    // for history we just ignore it.
    #[error("URL too long")]
    UrlTooLong,

    // Like Urls, a tag is considered private info, so the value isn't in the error.
    #[error("The tag value is invalid")]
    InvalidTag,
    #[error("Cannot change the '{0}' property of a bookmark of type {1:?}")]
    IllegalChange(&'static str, BookmarkType),

    #[error("Cannot update the bookmark root {0:?}")]
    CannotUpdateRoot(BookmarkRootGuid),
}

// Error types used when we can't continue due to corruption.
// Note that this is currently only for "logical" corruption. Should we
// consider mapping sqlite error codes which mean a lower-level of corruption
// into an enum value here?
#[derive(Debug, thiserror::Error)]
pub enum Corruption {
    #[error("Bookmark '{0}' has a parent of '{1}' which does not exist")]
    NoParent(String, String),

    #[error("The local roots are invalid")]
    InvalidLocalRoots,

    #[error("The synced roots are invalid")]
    InvalidSyncedRoots,

    #[error("Bookmark '{0}' has no parent but is not the bookmarks root")]
    NonRootWithoutParent(String),
}

#[derive(Debug, thiserror::Error)]
pub enum InvalidMetadataObservation {
    #[error("Observed view time is invalid (too long)")]
    ViewTimeTooLong,
}

// Define how our internal errors are handled and converted to external errors
// See `support/error/README.md` for how this works, especially the warning about PII.
impl GetErrorHandling for Error {
    type ExternalError = PlacesApiError;

    fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
        match self {
            Error::InvalidPlaceInfo(info) => {
                let label = info.to_string();
                ErrorHandling::convert(match &info {
                    InvalidPlaceInfo::InvalidParent(..) => {
                        PlacesApiError::InvalidBookmarkOperation { reason: label }
                    }
                    InvalidPlaceInfo::UrlTooLong => {
                        PlacesApiError::UrlParseFailed { reason: label }
                    }
                    InvalidPlaceInfo::NoSuchGuid(..) => {
                        PlacesApiError::UnknownBookmarkItem { reason: label }
                    }
                    InvalidPlaceInfo::IllegalChange(..) => {
                        PlacesApiError::InvalidBookmarkOperation { reason: label }
                    }
                    InvalidPlaceInfo::CannotUpdateRoot(..) => {
                        PlacesApiError::InvalidBookmarkOperation { reason: label }
                    }
                    _ => PlacesApiError::UnexpectedPlacesException { reason: label },
                })
                .report_error("places-invalid-place-info")
            }
            Error::UrlParseError(e) => {
                // This is a known issue with invalid URLs coming from Fenix. Let's just log a
                // warning for this one. See #5235 for more details.
                ErrorHandling::convert(PlacesApiError::UrlParseFailed {
                    reason: e.to_string(),
                })
                .log_warning()
            }
            // Can't pattern match on `err` without adding a dep on the sqlite3-sys crate,
            // so we just use a `if` guard.
            Error::SqlError(rusqlite::Error::SqliteFailure(err, _))
                if err.code == rusqlite::ErrorCode::DatabaseBusy =>
            {
                ErrorHandling::convert(PlacesApiError::PlacesConnectionBusy {
                    reason: self.to_string(),
                })
                .log_warning()
            }
            Error::SqlError(rusqlite::Error::SqliteFailure(err, _))
                if err.code == rusqlite::ErrorCode::OperationInterrupted =>
            {
                ErrorHandling::convert(PlacesApiError::OperationInterrupted {
                    reason: self.to_string(),
                })
                .log_info()
            }
            Error::InterruptedError(err) => {
                // Can't unify with the above ... :(
                ErrorHandling::convert(PlacesApiError::OperationInterrupted {
                    reason: err.to_string(),
                })
                .log_info()
            }
            Error::Corruption(e) => {
                ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
                    reason: e.to_string(),
                })
                .report_error("places-bookmarks-corruption")
            }
            Error::SyncAdapterError(e) => {
                match e {
                    sync15::Error::StoreError(store_error) => {
                        // If it's a type-erased version of one of our errors, try
                        // and resolve it.
                        if let Some(places_err) = store_error.downcast_ref::<Error>() {
                            log::info!("Recursing to resolve places error");
                            places_err.get_error_handling()
                        } else {
                            ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
                                reason: self.to_string(),
                            })
                            .report_error("places-unexpected-sync-error")
                        }
                    }
                    _ => ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
                        reason: self.to_string(),
                    })
                    .report_error("places-unexpected-sync-error"),
                }
            }
            Error::InvalidMetadataObservation(InvalidMetadataObservation::ViewTimeTooLong) => {
                ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
                    reason: self.to_string(),
                })
                .log_warning()
            }
            _ => ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
                reason: self.to_string(),
            })
            .report_error("places-unexpected-error"),
        }
    }
}