Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 1 | // Copyright 2020, The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | // TODO: Once this is stable, remove this and document everything public. |
| 16 | #![allow(missing_docs)] |
| 17 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 18 | use crate::error::Error as KsError; |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 19 | use anyhow::{Context, Result}; |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 20 | |
| 21 | use android_security_keystore2::aidl::android::security::keystore2::{ |
| 22 | Domain, Domain::Domain as DomainType, |
| 23 | }; |
| 24 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 25 | #[cfg(not(test))] |
| 26 | use rand::prelude::random; |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 27 | use rusqlite::{params, Connection, TransactionBehavior, NO_PARAMS}; |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 28 | use std::sync::Once; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 29 | #[cfg(test)] |
| 30 | use tests::random; |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 31 | |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 32 | static INIT_TABLES: Once = Once::new(); |
| 33 | |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 34 | pub struct KeystoreDB { |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 35 | conn: Connection, |
| 36 | } |
| 37 | |
| 38 | impl KeystoreDB { |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 39 | pub fn new() -> Result<Self> { |
| 40 | let conn = Self::make_connection("file:persistent.sqlite", "file:perboot.sqlite")?; |
| 41 | |
| 42 | INIT_TABLES.call_once(|| Self::init_tables(&conn).expect("Failed to initialize tables.")); |
| 43 | Ok(Self { conn }) |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 44 | } |
| 45 | |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 46 | fn init_tables(conn: &Connection) -> Result<()> { |
| 47 | conn.execute( |
| 48 | "CREATE TABLE IF NOT EXISTS persistent.keyentry ( |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 49 | id INTEGER UNIQUE, |
| 50 | creation_date DATETIME, |
| 51 | domain INTEGER, |
| 52 | namespace INTEGER, |
| 53 | alias TEXT);", |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 54 | NO_PARAMS, |
| 55 | ) |
| 56 | .context("Failed to initialize \"keyentry\" table.")?; |
| 57 | |
| 58 | conn.execute( |
| 59 | "CREATE TABLE IF NOT EXISTS persistent.keyparameter ( |
Hasini Gunasinghe | af99366 | 2020-07-24 18:40:20 +0000 | [diff] [blame] | 60 | keyentryid INTEGER, |
| 61 | tag INTEGER, |
| 62 | data ANY, |
| 63 | security_level INTEGER);", |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 64 | NO_PARAMS, |
| 65 | ) |
| 66 | .context("Failed to initialize \"keyparameter\" table.")?; |
| 67 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 68 | Ok(()) |
| 69 | } |
| 70 | |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 71 | fn make_connection(persistent_file: &str, perboot_file: &str) -> Result<Connection> { |
| 72 | let conn = |
| 73 | Connection::open_in_memory().context("Failed to initialize SQLite connection.")?; |
| 74 | |
| 75 | conn.execute("ATTACH DATABASE ? as persistent;", params![persistent_file]) |
| 76 | .context("Failed to attach database persistent.")?; |
| 77 | conn.execute("ATTACH DATABASE ? as perboot;", params![perboot_file]) |
| 78 | .context("Failed to attach database perboot.")?; |
| 79 | |
| 80 | Ok(conn) |
| 81 | } |
| 82 | |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 83 | pub fn create_key_entry(&self, domain: DomainType, namespace: i64) -> Result<i64> { |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 84 | match domain { |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 85 | Domain::App | Domain::SELinux => {} |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 86 | _ => { |
| 87 | return Err(KsError::sys()) |
| 88 | .context(format!("Domain {:?} must be either App or SELinux.", domain)); |
| 89 | } |
| 90 | } |
| 91 | // Loop until we get a unique id. |
| 92 | loop { |
| 93 | let newid: i64 = random(); |
| 94 | let ret = self.conn.execute( |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 95 | "INSERT into persistent.keyentry (id, creation_date, domain, namespace, alias) |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 96 | VALUES(?, datetime('now'), ?, ?, NULL);", |
| 97 | params![newid, domain as i64, namespace], |
| 98 | ); |
| 99 | match ret { |
| 100 | // If the id already existed, try again. |
| 101 | Err(rusqlite::Error::SqliteFailure( |
| 102 | libsqlite3_sys::Error { |
| 103 | code: libsqlite3_sys::ErrorCode::ConstraintViolation, |
| 104 | extended_code: libsqlite3_sys::SQLITE_CONSTRAINT_UNIQUE, |
| 105 | }, |
| 106 | _, |
| 107 | )) => (), |
| 108 | _ => return Ok(newid), |
| 109 | } |
| 110 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 111 | } |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 112 | |
| 113 | pub fn rebind_alias( |
| 114 | &mut self, |
| 115 | newid: u32, |
| 116 | alias: &str, |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 117 | domain: DomainType, |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 118 | namespace: i64, |
| 119 | ) -> Result<()> { |
| 120 | match domain { |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 121 | Domain::App | Domain::SELinux => {} |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 122 | _ => { |
| 123 | return Err(KsError::sys()) |
| 124 | .context(format!("Domain {:?} must be either App or SELinux.", domain)); |
| 125 | } |
| 126 | } |
| 127 | let tx = self |
| 128 | .conn |
| 129 | .transaction_with_behavior(TransactionBehavior::Immediate) |
| 130 | .context("Failed to initialize transaction.")?; |
| 131 | tx.execute( |
| 132 | "UPDATE persistent.keyentry |
| 133 | SET alias = NULL, domain = NULL, namespace = NULL |
| 134 | WHERE alias = ? AND domain = ? AND namespace = ?;", |
| 135 | params![alias, domain as i64, namespace], |
| 136 | ) |
| 137 | .context("Failed to rebind existing entry.")?; |
| 138 | let result = tx |
| 139 | .execute( |
| 140 | "UPDATE persistent.keyentry |
| 141 | SET alias = ? |
| 142 | WHERE id = ? AND domain = ? AND namespace = ?;", |
| 143 | params![alias, newid, domain as i64, namespace], |
| 144 | ) |
| 145 | .context("Failed to set alias.")?; |
| 146 | if result != 1 { |
| 147 | // Note that this explicit rollback is not required, as |
| 148 | // the transaction should rollback if we do not commit it. |
| 149 | // We leave it here for readability. |
| 150 | tx.rollback().context("Failed to rollback a failed transaction.")?; |
| 151 | return Err(KsError::sys()).context(format!( |
| 152 | "Expected to update a single entry but instead updated {}.", |
| 153 | result |
| 154 | )); |
| 155 | } |
| 156 | tx.commit().context("Failed to commit transaction.") |
| 157 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 158 | } |
| 159 | |
| 160 | #[cfg(test)] |
| 161 | mod tests { |
| 162 | |
| 163 | use super::*; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 164 | use std::cell::RefCell; |
| 165 | |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 166 | static PERSISTENT_TEST_SQL: &str = "/data/local/tmp/persistent.sqlite"; |
| 167 | static PERBOOT_TEST_SQL: &str = "/data/local/tmp/perboot.sqlite"; |
| 168 | |
| 169 | fn new_test_db() -> Result<KeystoreDB> { |
| 170 | let conn = KeystoreDB::make_connection("file::memory:", "file::memory:")?; |
| 171 | |
| 172 | KeystoreDB::init_tables(&conn).context("Failed to initialize tables.")?; |
| 173 | Ok(KeystoreDB { conn }) |
| 174 | } |
| 175 | |
| 176 | fn new_test_db_with_persistent_file() -> Result<KeystoreDB> { |
| 177 | let conn = KeystoreDB::make_connection(PERSISTENT_TEST_SQL, PERBOOT_TEST_SQL)?; |
| 178 | |
| 179 | KeystoreDB::init_tables(&conn).context("Failed to initialize tables.")?; |
| 180 | Ok(KeystoreDB { conn }) |
| 181 | } |
| 182 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 183 | // Ensure that we're using the "injected" random function, not the real one. |
| 184 | #[test] |
| 185 | fn test_mocked_random() { |
| 186 | let rand1 = random(); |
| 187 | let rand2 = random(); |
| 188 | let rand3 = random(); |
| 189 | if rand1 == rand2 { |
| 190 | assert_eq!(rand2 + 1, rand3); |
| 191 | } else { |
| 192 | assert_eq!(rand1 + 1, rand2); |
| 193 | assert_eq!(rand2, rand3); |
| 194 | } |
| 195 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 196 | |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 197 | // Test that we have the correct tables. |
| 198 | #[test] |
| 199 | fn test_tables() -> Result<()> { |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 200 | let db = new_test_db()?; |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 201 | let tables = db |
| 202 | .conn |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 203 | .prepare("SELECT name from persistent.sqlite_master WHERE type='table' ORDER BY name;")? |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 204 | .query_map(params![], |row| row.get(0))? |
| 205 | .collect::<rusqlite::Result<Vec<String>>>()?; |
Hasini Gunasinghe | af99366 | 2020-07-24 18:40:20 +0000 | [diff] [blame] | 206 | assert_eq!(tables.len(), 2); |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 207 | assert_eq!(tables[0], "keyentry"); |
Hasini Gunasinghe | af99366 | 2020-07-24 18:40:20 +0000 | [diff] [blame] | 208 | assert_eq!(tables[1], "keyparameter"); |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 209 | Ok(()) |
| 210 | } |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 211 | |
| 212 | #[test] |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 213 | fn test_no_persistence_for_tests() -> Result<()> { |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 214 | let db = new_test_db()?; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 215 | |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 216 | db.create_key_entry(Domain::App, 100)?; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 217 | let entries = get_keyentry(&db)?; |
| 218 | assert_eq!(entries.len(), 1); |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 219 | let db = new_test_db()?; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 220 | |
| 221 | let entries = get_keyentry(&db)?; |
| 222 | assert_eq!(entries.len(), 0); |
| 223 | Ok(()) |
| 224 | } |
| 225 | |
| 226 | #[test] |
| 227 | fn test_persistence_for_files() -> Result<()> { |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 228 | let _file_guard_persistent = TempFile { filename: PERSISTENT_TEST_SQL }; |
| 229 | let _file_guard_perboot = TempFile { filename: PERBOOT_TEST_SQL }; |
| 230 | let db = new_test_db_with_persistent_file()?; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 231 | |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 232 | db.create_key_entry(Domain::App, 100)?; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 233 | let entries = get_keyentry(&db)?; |
| 234 | assert_eq!(entries.len(), 1); |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 235 | let db = new_test_db_with_persistent_file()?; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 236 | |
| 237 | let entries_new = get_keyentry(&db)?; |
| 238 | assert_eq!(entries, entries_new); |
| 239 | Ok(()) |
| 240 | } |
| 241 | |
| 242 | #[test] |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 243 | fn test_create_key_entry() -> Result<()> { |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 244 | fn extractor(ke: &KeyEntryRow) -> (DomainType, i64, Option<&str>) { |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 245 | (ke.domain.unwrap(), ke.namespace.unwrap(), ke.alias.as_deref()) |
| 246 | } |
| 247 | |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 248 | let db = new_test_db()?; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 249 | |
| 250 | db.create_key_entry(Domain::App, 100)?; |
| 251 | db.create_key_entry(Domain::SELinux, 101)?; |
| 252 | |
| 253 | let entries = get_keyentry(&db)?; |
| 254 | assert_eq!(entries.len(), 2); |
| 255 | assert_eq!(extractor(&entries[0]), (Domain::App, 100, None)); |
| 256 | assert_eq!(extractor(&entries[1]), (Domain::SELinux, 101, None)); |
| 257 | |
| 258 | // Test that we must pass in a valid Domain. |
| 259 | check_result_is_error_containing_string( |
| 260 | db.create_key_entry(Domain::Grant, 102), |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 261 | "Domain 1 must be either App or SELinux.", |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 262 | ); |
| 263 | check_result_is_error_containing_string( |
| 264 | db.create_key_entry(Domain::Blob, 103), |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 265 | "Domain 3 must be either App or SELinux.", |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 266 | ); |
| 267 | check_result_is_error_containing_string( |
| 268 | db.create_key_entry(Domain::KeyId, 104), |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 269 | "Domain 4 must be either App or SELinux.", |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 270 | ); |
| 271 | |
| 272 | Ok(()) |
| 273 | } |
| 274 | |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 275 | #[test] |
| 276 | fn test_rebind_alias() -> Result<()> { |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 277 | fn extractor(ke: &KeyEntryRow) -> (Option<DomainType>, Option<i64>, Option<&str>) { |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 278 | (ke.domain, ke.namespace, ke.alias.as_deref()) |
| 279 | } |
| 280 | |
Janis Danisevskis | 4df44f4 | 2020-08-26 14:40:03 -0700 | [diff] [blame] | 281 | let mut db = new_test_db()?; |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 282 | db.create_key_entry(Domain::App, 42)?; |
| 283 | db.create_key_entry(Domain::App, 42)?; |
| 284 | let entries = get_keyentry(&db)?; |
| 285 | assert_eq!(entries.len(), 2); |
| 286 | assert_eq!(extractor(&entries[0]), (Some(Domain::App), Some(42), None)); |
| 287 | assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), None)); |
| 288 | |
| 289 | // Test that the first call to rebind_alias sets the alias. |
| 290 | db.rebind_alias(entries[0].id, "foo", Domain::App, 42)?; |
| 291 | let entries = get_keyentry(&db)?; |
| 292 | assert_eq!(entries.len(), 2); |
| 293 | assert_eq!(extractor(&entries[0]), (Some(Domain::App), Some(42), Some("foo"))); |
| 294 | assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), None)); |
| 295 | |
| 296 | // Test that the second call to rebind_alias also empties the old one. |
| 297 | db.rebind_alias(entries[1].id, "foo", Domain::App, 42)?; |
| 298 | let entries = get_keyentry(&db)?; |
| 299 | assert_eq!(entries.len(), 2); |
| 300 | assert_eq!(extractor(&entries[0]), (None, None, None)); |
| 301 | assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), Some("foo"))); |
| 302 | |
| 303 | // Test that we must pass in a valid Domain. |
| 304 | check_result_is_error_containing_string( |
| 305 | db.rebind_alias(0, "foo", Domain::Grant, 42), |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 306 | "Domain 1 must be either App or SELinux.", |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 307 | ); |
| 308 | check_result_is_error_containing_string( |
| 309 | db.rebind_alias(0, "foo", Domain::Blob, 42), |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 310 | "Domain 3 must be either App or SELinux.", |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 311 | ); |
| 312 | check_result_is_error_containing_string( |
| 313 | db.rebind_alias(0, "foo", Domain::KeyId, 42), |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 314 | "Domain 4 must be either App or SELinux.", |
Joel Galenson | 33c04ad | 2020-08-03 11:04:38 -0700 | [diff] [blame] | 315 | ); |
| 316 | |
| 317 | // Test that we correctly handle setting an alias for something that does not exist. |
| 318 | check_result_is_error_containing_string( |
| 319 | db.rebind_alias(0, "foo", Domain::SELinux, 42), |
| 320 | "Expected to update a single entry but instead updated 0", |
| 321 | ); |
| 322 | // Test that we correctly abort the transaction in this case. |
| 323 | let entries = get_keyentry(&db)?; |
| 324 | assert_eq!(entries.len(), 2); |
| 325 | assert_eq!(extractor(&entries[0]), (None, None, None)); |
| 326 | assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), Some("foo"))); |
| 327 | |
| 328 | Ok(()) |
| 329 | } |
| 330 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 331 | // Helpers |
| 332 | |
| 333 | // Checks that the given result is an error containing the given string. |
| 334 | fn check_result_is_error_containing_string<T>(result: Result<T>, target: &str) { |
| 335 | let error_str = format!( |
| 336 | "{:#?}", |
| 337 | result.err().unwrap_or_else(|| panic!("Expected the error: {}", target)) |
| 338 | ); |
| 339 | assert!( |
| 340 | error_str.contains(target), |
| 341 | "The string \"{}\" should contain \"{}\"", |
| 342 | error_str, |
| 343 | target |
| 344 | ); |
| 345 | } |
| 346 | |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 347 | #[derive(Debug, PartialEq)] |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 348 | #[allow(dead_code)] |
| 349 | struct KeyEntryRow { |
| 350 | id: u32, |
| 351 | creation_date: String, |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 352 | domain: Option<DomainType>, |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 353 | namespace: Option<i64>, |
| 354 | alias: Option<String>, |
| 355 | } |
| 356 | |
| 357 | fn get_keyentry(db: &KeystoreDB) -> Result<Vec<KeyEntryRow>> { |
| 358 | db.conn |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 359 | .prepare("SELECT * FROM persistent.keyentry;")? |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 360 | .query_map(NO_PARAMS, |row| { |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 361 | Ok(KeyEntryRow { |
| 362 | id: row.get(0)?, |
| 363 | creation_date: row.get(1)?, |
Janis Danisevskis | 60400fe | 2020-08-26 15:24:42 -0700 | [diff] [blame] | 364 | domain: row.get(2)?, |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 365 | namespace: row.get(3)?, |
| 366 | alias: row.get(4)?, |
| 367 | }) |
| 368 | })? |
| 369 | .map(|r| r.context("Could not read keyentry row.")) |
| 370 | .collect::<Result<Vec<_>>>() |
| 371 | } |
| 372 | |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame] | 373 | // A class that deletes a file when it is dropped. |
| 374 | // TODO: If we ever add a crate that does this, we can use it instead. |
| 375 | struct TempFile { |
| 376 | filename: &'static str, |
| 377 | } |
| 378 | |
| 379 | impl Drop for TempFile { |
| 380 | fn drop(&mut self) { |
| 381 | std::fs::remove_file(self.filename).expect("Cannot delete temporary file"); |
| 382 | } |
| 383 | } |
| 384 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 385 | // Use a custom random number generator that repeats each number once. |
| 386 | // This allows us to test repeated elements. |
| 387 | |
| 388 | thread_local! { |
| 389 | static RANDOM_COUNTER: RefCell<i64> = RefCell::new(0); |
| 390 | } |
| 391 | |
| 392 | pub fn random() -> i64 { |
| 393 | RANDOM_COUNTER.with(|counter| { |
| 394 | let result = *counter.borrow() / 2; |
| 395 | *counter.borrow_mut() += 1; |
| 396 | result |
| 397 | }) |
| 398 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 399 | } |