parent
c0d4568970
commit
6a1a265894
14 changed files with 367 additions and 56 deletions
|
@ -18,6 +18,7 @@ build = "build/version.rs"
|
||||||
[features]
|
[features]
|
||||||
default = ["bundled_sqlite3"]
|
default = ["bundled_sqlite3"]
|
||||||
bundled_sqlite3 = ["rusqlite/bundled"]
|
bundled_sqlite3 = ["rusqlite/bundled"]
|
||||||
|
sqlcipher = ["rusqlite/sqlcipher", "mentat_db/sqlcipher"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["tools/cli", "ffi"]
|
members = ["tools/cli", "ffi"]
|
||||||
|
|
|
@ -3,6 +3,10 @@ name = "mentat_db"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
workspace = ".."
|
workspace = ".."
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
sqlcipher = ["rusqlite/sqlcipher"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" }
|
error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" }
|
||||||
indexmap = "1"
|
indexmap = "1"
|
||||||
|
|
117
db/src/db.rs
117
db/src/db.rs
|
@ -71,30 +71,72 @@ use watcher::{
|
||||||
NullWatcher,
|
NullWatcher,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn new_connection<T>(uri: T) -> rusqlite::Result<rusqlite::Connection> where T: AsRef<Path> {
|
// In PRAGMA foo='bar', `'bar'` must be a constant string (it cannot be a
|
||||||
let conn = match uri.as_ref().to_string_lossy().len() {
|
// bound parameter), so we need to escape manually. According to
|
||||||
|
// https://www.sqlite.org/faq.html, the only character that must be escaped is
|
||||||
|
// the single quote, which is escaped by placing two single quotes in a row.
|
||||||
|
fn escape_string_for_pragma(s: &str) -> String {
|
||||||
|
s.replace("'", "''")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_connection(uri: &Path, maybe_encryption_key: Option<&str>) -> rusqlite::Result<rusqlite::Connection> {
|
||||||
|
let conn = match uri.to_string_lossy().len() {
|
||||||
0 => rusqlite::Connection::open_in_memory()?,
|
0 => rusqlite::Connection::open_in_memory()?,
|
||||||
_ => rusqlite::Connection::open(uri)?,
|
_ => rusqlite::Connection::open(uri)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let page_size = 32768;
|
||||||
|
|
||||||
|
let initial_pragmas = if let Some(encryption_key) = maybe_encryption_key {
|
||||||
|
assert!(cfg!(feature = "sqlcipher"),
|
||||||
|
"This function shouldn't be called with a key unless we have sqlcipher support");
|
||||||
|
// Important: The `cipher_page_size` cannot be changed without breaking
|
||||||
|
// the ability to open databases that were written when using a
|
||||||
|
// different `cipher_page_size`. Additionally, it (AFAICT) must be a
|
||||||
|
// positive multiple of `page_size`. We use the same value for both here.
|
||||||
|
format!("
|
||||||
|
PRAGMA key='{}';
|
||||||
|
PRAGMA cipher_page_size={};
|
||||||
|
", escape_string_for_pragma(encryption_key), page_size)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
// See https://github.com/mozilla/mentat/issues/505 for details on temp_store
|
// See https://github.com/mozilla/mentat/issues/505 for details on temp_store
|
||||||
// pragma and how it might interact together with consumers such as Firefox.
|
// pragma and how it might interact together with consumers such as Firefox.
|
||||||
// temp_store=2 is currently present to force SQLite to store temp files in memory.
|
// temp_store=2 is currently present to force SQLite to store temp files in memory.
|
||||||
// Some of the platforms we support do not have a tmp partition (e.g. Android)
|
// Some of the platforms we support do not have a tmp partition (e.g. Android)
|
||||||
// necessary to store temp files on disk. Ideally, consumers should be able to
|
// necessary to store temp files on disk. Ideally, consumers should be able to
|
||||||
// override this behaviour (see issue 505).
|
// override this behaviour (see issue 505).
|
||||||
conn.execute_batch("
|
conn.execute_batch(&format!("
|
||||||
PRAGMA page_size=32768;
|
{}
|
||||||
PRAGMA journal_mode=wal;
|
PRAGMA journal_mode=wal;
|
||||||
PRAGMA wal_autocheckpoint=32;
|
PRAGMA wal_autocheckpoint=32;
|
||||||
PRAGMA journal_size_limit=3145728;
|
PRAGMA journal_size_limit=3145728;
|
||||||
PRAGMA foreign_keys=ON;
|
PRAGMA foreign_keys=ON;
|
||||||
PRAGMA temp_store=2;
|
PRAGMA temp_store=2;
|
||||||
")?;
|
", initial_pragmas))?;
|
||||||
|
|
||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_connection<T>(uri: T) -> rusqlite::Result<rusqlite::Connection> where T: AsRef<Path> {
|
||||||
|
make_connection(uri.as_ref(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
pub fn new_connection_with_key(uri: impl AsRef<Path>, encryption_key: impl AsRef<str>) -> rusqlite::Result<rusqlite::Connection> {
|
||||||
|
make_connection(uri.as_ref(), Some(encryption_key.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
pub fn change_encryption_key(conn: &rusqlite::Connection, encryption_key: impl AsRef<str>) -> rusqlite::Result<()> {
|
||||||
|
let escaped = escape_string_for_pragma(encryption_key.as_ref());
|
||||||
|
// `conn.execute` complains that this returns a result, and using a query
|
||||||
|
// for it requires more boilerplate.
|
||||||
|
conn.execute_batch(&format!("PRAGMA rekey = '{}';", escaped))
|
||||||
|
}
|
||||||
|
|
||||||
/// Version history:
|
/// Version history:
|
||||||
///
|
///
|
||||||
/// 1: initial Rust Mentat schema.
|
/// 1: initial Rust Mentat schema.
|
||||||
|
@ -1228,11 +1270,8 @@ mod tests {
|
||||||
fn fulltext_values(&self) -> edn::Value {
|
fn fulltext_values(&self) -> edn::Value {
|
||||||
debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn()
|
debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TestConn {
|
fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn {
|
||||||
fn default() -> TestConn {
|
|
||||||
let mut conn = new_connection("").expect("Couldn't open in-memory db");
|
|
||||||
let db = ensure_current_version(&mut conn).unwrap();
|
let db = ensure_current_version(&mut conn).unwrap();
|
||||||
|
|
||||||
// Does not include :db/txInstant.
|
// Does not include :db/txInstant.
|
||||||
|
@ -1266,6 +1305,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for TestConn {
|
||||||
|
fn default() -> TestConn {
|
||||||
|
TestConn::with_sqlite(new_connection("").expect("Couldn't open in-memory db"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn tempids(report: &TxReport) -> edn::Value {
|
fn tempids(report: &TxReport) -> edn::Value {
|
||||||
let mut map: BTreeMap<edn::Value, edn::Value> = BTreeMap::default();
|
let mut map: BTreeMap<edn::Value, edn::Value> = BTreeMap::default();
|
||||||
for (tempid, &entid) in report.tempids.iter() {
|
for (tempid, &entid) in report.tempids.iter() {
|
||||||
|
@ -1274,10 +1319,7 @@ mod tests {
|
||||||
edn::Value::Map(map)
|
edn::Value::Map(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn run_test_add(mut conn: TestConn) {
|
||||||
fn test_add() {
|
|
||||||
let mut conn = TestConn::default();
|
|
||||||
|
|
||||||
// Test inserting :db.cardinality/one elements.
|
// Test inserting :db.cardinality/one elements.
|
||||||
assert_transact!(conn, "[[:db/add 100 :db.schema/version 1]
|
assert_transact!(conn, "[[:db/add 100 :db.schema/version 1]
|
||||||
[:db/add 101 :db.schema/version 2]]");
|
[:db/add 101 :db.schema/version 2]]");
|
||||||
|
@ -1342,6 +1384,11 @@ mod tests {
|
||||||
[200 :db.schema/attribute 101]]");
|
[200 :db.schema/attribute 101]]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
run_test_add(TestConn::default());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tx_assertions() {
|
fn test_tx_assertions() {
|
||||||
let mut conn = TestConn::default();
|
let mut conn = TestConn::default();
|
||||||
|
@ -2694,4 +2741,48 @@ mod tests {
|
||||||
]"#,
|
]"#,
|
||||||
Err("schema constraint violation: cardinality conflicts:\n AddRetractConflict { e: 100, a: 200, vs: {Long(7)} }\n AddRetractConflict { e: 100, a: 201, vs: {Long(8)} }\n"));
|
Err("schema constraint violation: cardinality conflicts:\n AddRetractConflict { e: 100, a: 200, vs: {Long(7)} }\n AddRetractConflict { e: 100, a: 201, vs: {Long(8)} }\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
fn test_sqlcipher_openable() {
|
||||||
|
let secret_key = "key";
|
||||||
|
let sqlite = new_connection_with_key("../fixtures/v1encrypted.db", secret_key).expect("Failed to find test DB");
|
||||||
|
sqlite.query_row("SELECT COUNT(*) FROM sqlite_master", &[], |row| row.get::<_, i64>(0))
|
||||||
|
.expect("Failed to execute sql query on encrypted DB");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
fn test_open_fail(opener: impl FnOnce() -> rusqlite::Result<rusqlite::Connection>) {
|
||||||
|
let err = opener().expect_err("Should fail to open encrypted DB");
|
||||||
|
match err {
|
||||||
|
rusqlite::Error::SqliteFailure(err, ..) => {
|
||||||
|
assert_eq!(err.extended_code, 26, "Should get error code 26 (not a database).");
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
panic!("Wrong error type! {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
fn test_sqlcipher_requires_key() {
|
||||||
|
// Don't use a key.
|
||||||
|
test_open_fail(|| new_connection("../fixtures/v1encrypted.db"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
fn test_sqlcipher_requires_correct_key() {
|
||||||
|
// Use a key, but the wrong one.
|
||||||
|
test_open_fail(|| new_connection_with_key("../fixtures/v1encrypted.db", "wrong key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
fn test_sqlcipher_some_transactions() {
|
||||||
|
let sqlite = new_connection_with_key("", "hunter2").expect("Failed to create encrypted connection");
|
||||||
|
// Run a basic test as a sanity check.
|
||||||
|
run_test_add(TestConn::with_sqlite(sqlite));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,12 @@ pub use db::{
|
||||||
new_connection,
|
new_connection,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
pub use db::{
|
||||||
|
new_connection_with_key,
|
||||||
|
change_encryption_key,
|
||||||
|
};
|
||||||
|
|
||||||
pub use watcher::{
|
pub use watcher::{
|
||||||
TransactWatcher,
|
TransactWatcher,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,14 @@ authors = ["Emily Toop <etoop@mozilla.com>"]
|
||||||
name = "mentat_ffi"
|
name = "mentat_ffi"
|
||||||
crate-type = ["lib", "staticlib", "cdylib"]
|
crate-type = ["lib", "staticlib", "cdylib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["bundled_sqlite3"]
|
||||||
|
sqlcipher = ["mentat/sqlcipher"]
|
||||||
|
bundled_sqlite3 = ["mentat/bundled_sqlite3"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
[dependencies.mentat]
|
[dependencies.mentat]
|
||||||
path = "../"
|
path = "../"
|
||||||
|
default-features = false
|
||||||
|
|
|
@ -248,6 +248,16 @@ pub extern "C" fn store_open(uri: *const c_char) -> *mut Store {
|
||||||
Box::into_raw(Box::new(store))
|
Box::into_raw(Box::new(store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Variant of store_open that opens an encrypted database.
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn store_open_encrypted(uri: *const c_char, key: *const c_char) -> *mut Store {
|
||||||
|
let uri = c_char_to_string(uri);
|
||||||
|
let key = c_char_to_string(key);
|
||||||
|
let store = Store::open_with_key(&uri, &key).expect("expected a store");
|
||||||
|
Box::into_raw(Box::new(store))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: open empty
|
// TODO: open empty
|
||||||
|
|
||||||
// TODO: dismantle
|
// TODO: dismantle
|
||||||
|
|
BIN
fixtures/v1encrypted.db
Normal file
BIN
fixtures/v1encrypted.db
Normal file
Binary file not shown.
42
src/conn.rs
42
src/conn.rs
|
@ -206,6 +206,48 @@ impl Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
impl Store {
|
||||||
|
/// Variant of `open` that allows a key (for encryption/decryption) to be
|
||||||
|
/// supplied. Fails unless linked against sqlcipher (or something else that
|
||||||
|
/// supports the Sqlite Encryption Extension).
|
||||||
|
pub fn open_with_key(path: &str, encryption_key: &str) -> Result<Store> {
|
||||||
|
let mut connection = ::new_connection_with_key(path, encryption_key)?;
|
||||||
|
let conn = Conn::connect(&mut connection)?;
|
||||||
|
Ok(Store {
|
||||||
|
conn: conn,
|
||||||
|
sqlite: connection,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variant of `open_empty` that allows a key (for encryption/decryption) to
|
||||||
|
/// be supplied. Fails unless linked against sqlcipher (or something else
|
||||||
|
/// that supports the Sqlite Encryption Extension).
|
||||||
|
pub fn open_empty_with_key(path: &str, encryption_key: &str) -> Result<Store> {
|
||||||
|
if !path.is_empty() {
|
||||||
|
if Path::new(path).exists() {
|
||||||
|
bail!(ErrorKind::PathAlreadyExists(path.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut connection = ::new_connection_with_key(path, encryption_key)?;
|
||||||
|
let conn = Conn::empty(&mut connection)?;
|
||||||
|
Ok(Store {
|
||||||
|
conn: conn,
|
||||||
|
sqlite: connection,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the key for a database that was opened using `open_with_key` or
|
||||||
|
/// `open_empty_with_key` (using `PRAGMA rekey`). Fails unless linked
|
||||||
|
/// against sqlcipher (or something else that supports the Sqlite Encryption
|
||||||
|
/// Extension).
|
||||||
|
pub fn change_encryption_key(&mut self, new_encryption_key: &str) -> Result<()> {
|
||||||
|
::change_encryption_key(&self.sqlite, new_encryption_key)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Queryable {
|
pub trait Queryable {
|
||||||
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
|
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
|
||||||
where T: Into<Option<QueryInputs>>;
|
where T: Into<Option<QueryInputs>>;
|
||||||
|
|
|
@ -59,6 +59,12 @@ pub use mentat_db::{
|
||||||
new_connection,
|
new_connection,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
pub use mentat_db::{
|
||||||
|
new_connection_with_key,
|
||||||
|
change_encryption_key,
|
||||||
|
};
|
||||||
|
|
||||||
/// Produce the appropriate `Variable` for the provided valid ?-prefixed name.
|
/// Produce the appropriate `Variable` for the provided valid ?-prefixed name.
|
||||||
/// This lives here because we can't re-export macros:
|
/// This lives here because we can't re-export macros:
|
||||||
/// https://github.com/rust-lang/rust/issues/29638.
|
/// https://github.com/rust-lang/rust/issues/29638.
|
||||||
|
|
|
@ -1474,10 +1474,7 @@ fn test_tx_ids() {
|
||||||
assert_tx_id_range(&store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)]);
|
assert_tx_id_range(&store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn run_tx_data_test(mut store: Store) {
|
||||||
fn test_tx_data() {
|
|
||||||
let mut store = Store::open("").expect("opened");
|
|
||||||
|
|
||||||
store.transact(r#"[
|
store.transact(r#"[
|
||||||
[:db/add "a" :db/ident :foo/term]
|
[:db/add "a" :db/ident :foo/term]
|
||||||
[:db/add "a" :db/valueType :db.type/string]
|
[:db/add "a" :db/valueType :db.type/string]
|
||||||
|
@ -1534,3 +1531,16 @@ fn test_tx_data() {
|
||||||
assert_tx_data(&store, &tx1, "1".into());
|
assert_tx_data(&store, &tx1, "1".into());
|
||||||
assert_tx_data(&store, &tx2, "2".into());
|
assert_tx_data(&store, &tx2, "2".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tx_data() {
|
||||||
|
run_tx_data_test(Store::open("").expect("opened"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
#[test]
|
||||||
|
fn test_encrypted() {
|
||||||
|
// We expect this to blow up completely if something is wrong with the encryption,
|
||||||
|
// so the specific test we use doesn't matter that much.
|
||||||
|
run_tx_data_test(Store::open_with_key("", "secret").expect("opened"));
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
name = "mentat_cli"
|
name = "mentat_cli"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
||||||
|
# Forward mentat's features.
|
||||||
|
[features]
|
||||||
|
default = ["bundled_sqlite3"]
|
||||||
|
sqlcipher = ["mentat/sqlcipher"]
|
||||||
|
bundled_sqlite3 = ["mentat/bundled_sqlite3"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "mentat_cli"
|
name = "mentat_cli"
|
||||||
path = "src/mentat_cli/lib.rs"
|
path = "src/mentat_cli/lib.rs"
|
||||||
|
@ -30,6 +36,7 @@ features = ["limits"]
|
||||||
|
|
||||||
[dependencies.mentat]
|
[dependencies.mentat]
|
||||||
path = "../.."
|
path = "../.."
|
||||||
|
default-features = false
|
||||||
|
|
||||||
[dependencies.mentat_parser_utils]
|
[dependencies.mentat_parser_utils]
|
||||||
path = "../../parser-utils"
|
path = "../../parser-utils"
|
||||||
|
|
|
@ -47,6 +47,8 @@ pub static COMMAND_IMPORT_LONG: &'static str = &"import";
|
||||||
pub static COMMAND_IMPORT_SHORT: &'static str = &"i";
|
pub static COMMAND_IMPORT_SHORT: &'static str = &"i";
|
||||||
pub static COMMAND_OPEN: &'static str = &"open";
|
pub static COMMAND_OPEN: &'static str = &"open";
|
||||||
pub static COMMAND_OPEN_EMPTY: &'static str = &"empty";
|
pub static COMMAND_OPEN_EMPTY: &'static str = &"empty";
|
||||||
|
pub static COMMAND_OPEN_ENCRYPTED: &'static str = &"open_encrypted";
|
||||||
|
pub static COMMAND_OPEN_EMPTY_ENCRYPTED: &'static str = &"empty_encrypted";
|
||||||
pub static COMMAND_QUERY_LONG: &'static str = &"query";
|
pub static COMMAND_QUERY_LONG: &'static str = &"query";
|
||||||
pub static COMMAND_QUERY_SHORT: &'static str = &"q";
|
pub static COMMAND_QUERY_SHORT: &'static str = &"q";
|
||||||
pub static COMMAND_QUERY_EXPLAIN_LONG: &'static str = &"explain_query";
|
pub static COMMAND_QUERY_EXPLAIN_LONG: &'static str = &"explain_query";
|
||||||
|
@ -67,6 +69,8 @@ pub enum Command {
|
||||||
Import(String),
|
Import(String),
|
||||||
Open(String),
|
Open(String),
|
||||||
OpenEmpty(String),
|
OpenEmpty(String),
|
||||||
|
OpenEncrypted(String, String),
|
||||||
|
OpenEmptyEncrypted(String, String),
|
||||||
Query(String),
|
Query(String),
|
||||||
QueryExplain(String),
|
QueryExplain(String),
|
||||||
QueryPrepared(String),
|
QueryPrepared(String),
|
||||||
|
@ -97,6 +101,8 @@ impl Command {
|
||||||
&Command::Import(_) |
|
&Command::Import(_) |
|
||||||
&Command::Open(_) |
|
&Command::Open(_) |
|
||||||
&Command::OpenEmpty(_) |
|
&Command::OpenEmpty(_) |
|
||||||
|
&Command::OpenEncrypted(_, _) |
|
||||||
|
&Command::OpenEmptyEncrypted(_, _) |
|
||||||
&Command::Timer(_) |
|
&Command::Timer(_) |
|
||||||
&Command::Schema |
|
&Command::Schema |
|
||||||
&Command::Sync(_)
|
&Command::Sync(_)
|
||||||
|
@ -118,6 +124,8 @@ impl Command {
|
||||||
&Command::Help(_) |
|
&Command::Help(_) |
|
||||||
&Command::Open(_) |
|
&Command::Open(_) |
|
||||||
&Command::OpenEmpty(_) |
|
&Command::OpenEmpty(_) |
|
||||||
|
&Command::OpenEncrypted(_, _) |
|
||||||
|
&Command::OpenEmptyEncrypted(_, _) |
|
||||||
&Command::QueryExplain(_) |
|
&Command::QueryExplain(_) |
|
||||||
&Command::Timer(_) |
|
&Command::Timer(_) |
|
||||||
&Command::Schema |
|
&Command::Schema |
|
||||||
|
@ -149,6 +157,12 @@ impl Command {
|
||||||
&Command::OpenEmpty(ref args) => {
|
&Command::OpenEmpty(ref args) => {
|
||||||
format!(".{} {}", COMMAND_OPEN_EMPTY, args)
|
format!(".{} {}", COMMAND_OPEN_EMPTY, args)
|
||||||
},
|
},
|
||||||
|
&Command::OpenEncrypted(ref db, ref key) => {
|
||||||
|
format!(".{} {} {}", COMMAND_OPEN_ENCRYPTED, db, key)
|
||||||
|
},
|
||||||
|
&Command::OpenEmptyEncrypted(ref db, ref key) => {
|
||||||
|
format!(".{} {} {}", COMMAND_OPEN_EMPTY_ENCRYPTED, db, key)
|
||||||
|
},
|
||||||
&Command::Query(ref args) => {
|
&Command::Query(ref args) => {
|
||||||
format!(".{} {}", COMMAND_QUERY_LONG, args)
|
format!(".{} {}", COMMAND_QUERY_LONG, args)
|
||||||
},
|
},
|
||||||
|
@ -197,6 +211,20 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
||||||
.skip(spaces())
|
.skip(spaces())
|
||||||
.skip(eof());
|
.skip(eof());
|
||||||
|
|
||||||
|
let opener = |command, num_args| {
|
||||||
|
string(command)
|
||||||
|
.with(spaces())
|
||||||
|
.with(arguments())
|
||||||
|
.map(move |args| {
|
||||||
|
if args.len() < num_args {
|
||||||
|
bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string()));
|
||||||
|
}
|
||||||
|
if args.len() > num_args {
|
||||||
|
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[num_args])));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// Commands.
|
// Commands.
|
||||||
let cache_parser = string(COMMAND_CACHE)
|
let cache_parser = string(COMMAND_CACHE)
|
||||||
|
@ -246,31 +274,17 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
||||||
Ok(Command::Import(x))
|
Ok(Command::Import(x))
|
||||||
});
|
});
|
||||||
|
|
||||||
let open_parser = string(COMMAND_OPEN)
|
let open_parser = opener(COMMAND_OPEN, 1).map(|args_res|
|
||||||
.with(spaces())
|
args_res.map(|args| Command::Open(args[0].clone())));
|
||||||
.with(arguments())
|
|
||||||
.map(|args| {
|
|
||||||
if args.len() < 1 {
|
|
||||||
bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string()));
|
|
||||||
}
|
|
||||||
if args.len() > 1 {
|
|
||||||
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])));
|
|
||||||
}
|
|
||||||
Ok(Command::Open(args[0].clone()))
|
|
||||||
});
|
|
||||||
|
|
||||||
let open_empty_parser = string(COMMAND_OPEN_EMPTY)
|
let open_empty_parser = opener(COMMAND_OPEN_EMPTY, 1).map(|args_res|
|
||||||
.with(spaces())
|
args_res.map(|args| Command::OpenEmpty(args[0].clone())));
|
||||||
.with(arguments())
|
|
||||||
.map(|args| {
|
let open_encrypted_parser = opener(COMMAND_OPEN_ENCRYPTED, 2).map(|args_res|
|
||||||
if args.len() < 1 {
|
args_res.map(|args| Command::OpenEncrypted(args[0].clone(), args[1].clone())));
|
||||||
bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string()));
|
|
||||||
}
|
let open_empty_encrypted_parser = opener(COMMAND_OPEN_EMPTY_ENCRYPTED, 2).map(|args_res|
|
||||||
if args.len() > 1 {
|
args_res.map(|args| Command::OpenEmptyEncrypted(args[0].clone(), args[1].clone())));
|
||||||
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])));
|
|
||||||
}
|
|
||||||
Ok(Command::OpenEmpty(args[0].clone()))
|
|
||||||
});
|
|
||||||
|
|
||||||
let query_parser = try(string(COMMAND_QUERY_LONG)).or(try(string(COMMAND_QUERY_SHORT)))
|
let query_parser = try(string(COMMAND_QUERY_LONG)).or(try(string(COMMAND_QUERY_SHORT)))
|
||||||
.with(edn_arg_parser())
|
.with(edn_arg_parser())
|
||||||
|
@ -321,11 +335,13 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
|
||||||
|
|
||||||
spaces()
|
spaces()
|
||||||
.skip(token('.'))
|
.skip(token('.'))
|
||||||
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 14], _>
|
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 16], _>
|
||||||
([&mut try(help_parser),
|
([&mut try(help_parser),
|
||||||
&mut try(import_parser),
|
&mut try(import_parser),
|
||||||
&mut try(timer_parser),
|
&mut try(timer_parser),
|
||||||
&mut try(cache_parser),
|
&mut try(cache_parser),
|
||||||
|
&mut try(open_encrypted_parser),
|
||||||
|
&mut try(open_empty_encrypted_parser),
|
||||||
&mut try(open_parser),
|
&mut try(open_parser),
|
||||||
&mut try(open_empty_parser),
|
&mut try(open_empty_parser),
|
||||||
&mut try(close_parser),
|
&mut try(close_parser),
|
||||||
|
@ -425,6 +441,46 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_open_encrypted_parser() {
|
||||||
|
let input = ".open_encrypted /path/to/my.db hunter2";
|
||||||
|
let cmd = command(&input).expect("Expected open_encrypted command");
|
||||||
|
match cmd {
|
||||||
|
Command::OpenEncrypted(path, key) => {
|
||||||
|
assert_eq!(path, "/path/to/my.db".to_string());
|
||||||
|
assert_eq!(key, "hunter2".to_string());
|
||||||
|
},
|
||||||
|
_ => assert!(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_encrypted_parser() {
|
||||||
|
let input = ".empty_encrypted /path/to/my.db hunter2";
|
||||||
|
let cmd = command(&input).expect("Expected empty_encrypted command");
|
||||||
|
match cmd {
|
||||||
|
Command::OpenEmptyEncrypted(path, key) => {
|
||||||
|
assert_eq!(path, "/path/to/my.db".to_string());
|
||||||
|
assert_eq!(key, "hunter2".to_string());
|
||||||
|
},
|
||||||
|
_ => assert!(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_open_encrypted_parser_missing_key() {
|
||||||
|
let input = ".open_encrypted path/to/db.db";
|
||||||
|
let err = command(&input).expect_err("Expected an error");
|
||||||
|
assert_eq!(err.to_string(), "Missing required argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_encrypted_parser_missing_key() {
|
||||||
|
let input = ".empty_encrypted path/to/db.db";
|
||||||
|
let err = command(&input).expect_err("Expected an error");
|
||||||
|
assert_eq!(err.to_string(), "Missing required argument");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_parser_path_arg() {
|
fn test_sync_parser_path_arg() {
|
||||||
let input = ".sync https://example.com/api/ 316ea470-ce35-4adf-9c61-e0de6e289c59";
|
let input = ".sync https://example.com/api/ 316ea470-ce35-4adf-9c61-e0de6e289c59";
|
||||||
|
|
|
@ -50,6 +50,9 @@ pub fn run() -> i32 {
|
||||||
let mut opts = Options::new();
|
let mut opts = Options::new();
|
||||||
|
|
||||||
opts.optopt("d", "", "The path to a database to open", "DATABASE");
|
opts.optopt("d", "", "The path to a database to open", "DATABASE");
|
||||||
|
if cfg!(feature = "sqlcipher") {
|
||||||
|
opts.optopt("k", "key", "The key to use to open the database (only available when using sqlcipher)", "KEY");
|
||||||
|
}
|
||||||
opts.optflag("h", "help", "Print this help message and exit");
|
opts.optflag("h", "help", "Print this help message and exit");
|
||||||
opts.optmulti("q", "query", "Execute a query on startup. Queries are executed after any transacts.", "QUERY");
|
opts.optmulti("q", "query", "Execute a query on startup. Queries are executed after any transacts.", "QUERY");
|
||||||
opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT");
|
opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT");
|
||||||
|
@ -74,12 +77,24 @@ pub fn run() -> i32 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's still possible to pass this in even if it's not a documented flag above.
|
||||||
|
let key = matches.opt_str("key");
|
||||||
|
if key.is_some() && !cfg!(feature = "sqlcipher") {
|
||||||
|
eprintln!("Decryption key provided, but this build does not have sqlcipher support");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
let mut last_arg: Option<&str> = None;
|
let mut last_arg: Option<&str> = None;
|
||||||
|
|
||||||
let cmds:Vec<command_parser::Command> = args.iter().filter_map(|arg| {
|
let cmds:Vec<command_parser::Command> = args.iter().filter_map(|arg| {
|
||||||
match last_arg {
|
match last_arg {
|
||||||
Some("-d") => {
|
Some("-d") => {
|
||||||
last_arg = None;
|
last_arg = None;
|
||||||
Some(command_parser::Command::Open(arg.clone()))
|
if let Some(k) = &key {
|
||||||
|
Some(command_parser::Command::OpenEncrypted(arg.clone(), k.clone()))
|
||||||
|
} else {
|
||||||
|
Some(command_parser::Command::Open(arg.clone()))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Some("-q") => {
|
Some("-q") => {
|
||||||
last_arg = None;
|
last_arg = None;
|
||||||
|
|
|
@ -52,6 +52,7 @@ use command_parser::{
|
||||||
COMMAND_HELP,
|
COMMAND_HELP,
|
||||||
COMMAND_IMPORT_LONG,
|
COMMAND_IMPORT_LONG,
|
||||||
COMMAND_OPEN,
|
COMMAND_OPEN,
|
||||||
|
COMMAND_OPEN_EMPTY,
|
||||||
COMMAND_QUERY_LONG,
|
COMMAND_QUERY_LONG,
|
||||||
COMMAND_QUERY_SHORT,
|
COMMAND_QUERY_SHORT,
|
||||||
COMMAND_QUERY_EXPLAIN_LONG,
|
COMMAND_QUERY_EXPLAIN_LONG,
|
||||||
|
@ -64,6 +65,16 @@ use command_parser::{
|
||||||
COMMAND_TRANSACT_SHORT,
|
COMMAND_TRANSACT_SHORT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// These are still defined when this feature is disabled (so that we can
|
||||||
|
// give decent error messages when a user tries open_encrypted when
|
||||||
|
// we weren't compiled with sqlcipher), but they're unused, since we
|
||||||
|
// omit them from help message (since they wouldn't work).
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
use command_parser::{
|
||||||
|
COMMAND_OPEN_EMPTY_ENCRYPTED,
|
||||||
|
COMMAND_OPEN_ENCRYPTED,
|
||||||
|
};
|
||||||
|
|
||||||
use input::InputReader;
|
use input::InputReader;
|
||||||
use input::InputResult::{
|
use input::InputResult::{
|
||||||
Empty,
|
Empty,
|
||||||
|
@ -81,6 +92,12 @@ lazy_static! {
|
||||||
(COMMAND_EXIT_SHORT, "Shortcut for `.exit`. Close the current database and exit the REPL."),
|
(COMMAND_EXIT_SHORT, "Shortcut for `.exit`. Close the current database and exit the REPL."),
|
||||||
|
|
||||||
(COMMAND_OPEN, "Open a database at path."),
|
(COMMAND_OPEN, "Open a database at path."),
|
||||||
|
(COMMAND_OPEN_EMPTY, "Open an empty database at path."),
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
(COMMAND_OPEN_ENCRYPTED, "Open an encrypted database at path using the provided key."),
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
(COMMAND_OPEN_EMPTY_ENCRYPTED, "Open an empty encrypted database at path using the provided key."),
|
||||||
|
|
||||||
(COMMAND_SCHEMA, "Output the schema for the current open database."),
|
(COMMAND_SCHEMA, "Output the schema for the current open database."),
|
||||||
|
|
||||||
|
@ -267,6 +284,18 @@ impl Repl {
|
||||||
Err(e) => eprintln!("{}", e.to_string()),
|
Err(e) => eprintln!("{}", e.to_string()),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
Command::OpenEncrypted(db, encryption_key) => {
|
||||||
|
match self.open_with_key(db, &encryption_key) {
|
||||||
|
Ok(_) => println!("Database {:?} opened with key {:?}", self.db_name(), encryption_key),
|
||||||
|
Err(e) => eprintln!("{}", e.to_string()),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::OpenEmptyEncrypted(db, encryption_key) => {
|
||||||
|
match self.open_empty_with_key(db, &encryption_key) {
|
||||||
|
Ok(_) => println!("Empty database {:?} opened with key {:?}", self.db_name(), encryption_key),
|
||||||
|
Err(e) => eprintln!("{}", e.to_string()),
|
||||||
|
}
|
||||||
|
},
|
||||||
Command::Query(query) => {
|
Command::Query(query) => {
|
||||||
self.store
|
self.store
|
||||||
.q_once(query.as_str(), None)
|
.q_once(query.as_str(), None)
|
||||||
|
@ -345,11 +374,32 @@ impl Repl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open<T>(&mut self, path: T) -> ::mentat::errors::Result<()>
|
fn open_common(
|
||||||
where T: Into<String> {
|
&mut self,
|
||||||
let path = path.into();
|
empty: bool,
|
||||||
|
path: String,
|
||||||
|
encryption_key: Option<&str>
|
||||||
|
) -> ::mentat::errors::Result<()> {
|
||||||
if self.path.is_empty() || path != self.path {
|
if self.path.is_empty() || path != self.path {
|
||||||
let next = Store::open(path.as_str())?;
|
let next = match encryption_key {
|
||||||
|
#[cfg(not(feature = "sqlcipher"))]
|
||||||
|
Some(_) => bail!(".open_encrypted and .empty_encrypted require the sqlcipher Mentat feature"),
|
||||||
|
#[cfg(feature = "sqlcipher")]
|
||||||
|
Some(k) => {
|
||||||
|
if empty {
|
||||||
|
Store::open_empty_with_key(path.as_str(), k)?
|
||||||
|
} else {
|
||||||
|
Store::open_with_key(path.as_str(), k)?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
if empty {
|
||||||
|
Store::open_empty(path.as_str())?
|
||||||
|
} else {
|
||||||
|
Store::open(path.as_str())?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
self.path = path;
|
self.path = path;
|
||||||
self.store = next;
|
self.store = next;
|
||||||
}
|
}
|
||||||
|
@ -357,16 +407,23 @@ impl Repl {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_empty<T>(&mut self, path: T) -> ::mentat::errors::Result<()>
|
fn open(&mut self, path: impl Into<String>) -> ::mentat::errors::Result<()> {
|
||||||
where T: Into<String> {
|
self.open_common(false, path.into(), None)
|
||||||
let path = path.into();
|
}
|
||||||
if self.path.is_empty() || path != self.path {
|
|
||||||
let next = Store::open_empty(path.as_str())?;
|
|
||||||
self.path = path;
|
|
||||||
self.store = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
fn open_empty(&mut self, path: impl Into<String>)
|
||||||
|
-> ::mentat::errors::Result<()> {
|
||||||
|
self.open_common(true, path.into(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_with_key(&mut self, path: impl Into<String>, encryption_key: impl AsRef<str>)
|
||||||
|
-> ::mentat::errors::Result<()> {
|
||||||
|
self.open_common(false, path.into(), Some(encryption_key.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_empty_with_key(&mut self, path: impl Into<String>, encryption_key: impl AsRef<str>)
|
||||||
|
-> ::mentat::errors::Result<()> {
|
||||||
|
self.open_common(true, path.into(), Some(encryption_key.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the current store by opening a new in-memory store in its place.
|
// Close the current store by opening a new in-memory store in its place.
|
||||||
|
|
Loading…
Reference in a new issue