/rust/registry/src/index.crates.io-6f17d22bba15001f/iana-time-zone-0.1.63/src/tz_linux.rs
Line | Count | Source (jump to first uncovered line) |
1 | | use std::fs::{read_link, read_to_string}; |
2 | | |
3 | 0 | pub(crate) fn get_timezone_inner() -> Result<String, crate::GetTimezoneError> { |
4 | 0 | etc_localtime() |
5 | 0 | .or_else(|_| etc_timezone()) |
6 | 0 | .or_else(|_| openwrt::etc_config_system()) |
7 | 0 | } |
8 | | |
9 | 0 | fn etc_timezone() -> Result<String, crate::GetTimezoneError> { |
10 | | // see https://stackoverflow.com/a/12523283 |
11 | 0 | let mut contents = read_to_string("/etc/timezone")?; |
12 | | // Trim to the correct length without allocating. |
13 | 0 | contents.truncate(contents.trim_end().len()); |
14 | 0 | Ok(contents) |
15 | 0 | } |
16 | | |
17 | 0 | 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 | 0 | let mut s = read_link("/etc/localtime")? |
36 | 0 | .into_os_string() |
37 | 0 | .into_string() |
38 | 0 | .map_err(|_| crate::GetTimezoneError::FailedParsingString)?; |
39 | 0 | for &prefix in PREFIXES { |
40 | 0 | if s.starts_with(prefix) { |
41 | | // Trim to the correct length without allocating. |
42 | 0 | s.replace_range(..prefix.len(), ""); |
43 | 0 | return Ok(s); |
44 | 0 | } |
45 | | } |
46 | 0 | Err(crate::GetTimezoneError::FailedParsingString) |
47 | 0 | } |
48 | | |
49 | | mod openwrt { |
50 | | use std::io::BufRead; |
51 | | use std::{fs, io, iter}; |
52 | | |
53 | 0 | pub(crate) fn etc_config_system() -> Result<String, crate::GetTimezoneError> { |
54 | 0 | let f = fs::OpenOptions::new() |
55 | 0 | .read(true) |
56 | 0 | .open("/etc/config/system")?; |
57 | 0 | let mut f = io::BufReader::new(f); |
58 | 0 | let mut in_system_section = false; |
59 | 0 | let mut line = String::with_capacity(80); |
60 | 0 |
|
61 | 0 | // prefer option "zonename" (IANA time zone) over option "timezone" (POSIX time zone) |
62 | 0 | let mut timezone = None; |
63 | | loop { |
64 | 0 | line.clear(); |
65 | 0 | f.read_line(&mut line)?; |
66 | 0 | if line.is_empty() { |
67 | 0 | break; |
68 | 0 | } |
69 | 0 |
|
70 | 0 | let mut iter = IterWords(&line); |
71 | 0 | let mut next = || iter.next().transpose(); |
72 | | |
73 | 0 | if let Some(keyword) = next()? { |
74 | 0 | if keyword == "config" { |
75 | 0 | in_system_section = next()? == Some("system") && next()?.is_none(); |
76 | 0 | } else if in_system_section && keyword == "option" { |
77 | 0 | if let Some(key) = next()? { |
78 | 0 | if key == "zonename" { |
79 | 0 | if let (Some(zonename), None) = (next()?, next()?) { |
80 | 0 | return Ok(zonename.to_owned()); |
81 | 0 | } |
82 | 0 | } else if key == "timezone" { |
83 | 0 | if let (Some(value), None) = (next()?, next()?) { |
84 | 0 | timezone = Some(value.to_owned()); |
85 | 0 | } |
86 | 0 | } |
87 | 0 | } |
88 | 0 | } |
89 | 0 | } |
90 | | } |
91 | | |
92 | 0 | timezone.ok_or(crate::GetTimezoneError::OsError) |
93 | 0 | } |
94 | | |
95 | | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
96 | | struct BrokenQuote; |
97 | | |
98 | | impl From<BrokenQuote> for crate::GetTimezoneError { |
99 | 0 | fn from(_: BrokenQuote) -> Self { |
100 | 0 | crate::GetTimezoneError::FailedParsingString |
101 | 0 | } |
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 | 0 | fn next(&mut self) -> Option<Self::Item> { |
111 | 0 | match read_word(self.0) { |
112 | 0 | Ok(Some((item, tail))) => { |
113 | 0 | self.0 = tail; |
114 | 0 | Some(Ok(item)) |
115 | | } |
116 | | Ok(None) => { |
117 | 0 | self.0 = ""; |
118 | 0 | None |
119 | | } |
120 | 0 | Err(err) => { |
121 | 0 | self.0 = ""; |
122 | 0 | Some(Err(err)) |
123 | | } |
124 | | } |
125 | 0 | } |
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 | 0 | fn read_word(s: &str) -> Result<Option<(&str, &str)>, BrokenQuote> { |
139 | 0 | let s = s.trim_start(); |
140 | 0 | if s.is_empty() || s.starts_with('#') { |
141 | 0 | Ok(None) |
142 | 0 | } else if s.starts_with('\'') { |
143 | 0 | let mut iter = s[1..].splitn(2, '\''); |
144 | 0 | match (iter.next(), iter.next()) { |
145 | 0 | (Some(item), Some(tail)) => Ok(Some((item, tail))), |
146 | 0 | _ => Err(BrokenQuote), |
147 | | } |
148 | 0 | } else if s.starts_with('"') { |
149 | 0 | let mut iter = s[1..].splitn(2, '"'); |
150 | 0 | match (iter.next(), iter.next()) { |
151 | 0 | (Some(item), Some(tail)) => Ok(Some((item, tail))), |
152 | 0 | _ => Err(BrokenQuote), |
153 | | } |
154 | | } else { |
155 | 0 | let mut iter = s.splitn(2, |c: char| c.is_whitespace()); |
156 | 0 | match (iter.next(), iter.next()) { |
157 | 0 | (Some(item), Some(tail)) => Ok(Some((item, tail))), |
158 | 0 | _ => Ok(Some((s, ""))), |
159 | | } |
160 | | } |
161 | 0 | } |
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 | | } |