use crate::error::{CaError, CaResult};
use crate::server::snapshot::EnumStringForm;
use crate::types::{DbFieldType, EpicsValue, PvString};
pub use super::dbd_generated::{
MENU_ALARM_SEVR, MENU_ALARM_STAT, MENU_CONVERT, MENU_FTYPE, MENU_IVOA, MENU_OMSL, MENU_PINI,
MENU_POST, MENU_PRIORITY, MENU_SCAN, MENU_SIMM, MENU_YES_NO,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ftype {
String,
Char,
UChar,
Short,
UShort,
Long,
ULong,
Int64,
UInt64,
Float,
Double,
Enum,
}
impl Ftype {
pub const ALL: [Ftype; 12] = [
Self::String,
Self::Char,
Self::UChar,
Self::Short,
Self::UShort,
Self::Long,
Self::ULong,
Self::Int64,
Self::UInt64,
Self::Float,
Self::Double,
Self::Enum,
];
pub fn index(self) -> i16 {
Self::ALL.iter().position(|f| *f == self).unwrap_or(0) as i16
}
pub fn from_index(index: i16) -> Option<Self> {
usize::try_from(index)
.ok()
.and_then(|i| Self::ALL.get(i))
.copied()
}
pub fn element_type(self) -> DbFieldType {
match self {
Self::String => DbFieldType::String,
Self::Char => DbFieldType::Char,
Self::UChar => DbFieldType::UChar,
Self::Short => DbFieldType::Short,
Self::UShort => DbFieldType::UShort,
Self::Long => DbFieldType::Long,
Self::ULong => DbFieldType::ULong,
Self::Int64 => DbFieldType::Int64,
Self::UInt64 => DbFieldType::UInt64,
Self::Float => DbFieldType::Float,
Self::Double => DbFieldType::Double,
Self::Enum => DbFieldType::Enum,
}
}
pub fn of_element_type(t: DbFieldType) -> Self {
match t {
DbFieldType::String => Self::String,
DbFieldType::Char => Self::Char,
DbFieldType::UChar => Self::UChar,
DbFieldType::Short => Self::Short,
DbFieldType::UShort => Self::UShort,
DbFieldType::Long => Self::Long,
DbFieldType::ULong => Self::ULong,
DbFieldType::Int64 => Self::Int64,
DbFieldType::UInt64 => Self::UInt64,
DbFieldType::Float => Self::Float,
DbFieldType::Double => Self::Double,
DbFieldType::Enum => Self::Enum,
}
}
pub fn zeroed(self, n: usize) -> EpicsValue {
match self {
Self::String => EpicsValue::StringArray(vec![PvString::new(); n]),
Self::Char => EpicsValue::CharArray(vec![0; n]),
Self::UChar => EpicsValue::UCharArray(vec![0; n]),
Self::Short => EpicsValue::ShortArray(vec![0; n]),
Self::UShort => EpicsValue::UShortArray(vec![0; n]),
Self::Long => EpicsValue::LongArray(vec![0; n]),
Self::ULong => EpicsValue::ULongArray(vec![0; n]),
Self::Int64 => EpicsValue::Int64Array(vec![0; n]),
Self::UInt64 => EpicsValue::UInt64Array(vec![0; n]),
Self::Float => EpicsValue::FloatArray(vec![0.0; n]),
Self::Double => EpicsValue::DoubleArray(vec![0.0; n]),
Self::Enum => EpicsValue::EnumArray(vec![0; n]),
}
}
}
pub fn shared_menu_choices(field: &str) -> Option<&'static [&'static str]> {
match field {
"SEVR" | "NSEV" | "ACKS" | "DISS" | "UDFS" | "HHSV" | "HSV" | "LSV" | "LLSV" | "ZSV"
| "OSV" | "COSV" | "UNSV" | "BRSV" | "ZRSV" | "ONSV" | "TWSV" | "THSV" | "FRSV"
| "FVSV" | "SXSV" | "SVSV" | "EISV" | "NISV" | "TESV" | "ELSV" | "TVSV" | "TTSV"
| "FTSV" | "FFSV" | "SIMS" => Some(MENU_ALARM_SEVR),
"STAT" | "NSTA" => Some(MENU_ALARM_STAT),
"ACKT" => Some(MENU_YES_NO),
"PINI" => Some(MENU_PINI),
"OLDSIMM" => Some(MENU_SIMM),
"SCAN" | "SSCN" => Some(MENU_SCAN),
"OMSL" => Some(MENU_OMSL),
"IVOA" => Some(MENU_IVOA),
"LINR" => Some(MENU_CONVERT),
"PBUF" => Some(MENU_YES_NO),
"PRIO" => Some(MENU_PRIORITY),
_ => None,
}
}
fn c_strtoul_base0(s: &str) -> Option<(u64, &str)> {
fn c_isspace(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
}
let b = s.as_bytes();
let mut i = 0;
while i < b.len() && c_isspace(b[i]) {
i += 1;
}
let mut negate = false;
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
negate = b[i] == b'-';
i += 1;
}
let mut base: u64 = 10;
if i < b.len() && b[i] == b'0' {
if i + 1 < b.len() && (b[i + 1] | 0x20) == b'x' {
if i + 2 < b.len() && b[i + 2].is_ascii_hexdigit() {
base = 16;
i += 2;
} else {
return Some((0, &s[i + 1..]));
}
} else {
base = 8;
}
}
let start = i;
let mut acc: u64 = 0;
while i < b.len() {
let digit = match b[i] {
c @ b'0'..=b'9' => u64::from(c - b'0'),
c @ b'a'..=b'f' => u64::from(c - b'a') + 10,
c @ b'A'..=b'F' => u64::from(c - b'A') + 10,
_ => break,
};
if digit >= base {
break;
}
acc = acc.checked_mul(base)?.checked_add(digit)?;
i += 1;
}
if i == start {
return None; }
let value = if negate { acc.wrapping_neg() } else { acc };
Some((value, &s[i..]))
}
fn epics_parse_uint16(s: &str) -> Option<u16> {
let (value, rest) = c_strtoul_base0(s)?;
if !rest
.bytes()
.all(|b| matches!(b, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'))
{
return None; }
if value > 0xffff && value <= !0xffff_u64 {
return None; }
Some(value as u16)
}
pub fn resolve_menu_field_string(
field: &str,
choices: &[&str],
dbf_type: DbFieldType,
s: &str,
) -> CaResult<EpicsValue> {
resolve_menu_field_string_bounded(field, choices, dbf_type, s, MenuBound::DbPut)
}
fn bad_choice(field: &str, choices: &[&str], s: &str) -> CaError {
CaError::BadChoice(format!(
"{field}: '{s}' is not one of {choices:?} nor an index below {}",
choices.len()
))
}
pub fn resolve_menu_field_string_db_load(
field: &str,
choices: &[&str],
dbf_type: DbFieldType,
s: &str,
) -> CaResult<EpicsValue> {
resolve_menu_field_string_bounded(field, choices, dbf_type, s, MenuBound::DbLoad)
}
pub fn binary_enum_states(znam: &PvString, onam: &PvString) -> Vec<PvString> {
if !znam.is_empty() && onam.is_empty() {
vec![znam.clone()]
} else {
vec![znam.clone(), onam.clone()]
}
}
pub fn multibit_enum_states(states: [&PvString; 16]) -> Vec<PvString> {
let no_str = states
.iter()
.rposition(|s| !s.is_empty())
.map(|i| i + 1)
.unwrap_or(0);
states[..no_str].iter().map(|s| (*s).clone()).collect()
}
pub fn binary_enum_string_form(znam: &PvString, onam: &PvString) -> EnumStringForm {
EnumStringForm::states(
vec![znam.clone(), onam.clone()],
PvString::from("Illegal_Value"),
)
}
pub fn multibit_enum_string_form(states: [&PvString; 16]) -> EnumStringForm {
EnumStringForm::states(
states.iter().map(|s| (*s).clone()).collect(),
PvString::from("Illegal Value"),
)
}
pub fn resolve_enum_state_string(
field: &str,
states: Option<&[PvString]>,
s: &PvString,
) -> CaResult<EpicsValue> {
let Some(states) = states else {
return Err(CaError::TypeMismatch(format!(
"{field}: DBF_ENUM field whose record supplies no enum-state table \
(C put_enum_str == NULL: S_db_noRSET)"
)));
};
let index = states
.iter()
.position(|state| state.as_bytes() == s.as_bytes())
.map(|i| i as u16)
.or_else(|| {
let value = epics_parse_uint16(&s.as_str_lossy())?;
(value < states.len() as u16).then_some(value)
})
.ok_or_else(|| {
CaError::BadChoice(format!(
"{field}: '{}' is not one of {:?} nor an index below {}",
s.as_str_lossy(),
states
.iter()
.map(|st| st.as_str_lossy().into_owned())
.collect::<Vec<_>>(),
states.len()
))
})?;
Ok(EpicsValue::Enum(index))
}
pub(crate) fn resolve_menu_field_string_bounded(
field: &str,
choices: &[&str],
dbf_type: DbFieldType,
s: &str,
bound: MenuBound,
) -> CaResult<EpicsValue> {
let index =
menu_index_from_string(choices, s, bound).ok_or_else(|| bad_choice(field, choices, s))?;
Ok(menu_index_value(dbf_type, index))
}
#[derive(Clone, Copy)]
pub(crate) enum MenuBound {
DbPut,
DbLoad,
}
fn menu_index_from_string(choices: &[&str], s: &str, bound: MenuBound) -> Option<i64> {
if let Some(i) = choices.iter().position(|c| *c == s) {
return Some(i as i64);
}
let value = epics_parse_uint16(s)?;
let n_choice = choices.len() as u16;
let in_range = match bound {
MenuBound::DbPut => value < n_choice,
MenuBound::DbLoad => !(value > n_choice && n_choice > 0 && value < u16::MAX),
};
in_range.then_some(i64::from(value))
}
fn menu_index_value(dbf_type: DbFieldType, index: i64) -> EpicsValue {
match dbf_type {
DbFieldType::Enum => EpicsValue::Enum(index as u16),
DbFieldType::Short => EpicsValue::Short(index as i16),
DbFieldType::UShort => EpicsValue::UShort(index as u16),
DbFieldType::Char => EpicsValue::Char(index as u8),
DbFieldType::UChar => EpicsValue::UChar(index as u8),
DbFieldType::Long => EpicsValue::Long(index as i32),
DbFieldType::ULong => EpicsValue::ULong(index as u32),
DbFieldType::Int64 => EpicsValue::Int64(index),
DbFieldType::UInt64 => EpicsValue::UInt64(index as u64),
_ => EpicsValue::Short(index as i16),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn menu_ftype_index_matches_the_choice_table() {
for (i, label) in MENU_FTYPE.iter().enumerate() {
let f = Ftype::from_index(i as i16).unwrap_or_else(|| panic!("{label} is a choice"));
assert_eq!(f.index(), i as i16, "{label}");
assert_eq!(Ftype::of_element_type(f.element_type()), f, "{label}");
assert_eq!(f.zeroed(3).count(), 3, "{label}");
assert_eq!(f.zeroed(1).db_field_type(), f.element_type(), "{label}");
}
assert_eq!(Ftype::from_index(MENU_FTYPE.len() as i16), None);
assert_eq!(Ftype::from_index(-1), None);
assert_eq!(Ftype::ALL.len(), MENU_FTYPE.len());
}
#[test]
fn alarm_severity_order_matches_dbd() {
assert_eq!(MENU_ALARM_SEVR[0], "NO_ALARM");
assert_eq!(MENU_ALARM_SEVR[2], "MAJOR");
assert_eq!(shared_menu_choices("HHSV"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("COSV"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("SIMS"), Some(MENU_ALARM_SEVR));
}
#[test]
fn simm_includes_raw_third_choice() {
assert_eq!(MENU_SIMM, &["NO", "YES", "RAW"]);
assert_eq!(shared_menu_choices("OLDSIMM"), Some(MENU_SIMM));
assert_eq!(shared_menu_choices("SIMM"), None);
}
#[test]
fn shared_names_map_to_their_menu() {
assert_eq!(shared_menu_choices("OMSL"), Some(MENU_OMSL));
assert_eq!(shared_menu_choices("IVOA"), Some(MENU_IVOA));
assert_eq!(shared_menu_choices("LINR"), Some(MENU_CONVERT));
assert_eq!(shared_menu_choices("SSCN"), Some(MENU_SCAN));
assert_eq!(shared_menu_choices("PRIO"), Some(MENU_PRIORITY));
assert_eq!(shared_menu_choices("PBUF"), Some(MENU_YES_NO));
}
#[test]
fn ftvl_is_not_shared_because_it_collides() {
assert_eq!(shared_menu_choices("FTVL"), None);
}
#[test]
fn record_varying_menu_names_are_not_shared() {
assert_eq!(shared_menu_choices("SIMM"), None);
assert_eq!(shared_menu_choices("MPST"), None);
assert_eq!(shared_menu_choices("APST"), None);
}
#[test]
fn dbcommon_menu_fields_resolve_to_their_menu() {
assert_eq!(shared_menu_choices("SEVR"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("NSEV"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("ACKS"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("DISS"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("UDFS"), Some(MENU_ALARM_SEVR));
assert_eq!(shared_menu_choices("STAT"), Some(MENU_ALARM_STAT));
assert_eq!(shared_menu_choices("NSTA"), Some(MENU_ALARM_STAT));
assert_eq!(shared_menu_choices("ACKT"), Some(MENU_YES_NO));
assert_eq!(shared_menu_choices("PINI"), Some(MENU_PINI));
}
#[test]
fn alarm_stat_and_pini_orders_match_the_dbd() {
assert_eq!(MENU_ALARM_STAT.len(), 22);
assert_eq!(MENU_ALARM_STAT[0], "NO_ALARM");
assert_eq!(MENU_ALARM_STAT[14], "LINK");
assert_eq!(MENU_ALARM_STAT[17], "UDF");
assert_eq!(MENU_ALARM_STAT[21], "WRITE_ACCESS");
assert_eq!(
MENU_PINI,
&["NO", "YES", "RUN", "RUNNING", "PAUSE", "PAUSED"]
);
}
#[test]
fn non_menu_name_is_none() {
assert_eq!(shared_menu_choices("VAL"), None);
assert_eq!(shared_menu_choices("DESC"), None);
assert_eq!(shared_menu_choices("OSV"), Some(MENU_ALARM_SEVR));
}
#[test]
fn db_put_and_db_load_bounds_differ_where_c_says_they_do() {
const N: usize = MENU_PRIORITY.len(); assert_eq!(
menu_index_from_string(MENU_PRIORITY, "2", MenuBound::DbPut),
Some(2)
);
assert_eq!(
menu_index_from_string(MENU_PRIORITY, &N.to_string(), MenuBound::DbPut),
None
);
assert_eq!(
menu_index_from_string(MENU_PRIORITY, &N.to_string(), MenuBound::DbLoad),
Some(N as i64)
);
assert_eq!(
menu_index_from_string(MENU_PRIORITY, "65535", MenuBound::DbPut),
None
);
assert_eq!(
menu_index_from_string(MENU_PRIORITY, "65535", MenuBound::DbLoad),
Some(65535)
);
assert_eq!(
menu_index_from_string(MENU_PRIORITY, "HIGH ", MenuBound::DbLoad),
None
);
}
#[test]
fn epics_parse_uint16_is_c_strtoul_base_0() {
assert_eq!(epics_parse_uint16("10"), Some(10));
assert_eq!(epics_parse_uint16(" 0x1f\t"), Some(31));
assert_eq!(epics_parse_uint16("010"), Some(8));
assert_eq!(epics_parse_uint16("65535"), Some(65535));
assert_eq!(epics_parse_uint16("65536"), None); assert_eq!(epics_parse_uint16("12abc"), None); assert_eq!(epics_parse_uint16(""), None); assert_eq!(epics_parse_uint16("abc"), None);
assert_eq!(epics_parse_uint16("-1"), Some(0xffff));
}
}