epics_base_rs/error.rs
1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum CaError {
5 #[error("I/O error: {0}")]
6 Io(#[from] std::io::Error),
7
8 #[error("timeout waiting for response")]
9 Timeout,
10
11 #[error("channel not found: {0}")]
12 ChannelNotFound(String),
13
14 #[error("protocol error: {0}")]
15 Protocol(String),
16
17 #[error("unsupported DBR type: {0}")]
18 UnsupportedType(u16),
19
20 #[error("write failed: ECA status {0:#06x}")]
21 WriteFailed(u32),
22
23 #[error("field not found: {0}")]
24 FieldNotFound(String),
25
26 #[error("field is read-only: {0}")]
27 ReadOnlyField(String),
28
29 #[error("type mismatch for field {0}")]
30 TypeMismatch(String),
31
32 #[error("invalid value: {0}")]
33 InvalidValue(String),
34
35 /// C `S_db_badField` ("Illegal RECORD FIELD") — a record's `special()`
36 /// refused the value that `dbPut` had already stored, e.g.
37 /// `calcRecord.c:146-152` returning it for an uncompilable `CALC`. The
38 /// value stays written, the field's monitor is not posted, the record is
39 /// not processed, and the status propagates to the client (rsrv
40 /// `write_action` → `ECA_PUTFAIL`).
41 #[error("illegal record field: {0}")]
42 BadField(String),
43
44 /// C `S_db_badChoice` ("Illegal choice") — a `DBR_STRING` write to a
45 /// `DBF_MENU` field named neither an exact choice label nor an in-range
46 /// index (`dbConvert.c::putStringMenu:1216-1229`). C's converter returns
47 /// this from inside `dbPut`, *before* the value is stored
48 /// (`dbAccess.c:1362`), so the field keeps its old value, no monitor is
49 /// posted and the record is not processed.
50 #[error("illegal menu choice: {0}")]
51 BadChoice(String),
52
53 /// C `S_db_badDbrtype` ("Illegal Database Request Type") — `dbPut`
54 /// refusing a put to a DBF link field (`field_type > DBF_DEVICE`,
55 /// `dbAccess.c:1340-1347`). Only `dbPutField` changes link fields, by
56 /// routing them through `dbPutFieldLink` (`dbAccess.c:1261-1262`); a
57 /// `dbPut` reached from a record's OUT link (`dbPutLink` →
58 /// `dbDbPutValue`) or from internal code refuses, so a DB link cannot
59 /// silently rewire another record's link field. rsrv answers it with
60 /// ECA_PUTFAIL like every non-zero put status — and with ECA_GETFAIL when
61 /// the same refusal comes back from a get, which is why
62 /// [`CaError::to_eca_status`] takes the direction.
63 #[error("illegal database request type: {0}")]
64 BadDbrType(String),
65
66 #[error("put disabled (DISP=1) for field {0}")]
67 PutDisabled(String),
68
69 #[error("link error: {0}")]
70 LinkError(String),
71
72 /// A `.db`/`.dbd` parse abort, carried as C `yyerror` prints it
73 /// (`dbYacc.y:370-383`): the line, the sentence, and `yytext` — the token
74 /// the lexer had matched when the parser rejected it, which is what C
75 /// quotes in its ` at or before '%s'` clause. An empty `token` is a
76 /// failure raised where no token was matched, and prints no such clause.
77 #[error("DB parse error at line {line}: {message}")]
78 DbParseError {
79 line: usize,
80 token: String,
81 message: String,
82 },
83
84 /// C `dbLoadRecords` returning non-zero after `yyerror(NULL)`
85 /// recovered from a bad item (`dbAccess.c:795-813`). The records
86 /// that parsed are still there; the load's *status* is the failure,
87 /// and `softMain` exits 2 on it (`softMain.cpp:198,274-278`).
88 #[error("Failed to load '{0}'")]
89 DbLoadFailed(String),
90
91 #[error("calc error: {0}")]
92 CalcError(String),
93
94 #[error("channel disconnected")]
95 Disconnected,
96
97 #[error("client shut down")]
98 Shutdown,
99
100 /// Server-emitted ECA status carried out-of-band on an otherwise
101 /// data-shaped frame — used by libca `cac::eventAddRespAction`
102 /// (`cac.cpp:973-977`) when a monitor frame's `m_cid` is non-
103 /// NORMAL (e.g. `ECA_NORDACCESS` from `no_read_access_event`
104 /// after an ACF reload). Routed to the per-subscription
105 /// callback as `Err(CaError::ServerError(eca_status))` so the
106 /// subscriber surfaces the status instead of seeing the bogus
107 /// zeroed payload that travels with the frame.
108 #[error("server reported ECA status {0:#06x}")]
109 ServerError(u32),
110
111 /// Request cannot be framed for the peer: it needs the extended
112 /// (24-byte) CA header, and the peer's protocol version predates
113 /// CA_V49, or the element count exceeds what the peer can carry.
114 /// libca raises this locally — `comQueSend::insertRequestHeader`
115 /// throws `cacChannel::outOfBounds()` (`comQueSend.cpp:299,313`)
116 /// and `ca_array_get`/`ca_array_put` return `ECA_TOLARGE` — so no
117 /// byte reaches the wire.
118 #[error("request too large for the peer's CA protocol version (ECA_TOLARGE)")]
119 TooLarge,
120
121 /// Element count out of bounds for the request C would build. libca's
122 /// put path throws `cacChannel::outOfBounds()` for an array that cannot
123 /// fit the peer's message-body limit (`comQueSend.cpp:361`) or that
124 /// needs an extended header the peer cannot parse
125 /// (`comQueSend.cpp:313`); `oldChannelNotify.cpp:309,378,453` map that
126 /// to `ECA_BADCOUNT`. Raised locally — no byte reaches the wire.
127 #[error("element count out of bounds for this CA circuit (ECA_BADCOUNT)")]
128 BadCount,
129
130 /// A get conversion returned a non-zero status: C
131 /// `dbGetConvertRoutine`/`dbFastGetConvertRoutine` refusing to render a
132 /// field in the requested DBR type, which for the `DBF_STRING` row means
133 /// `epicsParse*` rejecting the stored text (`cvt_st_d`,
134 /// `dbFastLinkConv.c:233-244`). `dbChannel_get` turns it into -1
135 /// (`db_access.c:816`) and rsrv answers the read with a ZEROED payload and
136 /// `m_cid = ECA_GETFAIL` (`camessage.c:545-561`) rather than a value.
137 /// Distinct from [`Self::InvalidValue`], which the put direction raises and
138 /// rsrv answers `ECA_PUTFAIL`.
139 #[error("get conversion failed: {0}")]
140 GetConvertFailed(String),
141}
142
143// ECA status constants (originally from protocol.rs, now in epics-ca-rs)
144const ECA_TIMEOUT: u32 = 80; // defmsg(CA_K_WARNING, 10)
145const ECA_NOWTACCESS: u32 = 376; // defmsg(CA_K_WARNING, 47)
146const ECA_PUTFAIL: u32 = 160; // defmsg(CA_K_WARNING, 20)
147const ECA_BADTYPE: u32 = 114; // defmsg(CA_K_ERROR, 14)
148const ECA_DISCONN: u32 = 192; // defmsg(CA_K_WARNING, 24)
149const ECA_TOLARGE: u32 = 72; // defmsg(CA_K_WARNING, 9)
150const ECA_BADCOUNT: u32 = 176; // defmsg(CA_K_WARNING, 22)
151const ECA_GETFAIL: u32 = 152; // defmsg(CA_K_WARNING, 19)
152
153/// Which CA operation failed — C's `read_action` or `write_action`
154/// (`rsrv/camessage.c`).
155///
156/// C never lets the error KIND choose the status once a request has reached
157/// the database: `read_action` answers a negative `dbChannel_get` with
158/// `ECA_GETFAIL` (`camessage.c:647-651`) and `write_action` answers a negative
159/// `dbChannel_put` with `ECA_PUTFAIL` (`camessage.c:781-789`), throwing the
160/// `dbStatus` away in both. The same underlying failure therefore has two
161/// correct answers, one per direction, and a mapping that sees only the error
162/// cannot pick between them. Naming the direction at the call is what makes a
163/// read unable to reach a put status: no arm reachable under
164/// [`CaOp::Read`] yields `ECA_PUTFAIL`.
165#[derive(Clone, Copy, PartialEq, Eq, Debug)]
166pub enum CaOp {
167 /// C `read_action` — `ca_get`, `ca_array_get_callback`, a monitor update.
168 Read,
169 /// C `write_action` / `write_notify_action` — `ca_put`, `ca_put_callback`.
170 Write,
171}
172
173impl CaOp {
174 /// The status C's action reports once the DATABASE has refused, whatever
175 /// the underlying `dbStatus` was.
176 const fn failed(self) -> u32 {
177 match self {
178 CaOp::Read => ECA_GETFAIL,
179 CaOp::Write => ECA_PUTFAIL,
180 }
181 }
182}
183
184impl CaError {
185 /// The ECA status a CA CLIENT reports for this error on `op`.
186 ///
187 /// Layered as C's `read_action`/`write_action` are. A status the error
188 /// already carries, or that a gate ABOVE the database produced, is the
189 /// same word whichever way the request was going; everything the database
190 /// itself refused is decided by `op` alone, because that is the point at
191 /// which C stops looking at the status.
192 ///
193 /// This is the client-side table, which sees libca's local statuses too.
194 /// The server's reply table is `PutStatus::of_failure`
195 /// (`epics-ca-rs/src/server/tcp.rs`) and is deliberately narrower: by the
196 /// time rsrv reaches `dbChannel_put`, the gates above it have already
197 /// answered, so every error left there is a database refusal.
198 pub fn to_eca_status(&self, op: CaOp) -> u32 {
199 match self {
200 // Raised by libca locally, before any database is reached — the
201 // request never became a read or a write.
202 CaError::Timeout => ECA_TIMEOUT,
203 CaError::TooLarge => ECA_TOLARGE,
204 CaError::BadCount => ECA_BADCOUNT,
205 // C's DBR-type gates: `INVALID_DB_REQ` above the read
206 // (`camessage.c:616-620`) and `caNetConvert` on both sides. They
207 // run before the database is touched and answer ECA_BADTYPE
208 // either way.
209 CaError::TypeMismatch(_) | CaError::UnsupportedType(_) => ECA_BADTYPE,
210 // Disconnection / shutdown are surfaced as ECA_DISCONN so a
211 // downstream client (e.g. caput on a CA gateway whose upstream
212 // just dropped) sees the actionable "CA channel disconnected"
213 // message rather than a request-failed status. I/O errors usually
214 // mean the circuit is wedged and read the same way.
215 CaError::Disconnected | CaError::Shutdown | CaError::Io(_) => ECA_DISCONN,
216 // Already an ECA status, decided by the peer or by libca.
217 // Re-deriving it would discard what was actually said.
218 CaError::WriteFailed(code) | CaError::ServerError(code) => *code,
219 // C's `rsrvCheckPut` gate, above the put (`camessage.c:741-751`).
220 // Its read-side twin ECA_NORDACCESS has no variant of its own —
221 // that one arrives from the wire as `ServerError`.
222 CaError::ReadOnlyField(_) => ECA_NOWTACCESS,
223 // Everything else is the database refusing: a value the field's
224 // converter rejected, a menu string naming no choice, a link field
225 // a get cannot render, a record-side veto. C answers all of them
226 // by direction, so listing any of them here would only be a way to
227 // get one of the two directions wrong.
228 _ => op.failed(),
229 }
230 }
231}
232
233pub type CaResult<T> = Result<T, CaError>;