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