diff --git a/Cargo.lock b/Cargo.lock index 8b30e92..7227f22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,14 +356,17 @@ dependencies = [ "hyper-rustls", "libflate", "lipsum", + "once_cell", "percent-encoding", "regex", "route-recognizer", "rust-embed", + "sealed_test", "serde", "serde_json", "time", "tokio", + "toml", "url", ] @@ -382,6 +385,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + [[package]] name = "futures" version = "0.3.24" @@ -768,9 +777,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "openssl-probe" @@ -846,6 +855,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.21" @@ -911,6 +926,15 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -1005,6 +1029,18 @@ dependencies = [ "base64", ] +[[package]] +name = "rusty-forkfork" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce85af4dfa2fb0c0143121ab5e424c71ea693867357c9159b8777b59984c218" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1046,6 +1082,28 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sealed_test" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a608d94641cc17fe203b102db2ae86d47a236630192f0244ddbbbb0044c0272" +dependencies = [ + "fs_extra", + "rusty-forkfork", + "sealed_test_derive", + "tempfile", +] + +[[package]] +name = "sealed_test_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -1168,6 +1226,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "textwrap" version = "0.15.1" @@ -1284,6 +1356,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1375,6 +1456,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 873a199..5afc5ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,9 @@ url = "2.2.2" rust-embed = "6.4.0" libflate = "1.2.0" brotli = { version = "3.3.4", features = ["std"] } +toml = "0.5.9" +once_cell = "1.16.0" [dev-dependencies] lipsum = "0.8.2" +sealed_test = "1.0.0" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8285581 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,142 @@ +use once_cell::sync::Lazy; +use std::env::var; + +// Waiting for https://github.com/rust-lang/rust/issues/74465 to land, so we +// can reduce reliance on once_cell. +// +// This is the local static that is initialized at runtime (technically at +// first request) and contains the instance settings. +static CONFIG: Lazy = Lazy::new(Config::load); + +/// Stores the configuration parsed from the environment variables and the +/// config file. `Config::Default()` contains None for each setting. +#[derive(Default, serde::Deserialize)] +pub struct Config { + #[serde(rename = "FERRIT_SFW_ONLY")] + sfw_only: Option, + + #[serde(rename = "FERRIT_DEFAULT_THEME")] + default_theme: Option, + + #[serde(rename = "FERRIT_DEFAULT_FRONT_PAGE")] + default_front_page: Option, + + #[serde(rename = "FERRIT_DEFAULT_LAYOUT")] + default_layout: Option, + + #[serde(rename = "FERRIT_DEFAULT_WIDE")] + default_wide: Option, + + #[serde(rename = "FERRIT_DEFAULT_COMMENT_SORT")] + default_comment_sort: Option, + + #[serde(rename = "FERRIT_DEFAULT_POST_SORT")] + default_post_sort: Option, + + #[serde(rename = "FERRIT_DEFAULT_SHOW_NSFW")] + default_show_nsfw: Option, + + #[serde(rename = "FERRIT_DEFAULT_BLUR_NSFW")] + default_blur_nsfw: Option, + + #[serde(rename = "FERRIT_DEFAULT_USE_HLS")] + default_use_hls: Option, + + #[serde(rename = "FERRIT_DEFAULT_HIDE_HLS_NOTIFICATION")] + default_hide_hls_notification: Option, +} + +impl Config { + /// Load the configuration from the environment variables and the config file. + /// In the case that there are no environment variables set and there is no + /// config file, this function returns a Config that contains all None values. + pub fn load() -> Self { + // Read from ferrit.toml config file. If for any reason, it fails, the + // default `Config` is used (all None values) + let config: Config = toml::from_str(&std::fs::read_to_string("ferrit.toml").unwrap_or_default()).unwrap_or_default(); + // This function defines the order of preference - first check for + // environment variables with "FERRIT", then check for environment variables + // with "LIBREDDIT" for reverse compatibility, then check the config, then if + // both are `None`, return a `None` via the `map_or_else` function + let parse = |key: &str| -> Option { + var(key) + .ok() + .map_or_else(|| var(key.replace("FERRIT", "LIBREDDIT")).ok(), Some) + .map_or_else(|| get_setting_from_config(key, &config), Some) + }; + Self { + sfw_only: parse("FERRIT_SFW_ONLY"), + default_theme: parse("FERRIT_DEFAULT_THEME"), + default_front_page: parse("FERRIT_DEFAULT_FRONT_PAGE"), + default_layout: parse("FERRIT_DEFAULT_LAYOUT"), + default_post_sort: parse("FERRIT_DEFAULT_POST_SORT"), + default_wide: parse("FERRIT_DEFAULT_WIDE"), + default_comment_sort: parse("FERRIT_DEFAULT_COMMENT_SORT"), + default_show_nsfw: parse("FERRIT_DEFAULT_SHOW_NSFW"), + default_blur_nsfw: parse("FERRIT_DEFAULT_BLUR_NSFW"), + default_use_hls: parse("FERRIT_DEFAULT_USE_HLS"), + default_hide_hls_notification: parse("FERRIT_DEFAULT_HIDE_HLS"), + } + } +} + +fn get_setting_from_config(name: &str, config: &Config) -> Option { + match name { + "FERRIT_SFW_ONLY" => config.sfw_only.clone(), + "FERRIT_DEFAULT_THEME" => config.default_theme.clone(), + "FERRIT_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(), + "FERRIT_DEFAULT_LAYOUT" => config.default_layout.clone(), + "FERRIT_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(), + "FERRIT_DEFAULT_POST_SORT" => config.default_post_sort.clone(), + "FERRIT_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(), + "FERRIT_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(), + "FERRIT_DEFAULT_USE_HLS" => config.default_use_hls.clone(), + "FERRIT_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(), + "FERRIT_DEFAULT_WIDE" => config.default_wide.clone(), + _ => None, + } +} + +/// Retrieves setting from environment variable or config file. +pub(crate) fn get_setting(name: &str) -> Option { + get_setting_from_config(name, &CONFIG) +} + +#[cfg(test)] +use sealed_test::prelude::*; + +#[test] +#[sealed_test(env = [("FERRIT_SFW_ONLY", "1")])] +fn test_env_var() { + assert!(crate::utils::sfw_only()) +} + +#[test] +#[sealed_test(env = [("FERRIT_DEFAULT_COMMENT_SORT", "top"), ("LIBREDDIT_DEFAULT_COMMENT_SORT", "best")])] +fn test_env_precedence() { + assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} + +#[test] +#[sealed_test] +fn test_config() { + let config_to_write = r#"FERRIT_DEFAULT_COMMENT_SORT = "best""#; + std::fs::write("ferrit.toml", config_to_write).unwrap(); + assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("best".into())); +} + +#[test] +#[sealed_test(env = [("FERRIT_DEFAULT_COMMENT_SORT", "top")])] +fn test_env_config_precedence() { + let config_to_write = r#"FERRIT_DEFAULT_COMMENT_SORT = "best""#; + std::fs::write("ferrit.toml", config_to_write).unwrap(); + assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} + +#[test] +#[sealed_test(env = [("LIBREDDIT_DEFAULT_COMMENT_SORT", "top")])] +fn test_alt_env_config_precedence() { + let config_to_write = r#"FERRIT_DEFAULT_COMMENT_SORT = "best""#; + std::fs::write("Ferrit.toml", config_to_write).unwrap(); + assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("top".into())) +} diff --git a/src/main.rs b/src/main.rs index d81b341..c7160c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![allow(clippy::cmp_owned)] // Reference local files +mod config; mod duplicates; mod post; mod search; diff --git a/src/utils.rs b/src/utils.rs index caeeb78..7aa5235 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -680,8 +680,8 @@ pub fn setting(req: &Request, name: &str) -> String { req .cookie(name) .unwrap_or_else(|| { - // If there is no cookie for this setting, try receiving a default from an environment variable - if let Ok(default) = std::env::var(format!("FERRIT_DEFAULT_{}", name.to_uppercase())) { + // If there is no cookie for this setting, try receiving a default from the config + if let Some(default) = crate::config::get_setting(&format!("FERRIT_DEFAULT_{}", name.to_uppercase())) { Cookie::new(name, default) } else { Cookie::named(name) @@ -866,11 +866,10 @@ pub async fn error(req: Request, msg: impl ToString) -> Result bool { - env::var("FERRIT_SFW_ONLY").is_ok() + crate::config::get_setting("FERRIT_SFW_ONLY").is_some() } /// Render the landing page for NSFW content when the user has not enabled