sqlx_core_oldapi/postgres/message/
response.rs

1use std::str::from_utf8;
2
3use bytes::Bytes;
4use memchr::memchr;
5
6use crate::error::Error;
7use crate::io::Decode;
8
9#[derive(Debug, Copy, Clone, Eq, PartialEq)]
10#[repr(u8)]
11pub enum PgSeverity {
12    Panic,
13    Fatal,
14    Error,
15    Warning,
16    Notice,
17    Debug,
18    Info,
19    Log,
20}
21
22impl PgSeverity {
23    #[inline]
24    pub fn is_error(self) -> bool {
25        matches!(self, Self::Panic | Self::Fatal | Self::Error)
26    }
27}
28
29impl TryFrom<&str> for PgSeverity {
30    type Error = Error;
31
32    fn try_from(s: &str) -> Result<PgSeverity, Error> {
33        let result = match s {
34            "PANIC" => PgSeverity::Panic,
35            "FATAL" => PgSeverity::Fatal,
36            "ERROR" => PgSeverity::Error,
37            "WARNING" => PgSeverity::Warning,
38            "NOTICE" => PgSeverity::Notice,
39            "DEBUG" => PgSeverity::Debug,
40            "INFO" => PgSeverity::Info,
41            "LOG" => PgSeverity::Log,
42
43            severity => {
44                return Err(err_protocol!("unknown severity: {:?}", severity));
45            }
46        };
47
48        Ok(result)
49    }
50}
51
52#[derive(Debug)]
53pub struct Notice {
54    storage: Bytes,
55    severity: PgSeverity,
56    message: (u16, u16),
57    code: (u16, u16),
58}
59
60impl Notice {
61    #[inline]
62    pub fn severity(&self) -> PgSeverity {
63        self.severity
64    }
65
66    #[inline]
67    pub fn code(&self) -> &str {
68        self.get_cached_str(self.code)
69    }
70
71    #[inline]
72    pub fn message(&self) -> &str {
73        self.get_cached_str(self.message)
74    }
75
76    // Field descriptions available here:
77    //  https://www.postgresql.org/docs/current/protocol-error-fields.html
78
79    #[inline]
80    pub fn get(&self, ty: u8) -> Option<&str> {
81        self.get_raw(ty).and_then(|v| from_utf8(v).ok())
82    }
83
84    pub fn get_raw(&self, ty: u8) -> Option<&[u8]> {
85        self.fields()
86            .filter(|(field, _)| *field == ty)
87            .map(|(_, (start, end))| &self.storage[start as usize..end as usize])
88            .next()
89    }
90}
91
92impl Notice {
93    #[inline]
94    fn fields(&self) -> Fields<'_> {
95        Fields {
96            storage: &self.storage,
97            offset: 0,
98        }
99    }
100
101    #[inline]
102    fn get_cached_str(&self, cache: (u16, u16)) -> &str {
103        // unwrap: this cannot fail at this stage
104        from_utf8(&self.storage[cache.0 as usize..cache.1 as usize]).unwrap()
105    }
106}
107
108impl Decode<'_> for Notice {
109    fn decode_with(buf: Bytes, _: ()) -> Result<Self, Error> {
110        // In order to support PostgreSQL 9.5 and older we need to parse the localized S field.
111        // Newer versions additionally come with the V field that is guaranteed to be in English.
112        // We thus read both versions and prefer the unlocalized one if available.
113        const DEFAULT_SEVERITY: PgSeverity = PgSeverity::Log;
114        let mut severity_v = None;
115        let mut severity_s = None;
116        let mut message = (0, 0);
117        let mut code = (0, 0);
118
119        // we cache the three always present fields
120        // this enables to keep the access time down for the fields most likely accessed
121
122        let fields = Fields {
123            storage: &buf,
124            offset: 0,
125        };
126
127        for (field, v) in fields {
128            if message.0 != 0 && code.0 != 0 {
129                // stop iterating when we have the 3 fields we were looking for
130                // we assume V (severity) was the first field as it should be
131                break;
132            }
133
134            match field {
135                b'S' => {
136                    severity_s = from_utf8(&buf[v.0 as usize..v.1 as usize])
137                        // If the error string is not UTF-8, we have no hope of interpreting it,
138                        // localized or not. The `V` field would likely fail to parse as well.
139                        .map_err(|_| notice_protocol_err())?
140                        .try_into()
141                        // If we couldn't parse the severity here, it might just be localized.
142                        .ok();
143                }
144
145                b'V' => {
146                    // Propagate errors here, because V is not localized and
147                    // thus we are missing a possible variant.
148                    severity_v = Some(
149                        from_utf8(&buf[v.0 as usize..v.1 as usize])
150                            .map_err(|_| notice_protocol_err())?
151                            .try_into()?,
152                    );
153                }
154
155                b'M' => {
156                    message = v;
157                }
158
159                b'C' => {
160                    code = v;
161                }
162
163                _ => {}
164            }
165        }
166
167        Ok(Self {
168            severity: severity_v.or(severity_s).unwrap_or(DEFAULT_SEVERITY),
169            message,
170            code,
171            storage: buf,
172        })
173    }
174}
175
176/// An iterator over each field in the Error (or Notice) response.
177struct Fields<'a> {
178    storage: &'a [u8],
179    offset: u16,
180}
181
182impl<'a> Iterator for Fields<'a> {
183    type Item = (u8, (u16, u16));
184
185    fn next(&mut self) -> Option<Self::Item> {
186        // The fields in the response body are sequentially stored as [tag][string],
187        // ending in a final, additional [nul]
188
189        let ty = self.storage[self.offset as usize];
190
191        if ty == 0 {
192            return None;
193        }
194
195        let nul =
196            u16::try_from(memchr(b'\0', &self.storage[(self.offset + 1) as usize..])?).ok()?;
197        let offset = self.offset;
198
199        self.offset += nul + 2;
200
201        Some((ty, (offset + 1, offset + nul + 1)))
202    }
203}
204
205fn notice_protocol_err() -> Error {
206    // https://github.com/launchbadge/sqlx/issues/1144
207    Error::Protocol(
208        "Postgres returned a non-UTF-8 string for its error message. \
209         This is most likely due to an error that occurred during authentication and \
210         the default lc_messages locale is not binary-compatible with UTF-8. \
211         See the server logs for the error details."
212            .into(),
213    )
214}
215
216#[test]
217fn test_decode_error_response() {
218    const DATA: &[u8] = b"SNOTICE\0VNOTICE\0C42710\0Mextension \"uuid-ossp\" already exists, skipping\0Fextension.c\0L1656\0RCreateExtension\0\0";
219
220    let m = Notice::decode(Bytes::from_static(DATA)).unwrap();
221
222    assert_eq!(
223        m.message(),
224        "extension \"uuid-ossp\" already exists, skipping"
225    );
226
227    assert!(matches!(m.severity(), PgSeverity::Notice));
228    assert_eq!(m.code(), "42710");
229}
230
231#[cfg(all(test, not(debug_assertions)))]
232#[bench]
233fn bench_error_response_get_message(b: &mut test::Bencher) {
234    const DATA: &[u8] = b"SNOTICE\0VNOTICE\0C42710\0Mextension \"uuid-ossp\" already exists, skipping\0Fextension.c\0L1656\0RCreateExtension\0\0";
235
236    let res = Notice::decode(test::black_box(Bytes::from_static(DATA))).unwrap();
237
238    b.iter(|| {
239        let _ = test::black_box(&res).message();
240    });
241}
242
243#[cfg(all(test, not(debug_assertions)))]
244#[bench]
245fn bench_decode_error_response(b: &mut test::Bencher) {
246    const DATA: &[u8] = b"SNOTICE\0VNOTICE\0C42710\0Mextension \"uuid-ossp\" already exists, skipping\0Fextension.c\0L1656\0RCreateExtension\0\0";
247
248    b.iter(|| {
249        let _ = Notice::decode(test::black_box(Bytes::from_static(DATA)));
250    });
251}