1use std::fs::{read_link, read_to_string};
2
3pub(crate) fn get_timezone_inner() -> Result<String, crate::GetTimezoneError> {
4 etc_localtime()
5 .or_else(|_| etc_timezone())
6 .or_else(|_| openwrt::etc_config_system())
7}
8
9fn etc_timezone() -> Result<String, crate::GetTimezoneError> {
10 // see https://stackoverflow.com/a/12523283
11 let mut contents: String = read_to_string(path:"/etc/timezone")?;
12 // Trim to the correct length without allocating.
13 contents.truncate(new_len:contents.trim_end().len());
14 Ok(contents)
15}
16
17fn etc_localtime() -> Result<String, crate::GetTimezoneError> {
18 // Per <https://www.man7.org/linux/man-pages/man5/localtime.5.html>:
19 // “ The /etc/localtime file configures the system-wide timezone of the local system that is
20 // used by applications for presentation to the user. It should be an absolute or relative
21 // symbolic link pointing to /usr/share/zoneinfo/, followed by a timezone identifier such as
22 // "Europe/Berlin" or "Etc/UTC". The resulting link should lead to the corresponding binary
23 // tzfile(5) timezone data for the configured timezone. ”
24
25 // Systemd does not canonicalize the link, but only checks if it is prefixed by
26 // "/usr/share/zoneinfo/" or "../usr/share/zoneinfo/". So we do the same.
27 // <https://github.com/systemd/systemd/blob/9102c625a673a3246d7e73d8737f3494446bad4e/src/basic/time-util.c#L1493>
28
29 const PREFIXES: &[&str] = &[
30 "/usr/share/zoneinfo/", // absolute path
31 "../usr/share/zoneinfo/", // relative path
32 "/etc/zoneinfo/", // absolute path for NixOS
33 "../etc/zoneinfo/", // relative path for NixOS
34 ];
35 let mut s = read_link("/etc/localtime")?
36 .into_os_string()
37 .into_string()
38 .map_err(|_| crate::GetTimezoneError::FailedParsingString)?;
39 for &prefix in PREFIXES {
40 if s.starts_with(prefix) {
41 // Trim to the correct length without allocating.
42 s.replace_range(..prefix.len(), "");
43 return Ok(s);
44 }
45 }
46 Err(crate::GetTimezoneError::FailedParsingString)
47}
48
49mod openwrt {
50 use std::io::BufRead;
51 use std::{fs, io, iter};
52
53 pub(crate) fn etc_config_system() -> Result<String, crate::GetTimezoneError> {
54 let f = fs::OpenOptions::new()
55 .read(true)
56 .open("/etc/config/system")?;
57 let mut f = io::BufReader::new(f);
58 let mut in_system_section = false;
59 let mut line = String::with_capacity(80);
60
61 // prefer option "zonename" (IANA time zone) over option "timezone" (POSIX time zone)
62 let mut timezone = None;
63 loop {
64 line.clear();
65 f.read_line(&mut line)?;
66 if line.is_empty() {
67 break;
68 }
69
70 let mut iter = IterWords(&line);
71 let mut next = || iter.next().transpose();
72
73 if let Some(keyword) = next()? {
74 if keyword == "config" {
75 in_system_section = next()? == Some("system") && next()?.is_none();
76 } else if in_system_section && keyword == "option" {
77 if let Some(key) = next()? {
78 if key == "zonename" {
79 if let (Some(zonename), None) = (next()?, next()?) {
80 return Ok(zonename.to_owned());
81 }
82 } else if key == "timezone" {
83 if let (Some(value), None) = (next()?, next()?) {
84 timezone = Some(value.to_owned());
85 }
86 }
87 }
88 }
89 }
90 }
91
92 timezone.ok_or_else(|| crate::GetTimezoneError::OsError)
93 }
94
95 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
96 struct BrokenQuote;
97
98 impl From<BrokenQuote> for crate::GetTimezoneError {
99 fn from(_: BrokenQuote) -> Self {
100 crate::GetTimezoneError::FailedParsingString
101 }
102 }
103
104 /// Iterated over all words in a OpenWRT config line.
105 struct IterWords<'a>(&'a str);
106
107 impl<'a> Iterator for IterWords<'a> {
108 type Item = Result<&'a str, BrokenQuote>;
109
110 fn next(&mut self) -> Option<Self::Item> {
111 match read_word(self.0) {
112 Ok(Some((item, tail))) => {
113 self.0 = tail;
114 Some(Ok(item))
115 }
116 Ok(None) => {
117 self.0 = "";
118 None
119 }
120 Err(err) => {
121 self.0 = "";
122 Some(Err(err))
123 }
124 }
125 }
126 }
127
128 impl iter::FusedIterator for IterWords<'_> {}
129
130 /// Read the next word in a OpenWRT config line. Strip any surrounding quotation marks.
131 ///
132 /// Returns
133 ///
134 /// * a tuple `Some((word, remaining_line))` if found,
135 /// * `None` if the line is exhausted, or
136 /// * `Err(BrokenQuote)` if the line could not be parsed.
137 #[allow(clippy::manual_strip)] // needs to be compatile to 1.36
138 fn read_word(s: &str) -> Result<Option<(&str, &str)>, BrokenQuote> {
139 let s = s.trim_start();
140 if s.is_empty() || s.starts_with('#') {
141 Ok(None)
142 } else if s.starts_with('\'') {
143 let mut iter = s[1..].splitn(2, '\'');
144 match (iter.next(), iter.next()) {
145 (Some(item), Some(tail)) => Ok(Some((item, tail))),
146 _ => Err(BrokenQuote),
147 }
148 } else if s.starts_with('"') {
149 let mut iter = s[1..].splitn(2, '"');
150 match (iter.next(), iter.next()) {
151 (Some(item), Some(tail)) => Ok(Some((item, tail))),
152 _ => Err(BrokenQuote),
153 }
154 } else {
155 let mut iter = s.splitn(2, |c: char| c.is_whitespace());
156 match (iter.next(), iter.next()) {
157 (Some(item), Some(tail)) => Ok(Some((item, tail))),
158 _ => Ok(Some((s, ""))),
159 }
160 }
161 }
162
163 #[cfg(test)]
164 #[test]
165 fn test_read_word() {
166 assert_eq!(
167 read_word(" option timezone 'CST-8'\n").unwrap(),
168 Some(("option", "timezone 'CST-8'\n")),
169 );
170 assert_eq!(
171 read_word("timezone 'CST-8'\n").unwrap(),
172 Some(("timezone", "'CST-8'\n")),
173 );
174 assert_eq!(read_word("'CST-8'\n").unwrap(), Some(("CST-8", "\n")));
175 assert_eq!(read_word("\n").unwrap(), None);
176
177 assert_eq!(
178 read_word(r#""time 'Zone'""#).unwrap(),
179 Some(("time 'Zone'", "")),
180 );
181
182 assert_eq!(read_word("'CST-8").unwrap_err(), BrokenQuote);
183 }
184}
185