| 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 | |