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 #[allow(useless_deprecated)]
444 #[deprecated(
445 since = "2.5.0",
446 note = "use `TryFrom<PrimitiveDateTime> for DateTime` instead"
447 )]
448 fn try_from(dt: OffsetDateTime) -> Result<Self, Self::Error> {
449 Self::try_from(PrimitiveDateTime::new(dt.date(), dt.time()))
450 }
451}
452
453#[cfg(feature = "time")]
454impl TryFrom<PrimitiveDateTime> for DateTime {
455 type Error = DateTimeRangeError;
456
457 fn try_from(dt: PrimitiveDateTime) -> Result<Self, Self::Error> {
458 Self::from_date_and_time(
459 dt.year().try_into()?,
460 dt.month().into(),
461 dt.day(),
462 dt.hour(),
463 dt.minute(),
464 dt.second(),
465 )
466 }
467}
468
469#[cfg(feature = "time")]
470impl TryFrom<DateTime> for OffsetDateTime {
471 type Error = ComponentRange;
472
473 #[allow(useless_deprecated)]
474 #[deprecated(
475 since = "2.5.0",
476 note = "use `TryFrom<DateTime> for PrimitiveDateTime` instead"
477 )]
478 fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
479 PrimitiveDateTime::try_from(dt).map(PrimitiveDateTime::assume_utc)
480 }
481}
482
483#[cfg(feature = "time")]
484impl TryFrom<DateTime> for PrimitiveDateTime {
485 type Error = ComponentRange;
486
487 fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
488 let date =
489 Date::from_calendar_date(dt.year() as i32, Month::try_from(dt.month())?, dt.day())?;
490 let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?;
491 Ok(PrimitiveDateTime::new(date, time))
492 }
493}
494
495pub const MIN_VERSION: u8 = 10;
496pub const DEFAULT_VERSION: u8 = 45;
497
498#[derive(Debug, Clone, Default)]
500pub struct ZipFileData {
501 pub system: System,
503 pub version_made_by: u8,
505 pub encrypted: bool,
507 pub is_utf8: bool,
509 pub using_data_descriptor: bool,
511 pub compression_method: crate::compression::CompressionMethod,
513 pub compression_level: Option<i64>,
515 pub last_modified_time: Option<DateTime>,
517 pub crc32: u32,
519 pub compressed_size: u64,
521 pub uncompressed_size: u64,
523 pub file_name: Box<str>,
525 pub file_name_raw: Box<[u8]>,
527 pub extra_field: Option<Arc<Vec<u8>>>,
529 pub central_extra_field: Option<Arc<Vec<u8>>>,
531 pub file_comment: Box<str>,
533 pub header_start: u64,
535 pub extra_data_start: Option<u64>,
537 pub central_header_start: u64,
541 pub data_start: OnceLock<u64>,
543 pub external_attributes: u32,
545 pub large_file: bool,
547 pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
549 pub aes_extra_data_start: u64,
551
552 pub extra_fields: Vec<ExtraField>,
554}
555
556impl ZipFileData {
557 pub fn data_start(&self, reader: &mut (impl Read + Seek + Sized)) -> ZipResult<u64> {
559 match self.data_start.get() {
560 Some(data_start) => Ok(*data_start),
561 None => Ok(find_data_start(self, reader)?),
562 }
563 }
564
565 #[allow(dead_code)]
566 pub fn is_dir(&self) -> bool {
567 is_dir(&self.file_name)
568 }
569
570 pub fn file_name_sanitized(&self) -> PathBuf {
571 let no_null_filename = match self.file_name.find('\0') {
572 Some(index) => &self.file_name[0..index],
573 None => &self.file_name,
574 }
575 .to_string();
576
577 let separator = path::MAIN_SEPARATOR;
581 let opposite_separator = match separator {
582 '/' => '\\',
583 _ => '/',
584 };
585 let filename =
586 no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string());
587
588 Path::new(&filename)
589 .components()
590 .filter(|component| matches!(*component, Component::Normal(..)))
591 .fold(PathBuf::new(), |mut path, ref cur| {
592 path.push(cur.as_os_str());
593 path
594 })
595 }
596
597 pub(crate) fn simplified_components(&self) -> Option<Vec<&OsStr>> {
599 if self.file_name.contains('\0') {
600 return None;
601 }
602 let input = Path::new(OsStr::new(&*self.file_name));
603 crate::path::simplified_components(input)
604 }
605
606 pub(crate) fn enclosed_name(&self) -> Option<PathBuf> {
607 if self.file_name.contains('\0') {
608 return None;
609 }
610 let path = PathBuf::from(self.file_name.to_string());
611 let mut depth = 0usize;
612 for component in path.components() {
613 match component {
614 Component::Prefix(_) | Component::RootDir => return None,
615 Component::ParentDir => depth = depth.checked_sub(1)?,
616 Component::Normal(_) => depth += 1,
617 Component::CurDir => (),
618 }
619 }
620 Some(path)
621 }
622
623 pub(crate) const fn unix_mode(&self) -> Option<u32> {
625 if self.external_attributes == 0 {
626 return None;
627 }
628
629 match self.system {
630 System::Unix => Some(self.external_attributes >> 16),
631 System::Dos => {
632 let mut mode = if 0x10 == (self.external_attributes & 0x10) {
634 ffi::S_IFDIR | 0o0775
635 } else {
636 ffi::S_IFREG | 0o0664
637 };
638 if 0x01 == (self.external_attributes & 0x01) {
639 mode &= 0o0555;
641 }
642 Some(mode)
643 }
644 _ => None,
645 }
646 }
647
648 pub fn version_needed(&self) -> u16 {
650 let compression_version: u16 = match self.compression_method {
651 CompressionMethod::Stored => MIN_VERSION.into(),
652 #[cfg(feature = "_deflate-any")]
653 CompressionMethod::Deflated => 20,
654 #[cfg(feature = "bzip2")]
655 CompressionMethod::Bzip2 => 46,
656 #[cfg(feature = "deflate64")]
657 CompressionMethod::Deflate64 => 21,
658 #[cfg(feature = "lzma")]
659 CompressionMethod::Lzma => 63,
660 #[cfg(feature = "xz")]
661 CompressionMethod::Xz => 63,
662 _ => DEFAULT_VERSION as u16,
664 };
665 let crypto_version: u16 = if self.aes_mode.is_some() {
666 51
667 } else if self.encrypted {
668 20
669 } else {
670 10
671 };
672 let misc_feature_version: u16 = if self.large_file {
673 45
674 } else if self
675 .unix_mode()
676 .is_some_and(|mode| mode & S_IFDIR == S_IFDIR)
677 {
678 20
680 } else {
681 10
682 };
683 compression_version
684 .max(crypto_version)
685 .max(misc_feature_version)
686 }
687 #[inline(always)]
688 pub(crate) fn extra_field_len(&self) -> usize {
689 self.extra_field
690 .as_ref()
691 .map(|v| v.len())
692 .unwrap_or_default()
693 }
694 #[inline(always)]
695 pub(crate) fn central_extra_field_len(&self) -> usize {
696 self.central_extra_field
697 .as_ref()
698 .map(|v| v.len())
699 .unwrap_or_default()
700 }
701
702 #[allow(clippy::too_many_arguments)]
703 pub(crate) fn initialize_local_block<S, T: FileOptionExtension>(
704 name: S,
705 options: &FileOptions<T>,
706 raw_values: ZipRawValues,
707 header_start: u64,
708 extra_data_start: Option<u64>,
709 aes_extra_data_start: u64,
710 compression_method: crate::compression::CompressionMethod,
711 aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
712 extra_field: &[u8],
713 ) -> Self
714 where
715 S: ToString,
716 {
717 let permissions = options.permissions.unwrap_or(0o100644);
718 let file_name: Box<str> = name.to_string().into_boxed_str();
719 let file_name_raw: Box<[u8]> = file_name.bytes().collect();
720 let mut local_block = ZipFileData {
721 system: System::Unix,
722 version_made_by: DEFAULT_VERSION,
723 encrypted: options.encrypt_with.is_some(),
724 using_data_descriptor: false,
725 is_utf8: !file_name.is_ascii(),
726 compression_method,
727 compression_level: options.compression_level,
728 last_modified_time: Some(options.last_modified_time),
729 crc32: raw_values.crc32,
730 compressed_size: raw_values.compressed_size,
731 uncompressed_size: raw_values.uncompressed_size,
732 file_name, file_name_raw,
734 extra_field: Some(extra_field.to_vec().into()),
735 central_extra_field: options.extended_options.central_extra_data().cloned(),
736 file_comment: String::with_capacity(0).into_boxed_str(),
737 header_start,
738 data_start: OnceLock::new(),
739 central_header_start: 0,
740 external_attributes: permissions << 16,
741 large_file: options.large_file,
742 aes_mode,
743 extra_fields: Vec::new(),
744 extra_data_start,
745 aes_extra_data_start,
746 };
747 local_block.version_made_by = local_block.version_needed() as u8;
748 local_block
749 }
750
751 pub(crate) fn from_local_block<R: std::io::Read>(
752 block: ZipLocalEntryBlock,
753 reader: &mut R,
754 ) -> ZipResult<Self> {
755 let ZipLocalEntryBlock {
756 version_made_by,
758 flags,
759 compression_method,
760 last_mod_time,
761 last_mod_date,
762 crc32,
763 compressed_size,
764 uncompressed_size,
765 file_name_length,
766 extra_field_length,
767 ..
768 } = block;
769
770 let encrypted: bool = flags & 1 == 1;
771 if encrypted {
772 return Err(ZipError::UnsupportedArchive(
773 "Encrypted files are not supported",
774 ));
775 }
776
777 let using_data_descriptor: bool = flags & (1 << 3) == 1 << 3;
780 if using_data_descriptor {
781 return Err(ZipError::UnsupportedArchive(
782 "The file length is not available in the local header",
783 ));
784 }
785
786 let is_utf8: bool = flags & (1 << 11) != 0;
788 let compression_method = crate::CompressionMethod::parse_from_u16(compression_method);
789 let file_name_length: usize = file_name_length.into();
790 let extra_field_length: usize = extra_field_length.into();
791
792 let mut file_name_raw = vec![0u8; file_name_length];
793 reader.read_exact(&mut file_name_raw)?;
794 let mut extra_field = vec![0u8; extra_field_length];
795 reader.read_exact(&mut extra_field)?;
796
797 let file_name: Box<str> = match is_utf8 {
798 true => String::from_utf8_lossy(&file_name_raw).into(),
799 false => file_name_raw.clone().from_cp437().into(),
800 };
801
802 let system: u8 = (version_made_by >> 8).try_into().unwrap();
803 Ok(ZipFileData {
804 system: System::from(system),
805 version_made_by: version_made_by as u8,
807 encrypted,
808 using_data_descriptor,
809 is_utf8,
810 compression_method,
811 compression_level: None,
812 last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
813 crc32,
814 compressed_size: compressed_size.into(),
815 uncompressed_size: uncompressed_size.into(),
816 file_name,
817 file_name_raw: file_name_raw.into(),
818 extra_field: Some(Arc::new(extra_field)),
819 central_extra_field: None,
820 file_comment: String::with_capacity(0).into_boxed_str(), header_start: 0,
824 data_start: OnceLock::new(),
825 central_header_start: 0,
826 external_attributes: 0,
830 large_file: false,
831 aes_mode: None,
832 extra_fields: Vec::new(),
833 extra_data_start: None,
834 aes_extra_data_start: 0,
835 })
836 }
837
838 fn is_utf8(&self) -> bool {
839 std::str::from_utf8(&self.file_name_raw).is_ok()
840 }
841
842 fn is_ascii(&self) -> bool {
843 self.file_name_raw.is_ascii()
844 }
845
846 fn flags(&self) -> u16 {
847 let utf8_bit: u16 = if self.is_utf8() && !self.is_ascii() {
848 1u16 << 11
849 } else {
850 0
851 };
852 let encrypted_bit: u16 = if self.encrypted { 1u16 << 0 } else { 0 };
853
854 utf8_bit | encrypted_bit
855 }
856
857 fn clamp_size_field(&self, field: u64) -> u32 {
858 if self.large_file {
859 spec::ZIP64_BYTES_THR as u32
860 } else {
861 field.min(spec::ZIP64_BYTES_THR).try_into().unwrap()
862 }
863 }
864
865 pub(crate) fn local_block(&self) -> ZipResult<ZipLocalEntryBlock> {
866 let compressed_size: u32 = self.clamp_size_field(self.compressed_size);
867 let uncompressed_size: u32 = self.clamp_size_field(self.uncompressed_size);
868 let extra_field_length: u16 = self
869 .extra_field_len()
870 .try_into()
871 .map_err(|_| invalid!("Extra data field is too large"))?;
872
873 let last_modified_time = self
874 .last_modified_time
875 .unwrap_or_else(DateTime::default_for_write);
876 Ok(ZipLocalEntryBlock {
877 magic: ZipLocalEntryBlock::MAGIC,
878 version_made_by: self.version_needed(),
879 flags: self.flags(),
880 compression_method: self.compression_method.serialize_to_u16(),
881 last_mod_time: last_modified_time.timepart(),
882 last_mod_date: last_modified_time.datepart(),
883 crc32: self.crc32,
884 compressed_size,
885 uncompressed_size,
886 file_name_length: self.file_name_raw.len().try_into().unwrap(),
887 extra_field_length,
888 })
889 }
890
891 pub(crate) fn block(&self) -> ZipResult<ZipCentralEntryBlock> {
892 let extra_field_len: u16 = self.extra_field_len().try_into().unwrap();
893 let central_extra_field_len: u16 = self.central_extra_field_len().try_into().unwrap();
894 let last_modified_time = self
895 .last_modified_time
896 .unwrap_or_else(DateTime::default_for_write);
897 let version_to_extract = self.version_needed();
898 let version_made_by = (self.version_made_by as u16).max(version_to_extract);
899 Ok(ZipCentralEntryBlock {
900 magic: ZipCentralEntryBlock::MAGIC,
901 version_made_by: ((self.system as u16) << 8) | version_made_by,
902 version_to_extract,
903 flags: self.flags(),
904 compression_method: self.compression_method.serialize_to_u16(),
905 last_mod_time: last_modified_time.timepart(),
906 last_mod_date: last_modified_time.datepart(),
907 crc32: self.crc32,
908 compressed_size: self
909 .compressed_size
910 .min(spec::ZIP64_BYTES_THR)
911 .try_into()
912 .unwrap(),
913 uncompressed_size: self
914 .uncompressed_size
915 .min(spec::ZIP64_BYTES_THR)
916 .try_into()
917 .unwrap(),
918 file_name_length: self.file_name_raw.len().try_into().unwrap(),
919 extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or(
920 invalid!("Extra field length in central directory exceeds 64KiB"),
921 )?,
922 file_comment_length: self.file_comment.len().try_into().unwrap(),
923 disk_number: 0,
924 internal_file_attributes: 0,
925 external_file_attributes: self.external_attributes,
926 offset: self
927 .header_start
928 .min(spec::ZIP64_BYTES_THR)
929 .try_into()
930 .unwrap(),
931 })
932 }
933
934 pub(crate) fn zip64_extra_field_block(&self) -> Option<Zip64ExtraFieldBlock> {
935 Zip64ExtraFieldBlock::maybe_new(
936 self.large_file,
937 self.uncompressed_size,
938 self.compressed_size,
939 self.header_start,
940 )
941 }
942}
943
944#[derive(Copy, Clone, Debug)]
945#[repr(packed, C)]
946pub(crate) struct ZipCentralEntryBlock {
947 magic: spec::Magic,
948 pub version_made_by: u16,
949 pub version_to_extract: u16,
950 pub flags: u16,
951 pub compression_method: u16,
952 pub last_mod_time: u16,
953 pub last_mod_date: u16,
954 pub crc32: u32,
955 pub compressed_size: u32,
956 pub uncompressed_size: u32,
957 pub file_name_length: u16,
958 pub extra_field_length: u16,
959 pub file_comment_length: u16,
960 pub disk_number: u16,
961 pub internal_file_attributes: u16,
962 pub external_file_attributes: u32,
963 pub offset: u32,
964}
965
966unsafe impl Pod for ZipCentralEntryBlock {}
967
968impl FixedSizeBlock for ZipCentralEntryBlock {
969 const MAGIC: spec::Magic = spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE;
970
971 #[inline(always)]
972 fn magic(self) -> spec::Magic {
973 self.magic
974 }
975
976 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header");
977
978 to_and_from_le![
979 (magic, spec::Magic),
980 (version_made_by, u16),
981 (version_to_extract, u16),
982 (flags, u16),
983 (compression_method, u16),
984 (last_mod_time, u16),
985 (last_mod_date, u16),
986 (crc32, u32),
987 (compressed_size, u32),
988 (uncompressed_size, u32),
989 (file_name_length, u16),
990 (extra_field_length, u16),
991 (file_comment_length, u16),
992 (disk_number, u16),
993 (internal_file_attributes, u16),
994 (external_file_attributes, u32),
995 (offset, u32),
996 ];
997}
998
999#[derive(Copy, Clone, Debug)]
1000#[repr(packed, C)]
1001pub(crate) struct ZipLocalEntryBlock {
1002 magic: spec::Magic,
1003 pub version_made_by: u16,
1004 pub flags: u16,
1005 pub compression_method: u16,
1006 pub last_mod_time: u16,
1007 pub last_mod_date: u16,
1008 pub crc32: u32,
1009 pub compressed_size: u32,
1010 pub uncompressed_size: u32,
1011 pub file_name_length: u16,
1012 pub extra_field_length: u16,
1013}
1014
1015unsafe impl Pod for ZipLocalEntryBlock {}
1016
1017impl FixedSizeBlock for ZipLocalEntryBlock {
1018 const MAGIC: spec::Magic = spec::Magic::LOCAL_FILE_HEADER_SIGNATURE;
1019
1020 #[inline(always)]
1021 fn magic(self) -> spec::Magic {
1022 self.magic
1023 }
1024
1025 const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header");
1026
1027 to_and_from_le![
1028 (magic, spec::Magic),
1029 (version_made_by, u16),
1030 (flags, u16),
1031 (compression_method, u16),
1032 (last_mod_time, u16),
1033 (last_mod_date, u16),
1034 (crc32, u32),
1035 (compressed_size, u32),
1036 (uncompressed_size, u32),
1037 (file_name_length, u16),
1038 (extra_field_length, u16),
1039 ];
1040}
1041
1042#[derive(Copy, Clone, Debug)]
1043pub(crate) struct Zip64ExtraFieldBlock {
1044 magic: spec::ExtraFieldMagic,
1045 size: u16,
1046 uncompressed_size: Option<u64>,
1047 compressed_size: Option<u64>,
1048 header_start: Option<u64>,
1049 }
1052
1053impl Zip64ExtraFieldBlock {
1054 pub(crate) fn maybe_new(
1055 large_file: bool,
1056 uncompressed_size: u64,
1057 compressed_size: u64,
1058 header_start: u64,
1059 ) -> Option<Zip64ExtraFieldBlock> {
1060 let mut size: u16 = 0;
1061 let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file {
1062 size += mem::size_of::<u64>() as u16;
1063 Some(uncompressed_size)
1064 } else {
1065 None
1066 };
1067 let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file {
1068 size += mem::size_of::<u64>() as u16;
1069 Some(compressed_size)
1070 } else {
1071 None
1072 };
1073 let header_start = if header_start >= ZIP64_BYTES_THR {
1074 size += mem::size_of::<u64>() as u16;
1075 Some(header_start)
1076 } else {
1077 None
1078 };
1079 if size == 0 {
1080 return None;
1081 }
1082
1083 Some(Zip64ExtraFieldBlock {
1084 magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
1085 size,
1086 uncompressed_size,
1087 compressed_size,
1088 header_start,
1089 })
1090 }
1091}
1092
1093impl Zip64ExtraFieldBlock {
1094 pub fn full_size(&self) -> usize {
1095 assert!(self.size > 0);
1096 self.size as usize + mem::size_of::<spec::ExtraFieldMagic>() + mem::size_of::<u16>()
1097 }
1098
1099 pub fn serialize(self) -> Box<[u8]> {
1100 let Self {
1101 magic,
1102 size,
1103 uncompressed_size,
1104 compressed_size,
1105 header_start,
1106 } = self;
1107
1108 let full_size = self.full_size();
1109
1110 let mut ret = Vec::with_capacity(full_size);
1111 ret.extend(magic.to_le_bytes());
1112 ret.extend(u16::to_le_bytes(size));
1113
1114 if let Some(uncompressed_size) = uncompressed_size {
1115 ret.extend(u64::to_le_bytes(uncompressed_size));
1116 }
1117 if let Some(compressed_size) = compressed_size {
1118 ret.extend(u64::to_le_bytes(compressed_size));
1119 }
1120 if let Some(header_start) = header_start {
1121 ret.extend(u64::to_le_bytes(header_start));
1122 }
1123 debug_assert_eq!(ret.len(), full_size);
1124
1125 ret.into_boxed_slice()
1126 }
1127}
1128
1129#[derive(Copy, Clone, Debug)]
1134#[repr(u16)]
1135pub enum AesVendorVersion {
1136 Ae1 = 0x0001,
1137 Ae2 = 0x0002,
1138}
1139
1140#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1142#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
1143#[repr(u8)]
1144pub enum AesMode {
1145 Aes128 = 0x01,
1147 Aes192 = 0x02,
1149 Aes256 = 0x03,
1151}
1152
1153#[cfg(feature = "aes-crypto")]
1154impl AesMode {
1155 pub const fn salt_length(&self) -> usize {
1157 self.key_length() / 2
1158 }
1159
1160 pub const fn key_length(&self) -> usize {
1162 match self {
1163 Self::Aes128 => 16,
1164 Self::Aes192 => 24,
1165 Self::Aes256 => 32,
1166 }
1167 }
1168}
1169
1170#[cfg(test)]
1171mod test {
1172 #[test]
1173 fn system() {
1174 use super::System;
1175 assert_eq!(u8::from(System::Dos), 0u8);
1176 assert_eq!(System::Dos as u8, 0u8);
1177 assert_eq!(System::Unix as u8, 3u8);
1178 assert_eq!(u8::from(System::Unix), 3u8);
1179 assert_eq!(System::from(0), System::Dos);
1180 assert_eq!(System::from(3), System::Unix);
1181 assert_eq!(u8::from(System::Unknown), 4u8);
1182 assert_eq!(System::Unknown as u8, 4u8);
1183 }
1184
1185 #[test]
1186 fn sanitize() {
1187 use super::*;
1188 let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string();
1189 let data = ZipFileData {
1190 system: System::Dos,
1191 version_made_by: 0,
1192 encrypted: false,
1193 using_data_descriptor: false,
1194 is_utf8: true,
1195 compression_method: crate::compression::CompressionMethod::Stored,
1196 compression_level: None,
1197 last_modified_time: None,
1198 crc32: 0,
1199 compressed_size: 0,
1200 uncompressed_size: 0,
1201 file_name: file_name.clone().into_boxed_str(),
1202 file_name_raw: file_name.into_bytes().into_boxed_slice(),
1203 extra_field: None,
1204 central_extra_field: None,
1205 file_comment: String::with_capacity(0).into_boxed_str(),
1206 header_start: 0,
1207 extra_data_start: None,
1208 data_start: OnceLock::new(),
1209 central_header_start: 0,
1210 external_attributes: 0,
1211 large_file: false,
1212 aes_mode: None,
1213 aes_extra_data_start: 0,
1214 extra_fields: Vec::new(),
1215 };
1216 assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
1217 }
1218
1219 #[test]
1220 #[allow(clippy::unusual_byte_groupings)]
1221 fn datetime_default() {
1222 use super::DateTime;
1223 let dt = DateTime::default();
1224 assert_eq!(dt.timepart(), 0);
1225 assert_eq!(dt.datepart(), 0b0000000_0001_00001);
1226 }
1227
1228 #[test]
1229 #[allow(clippy::unusual_byte_groupings)]
1230 fn datetime_max() {
1231 use super::DateTime;
1232 let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
1233 assert_eq!(dt.timepart(), 0b10111_111011_11101);
1234 assert_eq!(dt.datepart(), 0b1111111_1100_11111);
1235 }
1236
1237 #[test]
1238 fn datetime_equality() {
1239 use super::DateTime;
1240
1241 let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1242 assert_eq!(
1243 dt,
1244 DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1245 );
1246 assert_ne!(dt, DateTime::default());
1247 }
1248
1249 #[test]
1250 fn datetime_order() {
1251 use std::cmp::Ordering;
1252
1253 use super::DateTime;
1254
1255 let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1256 assert_eq!(
1257 dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()),
1258 Ordering::Equal
1259 );
1260 assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap());
1262 assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap());
1263 assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap());
1265 assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap());
1266 assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap());
1268 assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap());
1269 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap());
1271 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap());
1272 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap());
1274 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap());
1275 assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap());
1277 assert_eq!(
1278 dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()),
1279 Ordering::Equal
1280 );
1281 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap());
1282 assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap());
1283 }
1284
1285 #[test]
1286 fn datetime_display() {
1287 use super::DateTime;
1288
1289 assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00");
1290 assert_eq!(
1291 format!(
1292 "{}",
1293 DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1294 ),
1295 "2018-11-17 10:38:30"
1296 );
1297 assert_eq!(
1298 format!(
1299 "{}",
1300 DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap()
1301 ),
1302 "2107-12-31 23:59:58"
1303 );
1304 }
1305
1306 #[test]
1307 fn datetime_bounds() {
1308 use super::DateTime;
1309
1310 assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
1311 assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
1312 assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
1313 assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());
1314
1315 assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
1316 assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
1317 assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
1318 assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
1319 assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
1320 assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
1321 assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
1322 assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());
1323
1324 assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok());
1325 assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok());
1326 assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err());
1327 assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok());
1328 assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok());
1329 assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err());
1330 assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok());
1331 assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok());
1332 assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err());
1333 assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok());
1334 assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok());
1335 assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok());
1336 assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err());
1337 assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok());
1338 assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok());
1339 assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err());
1340 assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok());
1341
1342 assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok());
1344 assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok());
1346 assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err());
1348 }
1349
1350 #[cfg(feature = "time")]
1351 use time::{format_description::well_known::Rfc3339, OffsetDateTime, PrimitiveDateTime};
1352
1353 #[cfg(feature = "time")]
1354 #[test]
1355 fn datetime_try_from_offset_datetime() {
1356 use time::macros::datetime;
1357
1358 use super::DateTime;
1359
1360 let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap();
1362 assert_eq!(dt.year(), 2018);
1363 assert_eq!(dt.month(), 11);
1364 assert_eq!(dt.day(), 17);
1365 assert_eq!(dt.hour(), 10);
1366 assert_eq!(dt.minute(), 38);
1367 assert_eq!(dt.second(), 30);
1368 }
1369
1370 #[cfg(feature = "time")]
1371 #[test]
1372 fn datetime_try_from_primitive_datetime() {
1373 use time::macros::datetime;
1374
1375 use super::DateTime;
1376
1377 let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap();
1379 assert_eq!(dt.year(), 2018);
1380 assert_eq!(dt.month(), 11);
1381 assert_eq!(dt.day(), 17);
1382 assert_eq!(dt.hour(), 10);
1383 assert_eq!(dt.minute(), 38);
1384 assert_eq!(dt.second(), 30);
1385 }
1386
1387 #[cfg(feature = "time")]
1388 #[test]
1389 fn datetime_try_from_bounds() {
1390 use super::DateTime;
1391 use time::macros::datetime;
1392
1393 assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err());
1395
1396 assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok());
1398
1399 assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok());
1401
1402 assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err());
1404 }
1405
1406 #[cfg(feature = "time")]
1407 #[test]
1408 fn offset_datetime_try_from_datetime() {
1409 use time::macros::datetime;
1410
1411 use super::DateTime;
1412
1413 let dt =
1415 OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1416 assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC));
1417 }
1418
1419 #[cfg(feature = "time")]
1420 #[test]
1421 fn primitive_datetime_try_from_datetime() {
1422 use time::macros::datetime;
1423
1424 use super::DateTime;
1425
1426 let dt =
1428 PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1429 assert_eq!(dt, datetime!(2018-11-17 10:38:30));
1430 }
1431
1432 #[cfg(feature = "time")]
1433 #[test]
1434 fn offset_datetime_try_from_bounds() {
1435 use super::DateTime;
1436
1437 assert!(OffsetDateTime::try_from(unsafe {
1439 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1440 })
1441 .is_err());
1442
1443 assert!(OffsetDateTime::try_from(unsafe {
1445 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1446 })
1447 .is_err());
1448 }
1449
1450 #[cfg(feature = "time")]
1451 #[test]
1452 fn primitive_datetime_try_from_bounds() {
1453 use super::DateTime;
1454
1455 assert!(PrimitiveDateTime::try_from(unsafe {
1457 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1458 })
1459 .is_err());
1460
1461 assert!(PrimitiveDateTime::try_from(unsafe {
1463 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1464 })
1465 .is_err());
1466 }
1467
1468 #[cfg(feature = "jiff-02")]
1469 #[test]
1470 fn datetime_try_from_civil_datetime() {
1471 use jiff::civil;
1472
1473 use super::DateTime;
1474
1475 let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap();
1477 assert_eq!(dt.year(), 2018);
1478 assert_eq!(dt.month(), 11);
1479 assert_eq!(dt.day(), 17);
1480 assert_eq!(dt.hour(), 10);
1481 assert_eq!(dt.minute(), 38);
1482 assert_eq!(dt.second(), 30);
1483 }
1484
1485 #[cfg(feature = "jiff-02")]
1486 #[test]
1487 fn datetime_try_from_civil_datetime_bounds() {
1488 use jiff::civil;
1489
1490 use super::DateTime;
1491
1492 assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err());
1494
1495 assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok());
1497
1498 assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok());
1500
1501 assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err());
1503 }
1504
1505 #[cfg(feature = "jiff-02")]
1506 #[test]
1507 fn civil_datetime_try_from_datetime() {
1508 use jiff::civil;
1509
1510 use super::DateTime;
1511
1512 let dt =
1514 civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1515 assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0));
1516 }
1517
1518 #[cfg(feature = "jiff-02")]
1519 #[test]
1520 fn civil_datetime_try_from_datetime_bounds() {
1521 use jiff::civil;
1522
1523 use super::DateTime;
1524
1525 assert!(civil::DateTime::try_from(unsafe {
1527 DateTime::from_msdos_unchecked(0x0000, 0x0000)
1528 })
1529 .is_err());
1530
1531 assert!(civil::DateTime::try_from(unsafe {
1533 DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1534 })
1535 .is_err());
1536 }
1537
1538 #[test]
1539 #[allow(deprecated)]
1540 fn time_conversion() {
1541 use super::DateTime;
1542 let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap();
1543 assert_eq!(dt.year(), 2018);
1544 assert_eq!(dt.month(), 11);
1545 assert_eq!(dt.day(), 17);
1546 assert_eq!(dt.hour(), 10);
1547 assert_eq!(dt.minute(), 38);
1548 assert_eq!(dt.second(), 30);
1549
1550 let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap();
1551 assert_eq!(dt.year(), 2018);
1552 assert_eq!(dt.month(), 11);
1553 assert_eq!(dt.day(), 17);
1554 assert_eq!(dt.hour(), 10);
1555 assert_eq!(dt.minute(), 38);
1556 assert_eq!(dt.second(), 30);
1557
1558 #[cfg(feature = "time")]
1559 assert_eq!(
1560 dt.to_time().unwrap().format(&Rfc3339).unwrap(),
1561 "2018-11-17T10:38:30Z"
1562 );
1563
1564 assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF));
1565 }
1566
1567 #[test]
1568 #[allow(deprecated)]
1569 fn time_out_of_bounds() {
1570 use super::DateTime;
1571 let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) };
1572 assert_eq!(dt.year(), 2107);
1573 assert_eq!(dt.month(), 15);
1574 assert_eq!(dt.day(), 31);
1575 assert_eq!(dt.hour(), 31);
1576 assert_eq!(dt.minute(), 63);
1577 assert_eq!(dt.second(), 62);
1578
1579 #[cfg(feature = "time")]
1580 assert!(dt.to_time().is_err());
1581
1582 let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) };
1583 assert_eq!(dt.year(), 1980);
1584 assert_eq!(dt.month(), 0);
1585 assert_eq!(dt.day(), 0);
1586 assert_eq!(dt.hour(), 0);
1587 assert_eq!(dt.minute(), 0);
1588 assert_eq!(dt.second(), 0);
1589
1590 #[cfg(feature = "time")]
1591 assert!(dt.to_time().is_err());
1592 }
1593
1594 #[cfg(feature = "time")]
1595 #[test]
1596 fn time_at_january() {
1597 use super::DateTime;
1598
1599 let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
1601
1602 assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok());
1603 }
1604}