Skip to main content

pharos_plugins/
health.rs

1//! Health endpoint configuration.
2
3use std::time::Duration;
4
5#[cfg(feature = "plugin-health")]
6use pharos_corefile::{Cursor, Error};
7
8#[cfg(feature = "plugin-health")]
9#[derive(Debug)]
10pub struct Health {
11    pub address: String,
12    pub lameduck: Duration,
13}
14
15#[cfg(feature = "plugin-health")]
16pub(crate) fn parse(cursor: &mut Cursor<'_>) -> Result<Health, Error> {
17    let mut address = ":8080".to_owned();
18    let mut lameduck = Duration::ZERO;
19    while cursor.next() {
20        let args = cursor.remaining_args();
21        if args.len() > 1 {
22            return Err(cursor.arg_err());
23        }
24        if let Some(value) = args.first() {
25            if !crate::valid_http_address(value) {
26                return Err(cursor.error("invalid health address"));
27            }
28            address.clone_from(value);
29        }
30        while cursor.next_block() {
31            if cursor.val() != "lameduck" {
32                return Err(cursor.error(format!("unknown property: {}", cursor.val())));
33            }
34            let args = cursor.remaining_args();
35            if args.len() != 1 {
36                return Err(cursor.arg_err());
37            }
38            lameduck = parse_duration(&args[0]).ok_or_else(|| cursor.error("invalid duration"))?;
39        }
40    }
41    Ok(Health { address, lameduck })
42}
43
44pub(crate) fn parse_duration(value: &str) -> Option<Duration> {
45    let (negative, mut rest) = match value.as_bytes().first() {
46        Some(b'-') => (true, &value[1..]),
47        Some(b'+') => (false, &value[1..]),
48        _ => (false, value),
49    };
50    if rest == "0" {
51        return Some(Duration::ZERO);
52    }
53    if rest.is_empty() {
54        return None;
55    }
56    let mut total = 0u128;
57    while !rest.is_empty() {
58        let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
59        let integer = if digits == 0 {
60            0
61        } else {
62            rest[..digits].parse::<u128>().ok()?
63        };
64        rest = &rest[digits..];
65        let mut fraction = 0u128;
66        let mut scale = 1u128;
67        let mut decimals = 0;
68        if let Some(after_dot) = rest.strip_prefix('.') {
69            decimals = after_dot.bytes().take_while(u8::is_ascii_digit).count();
70            for digit in after_dot.bytes().take(decimals) {
71                if scale < 1_000_000_000_000_000_000 {
72                    fraction = fraction * 10 + u128::from(digit - b'0');
73                    scale *= 10;
74                }
75            }
76            rest = &after_dot[decimals..];
77        }
78        if digits == 0 && decimals == 0 {
79            return None;
80        }
81        let end = rest
82            .find(|ch: char| ch.is_ascii_digit() || ch == '.')
83            .unwrap_or(rest.len());
84        let unit = match &rest[..end] {
85            "ns" => 1,
86            "us" | "µs" | "μs" => 1_000,
87            "ms" => 1_000_000,
88            "s" => 1_000_000_000,
89            "m" => 60_000_000_000,
90            "h" => 3_600_000_000_000,
91            _ => return None,
92        };
93        total = total.checked_add(integer.checked_mul(unit)?)?;
94        total = total.checked_add(fraction * unit / scale)?;
95        let limit = if negative {
96            1u128 << 63
97        } else {
98            i64::MAX as u128
99        };
100        if total > limit {
101            return None;
102        }
103        rest = &rest[end..];
104    }
105    Some(if negative {
106        Duration::ZERO
107    } else {
108        Duration::from_nanos(total as u64)
109    })
110}