Coverage Report

Created: 2025-08-25 06:27

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