Skip to main content

storage/
client_storage.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 https://mozilla.org/MPL/2.0/. */
4use std::fmt::Debug;
5use std::path::PathBuf;
6use std::thread;
7
8use rusqlite::{Connection, Transaction};
9use servo_base::generic_channel::{self, GenericReceiver, GenericSender};
10use servo_base::id::{BrowsingContextId, WebViewId};
11use servo_url::ImmutableOrigin;
12use storage_traits::client_storage::{
13    ClientStorageErrorr, ClientStorageThreadHandle, ClientStorageThreadMessage, Mode,
14    StorageIdentifier, StorageProxyMap, StorageType,
15};
16use uuid::Uuid;
17
18trait RegistryEngine {
19    type Error: Debug;
20    fn create_database(
21        &mut self,
22        bottle_id: i64,
23        name: String,
24    ) -> Result<PathBuf, ClientStorageErrorr<Self::Error>>;
25    fn delete_database(
26        &mut self,
27        bottle_id: i64,
28        name: String,
29    ) -> Result<(), ClientStorageErrorr<Self::Error>>;
30    fn obtain_a_storage_bottle_map(
31        &mut self,
32        storage_type: StorageType,
33        webview: WebViewId,
34        storage_identifier: StorageIdentifier,
35        origin: ImmutableOrigin,
36        sender: &GenericSender<ClientStorageThreadMessage>,
37    ) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>>;
38}
39
40struct SqliteEngine {
41    connection: Connection,
42    base_dir: PathBuf,
43}
44
45impl SqliteEngine {
46    fn new(base_dir: PathBuf) -> rusqlite::Result<Self> {
47        let db_path = base_dir.join("reg.sqlite");
48        let connection = Connection::open(db_path)?;
49        Self::init(&connection)?;
50        Ok(SqliteEngine {
51            connection,
52            base_dir,
53        })
54    }
55
56    fn init(connection: &Connection) -> rusqlite::Result<()> {
57        connection.execute(r#"PRAGMA foreign_keys = ON;"#, [])?;
58        connection.execute(
59            r#"CREATE TABLE IF NOT EXISTS sheds (
60            id INTEGER PRIMARY KEY,
61            storage_type TEXT NOT NULL,
62            browsing_context TEXT
63        );"#,
64            [],
65        )?;
66
67        // Note: indices required for ON CONFLICT to work.
68        connection.execute(
69            r#"CREATE UNIQUE INDEX IF NOT EXISTS idx_sheds_local
70        ON sheds(storage_type) WHERE browsing_context IS NULL;"#,
71            [],
72        )?;
73        connection.execute(
74            r#"CREATE UNIQUE INDEX IF NOT EXISTS idx_sheds_session
75        ON sheds(browsing_context) WHERE browsing_context IS NOT NULL;"#,
76            [],
77        )?;
78
79        connection.execute(
80            r#"CREATE TABLE IF NOT EXISTS shelves (
81            id INTEGER PRIMARY KEY,
82            shed_id INTEGER NOT NULL,
83            origin TEXT NOT NULL,
84            UNIQUE (shed_id, origin),
85            FOREIGN KEY (shed_id) REFERENCES sheds(id) ON DELETE CASCADE
86        );"#,
87            [],
88        )?;
89
90        // Note: name is to support https://wicg.github.io/storage-buckets/
91        connection.execute(
92            r#"CREATE TABLE IF NOT EXISTS buckets (
93            id INTEGER PRIMARY KEY,
94            shelf_id INTEGER NOT NULL UNIQUE,
95            persisted BOOLEAN DEFAULT 0,
96            name TEXT,
97            mode TEXT,
98            expires DATETIME,
99            FOREIGN KEY (shelf_id) REFERENCES shelves(id) ON DELETE CASCADE
100        );"#,
101            [],
102        )?;
103
104        // Note: quota not in db, hardcoded at https://storage.spec.whatwg.org/#storage-endpoint-quota
105        connection.execute(
106            r#"CREATE TABLE IF NOT EXISTS bottles (
107                    id INTEGER PRIMARY KEY,
108                    bucket_id INTEGER NOT NULL,
109                    identifier TEXT NOT NULL,  -- "idb", "ls", "opfs", "cache"
110                    UNIQUE (bucket_id, identifier),
111                    FOREIGN KEY (bucket_id) REFERENCES buckets(id) ON DELETE CASCADE
112                );"#,
113            [],
114        )?;
115
116        connection.execute(
117            r#"CREATE TABLE IF NOT EXISTS databases (
118                    id INTEGER PRIMARY KEY,
119                    bottle_id INTEGER NOT NULL,
120                    name TEXT NOT NULL,
121                    UNIQUE (bottle_id, name),
122                    FOREIGN KEY (bottle_id) REFERENCES bottles(id) ON DELETE CASCADE
123                );
124                "#,
125            [],
126        )?;
127
128        connection.execute(
129            r#"CREATE TABLE IF NOT EXISTS directories (
130                id INTEGER PRIMARY KEY,
131                database_id INTEGER NOT NULL UNIQUE,
132                path TEXT NOT NULL,
133                FOREIGN KEY (database_id) REFERENCES databases(id) ON DELETE CASCADE
134            );"#,
135            [],
136        )?;
137
138        connection.execute_batch(
139            r#"
140                CREATE UNIQUE INDEX IF NOT EXISTS sheds_local_identity_idx
141                ON sheds(storage_type)
142                WHERE storage_type = 'local' AND browsing_context IS NULL;
143
144                CREATE UNIQUE INDEX IF NOT EXISTS sheds_session_identity_idx
145                ON sheds(storage_type, browsing_context)
146                WHERE storage_type = 'session' AND browsing_context IS NOT NULL;
147
148                CREATE UNIQUE INDEX IF NOT EXISTS shelves_origin_shed_identity_idx
149                ON shelves(origin, shed_id);
150            "#,
151        )?;
152        // TODO: Delete expired and non-persistent buckets on startup
153        Ok(())
154    }
155}
156
157fn ensure_storage_shed(
158    storage_type: &StorageType,
159    browsing_context: Option<String>,
160    tx: &Transaction,
161) -> rusqlite::Result<i64> {
162    match browsing_context {
163        Some(browsing_context) => {
164            tx.execute(
165                "INSERT INTO sheds (storage_type, browsing_context) VALUES (?1, ?2) ON CONFLICT DO NOTHING;",
166                (storage_type.as_str(), browsing_context.as_str()),
167            )?;
168
169            tx.query_row(
170                "SELECT id FROM sheds WHERE storage_type = ?1 AND browsing_context = ?2;",
171                (storage_type.as_str(), browsing_context.as_str()),
172                |row| row.get(0),
173            )
174        },
175        None => {
176            tx.execute(
177                "INSERT INTO sheds (storage_type, browsing_context) VALUES (?1, NULL) ON CONFLICT DO NOTHING;",
178                [storage_type.as_str()],
179            )?;
180
181            tx.query_row(
182                "SELECT id FROM sheds WHERE storage_type = ?1 AND browsing_context IS NULL;",
183                [storage_type.as_str()],
184                |row| row.get(0),
185            )
186        },
187    }
188}
189
190/// <https://storage.spec.whatwg.org/#create-a-storage-bucket>
191fn create_a_storage_bucket(
192    shelf_id: i64,
193    storage_type: StorageType,
194    tx: &Transaction,
195) -> rusqlite::Result<i64> {
196    // Step 1: Let bucket be null.
197    // Step 2: If type is "local", then set bucket to a new local storage bucket.
198    let bucket_id: i64 = if let StorageType::Local = storage_type {
199        tx.query_row(
200            "INSERT INTO buckets (mode, shelf_id) VALUES (?1, ?2)
201             ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
202             RETURNING id;",
203            [Mode::default().as_str(), &shelf_id.to_string()],
204            |row| row.get(0),
205        )?
206    } else {
207        // Step 3: Otherwise:
208        // Step 3.1: Assert: type is "session".
209        // Step 3.2: Set bucket to a new session storage bucket.
210        tx.query_row(
211            "INSERT INTO buckets (shelf_id) VALUES (?1)
212             ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
213             RETURNING id;",
214            [&shelf_id.to_string()],
215            |row| row.get(0),
216        )?
217    };
218
219    // Step 4: For each endpoint of registered storage endpoints whose types contain type,
220    // set bucket’s bottle map[endpoint’s identifier] to
221    // a new storage bottle whose quota is endpoint’s quota.
222
223    // <https://storage.spec.whatwg.org/#registered-storage-endpoints>
224    let registered_endpoints = match storage_type {
225        StorageType::Local => vec![
226            StorageIdentifier::Caches,
227            StorageIdentifier::IndexedDB,
228            StorageIdentifier::LocalStorage,
229            StorageIdentifier::ServiceWorkerRegistrations,
230        ],
231        StorageType::Session => vec![StorageIdentifier::SessionStorage],
232    };
233
234    for identifier in registered_endpoints {
235        tx.execute(
236            "INSERT INTO bottles (bucket_id, identifier) VALUES (?1, ?2)
237             ON CONFLICT(bucket_id, identifier) DO NOTHING;",
238            (bucket_id, identifier.as_str()),
239        )?;
240    }
241
242    // Step 5: Return bucket.
243    Ok(bucket_id)
244}
245
246/// <https://storage.spec.whatwg.org/#create-a-storage-shelf>
247fn create_a_storage_shelf(
248    shed: i64,
249    origin: &ImmutableOrigin,
250    storage_type: StorageType,
251    tx: &Transaction,
252) -> rusqlite::Result<i64> {
253    // Step 1: Let shelf be a new storage shelf.
254    let shelf_id: i64 = tx.query_row(
255        "INSERT INTO shelves (shed_id, origin) VALUES (?1, ?2)
256         ON CONFLICT(shed_id, origin) DO UPDATE SET origin = excluded.origin
257         RETURNING id;",
258        [&shed.to_string(), &origin.ascii_serialization()],
259        |row| row.get(0),
260    )?;
261
262    // Step 2: Set shelf’s bucket map["default"] to the result of running create a storage bucket with type.
263    // Note: returning `shelf’s bucket map["default"]`, which is the `bucket_id`.
264    create_a_storage_bucket(shelf_id, storage_type, tx)
265}
266
267/// <https://storage.spec.whatwg.org/#obtain-a-storage-shelf>
268fn obtain_a_storage_shelf(
269    shed: i64,
270    origin: &ImmutableOrigin,
271    storage_type: StorageType,
272    tx: &Transaction,
273) -> rusqlite::Result<i64> {
274    // Step 1: Let key be the result of running obtain a storage key with environment.
275    // Step 2: If key is failure, then return failure.
276    // Note: for now just using the origin as the key.
277    // TODO: implement https://storage.spec.whatwg.org/#obtain-a-storage-key
278
279    // Step 3: If shed[key] does not exist,
280    // then set shed[key] to the result of running create a storage shelf with type.
281    // Note: method internally conditions on shed[key] not existing.
282    let bucket_id = create_a_storage_shelf(shed, origin, storage_type, tx)?;
283
284    // Step 4: Return shed[key].
285    // Note: returning `shed[key]["default"]`, which is `bucket_id`.
286    Ok(bucket_id)
287}
288
289impl RegistryEngine for SqliteEngine {
290    type Error = rusqlite::Error;
291
292    /// Create a database for the indexedDB endpoint.
293    fn create_database(
294        &mut self,
295        bottle_id: i64,
296        name: String,
297    ) -> Result<PathBuf, ClientStorageErrorr<Self::Error>> {
298        let tx = self.connection.transaction()?;
299
300        let dir = Uuid::new_v4().to_string();
301        let cluster = dir.chars().last().unwrap();
302        let path = self
303            .base_dir
304            .join("bottles")
305            .join(cluster.to_string())
306            .join(dir);
307
308        let path_str = path.to_str().ok_or_else(|| {
309            ClientStorageErrorr::Internal(rusqlite::Error::InvalidParameterName(String::from(
310                "path",
311            )))
312        })?;
313
314        let database_id: i64 = tx
315            .query_row(
316                "INSERT INTO databases (bottle_id, name) VALUES (?1, ?2)
317             ON CONFLICT(bottle_id, name) DO NOTHING
318             RETURNING id;",
319                (bottle_id, name),
320                |row| row.get(0),
321            )
322            .map_err(|e| match e {
323                rusqlite::Error::QueryReturnedNoRows => ClientStorageErrorr::DatabaseAlreadyExists,
324                e => ClientStorageErrorr::Internal(e),
325            })?;
326
327        tx.execute(
328            "INSERT INTO directories (database_id, path) VALUES (?1, ?2);",
329            (database_id, path_str),
330        )?;
331
332        std::fs::create_dir_all(&path).map_err(|_| ClientStorageErrorr::DirectoryCreationFailed)?;
333
334        tx.commit()?;
335
336        Ok(path)
337    }
338
339    /// Delete a database for the indexedDB endpoint.
340    fn delete_database(
341        &mut self,
342        bottle_id: i64,
343        name: String,
344    ) -> Result<(), ClientStorageErrorr<Self::Error>> {
345        let tx = self.connection.transaction()?;
346
347        let database_id: i64 = tx.query_row(
348            "SELECT id FROM databases WHERE bottle_id = ?1 AND name = ?2;",
349            (bottle_id, name.clone()),
350            |row| row.get(0),
351        )?;
352
353        let path: String = tx.query_row(
354            "SELECT path FROM directories WHERE database_id = ?1;",
355            [database_id],
356            |row| row.get(0),
357        )?;
358
359        tx.execute(
360            "DELETE FROM databases WHERE bottle_id = ?1 AND name = ?2;",
361            (bottle_id, name),
362        )?;
363
364        if tx.changes() == 0 {
365            return Err(ClientStorageErrorr::DatabaseDoesNotExist);
366        }
367        // Note: directory deleted through SQL cascade.
368
369        // Delete the directory on disk
370        std::fs::remove_dir_all(&path).map_err(|_| ClientStorageErrorr::DirectoryDeletionFailed)?;
371
372        tx.commit()?;
373
374        Ok(())
375    }
376
377    /// <https://storage.spec.whatwg.org/#obtain-a-storage-bottle-map>
378    fn obtain_a_storage_bottle_map(
379        &mut self,
380        storage_type: StorageType,
381        webview: WebViewId,
382        storage_identifier: StorageIdentifier,
383        origin: ImmutableOrigin,
384        sender: &GenericSender<ClientStorageThreadMessage>,
385    ) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>> {
386        let tx = self.connection.transaction()?;
387
388        // Step 1: Let shed be null.
389        let shed_id: i64 = match storage_type {
390            StorageType::Local => {
391                // Step 2: If type is "local", then set shed to the user agent’s storage shed.
392                ensure_storage_shed(&storage_type, None, &tx)?
393            },
394            StorageType::Session => {
395                // Step 3: Otherwise:
396                // Step 3.1: Assert: type is "session".
397                // Step 3.2: Set shed to environment’s global object’s associated Document’s
398                // node navigable’s traversable navigable’s storage shed.
399                // Note: using the browsing context of the webview as the traversable navigable.
400                ensure_storage_shed(
401                    &storage_type,
402                    Some(Into::<BrowsingContextId>::into(webview).to_string()),
403                    &tx,
404                )?
405            },
406        };
407
408        // Step 4: Let shelf be the result of running obtain a storage shelf,
409        // with shed, environment, and type.
410        // Step 5: If shelf is failure, then return failure.
411        let bucket_id = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
412
413        // Step 6: Let bucket be shelf’s bucket map["default"].
414        // Done above with `bucket_id`.
415
416        let bottle_id: i64 = tx.query_row(
417            "SELECT id FROM bottles WHERE bucket_id = ?1 AND identifier = ?2;",
418            (bucket_id, storage_identifier.as_str()),
419            |row| row.get(0),
420        )?;
421
422        tx.commit()?;
423
424        // Step 7: Let bottle be bucket’s bottle map[identifier].
425        // Note: done with `bucket_id`.
426
427        // Step 8: Let proxyMap be a new storage proxy map whose backing map is bottle’s map.
428        // Step 9: Append proxyMap to bottle’s proxy map reference set.
429        // Note: not doing the reference set part for now, not sure what it is useful for.
430
431        // Step 10: Return proxyMap.
432        Ok(StorageProxyMap {
433            bottle_id,
434            handle: ClientStorageThreadHandle::new(sender.clone()),
435        })
436    }
437}
438
439pub trait ClientStorageThreadFactory {
440    fn new(config_dir: Option<PathBuf>) -> Self;
441}
442
443impl ClientStorageThreadFactory for ClientStorageThreadHandle {
444    fn new(config_dir: Option<PathBuf>) -> ClientStorageThreadHandle {
445        let (generic_sender, generic_receiver) = generic_channel::channel().unwrap();
446
447        let storage_dir = config_dir
448            .unwrap_or_else(|| {
449                let tmp_dir = tempfile::tempdir().unwrap();
450                tmp_dir.path().to_path_buf()
451            })
452            .join("clientstorage");
453        std::fs::create_dir_all(&storage_dir)
454            .expect("Failed to create ClientStorage storage directory");
455        let sender_clone = generic_sender.clone();
456        thread::Builder::new()
457            .name("ClientStorageThread".to_owned())
458            .spawn(move || {
459                let engine = SqliteEngine::new(storage_dir)
460                    .expect("Failed to initialize ClientStorage registry engine");
461                ClientStorageThread::new(sender_clone, generic_receiver, engine).start();
462            })
463            .expect("Thread spawning failed");
464
465        ClientStorageThreadHandle::new(generic_sender)
466    }
467}
468
469struct ClientStorageThread<E: RegistryEngine> {
470    receiver: GenericReceiver<ClientStorageThreadMessage>,
471    sender: GenericSender<ClientStorageThreadMessage>,
472    engine: E,
473}
474
475impl<E> ClientStorageThread<E>
476where
477    E: RegistryEngine,
478{
479    pub fn new(
480        sender: GenericSender<ClientStorageThreadMessage>,
481        receiver: GenericReceiver<ClientStorageThreadMessage>,
482        engine: E,
483    ) -> ClientStorageThread<E> {
484        ClientStorageThread {
485            sender,
486            receiver,
487            engine,
488        }
489    }
490
491    pub fn start(&mut self) {
492        while let Ok(message) = self.receiver.recv() {
493            match message {
494                ClientStorageThreadMessage::ObtainBottleMap {
495                    storage_type,
496                    storage_identifier,
497                    webview,
498                    origin,
499                    sender,
500                } => {
501                    let result = self.engine.obtain_a_storage_bottle_map(
502                        storage_type,
503                        webview,
504                        storage_identifier,
505                        origin,
506                        &self.sender,
507                    );
508                    let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
509                },
510                ClientStorageThreadMessage::CreateDatabase {
511                    bottle_id,
512                    name,
513                    sender,
514                } => {
515                    let result = self.engine.create_database(bottle_id, name);
516                    let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
517                },
518                ClientStorageThreadMessage::DeleteDatabase {
519                    bottle_id,
520                    name,
521                    sender,
522                } => {
523                    let result = self.engine.delete_database(bottle_id, name);
524                    let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
525                },
526                ClientStorageThreadMessage::Exit(sender) => {
527                    let _ = sender.send(());
528                    break;
529                },
530            }
531        }
532    }
533}