zip_merge/
types.rs

1//! Types that specify what is contained in a ZIP.
2use cfg_if::cfg_if;
3use num_enum::{FromPrimitive, IntoPrimitive};
4use std::convert::TryInto;
5use std::ops::{Range, RangeInclusive};
6use std::path;
7
8#[cfg(doc)]
9use {crate::read::ZipFile, crate::write::FileOptions};
10
11mod ffi {
12    pub const S_IFDIR: u32 = 0o0040000;
13    pub const S_IFREG: u32 = 0o0100000;
14}
15
16cfg_if! {
17    if #[cfg(any(
18        all(target_arch = "arm", target_pointer_width = "32"),
19        target_arch = "mips",
20        target_arch = "powerpc"
21    ))] {
22        mod atomic {
23            use crossbeam_utils::sync::ShardedLock;
24            pub use std::sync::atomic::Ordering;
25
26            #[derive(Debug, Default)]
27            pub struct AtomicU64 {
28                value: ShardedLock<u64>,
29            }
30
31            impl AtomicU64 {
32                pub fn new(v: u64) -> Self {
33                    Self {
34                        value: ShardedLock::new(v),
35                    }
36                }
37                pub fn get_mut(&mut self) -> &mut u64 {
38                    self.value.get_mut().unwrap()
39                }
40                pub fn load(&self, _: Ordering) -> u64 {
41                    *self.value.read().unwrap()
42                }
43                pub fn store(&self, value: u64, _: Ordering) {
44                    *self.value.write().unwrap() = value;
45                }
46            }
47        }
48    } else {
49        use std::sync::atomic;
50    }
51
52}
53
54cfg_if! {
55    if #[cfg(feature = "time")] {
56        use crate::result::DateTimeRangeError;
57        use time::{
58            Date,
59            Month,
60            OffsetDateTime,
61            PrimitiveDateTime,
62            Time,
63            UtcOffset,
64            error::ComponentRange,
65        };
66    } else {
67        use std::time::SystemTime;
68    }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
72#[repr(u8)]
73pub enum System {
74    Dos = 0,
75    Unix = 3,
76    #[num_enum(default)]
77    Unknown,
78}
79
80/// Representation of a moment in time.
81///
82/// Zip files use an old format from DOS to store timestamps,
83/// with its own set of peculiarities.
84/// For example, it has a resolution of 2 seconds!
85///
86/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`],
87/// or read from one with [`ZipFile::last_modified`]
88///
89/// # Warning
90///
91/// Because there is no timezone associated with the [`DateTime`], they should ideally only
92/// be used for user-facing descriptions. This also means [`DateTime::to_time`] returns an
93/// [`OffsetDateTime`] (which is the equivalent of chrono's `NaiveDateTime`).
94///
95/// Modern zip files store more precise timestamps, which are ignored by [`crate::read::ZipArchive`],
96/// so keep in mind that these timestamps are unreliable. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904).
97#[derive(Debug, Clone, Copy)]
98pub struct DateTime {
99    year: u16,
100    month: u8,
101    day: u8,
102    hour: u8,
103    minute: u8,
104    second: u8,
105}
106
107impl ::std::default::Default for DateTime {
108    fn default() -> DateTime {
109        Self::zero()
110    }
111}
112
113impl DateTime {
114    /// Constructs a 'default' datetime of 1980-01-01 00:00:00
115    pub const fn zero() -> Self {
116        Self {
117            year: 1980,
118            month: 1,
119            day: 1,
120            hour: 0,
121            minute: 0,
122            second: 0,
123        }
124    }
125
126    /// The allowed range for years in a zip file's timestamp.
127    pub const YEAR_RANGE: RangeInclusive<u16> = 1980..=2107;
128    /// The allowed range for months in a zip file's timestamp.
129    pub const MONTH_RANGE: RangeInclusive<u8> = 1..=12;
130    /// The allowed range for days in a zip file's timestamp.
131    pub const DAY_RANGE: RangeInclusive<u8> = 1..=31;
132    /// The allowed range for hours in a zip file's timestamp.
133    pub const HOUR_RANGE: Range<u8> = 0..24;
134    /// The allowed range for minutes in a zip file's timestamp.
135    pub const MINUTE_RANGE: Range<u8> = 0..60;
136    /// The allowed range for seconds in a zip file's timestamp.
137    pub const SECOND_RANGE: RangeInclusive<u8> = 0..=60;
138
139    fn check_year(year: u16) -> Result<(), DateTimeRangeError> {
140        if Self::YEAR_RANGE.contains(&year) {
141            Ok(())
142        } else {
143            Err(DateTimeRangeError::InvalidYear(year, Self::YEAR_RANGE))
144        }
145    }
146
147    fn check_month(month: u8) -> Result<(), DateTimeRangeError> {
148        if Self::MONTH_RANGE.contains(&month) {
149            Ok(())
150        } else {
151            Err(DateTimeRangeError::InvalidMonth(month, Self::MONTH_RANGE))
152        }
153    }
154
155    fn check_day(day: u8) -> Result<(), DateTimeRangeError> {
156        if Self::DAY_RANGE.contains(&day) {
157            Ok(())
158        } else {
159            Err(DateTimeRangeError::InvalidDay(day, Self::DAY_RANGE))
160        }
161    }
162
163    fn check_hour(hour: u8) -> Result<(), DateTimeRangeError> {
164        if Self::HOUR_RANGE.contains(&hour) {
165            Ok(())
166        } else {
167            Err(DateTimeRangeError::InvalidHour(hour, Self::HOUR_RANGE))
168        }
169    }
170
171    fn check_minute(minute: u8) -> Result<(), DateTimeRangeError> {
172        if Self::MINUTE_RANGE.contains(&minute) {
173            Ok(())
174        } else {
175            Err(DateTimeRangeError::InvalidMinute(
176                minute,
177                Self::MINUTE_RANGE,
178            ))
179        }
180    }
181
182    fn check_second(second: u8) -> Result<(), DateTimeRangeError> {
183        if Self::SECOND_RANGE.contains(&second) {
184            Ok(())
185        } else {
186            Err(DateTimeRangeError::InvalidSecond(
187                second,
188                Self::SECOND_RANGE,
189            ))
190        }
191    }
192
193    /// Converts an msdos (u16, u16) pair to a DateTime object
194    pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime {
195        let seconds = (timepart & 0b0000000000011111) << 1;
196        let minutes = (timepart & 0b0000011111100000) >> 5;
197        let hours = (timepart & 0b1111100000000000) >> 11;
198        let days = datepart & 0b0000000000011111;
199        let months = (datepart & 0b0000000111100000) >> 5;
200        let years = (datepart & 0b1111111000000000) >> 9;
201
202        DateTime {
203            year: years + 1980,
204            month: months as u8,
205            day: days as u8,
206            hour: hours as u8,
207            minute: minutes as u8,
208            second: seconds as u8,
209        }
210    }
211
212    /// Constructs a DateTime from a specific date and time
213    ///
214    /// The bounds are:
215    /// * year: [1980, 2107]
216    /// * month: [1, 12]
217    /// * day: [1, 31]
218    /// * hour: [0, 23]
219    /// * minute: [0, 59]
220    /// * second: [0, 60]
221    pub fn from_date_and_time(
222        year: u16,
223        month: u8,
224        day: u8,
225        hour: u8,
226        minute: u8,
227        second: u8,
228    ) -> Result<DateTime, DateTimeRangeError> {
229        Self::check_year(year)?;
230        Self::check_month(month)?;
231        Self::check_day(day)?;
232        Self::check_hour(hour)?;
233        Self::check_minute(minute)?;
234        Self::check_second(second)?;
235        Ok(Self {
236            year,
237            month,
238            day,
239            hour,
240            minute,
241            second,
242        })
243    }
244
245    /// Gets the time portion of this datetime in the msdos representation
246    pub fn timepart(&self) -> u16 {
247        ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11)
248    }
249
250    /// Gets the date portion of this datetime in the msdos representation
251    pub fn datepart(&self) -> u16 {
252        (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9)
253    }
254
255    /// Converts the DateTime to a OffsetDateTime structure, given a UTC offset.
256    #[cfg(feature = "time")]
257    #[cfg_attr(docsrs, doc(cfg(feature = "time")))]
258    pub fn to_time(&self, offset: UtcOffset) -> Result<OffsetDateTime, ComponentRange> {
259        let date =
260            Date::from_calendar_date(self.year as i32, Month::try_from(self.month)?, self.day)?;
261        let time = Time::from_hms(self.hour, self.minute, self.second)?;
262        Ok(PrimitiveDateTime::new(date, time).assume_offset(offset))
263    }
264
265    /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018.
266    pub fn year(&self) -> u16 {
267        self.year
268    }
269
270    /// Get the month, where 1 = january and 12 = december
271    ///
272    /// # Warning
273    ///
274    /// When read from a zip file, this may not be a reasonable value
275    pub fn month(&self) -> u8 {
276        self.month
277    }
278
279    /// Get the day
280    ///
281    /// # Warning
282    ///
283    /// When read from a zip file, this may not be a reasonable value
284    pub fn day(&self) -> u8 {
285        self.day
286    }
287
288    /// Get the hour
289    ///
290    /// # Warning
291    ///
292    /// When read from a zip file, this may not be a reasonable value
293    pub fn hour(&self) -> u8 {
294        self.hour
295    }
296
297    /// Get the minute
298    ///
299    /// # Warning
300    ///
301    /// When read from a zip file, this may not be a reasonable value
302    pub fn minute(&self) -> u8 {
303        self.minute
304    }
305
306    /// Get the second
307    ///
308    /// # Warning
309    ///
310    /// When read from a zip file, this may not be a reasonable value
311    pub fn second(&self) -> u8 {
312        self.second
313    }
314}
315
316#[cfg(feature = "time")]
317#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
318impl TryFrom<OffsetDateTime> for DateTime {
319    type Error = DateTimeRangeError;
320
321    fn try_from(dt: OffsetDateTime) -> Result<Self, Self::Error> {
322        let year: u16 = dt
323            .year()
324            .try_into()
325            .map_err(|e| DateTimeRangeError::NumericConversion("year", e))?;
326        let month: u8 = dt.month().into();
327        let day: u8 = dt.day().into();
328        let hour: u8 = dt.hour().into();
329        let minute: u8 = dt.minute().into();
330        let second: u8 = dt.second().into();
331
332        Self::from_date_and_time(year, month, day, hour, minute, second)
333    }
334}
335
336pub const DEFAULT_VERSION: u8 = 46;
337
338/// A type like `AtomicU64` except it implements `Clone` and has predefined
339/// ordering.
340///
341/// It uses `Relaxed` ordering because it is not used for synchronisation.
342#[derive(Debug)]
343pub struct AtomicU64(atomic::AtomicU64);
344
345impl AtomicU64 {
346    pub fn new(v: u64) -> Self {
347        Self(atomic::AtomicU64::new(v))
348    }
349
350    pub fn load(&self) -> u64 {
351        self.0.load(atomic::Ordering::Relaxed)
352    }
353
354    pub fn store(&self, val: u64) {
355        self.0.store(val, atomic::Ordering::Relaxed)
356    }
357
358    pub fn get_mut(&mut self) -> &mut u64 {
359        self.0.get_mut()
360    }
361}
362
363impl Clone for AtomicU64 {
364    fn clone(&self) -> Self {
365        Self(atomic::AtomicU64::new(self.load()))
366    }
367}
368
369/// Structure representing a ZIP file.
370#[derive(Debug, Clone)]
371pub struct ZipFileData {
372    /// Compatibility of the file attribute information
373    pub system: System,
374    /// Specification version
375    pub version_made_by: u8,
376    /// True if the file is encrypted.
377    pub encrypted: bool,
378    /// True if the file uses a data-descriptor section
379    pub using_data_descriptor: bool,
380    /// Compression method used to store the file
381    pub compression_method: crate::compression::CompressionMethod,
382    /// Compression level to store the file
383    pub compression_level: Option<i32>,
384    /// Last modified time. This will only have a 2 second precision.
385    pub last_modified_time: DateTime,
386    /// CRC32 checksum
387    pub crc32: u32,
388    /// Size of the file in the ZIP
389    pub compressed_size: u64,
390    /// Size of the file when extracted
391    pub uncompressed_size: u64,
392    /// Name of the file
393    pub file_name: String,
394    /// Raw file name. To be used when file_name was incorrectly decoded.
395    pub file_name_raw: Vec<u8>,
396    /// Extra field usually used for storage expansion
397    pub extra_field: Vec<u8>,
398    /// File comment
399    pub file_comment: String,
400    /// Specifies where the local header of the file starts
401    pub header_start: u64,
402    /// Specifies where the central header of the file starts
403    ///
404    /// Note that when this is not known, it is set to 0
405    pub central_header_start: u64,
406    /// Specifies where the compressed data of the file starts
407    pub data_start: AtomicU64,
408    /// External file attributes
409    pub external_attributes: u32,
410    /// Reserve local ZIP64 extra field
411    pub large_file: bool,
412    /// AES mode if applicable
413    pub aes_mode: Option<(AesMode, AesVendorVersion)>,
414}
415
416impl ZipFileData {
417    pub fn file_name_sanitized(&self) -> ::std::path::PathBuf {
418        let no_null_filename = match self.file_name.find('\0') {
419            Some(index) => &self.file_name[0..index],
420            None => &self.file_name,
421        }
422        .to_string();
423
424        // zip files can contain both / and \ as separators regardless of the OS
425        // and as we want to return a sanitized PathBuf that only supports the
426        // OS separator let's convert incompatible separators to compatible ones
427        let separator = ::std::path::MAIN_SEPARATOR;
428        let opposite_separator = match separator {
429            '/' => '\\',
430            _ => '/',
431        };
432        let filename =
433            no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string());
434
435        ::std::path::Path::new(&filename)
436            .components()
437            .filter(|component| matches!(*component, ::std::path::Component::Normal(..)))
438            .fold(::std::path::PathBuf::new(), |mut path, ref cur| {
439                path.push(cur.as_os_str());
440                path
441            })
442    }
443
444    pub(crate) fn enclosed_name(&self) -> Option<&path::Path> {
445        if self.file_name.contains('\0') {
446            return None;
447        }
448        let path = path::Path::new(&self.file_name);
449        let mut depth = 0usize;
450        for component in path.components() {
451            match component {
452                path::Component::Prefix(_) | path::Component::RootDir => return None,
453                path::Component::ParentDir => depth = depth.checked_sub(1)?,
454                path::Component::Normal(_) => depth += 1,
455                path::Component::CurDir => (),
456            }
457        }
458        Some(path)
459    }
460
461    /// Get unix mode for the file
462    pub(crate) fn unix_mode(&self) -> Option<u32> {
463        if self.external_attributes == 0 {
464            return None;
465        }
466
467        match self.system {
468            System::Unix => Some(self.external_attributes >> 16),
469            System::Dos => {
470                // Interpret MS-DOS directory bit
471                let mut mode = if 0x10 == (self.external_attributes & 0x10) {
472                    ffi::S_IFDIR | 0o0775
473                } else {
474                    ffi::S_IFREG | 0o0664
475                };
476                if 0x01 == (self.external_attributes & 0x01) {
477                    // Read-only bit; strip write permissions
478                    mode &= 0o0555;
479                }
480                Some(mode)
481            }
482            _ => None,
483        }
484    }
485
486    pub fn zip64_extension(&self) -> bool {
487        self.uncompressed_size > 0xFFFFFFFF
488            || self.compressed_size > 0xFFFFFFFF
489            || self.header_start > 0xFFFFFFFF
490    }
491
492    pub fn version_needed(&self) -> u16 {
493        // higher versions matched first
494        match (self.zip64_extension(), self.compression_method) {
495            #[cfg(feature = "bzip2")]
496            (_, crate::compression::CompressionMethod::Bzip2) => 46,
497            (true, _) => 45,
498            _ => 20,
499        }
500    }
501}
502
503/// The encryption specification used to encrypt a file with AES.
504///
505/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
506/// does not make use of the CRC check.
507#[derive(Copy, Clone, Debug)]
508pub enum AesVendorVersion {
509    Ae1,
510    Ae2,
511}
512
513/// AES variant used.
514#[derive(Copy, Clone, Debug)]
515pub enum AesMode {
516    Aes128,
517    Aes192,
518    Aes256,
519}
520
521#[cfg(feature = "aes-crypto")]
522#[cfg_attr(docsrs, doc(cfg(feature = "aes-crypto")))]
523impl AesMode {
524    pub fn salt_length(&self) -> usize {
525        self.key_length() / 2
526    }
527
528    pub fn key_length(&self) -> usize {
529        match self {
530            Self::Aes128 => 16,
531            Self::Aes192 => 24,
532            Self::Aes256 => 32,
533        }
534    }
535}
536
537#[cfg(test)]
538mod test {
539    #[test]
540    fn system() {
541        use super::System;
542        assert_eq!(u8::from(System::Dos), 0u8);
543        assert_eq!(System::Dos as u8, 0u8);
544        assert_eq!(System::Unix as u8, 3u8);
545        assert_eq!(u8::from(System::Unix), 3u8);
546        assert_eq!(System::from(0), System::Dos);
547        assert_eq!(System::from(3), System::Unix);
548        assert_eq!(u8::from(System::Unknown), 4u8);
549        assert_eq!(System::Unknown as u8, 4u8);
550    }
551
552    #[test]
553    fn sanitize() {
554        use super::*;
555        let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string();
556        let data = ZipFileData {
557            system: System::Dos,
558            version_made_by: 0,
559            encrypted: false,
560            using_data_descriptor: false,
561            compression_method: crate::compression::CompressionMethod::Stored,
562            compression_level: None,
563            last_modified_time: DateTime::default(),
564            crc32: 0,
565            compressed_size: 0,
566            uncompressed_size: 0,
567            file_name: file_name.clone(),
568            file_name_raw: file_name.into_bytes(),
569            extra_field: Vec::new(),
570            file_comment: String::new(),
571            header_start: 0,
572            data_start: AtomicU64::new(0),
573            central_header_start: 0,
574            external_attributes: 0,
575            large_file: false,
576            aes_mode: None,
577        };
578        assert_eq!(
579            data.file_name_sanitized(),
580            ::std::path::PathBuf::from("path/etc/passwd")
581        );
582    }
583
584    #[test]
585    #[allow(clippy::unusual_byte_groupings)]
586    fn datetime_default() {
587        use super::DateTime;
588        let dt = DateTime::default();
589        assert_eq!(dt.timepart(), 0);
590        assert_eq!(dt.datepart(), 0b0000000_0001_00001);
591    }
592
593    #[test]
594    #[allow(clippy::unusual_byte_groupings)]
595    fn datetime_max() {
596        use super::DateTime;
597        let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap();
598        assert_eq!(dt.timepart(), 0b10111_111011_11110);
599        assert_eq!(dt.datepart(), 0b1111111_1100_11111);
600    }
601
602    #[test]
603    fn datetime_bounds() {
604        use super::DateTime;
605
606        assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
607        assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
608        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
609        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());
610
611        assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
612        assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
613        assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
614        assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
615        assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
616        assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
617        assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
618        assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());
619    }
620
621    #[cfg(feature = "time")]
622    use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};
623
624    #[cfg(feature = "time")]
625    #[test]
626    fn datetime_try_from_bounds() {
627        use std::convert::TryFrom;
628
629        use super::DateTime;
630        use time::macros::datetime;
631
632        // 1979-12-31 23:59:59
633        assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59 UTC)).is_err());
634
635        // 1980-01-01 00:00:00
636        assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00 UTC)).is_ok());
637
638        // 2107-12-31 23:59:59
639        assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59 UTC)).is_ok());
640
641        // 2108-01-01 00:00:00
642        assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00 UTC)).is_err());
643    }
644
645    #[test]
646    fn time_conversion() {
647        use super::DateTime;
648        let dt = DateTime::from_msdos(0x4D71, 0x54CF);
649        assert_eq!(dt.year(), 2018);
650        assert_eq!(dt.month(), 11);
651        assert_eq!(dt.day(), 17);
652        assert_eq!(dt.hour(), 10);
653        assert_eq!(dt.minute(), 38);
654        assert_eq!(dt.second(), 30);
655
656        #[cfg(feature = "time")]
657        assert_eq!(
658            dt.to_time(UtcOffset::UTC)
659                .unwrap()
660                .format(&Rfc3339)
661                .unwrap(),
662            "2018-11-17T10:38:30Z"
663        );
664    }
665
666    #[test]
667    fn time_out_of_bounds() {
668        use super::DateTime;
669        let dt = DateTime::from_msdos(0xFFFF, 0xFFFF);
670        assert_eq!(dt.year(), 2107);
671        assert_eq!(dt.month(), 15);
672        assert_eq!(dt.day(), 31);
673        assert_eq!(dt.hour(), 31);
674        assert_eq!(dt.minute(), 63);
675        assert_eq!(dt.second(), 62);
676
677        #[cfg(feature = "time")]
678        assert!(dt.to_time(UtcOffset::UTC).is_err());
679
680        let dt = DateTime::from_msdos(0x0000, 0x0000);
681        assert_eq!(dt.year(), 1980);
682        assert_eq!(dt.month(), 0);
683        assert_eq!(dt.day(), 0);
684        assert_eq!(dt.hour(), 0);
685        assert_eq!(dt.minute(), 0);
686        assert_eq!(dt.second(), 0);
687
688        #[cfg(feature = "time")]
689        assert!(dt.to_time(UtcOffset::UTC).is_err());
690    }
691
692    #[cfg(feature = "time")]
693    #[test]
694    fn time_at_january() {
695        use super::DateTime;
696        use std::convert::TryFrom;
697
698        // 2020-01-01 00:00:00
699        let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
700
701        assert!(DateTime::try_from(clock).is_ok());
702    }
703}