places/import/
common.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::db::PlacesDb;
6use crate::error::*;
7use rusqlite::{named_params, Connection};
8use serde::Serialize;
9use sql_support::ConnExt;
10use types::Timestamp;
11use url::Url;
12
13// sanitize_timestamp can't use `Timestamp::now();` directly because it needs
14// to sanitize both created and modified, plus ensure modified isn't before
15// created - which isn't possible with the non-monotonic timestamp.
16// So we have a static `NOW`, which will be initialized the first time it is
17// referenced, and that value subsequently used for every imported bookmark (and
18// note that it's only used in cases where the existing timestamps are invalid.)
19// This is fine for our use-case, where we do exactly one import as soon as the
20// process starts.
21lazy_static::lazy_static! {
22    pub static ref NOW: Timestamp = Timestamp::now();
23}
24
25pub mod sql_fns {
26    use crate::import::common::NOW;
27    use crate::storage::URL_LENGTH_MAX;
28    use rusqlite::{functions::Context, types::ValueRef, Result};
29    use types::Timestamp;
30    use url::Url;
31
32    fn sanitize_timestamp(ts: i64) -> Result<Timestamp> {
33        let now = *NOW;
34        let is_sane = |ts: Timestamp| -> bool { Timestamp::EARLIEST <= ts && ts <= now };
35        let ts = Timestamp(u64::try_from(ts).unwrap_or(0));
36        if is_sane(ts) {
37            return Ok(ts);
38        }
39        // Maybe the timestamp was actually in μs?
40        let ts = Timestamp(ts.as_millis() / 1000);
41        if is_sane(ts) {
42            return Ok(ts);
43        }
44        Ok(now)
45    }
46
47    // Unfortunately dates for history visits in old iOS databases
48    // have a type of `REAL` in their schema. This means they are represented
49    // as a float value and have to be read as f64s.
50    // This is unconventional, and you probably don't need to use
51    // this function otherwise.
52    #[inline(never)]
53    pub fn sanitize_float_timestamp(ctx: &Context<'_>) -> Result<Timestamp> {
54        let ts = ctx
55            .get::<f64>(0)
56            .map(|num| {
57                if num.is_normal() && num > 0.0 {
58                    num.round() as i64
59                } else {
60                    0
61                }
62            })
63            .unwrap_or(0);
64        sanitize_timestamp(ts)
65    }
66
67    #[inline(never)]
68    pub fn sanitize_integer_timestamp(ctx: &Context<'_>) -> Result<Timestamp> {
69        sanitize_timestamp(ctx.get::<i64>(0).unwrap_or(0))
70    }
71
72    // Possibly better named as "normalize URL" - even in non-error cases, the
73    // result string may not be the same href used passed as input.
74    #[inline(never)]
75    pub fn validate_url(ctx: &Context<'_>) -> Result<Option<String>> {
76        let val = ctx.get_raw(0);
77        let href = if let ValueRef::Text(s) = val {
78            String::from_utf8_lossy(s).to_string()
79        } else {
80            return Ok(None);
81        };
82        if href.len() > URL_LENGTH_MAX {
83            return Ok(None);
84        }
85        if let Ok(url) = Url::parse(&href) {
86            Ok(Some(url.into()))
87        } else {
88            Ok(None)
89        }
90    }
91
92    // Sanitize a text column into valid utf-8. Leave NULLs alone, but all other
93    // types are converted to an empty string.
94    #[inline(never)]
95    pub fn sanitize_utf8(ctx: &Context<'_>) -> Result<Option<String>> {
96        let val = ctx.get_raw(0);
97        Ok(match val {
98            ValueRef::Text(s) => Some(String::from_utf8_lossy(s).to_string()),
99            ValueRef::Null => None,
100            _ => Some("".to_owned()),
101        })
102    }
103}
104
105pub fn attached_database<'a>(
106    conn: &'a PlacesDb,
107    path: &Url,
108    db_alias: &'static str,
109) -> Result<ExecuteOnDrop<'a>> {
110    conn.execute(
111        "ATTACH DATABASE :path AS :db_alias",
112        named_params! {
113            ":path": path.as_str(),
114            ":db_alias": db_alias,
115        },
116    )?;
117    Ok(ExecuteOnDrop {
118        conn,
119        sql: format!("DETACH DATABASE {};", db_alias),
120    })
121}
122
123/// We use/abuse the mirror to perform our import, but need to clean it up
124/// afterwards. This is an RAII helper to do so.
125///
126/// Ideally, you should call `execute_now` rather than letting this drop
127/// automatically, as we can't report errors beyond logging when running
128/// Drop.
129pub struct ExecuteOnDrop<'a> {
130    conn: &'a PlacesDb,
131    sql: String,
132}
133
134impl<'a> ExecuteOnDrop<'a> {
135    pub fn new(conn: &'a PlacesDb, sql: String) -> Self {
136        Self { conn, sql }
137    }
138
139    pub fn execute_now(self) -> Result<()> {
140        self.conn.execute_batch(&self.sql)?;
141        // Don't run our `drop` function.
142        std::mem::forget(self);
143        Ok(())
144    }
145}
146
147impl Drop for ExecuteOnDrop<'_> {
148    fn drop(&mut self) {
149        if let Err(e) = self.conn.execute_batch(&self.sql) {
150            error_support::report_error!(
151                "places-cleanup-failure",
152                "Failed to clean up after import! {}",
153                e
154            );
155            debug!("  Failed query: {}", &self.sql);
156        }
157    }
158}
159
160pub fn select_count(conn: &PlacesDb, stmt: &str) -> Result<u32> {
161    let count: Result<Option<u32>> =
162        conn.try_query_row(stmt, [], |row| Ok(row.get::<_, u32>(0)?), false);
163    count.map(|op| op.unwrap_or(0))
164}
165
166#[derive(Serialize, PartialEq, Eq, Debug, Clone, Default)]
167pub struct HistoryMigrationResult {
168    pub num_total: u32,
169    pub num_succeeded: u32,
170    pub num_failed: u32,
171    pub total_duration: u64,
172}
173
174pub fn define_history_migration_functions(c: &Connection) -> Result<()> {
175    use rusqlite::functions::FunctionFlags;
176    c.create_scalar_function(
177        "validate_url",
178        1,
179        FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
180        crate::import::common::sql_fns::validate_url,
181    )?;
182    c.create_scalar_function(
183        "sanitize_timestamp",
184        1,
185        FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
186        crate::import::common::sql_fns::sanitize_integer_timestamp,
187    )?;
188    c.create_scalar_function(
189        "hash",
190        -1,
191        FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
192        crate::db::db::sql_fns::hash,
193    )?;
194    c.create_scalar_function(
195        "generate_guid",
196        0,
197        FunctionFlags::SQLITE_UTF8,
198        crate::db::db::sql_fns::generate_guid,
199    )?;
200    c.create_scalar_function(
201        "sanitize_utf8",
202        1,
203        FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
204        crate::import::common::sql_fns::sanitize_utf8,
205    )?;
206    c.create_scalar_function(
207        "sanitize_float_timestamp",
208        1,
209        FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
210        crate::import::common::sql_fns::sanitize_float_timestamp,
211    )?;
212    Ok(())
213}