Skip to main content

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>;