Skip to main content

truefix_core/
error.rs

1//! Typed errors for the core layer. No code path panics (Constitution Principle I).
2
3use thiserror::Error;
4
5/// Error converting a field's raw value to a typed Rust value.
6#[derive(Debug, Clone, PartialEq, Eq, Error)]
7pub enum FieldError {
8    /// The raw value is not valid UTF-8.
9    #[error("field {tag}: value is not valid UTF-8")]
10    NotUtf8 {
11        /// The field tag.
12        tag: u32,
13    },
14    /// The value is not a valid integer.
15    #[error("field {tag}: expected an integer, got {value:?}")]
16    NotInt {
17        /// The field tag.
18        tag: u32,
19        /// The offending value.
20        value: String,
21    },
22    /// The value is not a valid decimal.
23    #[error("field {tag}: expected a decimal, got {value:?}")]
24    NotDecimal {
25        /// The field tag.
26        tag: u32,
27        /// The offending value.
28        value: String,
29    },
30    /// The value is not a single character.
31    #[error("field {tag}: expected a single character, got {value:?}")]
32    NotChar {
33        /// The field tag.
34        tag: u32,
35        /// The offending value.
36        value: String,
37    },
38    /// The value is not a boolean (`Y`/`N`).
39    #[error("field {tag}: expected 'Y' or 'N', got {value:?}")]
40    NotBool {
41        /// The field tag.
42        tag: u32,
43        /// The offending value.
44        value: String,
45    },
46    /// The value is not a valid UTC timestamp.
47    #[error("field {tag}: invalid UTC timestamp {value:?}")]
48    NotTimestamp {
49        /// The field tag.
50        tag: u32,
51        /// The offending value.
52        value: String,
53    },
54    /// The value is not a valid UTC date-only (`YYYYMMDD`).
55    #[error("field {tag}: invalid UTC date-only {value:?}")]
56    NotDateOnly {
57        /// The field tag.
58        tag: u32,
59        /// The offending value.
60        value: String,
61    },
62    /// The value is not a valid UTC time-only (`HH:MM:SS[.sss...]`).
63    #[error("field {tag}: invalid UTC time-only {value:?}")]
64    NotTimeOnly {
65        /// The field tag.
66        tag: u32,
67        /// The offending value.
68        value: String,
69    },
70}
71
72/// Error decoding raw bytes into a [`Message`](crate::Message).
73#[derive(Debug, Clone, PartialEq, Eq, Error)]
74pub enum DecodeError {
75    /// The input was empty.
76    #[error("empty input")]
77    Empty,
78    /// The message does not start with BeginString (tag 8).
79    #[error("message does not begin with BeginString (tag 8)")]
80    MissingBeginString,
81    /// BodyLength (tag 9) is missing or not a valid number.
82    #[error("missing or invalid BodyLength (tag 9)")]
83    InvalidBodyLength,
84    /// BodyLength (tag 9) is declared as exactly 0 (BUG-100/FR-014, feature 007) — every real FIX
85    /// message has a non-empty body (at minimum MsgType), matching QuickFIX/J (QFJ-903) and
86    /// QuickFIX/Go, which both reject this rather than framing an empty-body message. Distinct
87    /// from [`Self::InvalidBodyLength`] (missing/non-numeric) so the transport layer can treat it
88    /// like [`Self::BodyLengthTooLarge`] — closing the connection outright — rather than the
89    /// generic malformed-frame skip-and-resync recovery `InvalidBodyLength` gets.
90    #[error("declared BodyLength is 0")]
91    ZeroBodyLength,
92    /// BodyLength (tag 9) declares a frame larger than the configured/sane maximum (BUG-13/
93    /// FR-024, feature 006) — the connection must be closed instead of buffering unboundedly
94    /// while waiting for that much data to arrive (a memory-exhaustion DoS vector, reachable
95    /// pre- or post-Logon on any open acceptor port).
96    #[error("declared BodyLength {declared} exceeds the maximum frame size {max}")]
97    BodyLengthTooLarge {
98        /// Value declared in tag 9.
99        declared: usize,
100        /// The configured/sane maximum.
101        max: usize,
102    },
103    /// Dictionary-driven repeating-group nesting exceeded the defensive recursion limit.
104    #[error("repeating-group nesting exceeds the maximum depth {max}")]
105    GroupNestingTooDeep {
106        /// Maximum supported nesting depth.
107        max: usize,
108    },
109    /// The declared BodyLength does not match the actual body size.
110    #[error("BodyLength mismatch: declared {declared}, actual {actual}")]
111    BodyLengthMismatch {
112        /// Value declared in tag 9.
113        declared: usize,
114        /// Actual measured body length.
115        actual: usize,
116    },
117    /// The third field is not MsgType (tag 35) — BUG-80/FR-049 (feature 007): every real FIX
118    /// message has a MsgType immediately after BeginString/BodyLength (matching QuickFIX/J and
119    /// QuickFIX/Go, which both require it); a message missing it entirely was previously accepted.
120    #[error("message does not carry MsgType (tag 35) as its third field")]
121    MissingMsgType,
122    /// CheckSum (tag 10) is missing or not a valid number.
123    #[error("missing or invalid CheckSum (tag 10)")]
124    MissingChecksum,
125    /// The declared CheckSum does not match the computed one.
126    #[error("CheckSum mismatch: declared {declared:03}, computed {computed:03}")]
127    ChecksumMismatch {
128        /// Value declared in tag 10.
129        declared: u32,
130        /// Locally computed checksum.
131        computed: u32,
132    },
133    /// A field could not be parsed.
134    #[error("garbled field near byte {offset}: {reason}")]
135    GarbledField {
136        /// Byte offset where the problem was detected.
137        offset: usize,
138        /// Human-readable reason.
139        reason: &'static str,
140    },
141    /// The input ended before a field was complete.
142    #[error("truncated input near byte {offset}")]
143    Truncated {
144        /// Byte offset where truncation was detected.
145        offset: usize,
146    },
147    /// A tag number was not a valid positive integer.
148    #[error("invalid tag number near byte {offset}")]
149    InvalidTag {
150        /// Byte offset of the offending tag.
151        offset: usize,
152    },
153}
154
155/// A session-level rejection returned from an admin/logon callback — the engine emits a Reject
156/// (35=3). Typed, not stringly (Constitution Principle I; FR-016). `reason` is the numeric
157/// SessionRejectReason (tag 373).
158#[derive(Debug, Clone, PartialEq, Eq, Error)]
159#[error("reject (reason {reason}, ref tag {ref_tag:?}): {text:?}")]
160pub struct Reject {
161    /// SessionRejectReason (tag 373) numeric code.
162    pub reason: u32,
163    /// Optional referenced tag (RefTagID, 371).
164    pub ref_tag: Option<u32>,
165    /// Optional human-readable text (tag 58).
166    pub text: Option<String>,
167    /// Optional `SessionStatus` (tag 573) reason, propagated onto the outbound Logout when a
168    /// `from_admin` callback refuses a Logon (US10, FR-013).
169    pub session_status: Option<u16>,
170}
171
172/// Returned from an outbound callback to suppress a message: the engine does not send it and does
173/// not store it as sent (it consumes no outbound sequence number). FR-016.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
175#[error("do not send")]
176pub struct DoNotSend;
177
178/// A business-level rejection returned from an application callback — the engine emits a
179/// BusinessMessageReject (35=j). `reason` is the numeric BusinessRejectReason (tag 380). FR-016.
180#[derive(Debug, Clone, PartialEq, Eq, Error)]
181#[error("business reject (reason {reason}, ref tag {ref_tag:?}): {text:?}")]
182pub struct BusinessReject {
183    /// BusinessRejectReason (tag 380) numeric code.
184    pub reason: u32,
185    /// Optional referenced tag (RefTagID, 371).
186    pub ref_tag: Option<u32>,
187    /// Optional human-readable text (tag 58).
188    pub text: Option<String>,
189}