diesel/mysql/
value.rs

1use super::types::date_and_time::MysqlTime;
2use super::MysqlType;
3
4use crate::deserialize;
5use std::error::Error;
6use std::mem::MaybeUninit;
7
8/// Raw mysql value as received from the database
9#[derive(Clone, Debug)]
10pub struct MysqlValue<'a> {
11    raw: &'a [u8],
12    tpe: MysqlType,
13}
14
15impl<'a> MysqlValue<'a> {
16    /// Create a new instance of [MysqlValue] based on a byte buffer
17    /// and information about the type of the value represented by the
18    /// given buffer
19    #[cfg(feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes")]
20    pub fn new(raw: &'a [u8], tpe: MysqlType) -> Self {
21        Self::new_internal(raw, tpe)
22    }
23
24    pub(in crate::mysql) fn new_internal(raw: &'a [u8], tpe: MysqlType) -> Self {
25        Self { raw, tpe }
26    }
27
28    /// Get the underlying raw byte representation
29    pub fn as_bytes(&self) -> &[u8] {
30        self.raw
31    }
32
33    /// Get the mysql type of the current value
34    pub fn value_type(&self) -> MysqlType {
35        self.tpe
36    }
37
38    /// Checks that the type code is valid, and interprets the data as a
39    /// `MysqlTime` pointer
40    // We use `ptr::copy` to read the actual data
41    // and copy it over to the returned `MysqlTime` instance
42    #[allow(unsafe_code)] // MaybeUninit + ptr copy
43    pub(crate) fn time_value(&self) -> deserialize::Result<MysqlTime> {
44        match self.tpe {
45            MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => {
46                // we check for the size of the `MYSQL_TIME` type from `mysqlclient_sys` here as
47                // certain older libmysqlclient and newer libmariadb versions do not have all the
48                // same fields (and size) as the `MysqlTime` type from diesel. The later one is modeled after
49                // the type from newer libmysqlclient
50                self.too_short_buffer(
51                    #[cfg(feature = "mysql")]
52                    std::mem::size_of::<mysqlclient_sys::MYSQL_TIME>(),
53                    #[cfg(not(feature = "mysql"))]
54                    std::mem::size_of::<MysqlTime>(),
55                    "timestamp",
56                )?;
57                // To ensure we copy the right number of bytes we need to make sure to copy not more bytes than needed
58                // for `MysqlTime` and not more bytes than inside of the buffer
59                let len = std::cmp::min(std::mem::size_of::<MysqlTime>(), self.raw.len());
60                // Zero is a valid pattern for this type so we are fine with initializing all fields to zero
61                // If the provided byte buffer is too short we just use 0 as default value
62                let mut out = MaybeUninit::<MysqlTime>::zeroed();
63                // Make sure to check that the boolean is an actual bool value, so 0 or 1
64                // as anything else is UB in rust
65                let neg_offset = std::mem::offset_of!(MysqlTime, neg);
66                if neg_offset < self.raw.len()
67                    && self.raw[neg_offset] != 0
68                    && self.raw[neg_offset] != 1
69                {
70                    return Err(
71                        "Received invalid value for `neg` in the `MysqlTime` datastructure".into(),
72                    );
73                }
74                let result = unsafe {
75                    // SAFETY: We copy over the bytes from our raw buffer to the `MysqlTime` instance
76                    // This type is correctly aligned and we ensure that we do not copy more bytes than are there
77                    // We are also sure that these ptr do not overlap as they are completely different
78                    // instances
79                    std::ptr::copy_nonoverlapping(
80                        self.raw.as_ptr(),
81                        out.as_mut_ptr() as *mut u8,
82                        len,
83                    );
84                    // SAFETY: all zero is a valid pattern for this type
85                    // Otherwise any other bit pattern is also valid, beside
86                    // neg being something other than 0 or 1
87                    // We check for that above by looking at the byte before copying
88                    out.assume_init()
89                };
90                if result.neg {
91                    Err("Negative dates/times are not yet supported".into())
92                } else {
93                    Ok(result)
94                }
95            }
96            _ => Err(self.invalid_type_code("timestamp")),
97        }
98    }
99
100    /// Returns the numeric representation of this value, based on the type code.
101    /// Returns an error if the type code is not numeric.
102    pub(crate) fn numeric_value(&self) -> deserialize::Result<NumericRepresentation<'_>> {
103        Ok(match self.tpe {
104            MysqlType::UnsignedTiny | MysqlType::Tiny => {
105                NumericRepresentation::Tiny(self.raw[0].try_into()?)
106            }
107            MysqlType::UnsignedShort | MysqlType::Short => {
108                self.too_short_buffer(2, "Short")?;
109                NumericRepresentation::Small(i16::from_ne_bytes((&self.raw[..2]).try_into()?))
110            }
111            MysqlType::UnsignedLong | MysqlType::Long => {
112                self.too_short_buffer(4, "Long")?;
113                NumericRepresentation::Medium(i32::from_ne_bytes((&self.raw[..4]).try_into()?))
114            }
115            MysqlType::UnsignedLongLong | MysqlType::LongLong => {
116                self.too_short_buffer(8, "LongLong")?;
117                NumericRepresentation::Big(i64::from_ne_bytes(self.raw.try_into()?))
118            }
119            MysqlType::Float => {
120                self.too_short_buffer(4, "Float")?;
121                NumericRepresentation::Float(f32::from_ne_bytes(self.raw.try_into()?))
122            }
123            MysqlType::Double => {
124                self.too_short_buffer(8, "Double")?;
125                NumericRepresentation::Double(f64::from_ne_bytes(self.raw.try_into()?))
126            }
127
128            MysqlType::Numeric => NumericRepresentation::Decimal(self.raw),
129            _ => return Err(self.invalid_type_code("number")),
130        })
131    }
132
133    fn invalid_type_code(&self, expected: &str) -> Box<dyn Error + Send + Sync> {
134        format!(
135            "Invalid representation received for {}: {:?}",
136            expected, self.tpe
137        )
138        .into()
139    }
140
141    fn too_short_buffer(&self, expected: usize, tpe: &'static str) -> deserialize::Result<()> {
142        if self.raw.len() < expected {
143            Err(format!(
144                "Received a buffer with an invalid size while trying \
145             to read a {tpe} value: Expected at least {expected} bytes \
146             but got {}",
147                self.raw.len()
148            )
149            .into())
150        } else {
151            Ok(())
152        }
153    }
154}
155
156/// Represents all possible forms MySQL transmits integers
157#[derive(Debug, Clone, Copy)]
158#[non_exhaustive]
159pub enum NumericRepresentation<'a> {
160    /// Corresponds to `MYSQL_TYPE_TINY`
161    Tiny(i8),
162    /// Corresponds to `MYSQL_TYPE_SHORT`
163    Small(i16),
164    /// Corresponds to `MYSQL_TYPE_INT24` and `MYSQL_TYPE_LONG`
165    Medium(i32),
166    /// Corresponds to `MYSQL_TYPE_LONGLONG`
167    Big(i64),
168    /// Corresponds to `MYSQL_TYPE_FLOAT`
169    Float(f32),
170    /// Corresponds to `MYSQL_TYPE_DOUBLE`
171    Double(f64),
172    /// Corresponds to `MYSQL_TYPE_DECIMAL` and `MYSQL_TYPE_NEWDECIMAL`
173    Decimal(&'a [u8]),
174}
175
176#[test]
177#[allow(unsafe_code, reason = "Test code")]
178fn invalid_reads() {
179    use crate::data_types::MysqlTimestampType;
180
181    assert!(MysqlValue::new_internal(&[1], MysqlType::Timestamp)
182        .time_value()
183        .is_err());
184    let v = MysqlTime {
185        year: 2025,
186        month: 9,
187        day: 15,
188        hour: 22,
189        minute: 3,
190        second: 10,
191        second_part: 0,
192        neg: false,
193        time_type: MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME,
194        time_zone_displacement: 0,
195    };
196    let mut bytes = [0; std::mem::size_of::<MysqlTime>()];
197    unsafe {
198        // SAFETY: Test code
199        // also the size matches and we want to get raw bytes
200        std::ptr::copy(
201            &v as *const MysqlTime as *const u8,
202            bytes.as_mut_ptr(),
203            bytes.len(),
204        );
205    }
206    let offset = std::mem::offset_of!(MysqlTime, neg);
207    bytes[offset] = 42;
208    assert!(MysqlValue::new_internal(&bytes, MysqlType::Timestamp)
209        .time_value()
210        .is_err());
211
212    assert!(MysqlValue::new_internal(&[1, 2], MysqlType::Long)
213        .numeric_value()
214        .is_err());
215
216    assert!(MysqlValue::new_internal(&[1, 2, 3, 4], MysqlType::LongLong)
217        .numeric_value()
218        .is_err());
219
220    assert!(MysqlValue::new_internal(&[1], MysqlType::Short)
221        .numeric_value()
222        .is_err());
223
224    assert!(MysqlValue::new_internal(&[1, 2, 3, 4], MysqlType::Double)
225        .numeric_value()
226        .is_err());
227
228    assert!(MysqlValue::new_internal(&[1, 2], MysqlType::Float)
229        .numeric_value()
230        .is_err());
231
232    assert!(MysqlValue::new_internal(&[1], MysqlType::Tiny)
233        .numeric_value()
234        .is_ok());
235}