diff --git a/Cargo.lock b/Cargo.lock index abfac8f..a9c55c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,8 +40,10 @@ dependencies = [ "bincode", "bitcode_derive", "bytemuck", + "chrono", "flate2", "glam", + "jiff", "lz4_flex", "paste", "rand", @@ -86,6 +88,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -152,6 +163,28 @@ dependencies = [ "rand", ] +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -211,6 +244,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 46cc43e..30b804d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", default-features = false, 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 } 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..37d0f84 --- /dev/null +++ b/src/derive/try_convert.rs @@ -0,0 +1,74 @@ +use crate::{ + coder::{Decoder, View}, + derive::Decode, + fast::{CowSlice, NextUnchecked, 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 + Copy> 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 { + unsafe { self.data.mut_slice().next_unchecked() } + } +} diff --git a/src/ext/chrono/date_time_utc.rs b/src/ext/chrono/date_time_utc.rs new file mode 100644 index 0000000..15f14cb --- /dev/null +++ b/src/ext/chrono/date_time_utc.rs @@ -0,0 +1,67 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; + +use crate::convert::{impl_convert, ConvertFrom}; + +impl_convert!(DateTime, NaiveDateTime, NaiveDateTime); + +impl ConvertFrom<&DateTime> for NaiveDateTime { + #[inline(always)] + fn convert_from(x: &DateTime) -> Self { + x.naive_utc() + } +} + +impl ConvertFrom for DateTime { + #[inline(always)] + fn convert_from(enc: NaiveDateTime) -> Self { + DateTime::from_naive_utc_and_offset(enc, 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/mod.rs b/src/ext/chrono/mod.rs new file mode 100644 index 0000000..4b06313 --- /dev/null +++ b/src/ext/chrono/mod.rs @@ -0,0 +1,4 @@ +mod date_time_utc; +mod naive_date; +mod naive_date_time; +mod naive_time; diff --git a/src/ext/chrono/naive_date.rs b/src/ext/chrono/naive_date.rs new file mode 100644 index 0000000..58a4a15 --- /dev/null +++ b/src/ext/chrono/naive_date.rs @@ -0,0 +1,64 @@ +use chrono::{Datelike, NaiveDate}; + +use crate::{ + convert::ConvertFrom, + try_convert::{impl_try_convert, TryConvertFrom}, +}; + +// Number of days since the CE epoch(0001-01-01). +type NaiveDateCoder = i32; + +impl_try_convert!(NaiveDate, NaiveDateCoder, NaiveDateCoder); + +impl ConvertFrom<&NaiveDate> for NaiveDateCoder { + #[inline(always)] + fn convert_from(days: &NaiveDate) -> Self { + days.num_days_from_ce() + } +} + +impl TryConvertFrom for NaiveDate { + #[inline(always)] + fn try_convert_from(days: NaiveDateCoder) -> 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..aeb4f6f --- /dev/null +++ b/src/ext/chrono/naive_date_time.rs @@ -0,0 +1,81 @@ +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +use crate::convert::{impl_convert, ConvertFrom}; + +type NaiveDateTimeCoder = (NaiveDate, NaiveTime); + +impl_convert!(NaiveDateTime, NaiveDateTimeCoder, NaiveDateTimeCoder); + +impl ConvertFrom<&NaiveDateTime> for NaiveDateTimeCoder { + #[inline(always)] + fn convert_from(x: &NaiveDateTime) -> Self { + (x.date(), x.time()) + } +} + +impl ConvertFrom for NaiveDateTime { + #[inline(always)] + fn convert_from(value: (NaiveDate, NaiveTime)) -> Self { + NaiveDateTime::new(value.0, value.1) + } +} + +#[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..901d9df --- /dev/null +++ b/src/ext/chrono/naive_time.rs @@ -0,0 +1,89 @@ +use crate::{ + convert::ConvertFrom, + int::ranged_int, + try_convert::{impl_try_convert, TryConvertFrom}, +}; +use chrono::{NaiveTime, Timelike}; + +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); + +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..0c3c9f3 --- /dev/null +++ b/src/ext/jiff/date.rs @@ -0,0 +1,119 @@ +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 `1..=31`. +ranged_int!(Day, u8, 1, 31); + +pub type DateEncode = (i16, u8, u8); +pub type DateDecode = (Year, Month, Day); + +impl ConvertFrom<&Date> for DateEncode { + #[inline(always)] + fn convert_from(value: &Date) -> Self { + (value.year(), value.month() as u8, value.day() as u8) + } +} + +impl TryConvertFrom for Date { + #[inline(always)] + 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..e325507 --- /dev/null +++ b/src/ext/jiff/offset.rs @@ -0,0 +1,91 @@ +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 { + #[inline(always)] + fn convert_from(value: &Offset) -> Self { + value.seconds() + } +} + +impl TryConvertFrom for Offset { + #[inline(always)] + 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..5cf7182 --- /dev/null +++ b/src/ext/jiff/time.rs @@ -0,0 +1,110 @@ +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 { + #[inline(always)] + 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 { + #[inline(always)] + 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::