]>
Commit | Line | Data |
---|---|---|
51b499db DM |
1 | //! Tools and utilities |
2 | //! | |
3 | //! This is a collection of small and useful tools. | |
6100071f WB |
4 | use std::any::Any; |
5 | use std::collections::HashMap; | |
62ee2eb4 | 6 | use std::hash::BuildHasher; |
98c259b4 | 7 | use std::fs::File; |
386990ba | 8 | use std::io::{self, BufRead, ErrorKind, Read}; |
98c259b4 | 9 | use std::os::unix::io::RawFd; |
6100071f WB |
10 | use std::path::Path; |
11 | use std::time::Duration; | |
e693818a | 12 | use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH}; |
365bb90f | 13 | |
f7d4e4b5 | 14 | use anyhow::{bail, format_err, Error}; |
af926291 | 15 | use serde_json::Value; |
c5946faf | 16 | use openssl::hash::{hash, DigestBytes, MessageDigest}; |
8a1028e0 | 17 | use percent_encoding::AsciiSet; |
0fe5d605 | 18 | |
f35197f4 WB |
19 | use proxmox::tools::vec; |
20 | ||
00ec8d16 WB |
21 | pub use proxmox::tools::fd::Fd; |
22 | ||
6100071f | 23 | pub mod acl; |
556eb70e | 24 | pub mod async_io; |
6ed25cbe | 25 | pub mod borrow; |
ec01eead | 26 | pub mod cert; |
6100071f | 27 | pub mod daemon; |
10effc98 | 28 | pub mod disks; |
b4d5787d | 29 | pub mod fs; |
4939255f | 30 | pub mod format; |
1685c2e3 | 31 | pub mod lru_cache; |
daef93f4 | 32 | pub mod runtime; |
6100071f | 33 | pub mod ticket; |
ba974798 | 34 | pub mod statistics; |
f486e9e5 | 35 | pub mod systemd; |
177a2de9 | 36 | pub mod nom; |
f1d99e3f DM |
37 | |
38 | mod wrapped_reader_stream; | |
39 | pub use wrapped_reader_stream::*; | |
dcd033a5 DM |
40 | |
41 | mod std_channel_writer; | |
42 | pub use std_channel_writer::*; | |
43 | ||
2dcdd3b4 | 44 | pub mod xattr; |
8cf6e764 | 45 | |
a650f503 DM |
46 | mod process_locker; |
47 | pub use process_locker::*; | |
48 | ||
3b151414 DM |
49 | mod file_logger; |
50 | pub use file_logger::*; | |
51 | ||
490be29e DM |
52 | mod broadcast_future; |
53 | pub use broadcast_future::*; | |
54 | ||
fded74d0 | 55 | /// The `BufferedRead` trait provides a single function |
0a72e267 DM |
56 | /// `buffered_read`. It returns a reference to an internal buffer. The |
57 | /// purpose of this traid is to avoid unnecessary data copies. | |
fded74d0 | 58 | pub trait BufferedRead { |
318564ac DM |
59 | /// This functions tries to fill the internal buffers, then |
60 | /// returns a reference to the available data. It returns an empty | |
61 | /// buffer if `offset` points to the end of the file. | |
0a72e267 DM |
62 | fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>; |
63 | } | |
64 | ||
51b499db DM |
65 | /// Split a file into equal sized chunks. The last chunk may be |
66 | /// smaller. Note: We cannot implement an `Iterator`, because iterators | |
67 | /// cannot return a borrowed buffer ref (we want zero-copy) | |
6100071f WB |
68 | pub fn file_chunker<C, R>(mut file: R, chunk_size: usize, mut chunk_cb: C) -> Result<(), Error> |
69 | where | |
70 | C: FnMut(usize, &[u8]) -> Result<bool, Error>, | |
71 | R: Read, | |
43eeef28 | 72 | { |
6100071f | 73 | const READ_BUFFER_SIZE: usize = 4 * 1024 * 1024; // 4M |
43eeef28 | 74 | |
6100071f WB |
75 | if chunk_size > READ_BUFFER_SIZE { |
76 | bail!("chunk size too large!"); | |
77 | } | |
43eeef28 | 78 | |
8ea3b1d1 | 79 | let mut buf = vec::undefined(READ_BUFFER_SIZE); |
43eeef28 DM |
80 | |
81 | let mut pos = 0; | |
82 | let mut file_pos = 0; | |
83 | loop { | |
84 | let mut eof = false; | |
85 | let mut tmp = &mut buf[..]; | |
6100071f | 86 | // try to read large portions, at least chunk_size |
43eeef28 DM |
87 | while pos < chunk_size { |
88 | match file.read(tmp) { | |
6100071f WB |
89 | Ok(0) => { |
90 | eof = true; | |
91 | break; | |
92 | } | |
43eeef28 DM |
93 | Ok(n) => { |
94 | pos += n; | |
6100071f WB |
95 | if pos > chunk_size { |
96 | break; | |
97 | } | |
43eeef28 DM |
98 | tmp = &mut tmp[n..]; |
99 | } | |
100 | Err(ref e) if e.kind() == ErrorKind::Interrupted => { /* try again */ } | |
5f0c2d56 | 101 | Err(e) => bail!("read chunk failed - {}", e.to_string()), |
43eeef28 DM |
102 | } |
103 | } | |
43eeef28 DM |
104 | let mut start = 0; |
105 | while start + chunk_size <= pos { | |
6100071f WB |
106 | if !(chunk_cb)(file_pos, &buf[start..start + chunk_size])? { |
107 | break; | |
108 | } | |
43eeef28 DM |
109 | file_pos += chunk_size; |
110 | start += chunk_size; | |
111 | } | |
112 | if eof { | |
113 | if start < pos { | |
114 | (chunk_cb)(file_pos, &buf[start..pos])?; | |
115 | //file_pos += pos - start; | |
116 | } | |
117 | break; | |
118 | } else { | |
119 | let rest = pos - start; | |
120 | if rest > 0 { | |
121 | let ptr = buf.as_mut_ptr(); | |
6100071f WB |
122 | unsafe { |
123 | std::ptr::copy_nonoverlapping(ptr.add(start), ptr, rest); | |
124 | } | |
43eeef28 DM |
125 | pos = rest; |
126 | } else { | |
127 | pos = 0; | |
128 | } | |
129 | } | |
130 | } | |
131 | ||
132 | Ok(()) | |
43eeef28 | 133 | } |
0fe5d605 | 134 | |
f5f13ebc | 135 | pub fn json_object_to_query(data: Value) -> Result<String, Error> { |
f5f13ebc DM |
136 | let mut query = url::form_urlencoded::Serializer::new(String::new()); |
137 | ||
138 | let object = data.as_object().ok_or_else(|| { | |
139 | format_err!("json_object_to_query: got wrong data type (expected object).") | |
140 | })?; | |
141 | ||
142 | for (key, value) in object { | |
143 | match value { | |
6100071f WB |
144 | Value::Bool(b) => { |
145 | query.append_pair(key, &b.to_string()); | |
146 | } | |
147 | Value::Number(n) => { | |
148 | query.append_pair(key, &n.to_string()); | |
149 | } | |
150 | Value::String(s) => { | |
151 | query.append_pair(key, &s); | |
152 | } | |
f5f13ebc DM |
153 | Value::Array(arr) => { |
154 | for element in arr { | |
155 | match element { | |
6100071f WB |
156 | Value::Bool(b) => { |
157 | query.append_pair(key, &b.to_string()); | |
158 | } | |
159 | Value::Number(n) => { | |
160 | query.append_pair(key, &n.to_string()); | |
161 | } | |
162 | Value::String(s) => { | |
163 | query.append_pair(key, &s); | |
164 | } | |
165 | _ => bail!( | |
166 | "json_object_to_query: unable to handle complex array data types." | |
167 | ), | |
f5f13ebc DM |
168 | } |
169 | } | |
170 | } | |
171 | _ => bail!("json_object_to_query: unable to handle complex data types."), | |
172 | } | |
173 | } | |
174 | ||
175 | Ok(query.finish()) | |
176 | } | |
177 | ||
0fe5d605 | 178 | pub fn required_string_param<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> { |
6100071f | 179 | match param[name].as_str() { |
0fe5d605 DM |
180 | Some(s) => Ok(s), |
181 | None => bail!("missing parameter '{}'", name), | |
182 | } | |
183 | } | |
0d38dcb4 | 184 | |
e17d5d86 DM |
185 | pub fn required_string_property<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> { |
186 | match param[name].as_str() { | |
187 | Some(s) => Ok(s), | |
188 | None => bail!("missing property '{}'", name), | |
189 | } | |
190 | } | |
191 | ||
0d38dcb4 | 192 | pub fn required_integer_param<'a>(param: &'a Value, name: &str) -> Result<i64, Error> { |
6100071f | 193 | match param[name].as_i64() { |
0d38dcb4 DM |
194 | Some(s) => Ok(s), |
195 | None => bail!("missing parameter '{}'", name), | |
f8dfbb45 DM |
196 | } |
197 | } | |
198 | ||
e17d5d86 DM |
199 | pub fn required_integer_property<'a>(param: &'a Value, name: &str) -> Result<i64, Error> { |
200 | match param[name].as_i64() { | |
201 | Some(s) => Ok(s), | |
202 | None => bail!("missing property '{}'", name), | |
203 | } | |
204 | } | |
205 | ||
f8dfbb45 | 206 | pub fn required_array_param<'a>(param: &'a Value, name: &str) -> Result<Vec<Value>, Error> { |
6100071f | 207 | match param[name].as_array() { |
f8dfbb45 DM |
208 | Some(s) => Ok(s.to_vec()), |
209 | None => bail!("missing parameter '{}'", name), | |
0d38dcb4 DM |
210 | } |
211 | } | |
383e8577 | 212 | |
e17d5d86 DM |
213 | pub fn required_array_property<'a>(param: &'a Value, name: &str) -> Result<Vec<Value>, Error> { |
214 | match param[name].as_array() { | |
215 | Some(s) => Ok(s.to_vec()), | |
216 | None => bail!("missing property '{}'", name), | |
217 | } | |
218 | } | |
219 | ||
62ee2eb4 | 220 | pub fn complete_file_name<S: BuildHasher>(arg: &str, _param: &HashMap<String, String, S>) -> Vec<String> { |
383e8577 DM |
221 | let mut result = vec![]; |
222 | ||
6100071f | 223 | use nix::fcntl::AtFlags; |
383e8577 DM |
224 | use nix::fcntl::OFlag; |
225 | use nix::sys::stat::Mode; | |
383e8577 | 226 | |
62ee2eb4 | 227 | let mut dirname = std::path::PathBuf::from(if arg.is_empty() { "./" } else { arg }); |
383e8577 DM |
228 | |
229 | let is_dir = match nix::sys::stat::fstatat(libc::AT_FDCWD, &dirname, AtFlags::empty()) { | |
230 | Ok(stat) => (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR, | |
231 | Err(_) => false, | |
232 | }; | |
233 | ||
234 | if !is_dir { | |
235 | if let Some(parent) = dirname.parent() { | |
236 | dirname = parent.to_owned(); | |
237 | } | |
238 | } | |
239 | ||
6100071f WB |
240 | let mut dir = |
241 | match nix::dir::Dir::openat(libc::AT_FDCWD, &dirname, OFlag::O_DIRECTORY, Mode::empty()) { | |
242 | Ok(d) => d, | |
243 | Err(_) => return result, | |
244 | }; | |
383e8577 DM |
245 | |
246 | for item in dir.iter() { | |
247 | if let Ok(entry) = item { | |
248 | if let Ok(name) = entry.file_name().to_str() { | |
6100071f WB |
249 | if name == "." || name == ".." { |
250 | continue; | |
251 | } | |
383e8577 DM |
252 | let mut newpath = dirname.clone(); |
253 | newpath.push(name); | |
254 | ||
6100071f WB |
255 | if let Ok(stat) = |
256 | nix::sys::stat::fstatat(libc::AT_FDCWD, &newpath, AtFlags::empty()) | |
257 | { | |
383e8577 DM |
258 | if (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR { |
259 | newpath.push(""); | |
260 | if let Some(newpath) = newpath.to_str() { | |
261 | result.push(newpath.to_owned()); | |
262 | } | |
263 | continue; | |
6100071f | 264 | } |
383e8577 DM |
265 | } |
266 | if let Some(newpath) = newpath.to_str() { | |
267 | result.push(newpath.to_owned()); | |
268 | } | |
6100071f | 269 | } |
383e8577 DM |
270 | } |
271 | } | |
272 | ||
273 | result | |
274 | } | |
443f3743 DM |
275 | |
276 | /// Scan directory for matching file names. | |
277 | /// | |
278 | /// Scan through all directory entries and call `callback()` function | |
279 | /// if the entry name matches the regular expression. This function | |
280 | /// used unix `openat()`, so you can pass absolute or relative file | |
281 | /// names. This function simply skips non-UTF8 encoded names. | |
282 | pub fn scandir<P, F>( | |
283 | dirfd: RawFd, | |
121f18ef | 284 | path: &P, |
443f3743 | 285 | regex: ®ex::Regex, |
6100071f | 286 | mut callback: F, |
443f3743 | 287 | ) -> Result<(), Error> |
6100071f WB |
288 | where |
289 | F: FnMut(RawFd, &str, nix::dir::Type) -> Result<(), Error>, | |
290 | P: ?Sized + nix::NixPath, | |
443f3743 | 291 | { |
121f18ef | 292 | for entry in self::fs::scan_subdir(dirfd, path, regex)? { |
443f3743 DM |
293 | let entry = entry?; |
294 | let file_type = match entry.file_type() { | |
295 | Some(file_type) => file_type, | |
296 | None => bail!("unable to detect file type"), | |
297 | }; | |
443f3743 | 298 | |
6100071f WB |
299 | callback( |
300 | entry.parent_fd(), | |
301 | unsafe { entry.file_name_utf8_unchecked() }, | |
302 | file_type, | |
303 | )?; | |
443f3743 DM |
304 | } |
305 | Ok(()) | |
306 | } | |
7e13b2d6 | 307 | |
c5946faf WB |
308 | /// Shortcut for md5 sums. |
309 | pub fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> { | |
310 | hash(MessageDigest::md5(), data).map_err(Error::from) | |
311 | } | |
312 | ||
7e13b2d6 | 313 | pub fn get_hardware_address() -> Result<String, Error> { |
1631c54f | 314 | static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub"; |
7e13b2d6 | 315 | |
e18a6c9e | 316 | let contents = proxmox::tools::fs::file_get_contents(FILENAME)?; |
c5946faf | 317 | let digest = md5sum(&contents)?; |
7e13b2d6 | 318 | |
c5946faf | 319 | Ok(proxmox::tools::bin_to_hex(&digest)) |
7e13b2d6 | 320 | } |
22968600 | 321 | |
af2fddea DM |
322 | pub fn assert_if_modified(digest1: &str, digest2: &str) -> Result<(), Error> { |
323 | if digest1 != digest2 { | |
6100071f | 324 | bail!("detected modified configuration - file changed by other user? Try again."); |
af2fddea DM |
325 | } |
326 | Ok(()) | |
327 | } | |
b9903d63 | 328 | |
09f12d1c | 329 | /// Extract a specific cookie from cookie header. |
b9903d63 | 330 | /// We assume cookie_name is already url encoded. |
09f12d1c | 331 | pub fn extract_cookie(cookie: &str, cookie_name: &str) -> Option<String> { |
b9903d63 | 332 | for pair in cookie.split(';') { |
b9903d63 DM |
333 | let (name, value) = match pair.find('=') { |
334 | Some(i) => (pair[..i].trim(), pair[(i + 1)..].trim()), | |
335 | None => return None, // Cookie format error | |
336 | }; | |
337 | ||
338 | if name == cookie_name { | |
8a1028e0 | 339 | use percent_encoding::percent_decode; |
b9903d63 DM |
340 | if let Ok(value) = percent_decode(value.as_bytes()).decode_utf8() { |
341 | return Some(value.into()); | |
342 | } else { | |
343 | return None; // Cookie format error | |
344 | } | |
345 | } | |
346 | } | |
347 | ||
348 | None | |
349 | } | |
af53186e DM |
350 | |
351 | pub fn join(data: &Vec<String>, sep: char) -> String { | |
af53186e DM |
352 | let mut list = String::new(); |
353 | ||
354 | for item in data { | |
62ee2eb4 | 355 | if !list.is_empty() { |
6100071f WB |
356 | list.push(sep); |
357 | } | |
af53186e DM |
358 | list.push_str(item); |
359 | } | |
360 | ||
361 | list | |
362 | } | |
ff7049d4 | 363 | |
002a191a DM |
364 | /// Detect modified configuration files |
365 | /// | |
add5861e | 366 | /// This function fails with a reasonable error message if checksums do not match. |
002a191a DM |
367 | pub fn detect_modified_configuration_file(digest1: &[u8;32], digest2: &[u8;32]) -> Result<(), Error> { |
368 | if digest1 != digest2 { | |
369 | bail!("detected modified configuration - file changed by other user? Try again."); | |
370 | } | |
371 | Ok(()) | |
372 | } | |
373 | ||
3578d99f DM |
374 | /// normalize uri path |
375 | /// | |
376 | /// Do not allow ".", "..", or hidden files ".XXXX" | |
377 | /// Also remove empty path components | |
378 | pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> { | |
3578d99f DM |
379 | let items = path.split('/'); |
380 | ||
381 | let mut path = String::new(); | |
382 | let mut components = vec![]; | |
383 | ||
384 | for name in items { | |
6100071f WB |
385 | if name.is_empty() { |
386 | continue; | |
387 | } | |
62ee2eb4 | 388 | if name.starts_with('.') { |
3578d99f DM |
389 | bail!("Path contains illegal components."); |
390 | } | |
391 | path.push('/'); | |
392 | path.push_str(name); | |
393 | components.push(name); | |
394 | } | |
395 | ||
396 | Ok((path, components)) | |
397 | } | |
398 | ||
97fab7aa | 399 | /// Helper to check result from std::process::Command output |
143b6545 DM |
400 | /// |
401 | /// The exit_code_check() function should return true if the exit code | |
402 | /// is considered successful. | |
403 | pub fn command_output( | |
404 | output: std::process::Output, | |
144006fa | 405 | exit_code_check: Option<fn(i32) -> bool>, |
143b6545 | 406 | ) -> Result<String, Error> { |
97fab7aa DM |
407 | |
408 | if !output.status.success() { | |
409 | match output.status.code() { | |
410 | Some(code) => { | |
143b6545 DM |
411 | let is_ok = match exit_code_check { |
412 | Some(check_fn) => check_fn(code), | |
413 | None => code == 0, | |
414 | }; | |
415 | if !is_ok { | |
97fab7aa DM |
416 | let msg = String::from_utf8(output.stderr) |
417 | .map(|m| if m.is_empty() { String::from("no error message") } else { m }) | |
418 | .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)")); | |
419 | ||
420 | bail!("status code: {} - {}", code, msg); | |
421 | } | |
422 | } | |
423 | None => bail!("terminated by signal"), | |
424 | } | |
425 | } | |
426 | ||
427 | let output = String::from_utf8(output.stdout)?; | |
428 | ||
429 | Ok(output) | |
430 | } | |
431 | ||
144006fa DM |
432 | pub fn run_command( |
433 | mut command: std::process::Command, | |
434 | exit_code_check: Option<fn(i32) -> bool>, | |
435 | ) -> Result<String, Error> { | |
436 | ||
437 | let output = command.output() | |
438 | .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?; | |
439 | ||
440 | let output = crate::tools::command_output(output, exit_code_check) | |
441 | .map_err(|err| format_err!("command {:?} failed - {}", command, err))?; | |
442 | ||
443 | Ok(output) | |
444 | } | |
97fab7aa | 445 | |
ff7049d4 | 446 | pub fn fd_change_cloexec(fd: RawFd, on: bool) -> Result<(), Error> { |
6100071f | 447 | use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD}; |
ff7049d4 WB |
448 | let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?) |
449 | .ok_or_else(|| format_err!("unhandled file flags"))?; // nix crate is stupid this way... | |
450 | flags.set(FdFlag::FD_CLOEXEC, on); | |
451 | fcntl(fd, F_SETFD(flags))?; | |
452 | Ok(()) | |
453 | } | |
9136f857 | 454 | |
9136f857 DM |
455 | static mut SHUTDOWN_REQUESTED: bool = false; |
456 | ||
457 | pub fn request_shutdown() { | |
6100071f WB |
458 | unsafe { |
459 | SHUTDOWN_REQUESTED = true; | |
460 | } | |
7a630df7 | 461 | crate::server::server_shutdown(); |
9136f857 DM |
462 | } |
463 | ||
464 | #[inline(always)] | |
465 | pub fn shutdown_requested() -> bool { | |
466 | unsafe { SHUTDOWN_REQUESTED } | |
467 | } | |
92da93b2 DM |
468 | |
469 | pub fn fail_on_shutdown() -> Result<(), Error> { | |
470 | if shutdown_requested() { | |
471 | bail!("Server shutdown requested - aborting task"); | |
472 | } | |
473 | Ok(()) | |
474 | } | |
d96bb7f1 | 475 | |
c4044009 WB |
476 | /// safe wrapper for `nix::unistd::pipe2` defaulting to `O_CLOEXEC` and guarding the file |
477 | /// descriptors. | |
efd1536e WB |
478 | pub fn pipe() -> Result<(Fd, Fd), Error> { |
479 | let (pin, pout) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; | |
480 | Ok((Fd(pin), Fd(pout))) | |
481 | } | |
2edc341b | 482 | |
c4044009 WB |
483 | /// safe wrapper for `nix::sys::socket::socketpair` defaulting to `O_CLOEXEC` and guarding the file |
484 | /// descriptors. | |
485 | pub fn socketpair() -> Result<(Fd, Fd), Error> { | |
486 | use nix::sys::socket; | |
487 | let (pa, pb) = socket::socketpair( | |
488 | socket::AddressFamily::Unix, | |
489 | socket::SockType::Stream, | |
490 | None, | |
491 | socket::SockFlag::SOCK_CLOEXEC, | |
492 | )?; | |
493 | Ok((Fd(pa), Fd(pb))) | |
494 | } | |
495 | ||
496 | ||
2edc341b DM |
497 | /// An easy way to convert types to Any |
498 | /// | |
499 | /// Mostly useful to downcast trait objects (see RpcEnvironment). | |
500 | pub trait AsAny { | |
dd5495d6 | 501 | fn as_any(&self) -> &dyn Any; |
2edc341b DM |
502 | } |
503 | ||
504 | impl<T: Any> AsAny for T { | |
6100071f WB |
505 | fn as_any(&self) -> &dyn Any { |
506 | self | |
507 | } | |
2edc341b | 508 | } |
8a1028e0 WB |
509 | |
510 | /// This used to be: `SIMPLE_ENCODE_SET` plus space, `"`, `#`, `<`, `>`, backtick, `?`, `{`, `}` | |
511 | pub const DEFAULT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS // 0..1f and 7e | |
512 | // The SIMPLE_ENCODE_SET adds space and anything >= 0x7e (7e itself is already included above) | |
513 | .add(0x20) | |
514 | .add(0x7f) | |
515 | // the DEFAULT_ENCODE_SET added: | |
516 | .add(b' ') | |
517 | .add(b'"') | |
518 | .add(b'#') | |
519 | .add(b'<') | |
520 | .add(b'>') | |
521 | .add(b'`') | |
522 | .add(b'?') | |
523 | .add(b'{') | |
524 | .add(b'}'); | |
386990ba WB |
525 | |
526 | /// Get an iterator over lines of a file, skipping empty lines and comments (lines starting with a | |
527 | /// `#`). | |
528 | pub fn file_get_non_comment_lines<P: AsRef<Path>>( | |
529 | path: P, | |
530 | ) -> Result<impl Iterator<Item = io::Result<String>>, Error> { | |
531 | let path = path.as_ref(); | |
532 | ||
533 | Ok(io::BufReader::new( | |
534 | File::open(path).map_err(|err| format_err!("error opening {:?}: {}", path, err))?, | |
535 | ) | |
536 | .lines() | |
537 | .filter_map(|line| match line { | |
538 | Ok(line) => { | |
539 | let line = line.trim(); | |
540 | if line.is_empty() || line.starts_with('#') { | |
541 | None | |
542 | } else { | |
543 | Some(Ok(line.to_string())) | |
544 | } | |
545 | } | |
546 | Err(err) => Some(Err(err)), | |
547 | })) | |
548 | } | |
e693818a DC |
549 | |
550 | pub fn epoch_now() -> Result<Duration, SystemTimeError> { | |
551 | SystemTime::now().duration_since(UNIX_EPOCH) | |
552 | } | |
553 | ||
554 | pub fn epoch_now_f64() -> Result<f64, SystemTimeError> { | |
555 | Ok(epoch_now()?.as_secs_f64()) | |
556 | } | |
557 | ||
558 | pub fn epoch_now_u64() -> Result<u64, SystemTimeError> { | |
559 | Ok(epoch_now()?.as_secs()) | |
560 | } | |
ac7513e3 DM |
561 | |
562 | pub fn setup_safe_path_env() { | |
563 | std::env::set_var("PATH", "/sbin:/bin:/usr/sbin:/usr/bin"); | |
564 | // Make %ENV safer - as suggested by https://perldoc.perl.org/perlsec.html | |
565 | for name in &["IFS", "CDPATH", "ENV", "BASH_ENV"] { | |
566 | std::env::remove_var(name); | |
567 | } | |
568 | } | |
cdf1da28 WB |
569 | |
570 | pub fn strip_ascii_whitespace(line: &[u8]) -> &[u8] { | |
571 | let line = match line.iter().position(|&b| !b.is_ascii_whitespace()) { | |
572 | Some(n) => &line[n..], | |
573 | None => return &[], | |
574 | }; | |
575 | match line.iter().rev().position(|&b| !b.is_ascii_whitespace()) { | |
576 | Some(n) => &line[..(line.len() - n)], | |
577 | None => &[], | |
578 | } | |
579 | } |