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::result::DateTimeRangeError;
29use crate::spec::is_dir;
30use crate::types::ffi::S_IFDIR;
31use crate::{CompressionMethod, ZIP64_BYTES_THR};
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, Copy, Eq, Hash, PartialEq)]
87pub struct DateTime {
88 datepart: u16,
89 timepart: u16,
90}
91
92impl Debug for DateTime {
93 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94 if *self == Self::default() {
95 return f.write_str("DateTime::default()");
96 }
97 f.write_fmt(format_args!(
98 "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?",
99 self.year(),
100 self.month(),
101 self.day(),
102 self.hour(),
103 self.minute(),
104 self.second()
105 ))
106 }
107}
108
109impl Ord for DateTime {
110 fn cmp(&self, other: &Self) -> Ordering {
111 if let ord @ (Ordering::Less | Ordering::Greater) = self.year().cmp(&other.year()) {
112 return ord;
113 }
114 if let ord @ (Ordering::Less | Ordering::Greater) = self.month().cmp(&other.month()) {
115 return ord;
116 }
117 if let ord @ (Ordering::Less | Ordering::Greater) = self.day().cmp(&other.day()) {
118 return ord;
119 }
120 if let ord @ (Ordering::Less | Ordering::Greater) = self.hour().cmp(&other.hour()) {
121 return ord;
122 }
123 if let ord @ (Ordering::Less | Ordering::Greater) = self.minute().cmp(&other.minute()) {
124 return ord;
125 }
126 self.second().cmp(&other.second())
127 }
128}
129
130impl PartialOrd for DateTime {
131 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
132 Some(self.cmp(other))
133 }
134}
135
136impl DateTime {
137 #[cfg(feature = "time")]
139 pub fn default_for_write() -> Self {
140 let now = OffsetDateTime::now_utc();
141 PrimitiveDateTime::new(now.date(), now.time())
142 .try_into()
143 .unwrap_or_else(|_| DateTime::default())
144 }
145
146 #[cfg(not(feature = "time"))]
148 pub fn default_for_write() -> Self {
149 DateTime::default()
150 }
151}
152
153#[cfg(fuzzing)]
154impl arbitrary::Arbitrary<'_> for DateTime {
155 fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Self> {
156 let year: u16 = u.int_in_range(1980..=2107)?;
157 let month: u16 = u.int_in_range(1..=12)?;
158 let day: u16 = u.int_in_range(1..=31)?;
159 let datepart = day | (month << 5) | ((year - 1980) << 9);
160 let hour: u16 = u.int_in_range(0..=23)?;
161 let minute: u16 = u.int_in_range(0..=59)?;
162 let second: u16 = u.int_in_range(0..=58)?;
163 let timepart = (second >> 1) | (minute << 5) | (hour << 11);
164 Ok(DateTime { datepart, timepart })
165 }
166}
167
168#[cfg(feature = "chrono")]
169impl TryFrom<NaiveDateTime> for DateTime {
170 type Error = DateTimeRangeError;
171
172 fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
173 DateTime::from_date_and_time(
174 value.year().try_into()?,
175 value.month().try_into()?,
176 value.day().try_into()?,
177 value.hour().try_into()?,
178 value.minute().try_into()?,
179 value.second().try_into()?,
180 )
181 }
182}
183
184#[cfg(feature = "chrono")]
185impl TryFrom<DateTime> for NaiveDateTime {
186 type Error = DateTimeRangeError;
187
188 fn try_from(value: DateTime) -> Result<Self, Self::Error> {
189 let date = NaiveDate::from_ymd_opt(
190 value.year().into(),
191 value.month().into(),
192 value.day().into(),
193 )
194 .ok_or(DateTimeRangeError)?;
195 let time = NaiveTime::from_hms_opt(
196 value.hour().into(),
197 value.minute().into(),
198 value.second().into(),
199 )
200 .ok_or(DateTimeRangeError)?;
201 Ok(NaiveDateTime::new(date, time))
202 }
203}
204
205#[cfg(feature = "jiff-02")]
206impl TryFrom<civil::DateTime> for DateTime {
207 type Error = DateTimeRangeError;
208
209 fn try_from(value: civil::DateTime) -> Result<Self, Self::Error> {
210 Self::from_date_and_time(
211 value.year().try_into()?,
212 value.month() as u8,
213 value.day() as u8,
214 value.hour() as u8,
215 value.minute() as u8,
216 value.second() as u8,
217 )
218 }
219}
220
221#[cfg(feature = "jiff-02")]
222impl TryFrom<DateTime> for civil::DateTime {
223 type Error = jiff::Error;
224
225 fn try_from(value: DateTime) -> Result<Self, Self::Error> {
226 Self::new(
227 value.year() as i16,
228 value.month() as i8,
229 value.day() as i8,
230 value.hour() as i8,
231 value.minute() as i8,
232 value.second() as i8,
233 0,
234 )
235 }
236}
237
238impl TryFrom<(u16, u16)> for DateTime {
239 type Error = DateTimeRangeError;
240
241 #[inline]
242 fn try_from(values: (u16, u16)) -> Result<Self, Self::Error> {
243 Self::try_from_msdos(values.0, values.1)
244 }
245}
246
247impl From<DateTime> for (u16, u16) {
248 #[inline]
249 fn from(dt: DateTime) -> Self {
250 (dt.datepart(), dt.timepart())
251 }
252}
253
254impl Default for DateTime {
255 fn default() -> DateTime {
257 DateTime {
258 datepart: 0b0000000000100001,
259 timepart: 0,
260 }
261 }
262}
263
264impl fmt::Display for DateTime {
265 #[inline]
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 write!(
268 f,
269 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
270 self.year(),
271 self.month(),
272 self.day(),
273 self.hour(),
274 self.minute(),
275 self.second()
276 )
277 }
278}
279
280impl DateTime {
281 pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime {
286 DateTime { datepart, timepart }
287 }
288
289 pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result<DateTime, DateTimeRangeError> {
292 let seconds = (timepart & 0b0000000000011111) << 1;
293 let minutes = (timepart & 0b0000011111100000) >> 5;
294 let hours = (timepart & 0b1111100000000000) >> 11;
295 let days = datepart & 0b0000000000011111;
296 let months = (datepart & 0b0000000111100000) >> 5;
297 let years = (datepart & 0b1111111000000000) >> 9;
298 Self::from_date_and_time(
299 years.checked_add(1980).ok_or(DateTimeRangeError)?,
300 months.try_into()?,
301 days.try_into()?,
302 hours.try_into()?,
303 minutes.try_into()?,
304 seconds.try_into()?,
305 )
306 }
307
308 pub fn from_date_and_time(
318 year: u16,
319 month: u8,
320 day: u8,
321 hour: u8,
322 minute: u8,
323 second: u8,
324 ) -> Result<DateTime, DateTimeRangeError> {
325 fn is_leap_year(year: u16) -> bool {
326 (year % 4 == 0) && ((year % 25 != 0) || (year % 16 == 0))
327 }
328
329 if (1980..=2107).contains(&year)
330 && (1..=12).contains(&month)
331 && (1..=31).contains(&day)
332 && hour <= 23
333 && minute <= 59
334 && second <= 60
335 {
336 let second = second.min(58); let max_day = match month {
338 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
339 4 | 6 | 9 | 11 => 30,
340 2 if is_leap_year(year) => 29,
341 2 => 28,
342 _ => unreachable!(),
343 };
344 if day > max_day {
345 return Err(DateTimeRangeError);
346 }
347 let datepart = (day as u16) | ((month as u16) << 5) | ((year - 1980) << 9);
348 let timepart = ((second as u16) >> 1) | ((minute as u16) << 5) | ((hour as u16) << 11);
349 Ok(DateTime { datepart, timepart })
350 } else {
351 Err(DateTimeRangeError)
352 }
353 }
354
355 pub fn is_valid(&self) -> bool {
357 Self::try_from_msdos(self.datepart, self.timepart).is_ok()
358 }
359
360 #[cfg(feature = "time")]
361 #[deprecated(since = "0.6.4", note = "use `DateTime::try_from()` instead")]
365 pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, DateTimeRangeError> {
366 dt.try_into()
367 }
368
369 pub const fn timepart(&self) -> u16 {
371 self.timepart
372 }
373
374 pub const fn datepart(&self) -> u16 {
376 self.datepart
377 }
378
379 #[cfg(feature = "time")]
380 #[deprecated(since = "1.3.1", note = "use `OffsetDateTime::try_from()` instead")]
382 pub fn to_time(&self) -> Result<OffsetDateTime, ComponentRange> {
383 (*self).try_into()
384 }
385
386 pub const fn year(&self) -> u16 {
388 (self.datepart >> 9) + 1980
389 }
390
391 pub const fn month(&self) -> u8 {
397 ((self.datepart & 0b0000000111100000) >> 5) as u8
398 }
399
400 pub const fn day(&self) -> u8 {
406 (self.datepart & 0b0000000000011111) as u8
407 }
408
409 pub const fn hour(&self) -> u8 {
415 (self.timepart >> 11) as u8
416 }
417
418 pub const fn minute(&self) -> u8 {
424 ((self.timepart & 0b0000011111100000) >> 5) as u8
425 }
426
427 pub const fn second(&self) -> u8 {
433 ((self.timepart & 0b0000000000011111) << 1) as u8
434 }
435}
436
437#[cfg(feature = "time")]
438impl TryFrom<OffsetDateTime> for DateTime {
439 type Error = DateTimeRangeError;
440
441 #[allow(useless_deprecated)]
442 #[deprecated(
443 since = "2.5.0",
444 note = "use `TryFrom<PrimitiveDateTime> for DateTime` instead"
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 #[allow(useless_deprecated)]
472 #[deprecated(
473 since = "2.5.0",
474 note = "use `TryFrom<DateTime> for PrimitiveDateTime` instead"
475 )]
476 fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
477 PrimitiveDateTime::try_from(dt).map(PrimitiveDateTime::assume_utc)
478 }
479}
480
481#[cfg(feature = "time")]
482impl TryFrom<DateTime> for PrimitiveDateTime {
483 type Error = ComponentRange;
484
485 fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
486 let date =
487 Date::from_calendar_date(dt.year() as i32, Month::try_from(dt.month())?, dt.day())?;
488 let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?;
489 Ok(PrimitiveDateTime::new(date, time))
490 }
491}
492
493pub const MIN_VERSION: u8 = 10;
494pub const DEFAULT_VERSION: u8 = 45;
495
496#[derive(Debug, Clone, Default)]
498pub struct ZipFileData {
499 pub system: System,
501 pub version_made_by: u8,
503 pub encrypted: bool,
505 pub is_utf8: bool,
507 pub using_data_descriptor: bool,
509 pub compression_method: crate::compression::CompressionMethod,
511 pub compression_level: Option<i64>,
513 pub last_modified_time: Option<DateTime>,
515 pub crc32: u32,
517 pub compressed_size: u64,
519 pub uncompressed_size: u64,
521 pub file_name: Box<str>,
523 pub file_name_raw: Box<[u8]>,
525 pub extra_field: Option<Arc<Vec<u8>>>,
527 pub central_extra_field: Option<Arc<Vec<u8>>>,
529 pub file_comment: Box<str>,
531 pub header_start: u64,
533 pub extra_data_start: Option<u64>,
535 pub central_header_start: u64,
539 pub data_start: OnceLock<u64>,
541 pub external_attributes: u32,
543 pub large_file: bool,
545 pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
547 pub aes_extra_data_start: u64,
549
550 pub extra_fields: Vec<ExtraField>,
552}
553
554impl ZipFileData {
555 pub fn data_start(&self) -> u64 {
557 *self.data_start.get().unwrap()
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 encrypted: options.encrypt_with.is_some(),
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 encrypted,
803 using_data_descriptor,
804 is_utf8,
805 compression_method,
806 compression_level: None,
807 last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
808 crc32,
809 compressed_size: compressed_size.into(),
810 uncompressed_size: uncompressed_size.into(),
811 file_name,
812 file_name_raw: file_name_raw.into(),
813 extra_field: Some(Arc::new(extra_field)),
814 central_extra_field: None,
815 file_comment: String::with_capacity(0).into_boxed_str(), header_start: 0,
819 data_start: OnceLock::new(),
820 central_header_start: 0,
821 external_attributes: 0,
825 large_file: false,
826 aes_mode: None,
827 extra_fields: Vec::new(),
828 extra_data_start: None,
829 aes_extra_data_start: 0,
830 })
831 }
832
833 fn is_utf8(&self) -> bool {
834 std::str::from_utf8(&self.file_name_raw).is_ok()
835 }
836
837 fn is_ascii(&self) -> bool {
838 self.file_name_raw.is_ascii()
839 }
840
841 fn flags(&self) -> u16 {
842 let utf8_bit: u16 = if self.is_utf8() && !self.is_ascii() {
843 1u16 << 11
844 } else {
845 0
846 };
847 let encrypted_bit: u16 = if self.encrypted { 1u16 << 0 } else { 0 };
848
849 utf8_bit | encrypted_bit
850 }
851
852 fn clamp_size_field(&self, field: u64) -> u32 {
853 if self.large_file {
854 spec::ZIP64_BYTES_THR as u32
855 } else {
856 field.min(spec::ZIP64_BYTES_THR).try_into().unwrap()
857 }
858 }
859
860 pub(crate) fn local_block(&self) -> ZipResult<ZipLocalEntryBlock> {
861 let compressed_size: u32 = self.clamp_size_field(self.compressed_size);
862 let uncompressed_size: u32 = self.clamp_size_field(self.uncompressed_size);
863 let extra_field_length: u16 = self
864 .extra_field_len()
865 .try_into()
866 .map_err(|_| invalid!("Extra data field is too large"))?;
867
868 let last_modified_time = self
869 .last_modified_time
870 .unwrap_or_else(DateTime::default_for_write);
871 Ok(ZipLocalEntryBlock {
872 magic: ZipLocalEntryBlock::MAGIC,
873 version_made_by: self.version_needed(),
874 flags: self.flags(),
875 compression_method: self.compression_method.serialize_to_u16(),
876 last_mod_time: last_modified_time.timepart(),
877 last_mod_date: last_modified_time.datepart(),
878 crc32: self.crc32,
879 compressed_size,
880 uncompressed_size,
881 file_name_length: self.file_name_raw.len().try_into().unwrap(),
882 extra_field_length,
883 })
884 }
885
886 pub(crate) fn block(&self) -> ZipResult<ZipCentralEntryBlock> {
887 let extra_field_len: u16 = self.extra_field_len().try_into().unwrap();
888 let central_extra_field_len: u16 = self.central_extra_field_len().try_into().unwrap();
889 let last_modified_time = self
890 .last_modified_time
891 .unwrap_or_else(DateTime::default_for_write);
892 let version_to_extract = self.version_needed();
893 let version_made_by = (self.version_made_by as u16).max(version_to_extract);
894 Ok(ZipCentralEntryBlock {
895 magic: ZipCentralEntryBlock::MAGIC,
896 version_made_by: ((self.system as u16) << 8) | version_made_by,
897 version_to_extract,
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: self
904 .compressed_size
905 .min(spec::ZIP64_BYTES_THR)
906 .try_into()
907 .unwrap(),
908 uncompressed_size: self
909 .uncompressed_size
910 .min(spec::ZIP64_BYTES_THR)
911 .try_into()
912 .unwrap(),
913 file_name_length: self.file_name_raw.len().try_into().unwrap(),
914 extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or(
915 invalid!("Extra field length in central directory exceeds 64KiB"),
916 )?,
917 file_comment_length: self.file_comment.len().try_into().unwrap(),
918 disk_number: 0,
919 internal_file_attributes: 0,
920 external_file_attributes: self.external_attributes,
921 offset: self
922 .header_start
923 .min(spec::ZIP64_BYTES_THR)
924 .try_into()
925 .unwrap(),
926 })
927 }
928
929 pub(crate) fn zip64_extra_field_block(&self) -> Option<Zip64ExtraFieldBlock> {
930 Zip64ExtraFieldBlock::maybe_new(
931 self.large_file,
932 self.uncompressed_size,
933 self.compressed_size,
934 self.header_start,
935 )
936 }
937}
938
939#[derive(Copy, Clone, Debug)]
940#[repr(packed, C)]
941pub(crate) struct ZipCentralEntryBlock {
942 magic: spec::Magic,
943 pub version_made_by: u16,
944 pub version_to_extract: u16,
945 pub flags: u16,
946 pub compression_method: u16,
947 pub last_mod_time: u16,
948 pub last_mod_date: u16,
949 pub crc32: u32,
950 pub compressed_size: u32,
951 pub uncompressed_size: u32,
952 pub file_name_length: u16,
953 pub extra_field_length: u16,
954 pub file_comment_length: u16,
955 pub disk_number: u16,
956 pub internal_file_attributes: u16,
957 pub external_file_attributes: u32,
958 pub offset: u32,
959}
960
961unsafe impl Pod for ZipCentralEntryBlock {}
962
963impl FixedSizeBlock for ZipCentralEntryBlock {
964 const MAGIC: spec::Magic = spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE;
965
966 #[inline(always)]
967 fn magic(self) -> spec::Magic {
968 self.magic
969 }
970
971 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header");
972
973 to_and_from_le![
974 (magic, spec::Magic),
975 (version_made_by, u16),
976 (version_to_extract, u16),
977 (flags, u16),
978 (compression_method, u16),
979 (last_mod_time, u16),
980 (last_mod_date, u16),
981 (crc32, u32),
982 (compressed_size, u32),
983 (uncompressed_size, u32),
984 (file_name_length, u16),
985 (extra_field_length, u16),
986 (file_comment_length, u16),
987 (disk_number, u16),
988 (internal_file_attributes, u16),
989 (external_file_attributes, u32),
990 (offset, u32),
991 ];
992}
993
994#[derive(Copy, Clone, Debug)]
995#[repr(packed, C)]
996pub(crate) struct ZipLocalEntryBlock {
997 magic: spec::Magic,
998 pub version_made_by: u16,
999 pub flags: u16,
1000 pub compression_method: u16,
1001 pub last_mod_time: u16,
1002 pub last_mod_date: u16,
1003 pub crc32: u32,
1004 pub compressed_size: u32,
1005 pub uncompressed_size: u32,
1006 pub file_name_length: u16,
1007 pub extra_field_length: u16,
1008}
1009
1010unsafe impl Pod for ZipLocalEntryBlock {}
1011
1012impl FixedSizeBlock for ZipLocalEntryBlock {
1013 const MAGIC: spec::Magic = spec::Magic::LOCAL_FILE_HEADER_SIGNATURE;
1014
1015 #[inline(always)]
1016 fn magic(self) -> spec::Magic {
1017 self.magic
1018 }
1019
1020 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header");
1021
1022 to_and_from_le![
1023 (magic, spec::Magic),
1024 (version_made_by, u16),
1025 (flags, u16),
1026 (compression_method, u16),
1027 (last_mod_time, u16),
1028 (last_mod_date, u16),
1029 (crc32, u32),
1030 (compressed_size, u32),
1031 (uncompressed_size, u32),
1032 (file_name_length, u16),
1033 (extra_field_length, u16),
1034 ];
1035}
1036
1037#[derive(Copy, Clone, Debug)]
1038pub(crate) struct Zip64ExtraFieldBlock {
1039 magic: spec::ExtraFieldMagic,
1040 size: u16,
1041 uncompressed_size: Option<u64>,
1042 compressed_size: Option<u64>,
1043 header_start: Option<u64>,
1044 }
1047
1048impl Zip64ExtraFieldBlock {
1049 pub(crate) fn maybe_new(
1050 large_file: bool,
1051 uncompressed_size: u64,
1052 compressed_size: u64,
1053 header_start: u64,
1054 ) -> Option<Zip64ExtraFieldBlock> {
1055 let mut size: u16 = 0;
1056 let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file {
1057 size += mem::size_of::<u64>() as u16;
1058 Some(uncompressed_size)
1059 } else {
1060 None
1061 };
1062 let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file {
1063 size += mem::size_of::<u64>() as u16;
1064 Some(compressed_size)
1065 } else {
1066 None
1067 };
1068 let header_start = if header_start >= ZIP64_BYTES_THR {
1069 size += mem::size_of::<u64>() as u16;
1070 Some(header_start)
1071 } else {
1072 None
1073 };
1074 if size == 0 {
1075 return None;
1076 }
1077
1078 Some(Zip64ExtraFieldBlock {
1079 magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
1080 size,
1081 uncompressed_size,
1082 compressed_size,
1083 header_start,
1084 })
1085 }
1086}
1087
1088impl Zip64ExtraFieldBlock {
1089 pub fn full_size(&self) -> usize {
1090 assert!(self.size > 0);
1091 self.size as usize + mem::size_of::<spec::ExtraFieldMagic>() + mem::size_of::<u16>()
1092 }
1093
1094 pub fn serialize(self) -> Box<[u8]> {
1095 let Self {
1096 magic,
1097 size,
1098 uncompressed_size,
1099 compressed_size,
1100 header_start,
1101 } = self;
1102
1103 let full_size = self.full_size();
1104
1105 let mut ret = Vec::with_capacity(full_size);
1106 ret.extend(magic.to_le_bytes());
1107 ret.extend(u16::to_le_bytes(size));
1108
1109 if let Some(uncompressed_size) = uncompressed_size {
1110 ret.extend(u64::to_le_bytes(uncompressed_size));
1111 }
1112 if let Some(compressed_size) = compressed_size {
1113 ret.extend(u64::to_le_bytes(compressed_size));
1114 }
1115 if let Some(header_start) = header_start {
1116 ret.extend(u64::to_le_bytes(header_start));
1117 }
1118 debug_assert_eq!(ret.len(), full_size);
1119
1120 ret.into_boxed_slice()
1121 }
1122}
1123
1124#[derive(Copy, Clone, Debug)]
1129#[repr(u16)]
1130pub enum AesVendorVersion {
1131 Ae1 = 0x0001,
1132 Ae2 = 0x0002,
1133}
1134
1135#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1137#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
1138#[repr(u8)]
1139pub enum AesMode {
1140 Aes128 = 0x01,
1142 Aes192 = 0x02,
1144 Aes256 = 0x03,
1146}
1147
1148#[cfg(feature = "aes-crypto")]
1149impl AesMode {
1150 pub const fn salt_length(&self) -> usize {
1152 self.key_length() / 2
1153 }
1154
1155 pub const fn key_length(&self) -> usize {
1157 match self {
1158 Self::Aes128 => 16,
1159 Self::Aes192 => 24,
1160 Self::Aes256 => 32,
1161 }
1162 }
1163}
1164
1165#[cfg(test)]
1166mod test {
1167 #[test]
1168 fn system() {
1169 use super::System;
1170 assert_eq!(u8::from(System::Dos), 0u8);
1171 assert_eq!(System::Dos as u8, 0u8);
1172 assert_eq!(System::Unix as u8, 3u8);
1173 assert_eq!(u8::from(System::Unix), 3u8);
1174 assert_eq!(System::from(0), System::Dos);
1175 assert_eq!(System::from(3), System::Unix);
1176 assert_eq!(u8::from(System::Unknown), 4u8);
1177 assert_eq!(System::Unknown as u8, 4u8);
1178 }
1179
1180 #[test]
1181 fn sanitize() {
1182 use super::*;
1183 let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string();
1184 let data = ZipFileData {
1185 system: System::Dos,
1186 version_made_by: 0,
1187 encrypted: false,
1188 using_data_descriptor: false,
1189 is_utf8: true,
1190 compression_method: crate::compression::CompressionMethod::Stored,
1191 compression_level: None,
1192 last_modified_time: None,
1193 crc32: 0,
1194 compressed_size: 0,
1195 uncompressed_size: 0,
1196 file_name: file_name.clone().into_boxed_str(),
1197 file_name_raw: file_name.into_bytes().into_boxed_slice(),
1198 extra_field: None,
1199 central_extra_field: None,
1200 file_comment: String::with_capacity(0).into_boxed_str(),
1201 header_start: 0,
1202 extra_data_start: None,
1203 data_start: OnceLock::new(),
1204 central_header_start: 0,
1205 external_attributes: 0,
1206 large_file: false,
1207 aes_mode: None,
1208 aes_extra_data_start: 0,
1209 extra_fields: Vec::new(),
1210 };
1211 assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
1212 }
1213
1214 #[test]
1215 #[allow(clippy::unusual_byte_groupings)]
1216 fn datetime_default() {
1217 use super::DateTime;
1218 let dt = DateTime::default();
1219 assert_eq!(dt.timepart(), 0);
1220 assert_eq!(dt.datepart(), 0b0000000_0001_00001);
1221 }
1222
1223 #[test]
1224 #[allow(clippy::unusual_byte_groupings)]
1225 fn datetime_max() {
1226 use super::DateTime;
1227 let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
1228 assert_eq!(dt.timepart(), 0b10111_111011_11101);
1229 assert_eq!(dt.datepart(), 0b1111111_1100_11111);
1230 }
1231
1232 #[test]
1233 fn datetime_equality() {
1234 use super::DateTime;
1235
1236 let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1237 assert_eq!(
1238 dt,
1239 DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1240 );
1241 assert_ne!(dt, DateTime::default());
1242 }
1243
1244 #[test]
1245 fn datetime_order() {
1246 use std::cmp::Ordering;
1247
1248 use super::DateTime;
1249
1250 let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1251 assert_eq!(
1252 dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()),
1253 Ordering::Equal
1254 );
1255 assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap());
1257 assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap());
1258 assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap());
1260 assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap());
1261 assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap());
1263 assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap());
1264 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap());
1266 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap());
1267 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap());
1269 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap());
1270 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap());
1272 assert_eq!(
1273 dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()),
1274 Ordering::Equal
1275 );
1276 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap());
1277 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap());
1278 }
1279
1280 #[test]
1281 fn datetime_display() {
1282 use super::DateTime;
1283
1284 assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00");
1285 assert_eq!(
1286 format!(
1287 "{}",
1288 DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1289 ),
1290 "2018-11-17 10:38:30"
1291 );
1292 assert_eq!(
1293 format!(
1294 "{}",
1295 DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap()
1296 ),
1297 "2107-12-31 23:59:58"
1298 );
1299 }
1300
1301 #[test]
1302 fn datetime_bounds() {
1303 use super::DateTime;
1304
1305 assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
1306 assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
1307 assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
1308 assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());
1309
1310 assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
1311 assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
1312 assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
1313 assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
1314 assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
1315 assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
1316 assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
1317 assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());
1318
1319 assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok());
1320 assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok());
1321 assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err());
1322 assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok());
1323 assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok());
1324 assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err());
1325 assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok());
1326 assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok());
1327 assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err());
1328 assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok());
1329 assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok());
1330 assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok());
1331 assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err());
1332 assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok());
1333 assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok());
1334 assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err());
1335 assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok());
1336
1337 assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok());
1339 assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok());
1341 assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err());
1343 }
1344
1345 #[cfg(feature = "time")]
1346 use time::{format_description::well_known::Rfc3339, OffsetDateTime, PrimitiveDateTime};
1347
1348 #[cfg(feature = "time")]
1349 #[test]
1350 fn datetime_try_from_offset_datetime() {
1351 use time::macros::datetime;
1352
1353 use super::DateTime;
1354
1355 let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap();
1357 assert_eq!(dt.year(), 2018);
1358 assert_eq!(dt.month(), 11);
1359 assert_eq!(dt.day(), 17);
1360 assert_eq!(dt.hour(), 10);
1361 assert_eq!(dt.minute(), 38);
1362 assert_eq!(dt.second(), 30);
1363 }
1364
1365 #[cfg(feature = "time")]
1366 #[test]
1367 fn datetime_try_from_primitive_datetime() {
1368 use time::macros::datetime;
1369
1370 use super::DateTime;
1371
1372 let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap();
1374 assert_eq!(dt.year(), 2018);
1375 assert_eq!(dt.month(), 11);
1376 assert_eq!(dt.day(), 17);
1377 assert_eq!(dt.hour(), 10);
1378 assert_eq!(dt.minute(), 38);
1379 assert_eq!(dt.second(), 30);
1380 }
1381
1382 #[cfg(feature = "time")]
1383 #[test]
1384 fn datetime_try_from_bounds() {
1385 use super::DateTime;
1386 use time::macros::datetime;
1387
1388 assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err());
1390
1391 assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok());
1393
1394 assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok());
1396
1397 assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err());
1399 }
1400
1401 #[cfg(feature = "time")]
1402 #[test]
1403 fn offset_datetime_try_from_datetime() {
1404 use time::macros::datetime;
1405
1406 use super::DateTime;
1407
1408 let dt =
1410 OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1411 assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC));
1412 }
1413
1414 #[cfg(feature = "time")]
1415 #[test]
1416 fn primitive_datetime_try_from_datetime() {
1417 use time::macros::datetime;
1418
1419 use super::DateTime;
1420
1421 let dt =
1423 PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1424 assert_eq!(dt, datetime!(2018-11-17 10:38:30));
1425 }
1426
1427 #[cfg(feature = "time")]
1428 #[test]
1429 fn offset_datetime_try_from_bounds() {
1430 use super::DateTime;
1431
1432 assert!(OffsetDateTime::try_from(unsafe {
1434 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1435 })
1436 .is_err());
1437
1438 assert!(OffsetDateTime::try_from(unsafe {
1440 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1441 })
1442 .is_err());
1443 }
1444
1445 #[cfg(feature = "time")]
1446 #[test]
1447 fn primitive_datetime_try_from_bounds() {
1448 use super::DateTime;
1449
1450 assert!(PrimitiveDateTime::try_from(unsafe {
1452 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1453 })
1454 .is_err());
1455
1456 assert!(PrimitiveDateTime::try_from(unsafe {
1458 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1459 })
1460 .is_err());
1461 }
1462
1463 #[cfg(feature = "jiff-02")]
1464 #[test]
1465 fn datetime_try_from_civil_datetime() {
1466 use jiff::civil;
1467
1468 use super::DateTime;
1469
1470 let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).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 = "jiff-02")]
1481 #[test]
1482 fn datetime_try_from_civil_datetime_bounds() {
1483 use jiff::civil;
1484
1485 use super::DateTime;
1486
1487 assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err());
1489
1490 assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok());
1492
1493 assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok());
1495
1496 assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err());
1498 }
1499
1500 #[cfg(feature = "jiff-02")]
1501 #[test]
1502 fn civil_datetime_try_from_datetime() {
1503 use jiff::civil;
1504
1505 use super::DateTime;
1506
1507 let dt =
1509 civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1510 assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0));
1511 }
1512
1513 #[cfg(feature = "jiff-02")]
1514 #[test]
1515 fn civil_datetime_try_from_datetime_bounds() {
1516 use jiff::civil;
1517
1518 use super::DateTime;
1519
1520 assert!(civil::DateTime::try_from(unsafe {
1522 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1523 })
1524 .is_err());
1525
1526 assert!(civil::DateTime::try_from(unsafe {
1528 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1529 })
1530 .is_err());
1531 }
1532
1533 #[test]
1534 #[allow(deprecated)]
1535 fn time_conversion() {
1536 use super::DateTime;
1537 let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap();
1538 assert_eq!(dt.year(), 2018);
1539 assert_eq!(dt.month(), 11);
1540 assert_eq!(dt.day(), 17);
1541 assert_eq!(dt.hour(), 10);
1542 assert_eq!(dt.minute(), 38);
1543 assert_eq!(dt.second(), 30);
1544
1545 let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap();
1546 assert_eq!(dt.year(), 2018);
1547 assert_eq!(dt.month(), 11);
1548 assert_eq!(dt.day(), 17);
1549 assert_eq!(dt.hour(), 10);
1550 assert_eq!(dt.minute(), 38);
1551 assert_eq!(dt.second(), 30);
1552
1553 #[cfg(feature = "time")]
1554 assert_eq!(
1555 dt.to_time().unwrap().format(&Rfc3339).unwrap(),
1556 "2018-11-17T10:38:30Z"
1557 );
1558
1559 assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF));
1560 }
1561
1562 #[test]
1563 #[allow(deprecated)]
1564 fn time_out_of_bounds() {
1565 use super::DateTime;
1566 let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) };
1567 assert_eq!(dt.year(), 2107);
1568 assert_eq!(dt.month(), 15);
1569 assert_eq!(dt.day(), 31);
1570 assert_eq!(dt.hour(), 31);
1571 assert_eq!(dt.minute(), 63);
1572 assert_eq!(dt.second(), 62);
1573
1574 #[cfg(feature = "time")]
1575 assert!(dt.to_time().is_err());
1576
1577 let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) };
1578 assert_eq!(dt.year(), 1980);
1579 assert_eq!(dt.month(), 0);
1580 assert_eq!(dt.day(), 0);
1581 assert_eq!(dt.hour(), 0);
1582 assert_eq!(dt.minute(), 0);
1583 assert_eq!(dt.second(), 0);
1584
1585 #[cfg(feature = "time")]
1586 assert!(dt.to_time().is_err());
1587 }
1588
1589 #[cfg(feature = "time")]
1590 #[test]
1591 fn time_at_january() {
1592 use super::DateTime;
1593
1594 let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
1596
1597 assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok());
1598 }
1599}