Merge "Upgrade zip to 0.6.6" into main
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
index 535489c..cb0d9ad 100644
--- a/.cargo_vcs_info.json
+++ b/.cargo_vcs_info.json
@@ -1,6 +1,6 @@
 {
   "git": {
-    "sha1": "f7dcc666b75256e766295589a5ac5dc5a9617c39"
+    "sha1": "21a20584bc9e05dfa4f3c5b0bc420a1389fae2c3"
   },
   "path_in_vcs": ""
 }
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
index 7c4953a..54b6fde 100644
--- a/Android.bp
+++ b/Android.bp
@@ -23,9 +23,9 @@
     host_supported: true,
     crate_name: "zip",
     cargo_env_compat: true,
-    cargo_pkg_version: "0.6.4",
+    cargo_pkg_version: "0.6.6",
     srcs: ["src/lib.rs"],
-    edition: "2018",
+    edition: "2021",
     features: [
         "deflate-zlib",
         "flate2",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd79e39..96c6994 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## [0.6.6]
+### Changed
+
+- Updated `aes` dependency to `0.8.2` (https://github.com/zip-rs/zip/pull/354)
+
+## [0.6.5]
+### Changed
+
+- Added experimental [`zip::unstable::write::FileOptions::with_deprecated_encryption`] API to enable encrypting files with PKWARE encryption.
+
 ## [0.6.4]
 
 ### Changed
diff --git a/Cargo.toml b/Cargo.toml
index 7a1b656..da0180e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,9 +10,10 @@
 # See Cargo.toml.orig for the original contents.
 
 [package]
-edition = "2018"
+edition = "2021"
+rust-version = "1.59.0"
 name = "zip"
-version = "0.6.4"
+version = "0.6.6"
 authors = [
     "Mathijs van de Nes <git@mathijs.vd-nes.nl>",
     "Marli Frost <marli@frost.red>",
@@ -38,7 +39,7 @@
 harness = false
 
 [dependencies.aes]
-version = "0.7.5"
+version = "0.8.2"
 optional = true
 
 [dependencies.byteorder]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
index caf6a07..510df9c 100644
--- a/Cargo.toml.orig
+++ b/Cargo.toml.orig
@@ -1,6 +1,6 @@
 [package]
 name = "zip"
-version = "0.6.4"
+version = "0.6.6"
 authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"]
 license = "MIT"
 repository = "https://github.com/zip-rs/zip.git"
@@ -8,10 +8,11 @@
 description = """
 Library to support the reading and writing of zip files.
 """
-edition = "2018"
+edition = "2021"
+rust-version = "1.59.0"
 
 [dependencies]
-aes = { version = "0.7.5", optional = true }
+aes = { version = "0.8.2", optional = true }
 byteorder = "1.4.3"
 bzip2 = { version = "0.4.3", optional = true }
 constant_time_eq = { version = "0.1.5", optional = true }
diff --git a/METADATA b/METADATA
index a547795..7248554 100644
--- a/METADATA
+++ b/METADATA
@@ -1,23 +1,20 @@
 # This project was upgraded with external_updater.
 # Usage: tools/external_updater/updater.sh update rust/crates/zip
-# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md
+# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md
 
 name: "zip"
 description: "Library to support the reading and writing of zip files."
 third_party {
-  url {
-    type: HOMEPAGE
-    value: "https://crates.io/crates/zip"
-  }
-  url {
-    type: ARCHIVE
-    value: "https://static.crates.io/crates/zip/zip-0.6.4.crate"
-  }
-  version: "0.6.4"
   license_type: NOTICE
   last_upgrade_date {
     year: 2023
-    month: 2
-    day: 17
+    month: 12
+    day: 4
+  }
+  homepage: "https://crates.io/crates/zip"
+  identifier {
+    type: "Archive"
+    value: "https://static.crates.io/crates/zip/zip-0.6.6.crate"
+    version: "0.6.6"
   }
 }
diff --git a/README.md b/README.md
index 3754a7c..f06cdbb 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,6 @@
 
 [Documentation](https://docs.rs/zip/0.6.3/zip/)
 
-> PSA: This version of the ZIP crate will not gain any new features,
->      and will only be updated if major security issues are found.
-
 Info
 ----
 
@@ -35,14 +32,14 @@
 
 ```toml
 [dependencies]
-zip = "0.6.4"
+zip = "0.6"
 ```
 
 Without the default features:
 
 ```toml
 [dependencies]
-zip = { version = "0.6.4", default-features = false }
+zip = { version = "0.6.6", default-features = false }
 ```
 
 The features available are:
diff --git a/src/aes_ctr.rs b/src/aes_ctr.rs
index 0f34335..211727c 100644
--- a/src/aes_ctr.rs
+++ b/src/aes_ctr.rs
@@ -2,10 +2,11 @@
 //!
 //! This was implemented since the zip specification requires the mode to not use a nonce and uses a
 //! different byte order (little endian) than NIST (big endian).
-//! See [AesCtrZipKeyStream](./struct.AesCtrZipKeyStream.html) for more information.
+//! See [AesCtrZipKeyStream] for more information.
 
 use aes::cipher::generic_array::GenericArray;
-use aes::{BlockEncrypt, NewBlockCipher};
+// use aes::{BlockEncrypt, NewBlockCipher};
+use aes::cipher::{BlockEncrypt, KeyInit};
 use byteorder::WriteBytesExt;
 use std::{any, fmt};
 
@@ -82,7 +83,7 @@
 impl<C> AesCtrZipKeyStream<C>
 where
     C: AesKind,
-    C::Cipher: NewBlockCipher,
+    C::Cipher: KeyInit,
 {
     /// Creates a new zip variant AES-CTR key stream.
     ///
@@ -150,14 +151,14 @@
 #[cfg(test)]
 mod tests {
     use super::{Aes128, Aes192, Aes256, AesCipher, AesCtrZipKeyStream, AesKind};
-    use aes::{BlockEncrypt, NewBlockCipher};
+    use aes::cipher::{BlockEncrypt, KeyInit};
 
     /// Checks whether `crypt_in_place` produces the correct plaintext after one use and yields the
     /// cipertext again after applying it again.
     fn roundtrip<Aes>(key: &[u8], ciphertext: &mut [u8], expected_plaintext: &[u8])
     where
         Aes: AesKind,
-        Aes::Cipher: NewBlockCipher + BlockEncrypt,
+        Aes::Cipher: KeyInit + BlockEncrypt,
     {
         let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key);
 
diff --git a/src/lib.rs b/src/lib.rs
index 0fee99c..e2228e5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,3 +42,14 @@
 mod types;
 pub mod write;
 mod zipcrypto;
+
+/// Unstable APIs
+///
+/// All APIs accessible by importing this module are unstable; They may be changed in patch releases.
+/// You MUST you an exact version specifier in `Cargo.toml`, to indicate the version of this API you're using:
+///
+/// ```toml
+/// [dependencies]
+/// zip = "=0.6.6"
+/// ```
+pub mod unstable;
diff --git a/src/read.rs b/src/read.rs
index dad20c2..b702b4f 100644
--- a/src/read.rs
+++ b/src/read.rs
@@ -13,7 +13,7 @@
 use std::borrow::Cow;
 use std::collections::HashMap;
 use std::io::{self, prelude::*};
-use std::path::{Component, Path};
+use std::path::Path;
 use std::sync::Arc;
 
 #[cfg(any(
@@ -29,10 +29,8 @@
 #[cfg(feature = "zstd")]
 use zstd::stream::read::Decoder as ZstdDecoder;
 
-mod ffi {
-    pub const S_IFDIR: u32 = 0o0040000;
-    pub const S_IFREG: u32 = 0o0100000;
-}
+/// Provides high level API for reading from a stream.
+pub(crate) mod stream;
 
 // Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely
 pub(crate) mod zip_archive {
@@ -650,12 +648,22 @@
     archive_offset: u64,
 ) -> ZipResult<ZipFileData> {
     let central_header_start = reader.stream_position()?;
+
     // Parse central header
     let signature = reader.read_u32::<LittleEndian>()?;
     if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE {
-        return Err(ZipError::InvalidArchive("Invalid Central Directory header"));
+        Err(ZipError::InvalidArchive("Invalid Central Directory header"))
+    } else {
+        central_header_to_zip_file_inner(reader, archive_offset, central_header_start)
     }
+}
 
+/// Parse a central directory entry to collect the information for the file.
+fn central_header_to_zip_file_inner<R: Read>(
+    reader: &mut R,
+    archive_offset: u64,
+    central_header_start: u64,
+) -> ZipResult<ZipFileData> {
     let version_made_by = reader.read_u16::<LittleEndian>()?;
     let _version_to_extract = reader.read_u16::<LittleEndian>()?;
     let flags = reader.read_u16::<LittleEndian>()?;
@@ -896,20 +904,7 @@
     /// to path-based exploits. It is recommended over
     /// [`ZipFile::mangled_name`].
     pub fn enclosed_name(&self) -> Option<&Path> {
-        if self.data.file_name.contains('\0') {
-            return None;
-        }
-        let path = Path::new(&self.data.file_name);
-        let mut depth = 0usize;
-        for component in path.components() {
-            match component {
-                Component::Prefix(_) | Component::RootDir => return None,
-                Component::ParentDir => depth = depth.checked_sub(1)?,
-                Component::Normal(_) => depth += 1,
-                Component::CurDir => (),
-            }
-        }
-        Some(path)
+        self.data.enclosed_name()
     }
 
     /// Get the comment of the file
@@ -952,27 +947,7 @@
 
     /// Get unix mode for the file
     pub fn unix_mode(&self) -> Option<u32> {
-        if self.data.external_attributes == 0 {
-            return None;
-        }
-
-        match self.data.system {
-            System::Unix => Some(self.data.external_attributes >> 16),
-            System::Dos => {
-                // Interpret MS-DOS directory bit
-                let mut mode = if 0x10 == (self.data.external_attributes & 0x10) {
-                    ffi::S_IFDIR | 0o0775
-                } else {
-                    ffi::S_IFREG | 0o0664
-                };
-                if 0x01 == (self.data.external_attributes & 0x01) {
-                    // Read-only bit; strip write permissions
-                    mode &= 0o0555;
-                }
-                Some(mode)
-            }
-            _ => None,
-        }
+        self.data.unix_mode()
     }
 
     /// Get the CRC32 hash of the original file
@@ -1029,10 +1004,9 @@
                 match reader.read(&mut buffer) {
                     Ok(0) => break,
                     Ok(_) => (),
-                    Err(e) => panic!(
-                        "Could not consume all of the output of the current ZipFile: {:?}",
-                        e
-                    ),
+                    Err(e) => {
+                        panic!("Could not consume all of the output of the current ZipFile: {e:?}")
+                    }
                 }
             }
         }
diff --git a/src/read/stream.rs b/src/read/stream.rs
new file mode 100644
index 0000000..5a01b23
--- /dev/null
+++ b/src/read/stream.rs
@@ -0,0 +1,372 @@
+use std::fs;
+use std::io::{self, Read};
+use std::path::Path;
+
+use super::{
+    central_header_to_zip_file_inner, read_zipfile_from_stream, spec, ZipError, ZipFile,
+    ZipFileData, ZipResult,
+};
+
+use byteorder::{LittleEndian, ReadBytesExt};
+
+/// Stream decoder for zip.
+#[derive(Debug)]
+pub struct ZipStreamReader<R>(R);
+
+impl<R> ZipStreamReader<R> {
+    /// Create a new ZipStreamReader
+    pub fn new(reader: R) -> Self {
+        Self(reader)
+    }
+}
+
+impl<R: Read> ZipStreamReader<R> {
+    fn parse_central_directory(&mut self) -> ZipResult<Option<ZipStreamFileMetadata>> {
+        // Give archive_offset and central_header_start dummy value 0, since
+        // they are not used in the output.
+        let archive_offset = 0;
+        let central_header_start = 0;
+
+        // Parse central header
+        let signature = self.0.read_u32::<LittleEndian>()?;
+        if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE {
+            Ok(None)
+        } else {
+            central_header_to_zip_file_inner(&mut self.0, archive_offset, central_header_start)
+                .map(ZipStreamFileMetadata)
+                .map(Some)
+        }
+    }
+
+    /// Iteraate over the stream and extract all file and their
+    /// metadata.
+    pub fn visit<V: ZipStreamVisitor>(mut self, visitor: &mut V) -> ZipResult<()> {
+        while let Some(mut file) = read_zipfile_from_stream(&mut self.0)? {
+            visitor.visit_file(&mut file)?;
+        }
+
+        while let Some(metadata) = self.parse_central_directory()? {
+            visitor.visit_additional_metadata(&metadata)?;
+        }
+
+        Ok(())
+    }
+
+    /// Extract a Zip archive into a directory, overwriting files if they
+    /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`].
+    ///
+    /// Extraction is not atomic; If an error is encountered, some of the files
+    /// may be left on disk.
+    pub fn extract<P: AsRef<Path>>(self, directory: P) -> ZipResult<()> {
+        struct Extractor<'a>(&'a Path);
+        impl ZipStreamVisitor for Extractor<'_> {
+            fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
+                let filepath = file
+                    .enclosed_name()
+                    .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
+
+                let outpath = self.0.join(filepath);
+
+                if file.name().ends_with('/') {
+                    fs::create_dir_all(&outpath)?;
+                } else {
+                    if let Some(p) = outpath.parent() {
+                        fs::create_dir_all(p)?;
+                    }
+                    let mut outfile = fs::File::create(&outpath)?;
+                    io::copy(file, &mut outfile)?;
+                }
+
+                Ok(())
+            }
+
+            #[allow(unused)]
+            fn visit_additional_metadata(
+                &mut self,
+                metadata: &ZipStreamFileMetadata,
+            ) -> ZipResult<()> {
+                #[cfg(unix)]
+                {
+                    let filepath = metadata
+                        .enclosed_name()
+                        .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
+
+                    let outpath = self.0.join(filepath);
+
+                    use std::os::unix::fs::PermissionsExt;
+                    if let Some(mode) = metadata.unix_mode() {
+                        fs::set_permissions(outpath, fs::Permissions::from_mode(mode))?;
+                    }
+                }
+
+                Ok(())
+            }
+        }
+
+        self.visit(&mut Extractor(directory.as_ref()))
+    }
+}
+
+/// Visitor for ZipStreamReader
+pub trait ZipStreamVisitor {
+    ///  * `file` - contains the content of the file and most of the metadata,
+    ///    except:
+    ///     - `comment`: set to an empty string
+    ///     - `data_start`: set to 0
+    ///     - `external_attributes`: `unix_mode()`: will return None
+    fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()>;
+
+    /// This function is guranteed to be called after all `visit_file`s.
+    ///
+    ///  * `metadata` - Provides missing metadata in `visit_file`.
+    fn visit_additional_metadata(&mut self, metadata: &ZipStreamFileMetadata) -> ZipResult<()>;
+}
+
+/// Additional metadata for the file.
+#[derive(Debug)]
+pub struct ZipStreamFileMetadata(ZipFileData);
+
+impl ZipStreamFileMetadata {
+    /// Get the name of the file
+    ///
+    /// # Warnings
+    ///
+    /// It is dangerous to use this name directly when extracting an archive.
+    /// It may contain an absolute path (`/etc/shadow`), or break out of the
+    /// current directory (`../runtime`). Carelessly writing to these paths
+    /// allows an attacker to craft a ZIP archive that will overwrite critical
+    /// files.
+    ///
+    /// You can use the [`ZipFile::enclosed_name`] method to validate the name
+    /// as a safe path.
+    pub fn name(&self) -> &str {
+        &self.0.file_name
+    }
+
+    /// Get the name of the file, in the raw (internal) byte representation.
+    ///
+    /// The encoding of this data is currently undefined.
+    pub fn name_raw(&self) -> &[u8] {
+        &self.0.file_name_raw
+    }
+
+    /// Rewrite the path, ignoring any path components with special meaning.
+    ///
+    /// - Absolute paths are made relative
+    /// - [`ParentDir`]s are ignored
+    /// - Truncates the filename at a NULL byte
+    ///
+    /// This is appropriate if you need to be able to extract *something* from
+    /// any archive, but will easily misrepresent trivial paths like
+    /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this,
+    /// [`ZipFile::enclosed_name`] is the better option in most scenarios.
+    ///
+    /// [`ParentDir`]: `Component::ParentDir`
+    pub fn mangled_name(&self) -> ::std::path::PathBuf {
+        self.0.file_name_sanitized()
+    }
+
+    /// Ensure the file path is safe to use as a [`Path`].
+    ///
+    /// - It can't contain NULL bytes
+    /// - It can't resolve to a path outside the current directory
+    ///   > `foo/../bar` is fine, `foo/../../bar` is not.
+    /// - It can't be an absolute path
+    ///
+    /// This will read well-formed ZIP files correctly, and is resistant
+    /// to path-based exploits. It is recommended over
+    /// [`ZipFile::mangled_name`].
+    pub fn enclosed_name(&self) -> Option<&Path> {
+        self.0.enclosed_name()
+    }
+
+    /// Returns whether the file is actually a directory
+    pub fn is_dir(&self) -> bool {
+        self.name()
+            .chars()
+            .rev()
+            .next()
+            .map_or(false, |c| c == '/' || c == '\\')
+    }
+
+    /// Returns whether the file is a regular file
+    pub fn is_file(&self) -> bool {
+        !self.is_dir()
+    }
+
+    /// Get the comment of the file
+    pub fn comment(&self) -> &str {
+        &self.0.file_comment
+    }
+
+    /// Get the starting offset of the data of the compressed file
+    pub fn data_start(&self) -> u64 {
+        self.0.data_start.load()
+    }
+
+    /// Get unix mode for the file
+    pub fn unix_mode(&self) -> Option<u32> {
+        self.0.unix_mode()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use std::collections::BTreeSet;
+    use std::io;
+
+    struct DummyVisitor;
+    impl ZipStreamVisitor for DummyVisitor {
+        fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> {
+            Ok(())
+        }
+
+        fn visit_additional_metadata(
+            &mut self,
+            _metadata: &ZipStreamFileMetadata,
+        ) -> ZipResult<()> {
+            Ok(())
+        }
+    }
+
+    #[derive(Default, Debug, Eq, PartialEq)]
+    struct CounterVisitor(u64, u64);
+    impl ZipStreamVisitor for CounterVisitor {
+        fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> {
+            self.0 += 1;
+            Ok(())
+        }
+
+        fn visit_additional_metadata(
+            &mut self,
+            _metadata: &ZipStreamFileMetadata,
+        ) -> ZipResult<()> {
+            self.1 += 1;
+            Ok(())
+        }
+    }
+
+    #[test]
+    fn invalid_offset() {
+        ZipStreamReader::new(io::Cursor::new(include_bytes!(
+            "../../tests/data/invalid_offset.zip"
+        )))
+        .visit(&mut DummyVisitor)
+        .unwrap_err();
+    }
+
+    #[test]
+    fn invalid_offset2() {
+        ZipStreamReader::new(io::Cursor::new(include_bytes!(
+            "../../tests/data/invalid_offset2.zip"
+        )))
+        .visit(&mut DummyVisitor)
+        .unwrap_err();
+    }
+
+    #[test]
+    fn zip_read_streaming() {
+        let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!(
+            "../../tests/data/mimetype.zip"
+        )));
+
+        #[derive(Default)]
+        struct V {
+            filenames: BTreeSet<Box<str>>,
+        }
+        impl ZipStreamVisitor for V {
+            fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
+                if file.is_file() {
+                    self.filenames.insert(file.name().into());
+                }
+
+                Ok(())
+            }
+            fn visit_additional_metadata(
+                &mut self,
+                metadata: &ZipStreamFileMetadata,
+            ) -> ZipResult<()> {
+                if metadata.is_file() {
+                    assert!(
+                        self.filenames.contains(metadata.name()),
+                        "{} is missing its file content",
+                        metadata.name()
+                    );
+                }
+
+                Ok(())
+            }
+        }
+
+        reader.visit(&mut V::default()).unwrap();
+    }
+
+    #[test]
+    fn file_and_dir_predicates() {
+        let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!(
+            "../../tests/data/files_and_dirs.zip"
+        )));
+
+        #[derive(Default)]
+        struct V {
+            filenames: BTreeSet<Box<str>>,
+        }
+        impl ZipStreamVisitor for V {
+            fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
+                let full_name = file.enclosed_name().unwrap();
+                let file_name = full_name.file_name().unwrap().to_str().unwrap();
+                assert!(
+                    (file_name.starts_with("dir") && file.is_dir())
+                        || (file_name.starts_with("file") && file.is_file())
+                );
+
+                if file.is_file() {
+                    self.filenames.insert(file.name().into());
+                }
+
+                Ok(())
+            }
+            fn visit_additional_metadata(
+                &mut self,
+                metadata: &ZipStreamFileMetadata,
+            ) -> ZipResult<()> {
+                if metadata.is_file() {
+                    assert!(
+                        self.filenames.contains(metadata.name()),
+                        "{} is missing its file content",
+                        metadata.name()
+                    );
+                }
+
+                Ok(())
+            }
+        }
+
+        reader.visit(&mut V::default()).unwrap();
+    }
+
+    /// test case to ensure we don't preemptively over allocate based on the
+    /// declared number of files in the CDE of an invalid zip when the number of
+    /// files declared is more than the alleged offset in the CDE
+    #[test]
+    fn invalid_cde_number_of_files_allocation_smaller_offset() {
+        ZipStreamReader::new(io::Cursor::new(include_bytes!(
+            "../../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
+        )))
+        .visit(&mut DummyVisitor)
+        .unwrap_err();
+    }
+
+    /// test case to ensure we don't preemptively over allocate based on the
+    /// declared number of files in the CDE of an invalid zip when the number of
+    /// files declared is less than the alleged offset in the CDE
+    #[test]
+    fn invalid_cde_number_of_files_allocation_greater_offset() {
+        ZipStreamReader::new(io::Cursor::new(include_bytes!(
+            "../../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
+        )))
+        .visit(&mut DummyVisitor)
+        .unwrap_err();
+    }
+}
diff --git a/src/types.rs b/src/types.rs
index ad3a570..c3d0a45 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,6 +1,6 @@
 //! Types that specify what is contained in a ZIP.
-#[cfg(feature = "time")]
-use std::convert::{TryFrom, TryInto};
+use std::path;
+
 #[cfg(not(any(
     all(target_arch = "arm", target_pointer_width = "32"),
     target_arch = "mips",
@@ -12,6 +12,11 @@
 #[cfg(doc)]
 use {crate::read::ZipFile, crate::write::FileOptions};
 
+mod ffi {
+    pub const S_IFDIR: u32 = 0o0040000;
+    pub const S_IFREG: u32 = 0o0100000;
+}
+
 #[cfg(any(
     all(target_arch = "arm", target_pointer_width = "32"),
     target_arch = "mips",
@@ -375,6 +380,48 @@
             })
     }
 
+    pub(crate) fn enclosed_name(&self) -> Option<&path::Path> {
+        if self.file_name.contains('\0') {
+            return None;
+        }
+        let path = path::Path::new(&self.file_name);
+        let mut depth = 0usize;
+        for component in path.components() {
+            match component {
+                path::Component::Prefix(_) | path::Component::RootDir => return None,
+                path::Component::ParentDir => depth = depth.checked_sub(1)?,
+                path::Component::Normal(_) => depth += 1,
+                path::Component::CurDir => (),
+            }
+        }
+        Some(path)
+    }
+
+    /// Get unix mode for the file
+    pub(crate) fn unix_mode(&self) -> Option<u32> {
+        if self.external_attributes == 0 {
+            return None;
+        }
+
+        match self.system {
+            System::Unix => Some(self.external_attributes >> 16),
+            System::Dos => {
+                // Interpret MS-DOS directory bit
+                let mut mode = if 0x10 == (self.external_attributes & 0x10) {
+                    ffi::S_IFDIR | 0o0775
+                } else {
+                    ffi::S_IFREG | 0o0664
+                };
+                if 0x01 == (self.external_attributes & 0x01) {
+                    // Read-only bit; strip write permissions
+                    mode &= 0o0555;
+                }
+                Some(mode)
+            }
+            _ => None,
+        }
+    }
+
     pub fn zip64_extension(&self) -> bool {
         self.uncompressed_size > 0xFFFFFFFF
             || self.compressed_size > 0xFFFFFFFF
@@ -510,27 +557,6 @@
 
     #[cfg(feature = "time")]
     #[test]
-    fn datetime_from_time_bounds() {
-        use std::convert::TryFrom;
-
-        use super::DateTime;
-        use time::macros::datetime;
-
-        // 1979-12-31 23:59:59
-        assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59 UTC)).is_err());
-
-        // 1980-01-01 00:00:00
-        assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00 UTC)).is_ok());
-
-        // 2107-12-31 23:59:59
-        assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59 UTC)).is_ok());
-
-        // 2108-01-01 00:00:00
-        assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00 UTC)).is_err());
-    }
-
-    #[cfg(feature = "time")]
-    #[test]
     fn datetime_try_from_bounds() {
         use std::convert::TryFrom;
 
diff --git a/src/unstable.rs b/src/unstable.rs
new file mode 100644
index 0000000..f8b46a9
--- /dev/null
+++ b/src/unstable.rs
@@ -0,0 +1,20 @@
+/// Provides high level API for reading from a stream.
+pub mod stream {
+    pub use crate::read::stream::*;
+}
+/// Types for creating ZIP archives.
+pub mod write {
+    use crate::write::FileOptions;
+    /// Unstable methods for [`FileOptions`].
+    pub trait FileOptionsExt {
+        /// Write the file with the given password using the deprecated ZipCrypto algorithm.
+        /// 
+        /// This is not recommended for new archives, as ZipCrypto is not secure.
+        fn with_deprecated_encryption(self, password: &[u8]) -> Self;
+    }
+    impl FileOptionsExt for FileOptions {
+        fn with_deprecated_encryption(self, password: &[u8]) -> Self {
+            self.with_deprecated_encryption(password)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/write.rs b/src/write.rs
index 14252b4..4cdc031 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -29,19 +29,37 @@
 #[cfg(feature = "zstd")]
 use zstd::stream::write::Encoder as ZstdEncoder;
 
+enum MaybeEncrypted<W> {
+    Unencrypted(W),
+    Encrypted(crate::zipcrypto::ZipCryptoWriter<W>),
+}
+impl<W: Write> Write for MaybeEncrypted<W> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        match self {
+            MaybeEncrypted::Unencrypted(w) => w.write(buf),
+            MaybeEncrypted::Encrypted(w) => w.write(buf),
+        }
+    }
+    fn flush(&mut self) -> io::Result<()> {
+        match self {
+            MaybeEncrypted::Unencrypted(w) => w.flush(),
+            MaybeEncrypted::Encrypted(w) => w.flush(),
+        }
+    }
+}
 enum GenericZipWriter<W: Write + io::Seek> {
     Closed,
-    Storer(W),
+    Storer(MaybeEncrypted<W>),
     #[cfg(any(
         feature = "deflate",
         feature = "deflate-miniz",
         feature = "deflate-zlib"
     ))]
-    Deflater(DeflateEncoder<W>),
+    Deflater(DeflateEncoder<MaybeEncrypted<W>>),
     #[cfg(feature = "bzip2")]
-    Bzip2(BzEncoder<W>),
+    Bzip2(BzEncoder<MaybeEncrypted<W>>),
     #[cfg(feature = "zstd")]
-    Zstd(ZstdEncoder<'static, W>),
+    Zstd(ZstdEncoder<'static, MaybeEncrypted<W>>),
 }
 // Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
 pub(crate) mod zip_writer {
@@ -108,6 +126,7 @@
     last_modified_time: DateTime,
     permissions: Option<u32>,
     large_file: bool,
+    encrypt_with: Option<crate::zipcrypto::ZipCryptoKeys>,
 }
 
 impl FileOptions {
@@ -171,6 +190,10 @@
         self.large_file = large;
         self
     }
+    pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> FileOptions {
+        self.encrypt_with = Some(crate::zipcrypto::ZipCryptoKeys::derive(password));
+        self
+    }
 }
 
 impl Default for FileOptions {
@@ -196,6 +219,7 @@
             last_modified_time: DateTime::default(),
             permissions: None,
             large_file: false,
+            encrypt_with: None,
         }
     }
 }
@@ -284,7 +308,7 @@
         let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it
 
         Ok(ZipWriter {
-            inner: GenericZipWriter::Storer(readwriter),
+            inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)),
             files,
             stats: Default::default(),
             writing_to_file: false,
@@ -302,7 +326,7 @@
     /// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
     pub fn new(inner: W) -> ZipWriter<W> {
         ZipWriter {
-            inner: GenericZipWriter::Storer(inner),
+            inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(inner)),
             files: Vec::new(),
             stats: Default::default(),
             writing_to_file: false,
@@ -355,7 +379,7 @@
             let mut file = ZipFileData {
                 system: System::Unix,
                 version_made_by: DEFAULT_VERSION,
-                encrypted: false,
+                encrypted: options.encrypt_with.is_some(),
                 using_data_descriptor: false,
                 compression_method: options.compression_method,
                 compression_level: options.compression_level,
@@ -385,7 +409,13 @@
 
             self.files.push(file);
         }
+        if let Some(keys) = options.encrypt_with {
+            let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { writer: core::mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(), buffer: vec![], keys };
+            let mut crypto_header = [0u8; 12];
 
+            zipwriter.write_all(&crypto_header)?;
+            self.inner = GenericZipWriter::Storer(MaybeEncrypted::Encrypted(zipwriter));
+        }
         Ok(())
     }
 
@@ -395,6 +425,14 @@
             self.end_extra_data()?;
         }
         self.inner.switch_to(CompressionMethod::Stored, None)?;
+        match core::mem::replace(&mut self.inner, GenericZipWriter::Closed) {
+            GenericZipWriter::Storer(MaybeEncrypted::Encrypted(writer)) => {
+                let crc32 = self.stats.hasher.clone().finalize();
+                self.inner = GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
+            }
+            GenericZipWriter::Storer(w) => self.inner = GenericZipWriter::Storer(w),
+            _ => unreachable!()
+        }
         let writer = self.inner.get_plain();
 
         if !self.writing_raw {
@@ -699,7 +737,7 @@
 
     /// Add a directory entry.
     ///
-    /// You can't write data to the file afterwards.
+    /// As directories have no content, you must not call [`ZipWriter::write`] before adding a new file.
     pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
     where
         S: Into<String>,
@@ -985,8 +1023,8 @@
 
     fn get_plain(&mut self) -> &mut W {
         match *self {
-            GenericZipWriter::Storer(ref mut w) => w,
-            _ => panic!("Should have switched to stored beforehand"),
+            GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w,
+            _ => panic!("Should have switched to stored and unencrypted beforehand"),
         }
     }
 
@@ -1009,8 +1047,8 @@
 
     fn unwrap(self) -> W {
         match self {
-            GenericZipWriter::Storer(w) => w,
-            _ => panic!("Should have switched to stored beforehand"),
+            GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(w)) => w,
+            _ => panic!("Should have switched to stored and unencrypted beforehand"),
         }
     }
 }
@@ -1058,7 +1096,7 @@
         1u16 << 11
     } else {
         0
-    };
+    } | if file.encrypted { 1u16 << 0 } else { 0 };
     writer.write_u16::<LittleEndian>(flag)?;
     // Compression method
     #[allow(deprecated)]
@@ -1133,7 +1171,7 @@
         1u16 << 11
     } else {
         0
-    };
+    } | if file.encrypted { 1u16 << 0 } else { 0 };
     writer.write_u16::<LittleEndian>(flag)?;
     // compression method
     #[allow(deprecated)]
@@ -1428,6 +1466,7 @@
             last_modified_time: DateTime::default(),
             permissions: Some(33188),
             large_file: false,
+            encrypt_with: None,
         };
         writer.start_file("mimetype", options).unwrap();
         writer
diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs
index 91d4039..c3696e4 100644
--- a/src/zipcrypto.rs
+++ b/src/zipcrypto.rs
@@ -6,7 +6,8 @@
 use std::num::Wrapping;
 
 /// A container to hold the current key state
-struct ZipCryptoKeys {
+#[derive(Clone, Copy)]
+pub(crate) struct ZipCryptoKeys {
     key_0: Wrapping<u32>,
     key_1: Wrapping<u32>,
     key_2: Wrapping<u32>,
@@ -49,6 +50,13 @@
     fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> {
         (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize])
     }
+    pub(crate) fn derive(password: &[u8]) -> ZipCryptoKeys {
+        let mut keys = ZipCryptoKeys::new();
+        for byte in password.iter() {
+            keys.update(*byte);
+        }
+        keys
+    }
 }
 
 /// A ZipCrypto reader with unverified password
@@ -70,17 +78,10 @@
     /// would be impossible to decrypt files that were encrypted with a
     /// password byte sequence that is unrepresentable in UTF-8.
     pub fn new(file: R, password: &[u8]) -> ZipCryptoReader<R> {
-        let mut result = ZipCryptoReader {
+        ZipCryptoReader {
             file,
-            keys: ZipCryptoKeys::new(),
-        };
-
-        // Key the cipher by updating the keys with the password.
-        for byte in password.iter() {
-            result.keys.update(*byte);
+            keys: ZipCryptoKeys::derive(password),
         }
-
-        result
     }
 
     /// Read the ZipCrypto header bytes and validate the password.
@@ -122,6 +123,31 @@
         Ok(Some(ZipCryptoReaderValid { reader: self }))
     }
 }
+pub(crate) struct ZipCryptoWriter<W> {
+    pub(crate) writer: W,
+    pub(crate) buffer: Vec<u8>,
+    pub(crate) keys: ZipCryptoKeys,
+}
+impl<W: std::io::Write> ZipCryptoWriter<W> {
+    pub(crate) fn finish(mut self, crc32: u32) -> std::io::Result<W> {
+        self.buffer[11] = (crc32 >> 24) as u8;
+        for byte in self.buffer.iter_mut() {
+            *byte = self.keys.encrypt_byte(*byte);
+        }
+        self.writer.write_all(&self.buffer)?;
+        self.writer.flush()?;
+        Ok(self.writer)
+    }
+}
+impl<W: std::io::Write> std::io::Write for ZipCryptoWriter<W> {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.buffer.extend_from_slice(buf);
+        Ok(buf.len())
+    }
+    fn flush(&mut self) -> std::io::Result<()> {
+        Ok(())
+    }
+}
 
 /// A ZipCrypto reader with verified password
 pub struct ZipCryptoReaderValid<R> {
diff --git a/tests/issue_234.rs b/tests/issue_234.rs
index bd01d1d..f8c1d2c 100644
--- a/tests/issue_234.rs
+++ b/tests/issue_234.rs
@@ -26,6 +26,6 @@
     let archive = zip::ZipArchive::new(reader);
     match archive {
         Err(ZipError::InvalidArchive(_)) => {}
-        value => panic!("Unexpected value: {:?}", value),
+        value => panic!("Unexpected value: {value:?}"),
     }
 }
diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs
index 6c4d6b8..d831c1e 100644
--- a/tests/zip_crypto.rs
+++ b/tests/zip_crypto.rs
@@ -21,6 +21,23 @@
 use std::io::Read;
 
 #[test]
+fn encrypting_file() {
+    use zip::unstable::write::FileOptionsExt;
+    use std::io::{Read, Write};
+    let mut buf = vec![0; 2048];
+    let mut archive = zip::write::ZipWriter::new(std::io::Cursor::new(&mut buf));
+    archive.start_file("name", zip::write::FileOptions::default().with_deprecated_encryption(b"password")).unwrap();
+    archive.write_all(b"test").unwrap();
+    archive.finish().unwrap();
+    drop(archive);
+    let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&mut buf)).unwrap();
+    let mut file = archive.by_index_decrypt(0, b"password").unwrap().unwrap();
+    let mut buf = Vec::new();
+    file.read_to_end(&mut buf).unwrap();
+    assert_eq!(buf, b"test");
+
+}
+#[test]
 fn encrypted_file() {
     let zip_file_bytes = &mut Cursor::new(vec![
         0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x54, 0xbd, 0xb5, 0x50, 0x2f,