epics_base_rs/types/dbr.rs
1use crate::error::{CaError, CaResult};
2
3// DBR type ranges (matches db_access.h):
4// native (0..=6), STS (7..=13), TIME (14..=20), GR (21..=27),
5// CTRL (28..=34), special PUT_ACKT (35), PUT_ACKS (36),
6// STSACK_STRING (37), CLASS_NAME (38).
7
8// Native scalar types (also exposed via DbFieldType enum)
9pub const DBR_STRING: u16 = 0;
10pub const DBR_SHORT: u16 = 1;
11pub const DBR_FLOAT: u16 = 2;
12pub const DBR_ENUM: u16 = 3;
13pub const DBR_CHAR: u16 = 4;
14pub const DBR_LONG: u16 = 5;
15pub const DBR_DOUBLE: u16 = 6;
16// `DBR_INT` is the libca alias for `DBR_SHORT`.
17pub const DBR_INT: u16 = DBR_SHORT;
18
19// Status-only metadata layer (CA-261)
20pub const DBR_STS_STRING: u16 = 7;
21pub const DBR_STS_SHORT: u16 = 8;
22pub const DBR_STS_FLOAT: u16 = 9;
23pub const DBR_STS_ENUM: u16 = 10;
24pub const DBR_STS_CHAR: u16 = 11;
25pub const DBR_STS_LONG: u16 = 12;
26pub const DBR_STS_DOUBLE: u16 = 13;
27pub const DBR_STS_INT: u16 = DBR_STS_SHORT;
28
29// Status + timestamp layer (CA-262)
30pub const DBR_TIME_STRING: u16 = 14;
31pub const DBR_TIME_SHORT: u16 = 15;
32pub const DBR_TIME_FLOAT: u16 = 16;
33pub const DBR_TIME_ENUM: u16 = 17;
34pub const DBR_TIME_CHAR: u16 = 18;
35pub const DBR_TIME_LONG: u16 = 19;
36pub const DBR_TIME_DOUBLE: u16 = 20;
37pub const DBR_TIME_INT: u16 = DBR_TIME_SHORT;
38
39// Status + graphic (display limits / units / precision) layer (CA-263)
40pub const DBR_GR_STRING: u16 = 21;
41pub const DBR_GR_SHORT: u16 = 22;
42pub const DBR_GR_FLOAT: u16 = 23;
43pub const DBR_GR_ENUM: u16 = 24;
44pub const DBR_GR_CHAR: u16 = 25;
45pub const DBR_GR_LONG: u16 = 26;
46pub const DBR_GR_DOUBLE: u16 = 27;
47pub const DBR_GR_INT: u16 = DBR_GR_SHORT;
48
49// Status + graphic + control limits layer (CA-264)
50pub const DBR_CTRL_STRING: u16 = 28;
51pub const DBR_CTRL_SHORT: u16 = 29;
52pub const DBR_CTRL_FLOAT: u16 = 30;
53pub const DBR_CTRL_ENUM: u16 = 31;
54pub const DBR_CTRL_CHAR: u16 = 32;
55pub const DBR_CTRL_LONG: u16 = 33;
56pub const DBR_CTRL_DOUBLE: u16 = 34;
57pub const DBR_CTRL_INT: u16 = DBR_CTRL_SHORT;
58
59// Special alarm-acknowledgement / introspection types
60pub const DBR_PUT_ACKT: u16 = 35;
61pub const DBR_PUT_ACKS: u16 = 36;
62pub const DBR_STSACK_STRING: u16 = 37;
63/// Returns the IOC's record-type class name as a 40-byte string
64/// (CA-268, db_access.h: `DBR_CLASS_NAME`).
65pub const DBR_CLASS_NAME: u16 = 38;
66
67/// Last allocated DBR type code, matching the C `LAST_BUFFER_TYPE` macro.
68pub const LAST_BUFFER_TYPE: u16 = DBR_CLASS_NAME;
69
70/// EPICS DBR field types (native types only)
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[repr(u16)]
73pub enum DbFieldType {
74 String = 0,
75 Short = 1, // aka Int16
76 Float = 2,
77 Enum = 3,
78 Char = 4, // aka UInt8
79 Long = 5, // aka Int32
80 Double = 6,
81 /// Internal-only type for int64in/int64out records.
82 /// No CA wire type 7 exists; over CA these PVs appear as Double (type 6).
83 Int64 = 7,
84 /// Internal-only type for unsigned 64-bit EPICS fields (C `DBF_UINT64`,
85 /// dbStatic `dbfType` index 8). The CA wire protocol has no 64-bit
86 /// type, so over CA these PVs appear as Double (type 6); over PVA they
87 /// are served natively as `ulong`. Mirrors `Int64`'s CA handling.
88 UInt64 = 8,
89 /// Internal-only type for unsigned 16-bit EPICS fields (C `DBF_USHORT`,
90 /// dbStatic `dbfType`). The CA wire protocol has no unsigned types, so
91 /// the IOC promotes `DBF_USHORT` to the next signed type that holds its
92 /// full `0..=65535` range — `DBR_LONG` (the C `dbDBRnewToDBRold` table,
93 /// `db_convert.h`: `5, /*DBR_USHORT to DBR_LONG*/`). Over PVA pvxs
94 /// serves it natively as `ushort` (`ioc/typeutils.cpp:38-40`:
95 /// `DBR_USHORT -> TypeCode::UInt16`). The discriminant is an internal
96 /// marker (not a CA wire code); see [`Self::ca_wire_type`].
97 UShort = 9,
98 /// Internal-only type for unsigned 32-bit EPICS fields (C `DBF_ULONG`,
99 /// dbStatic `dbfType`). `0..=4294967295` does not fit in `i32`, so the
100 /// IOC promotes `DBF_ULONG` to `DBR_DOUBLE` over CA exactly like
101 /// `UInt64`/`Int64` (`db_convert.h`: `6, /*DBR_ULONG to DBR_DOUBLE*/`).
102 /// Over PVA pvxs serves it natively as `uint` (`ioc/typeutils.cpp:43-44`:
103 /// `DBR_ULONG -> TypeCode::UInt32`).
104 ULong = 10,
105 /// Internal-only type for unsigned 8-bit EPICS fields (C `DBF_UCHAR`,
106 /// dbStatic `dbfType` index 2 / `waveform` `FTVL=UCHAR`). Unlike the
107 /// signed `Char` (epicsInt8), this is epicsUInt8. The CA wire protocol
108 /// has no unsigned types, so the IOC promotes `DBF_UCHAR` to `DBR_CHAR`
109 /// — the same 1-byte wire type as `Char` (the C `dbDBRnewToDBRold` table,
110 /// `db_convert.h`: `4, /*DBR_UCHAR to DBR_CHAR*/`); the raw bytes are
111 /// identical, only the signedness of the interpretation differs. Over PVA
112 /// pvxs serves it natively as `ubyte` (`ioc/typeutils.cpp:34-35`:
113 /// `DBR_UCHAR -> TypeCode::UInt8`), distinct from `Char`'s signed `byte`.
114 /// The discriminant is an internal marker (not a CA wire code); see
115 /// [`Self::ca_wire_type`].
116 UChar = 11,
117}
118
119impl DbFieldType {
120 pub fn from_u16(v: u16) -> CaResult<Self> {
121 match v {
122 0 => Ok(Self::String),
123 1 => Ok(Self::Short),
124 2 => Ok(Self::Float),
125 3 => Ok(Self::Enum),
126 4 => Ok(Self::Char),
127 5 => Ok(Self::Long),
128 6 => Ok(Self::Double),
129 _ => Err(CaError::UnsupportedType(v)),
130 }
131 }
132
133 /// Size in bytes for a single element of this type's native carrier.
134 ///
135 /// This is the carrier width (`UShort` = 2, `ULong` = 4), not the
136 /// CA-wire-promoted width: the CA value path always promotes via
137 /// [`crate::types::EpicsValue::dbr_type`] first and sizes buffers off
138 /// the promoted type (`UShort`→`Long`=4, `ULong`→`Double`=8), so this
139 /// width is never used to size a CA value array for the unsigned types.
140 pub fn element_size(&self) -> usize {
141 match self {
142 Self::String => 40, // MAX_STRING_SIZE
143 Self::Short | Self::Enum | Self::UShort => 2,
144 Self::Float | Self::Long | Self::ULong => 4,
145 Self::Char | Self::UChar => 1,
146 Self::Double | Self::Int64 | Self::UInt64 => 8,
147 }
148 }
149
150 /// Return the wire type code as a `u16`. The internal-only types have
151 /// no CA wire code, so they report the signed CA type the IOC promotes
152 /// them to (C `dbDBRnewToDBRold`, `db_convert.h`): `Int64`/`UInt64`/
153 /// `ULong` → `DBR_DOUBLE` (6), `UShort` → `DBR_LONG` (5, the smallest
154 /// signed CA type that holds the full `0..=65535` range), `UChar` →
155 /// `DBR_CHAR` (4, same 1-byte wire type — the bytes are identical, only
156 /// the interpretation is unsigned).
157 pub fn ca_wire_type(&self) -> u16 {
158 match self {
159 Self::Int64 | Self::UInt64 | Self::ULong => Self::Double as u16,
160 Self::UShort => Self::Long as u16,
161 Self::UChar => Self::Char as u16,
162 other => *other as u16,
163 }
164 }
165
166 /// Return the `DBR_STS_xxx` type code for this native type
167 /// (Int64 maps to `DBR_STS_DOUBLE`).
168 pub fn sts_dbr_type(&self) -> u16 {
169 self.ca_wire_type() + 7
170 }
171
172 /// Return the `DBR_TIME_xxx` type code for this native type
173 /// (Int64 maps to `DBR_TIME_DOUBLE`).
174 pub fn time_dbr_type(&self) -> u16 {
175 self.ca_wire_type() + 14
176 }
177
178 /// Return the `DBR_GR_xxx` type code for this native type
179 /// (Int64 maps to `DBR_GR_DOUBLE`).
180 pub fn gr_dbr_type(&self) -> u16 {
181 self.ca_wire_type() + 21
182 }
183
184 /// Return the `DBR_CTRL_xxx` type code for this native type
185 /// (Int64 maps to `DBR_CTRL_DOUBLE`).
186 pub fn ctrl_dbr_type(&self) -> u16 {
187 self.ca_wire_type() + 28
188 }
189
190 /// Calculate total buffer size for N elements of this type.
191 /// Equivalent to C EPICS dbValueSize(type) * count.
192 pub fn buffer_size(&self, count: usize) -> usize {
193 self.element_size() * count
194 }
195
196 /// Map field type to request type (C EPICS mapDBFToDBR).
197 /// DBF_MENU and DBF_DEVICE map to DBR_ENUM in C EPICS.
198 /// In Rust these are already represented as DbFieldType::Enum,
199 /// so this is an identity mapping for documentation/completeness.
200 pub fn to_dbr_type(&self) -> DbFieldType {
201 *self
202 }
203}
204
205/// dbStatic link-field classes — the three `dbfType` values that mark a
206/// record field as a *link* rather than a value
207/// (`dbFldTypes.h`: `DBF_INLINK`=14, `DBF_OUTLINK`=15, `DBF_FWDLINK`=16).
208///
209/// pvxs rejects a QSRV group PUT to any field whose
210/// `dbChannelFinalFieldType` falls in `DBF_INLINK..=DBF_FWDLINK`
211/// (`ioc/groupsource.cpp:596-606`). The Rust port has no dbStatic field
212/// table, so [`dbf_link_class`] reconstructs the same classification from
213/// the EPICS Base / synApps `*.dbd(.pod)` link-field families and returns
214/// the matching class. Consumers gate "is this field a link" on
215/// `dbf_link_class(..).is_some()` (or [`is_link_dbf_type`] when they
216/// already hold a dbStatic code), rather than maintaining their own
217/// partial spelling lists.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219#[repr(u8)]
220pub enum DbfLinkClass {
221 /// `DBF_INLINK` (14) — an input link (`INP`, `DOL`, `SIML`, …).
222 InLink = DBF_INLINK,
223 /// `DBF_OUTLINK` (15) — an output link (`OUT`, `LNKn`, …).
224 OutLink = DBF_OUTLINK,
225 /// `DBF_FWDLINK` (16) — a forward link (`FLNK`).
226 FwdLink = DBF_FWDLINK,
227}
228
229/// `dbFldTypes.h` `DBF_INLINK`.
230pub const DBF_INLINK: u8 = 14;
231/// `dbFldTypes.h` `DBF_OUTLINK`.
232pub const DBF_OUTLINK: u8 = 15;
233/// `dbFldTypes.h` `DBF_FWDLINK`.
234pub const DBF_FWDLINK: u8 = 16;
235
236impl DbfLinkClass {
237 /// The dbStatic `dbfType` numeric code (`dbFldTypes.h`).
238 pub fn dbf_type(self) -> u8 {
239 self as u8
240 }
241}
242
243/// True iff `dbf_type` is a link class — the exact
244/// `DBF_INLINK <= t <= DBF_FWDLINK` range check pvxs applies in
245/// `ioc/groupsource.cpp:596-606`. Use when a caller already holds a
246/// dbStatic field-type code; [`dbf_link_class`] is the name-keyed entry
247/// point for the Rust port, which carries no dbStatic table.
248pub fn is_link_dbf_type(dbf_type: u8) -> bool {
249 (DBF_INLINK..=DBF_FWDLINK).contains(&dbf_type)
250}
251
252/// Classify a record field by its dbStatic link class, or `None` when it
253/// is not a link field. This is the single canonical owner of the
254/// "is this field a link" rule for the Rust port — the structural
255/// replacement for the partial, per-consumer name lists that the
256/// `groupsource.cpp:596-606` review flagged (a record-type-blind
257/// spelling list re-opens the bypass for every record that names a link
258/// field outside the list).
259///
260/// `record_type` is the record's `recordType` (`ai`, `bo`, `seq`, …); it
261/// is consulted to resolve the two direction-ambiguous fields that share a
262/// spelling across record families:
263/// - `SIOL` is `DBF_OUTLINK` on output records and `DBF_INLINK` on input
264/// records (compare `boRecord.dbd.pod:318` `SIOL,DBF_OUTLINK` vs
265/// `aiRecord`/`biRecord` `SIOL,DBF_INLINK`).
266/// - `LNK0..LNKF` is `DBF_FWDLINK` on `fanoutRecord` (the multi-forward
267/// fan-out family) and `DBF_OUTLINK` on `seqRecord` / synApps
268/// `sseqRecord` (compare `fanoutRecord.dbd.pod` `field(LNK0,DBF_FWDLINK)`
269/// vs `seqRecord.dbd.pod` `field(LNK0,DBF_OUTLINK)`).
270///
271/// Every other family has a fixed class across all record types.
272///
273/// Names and classes are taken verbatim from EPICS Base / synApps
274/// `*.dbd(.pod)`:
275/// - dbCommon: `FLNK`→Fwd, `SDIS`/`TSEL`→In.
276/// - `INP`/`DOL`/`SIML`/`NVL`/`SVL`/`SUBL`/`SELL`→In, `OUT`→Out.
277/// - `SIOL`→Out on output records, else In.
278/// - `INPA..INPU`/`INP0..INP9`→In; `OUTA..OUTU`→Out.
279/// - `DOL0..DOL9`/`DOLA..DOLF`→In.
280/// - `LNK0..LNK9`/`LNKA..LNKF`→Fwd on `fanout`, else Out.
281pub fn dbf_link_class(record_type: &str, field: &str) -> Option<DbfLinkClass> {
282 use DbfLinkClass::*;
283 let f = field.trim().to_ascii_uppercase();
284
285 // Exact dbCommon + record-specific named link fields.
286 match f.as_str() {
287 "FLNK" => return Some(FwdLink),
288 "SDIS" | "TSEL" | "INP" | "DOL" | "SIML" | "NVL" | "SVL" | "SUBL" | "SELL" => {
289 return Some(InLink);
290 }
291 "OUT" => return Some(OutLink),
292 "SIOL" => {
293 return Some(if is_output_record_type(record_type) {
294 OutLink
295 } else {
296 InLink
297 });
298 }
299 _ => {}
300 }
301
302 // Indexed / lettered link families: a known prefix plus exactly one
303 // alphanumeric suffix character. The DBDs use `A..U` / `0..9` / `A..F`,
304 // but any single-alnum suffix on these prefixes is a link in every
305 // base record that defines the family, so a one-char-suffix rule is
306 // both complete and on the safe (reject) side for custom records.
307 let one_alnum = |rest: &str| rest.len() == 1 && rest.as_bytes()[0].is_ascii_alphanumeric();
308 // `LNK0..LNKF` is direction-ambiguous by record type the same way
309 // `SIOL` is: `fanoutRecord` declares the family as `DBF_FWDLINK`
310 // (multi-forward fan-out), every other family that uses the spelling
311 // (`seqRecord`, synApps `sseqRecord`) declares it `DBF_OUTLINK`.
312 // Resolve it once here so the prefix table below carries a single
313 // class per spelling rather than collapsing fanout's forward links to
314 // output links by prefix.
315 let lnk_class = if is_fanout_link_record(record_type) {
316 FwdLink
317 } else {
318 OutLink
319 };
320 for (prefix, class) in [
321 ("INP", InLink),
322 ("DOL", InLink),
323 ("OUT", OutLink),
324 ("LNK", lnk_class),
325 ] {
326 if let Some(rest) = f.strip_prefix(prefix) {
327 if one_alnum(rest) {
328 return Some(class);
329 }
330 }
331 }
332 None
333}
334
335/// Record types whose `SIOL` simulation-output link is `DBF_OUTLINK`
336/// (the output records). Input records declare `SIOL` as `DBF_INLINK`.
337/// Same output-record set as the device-write records — compare
338/// `crate::server::record::Record::can_device_write`.
339fn is_output_record_type(record_type: &str) -> bool {
340 matches!(
341 record_type,
342 "ao" | "bo" | "longout" | "int64out" | "mbbo" | "mbboDirect" | "stringout" | "lso" | "aao"
343 )
344}
345
346/// `fanoutRecord` is the one Base family whose `LNK0..LNKF` fields are
347/// `DBF_FWDLINK` (forward links fired in numerical order). Every other
348/// family that uses the same `LNK*` spelling — `seqRecord`, synApps
349/// `sseqRecord` — declares them `DBF_OUTLINK`. Compare
350/// `fanoutRecord.dbd.pod` `field(LNK0,DBF_FWDLINK)` vs `seqRecord.dbd.pod`
351/// `field(LNK0,DBF_OUTLINK)`.
352fn is_fanout_link_record(record_type: &str) -> bool {
353 record_type == "fanout"
354}
355
356/// Calculate buffer size for a DBR type including metadata, matching C
357/// `dbr_size_n(TYPE, COUNT) = dbr_size[TYPE] + (COUNT-1)*dbr_value_size[TYPE]`.
358///
359/// the metadata length is taken from
360/// [`crate::types::codec::dbr_meta_size`] — the single owner that the
361/// serializers (`serialize_dbr` / `encode_dbr`) emit against — so the
362/// explicit-count pad/truncate and no-read-access frame paths size
363/// TIME / GR / CTRL bodies exactly as the encoder writes them. A
364/// `metadata_matches_encoded_length` test pins `encoded_len ==
365/// dbr_buffer_size` across the whole (dbr_type, native) matrix, so the
366/// sizer can no longer drift from the encoder.
367pub fn dbr_buffer_size(dbr_type: u16, native_type: DbFieldType, count: usize) -> usize {
368 // DBR_CLASS_NAME (38) is always one MAX_STRING_SIZE (40) string,
369 // regardless of `count` or `native_type` — it carries no value[]
370 // array, so the generic meta+value formula does not apply.
371 if dbr_type == DBR_CLASS_NAME {
372 return 40;
373 }
374 let value_size = native_type.element_size() * count;
375 crate::types::codec::dbr_meta_size(dbr_type, native_type) + value_size
376}
377
378/// Extract the native DBF type index (0-6) from any DBR type code.
379fn dbr_native_index(dbr_type: u16) -> Option<u16> {
380 match dbr_type {
381 0..=6 => Some(dbr_type),
382 7..=13 => Some(dbr_type - 7),
383 14..=20 => Some(dbr_type - 14),
384 21..=27 => Some(dbr_type - 21),
385 28..=34 => Some(dbr_type - 28),
386 // Alarm-acknowledge writes carry a single u16, so map them to
387 // Short for codec purposes. STSACK_STRING returns a string body
388 // so it maps to String.
389 35 | 36 => Some(1), // DBR_PUT_ACKT / DBR_PUT_ACKS — u16
390 37 => Some(0), // DBR_STSACK_STRING — value is a string
391 // DBR_CLASS_NAME is a single fixed 40-byte string carrying the
392 // record's recordType. Treat as String for codec purposes.
393 38 => Some(0),
394 _ => None,
395 }
396}
397
398pub fn native_type_for_dbr(dbr_type: u16) -> CaResult<DbFieldType> {
399 match dbr_native_index(dbr_type) {
400 Some(idx) => DbFieldType::from_u16(idx),
401 None => Err(CaError::UnsupportedType(dbr_type)),
402 }
403}
404
405/// DBR request-type names indexed by type code, mirroring the C
406/// `dbr_text[]` table (`ca/src/client/access.cpp`). Index 0 =
407/// `DBR_STRING` … index 38 = `DBR_CLASS_NAME`.
408const DBR_TEXT: [&str; (LAST_BUFFER_TYPE + 1) as usize] = [
409 "DBR_STRING",
410 "DBR_SHORT",
411 "DBR_FLOAT",
412 "DBR_ENUM",
413 "DBR_CHAR",
414 "DBR_LONG",
415 "DBR_DOUBLE",
416 "DBR_STS_STRING",
417 "DBR_STS_SHORT",
418 "DBR_STS_FLOAT",
419 "DBR_STS_ENUM",
420 "DBR_STS_CHAR",
421 "DBR_STS_LONG",
422 "DBR_STS_DOUBLE",
423 "DBR_TIME_STRING",
424 "DBR_TIME_SHORT",
425 "DBR_TIME_FLOAT",
426 "DBR_TIME_ENUM",
427 "DBR_TIME_CHAR",
428 "DBR_TIME_LONG",
429 "DBR_TIME_DOUBLE",
430 "DBR_GR_STRING",
431 "DBR_GR_SHORT",
432 "DBR_GR_FLOAT",
433 "DBR_GR_ENUM",
434 "DBR_GR_CHAR",
435 "DBR_GR_LONG",
436 "DBR_GR_DOUBLE",
437 "DBR_CTRL_STRING",
438 "DBR_CTRL_SHORT",
439 "DBR_CTRL_FLOAT",
440 "DBR_CTRL_ENUM",
441 "DBR_CTRL_CHAR",
442 "DBR_CTRL_LONG",
443 "DBR_CTRL_DOUBLE",
444 "DBR_PUT_ACKT",
445 "DBR_PUT_ACKS",
446 "DBR_STSACK_STRING",
447 "DBR_CLASS_NAME",
448];
449
450/// Resolve a DBR request-type name to its type code, mirroring the C
451/// `dbr_text_to_type` macro (`db_access.h`): an exact, **case-sensitive**
452/// `strcmp` search of the `dbr_text[]` table. Returns the matching code
453/// (`0..=38`) or `None` when no name matches.
454///
455/// The case sensitivity is faithful to C — the `caget`/`caput` tools
456/// feed `-d <type>` straight through this search, so `-d DBR_TIME_FLOAT`
457/// resolves while `-d dbr_time_float` does not (the C tool then reverts
458/// to its plain/native request). Callers that accept the bare family
459/// (`caget -d TIME_FLOAT`) retry with a `DBR_` prefix, exactly as
460/// `caget.c` does.
461pub fn dbr_text_to_type(text: &str) -> Option<u16> {
462 DBR_TEXT.iter().position(|&n| n == text).map(|i| i as u16)
463}
464
465/// Resolve a DBR type code to its name, mirroring the C
466/// `dbr_type_to_text` macro (`db_access.h`): an index into the same
467/// `dbr_text[]` table, with C's `"DBR_invalid"` for anything outside
468/// `0..=38`. Inverse of [`dbr_text_to_type`], and the single owner of
469/// that direction — the CA client's exception block
470/// (`CA.Client.Exception ... type=%s`) and `caget -d`'s "Request type:"
471/// line both read the names from here.
472pub fn dbr_type_to_text(code: u16) -> &'static str {
473 DBR_TEXT
474 .get(code as usize)
475 .copied()
476 .unwrap_or("DBR_invalid")
477}
478
479#[cfg(test)]
480mod buffer_size_tests {
481 use super::*;
482
483 /// STS meta size is per-type. `dbr_sts_double` carries a
484 /// 4-byte `dbr_long_t` RISC_pad (db_access.h:233-238) → meta 8.
485 #[test]
486 fn sts_double_meta_is_8() {
487 // scalar: 8 (meta) + 8 (value) = 16
488 assert_eq!(dbr_buffer_size(DBR_STS_DOUBLE, DbFieldType::Double, 1), 16);
489 // n elements: 8 + 8*n
490 assert_eq!(
491 dbr_buffer_size(DBR_STS_DOUBLE, DbFieldType::Double, 5),
492 8 + 8 * 5
493 );
494 }
495
496 /// `dbr_sts_char` carries a 1-byte RISC_pad
497 /// (db_access.h:218-223) → meta 5.
498 #[test]
499 fn sts_char_meta_is_5() {
500 assert_eq!(dbr_buffer_size(DBR_STS_CHAR, DbFieldType::Char, 1), 6);
501 assert_eq!(dbr_buffer_size(DBR_STS_CHAR, DbFieldType::Char, 10), 5 + 10);
502 }
503
504 /// types with no STS RISC pad keep the flat 4-byte meta.
505 #[test]
506 fn sts_short_meta_is_4() {
507 assert_eq!(dbr_buffer_size(DBR_STS_SHORT, DbFieldType::Short, 1), 6);
508 assert_eq!(dbr_buffer_size(DBR_STS_LONG, DbFieldType::Long, 1), 8);
509 assert_eq!(dbr_buffer_size(DBR_STS_FLOAT, DbFieldType::Float, 1), 8);
510 }
511
512 /// Plain values carry no metadata.
513 #[test]
514 fn plain_value_size_only() {
515 assert_eq!(dbr_buffer_size(DBR_DOUBLE, DbFieldType::Double, 3), 24);
516 }
517
518 /// TIME structs carry a per-type RISC pad before `value[0]`
519 /// (C `dbr_time_*`, db_access.h:250-300). The pre-fix flat 12-byte
520 /// TIME meta truncated double/short/enum/char bodies.
521 #[test]
522 fn time_meta_includes_risc_pad() {
523 // double: 12 + RISC_pad(4) + value(8) = 24 (was wrongly 20).
524 assert_eq!(dbr_buffer_size(DBR_TIME_DOUBLE, DbFieldType::Double, 1), 24);
525 // short/enum: 12 + pad(2) + value(2) = 16.
526 assert_eq!(dbr_buffer_size(DBR_TIME_SHORT, DbFieldType::Short, 1), 16);
527 assert_eq!(dbr_buffer_size(DBR_TIME_ENUM, DbFieldType::Enum, 1), 16);
528 // char: 12 + pad(3) + value(1) = 16.
529 assert_eq!(dbr_buffer_size(DBR_TIME_CHAR, DbFieldType::Char, 1), 16);
530 // float/long: no pad (value already 4-aligned at offset 12).
531 assert_eq!(dbr_buffer_size(DBR_TIME_FLOAT, DbFieldType::Float, 1), 16);
532 assert_eq!(dbr_buffer_size(DBR_TIME_LONG, DbFieldType::Long, 1), 16);
533 // Explicit count scales the value array after the pad.
534 assert_eq!(
535 dbr_buffer_size(DBR_TIME_DOUBLE, DbFieldType::Double, 4),
536 16 + 8 * 4
537 );
538 }
539
540 /// GR/CTRL metadata is per native type (the pre-fix single
541 /// broad formula over-padded short/char/float/long and dropped the
542 /// enum `no_str` word).
543 #[test]
544 fn gr_ctrl_meta_is_per_type() {
545 // GR (6 limits): head(4) + layout.
546 assert_eq!(dbr_buffer_size(DBR_GR_SHORT, DbFieldType::Short, 1), 24 + 2);
547 assert_eq!(dbr_buffer_size(DBR_GR_FLOAT, DbFieldType::Float, 1), 40 + 4);
548 assert_eq!(
549 dbr_buffer_size(DBR_GR_DOUBLE, DbFieldType::Double, 1),
550 64 + 8
551 );
552 assert_eq!(dbr_buffer_size(DBR_GR_CHAR, DbFieldType::Char, 1), 19 + 1);
553 assert_eq!(dbr_buffer_size(DBR_GR_LONG, DbFieldType::Long, 1), 36 + 4);
554 // Enum: head(4) + no_str(2) + 16*26 strings = 422, value(2).
555 assert_eq!(dbr_buffer_size(DBR_GR_ENUM, DbFieldType::Enum, 1), 422 + 2);
556 // CTRL adds two control limits.
557 assert_eq!(
558 dbr_buffer_size(DBR_CTRL_DOUBLE, DbFieldType::Double, 1),
559 80 + 8
560 );
561 assert_eq!(
562 dbr_buffer_size(DBR_CTRL_SHORT, DbFieldType::Short, 1),
563 28 + 2
564 );
565 assert_eq!(dbr_buffer_size(DBR_CTRL_CHAR, DbFieldType::Char, 1), 21 + 1);
566 }
567}
568
569#[cfg(test)]
570mod dbf_link_class_tests {
571 use super::*;
572
573 #[test]
574 fn dbcommon_links_classified_uniformly() {
575 // Present on every record (dbCommon.dbd).
576 assert_eq!(dbf_link_class("ai", "FLNK"), Some(DbfLinkClass::FwdLink));
577 assert_eq!(dbf_link_class("ao", "SDIS"), Some(DbfLinkClass::InLink));
578 assert_eq!(dbf_link_class("calc", "TSEL"), Some(DbfLinkClass::InLink));
579 }
580
581 #[test]
582 fn record_specific_link_families_the_old_name_list_missed() {
583 // The families the reviewed partial spelling list omitted, each a
584 // DBF_INLINK/OUTLINK in EPICS Base `*.dbd.pod`:
585 // seqRecord DOL0 (INLINK) / LNK0 (OUTLINK) / DOLA / DOLF / LNKF
586 assert_eq!(dbf_link_class("seq", "DOL0"), Some(DbfLinkClass::InLink));
587 assert_eq!(dbf_link_class("seq", "LNK0"), Some(DbfLinkClass::OutLink));
588 assert_eq!(dbf_link_class("seq", "DOLA"), Some(DbfLinkClass::InLink));
589 assert_eq!(dbf_link_class("seq", "DOLF"), Some(DbfLinkClass::InLink));
590 assert_eq!(dbf_link_class("seq", "LNKF"), Some(DbfLinkClass::OutLink));
591 // selRecord NVL (INLINK); histogramRecord SVL (INLINK)
592 assert_eq!(dbf_link_class("sel", "NVL"), Some(DbfLinkClass::InLink));
593 assert_eq!(
594 dbf_link_class("histogram", "SVL"),
595 Some(DbfLinkClass::InLink)
596 );
597 // calc/aSub INPA..INPU (INLINK); fanout/aSub OUTA (OUTLINK)
598 assert_eq!(dbf_link_class("calc", "INPA"), Some(DbfLinkClass::InLink));
599 assert_eq!(dbf_link_class("aSub", "INPU"), Some(DbfLinkClass::InLink));
600 assert_eq!(
601 dbf_link_class("fanout", "OUTA"),
602 Some(DbfLinkClass::OutLink)
603 );
604 // printf INP0..INP9 (INLINK)
605 assert_eq!(dbf_link_class("printf", "INP0"), Some(DbfLinkClass::InLink));
606 }
607
608 #[test]
609 fn siol_class_depends_on_record_direction() {
610 // boRecord.dbd.pod:318 SIOL=DBF_OUTLINK; ai/bi SIOL=DBF_INLINK.
611 assert_eq!(dbf_link_class("bo", "SIOL"), Some(DbfLinkClass::OutLink));
612 assert_eq!(dbf_link_class("ao", "SIOL"), Some(DbfLinkClass::OutLink));
613 assert_eq!(dbf_link_class("ai", "SIOL"), Some(DbfLinkClass::InLink));
614 assert_eq!(dbf_link_class("bi", "SIOL"), Some(DbfLinkClass::InLink));
615 // SIML is always DBF_INLINK regardless of direction.
616 assert_eq!(dbf_link_class("bo", "SIML"), Some(DbfLinkClass::InLink));
617 assert_eq!(dbf_link_class("ai", "SIML"), Some(DbfLinkClass::InLink));
618 }
619
620 /// `LNK0..LNKF` shares
621 /// its spelling across two DBF classes — `fanoutRecord` declares the
622 /// family `DBF_FWDLINK`, while `seqRecord`/`sseqRecord` declare it
623 /// `DBF_OUTLINK`. The classifier must resolve by `record_type`, not
624 /// collapse every `LNK*` to OutLink by prefix.
625 #[test]
626 fn lnk_class_depends_on_record_type() {
627 // fanoutRecord.dbd.pod field(LNK0,DBF_FWDLINK) … field(LNKF,…).
628 assert_eq!(
629 dbf_link_class("fanout", "LNK0"),
630 Some(DbfLinkClass::FwdLink),
631 "fanout LNK0 is DBF_FWDLINK (16), not OUTLINK"
632 );
633 assert_eq!(
634 dbf_link_class("fanout", "LNKF"),
635 Some(DbfLinkClass::FwdLink),
636 "fanout LNKF is DBF_FWDLINK (16), not OUTLINK"
637 );
638 // seqRecord.dbd.pod field(LNK0,DBF_OUTLINK): the default direction
639 // for the shared spelling.
640 assert_eq!(dbf_link_class("seq", "LNK0"), Some(DbfLinkClass::OutLink));
641 assert_eq!(dbf_link_class("seq", "LNKF"), Some(DbfLinkClass::OutLink));
642 // synApps sseqRecord LNK1..LNK9 are also DBF_OUTLINK (LNK10 is the
643 // two-char spelling the one-alnum suffix rule deliberately rejects).
644 assert_eq!(dbf_link_class("sseq", "LNK1"), Some(DbfLinkClass::OutLink));
645 // fanout's dbCommon FLNK is still a forward link (unchanged).
646 assert_eq!(
647 dbf_link_class("fanout", "FLNK"),
648 Some(DbfLinkClass::FwdLink)
649 );
650 }
651
652 #[test]
653 fn plain_value_fields_are_not_links() {
654 for f in [
655 "VAL", "EGU", "PREC", "HOPR", "RVAL", "DESC", "A", "B", "OVAL",
656 ] {
657 assert_eq!(dbf_link_class("ai", f), None, "{f} must not be a link");
658 }
659 }
660
661 #[test]
662 fn case_insensitive_and_trims() {
663 assert_eq!(dbf_link_class("ai", "inp"), Some(DbfLinkClass::InLink));
664 assert_eq!(dbf_link_class("ao", " OUT "), Some(DbfLinkClass::OutLink));
665 }
666
667 #[test]
668 fn dbf_codes_and_range_match_pvxs() {
669 assert_eq!(DbfLinkClass::InLink.dbf_type(), 14);
670 assert_eq!(DbfLinkClass::OutLink.dbf_type(), 15);
671 assert_eq!(DbfLinkClass::FwdLink.dbf_type(), 16);
672 // pvxs groupsource.cpp:596-606 range check.
673 assert!(is_link_dbf_type(DBF_INLINK));
674 assert!(is_link_dbf_type(DBF_OUTLINK));
675 assert!(is_link_dbf_type(DBF_FWDLINK));
676 assert!(!is_link_dbf_type(13)); // DBF_DEVICE
677 assert!(!is_link_dbf_type(17)); // DBF_NOACCESS
678 assert!(!is_link_dbf_type(0)); // DBF_STRING
679 }
680}
681
682#[cfg(test)]
683mod dbr_text_tests {
684 use super::*;
685
686 #[test]
687 fn resolves_every_type_name_to_its_code() {
688 // Round-trip the whole table: each name resolves to its index.
689 for (code, name) in DBR_TEXT.iter().enumerate() {
690 assert_eq!(
691 dbr_text_to_type(name),
692 Some(code as u16),
693 "{name} should resolve to {code}"
694 );
695 }
696 // Boundary names spot-check (the cited High-finding values).
697 assert_eq!(dbr_text_to_type("DBR_STRING"), Some(DBR_STRING));
698 assert_eq!(dbr_text_to_type("DBR_TIME_FLOAT"), Some(DBR_TIME_FLOAT));
699 assert_eq!(
700 dbr_text_to_type("DBR_STSACK_STRING"),
701 Some(DBR_STSACK_STRING)
702 );
703 assert_eq!(dbr_text_to_type("DBR_CLASS_NAME"), Some(DBR_CLASS_NAME));
704 }
705
706 #[test]
707 fn is_case_sensitive_like_c_strcmp() {
708 // C `dbr_text_to_type` uses `strcmp`, so lowercase does not
709 // match — the C tools then revert to their plain request.
710 assert_eq!(dbr_text_to_type("dbr_time_float"), None);
711 assert_eq!(dbr_text_to_type("DBR_Time_Float"), None);
712 }
713
714 #[test]
715 fn unknown_and_bare_family_names_do_not_match() {
716 // Bare family names need the caller's `DBR_` retry — the raw
717 // search rejects them, matching C's first-pass `strcmp`.
718 assert_eq!(dbr_text_to_type("TIME_FLOAT"), None);
719 assert_eq!(dbr_text_to_type("DOUBLE"), None);
720 assert_eq!(dbr_text_to_type("DBR_NONSENSE"), None);
721 assert_eq!(dbr_text_to_type(""), None);
722 }
723}