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
119/// C's dbStatic `dbfType` code — the `DBF_*` token a `.dbd` declares and
120/// the number `dbStaticLib` stores, exports and prints (`dbFldTypes.h:24-43`
121/// @R7.0.10).
122///
123/// A SECOND fact about a field, not a renumbering of [`DbFieldType`]:
124/// that enum's discriminants are the CA **wire** codes and are load-bearing
125/// on the protocol, whereas these are dbStatic's own indices, and the two
126/// orders disagree from the second entry on (`DBF_CHAR` is 1 here and
127/// `DBR_SHORT` is 1 there). Anything that prints C's `%d` type number —
128/// `dba`'s `Field Type` row, dbStatic's `dbDumpField` — needs this one.
129///
130/// It also has six codes [`DbFieldType`] cannot express at all: a served
131/// type is one of twelve scalars, while a *declaration* can additionally be
132/// `MENU`, `DEVICE`, the three link kinds, or `NOACCESS`. That is why
133/// [`super::super::server::record::FieldDesc::declared_dbf`] carries this
134/// type rather than deriving it — the collapse is one-way.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
136#[repr(i16)]
137pub enum DbfCode {
138 /// `DBF_STRING` (0).
139 String = 0,
140 /// `DBF_CHAR` (1) — `epicsInt8`.
141 Char = 1,
142 /// `DBF_UCHAR` (2) — `epicsUInt8`.
143 UChar = 2,
144 /// `DBF_SHORT` (3).
145 Short = 3,
146 /// `DBF_USHORT` (4).
147 UShort = 4,
148 /// `DBF_LONG` (5).
149 Long = 5,
150 /// `DBF_ULONG` (6).
151 ULong = 6,
152 /// `DBF_INT64` (7).
153 Int64 = 7,
154 /// `DBF_UINT64` (8).
155 UInt64 = 8,
156 /// `DBF_FLOAT` (9).
157 Float = 9,
158 /// `DBF_DOUBLE` (10).
159 Double = 10,
160 /// `DBF_ENUM` (11).
161 Enum = 11,
162 /// `DBF_MENU` (12) — served as an enum, declared as a menu.
163 Menu = 12,
164 /// `DBF_DEVICE` (13) — the `DTYP` field, served as an enum.
165 Device = 13,
166 /// `DBF_INLINK` (14).
167 Inlink = 14,
168 /// `DBF_OUTLINK` (15).
169 Outlink = 15,
170 /// `DBF_FWDLINK` (16).
171 Fwdlink = 16,
172 /// `DBF_NOACCESS` (17) — a C-internal with no dbStatic representation.
173 NoAccess = 17,
174}
175
176impl DbfCode {
177 /// The token without its `DBF_` prefix — C's `dbf[]` (`dbTest.c:76-81`),
178 /// which is `pamapdbfType`'s `strvalue` (`dbFldTypes.h:51-70`) with that
179 /// prefix stripped. `dba` prints `DBF_%s` around it and dbStatic prints
180 /// the full name, so one table serves both.
181 pub const fn name(self) -> &'static str {
182 match self {
183 DbfCode::String => "STRING",
184 DbfCode::Char => "CHAR",
185 DbfCode::UChar => "UCHAR",
186 DbfCode::Short => "SHORT",
187 DbfCode::UShort => "USHORT",
188 DbfCode::Long => "LONG",
189 DbfCode::ULong => "ULONG",
190 DbfCode::Int64 => "INT64",
191 DbfCode::UInt64 => "UINT64",
192 DbfCode::Float => "FLOAT",
193 DbfCode::Double => "DOUBLE",
194 DbfCode::Enum => "ENUM",
195 DbfCode::Menu => "MENU",
196 DbfCode::Device => "DEVICE",
197 DbfCode::Inlink => "INLINK",
198 DbfCode::Outlink => "OUTLINK",
199 DbfCode::Fwdlink => "FWDLINK",
200 DbfCode::NoAccess => "NOACCESS",
201 }
202 }
203
204 /// C's `mapDBFToDBR` (`dbAccess.c:76-95`) — the DBR code `dbEntryToAddr`
205 /// stores in `paddr->dbr_field_type` (`:639`), which is what a client
206 /// asking for this field's *native* type is answered with.
207 ///
208 /// `DBR_*` here is `dbFldTypes.h:75-90`'s numbering, where every
209 /// `DBR_x` is `#define`d to its `DBF_x`, so the map is a projection of
210 /// this enum onto itself and needs no second type: it collapses `MENU`
211 /// and `DEVICE` onto `ENUM` and the three link types onto `STRING`, and
212 /// is the identity on the other thirteen.
213 ///
214 /// NOT the CA wire type. That numbering is the older, narrower
215 /// `db_access.h` one carried by [`DbFieldType`]'s own discriminants and
216 /// reached through [`DbFieldType::ca_wire_type`]; the two disagree on
217 /// every code above `DBR_SHORT`.
218 pub const fn dbr_code(self) -> DbfCode {
219 match self {
220 DbfCode::Menu | DbfCode::Device => DbfCode::Enum,
221 DbfCode::Inlink | DbfCode::Outlink | DbfCode::Fwdlink => DbfCode::String,
222 other => other,
223 }
224 }
225}
226
227impl DbFieldType {
228 /// The dbStatic code for the type this field is **served** as.
229 ///
230 /// The mapping only ever answers one of the twelve scalar codes,
231 /// because that is all a served type can be. It is therefore NOT a way
232 /// to recover a declaration: `ai.INP` and `dbCommon.FLNK` are both
233 /// served as [`DbFieldType::String`] and this returns
234 /// [`DbfCode::String`] for both, while their declarations are
235 /// `DBF_INLINK` and `DBF_FWDLINK`. Read
236 /// [`crate::server::record::FieldDesc::declared_dbf`] when the
237 /// question is what the `.dbd` said.
238 pub const fn dbf_code(self) -> DbfCode {
239 match self {
240 DbFieldType::String => DbfCode::String,
241 DbFieldType::Char => DbfCode::Char,
242 DbFieldType::UChar => DbfCode::UChar,
243 DbFieldType::Short => DbfCode::Short,
244 DbFieldType::UShort => DbfCode::UShort,
245 DbFieldType::Long => DbfCode::Long,
246 DbFieldType::ULong => DbfCode::ULong,
247 DbFieldType::Int64 => DbfCode::Int64,
248 DbFieldType::UInt64 => DbfCode::UInt64,
249 DbFieldType::Float => DbfCode::Float,
250 DbFieldType::Double => DbfCode::Double,
251 DbFieldType::Enum => DbfCode::Enum,
252 }
253 }
254
255 /// This type as the CA **wire** carries its value.
256 ///
257 /// The single owner of the one row where the wire and the database
258 /// disagree. `db_access.h:40` is `typedef epicsUInt8 dbr_char_t;`, so a
259 /// `DBR_CHAR` element off the network is UNSIGNED; the `DBF_CHAR` it
260 /// shares a name with is `epicsInt8` (`epicsTypes.h:44`). Every other
261 /// row names the same type twice, and `DBF_UCHAR` has no wire code of
262 /// its own — it promotes to `DBR_CHAR` (`db_convert.h`
263 /// `dbDBRnewToDBRold`), which is why one carrier serves both.
264 ///
265 /// [`Self::from_u16`] and [`crate::types::native_type_for_dbr`] answer
266 /// the DATABASE question: which field type does this code name. Neither
267 /// is the wire's answer, so every site that turns *received* CA bytes
268 /// into a value composes one of them with this. The naive answer costs
269 /// a sign: byte `0xC8` is 200 to C and -56 without this.
270 ///
271 /// The signed reading is not lost, it is just not the carrier's. C
272 /// re-creates it at the DISPLAY step — `val2str` assigns the
273 /// `dbr_char_t` into a plain `char` before `sprintf("%d")`
274 /// (`ca/src/tools/tool_lib.c:114`, `:160-161`) — which is why C's own
275 /// `caget` prints -56 for a byte the wire called 200.
276 pub fn wire_carrier(self) -> Self {
277 match self {
278 Self::Char => Self::UChar,
279 other => other,
280 }
281 }
282
283 /// The DATABASE field type a `DBF_` index names. **Not** the carrier of
284 /// a CA wire payload — compose with [`Self::wire_carrier`] for that.
285 pub fn from_u16(v: u16) -> CaResult<Self> {
286 match v {
287 0 => Ok(Self::String),
288 1 => Ok(Self::Short),
289 2 => Ok(Self::Float),
290 3 => Ok(Self::Enum),
291 4 => Ok(Self::Char),
292 5 => Ok(Self::Long),
293 6 => Ok(Self::Double),
294 _ => Err(CaError::UnsupportedType(v)),
295 }
296 }
297
298 /// Size in bytes for a single element of this type's native carrier.
299 ///
300 /// This is the carrier width (`UShort` = 2, `ULong` = 4), not the
301 /// CA-wire-promoted width: the CA value path always promotes via
302 /// [`crate::types::EpicsValue::dbr_type`] first and sizes buffers off
303 /// the promoted type (`UShort`→`Long`=4, `ULong`→`Double`=8), so this
304 /// width is never used to size a CA value array for the unsigned types.
305 pub fn element_size(&self) -> usize {
306 match self {
307 Self::String => 40, // MAX_STRING_SIZE
308 Self::Short | Self::Enum | Self::UShort => 2,
309 Self::Float | Self::Long | Self::ULong => 4,
310 Self::Char | Self::UChar => 1,
311 Self::Double | Self::Int64 | Self::UInt64 => 8,
312 }
313 }
314
315 /// Return the wire type code as a `u16`. The internal-only types have
316 /// no CA wire code, so they report the signed CA type the IOC promotes
317 /// them to (C `dbDBRnewToDBRold`, `db_convert.h`): `Int64`/`UInt64`/
318 /// `ULong` → `DBR_DOUBLE` (6), `UShort` → `DBR_LONG` (5, the smallest
319 /// signed CA type that holds the full `0..=65535` range), `UChar` →
320 /// `DBR_CHAR` (4, same 1-byte wire type — the bytes are identical, only
321 /// the interpretation is unsigned).
322 pub fn ca_wire_type(&self) -> u16 {
323 match self {
324 Self::Int64 | Self::UInt64 | Self::ULong => Self::Double as u16,
325 Self::UShort => Self::Long as u16,
326 Self::UChar => Self::Char as u16,
327 other => *other as u16,
328 }
329 }
330
331 /// Return the `DBR_STS_xxx` type code for this native type
332 /// (Int64 maps to `DBR_STS_DOUBLE`).
333 pub fn sts_dbr_type(&self) -> u16 {
334 self.ca_wire_type() + 7
335 }
336
337 /// Return the `DBR_TIME_xxx` type code for this native type
338 /// (Int64 maps to `DBR_TIME_DOUBLE`).
339 pub fn time_dbr_type(&self) -> u16 {
340 self.ca_wire_type() + 14
341 }
342
343 /// Return the `DBR_GR_xxx` type code for this native type
344 /// (Int64 maps to `DBR_GR_DOUBLE`).
345 pub fn gr_dbr_type(&self) -> u16 {
346 self.ca_wire_type() + 21
347 }
348
349 /// Return the `DBR_CTRL_xxx` type code for this native type
350 /// (Int64 maps to `DBR_CTRL_DOUBLE`).
351 pub fn ctrl_dbr_type(&self) -> u16 {
352 self.ca_wire_type() + 28
353 }
354
355 /// Calculate total buffer size for N elements of this type.
356 /// Equivalent to C EPICS dbValueSize(type) * count.
357 pub fn buffer_size(&self, count: usize) -> usize {
358 self.element_size() * count
359 }
360
361 /// Map field type to request type (C EPICS mapDBFToDBR).
362 /// DBF_MENU and DBF_DEVICE map to DBR_ENUM in C EPICS.
363 /// In Rust these are already represented as DbFieldType::Enum,
364 /// so this is an identity mapping for documentation/completeness.
365 pub fn to_dbr_type(&self) -> DbFieldType {
366 *self
367 }
368}
369
370/// dbStatic link-field classes — the three `dbfType` values that mark a
371/// record field as a *link* rather than a value
372/// (`dbFldTypes.h`: `DBF_INLINK`=14, `DBF_OUTLINK`=15, `DBF_FWDLINK`=16).
373///
374/// pvxs rejects a QSRV group PUT to any field whose
375/// `dbChannelFinalFieldType` falls in `DBF_INLINK..=DBF_FWDLINK`
376/// (`ioc/groupsource.cpp:596-606`). [`dbf_link_class`] answers the same
377/// question from the port's dbStatic field table — the generated
378/// `FieldDesc::declared_dbf`. Consumers gate "is this field a link" on
379/// `dbf_link_class(..).is_some()` (or [`is_link_dbf_type`] when they
380/// already hold a dbStatic code), rather than maintaining their own
381/// partial spelling lists.
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383#[repr(u8)]
384pub enum DbfLinkClass {
385 /// `DBF_INLINK` (14) — an input link (`INP`, `DOL`, `SIML`, …).
386 InLink = DBF_INLINK,
387 /// `DBF_OUTLINK` (15) — an output link (`OUT`, `LNKn`, …).
388 OutLink = DBF_OUTLINK,
389 /// `DBF_FWDLINK` (16) — a forward link (`FLNK`).
390 FwdLink = DBF_FWDLINK,
391}
392
393/// `dbFldTypes.h` `DBF_INLINK`.
394pub const DBF_INLINK: u8 = 14;
395/// `dbFldTypes.h` `DBF_OUTLINK`.
396pub const DBF_OUTLINK: u8 = 15;
397/// `dbFldTypes.h` `DBF_FWDLINK`.
398pub const DBF_FWDLINK: u8 = 16;
399
400impl DbfLinkClass {
401 /// The dbStatic `dbfType` numeric code (`dbFldTypes.h`).
402 pub fn dbf_type(self) -> u8 {
403 self as u8
404 }
405}
406
407/// True iff `dbf_type` is a link class — the exact
408/// `DBF_INLINK <= t <= DBF_FWDLINK` range check pvxs applies in
409/// `ioc/groupsource.cpp:596-606`. Use when a caller already holds a
410/// dbStatic field-type code; [`dbf_link_class`] is the entry point that
411/// resolves the code from the record type and field name first.
412pub fn is_link_dbf_type(dbf_type: u8) -> bool {
413 (DBF_INLINK..=DBF_FWDLINK).contains(&dbf_type)
414}
415
416/// Classify a record field by its dbStatic link class, or `None` when it
417/// is not a link field. The single canonical owner of the "is this field a
418/// link" rule for the Rust port.
419///
420/// The answer is the `.dbd` declaration, read through
421/// [`declared_field`](crate::server::record::declared_field) — the by-name
422/// half of the same lookup a caller holding a record does with
423/// `field_desc_of`. Nothing here reasons about the field's spelling.
424///
425/// It used to. `FieldDesc` carried only the type a field is SERVED as, which
426/// is `DBF_STRING` for all three link classes, so the class had to be
427/// reconstructed from the name: an exact list, plus `INP`/`DOL`/`OUT`/`LNK`
428/// followed by exactly one alphanumeric, plus per-record-type exceptions for
429/// `SIOL` and `LNK*`. Measured against the declarations `declared_dbf` now
430/// carries, that rule was wrong at 86 fields — 24 `INAA..INLL` inputs on
431/// `acalcout`/`scalcout` and 52 more on `motor`, `table`, `scaler`, `epid` and
432/// `throttle` that it called non-links, and 10 non-links (`*.OUTV`,
433/// `calcout.INPV`, `compress.INPN`, five `swait` fields) that it called links.
434/// A one-character-suffix rule cannot express a two-character suffix, and no
435/// exception list catches a family it has never been told about.
436///
437/// `record_type` no longer resolves ambiguity — it selects the declaration
438/// table. A type `dbd_generated` does not cover (`motor`, `table`, …) must
439/// have registered its factory for its own fields to resolve; until then only
440/// `dbCommon` answers. That is the same condition under which the record could
441/// be instantiated at all.
442pub fn dbf_link_class(record_type: &str, field: &str) -> Option<DbfLinkClass> {
443 let desc = crate::server::record::declared_field(record_type, field.trim())?;
444 match desc.declared_dbf {
445 DbfCode::Inlink => Some(DbfLinkClass::InLink),
446 DbfCode::Outlink => Some(DbfLinkClass::OutLink),
447 DbfCode::Fwdlink => Some(DbfLinkClass::FwdLink),
448 _ => None,
449 }
450}
451
452/// Calculate buffer size for a DBR type including metadata, matching C
453/// `dbr_size_n(TYPE, COUNT) = dbr_size[TYPE] + (COUNT-1)*dbr_value_size[TYPE]`.
454///
455/// the metadata length is taken from
456/// `crate::types::codec::dbr_meta_size` — the single owner that the
457/// serializers (`serialize_dbr` / `encode_dbr`) emit against — so the
458/// explicit-count pad/truncate and no-read-access frame paths size
459/// TIME / GR / CTRL bodies exactly as the encoder writes them. A
460/// `metadata_matches_encoded_length` test pins `encoded_len ==
461/// dbr_buffer_size` across the whole (dbr_type, native) matrix, so the
462/// sizer can no longer drift from the encoder.
463pub fn dbr_buffer_size(dbr_type: u16, native_type: DbFieldType, count: usize) -> usize {
464 // DBR_CLASS_NAME (38) is always one MAX_STRING_SIZE (40) string,
465 // regardless of `count` or `native_type` — it carries no value[]
466 // array, so the generic meta+value formula does not apply.
467 if dbr_type == DBR_CLASS_NAME {
468 return 40;
469 }
470 let value_size = native_type.element_size() * count;
471 crate::types::codec::dbr_meta_size(dbr_type, native_type) + value_size
472}
473
474/// Extract the native DBF type index (0-6) from any DBR type code.
475fn dbr_native_index(dbr_type: u16) -> Option<u16> {
476 match dbr_type {
477 0..=6 => Some(dbr_type),
478 7..=13 => Some(dbr_type - 7),
479 14..=20 => Some(dbr_type - 14),
480 21..=27 => Some(dbr_type - 21),
481 28..=34 => Some(dbr_type - 28),
482 // Alarm-acknowledge writes carry a single u16, so map them to
483 // Short for codec purposes. STSACK_STRING returns a string body
484 // so it maps to String.
485 35 | 36 => Some(1), // DBR_PUT_ACKT / DBR_PUT_ACKS — u16
486 37 => Some(0), // DBR_STSACK_STRING — value is a string
487 // DBR_CLASS_NAME is a single fixed 40-byte string carrying the
488 // record's recordType. Treat as String for codec purposes.
489 38 => Some(0),
490 _ => None,
491 }
492}
493
494/// The DATABASE field type a CA DBR code is named after.
495///
496/// **Not** the carrier of a payload that arrived over the wire: compose
497/// with [`DbFieldType::wire_carrier`] for that. The two answers differ for
498/// the CHAR row only, and that one row is the whole of CA's signedness
499/// mismatch.
500pub fn native_type_for_dbr(dbr_type: u16) -> CaResult<DbFieldType> {
501 match dbr_native_index(dbr_type) {
502 Some(idx) => DbFieldType::from_u16(idx),
503 None => Err(CaError::UnsupportedType(dbr_type)),
504 }
505}
506
507/// DBR request-type names indexed by type code, mirroring the C
508/// `dbr_text[]` table (`ca/src/client/access.cpp`). Index 0 =
509/// `DBR_STRING` … index 38 = `DBR_CLASS_NAME`.
510const DBR_TEXT: [&str; (LAST_BUFFER_TYPE + 1) as usize] = [
511 "DBR_STRING",
512 "DBR_SHORT",
513 "DBR_FLOAT",
514 "DBR_ENUM",
515 "DBR_CHAR",
516 "DBR_LONG",
517 "DBR_DOUBLE",
518 "DBR_STS_STRING",
519 "DBR_STS_SHORT",
520 "DBR_STS_FLOAT",
521 "DBR_STS_ENUM",
522 "DBR_STS_CHAR",
523 "DBR_STS_LONG",
524 "DBR_STS_DOUBLE",
525 "DBR_TIME_STRING",
526 "DBR_TIME_SHORT",
527 "DBR_TIME_FLOAT",
528 "DBR_TIME_ENUM",
529 "DBR_TIME_CHAR",
530 "DBR_TIME_LONG",
531 "DBR_TIME_DOUBLE",
532 "DBR_GR_STRING",
533 "DBR_GR_SHORT",
534 "DBR_GR_FLOAT",
535 "DBR_GR_ENUM",
536 "DBR_GR_CHAR",
537 "DBR_GR_LONG",
538 "DBR_GR_DOUBLE",
539 "DBR_CTRL_STRING",
540 "DBR_CTRL_SHORT",
541 "DBR_CTRL_FLOAT",
542 "DBR_CTRL_ENUM",
543 "DBR_CTRL_CHAR",
544 "DBR_CTRL_LONG",
545 "DBR_CTRL_DOUBLE",
546 "DBR_PUT_ACKT",
547 "DBR_PUT_ACKS",
548 "DBR_STSACK_STRING",
549 "DBR_CLASS_NAME",
550];
551
552/// Resolve a DBR request-type name to its type code, mirroring the C
553/// `dbr_text_to_type` macro (`db_access.h`): an exact, **case-sensitive**
554/// `strcmp` search of the `dbr_text[]` table. Returns the matching code
555/// (`0..=38`) or `None` when no name matches.
556///
557/// The case sensitivity is faithful to C — the `caget`/`caput` tools
558/// feed `-d <type>` straight through this search, so `-d DBR_TIME_FLOAT`
559/// resolves while `-d dbr_time_float` does not (the C tool then reverts
560/// to its plain/native request). Callers that accept the bare family
561/// (`caget -d TIME_FLOAT`) retry with a `DBR_` prefix, exactly as
562/// `caget.c` does.
563pub fn dbr_text_to_type(text: &str) -> Option<u16> {
564 DBR_TEXT.iter().position(|&n| n == text).map(|i| i as u16)
565}
566
567/// Resolve a DBR type code to its name, mirroring the C
568/// `dbr_type_to_text` macro (`db_access.h`): an index into the same
569/// `dbr_text[]` table, with C's `"DBR_invalid"` for anything outside
570/// `0..=38`. Inverse of [`dbr_text_to_type`], and the single owner of
571/// that direction — the CA client's exception block
572/// (`CA.Client.Exception ... type=%s`) and `caget -d`'s "Request type:"
573/// line both read the names from here.
574pub fn dbr_type_to_text(code: u16) -> &'static str {
575 DBR_TEXT
576 .get(code as usize)
577 .copied()
578 .unwrap_or("DBR_invalid")
579}
580
581#[cfg(test)]
582mod buffer_size_tests {
583 use super::*;
584
585 /// STS meta size is per-type. `dbr_sts_double` carries a
586 /// 4-byte `dbr_long_t` RISC_pad (db_access.h:233-238) → meta 8.
587 #[test]
588 fn sts_double_meta_is_8() {
589 // scalar: 8 (meta) + 8 (value) = 16
590 assert_eq!(dbr_buffer_size(DBR_STS_DOUBLE, DbFieldType::Double, 1), 16);
591 // n elements: 8 + 8*n
592 assert_eq!(
593 dbr_buffer_size(DBR_STS_DOUBLE, DbFieldType::Double, 5),
594 8 + 8 * 5
595 );
596 }
597
598 /// `dbr_sts_char` carries a 1-byte RISC_pad
599 /// (db_access.h:218-223) → meta 5.
600 #[test]
601 fn sts_char_meta_is_5() {
602 assert_eq!(dbr_buffer_size(DBR_STS_CHAR, DbFieldType::Char, 1), 6);
603 assert_eq!(dbr_buffer_size(DBR_STS_CHAR, DbFieldType::Char, 10), 5 + 10);
604 }
605
606 /// types with no STS RISC pad keep the flat 4-byte meta.
607 #[test]
608 fn sts_short_meta_is_4() {
609 assert_eq!(dbr_buffer_size(DBR_STS_SHORT, DbFieldType::Short, 1), 6);
610 assert_eq!(dbr_buffer_size(DBR_STS_LONG, DbFieldType::Long, 1), 8);
611 assert_eq!(dbr_buffer_size(DBR_STS_FLOAT, DbFieldType::Float, 1), 8);
612 }
613
614 /// Plain values carry no metadata.
615 #[test]
616 fn plain_value_size_only() {
617 assert_eq!(dbr_buffer_size(DBR_DOUBLE, DbFieldType::Double, 3), 24);
618 }
619
620 /// TIME structs carry a per-type RISC pad before `value[0]`
621 /// (C `dbr_time_*`, db_access.h:250-300). The pre-fix flat 12-byte
622 /// TIME meta truncated double/short/enum/char bodies.
623 #[test]
624 fn time_meta_includes_risc_pad() {
625 // double: 12 + RISC_pad(4) + value(8) = 24 (was wrongly 20).
626 assert_eq!(dbr_buffer_size(DBR_TIME_DOUBLE, DbFieldType::Double, 1), 24);
627 // short/enum: 12 + pad(2) + value(2) = 16.
628 assert_eq!(dbr_buffer_size(DBR_TIME_SHORT, DbFieldType::Short, 1), 16);
629 assert_eq!(dbr_buffer_size(DBR_TIME_ENUM, DbFieldType::Enum, 1), 16);
630 // char: 12 + pad(3) + value(1) = 16.
631 assert_eq!(dbr_buffer_size(DBR_TIME_CHAR, DbFieldType::Char, 1), 16);
632 // float/long: no pad (value already 4-aligned at offset 12).
633 assert_eq!(dbr_buffer_size(DBR_TIME_FLOAT, DbFieldType::Float, 1), 16);
634 assert_eq!(dbr_buffer_size(DBR_TIME_LONG, DbFieldType::Long, 1), 16);
635 // Explicit count scales the value array after the pad.
636 assert_eq!(
637 dbr_buffer_size(DBR_TIME_DOUBLE, DbFieldType::Double, 4),
638 16 + 8 * 4
639 );
640 }
641
642 /// GR/CTRL metadata is per native type (the pre-fix single
643 /// broad formula over-padded short/char/float/long and dropped the
644 /// enum `no_str` word).
645 #[test]
646 fn gr_ctrl_meta_is_per_type() {
647 // GR (6 limits): head(4) + layout.
648 assert_eq!(dbr_buffer_size(DBR_GR_SHORT, DbFieldType::Short, 1), 24 + 2);
649 assert_eq!(dbr_buffer_size(DBR_GR_FLOAT, DbFieldType::Float, 1), 40 + 4);
650 assert_eq!(
651 dbr_buffer_size(DBR_GR_DOUBLE, DbFieldType::Double, 1),
652 64 + 8
653 );
654 assert_eq!(dbr_buffer_size(DBR_GR_CHAR, DbFieldType::Char, 1), 19 + 1);
655 assert_eq!(dbr_buffer_size(DBR_GR_LONG, DbFieldType::Long, 1), 36 + 4);
656 // Enum: head(4) + no_str(2) + 16*26 strings = 422, value(2).
657 assert_eq!(dbr_buffer_size(DBR_GR_ENUM, DbFieldType::Enum, 1), 422 + 2);
658 // CTRL adds two control limits.
659 assert_eq!(
660 dbr_buffer_size(DBR_CTRL_DOUBLE, DbFieldType::Double, 1),
661 80 + 8
662 );
663 assert_eq!(
664 dbr_buffer_size(DBR_CTRL_SHORT, DbFieldType::Short, 1),
665 28 + 2
666 );
667 assert_eq!(dbr_buffer_size(DBR_CTRL_CHAR, DbFieldType::Char, 1), 21 + 1);
668 }
669}
670
671#[cfg(test)]
672mod dbf_link_class_tests {
673 use super::*;
674
675 #[test]
676 fn dbcommon_links_classified_uniformly() {
677 // Present on every record (dbCommon.dbd).
678 assert_eq!(dbf_link_class("ai", "FLNK"), Some(DbfLinkClass::FwdLink));
679 assert_eq!(dbf_link_class("ao", "SDIS"), Some(DbfLinkClass::InLink));
680 assert_eq!(dbf_link_class("calc", "TSEL"), Some(DbfLinkClass::InLink));
681 }
682
683 #[test]
684 fn record_specific_link_families_the_old_name_list_missed() {
685 // The families the reviewed partial spelling list omitted, each a
686 // DBF_INLINK/OUTLINK in EPICS Base `*.dbd.pod`:
687 // seqRecord DOL0 (INLINK) / LNK0 (OUTLINK) / DOLA / DOLF / LNKF
688 assert_eq!(dbf_link_class("seq", "DOL0"), Some(DbfLinkClass::InLink));
689 assert_eq!(dbf_link_class("seq", "LNK0"), Some(DbfLinkClass::OutLink));
690 assert_eq!(dbf_link_class("seq", "DOLA"), Some(DbfLinkClass::InLink));
691 assert_eq!(dbf_link_class("seq", "DOLF"), Some(DbfLinkClass::InLink));
692 assert_eq!(dbf_link_class("seq", "LNKF"), Some(DbfLinkClass::OutLink));
693 // selRecord NVL (INLINK); histogramRecord SVL (INLINK)
694 assert_eq!(dbf_link_class("sel", "NVL"), Some(DbfLinkClass::InLink));
695 assert_eq!(
696 dbf_link_class("histogram", "SVL"),
697 Some(DbfLinkClass::InLink)
698 );
699 // calc/aSub INPA..INPU (INLINK); dfanout/aSub OUTA (OUTLINK)
700 assert_eq!(dbf_link_class("calc", "INPA"), Some(DbfLinkClass::InLink));
701 assert_eq!(dbf_link_class("aSub", "INPU"), Some(DbfLinkClass::InLink));
702 assert_eq!(
703 dbf_link_class("dfanout", "OUTA"),
704 Some(DbfLinkClass::OutLink)
705 );
706 // `fanout` declares LNK0..LNKF and no OUT family at all; the name
707 // rule this replaced answered `OutLink` for a field that does not
708 // exist on the type.
709 assert_eq!(dbf_link_class("fanout", "OUTA"), None);
710 // printf INP0..INP9 (INLINK)
711 assert_eq!(dbf_link_class("printf", "INP0"), Some(DbfLinkClass::InLink));
712 }
713
714 /// **The invariant this function exists to hold**: for every record type
715 /// and every field, `dbf_link_class` agrees with the `.dbd` declaration.
716 ///
717 /// One case per declaration, not one per link family — a per-family test
718 /// is what let 86 fields drift, because a family nobody wrote a case for
719 /// is invisible to it. This walks the table instead, so a new record type
720 /// or a new link family is covered the day it is generated.
721 #[test]
722 fn every_declared_field_classifies_as_its_dbd_says() {
723 use crate::server::record::dbd_generated::{DB_COMMON_FIELDS, RECORD_TYPES, record_fields};
724 let mut wrong = Vec::new();
725 for rt in RECORD_TYPES {
726 let own = record_fields(rt).unwrap_or(&[]);
727 for d in DB_COMMON_FIELDS.iter().chain(own.iter()) {
728 let declared = match d.declared_dbf {
729 DbfCode::Inlink => Some(DbfLinkClass::InLink),
730 DbfCode::Outlink => Some(DbfLinkClass::OutLink),
731 DbfCode::Fwdlink => Some(DbfLinkClass::FwdLink),
732 _ => None,
733 };
734 let got = dbf_link_class(rt, d.name);
735 if got != declared {
736 wrong.push(format!("{rt}.{} declared={declared:?} got={got:?}", d.name));
737 }
738 }
739 }
740 assert!(
741 wrong.is_empty(),
742 "{} fields disagree with their declaration:\n{}",
743 wrong.len(),
744 wrong.join("\n")
745 );
746 }
747
748 /// The two shapes the name rule could not express, pinned by name so a
749 /// regression names itself rather than appearing as a count.
750 #[test]
751 fn the_two_shapes_the_name_rule_could_not_express() {
752 // A two-character suffix. `INAA..INLL` are 12 declared `DBF_INLINK`
753 // inputs on each of `acalcout` and `scalcout`; the one-alphanumeric
754 // suffix rule called all 24 non-links.
755 for rt in ["acalcout", "scalcout"] {
756 for f in ["INAA", "INBB", "INLL"] {
757 assert_eq!(
758 dbf_link_class(rt, f),
759 Some(DbfLinkClass::InLink),
760 "{rt}.{f}"
761 );
762 }
763 }
764 // A non-link that merely spells like one. `OUTV` is the output-value
765 // field, `INPV` the input-value one; neither is a link.
766 for (rt, f) in [
767 ("acalcout", "OUTV"),
768 ("calcout", "OUTV"),
769 ("scalcout", "OUTV"),
770 ("calcout", "INPV"),
771 ("compress", "INPN"),
772 ("swait", "DOLN"),
773 ("swait", "OUTN"),
774 ("swait", "DOLV"),
775 ("swait", "OUTV"),
776 ("swait", "DOLD"),
777 ] {
778 assert_eq!(dbf_link_class(rt, f), None, "{rt}.{f} is not a link field");
779 }
780 }
781
782 /// A record type outside `dbd_generated` resolves through the registry a
783 /// factory registration fills — the by-name mirror of `field_list`.
784 #[test]
785 fn a_registered_downstream_type_resolves_its_own_declarations() {
786 use crate::server::record::{FieldDesc, register_declared_fields};
787 static FIELDS: &[FieldDesc] = &[FieldDesc {
788 name: "RDBL",
789 dbf_type: DbFieldType::String,
790 declared_dbf: DbfCode::Inlink,
791 runtime_typed: false,
792 read_only: false,
793 special: crate::server::record::Special::None,
794 declared_special: crate::server::record::Special::None,
795 pp: false,
796 asl: crate::server::record::Asl::Asl1,
797 size: 0,
798 extra: None,
799 menu: None,
800 initial: None,
801 interest: 0,
802 prop: false,
803 prompt: None,
804 promptgroup: None,
805 base: crate::server::record::Base::Decimal,
806 }];
807
808 // Before registration only `dbCommon` answers for an unknown type.
809 assert_eq!(dbf_link_class("zzTestRec", "RDBL"), None);
810 assert_eq!(
811 dbf_link_class("zzTestRec", "FLNK"),
812 Some(DbfLinkClass::FwdLink)
813 );
814
815 register_declared_fields("zzTestRec", FIELDS);
816 assert_eq!(
817 dbf_link_class("zzTestRec", "RDBL"),
818 Some(DbfLinkClass::InLink),
819 "the name rule this replaced answered None for motor's RDBL"
820 );
821 }
822
823 #[test]
824 fn siol_class_depends_on_record_direction() {
825 // boRecord.dbd.pod:318 SIOL=DBF_OUTLINK; ai/bi SIOL=DBF_INLINK.
826 assert_eq!(dbf_link_class("bo", "SIOL"), Some(DbfLinkClass::OutLink));
827 assert_eq!(dbf_link_class("ao", "SIOL"), Some(DbfLinkClass::OutLink));
828 assert_eq!(dbf_link_class("ai", "SIOL"), Some(DbfLinkClass::InLink));
829 assert_eq!(dbf_link_class("bi", "SIOL"), Some(DbfLinkClass::InLink));
830 // SIML is always DBF_INLINK regardless of direction.
831 assert_eq!(dbf_link_class("bo", "SIML"), Some(DbfLinkClass::InLink));
832 assert_eq!(dbf_link_class("ai", "SIML"), Some(DbfLinkClass::InLink));
833 }
834
835 /// Every `field(SIOL,DBF_*)` in the record types this workspace ports,
836 /// read out of the C dbds. synApps `busy` is the one non-Base output
837 /// record — it derives from `bo` and declares `field(SIOL,DBF_OUTLINK)`
838 /// (`busyRecord.dbd`) — while synApps `swait` (`swaitRecord.dbd`) and
839 /// `mca` (`mcaRecord.dbd`) declare `DBF_INLINK`. A missing output entry is
840 /// silent: the classifier defaults to `InLink`, and the CP/CPP mask C
841 /// applies to an output link (`dbStaticLib.c:2380-2391`) is then skipped.
842 #[test]
843 fn siol_direction_matches_every_c_dbd_this_workspace_ports() {
844 for rtype in [
845 "ao",
846 "bo",
847 "busy",
848 "longout",
849 "int64out",
850 "mbbo",
851 "mbboDirect",
852 "stringout",
853 "lso",
854 "aao",
855 ] {
856 assert_eq!(
857 dbf_link_class(rtype, "SIOL"),
858 Some(DbfLinkClass::OutLink),
859 "{rtype} declares field(SIOL,DBF_OUTLINK)"
860 );
861 }
862 for rtype in [
863 "ai",
864 "bi",
865 "mbbi",
866 "mbbiDirect",
867 "longin",
868 "int64in",
869 "stringin",
870 "lsi",
871 "event",
872 "waveform",
873 "aai",
874 "histogram",
875 "swait",
876 // `mca` declares `field(SIOL,DBF_INLINK)` too, but it lives in
877 // `mca-rs` and reaches the by-name lookup only once that crate
878 // registers its factory — which no `epics-base-rs` unit test
879 // does. Asserting it here only ever tested the name rule.
880 ] {
881 assert_eq!(
882 dbf_link_class(rtype, "SIOL"),
883 Some(DbfLinkClass::InLink),
884 "{rtype} declares field(SIOL,DBF_INLINK)"
885 );
886 }
887 }
888
889 /// The consequence a misclassified SIOL actually has: `check_link_assignment`
890 /// turns the class into a [`LinkFieldType`], and only the `Out` arm applies
891 /// C's `modifiers &= ~(pvlOptCPP|pvlOptCP)`. Classified as an input, a
892 /// `busy` SIOL would keep a CPP that C strips.
893 #[test]
894 fn a_busy_siol_discards_cp_the_way_an_output_link_must() {
895 use crate::server::record::{
896 LinkFieldType, LinkProcessPolicy, ParsedLink, parse_link_field,
897 };
898
899 let ftype = match dbf_link_class("busy", "SIOL").unwrap() {
900 DbfLinkClass::InLink => LinkFieldType::In,
901 DbfLinkClass::OutLink => LinkFieldType::Out,
902 DbfLinkClass::FwdLink => LinkFieldType::Fwd,
903 };
904 match parse_link_field("TGT.VAL CPP", ftype) {
905 ParsedLink::Db(db) => assert_eq!(db.policy, LinkProcessPolicy::NoProcess),
906 other => panic!("expected a db link, got {other:?}"),
907 }
908 }
909
910 /// `LNK0..LNKF` shares
911 /// its spelling across two DBF classes — `fanoutRecord` declares the
912 /// family `DBF_FWDLINK`, while `seqRecord`/`sseqRecord` declare it
913 /// `DBF_OUTLINK`. The classifier must resolve by `record_type`, not
914 /// collapse every `LNK*` to OutLink by prefix.
915 #[test]
916 fn lnk_class_depends_on_record_type() {
917 // fanoutRecord.dbd.pod field(LNK0,DBF_FWDLINK) … field(LNKF,…).
918 assert_eq!(
919 dbf_link_class("fanout", "LNK0"),
920 Some(DbfLinkClass::FwdLink),
921 "fanout LNK0 is DBF_FWDLINK (16), not OUTLINK"
922 );
923 assert_eq!(
924 dbf_link_class("fanout", "LNKF"),
925 Some(DbfLinkClass::FwdLink),
926 "fanout LNKF is DBF_FWDLINK (16), not OUTLINK"
927 );
928 // seqRecord.dbd.pod field(LNK0,DBF_OUTLINK): the default direction
929 // for the shared spelling.
930 assert_eq!(dbf_link_class("seq", "LNK0"), Some(DbfLinkClass::OutLink));
931 assert_eq!(dbf_link_class("seq", "LNKF"), Some(DbfLinkClass::OutLink));
932 // synApps sseqRecord LNK1..LNK9 are also DBF_OUTLINK (LNK10 is the
933 // two-char spelling the one-alnum suffix rule deliberately rejects).
934 assert_eq!(dbf_link_class("sseq", "LNK1"), Some(DbfLinkClass::OutLink));
935 // fanout's dbCommon FLNK is still a forward link (unchanged).
936 assert_eq!(
937 dbf_link_class("fanout", "FLNK"),
938 Some(DbfLinkClass::FwdLink)
939 );
940 }
941
942 #[test]
943 fn plain_value_fields_are_not_links() {
944 for f in [
945 "VAL", "EGU", "PREC", "HOPR", "RVAL", "DESC", "A", "B", "OVAL",
946 ] {
947 assert_eq!(dbf_link_class("ai", f), None, "{f} must not be a link");
948 }
949 }
950
951 #[test]
952 fn case_insensitive_and_trims() {
953 assert_eq!(dbf_link_class("ai", "inp"), Some(DbfLinkClass::InLink));
954 assert_eq!(dbf_link_class("ao", " OUT "), Some(DbfLinkClass::OutLink));
955 }
956
957 #[test]
958 fn dbf_codes_and_range_match_pvxs() {
959 assert_eq!(DbfLinkClass::InLink.dbf_type(), 14);
960 assert_eq!(DbfLinkClass::OutLink.dbf_type(), 15);
961 assert_eq!(DbfLinkClass::FwdLink.dbf_type(), 16);
962 // pvxs groupsource.cpp:596-606 range check.
963 assert!(is_link_dbf_type(DBF_INLINK));
964 assert!(is_link_dbf_type(DBF_OUTLINK));
965 assert!(is_link_dbf_type(DBF_FWDLINK));
966 assert!(!is_link_dbf_type(13)); // DBF_DEVICE
967 assert!(!is_link_dbf_type(17)); // DBF_NOACCESS
968 assert!(!is_link_dbf_type(0)); // DBF_STRING
969 }
970}
971
972#[cfg(test)]
973mod dbr_text_tests {
974 use super::*;
975
976 #[test]
977 fn resolves_every_type_name_to_its_code() {
978 // Round-trip the whole table: each name resolves to its index.
979 for (code, name) in DBR_TEXT.iter().enumerate() {
980 assert_eq!(
981 dbr_text_to_type(name),
982 Some(code as u16),
983 "{name} should resolve to {code}"
984 );
985 }
986 // Boundary names spot-check (the cited High-finding values).
987 assert_eq!(dbr_text_to_type("DBR_STRING"), Some(DBR_STRING));
988 assert_eq!(dbr_text_to_type("DBR_TIME_FLOAT"), Some(DBR_TIME_FLOAT));
989 assert_eq!(
990 dbr_text_to_type("DBR_STSACK_STRING"),
991 Some(DBR_STSACK_STRING)
992 );
993 assert_eq!(dbr_text_to_type("DBR_CLASS_NAME"), Some(DBR_CLASS_NAME));
994 }
995
996 #[test]
997 fn is_case_sensitive_like_c_strcmp() {
998 // C `dbr_text_to_type` uses `strcmp`, so lowercase does not
999 // match — the C tools then revert to their plain request.
1000 assert_eq!(dbr_text_to_type("dbr_time_float"), None);
1001 assert_eq!(dbr_text_to_type("DBR_Time_Float"), None);
1002 }
1003
1004 #[test]
1005 fn unknown_and_bare_family_names_do_not_match() {
1006 // Bare family names need the caller's `DBR_` retry — the raw
1007 // search rejects them, matching C's first-pass `strcmp`.
1008 assert_eq!(dbr_text_to_type("TIME_FLOAT"), None);
1009 assert_eq!(dbr_text_to_type("DOUBLE"), None);
1010 assert_eq!(dbr_text_to_type("DBR_NONSENSE"), None);
1011 assert_eq!(dbr_text_to_type(""), None);
1012 }
1013}
1014
1015#[cfg(test)]
1016mod dbf_code_tests {
1017 use super::*;
1018
1019 /// C's `dbfType` in declaration order (`dbFldTypes.h:24-43` @R7.0.10),
1020 /// paired with `pamapdbfType`'s strings (`:51-70`) as `dbTest.c:76-81`
1021 /// carries them, prefix-stripped.
1022 ///
1023 /// The WHOLE table, not a sample. A nineteenth type inserted in the
1024 /// middle has to fail on its own row here rather than silently shift
1025 /// every code after it — the codes are what `dbDumpField` prints and
1026 /// what a `.dbd` interchange file means by `DBF_MENU`.
1027 const C_DBF_TYPE: [(DbfCode, i16, &str); 18] = [
1028 (DbfCode::String, 0, "STRING"),
1029 (DbfCode::Char, 1, "CHAR"),
1030 (DbfCode::UChar, 2, "UCHAR"),
1031 (DbfCode::Short, 3, "SHORT"),
1032 (DbfCode::UShort, 4, "USHORT"),
1033 (DbfCode::Long, 5, "LONG"),
1034 (DbfCode::ULong, 6, "ULONG"),
1035 (DbfCode::Int64, 7, "INT64"),
1036 (DbfCode::UInt64, 8, "UINT64"),
1037 (DbfCode::Float, 9, "FLOAT"),
1038 (DbfCode::Double, 10, "DOUBLE"),
1039 (DbfCode::Enum, 11, "ENUM"),
1040 (DbfCode::Menu, 12, "MENU"),
1041 (DbfCode::Device, 13, "DEVICE"),
1042 (DbfCode::Inlink, 14, "INLINK"),
1043 (DbfCode::Outlink, 15, "OUTLINK"),
1044 (DbfCode::Fwdlink, 16, "FWDLINK"),
1045 (DbfCode::NoAccess, 17, "NOACCESS"),
1046 ];
1047
1048 /// The table above lists every variant: this match is exhaustive, so a
1049 /// nineteenth variant stops the build, and the pairwise-distinct check
1050 /// below then makes 18 rows a proof of coverage rather than of length.
1051 fn _the_enum_has_no_variant_outside_the_table(c: DbfCode) {
1052 match c {
1053 DbfCode::String
1054 | DbfCode::Char
1055 | DbfCode::UChar
1056 | DbfCode::Short
1057 | DbfCode::UShort
1058 | DbfCode::Long
1059 | DbfCode::ULong
1060 | DbfCode::Int64
1061 | DbfCode::UInt64
1062 | DbfCode::Float
1063 | DbfCode::Double
1064 | DbfCode::Enum
1065 | DbfCode::Menu
1066 | DbfCode::Device
1067 | DbfCode::Inlink
1068 | DbfCode::Outlink
1069 | DbfCode::Fwdlink
1070 | DbfCode::NoAccess => (),
1071 }
1072 }
1073
1074 #[test]
1075 fn every_dbf_code_carries_cs_number_and_name() {
1076 for (i, (code, n, name)) in C_DBF_TYPE.iter().enumerate() {
1077 assert_eq!(*code as i16, *n, "DBF_{name}: wrong dbfType code");
1078 assert_eq!(code.name(), *name, "DBF_{name}: wrong name");
1079 // C's dbfType is dense 0..DBF_NTYPES-1 and `pamapdbfType` is
1080 // indexed by it, so position and code are the same fact.
1081 assert_eq!(i as i16, *n, "DBF_{name}: table position != code");
1082 }
1083 // DBF_NTYPES (`dbFldTypes.h:45`).
1084 assert_eq!(C_DBF_TYPE.len(), 18);
1085 for (a, (x, ..)) in C_DBF_TYPE.iter().enumerate() {
1086 for (y, ..) in C_DBF_TYPE.iter().skip(a + 1) {
1087 assert_ne!(x, y, "{x:?} listed twice");
1088 }
1089 }
1090 }
1091
1092 #[test]
1093 fn every_served_type_maps_to_its_dbstatic_code() {
1094 // All twelve CA wire types, not a sample. The mapping only ever
1095 // answers a scalar code, because that is all a SERVED type can be —
1096 // `DbfCode::Menu`, `Device` and the three link codes are declarations
1097 // and are unreachable from here by construction.
1098 let table = [
1099 (DbFieldType::String, DbfCode::String),
1100 (DbFieldType::Short, DbfCode::Short),
1101 (DbFieldType::Float, DbfCode::Float),
1102 (DbFieldType::Enum, DbfCode::Enum),
1103 (DbFieldType::Char, DbfCode::Char),
1104 (DbFieldType::Long, DbfCode::Long),
1105 (DbFieldType::Double, DbfCode::Double),
1106 (DbFieldType::Int64, DbfCode::Int64),
1107 (DbFieldType::UInt64, DbfCode::UInt64),
1108 (DbFieldType::UShort, DbfCode::UShort),
1109 (DbFieldType::ULong, DbfCode::ULong),
1110 (DbFieldType::UChar, DbfCode::UChar),
1111 ];
1112 for (ft, code) in table {
1113 assert_eq!(ft.dbf_code(), code, "{ft:?}");
1114 }
1115 assert_eq!(table.len(), 12);
1116 // The CA wire codes are the OTHER numbering and must not have moved:
1117 // they are `DbFieldType`'s discriminants and are load-bearing on the
1118 // protocol, so the two tables disagreeing from the second entry on is
1119 // the expected state, not a defect.
1120 for (i, (ft, _)) in table.iter().enumerate() {
1121 assert_eq!(*ft as u16, i as u16, "{ft:?}: CA wire code moved");
1122 }
1123 }
1124
1125 /// C's `mapDBFToDBR` (`dbAccess.c:76-95`), all eighteen rows in C's
1126 /// order, written as the DBR *names* C's initialiser comments give so a
1127 /// silent renumbering of either enum shows up as a name mismatch.
1128 #[test]
1129 fn the_dbr_map_is_cs_whole_table() {
1130 const C_MAP: [(DbfCode, DbfCode); 18] = [
1131 (DbfCode::String, DbfCode::String),
1132 (DbfCode::Char, DbfCode::Char),
1133 (DbfCode::UChar, DbfCode::UChar),
1134 (DbfCode::Short, DbfCode::Short),
1135 (DbfCode::UShort, DbfCode::UShort),
1136 (DbfCode::Long, DbfCode::Long),
1137 (DbfCode::ULong, DbfCode::ULong),
1138 (DbfCode::Int64, DbfCode::Int64),
1139 (DbfCode::UInt64, DbfCode::UInt64),
1140 (DbfCode::Float, DbfCode::Float),
1141 (DbfCode::Double, DbfCode::Double),
1142 (DbfCode::Enum, DbfCode::Enum),
1143 (DbfCode::Menu, DbfCode::Enum),
1144 (DbfCode::Device, DbfCode::Enum),
1145 (DbfCode::Inlink, DbfCode::String),
1146 (DbfCode::Outlink, DbfCode::String),
1147 (DbfCode::Fwdlink, DbfCode::String),
1148 (DbfCode::NoAccess, DbfCode::NoAccess),
1149 ];
1150 for (dbf, dbr) in C_MAP {
1151 assert_eq!(dbf.dbr_code(), dbr, "mapDBFToDBR[DBF_{}]", dbf.name());
1152 }
1153 assert_eq!(C_MAP.len(), 18);
1154 // C indexes `mapDBFToDBR` by the dbfType, so the table's position
1155 // and the code it maps are one fact — the same density check the
1156 // name table gets.
1157 for (i, (dbf, _)) in C_MAP.iter().enumerate() {
1158 assert_eq!(
1159 *dbf as i16,
1160 i as i16,
1161 "DBF_{}: row out of order",
1162 dbf.name()
1163 );
1164 }
1165 // `DBR_NOACCESS` is `DBF_NOACCESS` = 17 (`dbFldTypes.h:90`), NOT the
1166 // 12 that `printDbAddr`'s `dbr[]` index suggests: C remaps the index
1167 // to `DBR_ENUM+1` only to reach its shorter name array, and still
1168 // prints the unremapped 17 as the number (`dbTest.c:812-817`). Both
1169 // halves fall out of one code here, so the port cannot print the
1170 // pair inconsistently.
1171 assert_eq!(DbfCode::NoAccess.dbr_code() as i16, 17);
1172 assert_eq!(DbfCode::NoAccess.dbr_code().name(), "NOACCESS");
1173 }
1174}