rust-sdk: start adding rustdoc (#2895)

* rust-sdk: start adding some rustdoc

* rust-sdk: whole bunch of rustdoc

* rustfmt
This commit is contained in:
Jon Häggblad
2023-01-24 08:50:59 +01:00
committed by GitHub
parent ded7e51071
commit 4c19187c78
12 changed files with 317 additions and 153 deletions
@@ -10,7 +10,7 @@ async fn main() {
let config = mixnet::Config::new(user_chosen_gateway_id, nym_api_endpoints);
let mut client = mixnet::MixnetClientBuilder::new(Some(config), None)
let mut client = mixnet::MixnetClient::builder(Some(config), None)
.await
.unwrap();
@@ -37,7 +37,8 @@ async fn main() {
println!("Our client address is {}", client.nym_address());
// Send important info up the pipe to a buddy
client.send_str("foo.bar@blah", "flappappa").await;
let recipient = mixnet::Recipient::try_from_base58_string("foo.bar@blah").unwrap();
client.send_str(recipient, "flappappa").await;
}
#[allow(unused)]
+2 -4
View File
@@ -14,7 +14,7 @@ async fn main() {
let keys = mixnet::StoragePaths::new_from_dir(mixnet::KeyMode::Keep, &config_dir).unwrap();
// Provide key paths for the client to read/write keys to.
let client = mixnet::MixnetClientBuilder::new(None, Some(keys))
let client = mixnet::MixnetClient::builder(None, Some(keys))
.await
.unwrap();
@@ -26,9 +26,7 @@ async fn main() {
println!("Our client nym address is: {our_address}");
// Send a message throught the mixnet to ourselves
client
.send_str(&our_address.to_string(), "hello there")
.await;
client.send_str(*our_address, "hello there").await;
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
+1 -3
View File
@@ -12,9 +12,7 @@ async fn main() {
println!("Our client nym address is: {our_address}");
// Send a message throught the mixnet to ourselves
client
.send_str(&our_address.to_string(), "hello there")
.await;
client.send_str(*our_address, "hello there").await;
println!("Waiting for message (ctrl-c to exit)");
client
@@ -7,7 +7,7 @@ async fn main() {
// Create client builder, including ephemeral keys. The builder can be usable in the context
// where you don't want to connect just yet.
// Since not storage paths are given, the surb storage will be inactive.
let client = mixnet::MixnetClientBuilder::new(None, None).await.unwrap();
let client = mixnet::MixnetClient::builder(None, None).await.unwrap();
// Now we connect to the mixnet, using ephemeral keys already created
let mut client = client.connect_to_mixnet().await.unwrap();
@@ -17,9 +17,7 @@ async fn main() {
println!("Our client nym address is: {our_address}");
// Send a message throught the mixnet to ourselves
client
.send_str(&our_address.to_string(), "hello there")
.await;
client.send_str(*our_address, "hello there").await;
println!("Waiting for message");
if let Some(received) = client.wait_for_messages().await {
+9 -1
View File
@@ -1,5 +1,6 @@
use std::path::PathBuf;
/// Top-level Error enum for the mixnet client and its relevant types.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("i/o error: {0}")]
@@ -20,9 +21,16 @@ pub enum Error {
DontOverwriteGatewayKey(PathBuf),
#[error("no gateway config available for writing")]
GatewayNotAvailableForWriting,
#[error("expected to received a directory, received: {0}")]
ExpectedDirectory(PathBuf),
#[error("failed to transition to registered state before connection to mixnet")]
FailedToTransitionToRegisteredState,
#[error(
"registering with gateway when the client is already in a registered state is not \
supported, and likely and user mistake"
)]
ReregisteringGatewayNotSupported,
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
+8 -1
View File
@@ -1,2 +1,9 @@
pub mod error;
//! Rust SDK for the Nym platform
//!
//! The main component currently is [`mixnet`].
mod error;
pub mod mixnet;
pub use error::{Error, Result};
+35 -5
View File
@@ -1,17 +1,47 @@
//! The mixnet component of the Rust SDK for the Nym platform
//!
//!
//! # Basic example
//!
//! ```no_run
//! use nym_sdk::mixnet;
//!
//! #[tokio::main]
//! async fn main() {
//! // Passing no config makes the client fire up an ephemeral session and figure stuff out on
//! // its own
//! let mut client = mixnet::MixnetClient::connect().await.unwrap();
//!
//! // Be able to get our client address
//! let our_address = client.nym_address();
//! println!("Our client nym address is: {our_address}");
//!
//! // Send a message throught the mixnet to ourselves
//! client.send_str(*our_address, "hello there").await;
//!
//! println!("Waiting for message");
//! if let Some(received) = client.wait_for_messages().await {
//! for r in received {
//! println!("Received: {}", String::from_utf8_lossy(&r.message));
//! }
//! }
//!
//! client.disconnect().await;
//! }
//! ```
mod client;
mod config;
mod connection_state;
mod keys;
mod paths;
pub use client::{MixnetClient, MixnetClientBuilder};
pub use client_core::config::GatewayEndpointConfig;
pub use config::Config;
pub use keys::{Keys, KeysArc};
pub use nymsphinx::{
addressing::clients::{ClientIdentity, Recipient},
receiver::ReconstructedMessage,
};
pub use keys::{Keys, KeysArc};
pub use paths::{GatewayKeyMode, KeyMode, StoragePaths};
pub use client::{MixnetClient, MixnetClientBuilder};
pub use config::Config;
+221 -124
View File
@@ -24,10 +24,18 @@ use task::TaskManager;
use futures::StreamExt;
use validator_client::nyxd::SigningNyxdClient;
use super::{connection_state::BuilderState, Config, GatewayKeyMode, Keys, KeysArc, StoragePaths};
use crate::error::{Error, Result};
use crate::{Error, Result};
pub struct MixnetClientBuilder<B: ReplyStorageBackend> {
use super::{connection_state::BuilderState, Config, GatewayKeyMode, Keys, KeysArc, StoragePaths};
/// Represents a client that is not yet connected to the mixnet. You typically create one when you
/// want to have a separate configuration and connection phase. Once the mixnet client builder is
/// configured, call [`MixnetClientBuilder::connect_to_mixnet()`] to transition to a connected
/// client.
pub struct MixnetClientBuilder<B>
where
B: ReplyStorageBackend,
{
/// Keys handled by the client
key_manager: KeyManager,
@@ -45,105 +53,10 @@ pub struct MixnetClientBuilder<B: ReplyStorageBackend> {
reply_storage_backend: B,
}
impl MixnetClientBuilder<reply_storage::fs_backend::Backend> {
/// Create a new mixnet client. If no config options are supplied, creates a new client with
/// ephemeral keys stored in RAM, which will be discarded at application close.
///
/// Callers have the option of supplying futher parameters to store persistent identities at a
/// location on-disk, if desired.
pub async fn new(config: Option<Config>, paths: Option<StoragePaths>) -> Result<Self> {
let config = config.unwrap_or_default();
let reply_surb_database_path = paths.as_ref().map(|p| p.reply_surb_database_path.clone());
let reply_storage_backend = non_wasm_helpers::setup_fs_reply_surb_backend(
reply_surb_database_path,
&config.debug_config,
)
.await?;
MixnetClientBuilder::new_with_custom_storage(Some(config), paths, reply_storage_backend)
}
}
impl MixnetClientBuilder<reply_storage::Empty> {
/// Create a new mixnet client. If no config options are supplied, creates a new client with
/// ephemeral keys stored in RAM, which will be discarded at application close.
///
/// Callers have the option of supplying futher parameters to store persistent identities at a
/// location on-disk, if desired.
pub fn new_without_storage(
config: Option<Config>,
paths: Option<StoragePaths>,
) -> Result<Self> {
let config = config.unwrap_or_default();
let reply_storage_backend =
non_wasm_helpers::setup_empty_reply_surb_backend(&config.debug_config);
MixnetClientBuilder::new_with_custom_storage(Some(config), paths, reply_storage_backend)
}
}
impl<B> MixnetClientBuilder<B>
where
B: ReplyStorageBackend + Sync + Send + 'static,
{
/// Create a new mixnet client. If no config options are supplied, creates a new client with
/// ephemeral keys stored in RAM, which will be discarded at application close.
///
/// Callers have the option of supplying futher parameters to store persistent identities at a
/// location on-disk, if desired.
///
/// A custom storage backend can be passed in.
pub fn new_with_custom_storage(
config_option: Option<Config>,
paths: Option<StoragePaths>,
reply_storage_backend: B,
) -> Result<Self> {
let config = config_option.unwrap_or_default();
// If we are provided paths to keys, use them if they are available. And if they are
// not, write the generated keys back to storage.
let key_manager = if let Some(ref paths) = paths {
let path_finder = ClientKeyPathfinder::from(paths.clone());
// Try load keys
match KeyManager::load_keys_but_gateway_is_optional(&path_finder) {
Ok(key_manager) => key_manager,
Err(err) => {
log::debug!("Not loading keys: {err}");
if let Some(path) = path_finder.any_file_exists_and_return() {
if paths.operating_mode.is_keep() {
return Err(Error::DontOverwrite(path));
}
}
// Double check using a function that has slightly different internal logic. I
// know this is a bit defensive, but I don't want to overwrite
assert!(!(path_finder.any_file_exists() && paths.operating_mode.is_keep()));
// Create new keys and write to storage
let key_manager = client_core::init::new_client_keys();
// WARN: this will overwrite!
key_manager.store_keys(&path_finder)?;
key_manager
}
}
} else {
// Ephemeral keys that we only store in memory
client_core::init::new_client_keys()
};
Ok(Self {
key_manager,
config,
storage_paths: paths,
state: BuilderState::New,
reply_storage_backend,
})
}
/// Client keys are generated at client creation if none were found. The gateway shared
/// key, however, is created during the gateway registration handshake so it might not
/// necessarily be available.
@@ -151,6 +64,7 @@ where
self.key_manager.is_gateway_key_set()
}
/// Sets the keys of this [`MixnetClientBuilder<B>`].
pub fn set_keys(&mut self, keys: Keys) {
self.key_manager.set_identity_keypair(keys.identity_keypair);
self.key_manager
@@ -161,25 +75,35 @@ where
.insert_gateway_shared_key(Arc::new(keys.gateway_shared_key));
}
/// Returns the keys of this [`MixnetClientBuilder<B>`]. Client keys are always available since
/// if none are specified at creation time, new random ones are generated.
pub fn get_keys(&self) -> KeysArc {
KeysArc::from(&self.key_manager)
}
/// Sets the gateway endpoint of this [`MixnetClientBuilder<B>`].
pub fn set_gateway_endpoint(&mut self, gateway_endpoint_config: GatewayEndpointConfig) {
self.state = BuilderState::Registered {
gateway_endpoint_config,
}
}
/// Returns the get gateway endpoint of this [`MixnetClientBuilder<B>`].
pub fn get_gateway_endpoint(&self) -> Option<&GatewayEndpointConfig> {
self.state.gateway_endpoint_config()
}
/// Register with a gateway. If a gateway is provided in the config then that will try to be
/// used. If none is specified, a gateway at random will be picked.
///
/// # Errors
///
/// This function will return an error if you try to re-register when in an already registered
/// state.
pub async fn register_with_gateway(&mut self) -> Result<()> {
assert!(
matches!(self.state, BuilderState::New),
"can only setup gateway when in `New` connection state"
);
if self.state != BuilderState::New {
return Err(Error::ReregisteringGatewayNotSupported);
}
let user_chosen_gateway = self
.config
@@ -237,7 +161,24 @@ where
Ok(())
}
/// Connects to the mixnet via the gateway in the client config
/// Connect the client to the mixnet.
///
/// - If the client is already registered with a gateway, use that gateway.
/// - If no gateway is registered, but there is an existing configuration and key, use that.
/// - If no gateway is registered, and there is no pre-existing configuration or key, try to
/// register a new gateway.
///
/// # Example
///
/// ```no_run
/// use nym_sdk::mixnet;
///
/// #[tokio::main]
/// async fn main() {
/// let client = mixnet::MixnetClient::builder(None, None).await.unwrap();
/// let client = client.connect_to_mixnet().await.unwrap();
/// }
/// ```
pub async fn connect_to_mixnet(mut self) -> Result<MixnetClient>
where
<B as ReplyStorageBackend>::StorageError: Sync + Send,
@@ -269,7 +210,7 @@ where
// At this point we should be in a registered state, either at function entry or by the
// above convenience logic.
let BuilderState::Registered { gateway_endpoint_config } = self.state else {
todo!();
return Err(Error::FailedToTransitionToRegisteredState);
};
let nym_address =
@@ -278,7 +219,7 @@ where
// TODO: we currently don't support having a bandwidth controller
let bandwidth_controller = None;
let base_builder: BaseClientBuilder<_, SigningNyxdClient> = BaseClientBuilder::new(
let base_builder: BaseClientBuilder<'_, _, SigningNyxdClient> = BaseClientBuilder::new(
&gateway_endpoint_config,
&self.config.debug_config,
self.key_manager.clone(),
@@ -288,13 +229,13 @@ where
self.config.nym_api_endpoints.clone(),
);
let mut started_client = base_builder.start_base().await.unwrap();
let mut started_client = base_builder.start_base().await?;
let client_input = started_client.client_input.register_producer();
let mut client_output = started_client.client_output.register_consumer();
let client_state = started_client.client_state;
// Register our receiver
let reconstructed_receiver = client_output.register_receiver().unwrap();
let reconstructed_receiver = client_output.register_receiver()?;
Ok(MixnetClient {
nym_address,
@@ -308,31 +249,170 @@ where
}
}
/// Client connected to the Nym mixnet.
pub struct MixnetClient {
/// The nym address of this connected client.
nym_address: Recipient,
/// Keys handled by the client
key_manager: KeyManager,
/// Input to the client from the users perspective. This can be either data to send or controll
/// messages.
client_input: ClientInput,
/// Output from the client from the users perspective. This is typically messages arriving from
/// the mixnet.
#[allow(dead_code)]
client_output: ClientOutput,
/// The current state of the client that is exposed to the user. This includes things like
/// current message send queue length.
#[allow(dead_code)]
client_state: ClientState,
/// A channel for messages arriving from the mixnet after they have been reconstructed.
reconstructed_receiver: ReconstructedMessagesReceiver,
/// The task manager that controlls all the spawned tasks that the clients uses to do it's job.
task_manager: TaskManager,
}
impl MixnetClient {
/// Create a new client and connect to the mixnet using ephemeral in-memory keys that are
/// discarded at application close.
///
/// # Examples
///
/// ```no_run
/// use nym_sdk::mixnet;
///
/// #[tokio::main]
/// async fn main() {
/// let mut client = mixnet::MixnetClient::connect().await;
/// }
///
/// ```
pub async fn connect() -> Result<Self> {
let client = MixnetClientBuilder::new_without_storage(None, None)?;
let client = MixnetClient::builder_without_storage(None, None)?;
client.connect_to_mixnet().await
}
/// Create a new mixnet client builder. If no config options are supplied, creates a new client
/// with ephemeral keys stored in RAM, which will be discarded at application close.
///
/// Callers have the option of supplying futher parameters to store persistent identities at a
/// location on-disk, if desired.
///
/// # Examples
///
/// ```no_run
/// use nym_sdk::mixnet;
///
/// #[tokio::main]
/// async fn main() {
/// let client = mixnet::MixnetClient::builder(None, None).await;
/// }
/// ```
pub async fn builder(
config: Option<Config>,
paths: Option<StoragePaths>,
) -> Result<MixnetClientBuilder<reply_storage::fs_backend::Backend>> {
let config = config.unwrap_or_default();
let reply_surb_database_path = paths.as_ref().map(|p| p.reply_surb_database_path.clone());
let reply_storage_backend = non_wasm_helpers::setup_fs_reply_surb_backend(
reply_surb_database_path,
&config.debug_config,
)
.await?;
MixnetClient::builder_with_custom_storage(Some(config), paths, reply_storage_backend)
}
/// Create a new mixnet client builder. If no config options are supplied, creates a new client with
/// ephemeral keys stored in RAM, which will be discarded at application close.
///
/// Callers have the option of supplying futher parameters to store persistent identities at a
/// location on-disk, if desired.
///
/// # Examples
///
/// ```
/// use nym_sdk::mixnet;
/// let client = mixnet::MixnetClient::builder_without_storage(None, None);
/// ```
pub fn builder_without_storage(
config: Option<Config>,
paths: Option<StoragePaths>,
) -> Result<MixnetClientBuilder<reply_storage::Empty>> {
let config = config.unwrap_or_default();
let reply_storage_backend =
non_wasm_helpers::setup_empty_reply_surb_backend(&config.debug_config);
MixnetClient::builder_with_custom_storage(Some(config), paths, reply_storage_backend)
}
/// Create a new mixnet client builder. If no config options are supplied, creates a new client with
/// ephemeral keys stored in RAM, which will be discarded at application close.
///
/// Callers have the option of supplying futher parameters to store persistent identities at a
/// location on-disk, if desired.
///
/// A custom storage backend can be passed in.
pub fn builder_with_custom_storage<B>(
config_option: Option<Config>,
paths: Option<StoragePaths>,
reply_storage_backend: B,
) -> Result<MixnetClientBuilder<B>>
where
B: ReplyStorageBackend,
{
let config = config_option.unwrap_or_default();
// If we are provided paths to keys, use them if they are available. And if they are
// not, write the generated keys back to storage.
let key_manager = if let Some(ref paths) = paths {
let path_finder = ClientKeyPathfinder::from(paths.clone());
// Try load keys
match KeyManager::load_keys_but_gateway_is_optional(&path_finder) {
Ok(key_manager) => key_manager,
Err(err) => {
log::debug!("Not loading keys: {err}");
if let Some(path) = path_finder.any_file_exists_and_return() {
if paths.operating_mode.is_keep() {
return Err(Error::DontOverwrite(path));
}
}
// Double check using a function that has slightly different internal logic. I
// know this is a bit defensive, but I don't want to overwrite
assert!(!(path_finder.any_file_exists() && paths.operating_mode.is_keep()));
// Create new keys and write to storage
let key_manager = client_core::init::new_client_keys();
// WARN: this will overwrite!
key_manager.store_keys(&path_finder)?;
key_manager
}
}
} else {
// Ephemeral keys that we only store in memory
client_core::init::new_client_keys()
};
Ok(MixnetClientBuilder {
key_manager,
config,
storage_paths: paths,
state: BuilderState::New,
reply_storage_backend,
})
}
/// Get the client identity, which is the public key of the identity key pair.
pub fn identity(&self) -> ClientIdentity {
*self.key_manager.identity_keypair().public_key()
@@ -345,39 +425,59 @@ impl MixnetClient {
}
/// Sends stringy data to the supplied Nym address
pub async fn send_str(&self, address: &str, message: &str) {
pub async fn send_str(&self, address: Recipient, message: &str) {
let message_bytes = message.to_string().into_bytes();
self.send_bytes(address, message_bytes).await;
}
/// Sends stringy data to the supplied Nym address, and skip sending reply-SURBs
pub async fn send_str_direct(&self, address: &str, message: &str) {
pub async fn send_str_direct(&self, address: Recipient, message: &str) {
let message_bytes = message.to_string().into_bytes();
self.send_bytes(address, message_bytes).await;
}
/// Sends bytes to the supplied Nym address
pub async fn send_bytes(&self, address: &str, message: Vec<u8>) {
///
/// # Example
///
/// ```no_run
/// use nym_sdk::mixnet;
///
/// #[tokio::main]
/// async fn main() {
/// let address = "foobar";
/// let recipient = mixnet::Recipient::try_from_base58_string(address).unwrap();
/// let mut client = mixnet::MixnetClient::connect().await.unwrap();
/// client.send_bytes(recipient, "hi".to_owned().into_bytes()).await;
/// }
/// ```
pub async fn send_bytes(&self, address: Recipient, message: Vec<u8>) {
let lane = TransmissionLane::General;
let recipient = Recipient::try_from_base58_string(address).unwrap();
let input_msg = InputMessage::new_anonymous(recipient, message, 20, lane);
self.client_input
let input_msg = InputMessage::new_anonymous(address, message, 20, lane);
if self
.client_input
.input_sender
.send(input_msg)
.await
.unwrap();
.is_err()
{
log::error!("Failed to send message");
}
}
/// Sends bytes to the supplied Nym address, and skip sending reply-SURBs
pub async fn send_bytes_direct(&self, address: &str, message: Vec<u8>) {
pub async fn send_bytes_direct(&self, address: Recipient, message: Vec<u8>) {
let lane = TransmissionLane::General;
let recipient = Recipient::try_from_base58_string(address).unwrap();
let input_msg = InputMessage::new_regular(recipient, message, lane);
self.client_input
let input_msg = InputMessage::new_regular(address, message, lane);
if self
.client_input
.input_sender
.send(input_msg)
.await
.unwrap();
.is_err()
{
log::error!("Failed to send message");
}
}
/// Wait for messages from the mixnet
@@ -385,10 +485,7 @@ impl MixnetClient {
self.reconstructed_receiver.next().await
}
pub fn wait_for_messages_split(&mut self) -> Option<ReconstructedMessage> {
todo!();
}
/// Provide a callback to execute on incoming messages from the mixnet.
pub async fn on_messages<F>(&mut self, fun: F)
where
F: Fn(ReconstructedMessage),
+2
View File
@@ -2,6 +2,7 @@ use client_core::config::DebugConfig;
use network_defaults::mainnet;
use url::Url;
/// Config struct for [`crate::mixnet::MixnetClient`]
pub struct Config {
/// If the user has explicitly specified a gateway.
pub user_chosen_gateway: Option<String>,
@@ -26,6 +27,7 @@ impl Default for Config {
}
impl Config {
/// Creates a new [`Config`].
pub fn new(user_chosen_gateway: Option<String>, nym_api_endpoints: Vec<Url>) -> Self {
Self {
user_chosen_gateway,
@@ -1,6 +1,6 @@
use client_core::config::GatewayEndpointConfig;
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub(super) enum BuilderState {
New,
Registered {
+15
View File
@@ -5,17 +5,32 @@ use crypto::asymmetric::{encryption, identity};
use gateway_requests::registration::handshake::SharedKeys;
use nymsphinx::acknowledgements::AckKey;
/// The set of keys used by the client. Identity, encryption and ack keys are generated at creating
/// unless specified to loaded from storage or somehow explictly specified. The gateway shared key
/// is generated when registering with a gateway.
pub struct Keys {
/// The identity key of the client.
pub identity_keypair: identity::KeyPair,
/// The encryption key of the client.
pub encryption_keypair: encryption::KeyPair,
/// The ack key used by the client.
pub ack_key: AckKey,
/// The gateway shared key that is obtained after registering with a gateway.
pub gateway_shared_key: SharedKeys,
}
/// The set of keys used by the client, but where each key is stored in an [`std::sync::Arc`] for
/// easy cloning.
pub struct KeysArc {
/// The identity key of the client.
pub identity_keypair: Arc<identity::KeyPair>,
/// The encryption key of the client.
pub encryption_keypair: Arc<encryption::KeyPair>,
/// The ack key used by the client.
pub ack_key: Arc<AckKey>,
/// The gateway shared key that is obtained after registering with a gateway.
pub gateway_shared_key: Arc<SharedKeys>,
}
+18 -8
View File
@@ -30,36 +30,46 @@ impl GatewayKeyMode {
}
}
/// Set of storage paths that the client will use if it is setup to persist keys, credentials, and
/// reply-SURBs.
#[derive(Clone, Debug)]
pub struct StoragePaths {
// Determines how to handle existing key files found.
/// Determines how to handle existing key files found.
pub operating_mode: KeyMode,
// Client identity keys
/// Client private identity key
pub private_identity: PathBuf,
/// Client public identity key
pub public_identity: PathBuf,
// Client encryption keys
/// Client private encryption key
pub private_encryption: PathBuf,
/// Client public encryption key
pub public_encryption: PathBuf,
// Key for handling acks
/// Key for handling acks
pub ack_key: PathBuf,
// Key setup after authenticating with a gateway
/// Key setup after authenticating with a gateway
pub gateway_shared_key: PathBuf,
// The key isn't much use without knowing which entity it refers to.
/// The key isn't much use without knowing which entity it refers to.
pub gateway_endpoint_config: PathBuf,
// The database containing credentials
/// The database containing credentials
pub credential_database_path: PathBuf,
// The database storing reply surbs in-between sessions
/// The database storing reply surbs in-between sessions
pub reply_surb_database_path: PathBuf,
}
impl StoragePaths {
/// Create a set of storage paths from a given directory.
///
/// # Errors
///
/// This function will return an error if it is passed a path to an existing file instead of a
/// directory.
pub fn new_from_dir(operating_mode: KeyMode, dir: &Path) -> Result<Self> {
if dir.is_file() {
return Err(Error::ExpectedDirectory(dir.to_owned()));