diff --git a/Cargo.toml b/Cargo.toml index 7b3ba96..1b8f98c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ 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 } rust_decimal = { version = "1.36", default-features = false, optional = true } serde = { version = "1.0", default-features = false, features = [ "alloc" ], optional = true } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 72805f1..0beb14b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,11 +10,13 @@ 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"] } 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"] } + # Prevent this from interfering with workspaces [workspace] @@ -24,4 +26,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..4e3545d 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,9 @@ fuzz_target!(|data: &[u8]| { SocketAddrV6, SocketAddr, time::Time, + chrono::NaiveTime, + chrono::NaiveDate, + chrono::NaiveDateTime, + chrono::DateTime, ); }); 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..6c31c46 --- /dev/null +++ b/src/ext/chrono/date_time_utc.rs @@ -0,0 +1,70 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::chrono::{DateTimeDecode, DateTimeEncode}, +}; + +impl_convert!(DateTime, DateTimeEncode, DateTimeDecode); + +impl ConvertFrom<&DateTime> for DateTimeEncode { + fn convert_from(x: &DateTime) -> Self { + DateTimeEncode::convert_from(&x.naive_utc()) + } +} + +impl ConvertFrom for DateTime { + fn convert_from(enc: DateTimeDecode) -> Self { + let naive = NaiveDateTime::convert_from(enc); + + 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..f87f444 --- /dev/null +++ b/src/ext/chrono/naive_date.rs @@ -0,0 +1,58 @@ +use chrono::{Datelike, NaiveDate}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::chrono::{DateDecode, DateEncode}, +}; + +impl_convert!(NaiveDate, DateEncode, DateDecode); + +impl ConvertFrom<&NaiveDate> for DateEncode { + fn convert_from(days: &NaiveDate) -> Self { + days.num_days_from_ce() + } +} + +impl ConvertFrom for NaiveDate { + fn convert_from(days: DateDecode) -> Self { + NaiveDate::from_num_days_from_ce_opt(days).unwrap() + } +} + +#[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..d5c5f06 --- /dev/null +++ b/src/ext/chrono/naive_date_time.rs @@ -0,0 +1,85 @@ +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::chrono::{DateEncode, DateTimeDecode, DateTimeEncode, TimeEncode}, +}; + +impl_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 ConvertFrom for NaiveDateTime { + #[inline(always)] + fn convert_from((date, time): DateTimeDecode) -> Self { + NaiveDateTime::new(NaiveDate::convert_from(date), NaiveTime::convert_from(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..20ab425 --- /dev/null +++ b/src/ext/chrono/naive_time.rs @@ -0,0 +1,77 @@ +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::chrono::{TimeDecode, TimeEncode}, +}; +use chrono::{NaiveTime, Timelike}; + +impl_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 ConvertFrom for NaiveTime { + #[inline(always)] + fn convert_from(value: TimeDecode) -> Self { + 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(), + ) + .unwrap() + } +} + +#[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_err() + ); + } + + use alloc::vec::Vec; + use chrono::NaiveTime; + use time::Time; + fn bench_data() -> Vec