diff --git a/Cargo.lock b/Cargo.lock index 21b59b6..fa9f4d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,6 +753,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite", + "toml", ] [[package]] @@ -900,6 +901,15 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "tungstenite" version = "0.14.0" diff --git a/client/Cargo.toml b/client/Cargo.toml index 0636095..55ef8fa 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -19,6 +19,7 @@ solstice-proto = { path = "../proto" } thiserror = "^1.0" tokio = { version = "1.0", features = ["full"] } tokio-tungstenite = "0.15" +toml = "^0.5" [dev-dependencies] parking_lot = "^0.11.0" diff --git a/client/src/config.rs b/client/src/config.rs index f242bec..767e9f0 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -1,7 +1,57 @@ +use std::path::Path; + +use anyhow::Context; +use serde::{Deserialize, Serialize}; use solstice_proto::server::Credentials; +use tokio::fs::File; +use tokio::io::AsyncReadExt; + +/// Represents a TOML configuration file. +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct TomlConfig { + /// The address of the server to connect to. + /// If `None`, a default is used. + pub server_address: Option, + + /// User name with which to log in to the server. + user_name: String, + + /// Password with which to log in to the server. + password: String, + + /// The address on which to listen for incoming peer connections. + /// If `None`, a default value is used. + pub peer_listen_address: Option, + + /// The address on which to listen for incoming control connections. + /// If `None`, a default value is used. + pub control_listen_address: Option, + + /// The maximum number of peer connections allowed at once. + /// If `None`, a default value is used. + pub max_peers: Option, +} + +impl TomlConfig { + fn from_str(s: &str) -> Result { + toml::from_str(s) + } + + async fn from_file_path(path: &Path) -> anyhow::Result { + let mut file = + File::open(path).await.context("opening toml config file")?; + + let mut contents = String::new(); + file + .read_to_string(&mut contents) + .await + .context("reading toml config file")?; -// TODO: Implement reading this from .toml files. -#[derive(Debug)] + Self::from_str(&contents).context("parsing toml config file") + } +} + +#[derive(Debug, Eq, PartialEq)] pub struct Config { /// Credentials to present to the server. pub credentials: Credentials, @@ -25,11 +75,20 @@ pub struct Config { pub max_peers: usize, } +// TODO: Delete in favor of `new`, or mark test-only. impl Default for Config { fn default() -> Self { let credentials = Credentials::new("solstice".to_string(), "topsekrit".to_string()) .expect("building default credentials"); + + Self::new(credentials) + } +} + +impl Config { + /// Builds a new config with the given `credentials` and default values. + fn new(credentials: Credentials) -> Self { Self { credentials, server_address: "server.slsknet.org:2242".to_string(), @@ -39,4 +98,179 @@ impl Default for Config { max_peers: 1000, } } + + /// Attempts to build a `Config` from the given `TomlConfig`. + fn from_toml(toml: TomlConfig) -> anyhow::Result { + let credentials = Credentials::new(toml.user_name, toml.password) + .context("validating credentials")?; + + let mut config = Self::new(credentials); + + if let Some(server_address) = toml.server_address { + config.server_address = server_address; + } + + if let Some(peer_listen_address) = toml.peer_listen_address { + config.peer_listen_address = peer_listen_address; + } + if let Some(control_listen_address) = toml.control_listen_address { + config.control_listen_address = control_listen_address; + } + if let Some(max_peers) = toml.max_peers { + config.max_peers = max_peers; + } + + Ok(config) + } +} + +#[cfg(test)] +mod tests { + use std::env; + use std::path::{Path, PathBuf}; + use tokio::fs::{DirBuilder, File}; + use tokio::io::AsyncWriteExt; + + use solstice_proto::server::Credentials; + + use super::{Config, TomlConfig}; + + async fn make_test_dir(test_name: &Path) -> PathBuf { + let path = env::temp_dir() + .join("rust_tests/solstice_client/config") + .join(test_name); + + DirBuilder::new() + .recursive(true) + .create(&path) + .await + .expect(&format!("creating test dir {:?}", path)); + + path + } + + #[test] + fn toml_config_from_str_missing_field() { + TomlConfig::from_str( + r#" + user_name = "emiliano" + "#, + ) + .unwrap_err(); + } + + #[test] + fn toml_config_from_str_minimal() { + let config = TomlConfig::from_str( + r#" + user_name = "emiliano" + password = "zapata" + "#, + ) + .unwrap(); + + assert_eq!( + config, + TomlConfig { + user_name: "emiliano".to_string(), + password: "zapata".to_string(), + server_address: None, + peer_listen_address: None, + control_listen_address: None, + max_peers: None, + } + ); + } + + #[test] + fn toml_config_from_str_maximal() { + let config = TomlConfig::from_str( + r#" + user_name = "emiliano" + password = "zapata" + server_address = "foo.example:1234" + peer_listen_address = "localhost:1337" + control_listen_address = "[::1]:1338" + max_peers = 42 + "#, + ) + .unwrap(); + + assert_eq!( + config, + TomlConfig { + user_name: "emiliano".to_string(), + password: "zapata".to_string(), + server_address: Some("foo.example:1234".to_string()), + peer_listen_address: Some("localhost:1337".to_string()), + control_listen_address: Some("[::1]:1338".to_string()), + max_peers: Some(42), + } + ); + } + + #[tokio::test] + async fn toml_config_from_file_path() { + let dir = make_test_dir(Path::new("toml_config_from_file_path")).await; + let path = dir.join("config.toml"); + + let mut file = File::create(&path) + .await + .expect(&format!("creating config file {:?}", path)); + + let contents = r#" + user_name = "emiliano" + password = "zapata" + "#; + file + .write_all(contents.as_bytes()) + .await + .expect("writing config file contents"); + + let config = TomlConfig::from_file_path(&path) + .await + .expect("reading config file"); + + assert_eq!( + config, + TomlConfig { + user_name: "emiliano".to_string(), + password: "zapata".to_string(), + server_address: None, + peer_listen_address: None, + control_listen_address: None, + max_peers: None, + } + ); + } + + #[test] + fn config_from_toml_fails_empty_password() { + Config::from_toml(TomlConfig { + user_name: "emiliano".to_string(), + password: "".to_string(), + server_address: None, + peer_listen_address: None, + control_listen_address: None, + max_peers: None, + }) + .unwrap_err(); + } + + #[test] + fn config_from_toml_success() { + let config = Config::from_toml(TomlConfig { + user_name: "emiliano".to_string(), + password: "zapata".to_string(), + server_address: None, + peer_listen_address: None, + control_listen_address: None, + max_peers: None, + }) + .unwrap(); + + let credentials = + Credentials::new("emiliano".to_string(), "zapata".to_string()).unwrap(); + assert_eq!(config, Config::new(credentials)); + } }