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