binstall_zip/
types.rs

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