1use crate::cp437::FromCp437;
3use crate::write::FileOptionExtension;
4use crate::zipcrypto::EncryptWith;
5use core::fmt::{self, Debug, Formatter};
6use core::mem;
7use std::ffi::OsStr;
8use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
9use std::sync::{Arc, OnceLock};
10
11#[cfg(feature = "chrono")]
12use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
13#[cfg(feature = "jiff-02")]
14use jiff::civil;
15
16use crate::result::{invalid, ZipError, ZipResult};
17use crate::spec::{self, FixedSizeBlock, Pod};
18
19pub(crate) mod ffi {
20 pub const S_IFDIR: u32 = 0o0040000;
21 pub const S_IFREG: u32 = 0o0100000;
22 pub const S_IFLNK: u32 = 0o0120000;
23}
24
25use crate::extra_fields::ExtraField;
26use crate::read::find_data_start;
27use crate::result::DateTimeRangeError;
28use crate::spec::is_dir;
29use crate::types::ffi::S_IFDIR;
30use crate::{CompressionMethod, ZIP64_BYTES_THR};
31use std::io::{Read, Seek};
32#[cfg(feature = "time")]
33use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
34
35pub(crate) struct ZipRawValues {
36 pub(crate) crc32: u32,
37 pub(crate) compressed_size: u64,
38 pub(crate) uncompressed_size: u64,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
42#[repr(u8)]
43pub enum System {
44 Dos = 0,
45 Unix = 3,
46 #[default]
47 Unknown,
48}
49
50impl From<u8> for System {
51 fn from(system: u8) -> Self {
52 match system {
53 0 => Self::Dos,
54 3 => Self::Unix,
55 _ => Self::Unknown,
56 }
57 }
58}
59
60impl From<System> for u8 {
61 fn from(system: System) -> Self {
62 match system {
63 System::Dos => 0,
64 System::Unix => 3,
65 System::Unknown => 4,
66 }
67 }
68}
69
70#[derive(Clone, Debug, Copy, Eq, PartialEq)]
72pub struct FileOptions<'k, T: FileOptionExtension> {
73 pub(crate) compression_method: CompressionMethod,
74 pub(crate) compression_level: Option<i64>,
75 pub(crate) last_modified_time: DateTime,
76 pub(crate) permissions: Option<u32>,
77 pub(crate) large_file: bool,
78 pub(crate) encrypt_with: Option<EncryptWith<'k>>,
79 pub(crate) extended_options: T,
80 pub(crate) alignment: u16,
81 #[cfg(feature = "deflate-zopfli")]
82 pub(super) zopfli_buffer_size: Option<usize>,
83 #[cfg(feature = "aes-crypto")]
84 pub(crate) aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
85}
86pub type SimpleFileOptions = FileOptions<'static, ()>;
88
89#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct DateTime {
107 datepart: u16,
108 timepart: u16,
109}
110
111impl Debug for DateTime {
112 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113 if *self == Self::default() {
114 return f.write_str("DateTime::default()");
115 }
116 f.write_fmt(format_args!(
117 "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?",
118 self.year(),
119 self.month(),
120 self.day(),
121 self.hour(),
122 self.minute(),
123 self.second()
124 ))
125 }
126}
127
128impl DateTime {
129 pub const DEFAULT: Self = DateTime {
131 datepart: 0b0000000000100001,
132 timepart: 0,
133 };
134
135 #[cfg(feature = "time")]
137 pub fn default_for_write() -> Self {
138 let now = OffsetDateTime::now_utc();
139 PrimitiveDateTime::new(now.date(), now.time())
140 .try_into()
141 .unwrap_or_else(|_| DateTime::default())
142 }
143
144 #[cfg(not(feature = "time"))]
146 pub fn default_for_write() -> Self {
147 DateTime::default()
148 }
149}
150
151#[cfg(feature = "_arbitrary")]
152impl arbitrary::Arbitrary<'_> for DateTime {
153 fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Self> {
154 let year: u16 = u.int_in_range(1980..=2107)?;
155 let month: u16 = u.int_in_range(1..=12)?;
156 let day: u16 = u.int_in_range(1..=31)?;
157 let datepart = day | (month << 5) | ((year - 1980) << 9);
158 let hour: u16 = u.int_in_range(0..=23)?;
159 let minute: u16 = u.int_in_range(0..=59)?;
160 let second: u16 = u.int_in_range(0..=58)?;
161 let timepart = (second >> 1) | (minute << 5) | (hour << 11);
162 Ok(DateTime { datepart, timepart })
163 }
164}
165
166#[cfg(feature = "chrono")]
167impl TryFrom<NaiveDateTime> for DateTime {
168 type Error = DateTimeRangeError;
169
170 fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
171 DateTime::from_date_and_time(
172 value.year().try_into()?,
173 value.month().try_into()?,
174 value.day().try_into()?,
175 value.hour().try_into()?,
176 value.minute().try_into()?,
177 value.second().try_into()?,
178 )
179 }
180}
181
182#[cfg(feature = "chrono")]
183impl TryFrom<DateTime> for NaiveDateTime {
184 type Error = DateTimeRangeError;
185
186 fn try_from(value: DateTime) -> Result<Self, Self::Error> {
187 let date = NaiveDate::from_ymd_opt(
188 value.year().into(),
189 value.month().into(),
190 value.day().into(),
191 )
192 .ok_or(DateTimeRangeError)?;
193 let time = NaiveTime::from_hms_opt(
194 value.hour().into(),
195 value.minute().into(),
196 value.second().into(),
197 )
198 .ok_or(DateTimeRangeError)?;
199 Ok(NaiveDateTime::new(date, time))
200 }
201}
202
203#[cfg(feature = "jiff-02")]
204impl TryFrom<civil::DateTime> for DateTime {
205 type Error = DateTimeRangeError;
206
207 fn try_from(value: civil::DateTime) -> Result<Self, Self::Error> {
208 Self::from_date_and_time(
209 value.year().try_into()?,
210 value.month() as u8,
211 value.day() as u8,
212 value.hour() as u8,
213 value.minute() as u8,
214 value.second() as u8,
215 )
216 }
217}
218
219#[cfg(feature = "jiff-02")]
220impl TryFrom<DateTime> for civil::DateTime {
221 type Error = jiff::Error;
222
223 fn try_from(value: DateTime) -> Result<Self, Self::Error> {
224 Self::new(
225 value.year() as i16,
226 value.month() as i8,
227 value.day() as i8,
228 value.hour() as i8,
229 value.minute() as i8,
230 value.second() as i8,
231 0,
232 )
233 }
234}
235
236impl TryFrom<(u16, u16)> for DateTime {
237 type Error = DateTimeRangeError;
238
239 #[inline]
240 fn try_from(values: (u16, u16)) -> Result<Self, Self::Error> {
241 Self::try_from_msdos(values.0, values.1)
242 }
243}
244
245impl From<DateTime> for (u16, u16) {
246 #[inline]
247 fn from(dt: DateTime) -> Self {
248 (dt.datepart(), dt.timepart())
249 }
250}
251
252impl Default for DateTime {
253 fn default() -> DateTime {
255 DateTime::DEFAULT
256 }
257}
258
259impl fmt::Display for DateTime {
260 #[inline]
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 write!(
263 f,
264 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
265 self.year(),
266 self.month(),
267 self.day(),
268 self.hour(),
269 self.minute(),
270 self.second()
271 )
272 }
273}
274
275impl DateTime {
276 pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime {
281 DateTime { datepart, timepart }
282 }
283
284 pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result<DateTime, DateTimeRangeError> {
287 let seconds = (timepart & 0b0000000000011111) << 1;
288 let minutes = (timepart & 0b0000011111100000) >> 5;
289 let hours = (timepart & 0b1111100000000000) >> 11;
290 let days = datepart & 0b0000000000011111;
291 let months = (datepart & 0b0000000111100000) >> 5;
292 let years = (datepart & 0b1111111000000000) >> 9;
293 Self::from_date_and_time(
294 years.checked_add(1980).ok_or(DateTimeRangeError)?,
295 months.try_into()?,
296 days.try_into()?,
297 hours.try_into()?,
298 minutes.try_into()?,
299 seconds.try_into()?,
300 )
301 }
302
303 pub fn from_date_and_time(
313 year: u16,
314 month: u8,
315 day: u8,
316 hour: u8,
317 minute: u8,
318 second: u8,
319 ) -> Result<DateTime, DateTimeRangeError> {
320 fn is_leap_year(year: u16) -> bool {
321 (year % 4 == 0) && ((year % 25 != 0) || (year % 16 == 0))
322 }
323
324 if (1980..=2107).contains(&year)
325 && (1..=12).contains(&month)
326 && (1..=31).contains(&day)
327 && hour <= 23
328 && minute <= 59
329 && second <= 60
330 {
331 let second = second.min(58); let max_day = match month {
333 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
334 4 | 6 | 9 | 11 => 30,
335 2 if is_leap_year(year) => 29,
336 2 => 28,
337 _ => unreachable!(),
338 };
339 if day > max_day {
340 return Err(DateTimeRangeError);
341 }
342 let datepart = (day as u16) | ((month as u16) << 5) | ((year - 1980) << 9);
343 let timepart = ((second as u16) >> 1) | ((minute as u16) << 5) | ((hour as u16) << 11);
344 Ok(DateTime { datepart, timepart })
345 } else {
346 Err(DateTimeRangeError)
347 }
348 }
349
350 pub fn is_valid(&self) -> bool {
352 Self::try_from_msdos(self.datepart, self.timepart).is_ok()
353 }
354
355 #[cfg(feature = "time")]
356 #[deprecated(since = "0.6.4", note = "use `DateTime::try_from()` instead")]
360 pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, DateTimeRangeError> {
361 dt.try_into()
362 }
363
364 pub const fn timepart(&self) -> u16 {
366 self.timepart
367 }
368
369 pub const fn datepart(&self) -> u16 {
371 self.datepart
372 }
373
374 #[cfg(feature = "time")]
375 #[deprecated(since = "1.3.1", note = "use `OffsetDateTime::try_from()` instead")]
377 pub fn to_time(&self) -> Result<OffsetDateTime, ComponentRange> {
378 (*self).try_into()
379 }
380
381 pub const fn year(&self) -> u16 {
383 (self.datepart >> 9) + 1980
384 }
385
386 pub const fn month(&self) -> u8 {
392 ((self.datepart & 0b0000000111100000) >> 5) as u8
393 }
394
395 pub const fn day(&self) -> u8 {
401 (self.datepart & 0b0000000000011111) as u8
402 }
403
404 pub const fn hour(&self) -> u8 {
410 (self.timepart >> 11) as u8
411 }
412
413 pub const fn minute(&self) -> u8 {
419 ((self.timepart & 0b0000011111100000) >> 5) as u8
420 }
421
422 pub const fn second(&self) -> u8 {
428 ((self.timepart & 0b0000000000011111) << 1) as u8
429 }
430}
431
432#[cfg(feature = "time")]
433impl TryFrom<OffsetDateTime> for DateTime {
434 type Error = DateTimeRangeError;
435
436 fn try_from(dt: OffsetDateTime) -> Result<Self, Self::Error> {
437 Self::try_from(PrimitiveDateTime::new(dt.date(), dt.time()))
438 }
439}
440
441#[cfg(feature = "time")]
442impl TryFrom<PrimitiveDateTime> for DateTime {
443 type Error = DateTimeRangeError;
444
445 fn try_from(dt: PrimitiveDateTime) -> Result<Self, Self::Error> {
446 Self::from_date_and_time(
447 dt.year().try_into()?,
448 dt.month().into(),
449 dt.day(),
450 dt.hour(),
451 dt.minute(),
452 dt.second(),
453 )
454 }
455}
456
457#[cfg(feature = "time")]
458impl TryFrom<DateTime> for OffsetDateTime {
459 type Error = ComponentRange;
460
461 fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
462 PrimitiveDateTime::try_from(dt).map(PrimitiveDateTime::assume_utc)
463 }
464}
465
466#[cfg(feature = "time")]
467impl TryFrom<DateTime> for PrimitiveDateTime {
468 type Error = ComponentRange;
469
470 fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
471 let date =
472 Date::from_calendar_date(dt.year() as i32, Month::try_from(dt.month())?, dt.day())?;
473 let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?;
474 Ok(PrimitiveDateTime::new(date, time))
475 }
476}
477
478pub const MIN_VERSION: u8 = 10;
479pub const DEFAULT_VERSION: u8 = 45;
480
481#[derive(Debug, Clone, Default)]
483pub struct ZipFileData {
484 pub system: System,
486 pub version_made_by: u8,
488 pub flags: u16,
490 pub encrypted: bool,
492 pub is_utf8: bool,
494 pub using_data_descriptor: bool,
496 pub compression_method: crate::compression::CompressionMethod,
498 pub compression_level: Option<i64>,
500 pub last_modified_time: Option<DateTime>,
502 pub crc32: u32,
504 pub compressed_size: u64,
506 pub uncompressed_size: u64,
508 pub file_name: Box<str>,
510 pub file_name_raw: Box<[u8]>,
512 pub extra_field: Option<Arc<Vec<u8>>>,
514 pub central_extra_field: Option<Arc<Vec<u8>>>,
516 pub file_comment: Box<str>,
518 pub header_start: u64,
520 pub extra_data_start: Option<u64>,
522 pub central_header_start: u64,
526 pub data_start: OnceLock<u64>,
528 pub external_attributes: u32,
530 pub large_file: bool,
532 pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
534 pub aes_extra_data_start: u64,
536
537 pub extra_fields: Vec<ExtraField>,
539}
540
541impl ZipFileData {
542 pub fn data_start(&self, reader: &mut (impl Read + Seek + Sized)) -> ZipResult<u64> {
544 match self.data_start.get() {
545 Some(data_start) => Ok(*data_start),
546 None => Ok(find_data_start(self, reader)?),
547 }
548 }
549
550 #[allow(dead_code)]
551 pub fn is_dir(&self) -> bool {
552 is_dir(&self.file_name)
553 }
554
555 pub fn file_name_sanitized(&self) -> PathBuf {
556 let no_null_filename = match self.file_name.find('\0') {
557 Some(index) => &self.file_name[0..index],
558 None => &self.file_name,
559 }
560 .to_string();
561
562 let separator = MAIN_SEPARATOR;
566 let opposite_separator = match separator {
567 '/' => '\\',
568 _ => '/',
569 };
570 let filename =
571 no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string());
572
573 Path::new(&filename)
574 .components()
575 .filter(|component| matches!(*component, Component::Normal(..)))
576 .fold(PathBuf::new(), |mut path, ref cur| {
577 path.push(cur.as_os_str());
578 path
579 })
580 }
581
582 pub(crate) fn simplified_components(&self) -> Option<Vec<&OsStr>> {
584 if self.file_name.contains('\0') {
585 return None;
586 }
587 let input = Path::new(OsStr::new(&*self.file_name));
588 crate::path::simplified_components(input)
589 }
590
591 pub(crate) fn enclosed_name(&self) -> Option<PathBuf> {
592 if self.file_name.contains('\0') {
593 return None;
594 }
595 let path = PathBuf::from(self.file_name.to_string());
596 let mut depth = 0usize;
597 for component in path.components() {
598 match component {
599 Component::Prefix(_) | Component::RootDir => return None,
600 Component::ParentDir => depth = depth.checked_sub(1)?,
601 Component::Normal(_) => depth += 1,
602 Component::CurDir => (),
603 }
604 }
605 Some(path)
606 }
607
608 pub(crate) const fn unix_mode(&self) -> Option<u32> {
610 if self.external_attributes == 0 {
611 return None;
612 }
613
614 match self.system {
615 System::Unix => Some(self.external_attributes >> 16),
616 System::Dos => {
617 let mut mode = if 0x10 == (self.external_attributes & 0x10) {
619 ffi::S_IFDIR | 0o0775
620 } else {
621 ffi::S_IFREG | 0o0664
622 };
623 if 0x01 == (self.external_attributes & 0x01) {
624 mode &= 0o0555;
626 }
627 Some(mode)
628 }
629 _ => None,
630 }
631 }
632
633 pub fn version_needed(&self) -> u16 {
635 let compression_version: u16 = match self.compression_method {
636 CompressionMethod::Stored => MIN_VERSION.into(),
637 #[cfg(feature = "_deflate-any")]
638 CompressionMethod::Deflated => 20,
639 #[cfg(feature = "bzip2")]
640 CompressionMethod::Bzip2 => 46,
641 #[cfg(feature = "deflate64")]
642 CompressionMethod::Deflate64 => 21,
643 #[cfg(feature = "lzma")]
644 CompressionMethod::Lzma => 63,
645 #[cfg(feature = "xz")]
646 CompressionMethod::Xz => 63,
647 _ => DEFAULT_VERSION as u16,
649 };
650 let crypto_version: u16 = if self.aes_mode.is_some() {
651 51
652 } else if self.encrypted {
653 20
654 } else {
655 10
656 };
657 let misc_feature_version: u16 = if self.large_file {
658 45
659 } else if self
660 .unix_mode()
661 .is_some_and(|mode| mode & S_IFDIR == S_IFDIR)
662 {
663 20
665 } else {
666 10
667 };
668 compression_version
669 .max(crypto_version)
670 .max(misc_feature_version)
671 }
672 #[inline(always)]
673 pub(crate) fn extra_field_len(&self) -> usize {
674 self.extra_field
675 .as_ref()
676 .map(|v| v.len())
677 .unwrap_or_default()
678 }
679 #[inline(always)]
680 pub(crate) fn central_extra_field_len(&self) -> usize {
681 self.central_extra_field
682 .as_ref()
683 .map(|v| v.len())
684 .unwrap_or_default()
685 }
686
687 #[allow(clippy::too_many_arguments)]
688 pub(crate) fn initialize_local_block<S, T: FileOptionExtension>(
689 name: S,
690 options: &FileOptions<T>,
691 raw_values: ZipRawValues,
692 header_start: u64,
693 extra_data_start: Option<u64>,
694 aes_extra_data_start: u64,
695 compression_method: crate::compression::CompressionMethod,
696 aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
697 extra_field: &[u8],
698 ) -> Self
699 where
700 S: ToString,
701 {
702 let permissions = options.permissions.unwrap_or(0o100644);
703 let file_name: Box<str> = name.to_string().into_boxed_str();
704 let file_name_raw: Box<[u8]> = file_name.bytes().collect();
705 let mut local_block = ZipFileData {
706 system: System::Unix,
707 version_made_by: DEFAULT_VERSION,
708 flags: 0,
709 encrypted: options.encrypt_with.is_some() || {
710 #[cfg(feature = "aes-crypto")]
711 {
712 options.aes_mode.is_some()
713 }
714 #[cfg(not(feature = "aes-crypto"))]
715 {
716 false
717 }
718 },
719 using_data_descriptor: false,
720 is_utf8: !file_name.is_ascii(),
721 compression_method,
722 compression_level: options.compression_level,
723 last_modified_time: Some(options.last_modified_time),
724 crc32: raw_values.crc32,
725 compressed_size: raw_values.compressed_size,
726 uncompressed_size: raw_values.uncompressed_size,
727 file_name, file_name_raw,
729 extra_field: Some(extra_field.to_vec().into()),
730 central_extra_field: options.extended_options.central_extra_data().cloned(),
731 file_comment: String::with_capacity(0).into_boxed_str(),
732 header_start,
733 data_start: OnceLock::new(),
734 central_header_start: 0,
735 external_attributes: permissions << 16,
736 large_file: options.large_file,
737 aes_mode,
738 extra_fields: Vec::new(),
739 extra_data_start,
740 aes_extra_data_start,
741 };
742 local_block.version_made_by = local_block.version_needed() as u8;
743 local_block
744 }
745
746 pub(crate) fn from_local_block<R: std::io::Read>(
747 block: ZipLocalEntryBlock,
748 reader: &mut R,
749 ) -> ZipResult<Self> {
750 let ZipLocalEntryBlock {
751 version_made_by,
753 flags,
754 compression_method,
755 last_mod_time,
756 last_mod_date,
757 crc32,
758 compressed_size,
759 uncompressed_size,
760 file_name_length,
761 extra_field_length,
762 ..
763 } = block;
764
765 let encrypted: bool = flags & 1 == 1;
766 if encrypted {
767 return Err(ZipError::UnsupportedArchive(
768 "Encrypted files are not supported",
769 ));
770 }
771
772 let using_data_descriptor: bool = flags & (1 << 3) == 1 << 3;
775 if using_data_descriptor {
776 return Err(ZipError::UnsupportedArchive(
777 "The file length is not available in the local header",
778 ));
779 }
780
781 let is_utf8: bool = flags & (1 << 11) != 0;
783 let compression_method = crate::CompressionMethod::parse_from_u16(compression_method);
784 let file_name_length: usize = file_name_length.into();
785 let extra_field_length: usize = extra_field_length.into();
786
787 let mut file_name_raw = vec![0u8; file_name_length];
788 reader.read_exact(&mut file_name_raw)?;
789 let mut extra_field = vec![0u8; extra_field_length];
790 reader.read_exact(&mut extra_field)?;
791
792 let file_name: Box<str> = match is_utf8 {
793 true => String::from_utf8_lossy(&file_name_raw).into(),
794 false => file_name_raw.clone().from_cp437().into(),
795 };
796
797 let system: u8 = (version_made_by >> 8).try_into().unwrap();
798 Ok(ZipFileData {
799 system: System::from(system),
800 version_made_by: version_made_by as u8,
802 flags,
803 encrypted,
804 using_data_descriptor,
805 is_utf8,
806 compression_method,
807 compression_level: None,
808 last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
809 crc32,
810 compressed_size: compressed_size.into(),
811 uncompressed_size: uncompressed_size.into(),
812 file_name,
813 file_name_raw: file_name_raw.into(),
814 extra_field: Some(Arc::new(extra_field)),
815 central_extra_field: None,
816 file_comment: String::with_capacity(0).into_boxed_str(), header_start: 0,
820 data_start: OnceLock::new(),
821 central_header_start: 0,
822 external_attributes: 0,
826 large_file: false,
827 aes_mode: None,
828 extra_fields: Vec::new(),
829 extra_data_start: None,
830 aes_extra_data_start: 0,
831 })
832 }
833
834 fn is_utf8(&self) -> bool {
835 std::str::from_utf8(&self.file_name_raw).is_ok()
836 }
837
838 fn is_ascii(&self) -> bool {
839 self.file_name_raw.is_ascii()
840 }
841
842 fn flags(&self) -> u16 {
843 let utf8_bit: u16 = if self.is_utf8() && !self.is_ascii() {
844 1u16 << 11
845 } else {
846 0
847 };
848
849 let using_data_descriptor_bit = if self.using_data_descriptor {
850 1u16 << 3
851 } else {
852 0
853 };
854
855 let encrypted_bit: u16 = if self.encrypted { 1u16 << 0 } else { 0 };
856
857 utf8_bit | using_data_descriptor_bit | encrypted_bit
858 }
859
860 fn clamp_size_field(&self, field: u64) -> u32 {
861 if self.large_file {
862 spec::ZIP64_BYTES_THR as u32
863 } else {
864 field.min(spec::ZIP64_BYTES_THR).try_into().unwrap()
865 }
866 }
867
868 pub(crate) fn local_block(&self) -> ZipResult<ZipLocalEntryBlock> {
869 let (compressed_size, uncompressed_size) = if self.using_data_descriptor {
870 (0, 0)
871 } else {
872 (
873 self.clamp_size_field(self.compressed_size),
874 self.clamp_size_field(self.uncompressed_size),
875 )
876 };
877 let extra_field_length: u16 = self
878 .extra_field_len()
879 .try_into()
880 .map_err(|_| invalid!("Extra data field is too large"))?;
881
882 let last_modified_time = self
883 .last_modified_time
884 .unwrap_or_else(DateTime::default_for_write);
885 Ok(ZipLocalEntryBlock {
886 magic: ZipLocalEntryBlock::MAGIC,
887 version_made_by: self.version_needed(),
888 flags: self.flags(),
889 compression_method: self.compression_method.serialize_to_u16(),
890 last_mod_time: last_modified_time.timepart(),
891 last_mod_date: last_modified_time.datepart(),
892 crc32: self.crc32,
893 compressed_size,
894 uncompressed_size,
895 file_name_length: self.file_name_raw.len().try_into().unwrap(),
896 extra_field_length,
897 })
898 }
899
900 pub(crate) fn block(&self) -> ZipResult<ZipCentralEntryBlock> {
901 let extra_field_len: u16 = self.extra_field_len().try_into().unwrap();
902 let central_extra_field_len: u16 = self.central_extra_field_len().try_into().unwrap();
903 let last_modified_time = self
904 .last_modified_time
905 .unwrap_or_else(DateTime::default_for_write);
906 let version_to_extract = self.version_needed();
907 let version_made_by = (self.version_made_by as u16).max(version_to_extract);
908 Ok(ZipCentralEntryBlock {
909 magic: ZipCentralEntryBlock::MAGIC,
910 version_made_by: ((self.system as u16) << 8) | version_made_by,
911 version_to_extract,
912 flags: self.flags(),
913 compression_method: self.compression_method.serialize_to_u16(),
914 last_mod_time: last_modified_time.timepart(),
915 last_mod_date: last_modified_time.datepart(),
916 crc32: self.crc32,
917 compressed_size: self
918 .compressed_size
919 .min(spec::ZIP64_BYTES_THR)
920 .try_into()
921 .unwrap(),
922 uncompressed_size: self
923 .uncompressed_size
924 .min(spec::ZIP64_BYTES_THR)
925 .try_into()
926 .unwrap(),
927 file_name_length: self.file_name_raw.len().try_into().unwrap(),
928 extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or(
929 invalid!("Extra field length in central directory exceeds 64KiB"),
930 )?,
931 file_comment_length: self.file_comment.len().try_into().unwrap(),
932 disk_number: 0,
933 internal_file_attributes: 0,
934 external_file_attributes: self.external_attributes,
935 offset: self
936 .header_start
937 .min(spec::ZIP64_BYTES_THR)
938 .try_into()
939 .unwrap(),
940 })
941 }
942
943 pub(crate) fn zip64_extra_field_block(&self) -> Option<Zip64ExtraFieldBlock> {
944 Zip64ExtraFieldBlock::maybe_new(
945 self.large_file,
946 self.uncompressed_size,
947 self.compressed_size,
948 self.header_start,
949 )
950 }
951
952 pub(crate) fn write_data_descriptor<W: std::io::Write>(
953 &self,
954 writer: &mut W,
955 auto_large_file: bool,
956 ) -> Result<(), ZipError> {
957 if self.large_file {
958 return self.zip64_data_descriptor_block().write(writer);
959 }
960 if self.compressed_size > spec::ZIP64_BYTES_THR
961 || self.uncompressed_size > spec::ZIP64_BYTES_THR
962 {
963 if auto_large_file {
964 return self.zip64_data_descriptor_block().write(writer);
965 }
966 return Err(ZipError::Io(std::io::Error::other(
967 "Large file option has not been set - use .large_file(true) in options",
968 )));
969 }
970 self.data_descriptor_block().write(writer)
971 }
972
973 pub(crate) fn data_descriptor_block(&self) -> ZipDataDescriptorBlock {
974 ZipDataDescriptorBlock {
975 magic: ZipDataDescriptorBlock::MAGIC,
976 crc32: self.crc32,
977 compressed_size: self.compressed_size as u32,
978 uncompressed_size: self.uncompressed_size as u32,
979 }
980 }
981
982 pub(crate) fn zip64_data_descriptor_block(&self) -> Zip64DataDescriptorBlock {
983 Zip64DataDescriptorBlock {
984 magic: Zip64DataDescriptorBlock::MAGIC,
985 crc32: self.crc32,
986 compressed_size: self.compressed_size,
987 uncompressed_size: self.uncompressed_size,
988 }
989 }
990}
991
992#[derive(Copy, Clone, Debug)]
993#[repr(packed, C)]
994pub(crate) struct ZipCentralEntryBlock {
995 magic: spec::Magic,
996 pub version_made_by: u16,
997 pub version_to_extract: u16,
998 pub flags: u16,
999 pub compression_method: u16,
1000 pub last_mod_time: u16,
1001 pub last_mod_date: u16,
1002 pub crc32: u32,
1003 pub compressed_size: u32,
1004 pub uncompressed_size: u32,
1005 pub file_name_length: u16,
1006 pub extra_field_length: u16,
1007 pub file_comment_length: u16,
1008 pub disk_number: u16,
1009 pub internal_file_attributes: u16,
1010 pub external_file_attributes: u32,
1011 pub offset: u32,
1012}
1013
1014unsafe impl Pod for ZipCentralEntryBlock {}
1015
1016impl FixedSizeBlock for ZipCentralEntryBlock {
1017 const MAGIC: spec::Magic = spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE;
1018
1019 #[inline(always)]
1020 fn magic(self) -> spec::Magic {
1021 self.magic
1022 }
1023
1024 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header");
1025
1026 to_and_from_le![
1027 (magic, spec::Magic),
1028 (version_made_by, u16),
1029 (version_to_extract, u16),
1030 (flags, u16),
1031 (compression_method, u16),
1032 (last_mod_time, u16),
1033 (last_mod_date, u16),
1034 (crc32, u32),
1035 (compressed_size, u32),
1036 (uncompressed_size, u32),
1037 (file_name_length, u16),
1038 (extra_field_length, u16),
1039 (file_comment_length, u16),
1040 (disk_number, u16),
1041 (internal_file_attributes, u16),
1042 (external_file_attributes, u32),
1043 (offset, u32),
1044 ];
1045}
1046
1047#[derive(Copy, Clone, Debug)]
1048#[repr(packed, C)]
1049pub(crate) struct ZipLocalEntryBlock {
1050 magic: spec::Magic,
1051 pub version_made_by: u16,
1052 pub flags: u16,
1053 pub compression_method: u16,
1054 pub last_mod_time: u16,
1055 pub last_mod_date: u16,
1056 pub crc32: u32,
1057 pub compressed_size: u32,
1058 pub uncompressed_size: u32,
1059 pub file_name_length: u16,
1060 pub extra_field_length: u16,
1061}
1062
1063unsafe impl Pod for ZipLocalEntryBlock {}
1064
1065impl FixedSizeBlock for ZipLocalEntryBlock {
1066 const MAGIC: spec::Magic = spec::Magic::LOCAL_FILE_HEADER_SIGNATURE;
1067
1068 #[inline(always)]
1069 fn magic(self) -> spec::Magic {
1070 self.magic
1071 }
1072
1073 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header");
1074
1075 to_and_from_le![
1076 (magic, spec::Magic),
1077 (version_made_by, u16),
1078 (flags, u16),
1079 (compression_method, u16),
1080 (last_mod_time, u16),
1081 (last_mod_date, u16),
1082 (crc32, u32),
1083 (compressed_size, u32),
1084 (uncompressed_size, u32),
1085 (file_name_length, u16),
1086 (extra_field_length, u16),
1087 ];
1088}
1089
1090#[derive(Copy, Clone, Debug)]
1091pub(crate) struct Zip64ExtraFieldBlock {
1092 magic: spec::ExtraFieldMagic,
1093 size: u16,
1094 uncompressed_size: Option<u64>,
1095 compressed_size: Option<u64>,
1096 header_start: Option<u64>,
1097 }
1100
1101impl Zip64ExtraFieldBlock {
1102 pub(crate) fn maybe_new(
1103 large_file: bool,
1104 uncompressed_size: u64,
1105 compressed_size: u64,
1106 header_start: u64,
1107 ) -> Option<Zip64ExtraFieldBlock> {
1108 let mut size: u16 = 0;
1109 let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file {
1110 size += mem::size_of::<u64>() as u16;
1111 Some(uncompressed_size)
1112 } else {
1113 None
1114 };
1115 let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file {
1116 size += mem::size_of::<u64>() as u16;
1117 Some(compressed_size)
1118 } else {
1119 None
1120 };
1121 let header_start = if header_start >= ZIP64_BYTES_THR {
1122 size += mem::size_of::<u64>() as u16;
1123 Some(header_start)
1124 } else {
1125 None
1126 };
1127 if size == 0 {
1128 return None;
1129 }
1130
1131 Some(Zip64ExtraFieldBlock {
1132 magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
1133 size,
1134 uncompressed_size,
1135 compressed_size,
1136 header_start,
1137 })
1138 }
1139}
1140
1141impl Zip64ExtraFieldBlock {
1142 pub fn full_size(&self) -> usize {
1143 assert!(self.size > 0);
1144 self.size as usize + mem::size_of::<spec::ExtraFieldMagic>() + mem::size_of::<u16>()
1145 }
1146
1147 pub fn serialize(self) -> Box<[u8]> {
1148 let Self {
1149 magic,
1150 size,
1151 uncompressed_size,
1152 compressed_size,
1153 header_start,
1154 } = self;
1155
1156 let full_size = self.full_size();
1157
1158 let mut ret = Vec::with_capacity(full_size);
1159 ret.extend(magic.to_le_bytes());
1160 ret.extend(u16::to_le_bytes(size));
1161
1162 if let Some(uncompressed_size) = uncompressed_size {
1163 ret.extend(u64::to_le_bytes(uncompressed_size));
1164 }
1165 if let Some(compressed_size) = compressed_size {
1166 ret.extend(u64::to_le_bytes(compressed_size));
1167 }
1168 if let Some(header_start) = header_start {
1169 ret.extend(u64::to_le_bytes(header_start));
1170 }
1171 debug_assert_eq!(ret.len(), full_size);
1172
1173 ret.into_boxed_slice()
1174 }
1175}
1176
1177#[derive(Copy, Clone, Debug)]
1178#[repr(packed, C)]
1179pub(crate) struct ZipDataDescriptorBlock {
1180 magic: spec::Magic,
1181 pub crc32: u32,
1182 pub compressed_size: u32,
1183 pub uncompressed_size: u32,
1184}
1185
1186unsafe impl Pod for ZipDataDescriptorBlock {}
1187
1188impl FixedSizeBlock for ZipDataDescriptorBlock {
1189 const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE;
1190
1191 #[inline(always)]
1192 fn magic(self) -> spec::Magic {
1193 self.magic
1194 }
1195
1196 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header");
1197
1198 to_and_from_le![
1199 (magic, spec::Magic),
1200 (crc32, u32),
1201 (compressed_size, u32),
1202 (uncompressed_size, u32),
1203 ];
1204}
1205
1206#[derive(Copy, Clone, Debug)]
1207#[repr(packed, C)]
1208pub(crate) struct Zip64DataDescriptorBlock {
1209 magic: spec::Magic,
1210 pub crc32: u32,
1211 pub compressed_size: u64,
1212 pub uncompressed_size: u64,
1213}
1214
1215unsafe impl Pod for Zip64DataDescriptorBlock {}
1216
1217impl FixedSizeBlock for Zip64DataDescriptorBlock {
1218 const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE;
1219
1220 #[inline(always)]
1221 fn magic(self) -> spec::Magic {
1222 self.magic
1223 }
1224
1225 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header");
1226
1227 to_and_from_le![
1228 (magic, spec::Magic),
1229 (crc32, u32),
1230 (compressed_size, u64),
1231 (uncompressed_size, u64),
1232 ];
1233}
1234
1235#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1240#[repr(u16)]
1241pub enum AesVendorVersion {
1242 Ae1 = 0x0001,
1243 Ae2 = 0x0002,
1244}
1245
1246#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1248#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))]
1249#[repr(u8)]
1250pub enum AesMode {
1251 Aes128 = 0x01,
1253 Aes192 = 0x02,
1255 Aes256 = 0x03,
1257}
1258
1259#[cfg(feature = "aes-crypto")]
1260impl AesMode {
1261 pub const fn salt_length(&self) -> usize {
1263 self.key_length() / 2
1264 }
1265
1266 pub const fn key_length(&self) -> usize {
1268 match self {
1269 Self::Aes128 => 16,
1270 Self::Aes192 => 24,
1271 Self::Aes256 => 32,
1272 }
1273 }
1274}
1275
1276#[cfg(test)]
1277mod test {
1278 #[test]
1279 fn system() {
1280 use super::System;
1281 assert_eq!(u8::from(System::Dos), 0u8);
1282 assert_eq!(System::Dos as u8, 0u8);
1283 assert_eq!(System::Unix as u8, 3u8);
1284 assert_eq!(u8::from(System::Unix), 3u8);
1285 assert_eq!(System::from(0), System::Dos);
1286 assert_eq!(System::from(3), System::Unix);
1287 assert_eq!(u8::from(System::Unknown), 4u8);
1288 assert_eq!(System::Unknown as u8, 4u8);
1289 }
1290
1291 #[test]
1292 fn sanitize() {
1293 let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string();
1294 let data = ZipFileData {
1295 system: System::Dos,
1296 version_made_by: 0,
1297 flags: 0,
1298 encrypted: false,
1299 using_data_descriptor: false,
1300 is_utf8: true,
1301 compression_method: crate::compression::CompressionMethod::Stored,
1302 compression_level: None,
1303 last_modified_time: None,
1304 crc32: 0,
1305 compressed_size: 0,
1306 uncompressed_size: 0,
1307 file_name: file_name.clone().into_boxed_str(),
1308 file_name_raw: file_name.into_bytes().into_boxed_slice(),
1309 extra_field: None,
1310 central_extra_field: None,
1311 file_comment: String::with_capacity(0).into_boxed_str(),
1312 header_start: 0,
1313 extra_data_start: None,
1314 data_start: OnceLock::new(),
1315 central_header_start: 0,
1316 external_attributes: 0,
1317 large_file: false,
1318 aes_mode: None,
1319 aes_extra_data_start: 0,
1320 extra_fields: Vec::new(),
1321 };
1322 assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
1323 }
1324
1325 #[test]
1326 #[allow(clippy::unusual_byte_groupings)]
1327 fn datetime_default() {
1328 use super::DateTime;
1329 let dt = DateTime::default();
1330 assert_eq!(dt.timepart(), 0);
1331 assert_eq!(dt.datepart(), 0b0000000_0001_00001);
1332 }
1333
1334 #[test]
1335 #[allow(clippy::unusual_byte_groupings)]
1336 fn datetime_max() {
1337 use super::DateTime;
1338 let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
1339 assert_eq!(dt.timepart(), 0b10111_111011_11101);
1340 assert_eq!(dt.datepart(), 0b1111111_1100_11111);
1341 }
1342
1343 #[test]
1344 fn datetime_equality() {
1345 use super::DateTime;
1346
1347 let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1348 assert_eq!(
1349 dt,
1350 DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1351 );
1352 assert_ne!(dt, DateTime::default());
1353 }
1354
1355 #[test]
1356 fn datetime_order() {
1357 use std::cmp::Ordering;
1358
1359 use super::DateTime;
1360
1361 let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1362 assert_eq!(
1363 dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()),
1364 Ordering::Equal
1365 );
1366 assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap());
1368 assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap());
1369 assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap());
1371 assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap());
1372 assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap());
1374 assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap());
1375 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap());
1377 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap());
1378 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap());
1380 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap());
1381 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap());
1383 assert_eq!(
1384 dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()),
1385 Ordering::Equal
1386 );
1387 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap());
1388 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap());
1389 }
1390
1391 #[test]
1392 fn datetime_display() {
1393 use super::DateTime;
1394
1395 assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00");
1396 assert_eq!(
1397 format!(
1398 "{}",
1399 DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1400 ),
1401 "2018-11-17 10:38:30"
1402 );
1403 assert_eq!(
1404 format!(
1405 "{}",
1406 DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap()
1407 ),
1408 "2107-12-31 23:59:58"
1409 );
1410 }
1411
1412 #[test]
1413 fn datetime_bounds() {
1414 use super::DateTime;
1415
1416 assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
1417 assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
1418 assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
1419 assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());
1420
1421 assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
1422 assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
1423 assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
1424 assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
1425 assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
1426 assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
1427 assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
1428 assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());
1429
1430 assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok());
1431 assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok());
1432 assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err());
1433 assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok());
1434 assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok());
1435 assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err());
1436 assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok());
1437 assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok());
1438 assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err());
1439 assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok());
1440 assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok());
1441 assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok());
1442 assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err());
1443 assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok());
1444 assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok());
1445 assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err());
1446 assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok());
1447
1448 assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok());
1450 assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok());
1452 assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err());
1454 }
1455
1456 use std::{path::PathBuf, sync::OnceLock};
1457
1458 #[cfg(feature = "time")]
1459 use time::{format_description::well_known::Rfc3339, OffsetDateTime, PrimitiveDateTime};
1460
1461 use crate::types::{System, ZipFileData};
1462
1463 #[cfg(feature = "time")]
1464 #[test]
1465 fn datetime_try_from_offset_datetime() {
1466 use time::macros::datetime;
1467
1468 use super::DateTime;
1469
1470 let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap();
1472 assert_eq!(dt.year(), 2018);
1473 assert_eq!(dt.month(), 11);
1474 assert_eq!(dt.day(), 17);
1475 assert_eq!(dt.hour(), 10);
1476 assert_eq!(dt.minute(), 38);
1477 assert_eq!(dt.second(), 30);
1478 }
1479
1480 #[cfg(feature = "time")]
1481 #[test]
1482 fn datetime_try_from_primitive_datetime() {
1483 use time::macros::datetime;
1484
1485 use super::DateTime;
1486
1487 let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap();
1489 assert_eq!(dt.year(), 2018);
1490 assert_eq!(dt.month(), 11);
1491 assert_eq!(dt.day(), 17);
1492 assert_eq!(dt.hour(), 10);
1493 assert_eq!(dt.minute(), 38);
1494 assert_eq!(dt.second(), 30);
1495 }
1496
1497 #[cfg(feature = "time")]
1498 #[test]
1499 fn datetime_try_from_bounds() {
1500 use super::DateTime;
1501 use time::macros::datetime;
1502
1503 assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err());
1505
1506 assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok());
1508
1509 assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok());
1511
1512 assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err());
1514 }
1515
1516 #[cfg(feature = "time")]
1517 #[test]
1518 fn offset_datetime_try_from_datetime() {
1519 use time::macros::datetime;
1520
1521 use super::DateTime;
1522
1523 let dt =
1525 OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1526 assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC));
1527 }
1528
1529 #[cfg(feature = "time")]
1530 #[test]
1531 fn primitive_datetime_try_from_datetime() {
1532 use time::macros::datetime;
1533
1534 use super::DateTime;
1535
1536 let dt =
1538 PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1539 assert_eq!(dt, datetime!(2018-11-17 10:38:30));
1540 }
1541
1542 #[cfg(feature = "time")]
1543 #[test]
1544 fn offset_datetime_try_from_bounds() {
1545 use super::DateTime;
1546
1547 assert!(OffsetDateTime::try_from(unsafe {
1549 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1550 })
1551 .is_err());
1552
1553 assert!(OffsetDateTime::try_from(unsafe {
1555 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1556 })
1557 .is_err());
1558 }
1559
1560 #[cfg(feature = "time")]
1561 #[test]
1562 fn primitive_datetime_try_from_bounds() {
1563 use super::DateTime;
1564
1565 assert!(PrimitiveDateTime::try_from(unsafe {
1567 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1568 })
1569 .is_err());
1570
1571 assert!(PrimitiveDateTime::try_from(unsafe {
1573 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1574 })
1575 .is_err());
1576 }
1577
1578 #[cfg(feature = "jiff-02")]
1579 #[test]
1580 fn datetime_try_from_civil_datetime() {
1581 use jiff::civil;
1582
1583 use super::DateTime;
1584
1585 let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap();
1587 assert_eq!(dt.year(), 2018);
1588 assert_eq!(dt.month(), 11);
1589 assert_eq!(dt.day(), 17);
1590 assert_eq!(dt.hour(), 10);
1591 assert_eq!(dt.minute(), 38);
1592 assert_eq!(dt.second(), 30);
1593 }
1594
1595 #[cfg(feature = "jiff-02")]
1596 #[test]
1597 fn datetime_try_from_civil_datetime_bounds() {
1598 use jiff::civil;
1599
1600 use super::DateTime;
1601
1602 assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err());
1604
1605 assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok());
1607
1608 assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok());
1610
1611 assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err());
1613 }
1614
1615 #[cfg(feature = "jiff-02")]
1616 #[test]
1617 fn civil_datetime_try_from_datetime() {
1618 use jiff::civil;
1619
1620 use super::DateTime;
1621
1622 let dt =
1624 civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1625 assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0));
1626 }
1627
1628 #[cfg(feature = "jiff-02")]
1629 #[test]
1630 fn civil_datetime_try_from_datetime_bounds() {
1631 use jiff::civil;
1632
1633 use super::DateTime;
1634
1635 assert!(civil::DateTime::try_from(unsafe {
1637 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1638 })
1639 .is_err());
1640
1641 assert!(civil::DateTime::try_from(unsafe {
1643 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1644 })
1645 .is_err());
1646 }
1647
1648 #[test]
1649 #[allow(deprecated)]
1650 fn time_conversion() {
1651 use super::DateTime;
1652 let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap();
1653 assert_eq!(dt.year(), 2018);
1654 assert_eq!(dt.month(), 11);
1655 assert_eq!(dt.day(), 17);
1656 assert_eq!(dt.hour(), 10);
1657 assert_eq!(dt.minute(), 38);
1658 assert_eq!(dt.second(), 30);
1659
1660 let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap();
1661 assert_eq!(dt.year(), 2018);
1662 assert_eq!(dt.month(), 11);
1663 assert_eq!(dt.day(), 17);
1664 assert_eq!(dt.hour(), 10);
1665 assert_eq!(dt.minute(), 38);
1666 assert_eq!(dt.second(), 30);
1667
1668 #[cfg(feature = "time")]
1669 assert_eq!(
1670 dt.to_time().unwrap().format(&Rfc3339).unwrap(),
1671 "2018-11-17T10:38:30Z"
1672 );
1673
1674 assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF));
1675 }
1676
1677 #[test]
1678 #[allow(deprecated)]
1679 fn time_out_of_bounds() {
1680 use super::DateTime;
1681 let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) };
1682 assert_eq!(dt.year(), 2107);
1683 assert_eq!(dt.month(), 15);
1684 assert_eq!(dt.day(), 31);
1685 assert_eq!(dt.hour(), 31);
1686 assert_eq!(dt.minute(), 63);
1687 assert_eq!(dt.second(), 62);
1688
1689 #[cfg(feature = "time")]
1690 assert!(dt.to_time().is_err());
1691
1692 let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) };
1693 assert_eq!(dt.year(), 1980);
1694 assert_eq!(dt.month(), 0);
1695 assert_eq!(dt.day(), 0);
1696 assert_eq!(dt.hour(), 0);
1697 assert_eq!(dt.minute(), 0);
1698 assert_eq!(dt.second(), 0);
1699
1700 #[cfg(feature = "time")]
1701 assert!(dt.to_time().is_err());
1702 }
1703
1704 #[cfg(feature = "time")]
1705 #[test]
1706 fn time_at_january() {
1707 use super::DateTime;
1708
1709 let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
1711
1712 assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok());
1713 }
1714}