// False positive with itertools::process_results #![allow (clippy::redundant_closure)] use std::{ collections::{ HashMap, }, convert::{TryFrom}, net::IpAddr, path::Path, str::FromStr, }; use crate::{ errors::ConfigError, key_validity::{ ScraperKey, }, }; /// Machine-editable configs. /// These are stored in the `data` directory and shouldn't be touched by /// humans. `ptth_relay` will re-write them while it's running. pub mod machine_editable { use std::{ collections::BTreeMap, path::Path, }; use serde::{Deserialize, Serialize}; use super::file::Server; #[derive (Deserialize, Serialize)] pub struct ConfigFile { pub servers: Vec , } #[derive (Default)] pub struct Config { pub servers: BTreeMap , } impl ConfigFile { pub fn from_file (path: &Path) -> Result { let config_s = std::fs::read_to_string (path)?; Ok (toml::from_str (&config_s)?) } pub async fn save (&self, path: &Path) -> Result <(), crate::ConfigError> { let s = toml::to_string (self)?; // This is way easier in C++ but also not safe let mut temp_path = path.file_name ().unwrap ().to_os_string (); temp_path.push (".partial"); let temp_path = path.with_file_name (temp_path); tokio::fs::write (&temp_path, &s).await?; tokio::fs::rename (&temp_path, path).await?; Ok (()) } } impl Config { pub fn from_file (path: &Path) -> Result { let c = ConfigFile::from_file (path)?; let servers = c.servers.into_iter () .map (|s| (s.name.clone (), s)) .collect (); Ok (Self { servers, }) } pub async fn save (&self, path: &Path) -> Result <(), crate::ConfigError> { let servers = self.servers.values () .cloned ().into_iter ().collect (); let c = ConfigFile { servers, }; c.save (path).await?; Ok (()) } } } /// Config fields as they are loaded from the config file pub mod file { use serde::{Deserialize, Serialize}; use crate::key_validity::{ BlakeHashWrapper, ScraperKey, }; #[derive (Clone, Debug, Deserialize, Serialize)] pub struct Server { /// This is duplicated in the hashmap, but it's not a problem pub name: String, pub tripcode: BlakeHashWrapper, /// This allows a relay-side rename of servers pub display_name: Option , } /// Empty #[derive (Deserialize)] pub struct DevMode { } /// Config fields that are identical in the file and at runtime #[derive (Default, Deserialize)] pub struct Isomorphic { #[serde (default)] pub enable_scraper_api: bool, /// If any of the `DevMode` fields are used, we are in dev mode /// and have to show extra warnings, since auth may be weakened pub dev_mode: Option , } #[derive (Deserialize)] pub struct Config { #[serde (flatten)] pub iso: Isomorphic, pub address: Option , pub port: Option , pub servers: Option >, // Adding a DB will take a while, so I'm moving these out of dev mode. pub scraper_keys: Option >, pub news_url: Option , } } /// Config fields as they are used at runtime pub struct Config { pub iso: file::Isomorphic, pub address: IpAddr, pub port: Option , pub servers: HashMap , pub scraper_keys: HashMap , pub news_url: Option , } impl Default for Config { fn default () -> Self { Self { iso: Default::default (), address: IpAddr::from ([0, 0, 0, 0]), port: None, servers: Default::default (), scraper_keys: Default::default (), news_url: None, } } } impl TryFrom for Config { type Error = ConfigError; fn try_from (f: file::Config) -> Result { let servers = f.servers.unwrap_or_else (|| vec! []); let servers = servers.into_iter ().map (|server| Ok::<_, ConfigError> ((server.name.clone (), server))); let servers = itertools::process_results (servers, |i| i.collect ())?; let scraper_keys = f.scraper_keys.unwrap_or_else (|| vec! []); let scraper_keys = if f.iso.enable_scraper_api { scraper_keys.into_iter ().map (|key| (key.hash.encode_base64 (), key)).collect () } else { Default::default () }; Ok (Self { iso: f.iso, address: parse_address (f.address.as_ref ().map (|s| &s[..]))?, port: f.port, servers, scraper_keys, news_url: f.news_url, }) } } fn parse_address (s: Option <&str>) -> Result { Ok (s .map (|s| IpAddr::from_str (s)) .transpose ().map_err (|_| ConfigError::BadServerAddress)? .unwrap_or_else (|| IpAddr::from ([0, 0, 0, 0])) ) } impl Config { pub async fn from_file (path: &Path) -> Result { let config_s = tokio::fs::read_to_string (path).await?; let new_config: file::Config = toml::from_str (&config_s)?; Self::try_from (new_config) } } #[cfg (test)] mod tests { use super::*; #[test] fn ip_address () { for (input, expected) in vec! [ (None, Some (IpAddr::from ([0, 0, 0, 0]))), (Some ("bogus"), None), (Some ("0.0.0.0"), Some (IpAddr::from ([0, 0, 0, 0]))), (Some ("0"), None), (Some ("127.0.0.1"), Some (IpAddr::from ([127, 0, 0, 1]))), (Some ("10.0.0.1"), Some (IpAddr::from ([10, 0, 0, 1]))), ].into_iter () { let actual = parse_address (input).ok (); assert_eq! (actual, expected); } } }