use super::types::date_and_time::MysqlTime;
use super::MysqlType;
use crate::deserialize;
use std::error::Error;
use std::mem::MaybeUninit;
#[derive(Clone, Debug)]
pub struct MysqlValue<'a> {
raw: &'a [u8],
tpe: MysqlType,
}
impl<'a> MysqlValue<'a> {
#[cfg(feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes")]
pub fn new(raw: &'a [u8], tpe: MysqlType) -> Self {
Self::new_internal(raw, tpe)
}
pub(in crate::mysql) fn new_internal(raw: &'a [u8], tpe: MysqlType) -> Self {
Self { raw, tpe }
}
pub fn as_bytes(&self) -> &'a [u8] {
self.raw
}
pub fn value_type(&self) -> MysqlType {
self.tpe
}
#[allow(unsafe_code)] pub(crate) fn time_value(&self) -> deserialize::Result<MysqlTime> {
match self.tpe {
MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => {
self.too_short_buffer(
#[cfg(feature = "mysql")]
std::mem::size_of::<mysqlclient_sys::MYSQL_TIME>(),
#[cfg(not(feature = "mysql"))]
std::mem::size_of::<MysqlTime>(),
"timestamp",
)?;
let len = std::cmp::min(std::mem::size_of::<MysqlTime>(), self.raw.len());
let mut out = MaybeUninit::<MysqlTime>::zeroed();
let neg_offset = std::mem::offset_of!(MysqlTime, neg);
if neg_offset < self.raw.len()
&& self.raw[neg_offset] != 0
&& self.raw[neg_offset] != 1
{
return Err(
"Received invalid value for `neg` in the `MysqlTime` datastructure".into(),
);
}
let result = unsafe {
std::ptr::copy_nonoverlapping(
self.raw.as_ptr(),
out.as_mut_ptr() as *mut u8,
len,
);
out.assume_init()
};
if result.neg {
Err("Negative dates/times are not yet supported".into())
} else {
Ok(result)
}
}
_ => Err(self.invalid_type_code("timestamp")),
}
}
pub(crate) fn numeric_value(&self) -> deserialize::Result<NumericRepresentation<'_>> {
Ok(match self.tpe {
MysqlType::Tiny => NumericRepresentation::Tiny(self.read()?),
MysqlType::UnsignedTiny => NumericRepresentation::UnsignedTiny(self.read()?),
MysqlType::Short => NumericRepresentation::Small(self.read()?),
MysqlType::UnsignedShort => NumericRepresentation::UnsignedSmall(self.read()?),
MysqlType::Long => NumericRepresentation::Medium(self.read()?),
MysqlType::UnsignedLong => NumericRepresentation::UnsignedMedium(self.read()?),
MysqlType::LongLong => NumericRepresentation::Big(self.read()?),
MysqlType::UnsignedLongLong => NumericRepresentation::UnsignedBig(self.read()?),
MysqlType::Float => NumericRepresentation::Float(self.read()?),
MysqlType::Double => NumericRepresentation::Double(self.read()?),
MysqlType::Numeric => NumericRepresentation::Decimal(self.raw),
_ => return Err(self.invalid_type_code("number")),
})
}
fn invalid_type_code(&self, expected: &str) -> Box<dyn Error + Send + Sync> {
format!(
"Invalid representation received for {}: {:?}",
expected, self.tpe
)
.into()
}
fn too_short_buffer(&self, expected: usize, tpe: &'static str) -> deserialize::Result<()> {
if self.raw.len() < expected {
Err(format!(
"Received a buffer with an invalid size while trying \
to read a {tpe} value: Expected at least {expected} bytes \
but got {}",
self.raw.len()
)
.into())
} else {
Ok(())
}
}
fn read<T: FromNeBytes>(&self) -> deserialize::Result<T> {
self.too_short_buffer(core::mem::size_of::<T>(), core::any::type_name::<T>())?;
Ok(T::from_ne_prefix(self.raw))
}
}
trait FromNeBytes: Sized {
fn from_ne_prefix(buffer: &[u8]) -> Self;
}
macro_rules! impl_from_ne_bytes {
($($t:ty),+ $(,)?) => {
$(
impl FromNeBytes for $t {
fn from_ne_prefix(buffer: &[u8]) -> Self {
let mut bytes = [0_u8; core::mem::size_of::<Self>()];
bytes.copy_from_slice(&buffer[..core::mem::size_of::<Self>()]);
Self::from_ne_bytes(bytes)
}
}
)+
};
}
impl_from_ne_bytes!(i8, u8, i16, u16, i32, u32, i64, u64, f32, f64);
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum NumericRepresentation<'a> {
Tiny(i8),
UnsignedTiny(u8),
Small(i16),
UnsignedSmall(u16),
Medium(i32),
UnsignedMedium(u32),
Big(i64),
UnsignedBig(u64),
Float(f32),
Double(f64),
Decimal(&'a [u8]),
}
#[test]
#[allow(unsafe_code, reason = "Test code")]
fn invalid_reads() {
use crate::data_types::MysqlTimestampType;
assert!(MysqlValue::new_internal(&[1], MysqlType::Timestamp)
.time_value()
.is_err());
let v = MysqlTime {
year: 2025,
month: 9,
day: 15,
hour: 22,
minute: 3,
second: 10,
second_part: 0,
neg: false,
time_type: MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME,
time_zone_displacement: 0,
};
let mut bytes = [0; std::mem::size_of::<MysqlTime>()];
unsafe {
std::ptr::copy(
&v as *const MysqlTime as *const u8,
bytes.as_mut_ptr(),
bytes.len(),
);
}
let offset = std::mem::offset_of!(MysqlTime, neg);
bytes[offset] = 42;
assert!(MysqlValue::new_internal(&bytes, MysqlType::Timestamp)
.time_value()
.is_err());
assert!(MysqlValue::new_internal(&[1, 2], MysqlType::Long)
.numeric_value()
.is_err());
assert!(MysqlValue::new_internal(&[1, 2, 3, 4], MysqlType::LongLong)
.numeric_value()
.is_err());
assert!(MysqlValue::new_internal(&[1], MysqlType::Short)
.numeric_value()
.is_err());
assert!(MysqlValue::new_internal(&[1, 2, 3, 4], MysqlType::Double)
.numeric_value()
.is_err());
assert!(MysqlValue::new_internal(&[1, 2], MysqlType::Float)
.numeric_value()
.is_err());
assert!(MysqlValue::new_internal(&[1], MysqlType::Tiny)
.numeric_value()
.is_ok());
assert!(MysqlValue::new_internal(&[], MysqlType::Tiny)
.numeric_value()
.is_err());
assert!(MysqlValue::new_internal(&[], MysqlType::UnsignedTiny)
.numeric_value()
.is_err());
assert!(
MysqlValue::new_internal(&[1, 2, 3], MysqlType::UnsignedShort)
.numeric_value()
.is_ok()
);
assert!(
MysqlValue::new_internal(&[1, 2, 3, 4, 5], MysqlType::UnsignedLong)
.numeric_value()
.is_ok()
);
assert!(
MysqlValue::new_internal(&[1, 2, 3, 4, 5, 6, 7, 8, 9], MysqlType::UnsignedLongLong)
.numeric_value()
.is_ok()
);
}
#[test]
fn numeric_value_keeps_signedness() {
use super::NumericRepresentation as N;
assert!(matches!(
MysqlValue::new_internal(&[0xFF], MysqlType::Tiny).numeric_value(),
Ok(N::Tiny(-1))
));
assert!(matches!(
MysqlValue::new_internal(&[200], MysqlType::UnsignedTiny).numeric_value(),
Ok(N::UnsignedTiny(200))
));
assert!(matches!(
MysqlValue::new_internal(&(-1i16).to_ne_bytes(), MysqlType::Short).numeric_value(),
Ok(N::Small(-1))
));
assert!(matches!(
MysqlValue::new_internal(&40000u16.to_ne_bytes(), MysqlType::UnsignedShort).numeric_value(),
Ok(N::UnsignedSmall(40000))
));
assert!(matches!(
MysqlValue::new_internal(&(-1i32).to_ne_bytes(), MysqlType::Long).numeric_value(),
Ok(N::Medium(-1))
));
assert!(matches!(
MysqlValue::new_internal(&u32::MAX.to_ne_bytes(), MysqlType::UnsignedLong).numeric_value(),
Ok(N::UnsignedMedium(u32::MAX))
));
assert!(matches!(
MysqlValue::new_internal(&(-1i64).to_ne_bytes(), MysqlType::LongLong).numeric_value(),
Ok(N::Big(-1))
));
assert!(matches!(
MysqlValue::new_internal(&u64::MAX.to_ne_bytes(), MysqlType::UnsignedLongLong)
.numeric_value(),
Ok(N::UnsignedBig(u64::MAX))
));
}