pub fn get(key: &str) -> Option<String> {
std::env::var(key).ok()
}
pub fn get_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
pub const IPPORT_USERRESERVED: u32 = 5000;
fn sscanf_long(text: &str) -> Option<i64> {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
let mut neg = false;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
neg = bytes[i] == b'-';
i += 1;
}
let start = i;
let mut value: i64 = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
value = value
.saturating_mul(10)
.saturating_add((bytes[i] - b'0') as i64);
i += 1;
}
if i == start {
return None;
}
Some(if neg { -value } else { value })
}
pub fn get_u16(key: &str, default: u16) -> u16 {
let Some(raw) = std::env::var(key).ok() else {
return default;
};
let parsed = match sscanf_long(&raw) {
Some(v) => v,
None => {
tracing::warn!(
target: "epics_base_rs::env",
var = key,
value = %raw,
default,
"EPICS environment integer fetch failed; using default"
);
return default;
}
};
if parsed <= IPPORT_USERRESERVED as i64 || parsed > u16::MAX as i64 {
tracing::warn!(
target: "epics_base_rs::env",
var = key,
value = parsed,
default,
"EPICS environment port out of range (must be > {IPPORT_USERRESERVED} and <= {}); using default",
u16::MAX
);
return default;
}
parsed as u16
}
pub fn get_bool(key: &str, default: bool) -> bool {
match std::env::var(key).ok() {
Some(v) => v.eq_ignore_ascii_case("yes"),
None => default,
}
}
pub fn set_default(name: &str, value: &str) {
if std::env::var_os(name).is_none() {
unsafe { std::env::set_var(name, value) };
}
}
pub fn set_crate_path(name: &str, manifest_dir: &str, relative: &str) {
set_default(name, &format!("{manifest_dir}/{relative}"));
}
pub fn hostname() -> String {
hostname::get()
.ok()
.and_then(|s| s.into_string().ok())
.unwrap_or_else(|| "localhost".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn test_get_existing() {
unsafe { std::env::set_var("_EPICS_RT_TEST_VAR", "hello") };
assert_eq!(get("_EPICS_RT_TEST_VAR"), Some("hello".to_string()));
unsafe { std::env::remove_var("_EPICS_RT_TEST_VAR") };
}
#[test]
fn test_get_missing() {
assert_eq!(get("_EPICS_RT_NONEXISTENT_VAR_12345"), None);
}
#[test]
fn test_get_or_default() {
assert_eq!(
get_or("_EPICS_RT_NONEXISTENT_VAR_12345", "fallback"),
"fallback"
);
}
#[test]
fn test_sscanf_long_lenient_parsing() {
assert_eq!(sscanf_long("5064"), Some(5064));
assert_eq!(sscanf_long(" 6064"), Some(6064));
assert_eq!(sscanf_long("\t6064\n"), Some(6064));
assert_eq!(sscanf_long("5064abc"), Some(5064));
assert_eq!(sscanf_long("+6064"), Some(6064));
assert_eq!(sscanf_long("-1"), Some(-1));
assert_eq!(sscanf_long("not_a_number"), None);
assert_eq!(sscanf_long(""), None);
assert_eq!(sscanf_long(" "), None);
}
#[test]
#[serial(epics_env)]
fn test_get_u16_valid() {
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT", "8080") };
assert_eq!(get_u16("_EPICS_RT_TEST_PORT", 5064), 8080);
unsafe { std::env::remove_var("_EPICS_RT_TEST_PORT") };
}
#[test]
#[serial(epics_env)]
fn test_get_u16_invalid() {
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT_BAD", "not_a_number") };
assert_eq!(get_u16("_EPICS_RT_TEST_PORT_BAD", 5064), 5064);
unsafe { std::env::remove_var("_EPICS_RT_TEST_PORT_BAD") };
}
#[test]
#[serial(epics_env)]
fn test_get_u16_missing() {
assert_eq!(get_u16("_EPICS_RT_NONEXISTENT_VAR_12345", 5064), 5064);
}
#[test]
#[serial(epics_env)]
fn test_get_u16_lenient_whitespace_and_suffix() {
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT_WS", " 6064") };
assert_eq!(get_u16("_EPICS_RT_TEST_PORT_WS", 5064), 6064);
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT_WS", "6064abc") };
assert_eq!(get_u16("_EPICS_RT_TEST_PORT_WS", 5064), 6064);
unsafe { std::env::remove_var("_EPICS_RT_TEST_PORT_WS") };
}
#[test]
#[serial(epics_env)]
fn test_get_u16_rejects_reserved_and_out_of_range_ports() {
for bad in ["0", "1", "80", "443", "5000", "-1", "65536", "99999"] {
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT_RANGE", bad) };
assert_eq!(
get_u16("_EPICS_RT_TEST_PORT_RANGE", 5064),
5064,
"out-of-range port {bad:?} must fall back to default"
);
}
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT_RANGE", "5001") };
assert_eq!(get_u16("_EPICS_RT_TEST_PORT_RANGE", 5064), 5001);
unsafe { std::env::set_var("_EPICS_RT_TEST_PORT_RANGE", "65535") };
assert_eq!(get_u16("_EPICS_RT_TEST_PORT_RANGE", 5064), 65535);
unsafe { std::env::remove_var("_EPICS_RT_TEST_PORT_RANGE") };
}
#[test]
#[serial(epics_env)]
fn test_get_bool_only_yes_case_insensitive() {
for truthy in &["yes", "YES", "Yes", "yEs", "yeS"] {
unsafe { std::env::set_var("_EPICS_RT_TEST_BOOL", truthy) };
assert!(
get_bool("_EPICS_RT_TEST_BOOL", false),
"case-insensitive 'yes' must be true: {truthy:?}"
);
}
for falsy in &[
"1", "true", "TRUE", "on", "yes ", " yes", "yes\n", "no", "0", "",
] {
unsafe { std::env::set_var("_EPICS_RT_TEST_BOOL", falsy) };
assert!(
!get_bool("_EPICS_RT_TEST_BOOL", true),
"non-'yes' value must be false: {falsy:?}"
);
}
unsafe { std::env::remove_var("_EPICS_RT_TEST_BOOL") };
}
#[test]
#[serial(epics_env)]
fn test_get_bool_missing() {
assert!(get_bool("_EPICS_RT_NONEXISTENT_VAR_12345", true));
assert!(!get_bool("_EPICS_RT_NONEXISTENT_VAR_12345", false));
}
#[test]
fn test_hostname() {
let h = hostname();
assert!(!h.is_empty());
}
}