places/
error.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 crate::storage::bookmarks::BookmarkRootGuid;
6use crate::types::BookmarkType;
7use error_support::{ErrorHandling, GetErrorHandling};
8use interrupt_support::Interrupted;
9
10// reexport logging helpers.
11pub use error_support::{debug, error, info, trace, warn};
12
13// Result type used internally
14pub type Result<T> = std::result::Result<T, Error>;
15// Functions which are part of the public API should use this Result.
16pub type ApiResult<T> = std::result::Result<T, PlacesApiError>;
17
18// Errors we return via the public interface.
19#[derive(Debug, thiserror::Error)]
20pub enum PlacesApiError {
21    #[error("Unexpected error: {reason}")]
22    UnexpectedPlacesException { reason: String },
23
24    /// Thrown for invalid URLs
25    ///
26    /// This includes attempting to insert a URL greater than 65536 bytes
27    /// (after punycoding and percent encoding).
28    #[error("UrlParseFailed: {reason}")]
29    UrlParseFailed { reason: String },
30
31    #[error("PlacesConnectionBusy error: {reason}")]
32    PlacesConnectionBusy { reason: String },
33
34    #[error("Operation Interrupted: {reason}")]
35    OperationInterrupted { reason: String },
36
37    /// Thrown when providing a guid to a create or update function
38    /// which does not refer to a known bookmark.
39    #[error("Unknown bookmark: {reason}")]
40    UnknownBookmarkItem { reason: String },
41
42    /// Attempt to create/update/delete a bookmark item in an illegal way.
43    ///
44    /// Some examples:
45    ///  - Attempting to change the URL of a bookmark folder
46    ///  - Referring to a non-folder as the parentGUID parameter to a create or update
47    ///  - Attempting to insert a child under BookmarkRoot.Root,
48    #[error("Invalid bookmark operation: {reason}")]
49    InvalidBookmarkOperation { reason: String },
50}
51
52/// Error enum used internally
53#[derive(Debug, thiserror::Error)]
54pub enum Error {
55    #[error("Invalid place info: {0}")]
56    InvalidPlaceInfo(#[from] InvalidPlaceInfo),
57
58    #[error("The store is corrupt: {0}")]
59    Corruption(#[from] Corruption),
60
61    #[error("Error synchronizing: {0}")]
62    SyncAdapterError(#[from] sync15::Error),
63
64    #[error("Error merging: {0}")]
65    MergeError(#[from] dogear::Error),
66
67    #[error("Error parsing JSON data: {0}")]
68    JsonError(#[from] serde_json::Error),
69
70    #[error("Error executing SQL: {0}")]
71    SqlError(#[from] rusqlite::Error),
72
73    #[error("Error parsing URL: {0}")]
74    UrlParseError(#[from] url::ParseError),
75
76    #[error("A connection of this type is already open")]
77    ConnectionAlreadyOpen,
78
79    #[error("An invalid connection type was specified")]
80    InvalidConnectionType,
81
82    #[error("IO error: {0}")]
83    IoError(#[from] std::io::Error),
84
85    #[error("Operation interrupted")]
86    InterruptedError(#[from] Interrupted),
87
88    #[error("Tried to close connection on wrong PlacesApi instance")]
89    WrongApiForClose,
90
91    #[error("Incoming bookmark missing type")]
92    MissingBookmarkKind,
93
94    #[error("Synced bookmark has unsupported kind {0}")]
95    UnsupportedSyncedBookmarkKind(u8),
96
97    #[error("Synced bookmark has unsupported validity {0}")]
98    UnsupportedSyncedBookmarkValidity(u8),
99
100    // This will happen if you provide something absurd like
101    // "/" or "" as your database path. For more subtley broken paths,
102    // we'll likely return an IoError.
103    #[error("Illegal database path: {0:?}")]
104    IllegalDatabasePath(std::path::PathBuf),
105
106    #[error("UTF8 Error: {0}")]
107    Utf8Error(#[from] std::str::Utf8Error),
108
109    // This error is saying an old Fennec or iOS version isn't supported - it's never used for
110    // our specific version.
111    #[error("Can not import from database version {0}")]
112    UnsupportedDatabaseVersion(i64),
113
114    #[error("Error opening database: {0}")]
115    OpenDatabaseError(#[from] sql_support::open_database::Error),
116
117    #[error("Invalid metadata observation: {0}")]
118    InvalidMetadataObservation(#[from] InvalidMetadataObservation),
119}
120
121#[derive(Debug, thiserror::Error)]
122pub enum InvalidPlaceInfo {
123    #[error("No url specified")]
124    NoUrl,
125    #[error("Invalid guid")]
126    InvalidGuid,
127    #[error("Invalid parent: {0}")]
128    InvalidParent(String),
129    #[error("Invalid child guid")]
130    InvalidChildGuid,
131
132    // NoSuchGuid is used for guids, which aren't considered private information,
133    // so it's fine if this error, including the guid, is in the logs.
134    #[error("No such item: {0}")]
135    NoSuchGuid(String),
136
137    // NoSuchUrl is used for URLs, which are private information, so the URL
138    // itself is not included in the error.
139    #[error("No such url")]
140    NoSuchUrl,
141
142    #[error("Can't update a bookmark of type {0} with one of type {1}")]
143    MismatchedBookmarkType(u8, u8),
144
145    // Only returned when attempting to insert a bookmark --
146    // for history we just ignore it.
147    #[error("URL too long")]
148    UrlTooLong,
149
150    // Like Urls, a tag is considered private info, so the value isn't in the error.
151    #[error("The tag value is invalid")]
152    InvalidTag,
153    #[error("Cannot change the '{0}' property of a bookmark of type {1:?}")]
154    IllegalChange(&'static str, BookmarkType),
155
156    #[error("Cannot update the bookmark root {0:?}")]
157    CannotUpdateRoot(BookmarkRootGuid),
158}
159
160// Error types used when we can't continue due to corruption.
161// Note that this is currently only for "logical" corruption. Should we
162// consider mapping sqlite error codes which mean a lower-level of corruption
163// into an enum value here?
164#[derive(Debug, thiserror::Error)]
165pub enum Corruption {
166    #[error("Bookmark '{0}' has a parent of '{1}' which does not exist")]
167    NoParent(String, String),
168
169    #[error("The local roots are invalid")]
170    InvalidLocalRoots,
171
172    #[error("The synced roots are invalid")]
173    InvalidSyncedRoots,
174
175    #[error("Bookmark '{0}' has no parent but is not the bookmarks root")]
176    NonRootWithoutParent(String),
177}
178
179#[derive(Debug, thiserror::Error)]
180pub enum InvalidMetadataObservation {
181    #[error("Observed view time is invalid (too long)")]
182    ViewTimeTooLong,
183}
184
185// Define how our internal errors are handled and converted to external errors
186// See `support/error/README.md` for how this works, especially the warning about PII.
187impl GetErrorHandling for Error {
188    type ExternalError = PlacesApiError;
189
190    fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
191        match self {
192            Error::InvalidPlaceInfo(info) => {
193                let label = info.to_string();
194                ErrorHandling::convert(match &info {
195                    InvalidPlaceInfo::InvalidParent(..) => {
196                        PlacesApiError::InvalidBookmarkOperation { reason: label }
197                    }
198                    InvalidPlaceInfo::UrlTooLong => {
199                        PlacesApiError::UrlParseFailed { reason: label }
200                    }
201                    InvalidPlaceInfo::NoSuchGuid(..) => {
202                        PlacesApiError::UnknownBookmarkItem { reason: label }
203                    }
204                    InvalidPlaceInfo::IllegalChange(..) => {
205                        PlacesApiError::InvalidBookmarkOperation { reason: label }
206                    }
207                    InvalidPlaceInfo::CannotUpdateRoot(..) => {
208                        PlacesApiError::InvalidBookmarkOperation { reason: label }
209                    }
210                    _ => PlacesApiError::UnexpectedPlacesException { reason: label },
211                })
212                .report_error("places-invalid-place-info")
213            }
214            Error::UrlParseError(e) => {
215                // This is a known issue with invalid URLs coming from Fenix. Let's just log a
216                // warning for this one. See #5235 for more details.
217                ErrorHandling::convert(PlacesApiError::UrlParseFailed {
218                    reason: e.to_string(),
219                })
220                .log_warning()
221            }
222            Error::SqlError(rusqlite::Error::SqliteFailure(err, _)) => match err.code {
223                rusqlite::ErrorCode::DatabaseBusy => {
224                    ErrorHandling::convert(PlacesApiError::PlacesConnectionBusy {
225                        reason: self.to_string(),
226                    })
227                    .log_warning()
228                }
229                rusqlite::ErrorCode::OperationInterrupted => {
230                    ErrorHandling::convert(PlacesApiError::OperationInterrupted {
231                        reason: self.to_string(),
232                    })
233                    .log_info()
234                }
235                rusqlite::ErrorCode::DatabaseCorrupt => {
236                    ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
237                        reason: self.to_string(),
238                    })
239                    .report_error("places-db-corrupt")
240                }
241                rusqlite::ErrorCode::DiskFull => {
242                    ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
243                        reason: self.to_string(),
244                    })
245                    .report_error("places-db-disk-full")
246                }
247                _ => ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
248                    reason: self.to_string(),
249                })
250                .report_error("places-unexpected"),
251            },
252            Error::InterruptedError(err) => {
253                // Can't unify with the above ... :(
254                ErrorHandling::convert(PlacesApiError::OperationInterrupted {
255                    reason: err.to_string(),
256                })
257                .log_info()
258            }
259            Error::Corruption(e) => {
260                ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
261                    reason: e.to_string(),
262                })
263                .report_error("places-bookmarks-corruption")
264            }
265            Error::SyncAdapterError(e) => {
266                match e {
267                    sync15::Error::StoreError(store_error) => {
268                        // If it's a type-erased version of one of our errors, try
269                        // and resolve it.
270                        if let Some(places_err) = store_error.downcast_ref::<Error>() {
271                            info!("Recursing to resolve places error");
272                            places_err.get_error_handling()
273                        } else {
274                            ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
275                                reason: self.to_string(),
276                            })
277                            .report_error("places-unexpected-sync-error")
278                        }
279                    }
280                    _ => ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
281                        reason: self.to_string(),
282                    })
283                    .report_error("places-unexpected-sync-error"),
284                }
285            }
286            Error::InvalidMetadataObservation(InvalidMetadataObservation::ViewTimeTooLong) => {
287                ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
288                    reason: self.to_string(),
289                })
290                .log_warning()
291            }
292            _ => ErrorHandling::convert(PlacesApiError::UnexpectedPlacesException {
293                reason: self.to_string(),
294            })
295            .report_error("places-unexpected-error"),
296        }
297    }
298}