use super::stdlib::epics_parse_double;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnvParam {
name: &'static str,
default: &'static str,
}
impl EnvParam {
pub(crate) const fn new(name: &'static str, default: &'static str) -> Self {
Self { name, default }
}
pub const fn name(&self) -> &'static str {
self.name
}
pub const fn default_str(&self) -> &'static str {
self.default
}
pub fn get(&self) -> Option<String> {
let v = std::env::var(self.name).unwrap_or_else(|_| self.default.to_string());
if v.is_empty() { None } else { Some(v) }
}
pub fn long(&self) -> Option<i64> {
let text = self.get()?;
let text = truncate_to_c_buffer(&text);
match sscanf_long(text) {
Some(v) => Some(v),
None => {
eprintln!("Unable to find an integer in {}={text}", self.name);
None
}
}
}
pub fn long_or_default(&self) -> i64 {
self.long()
.or_else(|| sscanf_long(self.default))
.unwrap_or(0)
}
pub fn double(&self) -> Result<f64, EnvDoubleError> {
let raw = self.get().ok_or(EnvDoubleError::Unresolvable)?;
epics_parse_double(&raw).map_err(|_| {
eprintln!("Unable to find a real number in {}={raw}", self.name);
EnvDoubleError::Invalid
})
}
pub fn bool(&self) -> Option<bool> {
Some(self.get()?.eq_ignore_ascii_case("yes"))
}
pub fn inet_port(&self, fallback: u16) -> u16 {
let mut port = match self.long() {
Some(v) => v,
None => {
eprintln!("EPICS Environment \"{}\" integer fetch failed", self.name);
eprintln!("setting \"{}\" = {fallback}", self.name);
i64::from(fallback)
}
};
if port <= i64::from(IPPORT_USERRESERVED) || port > i64::from(u16::MAX) {
eprintln!("EPICS Environment \"{}\" out of range", self.name);
port = i64::from(fallback);
eprintln!("Setting \"{}\" = {fallback}", self.name);
}
port as u16
}
pub const fn default_port(&self) -> u16 {
let bytes = self.default.as_bytes();
assert!(!bytes.is_empty(), "parameter has no compiled default port");
let mut i = 0;
let mut value: u32 = 0;
while i < bytes.len() {
let d = bytes[i];
assert!(d.is_ascii_digit(), "compiled default is not a port number");
value = value * 10 + (d - b'0') as u32;
assert!(
value <= u16::MAX as u32,
"compiled default exceeds USHRT_MAX"
);
i += 1;
}
value as u16
}
pub fn describe(&self) -> String {
match self.get() {
Some(v) => format!("{}: {v}", self.name),
None => format!("{} is undefined", self.name),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvDoubleError {
Unresolvable,
Invalid,
}
pub fn get(key: &str) -> Option<String> {
std::env::var(key).ok()
}
pub const IPPORT_USERRESERVED: u32 = 5000;
fn truncate_to_c_buffer(raw: &str) -> &str {
let mut end = raw.len().min(127);
while !raw.is_char_boundary(end) {
end -= 1;
}
&raw[..end]
}
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 prt_env_params() -> impl Iterator<Item = String> {
super::env_table::ENV_PARAM_LIST
.iter()
.map(EnvParam::describe)
}
pub fn register_iocsh_env_vars() {
use super::version as v;
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let mut vars: Vec<(&str, String)> = super::env_table::EPICS_BUILD_TARGET_ARCH
.get()
.map(|arch| ("ARCH", arch))
.into_iter()
.collect();
vars.extend([
("EPICS_VERSION_MAJOR", v::EPICS_VERSION.to_string()),
("EPICS_VERSION_MIDDLE", v::EPICS_REVISION.to_string()),
("EPICS_VERSION_MINOR", v::EPICS_MODIFICATION.to_string()),
("EPICS_VERSION_PATCH", v::EPICS_PATCH_LEVEL.to_string()),
("EPICS_VERSION_SNAPSHOT", v::EPICS_DEV_SNAPSHOT.to_string()),
("EPICS_VERSION_SITE", v::EPICS_SITE_VERSION.to_string()),
("EPICS_VERSION_SHORT", v::EPICS_VERSION_SHORT.to_string()),
("EPICS_VERSION_FULL", v::EPICS_VERSION_FULL.to_string()),
]);
for (name, value) in vars {
unsafe { std::env::set_var(name, value) };
}
});
}
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::super::env_table::*;
use super::*;
use serial_test::serial;
const SCRATCH: EnvParam = EnvParam::new("_EPICS_RT_TEST_VAR", "");
const SCRATCH_PORT: EnvParam = EnvParam::new("_EPICS_RT_TEST_PORT", "5064");
fn clear(p: EnvParam) {
unsafe { std::env::remove_var(p.name()) };
}
fn set(p: EnvParam, v: &str) {
unsafe { std::env::set_var(p.name(), v) };
}
#[test]
fn test_get_missing() {
assert_eq!(get("_EPICS_RT_NONEXISTENT_VAR_12345"), None);
}
#[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 unset_resolves_to_the_compiled_default() {
clear(EPICS_CA_CONN_TMO);
clear(EPICS_CA_SERVER_PORT);
clear(EPICS_CA_ADDR_LIST);
assert_eq!(EPICS_CA_CONN_TMO.double(), Ok(30.0));
assert_eq!(EPICS_CA_SERVER_PORT.long(), Some(5064));
assert_eq!(EPICS_CA_AUTO_ADDR_LIST.bool(), Some(true));
assert_eq!(EPICS_CA_ADDR_LIST.get(), None);
assert_eq!(EPICS_CAS_SERVER_PORT.get(), None);
}
#[test]
fn default_port_is_const_derived_from_the_table() {
const SERVER: u16 = EPICS_CA_SERVER_PORT.default_port();
const REPEATER: u16 = EPICS_CA_REPEATER_PORT.default_port();
const LOG: u16 = EPICS_IOC_LOG_PORT.default_port();
assert_eq!((SERVER, REPEATER, LOG), (5064, 5065, 7004));
}
#[test]
#[serial(epics_env)]
fn test_env_inet_port_valid() {
set(SCRATCH_PORT, "8080");
assert_eq!(SCRATCH_PORT.inet_port(5064), 8080);
clear(SCRATCH_PORT);
}
#[test]
#[serial(epics_env)]
fn test_env_inet_port_invalid_and_missing() {
set(SCRATCH_PORT, "not_a_number");
assert_eq!(SCRATCH_PORT.inet_port(5064), 5064);
clear(SCRATCH_PORT);
assert_eq!(SCRATCH_PORT.inet_port(5064), 5064);
}
#[test]
#[serial(epics_env)]
fn test_env_inet_port_empty_value_is_a_failed_fetch() {
set(SCRATCH, "");
assert_eq!(SCRATCH.get(), None);
assert_eq!(SCRATCH.inet_port(5064), 5064);
clear(SCRATCH);
}
#[test]
#[serial(epics_env)]
fn test_env_inet_port_lenient_whitespace_and_suffix() {
set(SCRATCH_PORT, " 6064");
assert_eq!(SCRATCH_PORT.inet_port(5064), 6064);
set(SCRATCH_PORT, "6064abc");
assert_eq!(SCRATCH_PORT.inet_port(5064), 6064);
clear(SCRATCH_PORT);
}
#[test]
#[serial(epics_env)]
fn test_env_inet_port_truncates_at_the_c_buffer_length() {
set(SCRATCH_PORT, &format!("{}6064", " ".repeat(130)));
assert_eq!(SCRATCH_PORT.inet_port(5064), 5064);
set(SCRATCH_PORT, &format!("{}6064", " ".repeat(100)));
assert_eq!(SCRATCH_PORT.inet_port(5064), 6064);
clear(SCRATCH_PORT);
}
#[test]
#[serial(epics_env)]
fn test_env_inet_port_rejects_reserved_and_out_of_range_ports() {
for bad in [
"0", "1", "80", "443", "3000", "5000", "-1", "65536", "70000", "99999",
] {
set(SCRATCH_PORT, bad);
assert_eq!(
SCRATCH_PORT.inet_port(5064),
5064,
"out-of-range port {bad:?} must fall back to default"
);
}
set(SCRATCH_PORT, "5001");
assert_eq!(SCRATCH_PORT.inet_port(5064), 5001);
set(SCRATCH_PORT, "65535");
assert_eq!(SCRATCH_PORT.inet_port(5064), 65535);
clear(SCRATCH_PORT);
}
#[test]
#[serial(epics_env)]
fn test_get_bool_only_yes_case_insensitive() {
for truthy in &["yes", "YES", "Yes", "yEs", "yeS"] {
set(SCRATCH, truthy);
assert_eq!(SCRATCH.bool(), Some(true), "{truthy:?} must be true");
}
for falsy in &[
"1", "true", "TRUE", "on", "yes ", " yes", "yes\n", "no", "0",
] {
set(SCRATCH, falsy);
assert_eq!(SCRATCH.bool(), Some(false), "{falsy:?} must be false");
}
set(SCRATCH, "");
assert_eq!(SCRATCH.bool(), None);
clear(SCRATCH);
assert_eq!(SCRATCH.bool(), None);
}
#[test]
#[serial(epics_env)]
fn test_long_or_default_keeps_the_table_default_on_garbage() {
clear(IOCSH_HISTSIZE);
assert_eq!(IOCSH_HISTSIZE.long_or_default(), 50);
set(IOCSH_HISTSIZE, "garbage");
assert_eq!(IOCSH_HISTSIZE.long_or_default(), 50);
set(IOCSH_HISTSIZE, "1000");
assert_eq!(IOCSH_HISTSIZE.long_or_default(), 1000);
clear(IOCSH_HISTSIZE);
}
#[test]
#[serial(epics_env)]
fn describe_matches_env_prt_config_param() {
clear(EPICS_CA_ADDR_LIST);
clear(EPICS_CA_SERVER_PORT);
assert_eq!(
EPICS_CA_ADDR_LIST.describe(),
"EPICS_CA_ADDR_LIST is undefined"
);
assert_eq!(
EPICS_CA_SERVER_PORT.describe(),
"EPICS_CA_SERVER_PORT: 5064"
);
set(EPICS_CA_ADDR_LIST, "10.0.0.1");
assert_eq!(
EPICS_CA_ADDR_LIST.describe(),
"EPICS_CA_ADDR_LIST: 10.0.0.1"
);
clear(EPICS_CA_ADDR_LIST);
}
#[test]
fn test_hostname() {
let h = hostname();
assert!(!h.is_empty());
}
#[test]
#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
fn env_param_list_matches_compiled_c() {
let expected = include_str!("testdata/epicsPrtEnvParams.txt");
let ours: Vec<String> = ENV_PARAM_LIST
.iter()
.map(|p| {
if p.default_str().is_empty() {
format!("{} is undefined", p.name())
} else {
format!("{}: {}", p.name(), p.default_str())
}
})
.collect();
let theirs: Vec<&str> = expected.lines().collect();
assert_eq!(
ours.len(),
theirs.len(),
"C's env_param_list[] has {} rows, ours has {}",
theirs.len(),
ours.len()
);
for (i, (a, b)) in ours.iter().zip(&theirs).enumerate() {
assert_eq!(a, b, "row {i} differs from compiled C");
}
}
#[test]
#[serial(epics_env)]
#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
fn iocsh_registers_the_version_env_vars_c_does() {
let expected = include_str!("testdata/iocshRegisterCommon.txt");
for name in expected.lines().filter_map(|l| l.split_once('=')) {
unsafe { std::env::remove_var(name.0) };
}
register_iocsh_env_vars();
for (i, line) in expected.lines().enumerate() {
let (name, value) = line.split_once('=').expect("NAME=value");
assert_eq!(
std::env::var(name).ok().as_deref(),
Some(value),
"line {i}: C's iocshRegisterCommon publishes `{line}`"
);
}
}
}