ahtml_from_markdown/
util.rs

1use std::{
2    borrow::Borrow,
3    collections::{
4        btree_map,
5        hash_map::{Entry, OccupiedEntry},
6        BTreeMap, HashMap,
7    },
8    env::VarError,
9    ffi::{OsStr, OsString},
10    fmt::{Debug, Display},
11    fs::create_dir_all,
12    hash::Hash,
13    path::PathBuf,
14    time::Duration,
15};
16
17use anyhow::{anyhow, bail, Context, Result};
18use num::CheckedAdd;
19
20/// Return name of the enum value, without the rest of its Debug
21/// serialisation. Slower than it needed to be given better ways, but
22/// what would those be?
23pub fn enum_name<T: Debug>(v: T) -> String {
24    let mut s = format!("{v:?}");
25    if let Some(i) = s.find(|c| c == '(') {
26        s.shrink_to(i);
27        s
28    } else {
29        s
30    }
31}
32
33/// Return last element of a Vec, first creating it if not present.
34pub fn autovivify_last<T>(v: &mut Vec<T>, create: impl FnOnce() -> T) -> &mut T {
35    let v: *mut _ = v; // Work around serious rustc deficiency?
36    if let Some(last) = unsafe { &mut *v }.last_mut() {
37        last
38    } else {
39        unsafe { &mut *v }.push(create());
40        unsafe { &mut *v }.last_mut().unwrap()
41    }
42}
43
44// XX I have *done* these before
45// fn str_skip_while(s: &str, pred: impl Fn(char) -> bool) -> &str {
46//     if let Some(i) = s.find(|c| c != '/') {
47//         &s[i..]
48//     } else {
49//         s
50//     }
51// }
52
53// fn rootbased_to_relative(s: &str) -> &str {
54//     str_skip_while(s, |c| c != '/')
55// }
56
57// A HashMap::get_mut variant that allows to work around the issue
58// that rustc (pre polonius) does not let go of the reference in the
59// None case; so we use an Err instead and pick up the reference from
60// there.
61pub fn hashmap_get_mut<'m, K: Eq + Hash, P: Eq + Hash + ?Sized, V>(
62    m: &'m mut HashMap<K, V>,
63    k: &P,
64) -> Result<&'m mut V, &'m mut HashMap<K, V>>
65where
66    K: Borrow<P>,
67{
68    let pm: *mut _ = m;
69    // Safe because in the true branch we track using the lifetimes
70    // (just like in the original get_mut), and in the false branch we
71    // just pass the input value.
72    if let Some(v) = unsafe { &mut *pm }.get_mut(k) {
73        Ok(v)
74    } else {
75        Err(unsafe { &mut *pm })
76    }
77}
78
79// Same (see hashmap_get_mut) for BTreeMap.
80pub fn btreemap_get_mut<'m, K: Ord, P: Ord + ?Sized, V>(
81    m: &'m mut BTreeMap<K, V>,
82    k: &P,
83) -> Result<&'m mut V, &'m mut BTreeMap<K, V>>
84where
85    K: Borrow<P>,
86{
87    let pm: *mut _ = m;
88    // Safe because in the true branch we track using the lifetimes
89    // (just like in the original get_mut), and in the false branch we
90    // just pass the input value.
91    if let Some(v) = unsafe { &mut *pm }.get_mut(k) {
92        Ok(v)
93    } else {
94        Err(unsafe { &mut *pm })
95    }
96}
97
98// Modified copy of #[unstable(feature = "map_try_insert", issue =
99// "82766")] from
100// https://doc.rust-lang.org/src/std/collections/hash/map.rs.html#1132-1137,
101// avoiding OccupiedError because that's also unstable. FUTURE:
102// replace with try_insert.
103pub fn hashmap_try_insert<'m, K: Eq + Hash, V>(
104    m: &'m mut HashMap<K, V>,
105    key: K,
106    value: V,
107) -> Result<&mut V, OccupiedEntry<K, V>> {
108    match m.entry(key) {
109        Entry::Occupied(entry) => Err(entry),
110        Entry::Vacant(entry) => Ok(entry.insert(value)),
111    }
112}
113
114// Modified copy of #[unstable(feature = "map_try_insert", issue =
115// "82766")] from
116// https://doc.rust-lang.org/src/alloc/collections/btree/map.rs.html#1016-1018;
117// see comments on hashmap_try_insert.
118pub fn btreemap_try_insert<'m, K: Ord, V>(
119    m: &'m mut BTreeMap<K, V>,
120    key: K,
121    value: V,
122) -> Result<&'m mut V, btree_map::OccupiedEntry<K, V>> {
123    match m.entry(key) {
124        btree_map::Entry::Occupied(entry) => Err(entry),
125        btree_map::Entry::Vacant(entry) => Ok(entry.insert(value)),
126    }
127}
128
129/// Similar to `?` in a context that returns `Option`, this propagates
130/// `None` values, but wraps them in `Ok`. I.e. behaves like `?`
131/// except if the `Option` context is wrapped in a `Result`.
132#[macro_export]
133macro_rules! or_return_none {
134    ($e:expr) => {{
135        let res = $e;
136        if let Some(val) = res {
137            val
138        } else {
139            return Ok(None);
140        }
141    }};
142}
143
144// Sigh, for exponential backoff, everybody doing this for themselves?
145pub fn duration_mul_div(orig: Duration, multiplier: u64, divider: u64) -> Option<Duration> {
146    let nanos: u64 = orig
147        .as_nanos()
148        .checked_mul(multiplier as u128)?
149        .checked_div(divider as u128)?
150        .try_into()
151        .ok()?;
152    Some(Duration::from_nanos(nanos))
153}
154
155pub fn debug_stringlikes<S: Display>(v: &[S]) -> Vec<String> {
156    v.iter().map(|s| s.to_string()).collect()
157}
158
159/// A loop that caches errors and retries with exponential
160/// backoff. (Backoff parameters and error messaging hard coded for
161/// now, as is anyhow::Result.)
162#[macro_export]
163macro_rules! loop_try {
164    ( $($body_parts:tt)* ) => {{
165        let default_error_sleep_duration = Duration::from_millis(500);
166        let mut error_sleep_duration = default_error_sleep_duration;
167        loop {
168            match (|| -> Result<()> { $($body_parts)* })() {
169                Ok(()) => {
170                    error_sleep_duration = default_error_sleep_duration;
171                }
172                Err(e) => {
173                    eprintln!("loop_try: got error {e:#}, sleeping for \
174                               {error_sleep_duration:?}");
175                    thread::sleep(error_sleep_duration);
176                    error_sleep_duration =
177                        $crate::util::duration_mul_div(error_sleep_duration,
178                                         1200,
179                                         1000)
180                        .unwrap_or(default_error_sleep_duration);
181                }
182            }
183        }
184    }}
185}
186
187#[macro_export]
188macro_rules! try_do {
189    ( $($b:tt)* ) => ( (|| { $($b)* })() )
190}
191
192#[macro_export]
193macro_rules! try_result {
194    ( $($b:tt)* ) => ( (|| -> Result<_, _> { $($b)* })() )
195}
196
197#[macro_export]
198macro_rules! try_option {
199    ( $($b:tt)* ) => ( (|| -> Option<_> { $($b)* })() )
200}
201
202/// A counter. Panics if T wraps around.
203pub fn infinite_sequence<T: CheckedAdd + Copy>(start: T, inc: T) -> impl FnMut() -> T {
204    let mut current = start;
205    move || -> T {
206        let n = current;
207        current = n.checked_add(&inc).expect("number not overflowing");
208        n
209    }
210}
211
212pub fn alphanumber(i: u32) -> String {
213    let mut s = Vec::new();
214    let mut j: i64 = i.into();
215    while j >= 0 {
216        s.push(b'a' + ((j % 26) as u8));
217        j = (j / 26) - 1;
218    }
219    s.reverse();
220    String::from_utf8(s).expect("all ascii")
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn t_alphanumber() {
229        fn t(i: u32, s: &str) {
230            assert_eq!(&alphanumber(i), s);
231        }
232        t(0, "a");
233        t(1, "b");
234        t(26, "aa");
235        t(27, "ab");
236        t(27 * 26 - 1, "zz");
237        t(27 * 26, "aaa");
238        t(27 * 26 + 1, "aab");
239        t(27 * 26 + 26, "aba");
240    }
241}
242
243pub fn osstring_into_string(s: OsString) -> Result<String> {
244    match s.into_string() {
245        Ok(s2) => Ok(s2),
246        Err(s) => bail!("can't properly decode to string {:?}", s.to_string_lossy()),
247    }
248}
249
250pub fn osstr_to_str(s: &OsStr) -> Result<&str> {
251    match s.to_str() {
252        Some(s2) => Ok(s2),
253        None => bail!("can't properly decode to string {:?}", s.to_string_lossy()),
254    }
255}
256
257pub fn program_path() -> Result<String> {
258    let path = std::env::args_os()
259        .into_iter()
260        .next()
261        .ok_or_else(|| anyhow!("missing program executable path in args_os"))?;
262    osstring_into_string(path)
263        .with_context(|| anyhow!("decoding of program executable path failed"))
264}
265
266pub fn program_name() -> Result<String> {
267    let path = std::env::args_os()
268        .into_iter()
269        .next()
270        .ok_or_else(|| anyhow!("missing program executable path in args_os"))?;
271    let pb = PathBuf::from(path);
272    let fname = pb
273        .file_name()
274        .ok_or_else(|| anyhow!("cannot get file name from path {:?}", pb.to_string_lossy()))?;
275    Ok(osstr_to_str(fname)
276        .with_context(|| anyhow!("cannot decode file name {:?}", fname.to_string_lossy()))?
277        .to_string())
278}
279
280pub fn log_basedir() -> Result<String> {
281    let logbasedir = format!(
282        "{}/log/{}",
283        std::env::var("HOME").with_context(|| anyhow!("can't get HOME env var"))?,
284        program_name()?
285    );
286    // XX todo: perms / umask!
287    create_dir_all(&logbasedir)
288        .with_context(|| anyhow!("can't create log base directory {:?}", logbasedir))?;
289    Ok(logbasedir)
290}
291
292/// Get an env var as a String; decoding failures are reported as
293/// errors. If the var is not set and no fallback was given, an error
294/// is reported as well.
295pub fn getenv_or(name: &str, fallbackvalue: Option<&str>) -> Result<String> {
296    match std::env::var(name) {
297        Ok(s) => Ok(s),
298        Err(e) => match e {
299            VarError::NotPresent => match fallbackvalue {
300                Some(v) => Ok(v.to_string()),
301                None => bail!(
302                    "{name:?} env var is missing and \
303                                   no default provided"
304                ),
305            },
306            VarError::NotUnicode(_) => bail!("{name:?} env var is not unicode"),
307        },
308    }
309}
310
311/// Get an env var as a String; decoding failures are reported as
312/// errors.
313pub fn getenv(name: &str) -> Result<Option<String>> {
314    match std::env::var(name) {
315        Ok(s) => Ok(Some(s)),
316        Err(e) => match e {
317            VarError::NotPresent => Ok(None),
318            VarError::NotUnicode(_) => bail!("{name:?} env var is not unicode"),
319        },
320    }
321}
322
323/// Like getenv but reports an error mentioning the variable name if
324/// it isn't set.
325pub fn xgetenv(name: &str) -> Result<String> {
326    getenv(name)?.ok_or_else(|| anyhow!("missing env var {name:?}"))
327}
328
329/// Retrieve a boolean from an env var. A missing env var or the
330/// strings "0", "false" or "no" represent false, a present env var
331/// with the strings "", "1", "true", or "yes" represent true.
332pub fn getenv_bool(name: &str) -> Result<bool> {
333    if let Some(s) = getenv(name)? {
334        match &*s {
335            "" => Ok(true), // really?
336            "1" => Ok(true),
337            "true" => Ok(true),
338            "yes" => Ok(true),
339            "0" => Ok(false),
340            "false" => Ok(false),
341            "no" => Ok(false),
342            _ => bail!("boolean env var {name:?} has invalid contents {s:?}"),
343        }
344    } else {
345        Ok(false)
346    }
347}
348
349/// Takes a place (variable or field) holding an `Option<T>` and an
350/// expression that returns `T`; returns a `&T` to the value held by
351/// the `Option`, runs the expression and stores the result in the
352/// place if it holds `None`. The name is from what the `//` operator
353/// in Perl (there`s also `//=` which this is really) is called. The
354/// expression is executed in the context of the `oerr` call, with no
355/// additional subroutine wrapper, meaning e.g. `?` jumps out of the
356/// `oerr` (it is designed like this on purpose).
357#[macro_export]
358macro_rules! oerr {
359    { $var:expr, $e:expr } => {
360        if let Some(v) = &mut $var {
361            v
362        } else {
363            $var = Some($e);
364            $var.as_mut().unwrap()
365        }
366    }
367}