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