1 | use std::fs::{read_link, read_to_string}; |
2 | |
3 | pub(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 | |
9 | fn 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 | |
17 | fn 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 | |
49 | mod 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 | |