rusty_time_core/
config.rs1use core::fmt;
7
8#[derive(Clone, Debug, PartialEq)]
9pub struct ServerDirective {
10 pub host: String,
11 pub is_pool: bool,
12 pub iburst: bool,
13 pub min_poll: Option<i8>,
14 pub max_poll: Option<i8>,
15}
16
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct Config {
19 pub servers: Vec<ServerDirective>,
20 pub makestep: Option<(f64, u32)>,
22 pub max_slew_ppm: Option<f64>,
24 pub allow: Vec<String>,
25 pub deny: Vec<String>,
26 pub ignored: Vec<(usize, String)>,
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub struct ConfigError {
33 pub line: usize,
34 pub message: String,
35}
36
37impl fmt::Display for ConfigError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(f, "config line {}: {}", self.line, self.message)
40 }
41}
42
43impl std::error::Error for ConfigError {}
44
45pub fn parse(text: &str) -> Result<Config, ConfigError> {
46 let mut cfg = Config::default();
47 for (idx, raw) in text.lines().enumerate() {
48 let line_no = idx + 1;
49 let line = raw.trim();
50 if line.is_empty() || matches!(line.as_bytes()[0], b'!' | b';' | b'#' | b'%') {
52 continue;
53 }
54 let mut words = line.split_whitespace();
55 let Some(directive) = words.next() else {
56 continue;
57 };
58 let args: Vec<&str> = words.collect();
59 match directive.to_ascii_lowercase().as_str() {
60 d @ ("server" | "pool") => {
61 let host = args
62 .first()
63 .ok_or_else(|| err(line_no, format!("{d} needs a host")))?;
64 let mut s = ServerDirective {
65 host: (*host).to_string(),
66 is_pool: d == "pool",
67 iburst: false,
68 min_poll: None,
69 max_poll: None,
70 };
71 let mut rest = args[1..].iter();
72 while let Some(opt) = rest.next() {
73 match opt.to_ascii_lowercase().as_str() {
74 "iburst" => s.iburst = true,
75 "minpoll" => {
76 s.min_poll = Some(parse_num(line_no, "minpoll", rest.next().copied())?)
77 }
78 "maxpoll" => {
79 s.max_poll = Some(parse_num(line_no, "maxpoll", rest.next().copied())?)
80 }
81 other => {
82 cfg.ignored.push((line_no, format!("{d} option '{other}'")));
83 if matches!(other, "key" | "maxdelay" | "maxdelayratio" | "presend") {
85 let _ = rest.next();
86 }
87 }
88 }
89 }
90 cfg.servers.push(s);
91 }
92 "makestep" => {
93 let threshold: f64 =
94 parse_num(line_no, "makestep threshold", args.first().copied())?;
95 let limit: i64 = parse_num(line_no, "makestep limit", args.get(1).copied())?;
96 let limit = if limit < 0 { u32::MAX } else { limit as u32 };
98 cfg.makestep = Some((threshold, limit));
99 }
100 "maxslewrate" => {
101 cfg.max_slew_ppm = Some(parse_num(line_no, "maxslewrate", args.first().copied())?);
102 }
103 "allow" => cfg
104 .allow
105 .push(args.first().copied().unwrap_or("all").to_string()),
106 "deny" => cfg
107 .deny
108 .push(args.first().copied().unwrap_or("all").to_string()),
109 other => {
110 cfg.ignored.push((line_no, other.to_string()));
111 }
112 }
113 }
114 Ok(cfg)
115}
116
117fn err(line: usize, message: String) -> ConfigError {
118 ConfigError { line, message }
119}
120
121fn parse_num<T: core::str::FromStr>(
122 line: usize,
123 what: &str,
124 value: Option<&str>,
125) -> Result<T, ConfigError> {
126 let v = value.ok_or_else(|| err(line, format!("{what} needs a value")))?;
127 v.parse()
128 .map_err(|_| err(line, format!("{what}: cannot parse '{v}'")))
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn parses_typical_chrony_conf() {
137 let text = "\
138# NTP servers
139pool 2.pool.ntp.org iburst
140server ntp.example.com iburst minpoll 4 maxpoll 8
141makestep 1.0 3
142maxslewrate 83333.0
143driftfile /var/lib/chrony/drift
144rtcsync
145allow 192.168.0.0/16
146";
147 let cfg = parse(text).expect("parse");
148 assert_eq!(cfg.servers.len(), 2);
149 assert!(cfg.servers[0].is_pool && cfg.servers[0].iburst);
150 assert_eq!(cfg.servers[1].min_poll, Some(4));
151 assert_eq!(cfg.makestep, Some((1.0, 3)));
152 assert_eq!(cfg.max_slew_ppm, Some(83333.0));
153 assert_eq!(cfg.allow, vec!["192.168.0.0/16".to_string()]);
154 assert_eq!(cfg.ignored.len(), 2);
156 assert_eq!(cfg.ignored[0], (6, "driftfile".to_string()));
157 }
158
159 #[test]
160 fn negative_makestep_limit_means_always() {
161 let cfg = parse("makestep 0.1 -1").expect("parse");
162 assert_eq!(cfg.makestep, Some((0.1, u32::MAX)));
163 }
164
165 #[test]
166 fn bad_value_is_an_error_with_line() {
167 let e = parse("server\n").expect_err("should fail");
168 assert_eq!(e.line, 1);
169 let e = parse("# ok\nmakestep abc 3").expect_err("should fail");
170 assert_eq!(e.line, 2);
171 }
172}