diff --git a/Cargo.toml b/Cargo.toml index 7b3ba96..82d86dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [package] name = "bitcode" authors = [ "Cai Bear", "Finn Bear" ] -version = "0.6.9" +version = "0.7.0" edition = "2021" rust-version = "1.70" license = "MIT OR Apache-2.0" @@ -18,7 +18,9 @@ exclude = ["fuzz/"] arrayvec = { version = "0.7", default-features = false, optional = true } bitcode_derive = { version = "=0.6.9", path = "./bitcode_derive", optional = true } bytemuck = { version = "1.14", features = [ "min_const_generics", "must_cast" ] } +chrono = { version = ">=0.4", default-features = false, optional = true } glam = { version = ">=0.21", default-features = false, optional = true } +jiff = { version = ">=0.2.0", optional = true } rust_decimal = { version = "1.36", default-features = false, optional = true } serde = { version = "1.0", default-features = false, features = [ "alloc" ], optional = true } time = { version = "0.3", default-features = false, optional = true } @@ -29,7 +31,7 @@ arrayvec = { version = "0.7", features = [ "serde" ] } bincode = "1.3.3" flate2 = "1.0.28" glam = { version = "0.22", default-features = false, features = [ "rand" ] } -lz4_flex = { version = "=0.11.2", default-features = false } +lz4_flex = { version = "=0.13.0", default-features = false } paste = "1.0.14" rand = "0.8.5" rand_chacha = "0.3.1" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 72805f1..f421426 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,11 +10,14 @@ cargo-fuzz = true [dependencies] arrayvec = { version = "0.7", features = ["serde"] } -bitcode = { path = "..", features = [ "arrayvec", "rust_decimal", "serde", "time" ] } +bitcode = { path = "..", features = [ "arrayvec", "rust_decimal", "serde", "time", "chrono", "jiff" ] } libfuzzer-sys = "0.4" rust_decimal = "1.36.0" serde = { version ="1.0", features = [ "derive" ] } time = { version = "0.3", features = ["serde"]} +chrono = { version = "0.4.42", features = ["serde"] } +jiff = {version = "0.2.23", features = ["serde"]} + # Prevent this from interfering with workspaces [workspace] @@ -24,4 +27,4 @@ members = ["."] name = "fuzz" path = "fuzz_targets/fuzz.rs" test = false -doc = false \ No newline at end of file +doc = false diff --git a/fuzz/crash-fc97eeff55d6352bdad6f9d50b74bc7b33128a74 b/fuzz/crash-fc97eeff55d6352bdad6f9d50b74bc7b33128a74 new file mode 100644 index 0000000..ef5873a Binary files /dev/null and b/fuzz/crash-fc97eeff55d6352bdad6f9d50b74bc7b33128a74 differ diff --git a/fuzz/fuzz_targets/fuzz.rs b/fuzz/fuzz_targets/fuzz.rs index a9beda4..7acc957 100644 --- a/fuzz/fuzz_targets/fuzz.rs +++ b/fuzz/fuzz_targets/fuzz.rs @@ -3,14 +3,14 @@ use libfuzzer_sys::fuzz_target; extern crate bitcode; use arrayvec::{ArrayString, ArrayVec}; use bitcode::{Decode, DecodeOwned, Encode}; +use rust_decimal::Decimal; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::num::NonZeroU32; use std::time::Duration; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; -use rust_decimal::Decimal; #[inline(never)] fn test_derive(data: &[u8]) { @@ -148,10 +148,20 @@ fuzz_target!(|data: &[u8]| { A, B, C(u16), - D { a: u8, b: u8, #[serde(skip)] #[bitcode(skip)] c: u8 }, + D { + a: u8, + b: u8, + #[serde(skip)] + #[bitcode(skip)] + c: u8, + }, E(String), F, - G(#[bitcode(skip)] #[serde(skip)] i16), + G( + #[bitcode(skip)] + #[serde(skip)] + i16, + ), P(BTreeMap), } @@ -233,5 +243,13 @@ fuzz_target!(|data: &[u8]| { SocketAddrV6, SocketAddr, time::Time, + chrono::NaiveTime, + chrono::NaiveDate, + chrono::NaiveDateTime, + chrono::DateTime, + jiff::civil::Date, + jiff::civil::Time, + jiff::Timestamp, + jiff::Zoned, ); }); diff --git a/src/derive/mod.rs b/src/derive/mod.rs index 235d711..24af28f 100644 --- a/src/derive/mod.rs +++ b/src/derive/mod.rs @@ -10,6 +10,8 @@ pub(crate) mod convert; mod duration; mod empty; mod impls; +#[cfg(any(feature = "chrono", feature = "jiff"))] +pub(crate) mod try_convert; // TODO: When ip_in_core has been stable (https://github.com/rust-lang/rust/issues/108443) // for long enough, remove feature check. #[cfg(feature = "std")] diff --git a/src/derive/try_convert.rs b/src/derive/try_convert.rs new file mode 100644 index 0000000..64947a0 --- /dev/null +++ b/src/derive/try_convert.rs @@ -0,0 +1,81 @@ +use crate::{ + coder::{Decoder, View}, + derive::Decode, + fast::{CowSlice, PushUnchecked, SliceImpl, Unaligned}, +}; + +#[allow(unused)] +macro_rules! impl_try_convert { + ($want: path, $have: ty) => { + impl_try_convert!($want, $have, $have); + }; + ($want: path, $have_encode: ty, $have_decode: ty) => { + impl crate::derive::Encode for $want { + type Encoder = crate::derive::convert::ConvertIntoEncoder<$have_encode>; + } + impl<'a> crate::derive::Decode<'a> for $want { + type Decoder = + crate::derive::try_convert::TryConvertFromDecoder<'a, $have_decode, $want>; + } + }; +} + +#[allow(unused)] +pub(crate) use impl_try_convert; + +// Like [`TryFrom`] but we can implement it ourselves. +pub trait TryConvertFrom: Sized { + fn try_convert_from(value: T) -> Result; +} +/// Decodes a `T` and then converts it with [`TryConvertFrom`]. +pub struct TryConvertFromDecoder<'a, T: Decode<'a>, F: TryConvertFrom> { + data: CowSlice<'a, F>, + decoder: T::Decoder, +} + +// Can't derive since it would bound T: Default. +impl<'a, T: Decode<'a>, F: TryConvertFrom> Default for TryConvertFromDecoder<'a, T, F> { + fn default() -> Self { + Self { + data: CowSlice::with_allocation(Vec::new()), + decoder: Default::default(), + } + } +} + +impl<'a, T: Decode<'a>, F: TryConvertFrom> View<'a> for TryConvertFromDecoder<'a, T, F> { + fn populate(&mut self, input: &mut &'a [u8], length: usize) -> Result<(), crate::Error> { + self.decoder.populate(input, length)?; + + let out: &mut Vec = &mut self.data.set_owned(); + out.reserve(length); + + for _ in 0..length { + let value = F::try_convert_from(self.decoder.decode())?; + unsafe { out.push_unchecked(value) }; + } + + Ok(()) + } +} + +impl<'a, T: Decode<'a>, F: TryConvertFrom + Send + Sync> Decoder<'a, F> + for TryConvertFromDecoder<'a, T, F> +{ + #[inline(always)] + fn as_primitive(&mut self) -> Option<&mut SliceImpl<'_, Unaligned>> { + None + } + + #[inline(always)] + fn decode(&mut self) -> F { + let slice = self.data.mut_slice(); + let ptr = slice.as_ptr(); + unsafe { + let val = ptr.read(); + slice.advance(1); + + val + } + } +} diff --git a/src/ext/chrono.rs b/src/ext/chrono.rs new file mode 100644 index 0000000..29f7ff8 --- /dev/null +++ b/src/ext/chrono.rs @@ -0,0 +1,20 @@ +mod date_time_utc; +mod naive_date; +mod naive_date_time; +mod naive_time; + +use crate::int::ranged_int; + +ranged_int!(Hour, u8, 0, 23); +ranged_int!(Minute, u8, 0, 59); +ranged_int!(Second, u8, 0, 59); +ranged_int!(Nanosecond, u32, 0, 1_999_999_999); + +type TimeEncode = (u8, u8, u8, u32); +type TimeDecode = (Hour, Minute, Second, Nanosecond); + +type DateEncode = i32; +type DateDecode = i32; + +type DateTimeEncode = (DateEncode, TimeEncode); +type DateTimeDecode = (DateEncode, TimeDecode); diff --git a/src/ext/chrono/date_time_utc.rs b/src/ext/chrono/date_time_utc.rs new file mode 100644 index 0000000..c7394d2 --- /dev/null +++ b/src/ext/chrono/date_time_utc.rs @@ -0,0 +1,70 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; + +use crate::{ + convert::ConvertFrom, + ext::chrono::{DateTimeDecode, DateTimeEncode}, + try_convert::{impl_try_convert, TryConvertFrom}, +}; + +impl_try_convert!(DateTime, DateTimeEncode, DateTimeDecode); + +impl ConvertFrom<&DateTime> for DateTimeEncode { + fn convert_from(x: &DateTime) -> Self { + DateTimeEncode::convert_from(&x.naive_utc()) + } +} + +impl TryConvertFrom for DateTime { + fn try_convert_from(enc: DateTimeDecode) -> Result { + NaiveDateTime::try_convert_from(enc) + .map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc)) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use chrono::{DateTime, NaiveDate, Utc}; + + #[test] + fn test_chrono_datetime_utc() { + let ymds = [ + (1970, 1, 1), // epoch + (2025, 10, 6), + (1, 1, 1), + (-44, 3, 15), // BCE + (9999, 12, 31), + ]; + + for &(y, m, d) in ymds.iter() { + let naive = NaiveDate::from_ymd_opt(y, m, d) + .unwrap() + .and_hms_opt(12, 34, 56) + .unwrap(); + let dt_utc = DateTime::::from_naive_utc_and_offset(naive, Utc); + + let enc = crate::encode(&dt_utc); + let decoded: DateTime = crate::decode(&enc).unwrap(); + + assert_eq!(dt_utc, decoded, "failed for datetime {:?}", dt_utc); + } + } + + fn bench_data() -> Vec> { + crate::random_data(1000) + .into_iter() + .map( + |(y, m, d, h, mi, s, n, _offset_sec): (i32, u32, u32, u32, u32, u32, u32, i32)| { + let naive = + NaiveDate::from_ymd_opt((y % 9999).max(1), (m % 12).max(1), (d % 28) + 1) + .unwrap() + .and_hms_nano_opt(h % 24, mi % 60, s % 60, n % 1_000_000_000) + .unwrap(); + DateTime::::from_naive_utc_and_offset(naive, Utc) + }, + ) + .collect() + } + + crate::bench_encode_decode!(utc_vec: Vec>); +} diff --git a/src/ext/chrono/naive_date.rs b/src/ext/chrono/naive_date.rs new file mode 100644 index 0000000..9a164d1 --- /dev/null +++ b/src/ext/chrono/naive_date.rs @@ -0,0 +1,60 @@ +use chrono::{Datelike, NaiveDate}; + +use crate::{ + convert::ConvertFrom, + ext::chrono::{DateDecode, DateEncode}, + try_convert::{impl_try_convert, TryConvertFrom}, +}; + +impl_try_convert!(NaiveDate, DateEncode, DateDecode); + +impl ConvertFrom<&NaiveDate> for DateEncode { + fn convert_from(days: &NaiveDate) -> Self { + days.num_days_from_ce() + } +} + +impl TryConvertFrom for NaiveDate { + fn try_convert_from(days: DateDecode) -> Result { + NaiveDate::from_num_days_from_ce_opt(days) + .ok_or_else(|| crate::error::error("Failed to convert DateDecode to chrono::NaiveDate")) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_chrono_naive_date() { + let dates = [ + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), // epoch + NaiveDate::from_ymd_opt(2025, 10, 6).unwrap(), + NaiveDate::from_ymd_opt(1, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(-44, 3, 15).unwrap(), // BCE + NaiveDate::from_ymd_opt(-44, 3, 15).unwrap(), // BCE + NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), + ]; + + for x in dates { + let enc = crate::encode(&x); + let date: NaiveDate = crate::decode(&enc).unwrap(); + + assert_eq!(x, date, "failed for date {:?}", x); + } + } + + use alloc::vec::Vec; + use chrono::NaiveDate; + + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map(|(y, m, d): (i32, u32, u32)| { + let year = (y % 9999).max(1); // 1 ~ 9998 + let month = (m % 12).max(1); // 1 ~ 12 + let day = (d % 28) + 1; // 1 ~ 28 + NaiveDate::from_ymd_opt(year, month, day).unwrap() + }) + .collect() + } + crate::bench_encode_decode!(data: Vec<_>); +} diff --git a/src/ext/chrono/naive_date_time.rs b/src/ext/chrono/naive_date_time.rs new file mode 100644 index 0000000..960fe3a --- /dev/null +++ b/src/ext/chrono/naive_date_time.rs @@ -0,0 +1,89 @@ +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +use crate::{ + convert::ConvertFrom, + ext::chrono::{DateEncode, DateTimeDecode, DateTimeEncode, TimeEncode}, + try_convert::{impl_try_convert, TryConvertFrom}, +}; + +impl_try_convert!(NaiveDateTime, DateTimeEncode, DateTimeDecode); + +impl ConvertFrom<&NaiveDateTime> for DateTimeEncode { + #[inline(always)] + fn convert_from(x: &NaiveDateTime) -> Self { + ( + DateEncode::convert_from(&x.date()), + TimeEncode::convert_from(&x.time()), + ) + } +} + +impl TryConvertFrom for NaiveDateTime { + #[inline(always)] + fn try_convert_from((date, time): DateTimeDecode) -> Result { + let date = NaiveDate::try_convert_from(date)?; + let time = NaiveTime::try_convert_from(time)?; + + Ok(NaiveDateTime::new(date, time)) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + + use crate::decode; + use crate::encode; + + #[test] + fn test_chrono_naive_datetime() { + let dt = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 10, 6).unwrap(), + NaiveTime::from_hms_nano_opt(12, 34, 56, 123_456_789).unwrap(), + ); + + let encoded = encode(&dt); + let decoded: NaiveDateTime = decode(&encoded).unwrap(); + + assert_eq!(dt, decoded); + + let dt2 = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1, 1, 1).unwrap(), + NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap(), + ); + let encoded2 = encode(&dt2); + let decoded2: NaiveDateTime = decode(&encoded2).unwrap(); + assert_eq!(dt2, decoded2); + } + + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map( + |(y, m, d, h, min, s, n): (i32, u32, u32, u8, u8, u8, u32)| { + let year = (y % 9999).max(1); + let month = (m % 12).max(1); + let day = (d % 28) + 1; + let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); + + let hour = h % 24; + let minute = min % 60; + let second = s % 60; + let nano = n % 1_000_000_000; + let time = NaiveTime::from_hms_nano_opt( + hour as u32, + minute as u32, + second as u32, + nano, + ) + .unwrap(); + + NaiveDateTime::new(date, time) + }, + ) + .collect() + } + + crate::bench_encode_decode!(data_vec: Vec<_>); +} diff --git a/src/ext/chrono/naive_time.rs b/src/ext/chrono/naive_time.rs new file mode 100644 index 0000000..3ed1ade --- /dev/null +++ b/src/ext/chrono/naive_time.rs @@ -0,0 +1,81 @@ +use crate::{ + convert::ConvertFrom, + ext::chrono::{TimeDecode, TimeEncode}, + try_convert::{impl_try_convert, TryConvertFrom}, +}; +use chrono::{NaiveTime, Timelike}; + +impl_try_convert!(NaiveTime, TimeEncode, TimeDecode); + +impl ConvertFrom<&NaiveTime> for TimeEncode { + #[inline(always)] + fn convert_from(value: &NaiveTime) -> Self { + ( + value.hour() as u8, + value.minute() as u8, + value.second() as u8, + value.nanosecond(), + ) + } +} + +impl TryConvertFrom for NaiveTime { + #[inline(always)] + fn try_convert_from(value: TimeDecode) -> Result { + let (hour, min, sec, nano) = value; + + NaiveTime::from_hms_nano_opt( + hour.into_inner() as u32, + min.into_inner() as u32, + sec.into_inner() as u32, + nano.into_inner(), + ) + .ok_or_else(|| crate::error::error("Failed to convert TimeDecode to NaiveTime")) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_chrono_naive_time() { + assert!(crate::decode::(&crate::encode( + &NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap() + )) + .is_ok()); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 59u8, 999_999_999u32))).is_ok() + ); + assert!( + crate::decode::(&crate::encode(&(24u8, 59u8, 59u8, 999_999_999u32))) + .is_err() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 60u8, 59u8, 999_999_999u32))) + .is_err() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 60u8, 999_999_999u32))) + .is_err() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 59u8, 1_000_000_000u32))) + .is_ok() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 58u8, 1_000_000_000u32))) + .is_err() + ); + } + + use alloc::vec::Vec; + use chrono::NaiveTime; + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map(|(h, m, s, n): (u32, u32, u32, u32)| { + NaiveTime::from_hms_nano_opt(h % 24, m % 60, s % 60, n % 1_000_000_000).unwrap() + }) + .collect() + } + crate::bench_encode_decode!(duration_vec: Vec<_>); +} diff --git a/src/ext/jiff/date.rs b/src/ext/jiff/date.rs new file mode 100644 index 0000000..286364e --- /dev/null +++ b/src/ext/jiff/date.rs @@ -0,0 +1,117 @@ +use jiff::civil::Date; + +use crate::{ + convert::ConvertFrom, + error::error, + int::ranged_int, + try_convert::{impl_try_convert, TryConvertFrom}, +}; + +impl_try_convert!(Date, DateEncode, DateDecode); + +// The value is guaranteed to be in the range `-9999..=9999`. +ranged_int!(Year, i16, -9999, 9999); +// The value is guaranteed to be in the range `1..=12`. +ranged_int!(Month, u8, 1, 12); +// The value is guaranteed to be in the range `0..=59`. +ranged_int!(Day, u8, 1, 31); + +pub type DateEncode = (i16, u8, u8); +pub type DateDecode = (Year, Month, Day); + +impl ConvertFrom<&Date> for DateEncode { + fn convert_from(value: &Date) -> Self { + (value.year(), value.month() as u8, value.day() as u8) + } +} + +impl TryConvertFrom for Date { + fn try_convert_from(value: DateDecode) -> Result { + Date::new( + value.0.into_inner(), + value.1.into_inner() as i8, + value.2.into_inner() as i8, + ) + .map_err(|_| error("Failed to decode date")) + } +} + +#[cfg(test)] +mod tests { + use jiff::civil::Date; + + #[test] + fn test_date() { + // -9999-01-01 + let date = Date::new(-9999, 1, 1).unwrap(); + let bytes = crate::encode(&date); + let decoded: Date = crate::decode(&bytes).unwrap(); + assert_eq!(decoded, date); + + // 9999-12-30 + let date = Date::new(9999, 12, 30).unwrap(); + let bytes = crate::encode(&date); + let decoded: Date = crate::decode(&bytes).unwrap(); + assert_eq!(decoded, date); + + // 2025-03-28 + let date = Date::new(2025, 3, 28).unwrap(); + let bytes = crate::encode(&date); + let decoded: Date = crate::decode(&bytes).unwrap(); + assert_eq!(decoded, date); + + let date = Date::new(2025, 1, 15).unwrap(); + let bytes = crate::encode(&date); + let decoded: Date = crate::decode(&bytes).unwrap(); + assert_eq!(decoded, date); + + let date = Date::new(2025, 12, 15).unwrap(); + let bytes = crate::encode(&date); + let decoded: Date = crate::decode(&bytes).unwrap(); + assert_eq!(decoded, date); + + let date = Date::new(2025, 4, 30).unwrap(); + let bytes = crate::encode(&date); + let decoded: Date = crate::decode(&bytes).unwrap(); + assert_eq!(decoded, date); + + let bytes = crate::encode(&(-10000i16, 1u8, 1u8)); + let result: Result = crate::decode(&bytes); + assert!(result.is_err()); + + let bytes = crate::encode(&(10000i16, 1u8, 1u8)); + let result: Result = crate::decode(&bytes); + assert!(result.is_err()); + + let bytes = crate::encode(&(2025i16, 0u8, 1u8)); + let result: Result = crate::decode(&bytes); + assert!(result.is_err()); + + let bytes = crate::encode(&(2025i16, 13u8, 1u8)); + let result: Result = crate::decode(&bytes); + assert!(result.is_err()); + + let bytes = crate::encode(&(2025i16, 1u8, 0u8)); + let result: Result = crate::decode(&bytes); + assert!(result.is_err()); + + let date = Date::new(2025, 3, 28).unwrap(); + assert!(crate::decode::(&crate::encode(&date)).is_ok()); + } + + use alloc::vec::Vec; + + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map(|(year, month, day): (i16, i8, i8)| { + let year = year.clamp(-9999, 9999); + let month = month.clamp(1, 12); + let day = day.clamp(1, 28); + + Date::new(year, month, day).unwrap() + }) + .collect() + } + crate::bench_encode_decode!(date_vec: Vec<_>); +} diff --git a/src/ext/jiff/mod.rs b/src/ext/jiff/mod.rs new file mode 100644 index 0000000..3f0de44 --- /dev/null +++ b/src/ext/jiff/mod.rs @@ -0,0 +1,5 @@ +mod date; +mod offset; +mod time; +mod timestamp; +mod zoned; diff --git a/src/ext/jiff/offset.rs b/src/ext/jiff/offset.rs new file mode 100644 index 0000000..0a68e7e --- /dev/null +++ b/src/ext/jiff/offset.rs @@ -0,0 +1,89 @@ +use jiff::tz::Offset; + +use crate::{ + convert::ConvertFrom, + error::error, + int::ranged_int, + try_convert::{impl_try_convert, TryConvertFrom}, +}; + +impl_try_convert!(Offset, OffsetEncoder, OffsetDecoder); + +ranged_int!(OffsetDecoder, i32, -93599, 93599); + +pub(super) type OffsetEncoder = i32; + +impl ConvertFrom<&Offset> for OffsetEncoder { + fn convert_from(value: &Offset) -> Self { + value.seconds() + } +} + +impl TryConvertFrom for Offset { + fn try_convert_from(value: OffsetDecoder) -> Result { + Offset::from_seconds(value.into_inner()).map_err(|_| error("Failed to decode offset")) + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn test_offset() { + let offset = Offset::UTC; + let bytes = bitcode::encode(&offset); + let decoded: Offset = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, offset); + + let offset = Offset::from_seconds(93599).unwrap(); + let bytes = bitcode::encode(&offset); + let decoded: Offset = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, offset); + + let offset = Offset::from_seconds(-93599).unwrap(); + let bytes = bitcode::encode(&offset); + let decoded: Offset = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, offset); + + let offset = Offset::from_seconds(28800).unwrap(); + let bytes = bitcode::encode(&offset); + let decoded: Offset = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, offset); + + let offset = Offset::from_seconds(-21600).unwrap(); + let bytes = bitcode::encode(&offset); + let decoded: Offset = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, offset); + + let bytes = bitcode::encode(&93600); + let result: Result = bitcode::decode(&bytes); + assert!(result.is_err()); + + let bytes = bitcode::encode(&-93600); + let result: Result = bitcode::decode(&bytes); + assert!(result.is_err()); + + assert!(crate::decode::(&crate::encode(&Offset::UTC)).is_ok()); + } + + use alloc::vec::Vec; + use jiff::tz::Offset; + + fn offset_min() -> i32 { + Offset::MIN.seconds() + } + fn offset_max() -> i32 { + Offset::MAX.seconds() + } + + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map(|secs: i32| { + let secs = secs.clamp(offset_min(), offset_max()); + Offset::from_seconds(secs).unwrap() + }) + .collect() + } + crate::bench_encode_decode!(offset_vec: Vec<_>); +} diff --git a/src/ext/jiff/time.rs b/src/ext/jiff/time.rs new file mode 100644 index 0000000..84cb8fd --- /dev/null +++ b/src/ext/jiff/time.rs @@ -0,0 +1,108 @@ +use jiff::civil::Time; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + int::ranged_int, +}; + +impl_convert!(Time, TimeEncode, TimeDecode); + +// The value is guaranteed to be in the range `0..=23`. +ranged_int!(Hour, u8, 0, 23); +// The value is guaranteed to be in the range `0..=59`. +ranged_int!(Minute, u8, 0, 59); +// The value is guaranteed to be in the range `0..=59`. +ranged_int!(Second, u8, 0, 59); +// The value is guaranteed to be in the range `0..=999_999_999` +ranged_int!(Nanosecond, u32, 0, 999_999_999); + +type TimeEncode = (u8, u8, u8, u32); +type TimeDecode = (Hour, Minute, Second, Nanosecond); + +impl ConvertFrom<&Time> for TimeEncode { + fn convert_from(value: &Time) -> Self { + ( + value.hour() as u8, + value.minute() as u8, + value.second() as u8, + value.subsec_nanosecond() as u32, + ) + } +} + +impl ConvertFrom for Time { + fn convert_from(value: TimeDecode) -> Self { + Time::constant( + value.0.into_inner() as i8, + value.1.into_inner() as i8, + value.2.into_inner() as i8, + value.3.into_inner() as i32, + ) + } +} +#[cfg(test)] +mod tests { + #[test] + fn test_time() { + // 00:00:00.000000000 + let time = Time::constant(0, 0, 0, 0); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + // 23:59:59.999999999 + let time = Time::constant(23, 59, 59, 999_999_999); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + // 23:00:00 + let time = Time::constant(23, 0, 0, 0); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + // 00:59:00 + let time = Time::constant(0, 59, 0, 0); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + // 00:00:59 + let time = Time::constant(0, 0, 59, 0); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + // 12:30:45.123456789 + let time = Time::constant(12, 30, 45, 123456789); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + let time = Time::default(); + let bytes = bitcode::encode(&time); + let decoded: Time = bitcode::decode(&bytes).unwrap(); + assert_eq!(decoded, time); + + assert!(crate::decode::