1#![forbid(unsafe_code)]
25#![warn(missing_docs)]
26
27mod apply;
28mod error;
29mod lex;
30mod parse;
31mod preserve;
32mod schema;
33mod size;
34
35pub use schema::{
36 AdvancedSection, AppendFsync, Config, ConfigError, EvictionPolicy, ExpirySection, LogLevel,
37 LogOutput, LogSection, MemorySection, NotificationFlags, NotificationSection,
38 PersistenceSection, ServerSection, SlowlogSection, parse_notification_flags,
39};
40pub use size::parse_size;
41
42use std::path::{Path, PathBuf};
43
44const AUTODETECT_PATHS: &[&str] = &[
46 "./kevy.toml",
47 "/etc/kevy/kevy.toml",
48];
49
50impl Config {
51 pub fn load(path: Option<&Path>) -> Result<Self, ConfigError> {
61 if let Some(p) = path {
62 let text = read_required(p)?;
63 return Self::from_toml_str(&text, Some(p));
64 }
65 if let Some(p) = autodetect() {
66 let text = read_required(&p)?;
67 let mut cfg = Self::from_toml_str(&text, Some(&p))?;
68 cfg.source_path = Some(p);
69 return Ok(cfg);
70 }
71 Ok(Self::default())
72 }
73
74 pub fn from_toml_str(text: &str, source_path: Option<&Path>) -> Result<Self, ConfigError> {
77 let mut cfg = Self::default();
78 let items = parse::parse(text)?;
79 for item in items {
80 cfg.apply_item(item)?;
81 }
82 if let Some(p) = source_path {
83 cfg.source_path = Some(p.to_path_buf());
84 }
85 Ok(cfg)
86 }
87
88 pub fn merge_env<I, K, V>(&mut self, env: I) -> Result<(), ConfigError>
97 where
98 I: IntoIterator<Item = (K, V)>,
99 K: AsRef<str>,
100 V: AsRef<str>,
101 {
102 for (k, v) in env {
103 self.apply_env_var(k.as_ref(), v.as_ref())?;
104 }
105 Ok(())
106 }
107
108 pub fn merge_cli(&mut self, cli: CliOverrides) -> Result<(), ConfigError> {
113 if let Some(bind) = cli.bind {
114 self.server.bind = bind;
115 }
116 if let Some(port) = cli.port {
117 self.server.port = port;
118 }
119 if let Some(t) = cli.threads {
120 self.server.threads = t;
121 }
122 if let Some(d) = cli.data_dir {
123 self.server.data_dir = d;
124 }
125 if let Some(aof) = cli.aof {
126 self.persistence.aof = aof;
127 }
128 if let Some(cluster) = cli.cluster {
129 self.cluster.enabled = cluster;
130 }
131 Ok(())
132 }
133
134 pub fn to_toml_string(&self) -> String {
143 use std::fmt::Write;
144 let mut out = String::new();
145 let [a, b, c, d] = self.server.bind;
146 let _ = writeln!(out, "[server]");
147 let _ = writeln!(out, "bind = \"{a}.{b}.{c}.{d}\"");
148 let _ = writeln!(out, "port = {}", self.server.port);
149 let _ = writeln!(out, "threads = {}", self.server.threads);
150 let _ = writeln!(
151 out,
152 "data_dir = \"{}\"",
153 escape_toml_basic_string(&self.server.data_dir.display().to_string()),
154 );
155 let _ = writeln!(out);
156 let _ = writeln!(out, "[persistence]");
157 let _ = writeln!(out, "aof = {}", self.persistence.aof);
158 let _ = writeln!(
159 out,
160 "appendfsync = \"{}\"",
161 self.persistence.appendfsync.as_str(),
162 );
163 let _ = writeln!(
164 out,
165 "auto_aof_rewrite_percentage = {}",
166 self.persistence.auto_aof_rewrite_percentage,
167 );
168 let _ = writeln!(
169 out,
170 "auto_aof_rewrite_min_size = {}",
171 self.persistence.auto_aof_rewrite_min_size,
172 );
173 let _ = writeln!(out);
174 let _ = writeln!(out, "[memory]");
175 let _ = writeln!(out, "maxmemory = {}", self.memory.maxmemory);
176 let _ = writeln!(
177 out,
178 "maxmemory_policy = \"{}\"",
179 self.memory.maxmemory_policy.as_str(),
180 );
181 let _ = writeln!(out);
182 let _ = writeln!(out, "[expiry]");
183 let _ = writeln!(out, "hz = {}", self.expiry.hz);
184 let _ = writeln!(out, "sample = {}", self.expiry.sample);
185 let _ = writeln!(out);
186 let _ = writeln!(out, "[log]");
187 let _ = writeln!(out, "level = \"{}\"", self.log.level.as_str());
188 let _ = writeln!(
189 out,
190 "output = \"{}\"",
191 escape_toml_basic_string(&self.log.output.as_str()),
192 );
193 let _ = writeln!(out);
194 let _ = writeln!(out, "[notification]");
195 let _ = writeln!(
196 out,
197 "notify_keyspace_events = \"{}\"",
198 escape_toml_basic_string(&self.notification.notify_keyspace_events),
199 );
200 let _ = writeln!(out);
201 let _ = writeln!(out, "[advanced]");
202 let _ = writeln!(out, "spin_limit = {}", self.advanced.spin_limit);
203 let _ = writeln!(out, "park_timeout_ms = {}", self.advanced.park_timeout_ms);
204 let _ = writeln!(out, "tick_check_every = {}", self.advanced.tick_check_every);
205 let _ = writeln!(out, "ring_capacity = {}", self.advanced.ring_capacity);
206 let _ = writeln!(out);
207 let _ = writeln!(out, "[slowlog]");
208 let _ = writeln!(
209 out,
210 "slower_than_micros = {}",
211 self.slowlog.slower_than_micros,
212 );
213 let _ = writeln!(out, "max_len = {}", self.slowlog.max_len);
214 out
215 }
216}
217
218fn escape_toml_basic_string(s: &str) -> String {
223 let mut out = String::with_capacity(s.len());
224 for c in s.chars() {
225 match c {
226 '\\' => out.push_str("\\\\"),
227 '"' => out.push_str("\\\""),
228 other => out.push(other),
229 }
230 }
231 out
232}
233
234#[derive(Default, Debug, Clone, PartialEq, Eq)]
239pub struct CliOverrides {
240 pub bind: Option<[u8; 4]>,
242 pub port: Option<u16>,
244 pub threads: Option<usize>,
246 pub data_dir: Option<PathBuf>,
248 pub aof: Option<bool>,
250 pub cluster: Option<bool>,
252}
253
254fn read_required(p: &Path) -> Result<String, ConfigError> {
255 std::fs::read_to_string(p).map_err(|e| ConfigError::IoOpen {
256 path: p.to_path_buf(),
257 err: e.to_string(),
258 })
259}
260
261fn autodetect() -> Option<PathBuf> {
262 if let Ok(dir) = std::env::var("KEVY_DIR") {
263 let p = PathBuf::from(dir).join("kevy.toml");
264 if p.exists() {
265 return Some(p);
266 }
267 }
268 for relative in AUTODETECT_PATHS {
269 let p = PathBuf::from(relative);
270 if p.exists() {
271 return Some(p);
272 }
273 }
274 None
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn defaults_match_documented_values() {
283 let cfg = Config::default();
284 assert_eq!(cfg.server.bind, [127, 0, 0, 1]);
285 assert_eq!(cfg.server.port, 6004);
286 assert_eq!(cfg.server.threads, 0);
287 assert!(cfg.persistence.aof);
288 assert_eq!(cfg.persistence.appendfsync, AppendFsync::EverySec);
289 assert_eq!(cfg.memory.maxmemory, 0);
290 assert_eq!(cfg.memory.maxmemory_policy, EvictionPolicy::NoEviction);
291 assert_eq!(cfg.expiry.hz, 10);
292 assert_eq!(cfg.expiry.sample, 20);
293 assert_eq!(cfg.log.level, LogLevel::Info);
294 }
295
296 #[test]
297 fn cli_overrides_apply_in_order() {
298 let mut cfg = Config::default();
299 let cli = CliOverrides {
300 bind: Some([0, 0, 0, 0]),
301 port: Some(7000),
302 threads: Some(4),
303 ..CliOverrides::default()
304 };
305 cfg.merge_cli(cli).unwrap();
306 assert_eq!(cfg.server.bind, [0, 0, 0, 0]);
307 assert_eq!(cfg.server.port, 7000);
308 assert_eq!(cfg.server.threads, 4);
309 }
310
311 #[test]
312 fn env_overrides_apply() {
313 let mut cfg = Config::default();
314 cfg.merge_env([
315 ("KEVY_BIND", "0.0.0.0"),
316 ("KEVY_PORT", "7001"),
317 ("UNRELATED_VAR", "ignored"),
318 ])
319 .unwrap();
320 assert_eq!(cfg.server.bind, [0, 0, 0, 0]);
321 assert_eq!(cfg.server.port, 7001);
322 }
323
324 #[test]
325 fn to_toml_string_round_trips_through_parser() {
326 let mut original = Config::default();
327 original.server.bind = [10, 0, 0, 1];
328 original.server.port = 7779;
329 original.server.threads = 4;
330 original.server.data_dir = PathBuf::from("/var/lib/kevy");
331 original.persistence.aof = false;
332 original.persistence.appendfsync = AppendFsync::Always;
333 original.persistence.auto_aof_rewrite_percentage = 200;
334 original.persistence.auto_aof_rewrite_min_size = 128 * 1024 * 1024;
335 original.memory.maxmemory = 4 * 1024 * 1024 * 1024;
336 original.memory.maxmemory_policy = EvictionPolicy::AllKeysLfu;
337 original.expiry.hz = 100;
338 original.expiry.sample = 50;
339 original.log.level = LogLevel::Warn;
340 original.log.output = LogOutput::File(PathBuf::from("/var/log/kevy.log"));
341
342 let toml_text = original.to_toml_string();
343 let mut reparsed = Config::from_toml_str(&toml_text, None).unwrap_or_else(|e| {
344 panic!("to_toml_string output did not reparse: {e}\n--- TOML ---\n{toml_text}")
345 });
346 reparsed.source_path = original.source_path.clone();
349 assert_eq!(original, reparsed);
350 }
351
352 #[test]
353 fn to_toml_string_escapes_quotes_and_backslashes_in_paths() {
354 let mut cfg = Config::default();
355 cfg.server.data_dir = PathBuf::from(r#"/path with "quote" and \back"#);
356 let text = cfg.to_toml_string();
357 assert!(
358 text.contains(r#"data_dir = "/path with \"quote\" and \\back""#),
359 "did not escape correctly: {text}"
360 );
361 let reparsed = Config::from_toml_str(&text, None).expect("reparse");
363 assert_eq!(reparsed.server.data_dir, cfg.server.data_dir);
364 }
365}