use crate::error::{CaError, CaResult};
use crate::server::record::{ProcessOutcome, Record};
use crate::types::{EpicsValue, PvString};
const MAX_STRING_SIZE: usize = 40;
fn truncate_bytes(s: PvString, max: usize) -> PvString {
if s.len() <= max {
return s;
}
PvString::from_bytes(s.as_bytes()[..max].to_vec())
}
pub struct PrintfRecord {
pub val: String,
pub sizv: u16,
pub fmt: PvString,
pub inp_links: [String; 10],
pub vals: [EpicsValue; 10],
pub len: u32,
pub ivls: String,
resolved: [bool; 10],
}
impl Default for PrintfRecord {
fn default() -> Self {
Self {
val: String::new(),
sizv: 256,
fmt: PvString::new(),
inp_links: Default::default(),
vals: std::array::from_fn(|_| EpicsValue::Double(0.0)),
len: 0,
ivls: "LNK".to_string(),
resolved: [false; 10],
}
}
}
struct Directive {
width: Option<usize>,
star_width: bool,
precision: Option<usize>,
star_prec: bool,
left_align: bool,
zero_pad: bool,
alt_form: bool, conv: u8,
long: bool,
length: LengthMod,
bad: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
enum LengthMod {
#[default]
None,
Char,
Short,
Long,
LongLong,
}
impl PrintfRecord {
fn val_as_string(v: &EpicsValue) -> String {
match v {
EpicsValue::String(s) => s.as_str_lossy().into_owned(),
EpicsValue::CharArray(bytes) => {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
EpicsValue::Double(d) => format!("{d}"),
EpicsValue::Float(f) => format!("{f}"),
EpicsValue::Long(n) => format!("{n}"),
EpicsValue::Short(n) => format!("{n}"),
EpicsValue::Int64(n) => format!("{n}"),
EpicsValue::Char(c) => format!("{c}"),
EpicsValue::Enum(e) => format!("{e}"),
other => format!("{other:?}"),
}
}
fn val_as_f64(v: &EpicsValue) -> f64 {
v.to_f64().unwrap_or(0.0)
}
fn val_as_int(v: &EpicsValue) -> i64 {
if let Some(n) = v.as_int_i64() {
return n;
}
let f = v.to_f64().unwrap_or(0.0);
if f.is_finite() { f as i64 } else { 0 }
}
fn narrow_signed(n: i64, length: LengthMod) -> i64 {
match length {
LengthMod::Char => n as i8 as i64,
LengthMod::Short => n as i16 as i64,
LengthMod::LongLong => n,
LengthMod::None | LengthMod::Long => n as i32 as i64,
}
}
fn narrow_unsigned(n: i64, length: LengthMod) -> u64 {
match length {
LengthMod::Char => n as u8 as u64,
LengthMod::Short => n as u16 as u64,
LengthMod::LongLong => n as u64,
LengthMod::None | LengthMod::Long => n as u32 as u64,
}
}
fn plain_string_slots(&self) -> [bool; 10] {
let mut out = [false; 10];
let bytes = self.fmt.as_bytes();
let mut i = 0;
let mut slot = 0usize;
while i < bytes.len() && slot < out.len() {
if bytes[i] != b'%' {
i += 1;
continue;
}
if bytes.get(i + 1) == Some(&b'%') {
i += 2;
continue;
}
let (d, next) = Self::parse_directive(bytes, i);
i = next;
if d.bad {
continue;
}
if d.star_width {
slot += 1;
}
if d.star_prec {
slot += 1;
}
if slot >= out.len() {
break;
}
if d.conv == b's' && !d.long {
out[slot] = true;
}
slot += 1;
}
out
}
fn parse_directive(bytes: &[u8], mut i: usize) -> (Directive, usize) {
let mut d = Directive {
width: None,
star_width: false,
precision: None,
star_prec: false,
left_align: false,
zero_pad: false,
alt_form: false,
conv: b's',
long: false,
length: LengthMod::None,
bad: false,
};
i += 1; loop {
match bytes.get(i) {
Some(b'-') => d.left_align = true,
Some(b'+') | Some(b' ') => {}
Some(b'#') => d.alt_form = true,
Some(b'0') => d.zero_pad = true,
_ => break,
}
i += 1;
}
if bytes.get(i) == Some(&b'*') {
d.star_width = true;
i += 1;
} else {
let mut w = 0usize;
let mut any = false;
while let Some(c) = bytes.get(i) {
if c.is_ascii_digit() {
w = w * 10 + (c - b'0') as usize;
any = true;
i += 1;
} else {
break;
}
}
if any {
d.width = Some(w);
}
}
if bytes.get(i) == Some(&b'.') {
i += 1;
if bytes.get(i) == Some(&b'*') {
d.star_prec = true;
i += 1;
} else {
let mut p = 0usize;
while let Some(c) = bytes.get(i) {
if c.is_ascii_digit() {
p = p * 10 + (c - b'0') as usize;
i += 1;
} else {
break;
}
}
d.precision = Some(p);
}
}
loop {
match bytes.get(i) {
Some(b'h') => {
d.length = match d.length {
LengthMod::None => LengthMod::Short,
LengthMod::Short => LengthMod::Char,
_ => {
d.bad = true;
d.length
}
};
i += 1;
}
Some(b'l') => {
d.length = match d.length {
LengthMod::None => LengthMod::Long,
LengthMod::Long => LengthMod::LongLong,
_ => {
d.bad = true;
d.length
}
};
i += 1;
}
_ => break,
}
}
d.long = d.length == LengthMod::Long;
match bytes.get(i) {
Some(&c) if b"diouxXeEfFgGcs".contains(&c) => {
d.conv = c;
i += 1;
}
Some(_) => {
d.bad = true;
i += 1;
}
None => {
d.bad = true;
}
}
(d, i)
}
fn apply_fmt(&self) -> String {
let mut result = String::new();
let bytes = self.fmt.as_bytes();
let mut i = 0;
let mut inp_idx = 0usize;
let take = |idx: &mut usize| -> Option<&EpicsValue> {
let cur = *idx;
*idx += 1;
if cur < 10 && self.resolved[cur] {
Some(&self.vals[cur])
} else {
None
}
};
while i < bytes.len() {
if bytes[i] != b'%' {
result.push(bytes[i] as char);
i += 1;
continue;
}
if bytes.get(i + 1) == Some(&b'%') {
result.push('%');
i += 2;
continue;
}
let start = i;
let (d, next) = Self::parse_directive(bytes, i);
i = next;
if d.bad {
for &b in &bytes[start..i] {
result.push(b as char);
}
continue;
}
let mut bad_link = false;
let width = if d.star_width {
match take(&mut inp_idx) {
Some(v) => Self::val_as_f64(v) as i64,
None => {
bad_link = true;
0
}
}
} else {
d.width.unwrap_or(0) as i64
};
let precision = if d.star_prec {
match take(&mut inp_idx) {
Some(v) => (Self::val_as_f64(v) as i64).max(0) as usize,
None => {
bad_link = true;
0
}
}
} else {
d.precision.unwrap_or(usize::MAX)
};
let (width, left_align) = if width < 0 {
((-width) as usize, true)
} else {
(width as usize, d.left_align)
};
let arg = if bad_link {
None
} else {
let a = take(&mut inp_idx);
if a.is_none() {
bad_link = true;
}
a
};
if bad_link {
result.push_str(&self.ivls);
continue;
}
let conv_prec = if precision == usize::MAX {
6
} else {
precision
};
let substituted = match d.conv {
b'd' | b'i' => {
let v = arg.map(Self::val_as_int).unwrap_or(0);
let v = Self::narrow_signed(v, d.length);
pad_string(format!("{v}"), width, left_align, d.zero_pad)
}
b'u' => {
let v = arg.map(Self::val_as_int).unwrap_or(0);
let v = Self::narrow_unsigned(v, d.length);
pad_string(format!("{v}"), width, left_align, d.zero_pad)
}
b'o' => {
let v = Self::narrow_unsigned(arg.map(Self::val_as_int).unwrap_or(0), d.length);
let s = if d.alt_form && v != 0 {
format!("0{v:o}")
} else {
format!("{v:o}")
};
pad_string(s, width, left_align, d.zero_pad)
}
b'x' => {
let v = Self::narrow_unsigned(arg.map(Self::val_as_int).unwrap_or(0), d.length);
let s = if d.alt_form && v != 0 {
format!("0x{v:x}")
} else {
format!("{v:x}")
};
pad_string(s, width, left_align, d.zero_pad)
}
b'X' => {
let v = Self::narrow_unsigned(arg.map(Self::val_as_int).unwrap_or(0), d.length);
let s = if d.alt_form && v != 0 {
format!("0X{v:X}")
} else {
format!("{v:X}")
};
pad_string(s, width, left_align, d.zero_pad)
}
b'e' | b'E' | b'f' | b'F' | b'g' | b'G' => {
let v = arg.map(Self::val_as_f64).unwrap_or(0.0);
let v = if d.length == LengthMod::Short {
v as f32 as f64
} else {
v
};
let s = format_float_conv(d.conv, v, conv_prec, d.alt_form);
pad_string(s, width, left_align, d.zero_pad)
}
b'c' => {
let ch = match arg {
Some(EpicsValue::String(s)) => {
s.as_str_lossy().chars().next().unwrap_or(' ')
}
Some(v) => {
let code = Self::val_as_f64(v) as u32;
char::from_u32(code).unwrap_or('\u{0}')
}
None => '\u{0}',
};
pad_string(ch.to_string(), width, left_align, false)
}
b's' => {
let mut s = arg.map(Self::val_as_string).unwrap_or_default();
if precision != usize::MAX && s.chars().count() > precision {
s = s.chars().take(precision).collect();
}
pad_string(s, width, left_align, false)
}
_ => String::new(),
};
result.push_str(&substituted);
}
let max = (self.sizv as usize).saturating_sub(1);
if result.len() > max {
let trunc = (0..=max)
.rev()
.find(|&n| result.is_char_boundary(n))
.unwrap_or(0);
result.truncate(trunc);
}
result
}
fn inp_index(name: &str) -> Option<usize> {
let bytes = name.as_bytes();
if bytes.len() == 4 && &bytes[0..3] == b"INP" {
let digit = bytes[3];
if digit.is_ascii_digit() {
return Some((digit - b'0') as usize);
}
}
None
}
fn val_index(name: &str) -> Option<usize> {
if name.len() == 1 {
let c = name.as_bytes()[0];
if (b'A'..=b'J').contains(&c) {
return Some((c - b'A') as usize);
}
}
None
}
}
fn pad_string(s: String, width: usize, left_align: bool, zero_pad: bool) -> String {
if width <= s.chars().count() {
return s;
}
if left_align {
format!("{s:<width$}")
} else if zero_pad {
if s.starts_with('-') || s.starts_with('+') {
format!("{}{:0>width$}", &s[..1], &s[1..], width = width - 1)
} else {
format!("{s:0>width$}")
}
} else {
format!("{s:>width$}")
}
}
fn format_float_conv(conv: u8, val: f64, prec: usize, alt_form: bool) -> String {
let upper = conv.is_ascii_uppercase();
if !val.is_finite() {
let sign = if val.is_sign_negative() { "-" } else { "" };
let word = if val.is_nan() { "nan" } else { "inf" };
return if upper {
format!("{sign}{}", word.to_ascii_uppercase())
} else {
format!("{sign}{word}")
};
}
match conv {
b'e' | b'E' => format_e_val(val, prec, upper, alt_form),
b'g' => format_g_val(val, prec, false, alt_form),
b'G' => format_g_val(val, prec, true, alt_form),
_ => format_f_val(val, prec, alt_form),
}
}
fn format_e_val(val: f64, prec: usize, upper: bool, alt_form: bool) -> String {
let sep = if upper { 'E' } else { 'e' };
let raw = if upper {
format!("{val:.prec$E}")
} else {
format!("{val:.prec$e}")
};
let (mantissa, exp_digits) = raw
.split_once(sep)
.expect("Rust's LowerExp/UpperExp always writes an exponent");
let e: i32 = exp_digits
.parse()
.expect("...followed by a decimal exponent");
let point = if alt_form && !mantissa.contains('.') {
"."
} else {
""
};
format!(
"{mantissa}{point}{sep}{}{:02}",
if e < 0 { '-' } else { '+' },
e.abs()
)
}
fn format_f_val(val: f64, prec: usize, alt_form: bool) -> String {
let s = format!("{val:.prec$}");
if alt_form && !s.contains('.') {
format!("{s}.")
} else {
s
}
}
pub fn g_decision_exponent(abs: f64, precision: usize) -> i32 {
let raw_exp = abs.log10().floor() as i32;
let scale = 10f64.powi(precision as i32 - 1 - raw_exp);
if !scale.is_finite() || scale == 0.0 {
return raw_exp;
}
let rounded_scaled = (abs * scale).round();
if !rounded_scaled.is_finite() || rounded_scaled <= 0.0 {
return raw_exp;
}
raw_exp + (rounded_scaled.log10().floor() as i32 - (precision as i32 - 1))
}
pub fn format_g(x: f64, precision: usize) -> String {
if x == 0.0 {
return if x.is_sign_negative() { "-0" } else { "0" }.to_string();
}
if x.is_nan() {
return if x.is_sign_negative() { "-nan" } else { "nan" }.to_string();
}
if x.is_infinite() {
return if x < 0.0 { "-inf" } else { "inf" }.to_string();
}
let p = precision.max(1);
let exp = g_decision_exponent(x.abs(), p);
if exp >= -4 && exp < p as i32 {
let digits = (p as i32 - 1 - exp).max(0) as usize;
let s = format!("{x:.digits$}");
if !s.contains('.') {
return s;
}
s.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
strip_trailing_zeros_sci(&format_e_val(x, p - 1, false, false), false)
}
}
fn format_g_val(val: f64, prec: usize, upper: bool, alt_form: bool) -> String {
if val == 0.0 {
let sign = if val.is_sign_negative() { "-" } else { "" };
if alt_form {
let p = if prec == 0 { 1 } else { prec };
let decimals = p.saturating_sub(1);
return format!("{sign}{}", format_f_val(0.0, decimals, true));
}
return format!("{sign}0");
}
let p = if prec == 0 { 1 } else { prec };
let exp = g_decision_exponent(val.abs(), p);
if exp < -4 || exp >= p as i32 {
let sig_prec = p.saturating_sub(1);
let raw = format_e_val(val, sig_prec, upper, alt_form);
if alt_form {
raw
} else {
strip_trailing_zeros_sci(&raw, upper)
}
} else {
let decimal_places = (p as i32 - 1 - exp).max(0) as usize;
let raw = format_f_val(val, decimal_places, alt_form);
if alt_form {
raw
} else if raw.contains('.') {
raw.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
raw
}
}
}
fn strip_trailing_zeros_sci(s: &str, upper: bool) -> String {
let sep = if upper { 'E' } else { 'e' };
if let Some(pos) = s.find(sep) {
let mantissa = &s[..pos];
let exp_part = &s[pos..];
let trimmed = if mantissa.contains('.') {
mantissa.trim_end_matches('0').trim_end_matches('.')
} else {
mantissa
};
format!("{trimmed}{exp_part}")
} else {
s.to_string()
}
}
impl Record for PrintfRecord {
fn record_type(&self) -> &'static str {
"printf"
}
fn constant_inputs_deliver_at_process(&self) -> bool {
true
}
fn long_string_fields(&self) -> &'static [&'static str] {
&["VAL"]
}
fn uses_monitor_deadband(&self) -> bool {
false
}
fn process(&mut self) -> CaResult<ProcessOutcome> {
self.val = self.apply_fmt();
self.len = (self.val.len() + 1) as u32;
Ok(ProcessOutcome::complete())
}
fn val(&self) -> Option<EpicsValue> {
Some(EpicsValue::CharArray(self.val.as_bytes().to_vec()))
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => Some(EpicsValue::CharArray(self.val.as_bytes().to_vec())),
"LEN" => Some(EpicsValue::ULong(self.len)),
"SIZV" => Some(EpicsValue::UShort(self.sizv)),
"FMT" => Some(EpicsValue::String(self.fmt.clone())),
"IVLS" => Some(EpicsValue::String(self.ivls.clone().into())),
_ => {
if let Some(idx) = Self::inp_index(name) {
return Some(EpicsValue::String(self.inp_links[idx].clone().into()));
}
if let Some(idx) = Self::val_index(name) {
return Some(self.vals[idx].clone());
}
None
}
}
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"SIZV" => {
let raw = match value {
EpicsValue::UShort(v) => Some(v as i32),
EpicsValue::Short(v) => Some(v as i32),
_ => None,
};
if let Some(raw) = raw {
self.sizv = raw.clamp(16, 0x7fff) as u16;
}
}
"VAL" => {
let mut text = match value {
EpicsValue::String(s) => truncate_bytes(s, MAX_STRING_SIZE - 1),
EpicsValue::CharArray(bytes) => {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
PvString::from_bytes(bytes[..end].to_vec())
}
_ => return Err(CaError::TypeMismatch("VAL".into())),
};
let max = (self.sizv as usize).saturating_sub(1);
if text.len() > max {
text = truncate_bytes(text, max);
}
self.val = text.as_str_lossy().into_owned();
self.len = (self.val.len() + 1) as u32;
}
"FMT" => {
if let EpicsValue::String(s) = value {
self.fmt = s;
} else {
return Err(CaError::TypeMismatch("FMT".into()));
}
}
"IVLS" => {
if let EpicsValue::String(s) = value {
self.ivls = s.as_str_lossy().chars().take(15).collect();
} else {
return Err(CaError::TypeMismatch("IVLS".into()));
}
}
_ => {
if let Some(idx) = Self::inp_index(name) {
if let EpicsValue::String(s) = value {
self.inp_links[idx] = s.as_str_lossy().into_owned();
} else {
return Err(CaError::TypeMismatch(name.into()));
}
} else if let Some(idx) = Self::val_index(name) {
self.vals[idx] = value;
} else {
return Err(CaError::FieldNotFound(name.to_string()));
}
}
}
Ok(())
}
fn put_field_internal(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
if let Some(idx) = Self::val_index(name) {
self.vals[idx] = value;
return Ok(());
}
crate::server::record::put_field_internal_default(self, name, value)
}
fn input_link_read_as(
&self,
link_field: &str,
_source: &crate::server::record::OutTarget,
) -> Option<crate::server::record::LinkReadAs> {
use crate::server::record::LinkReadAs;
Some(match Self::inp_index(link_field) {
Some(idx) if self.plain_string_slots()[idx] => LinkReadAs::String,
_ => LinkReadAs::Native,
})
}
fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
&[
("INP0", "A"),
("INP1", "B"),
("INP2", "C"),
("INP3", "D"),
("INP4", "E"),
("INP5", "F"),
("INP6", "G"),
("INP7", "H"),
("INP8", "I"),
("INP9", "J"),
]
}
fn set_resolved_input_links(&mut self, resolved: &[&'static str]) {
self.resolved = [false; 10];
for &lf in resolved {
if let Some(idx) = Self::inp_index(lf) {
self.resolved[idx] = true;
}
}
}
}
#[cfg(all(test, unix, target_env = "gnu"))]
mod libc_differential {
use super::format_g;
pub(super) fn libc_g(v: f64, prec: usize) -> String {
let mut buf = [0u8; 512];
let n = unsafe {
libc::snprintf(
buf.as_mut_ptr().cast(),
buf.len(),
c"%.*g".as_ptr(),
prec as libc::c_int,
v,
)
};
assert!(n >= 0 && (n as usize) < buf.len(), "snprintf overflow");
String::from_utf8(buf[..n as usize].to_vec()).expect("glibc writes ASCII")
}
pub(super) struct XorShift64(u64);
impl XorShift64 {
pub(super) fn new(seed: u64) -> Self {
Self(seed)
}
pub(super) fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
}
pub(super) fn sample() -> Vec<f64> {
let mut out = vec![
0.0,
-0.0,
f64::NAN,
-f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
f64::MIN_POSITIVE,
f64::from_bits(1),
f64::MAX,
f64::MIN,
1.0 / 3.0,
0.1 + 0.2,
9.999_995,
99_999.5,
999_999.5,
0.000_099_999_95,
];
for e in -320i32..=308 {
for m in [1.0_f64, 1.5, 3.3333333, 9.999999, 9.9999995] {
let v = m * 10f64.powi(e);
if v.is_finite() {
out.push(v);
out.push(-v);
}
}
}
let mut rng = XorShift64::new(0x5EED_1234_ABCD_0001);
for _ in 0..50_000 {
out.push(f64::from_bits(rng.next()));
}
out
}
#[test]
fn format_float_conv_is_byte_identical_to_glibc() {
let values = sample();
let mut checked = 0usize;
let mut mismatches: Vec<String> = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let mut exempted = 0usize;
for &conv in b"eEfFgG" {
for prec in [0usize, 1, 3, 6, 17] {
for alt in [false, true] {
for &v in &values {
let ours = super::format_float_conv(conv, v, prec, alt);
let theirs = libc_conv(conv, prec, alt, v);
checked += 1;
if ours != theirs && is_glibc_alt_g_carry_quirk(conv, alt, &ours, &theirs) {
exempted += 1;
continue;
}
if ours != theirs {
let class = if v.is_nan() {
"nan"
} else if v.is_infinite() {
"inf"
} else if v == 0.0 {
"zero"
} else if v.abs() < f64::MIN_POSITIVE {
"subnormal"
} else {
"normal"
};
let key = format!(
"%{}.{prec}{} {class}{}",
if alt { "#" } else { "" },
conv as char,
if v.is_sign_negative() { "-" } else { "+" }
);
if seen.insert(key.clone()) {
mismatches.push(format!(
"{key}: {v:?} -> ours {ours:?} != glibc {theirs:?}"
));
}
}
}
}
}
}
assert!(
mismatches.is_empty(),
"{} of {checked} samples disagree with glibc:\n{}",
mismatches.len(),
mismatches.join("\n")
);
assert_eq!(
exempted, 18,
"the glibc %#g carry quirk should cover exactly these samples"
);
}
fn is_glibc_alt_g_carry_quirk(conv: u8, alt: bool, ours: &str, theirs: &str) -> bool {
if !alt || !matches!(conv, b'g' | b'G') {
return false;
}
let sep = if conv == b'G' { 'E' } else { 'e' };
let (Some((ours_mant, ours_exp)), Some((theirs_mant, theirs_exp))) =
(ours.split_once(sep), theirs.split_once(sep))
else {
return false;
};
ours_exp == theirs_exp
&& ours_mant.ends_with('0')
&& ours_mant.trim_end_matches('0') == theirs_mant
}
fn libc_conv(conv: u8, prec: usize, alt: bool, v: f64) -> String {
let spec = format!("%{}.{prec}{}\0", if alt { "#" } else { "" }, conv as char);
let mut buf = [0u8; 512];
let n =
unsafe { libc::snprintf(buf.as_mut_ptr().cast(), buf.len(), spec.as_ptr().cast(), v) };
assert!(n >= 0 && (n as usize) < buf.len(), "snprintf overflow");
String::from_utf8(buf[..n as usize].to_vec()).expect("glibc writes ASCII")
}
#[test]
fn format_g_is_byte_identical_to_glibc() {
let values = sample();
let mut checked = 0usize;
let mut mismatches: Vec<String> = Vec::new();
for prec in [1usize, 3, 6, 17] {
for &v in &values {
let ours = format_g(v, prec);
let theirs = libc_g(v, prec);
checked += 1;
if ours != theirs && mismatches.len() < 20 {
mismatches.push(format!(
"%.{prec}g of {v:?} (bits {:#018x}): ours {ours:?} != glibc {theirs:?}",
v.to_bits()
));
}
}
}
assert!(
mismatches.is_empty(),
"{} of {checked} samples disagree with glibc:\n{}",
mismatches.len(),
mismatches.join("\n")
);
}
fn libc_u32(v: i32) -> String {
let mut buf = [0u8; 64];
let n = unsafe {
libc::snprintf(
buf.as_mut_ptr().cast(),
buf.len(),
c"%u".as_ptr().cast(),
v as libc::c_uint,
)
};
assert!(n >= 0 && (n as usize) < buf.len());
String::from_utf8(buf[..n as usize].to_vec()).expect("glibc writes ASCII")
}
#[test]
fn u32_of_minus_one_is_what_glibc_prints() {
assert_eq!(libc_u32(-1i32), "4294967295");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rec_with(fmt: &str) -> PrintfRecord {
PrintfRecord {
fmt: PvString::from(fmt),
..Default::default()
}
}
impl PrintfRecord {
fn set_input(&mut self, idx: usize, value: EpicsValue) {
self.vals[idx] = value;
self.resolved[idx] = true;
}
}
#[test]
fn integer_conversions_narrow_by_dbr_type_not_through_f64() {
let big = (1i64 << 53) + 1;
let mut rec = rec_with("%lld");
rec.set_input(0, EpicsValue::Int64(big));
rec.process().unwrap();
assert_eq!(rec.val, format!("{big}"), "%lld must not round through f64");
let mut rec = rec_with("%d");
rec.set_input(0, EpicsValue::Int64(big));
rec.process().unwrap();
assert_eq!(rec.val, format!("{}", big as i32));
let mut rec = rec_with("%hd");
rec.set_input(0, EpicsValue::Long(40000));
rec.process().unwrap();
assert_eq!(rec.val, "-25536");
let mut rec = rec_with("%hhd");
rec.set_input(0, EpicsValue::Long(300));
rec.process().unwrap();
assert_eq!(rec.val, "44");
let mut rec = rec_with("%u");
rec.set_input(0, EpicsValue::Long(-1));
rec.process().unwrap();
assert_eq!(rec.val, "4294967295");
let mut rec = rec_with("%hx");
rec.set_input(0, EpicsValue::Long(-1));
rec.process().unwrap();
assert_eq!(rec.val, "ffff");
let mut rec = rec_with("%llx");
rec.set_input(0, EpicsValue::Int64(-1));
rec.process().unwrap();
assert_eq!(rec.val, "ffffffffffffffff");
let mut rec = rec_with("%hhx");
rec.set_input(0, EpicsValue::Long(-1));
rec.process().unwrap();
assert_eq!(rec.val, "ff");
let mut rec = rec_with("%d/%o");
rec.set_input(0, EpicsValue::Long(-1));
rec.set_input(1, EpicsValue::Long(8));
rec.process().unwrap();
assert_eq!(rec.val, "-1/10");
}
#[test]
fn h_on_a_float_conversion_rounds_through_f32() {
let v = 0.12345678901234568f64;
let mut rec = rec_with("%.10f");
rec.set_input(0, EpicsValue::Double(v));
rec.process().unwrap();
let full = rec.val.clone();
let mut rec = rec_with("%.10hf");
rec.set_input(0, EpicsValue::Double(v));
rec.process().unwrap();
assert_eq!(rec.val, format!("{:.10}", v as f32 as f64));
assert_ne!(rec.val, full, "h must actually narrow to single precision");
}
#[test]
fn mixed_length_modifiers_are_a_bad_format() {
for fmt in ["%hld", "%lhd", "%hhhd", "%llld"] {
let mut rec = rec_with(fmt);
rec.set_input(0, EpicsValue::Long(7));
rec.process().unwrap();
assert_eq!(rec.val, fmt, "{fmt} must be F_BADFMT, got {:?}", rec.val);
}
}
#[test]
fn percent_s_formats_string_input() {
let mut rec = rec_with("name=%s");
rec.set_input(0, EpicsValue::String("motor1".into()));
rec.process().unwrap();
assert_eq!(rec.val, "name=motor1");
}
#[test]
fn percent_s_with_width_padding() {
let mut rec = rec_with("[%8s]");
rec.set_input(0, EpicsValue::String("ab".into()));
rec.process().unwrap();
assert_eq!(rec.val, "[ ab]");
}
#[test]
fn star_width_consumes_an_input() {
let mut rec = rec_with("%*d");
rec.set_input(0, EpicsValue::Long(6)); rec.set_input(1, EpicsValue::Long(42)); rec.process().unwrap();
assert_eq!(rec.val, " 42");
}
#[test]
fn long_modifier_consumed() {
let mut rec = rec_with("%ld");
rec.set_input(0, EpicsValue::Long(99));
rec.process().unwrap();
assert_eq!(rec.val, "99");
}
#[test]
fn long_string_conversion() {
let mut rec = rec_with("%ls");
rec.set_input(0, EpicsValue::String("hello".into()));
rec.process().unwrap();
assert_eq!(rec.val, "hello");
}
#[test]
fn percent_c_formats_char() {
let mut rec = rec_with("%c");
rec.set_input(0, EpicsValue::Long(65)); rec.process().unwrap();
assert_eq!(rec.val, "A");
}
#[test]
fn bad_format_echoes_directive_text() {
let mut rec = rec_with("x=%q");
rec.process().unwrap();
assert_eq!(rec.val, "x=%q");
}
#[test]
fn trailing_percent_echoes_verbatim() {
let mut rec = rec_with("v=%");
rec.process().unwrap();
assert_eq!(rec.val, "v=%");
}
#[test]
fn len_counts_formatted_bytes_plus_nul() {
let mut rec = rec_with("name=%s");
rec.set_input(0, EpicsValue::String("motor1".into()));
rec.process().unwrap();
assert_eq!(rec.val, "name=motor1");
assert_eq!(rec.len, 12);
assert_eq!(rec.get_field("LEN"), Some(EpicsValue::ULong(12)));
}
#[test]
fn len_of_empty_value_is_one() {
let mut rec = rec_with("");
rec.process().unwrap();
assert_eq!(rec.val, "");
assert_eq!(rec.len, 1);
assert_eq!(rec.get_field("LEN"), Some(EpicsValue::ULong(1)));
}
#[test]
fn g_zero_alt_form() {
let mut rec = rec_with("%g");
rec.set_input(0, EpicsValue::Double(0.0));
rec.process().unwrap();
assert_eq!(rec.val, "0");
let mut rec = rec_with("%#.3g");
rec.set_input(0, EpicsValue::Double(0.0));
rec.process().unwrap();
assert_eq!(rec.val, "0.00");
}
#[test]
fn g_style_comes_from_the_rounded_exponent() {
let mut rec = rec_with("%g");
rec.set_input(0, EpicsValue::Double(9.9999995e-05));
rec.process().unwrap();
assert_eq!(rec.val, "0.0001");
}
#[test]
fn percent_escape() {
let mut rec = rec_with("100%%");
rec.process().unwrap();
assert_eq!(rec.val, "100%");
}
#[test]
fn unresolved_link_emits_ivls() {
let mut rec = rec_with("v=%d");
rec.process().unwrap();
assert_eq!(rec.val, "v=LNK");
}
#[test]
fn mixed_resolved_and_unresolved() {
let mut rec = rec_with("%d/%d");
rec.set_input(0, EpicsValue::Long(7)); rec.process().unwrap();
assert_eq!(rec.val, "7/LNK");
}
#[test]
fn exhausted_links_emit_ivls() {
let mut rec = rec_with("%d%d%d%d%d%d%d%d%d%d%d"); for i in 0..10 {
rec.set_input(i, EpicsValue::Long(i as i32));
}
rec.process().unwrap();
assert_eq!(rec.val, "0123456789LNK");
}
#[test]
fn custom_ivls_is_emitted() {
let mut rec = rec_with("%d");
rec.put_field("IVLS", EpicsValue::String("BAD".into()))
.unwrap();
rec.process().unwrap();
assert_eq!(rec.val, "BAD");
assert_eq!(
rec.get_field("IVLS"),
Some(EpicsValue::String("BAD".into()))
);
}
#[test]
fn ivls_clamps_to_fifteen_chars() {
let mut rec = rec_with("");
rec.put_field("IVLS", EpicsValue::String("0123456789ABCDEFGHIJ".into()))
.unwrap();
assert_eq!(rec.ivls, "0123456789ABCDE");
}
#[test]
fn failed_star_width_does_not_consume_conversion_slot() {
let mut rec = rec_with("%*d|%d");
rec.set_input(1, EpicsValue::Long(42));
rec.process().unwrap();
assert_eq!(rec.val, "LNK|42");
}
}