Skip to main content

tiberius/tds/codec/token/
token_error.rs

1use crate::{tds::codec::FeatureLevel, SqlReadBytes};
2use std::fmt;
3
4#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
5/// An error token returned from the server.
6pub struct TokenError {
7    /// ErrorCode
8    pub(crate) code: u32,
9    /// ErrorState (describing code)
10    pub(crate) state: u8,
11    /// The class (severity) of the error
12    pub(crate) class: u8,
13    /// The error message
14    pub(crate) message: String,
15    pub(crate) server: String,
16    pub(crate) procedure: String,
17    pub(crate) line: u32,
18}
19
20impl TokenError {
21    pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
22    where
23        R: SqlReadBytes + Unpin,
24    {
25        let _length = src.read_u16_le().await? as usize;
26
27        let code = src.read_u32_le().await?;
28        let state = src.read_u8().await?;
29        let class = src.read_u8().await?;
30
31        let message = src.read_us_varchar().await?;
32        let server = src.read_b_varchar().await?;
33        let procedure = src.read_b_varchar().await?;
34
35        // MS-TDS 2.2.7.10: LineNumber is a 4-byte LONG for TDS 7.2 (SQL Server
36        // 2005) and later, and a 2-byte USHORT before that. The boundary is
37        // inclusive of 7.2, so use `>=` — a strict `>` mis-reads a 2-byte value
38        // against a real SQL Server 2005 and desyncs the token stream.
39        let line = if src.context().version() >= FeatureLevel::SqlServer2005 {
40            src.read_u32_le().await?
41        } else {
42            src.read_u16_le().await? as u32
43        };
44
45        let token = TokenError {
46            code,
47            state,
48            class,
49            message,
50            server,
51            procedure,
52            line,
53        };
54
55        Ok(token)
56    }
57
58    /// The error code, see descriptions from [the manual].
59    ///
60    /// [the manual]: https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver15
61    pub fn code(&self) -> u32 {
62        self.code
63    }
64
65    /// The error state, used as a modifier to the error number.
66    pub fn state(&self) -> u8 {
67        self.state
68    }
69
70    /// The class (severity) of the error. A class of less than 10 indicates an
71    /// informational message.
72    pub fn class(&self) -> u8 {
73        self.class
74    }
75
76    /// The error message returned from the server.
77    pub fn message(&self) -> &str {
78        &self.message
79    }
80
81    /// The server name.
82    pub fn server(&self) -> &str {
83        &self.server
84    }
85
86    /// The name of the stored procedure causing the error.
87    pub fn procedure(&self) -> &str {
88        &self.procedure
89    }
90
91    /// The line number in the SQL batch or stored procedure that caused the
92    /// error. Line numbers begin at 1. If the line number is not applicable to
93    /// the message, the value is 0.
94    pub fn line(&self) -> u32 {
95        self.line
96    }
97}
98
99impl fmt::Display for TokenError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(
102            f,
103            "'{}' on server {} executing {} on line {} (code: {}, state: {}, class: {})",
104            self.message, self.server, self.procedure, self.line, self.code, self.state, self.class
105        )
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    fn sample() -> TokenError {
114        TokenError {
115            code: 1205,
116            state: 2,
117            class: 13,
118            message: "deadlocked".to_string(),
119            server: "myserver".to_string(),
120            procedure: "myproc".to_string(),
121            line: 42,
122        }
123    }
124
125    #[test]
126    fn accessors() {
127        let e = sample();
128        assert_eq!(e.code(), 1205);
129        assert_eq!(e.state(), 2);
130        assert_eq!(e.class(), 13);
131        assert_eq!(e.message(), "deadlocked");
132        assert_eq!(e.server(), "myserver");
133        assert_eq!(e.procedure(), "myproc");
134        assert_eq!(e.line(), 42);
135    }
136
137    #[test]
138    fn display_contains_all_fields() {
139        let rendered = format!("{}", sample());
140        assert_eq!(
141            rendered,
142            "'deadlocked' on server myserver executing myproc on line 42 (code: 1205, state: 2, class: 13)"
143        );
144    }
145
146    #[tokio::test]
147    async fn decode_reads_all_fields_with_four_byte_line_number() {
148        use crate::sql_read_bytes::test_utils::IntoSqlReadBytes;
149        use byteorder::{LittleEndian, WriteBytesExt};
150        use bytes::{BufMut, BytesMut};
151
152        fn write_us_varchar(buf: &mut Vec<u8>, s: &str) {
153            buf.write_u16::<LittleEndian>(s.encode_utf16().count() as u16)
154                .unwrap();
155            for u in s.encode_utf16() {
156                buf.write_u16::<LittleEndian>(u).unwrap();
157            }
158        }
159
160        fn write_b_varchar(buf: &mut Vec<u8>, s: &str) {
161            buf.push(s.encode_utf16().count() as u8);
162            for u in s.encode_utf16() {
163                buf.write_u16::<LittleEndian>(u).unwrap();
164            }
165        }
166
167        let mut body = Vec::new();
168        body.write_u32::<LittleEndian>(1205).unwrap(); // code
169        body.push(2); // state
170        body.push(13); // class
171        write_us_varchar(&mut body, "deadlocked");
172        write_b_varchar(&mut body, "myserver");
173        write_b_varchar(&mut body, "myproc");
174        body.write_u32::<LittleEndian>(42).unwrap(); // line, TDS >= 7.2 (default context)
175
176        let mut buf = BytesMut::new();
177        buf.put_u16_le(body.len() as u16); // length prefix, ignored by decode
178        buf.put_slice(&body);
179
180        let decoded = TokenError::decode(&mut buf.into_sql_read_bytes())
181            .await
182            .unwrap();
183
184        assert_eq!(decoded, sample());
185    }
186
187    #[tokio::test]
188    async fn decode_reads_full_four_byte_line_number_on_tds72_plus() {
189        // The default test context reports SqlServerN (>= TDS 7.2), so the
190        // LineNumber must be read as a 4-byte LONG. A `>` mutation of the
191        // `>=` boundary check would read only 2 bytes and mis-decode the value.
192        // 0x0001_0001 (65537) has distinct low-16-bit and full-32-bit values, so
193        // a 2-byte read yields 1 while the correct 4-byte read yields 65537.
194        use crate::sql_read_bytes::test_utils::IntoSqlReadBytes;
195        use bytes::{BufMut, BytesMut};
196
197        let mut body = BytesMut::new();
198        body.put_u32_le(1205); // code
199        body.put_u8(2); // state
200        body.put_u8(13); // class
201        body.put_u16_le(0); // message: us_varchar, length 0
202        body.put_u8(0); // server: b_varchar, length 0
203        body.put_u8(0); // procedure: b_varchar, length 0
204        body.put_u32_le(0x0001_0001); // line number, 4 bytes
205
206        let mut buf = BytesMut::new();
207        buf.put_u16_le(body.len() as u16); // length prefix, ignored by decode
208        buf.put_slice(&body);
209
210        let decoded = TokenError::decode(&mut buf.into_sql_read_bytes())
211            .await
212            .unwrap();
213
214        assert_eq!(decoded.line(), 0x0001_0001);
215    }
216}