Snap for 8570526 from fe147278404dc350dc3ea8280405f8ec3f2a95bb to mainline-scheduling-release

Change-Id: Ic827044f89e8133753aa89e5a45e343f9dff4059
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
index 0342b7b..b50dc02 100644
--- a/.cargo_vcs_info.json
+++ b/.cargo_vcs_info.json
@@ -1,5 +1,6 @@
 {
   "git": {
-    "sha1": "08adaa5fbfcc4aae019a15068f3ae56a3fe7a877"
-  }
-}
+    "sha1": "4f7609cec700765525a537747c8f340dd1090aa0"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 1e7b395..6f0e4b9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -16,7 +16,7 @@
     strategy:
       matrix:
         os: [ubuntu-latest, macOS-latest, windows-latest]
-        rust: [stable, 1.34.0]
+        rust: [stable, 1.54.0]
 
     steps:
     - uses: actions/checkout@master
@@ -39,6 +39,25 @@
         command: test
         args: --all
 
+  clippy:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: nightly
+          override: true
+          components: clippy
+
+      - name: clippy
+        uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: --all-targets --all-features -- -D warnings
+
   check_fmt_and_docs:
     name: Checking fmt and docs
     runs-on: ubuntu-latest
@@ -54,4 +73,4 @@
       run: cargo fmt --all -- --check
 
     - name: Docs
-      run: cargo doc
\ No newline at end of file
+      run: cargo doc
diff --git a/Android.bp b/Android.bp
index e5b607d..c0b2349 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,4 +1,4 @@
-// This file is generated by cargo2android.py --run --device --dependencies --features=deflate-zlib.
+// This file is generated by cargo2android.py --config cargo2android.json.
 // Do not modify this file as changes will be overridden on upgrade.
 
 package {
@@ -22,6 +22,8 @@
     name: "libzip",
     host_supported: true,
     crate_name: "zip",
+    cargo_env_compat: true,
+    cargo_pkg_version: "0.6.2",
     srcs: ["src/lib.rs"],
     edition: "2018",
     features: [
@@ -32,23 +34,14 @@
         "libbyteorder",
         "libcrc32fast",
         "libflate2",
-        "libthiserror",
     ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
+    arch: {
+        arm: {
+            rustlibs: ["libcrossbeam_utils"],
+        },
+    },
 }
-
-// dependent_library ["feature_list"]
-//   byteorder-1.4.3 "default,std"
-//   cc-1.0.67
-//   cfg-if-0.1.10
-//   cfg-if-1.0.0
-//   crc32fast-1.2.1 "default,std"
-//   flate2-1.0.14 "any_zlib,libz-sys,zlib"
-//   libc-0.2.94 "default,std"
-//   libz-sys-1.1.3 "default,libc,stock-zlib"
-//   pkg-config-0.3.19
-//   proc-macro2-1.0.26 "default,proc-macro"
-//   quote-1.0.9 "default,proc-macro"
-//   syn-1.0.72 "clone-impls,default,derive,parsing,printing,proc-macro,quote"
-//   thiserror-1.0.24
-//   thiserror-impl-1.0.24
-//   unicode-xid-0.2.2 "default"
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 845634e..2290ec2 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,4 +1,3 @@
-
 # Contributor Covenant Code of Conduct
 
 ## Our Pledge
diff --git a/Cargo.toml b/Cargo.toml
index 7831bc5..fe1a031 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,17 +3,16 @@
 # When uploading crates to the registry Cargo will automatically
 # "normalize" Cargo.toml files for maximal compatibility
 # with all versions of Cargo and also rewrite `path` dependencies
-# to registry (e.g., crates.io) dependencies
+# to registry (e.g., crates.io) dependencies.
 #
-# If you believe there's an error in this file please file an
-# issue against the rust-lang/cargo repository. If you're
-# editing this file be aware that the upstream Cargo.toml
-# will likely look very different (and much more reasonable)
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
 
 [package]
 edition = "2018"
 name = "zip"
-version = "0.5.11"
+version = "0.6.2"
 authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"]
 description = "Library to support the reading and writing of zip files.\n"
 keywords = ["zip", "archive"]
@@ -23,38 +22,65 @@
 [[bench]]
 name = "read_entry"
 harness = false
+[dependencies.aes]
+version = "0.7.5"
+optional = true
+
 [dependencies.byteorder]
-version = "1.3"
+version = "1.4.3"
 
 [dependencies.bzip2]
-version = "0.3"
+version = "0.4.3"
+optional = true
+
+[dependencies.constant_time_eq]
+version = "0.1.5"
 optional = true
 
 [dependencies.crc32fast]
-version = "1.0"
+version = "1.3.2"
 
 [dependencies.flate2]
-version = ">=1.0.0, <=1.0.14"
+version = "1.0.22"
 optional = true
 default-features = false
 
-[dependencies.thiserror]
-version = "1.0"
+[dependencies.hmac]
+version = "0.12.1"
+features = ["reset"]
+optional = true
+
+[dependencies.pbkdf2]
+version = "0.10.1"
+optional = true
+
+[dependencies.sha1]
+version = "0.10.1"
+optional = true
 
 [dependencies.time]
-version = "0.1"
+version = "0.3.7"
+features = ["formatting", "macros"]
+optional = true
+
+[dependencies.zstd]
+version = "0.10.0"
 optional = true
 [dev-dependencies.bencher]
-version = "0.1"
+version = "0.1.5"
 
-[dev-dependencies.rand]
-version = "0.7"
+[dev-dependencies.getrandom]
+version = "0.2.5"
 
 [dev-dependencies.walkdir]
-version = "2"
+version = "2.3.2"
 
 [features]
-default = ["bzip2", "deflate", "time"]
+aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1"]
+default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"]
 deflate = ["flate2/rust_backend"]
 deflate-miniz = ["flate2/default"]
 deflate-zlib = ["flate2/zlib"]
+unreserved = []
+[target."cfg(any(all(target_arch = \"arm\", target_pointer_width = \"32\"), target_arch = \"mips\", target_arch = \"powerpc\"))".dependencies.crossbeam-utils]
+version = "0.8.8"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
index 68fe4ca..cc87821 100644
--- a/Cargo.toml.orig
+++ b/Cargo.toml.orig
@@ -1,6 +1,6 @@
 [package]
 name = "zip"
-version = "0.5.11"
+version = "0.6.2"
 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"
@@ -11,24 +11,33 @@
 edition = "2018"
 
 [dependencies]
-# FIXME(#170): flate2 1.0.15 has an MSRV of 1.36.0, breaking ours. We'll update when we know if this will be addressed
-flate2 = { version = ">=1.0.0, <=1.0.14", default-features = false, optional = true }
-time = { version = "0.1", optional = true }
-byteorder = "1.3"
-bzip2 = { version = "0.3", optional = true }
-crc32fast = "1.0"
-thiserror = "1.0"
+aes = { version = "0.7.5", optional = true }
+byteorder = "1.4.3"
+bzip2 = { version = "0.4.3", optional = true }
+constant_time_eq = { version = "0.1.5", optional = true }
+crc32fast = "1.3.2"
+flate2 = { version = "1.0.22", default-features = false, optional = true }
+hmac = { version = "0.12.1", optional = true, features = ["reset"] }
+pbkdf2 = {version = "0.10.1", optional = true }
+sha1 = {version = "0.10.1", optional = true }
+time = { version = "0.3.7", features = ["formatting", "macros" ], optional = true }
+zstd = { version = "0.10.0", optional = true }
+
+[target.'cfg(any(all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc"))'.dependencies]
+crossbeam-utils = "0.8.8"
 
 [dev-dependencies]
-bencher = "0.1"
-rand = "0.7"
-walkdir = "2"
+bencher = "0.1.5"
+getrandom = "0.2.5"
+walkdir = "2.3.2"
 
 [features]
+aes-crypto = [ "aes", "constant_time_eq", "hmac", "pbkdf2", "sha1" ]
 deflate = ["flate2/rust_backend"]
 deflate-miniz = ["flate2/default"]
 deflate-zlib = ["flate2/zlib"]
-default = ["bzip2", "deflate", "time"]
+unreserved = []
+default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"]
 
 [[bench]]
 name = "read_entry"
diff --git a/METADATA b/METADATA
index c0a206d..c6e83c0 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@
   }
   url {
     type: ARCHIVE
-    value: "https://static.crates.io/crates/zip/zip-0.5.11.crate"
+    value: "https://static.crates.io/crates/zip/zip-0.6.2.crate"
   }
-  version: "0.5.11"
+  version: "0.6.2"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2021
+    year: 2022
     month: 4
-    day: 1
+    day: 13
   }
 }
diff --git a/README.md b/README.md
index 79d2dcc..7db31d4 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,17 @@
 
 [![Build Status](https://img.shields.io/github/workflow/status/zip-rs/zip/CI)](https://github.com/zip-rs/zip/actions?query=branch%3Amaster+workflow%3ACI)
 [![Crates.io version](https://img.shields.io/crates/v/zip.svg)](https://crates.io/crates/zip)
+[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/rQ7H9cSsF4)
 
-[Documentation](https://docs.rs/zip/0.5.10/zip/)
+[Documentation](https://docs.rs/zip/0.6.2/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
 ----
 
+
 A zip library for rust which supports reading and writing of simple ZIP files.
 
 Supported compression formats:
@@ -17,6 +21,7 @@
 * stored (i.e. none)
 * deflate
 * bzip2
+* zstd
 
 Currently unsupported zip extensions:
 
@@ -30,28 +35,30 @@
 
 ```toml
 [dependencies]
-zip = "0.5"
+zip = "0.6.2"
 ```
 
 Without the default features:
 
 ```toml
 [dependencies]
-zip = { version = "0.5", default-features = false }
+zip = { version = "0.6.2", default-features = false }
 ```
 
 The features available are:
 
-* `deflate`: Enables the deflate compression algorithm, which is the default for zipfiles
+* `aes-crypto`: Enables decryption of files which were encrypted with AES. Supports AE-1 and AE-2 methods.
+* `deflate`: Enables the deflate compression algorithm, which is the default for zip files.
 * `bzip2`: Enables the BZip2 compression algorithm.
 * `time`: Enables features using the [time](https://github.com/rust-lang-deprecated/time) crate.
+* `zstd`: Enables the Zstandard compression algorithm.
 
 All of these are enabled by default.
 
 MSRV
 ----
 
-Our current Minimum Supported Rust Version is **1.34.0**. When adding features,
+Our current Minimum Supported Rust Version is **1.54.0**. When adding features,
 we will follow these guidelines:
 
 - We will always support the latest four minor Rust versions. This gives you a 6
diff --git a/TEST_MAPPING b/TEST_MAPPING
index d115d6f..7325ef4 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -3,6 +3,35 @@
   "presubmit": [
     {
       "name": "ZipFuseTest"
+    },
+    {
+      "name": "libapkverify.integration_test"
+    },
+    {
+      "name": "libapkverify.test"
+    },
+    {
+      "name": "microdroid_manager_test"
+    },
+    {
+      "name": "virtualizationservice_device_test"
+    }
+  ],
+  "presubmit-rust": [
+    {
+      "name": "ZipFuseTest"
+    },
+    {
+      "name": "libapkverify.integration_test"
+    },
+    {
+      "name": "libapkverify.test"
+    },
+    {
+      "name": "microdroid_manager_test"
+    },
+    {
+      "name": "virtualizationservice_device_test"
     }
   ]
 }
diff --git a/benches/read_entry.rs b/benches/read_entry.rs
index 25c0b94..af9affe 100644
--- a/benches/read_entry.rs
+++ b/benches/read_entry.rs
@@ -3,7 +3,7 @@
 use std::io::{Cursor, Read, Write};
 
 use bencher::Bencher;
-use rand::Rng;
+use getrandom::getrandom;
 use zip::{ZipArchive, ZipWriter};
 
 fn generate_random_archive(size: usize) -> Vec<u8> {
@@ -14,7 +14,7 @@
 
     writer.start_file("random.dat", options).unwrap();
     let mut bytes = vec![0u8; size];
-    rand::thread_rng().fill_bytes(&mut bytes);
+    getrandom(&mut bytes).unwrap();
     writer.write_all(&bytes).unwrap();
 
     writer.finish().unwrap().into_inner()
diff --git a/cargo2android.json b/cargo2android.json
new file mode 100644
index 0000000..42d8c8b
--- /dev/null
+++ b/cargo2android.json
@@ -0,0 +1,11 @@
+{
+  "apex-available": [
+    "//apex_available:platform",
+    "com.android.virt"
+  ],
+  "dependencies": true,
+  "device": true,
+  "patch": "patches/Android.bp.diff",
+  "features": "deflate-zlib",
+  "run": true
+}
diff --git a/examples/extract.rs b/examples/extract.rs
index 05c5a4a..7b8860c 100644
--- a/examples/extract.rs
+++ b/examples/extract.rs
@@ -30,7 +30,7 @@
             }
         }
 
-        if (&*file.name()).ends_with('/') {
+        if (*file.name()).ends_with('/') {
             println!("File {} extracted to \"{}\"", i, outpath.display());
             fs::create_dir_all(&outpath).unwrap();
         } else {
@@ -59,5 +59,6 @@
             }
         }
     }
-    return 0;
+
+    0
 }
diff --git a/examples/extract_lorem.rs b/examples/extract_lorem.rs
index 89e33ef..a34a04f 100644
--- a/examples/extract_lorem.rs
+++ b/examples/extract_lorem.rs
@@ -27,5 +27,5 @@
     file.read_to_string(&mut contents).unwrap();
     println!("{}", contents);
 
-    return 0;
+    0
 }
diff --git a/examples/file_info.rs b/examples/file_info.rs
index 315b5c3..64969b6 100644
--- a/examples/file_info.rs
+++ b/examples/file_info.rs
@@ -34,7 +34,7 @@
             }
         }
 
-        if (&*file.name()).ends_with('/') {
+        if (*file.name()).ends_with('/') {
             println!(
                 "Entry {} is a directory with name \"{}\"",
                 i,
@@ -49,5 +49,6 @@
             );
         }
     }
-    return 0;
+
+    0
 }
diff --git a/examples/stdin_info.rs b/examples/stdin_info.rs
index 606944c..10d7aa8 100644
--- a/examples/stdin_info.rs
+++ b/examples/stdin_info.rs
@@ -30,5 +30,6 @@
             }
         }
     }
-    return 0;
+
+    0
 }
diff --git a/examples/write_dir.rs b/examples/write_dir.rs
index 793bd6b..8cc561f 100644
--- a/examples/write_dir.rs
+++ b/examples/write_dir.rs
@@ -32,6 +32,11 @@
 #[cfg(not(feature = "bzip2"))]
 const METHOD_BZIP2: Option<zip::CompressionMethod> = None;
 
+#[cfg(feature = "zstd")]
+const METHOD_ZSTD: Option<zip::CompressionMethod> = Some(zip::CompressionMethod::Zstd);
+#[cfg(not(feature = "zstd"))]
+const METHOD_ZSTD: Option<zip::CompressionMethod> = None;
+
 fn real_main() -> i32 {
     let args: Vec<_> = std::env::args().collect();
     if args.len() < 3 {
@@ -44,7 +49,7 @@
 
     let src_dir = &*args[1];
     let dst_file = &*args[2];
-    for &method in [METHOD_STORED, METHOD_DEFLATED, METHOD_BZIP2].iter() {
+    for &method in [METHOD_STORED, METHOD_DEFLATED, METHOD_BZIP2, METHOD_ZSTD].iter() {
         if method.is_none() {
             continue;
         }
@@ -54,7 +59,7 @@
         }
     }
 
-    return 0;
+    0
 }
 
 fn zip_dir<T>(
@@ -87,7 +92,7 @@
             f.read_to_end(&mut buffer)?;
             zip.write_all(&*buffer)?;
             buffer.clear();
-        } else if name.as_os_str().len() != 0 {
+        } else if !name.as_os_str().is_empty() {
             // Only if not root! Avoids path spec / warning
             // and mapname conversion failed error on unzip
             println!("adding dir {:?} as {:?} ...", path, name);
@@ -111,7 +116,7 @@
     let path = Path::new(dst_file);
     let file = File::create(&path).unwrap();
 
-    let walkdir = WalkDir::new(src_dir.to_string());
+    let walkdir = WalkDir::new(src_dir);
     let it = walkdir.into_iter();
 
     zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method)?;
diff --git a/examples/write_sample.rs b/examples/write_sample.rs
index 4ef5ce3..b574950 100644
--- a/examples/write_sample.rs
+++ b/examples/write_sample.rs
@@ -18,7 +18,7 @@
         Err(e) => println!("Error: {:?}", e),
     }
 
-    return 0;
+    0
 }
 
 fn doit(filename: &str) -> zip::result::ZipResult<()> {
@@ -42,7 +42,7 @@
     Ok(())
 }
 
-const LOREM_IPSUM : &'static [u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
+const LOREM_IPSUM : &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
 molestie. Proin blandit ornare dui, a tempor nisl accumsan in. Praesent a consequat felis. Morbi metus diam, auctor in auctor vel, feugiat id odio. Curabitur ex ex,
 dictum quis auctor quis, suscipit id lorem. Aliquam vestibulum dolor nec enim vehicula, porta tristique augue tincidunt. Vivamus ut gravida est. Sed pellentesque, dolor
 vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam id lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
diff --git a/patches/Android.bp.diff b/patches/Android.bp.diff
new file mode 100644
index 0000000..c0037cc
--- /dev/null
+++ b/patches/Android.bp.diff
@@ -0,0 +1,14 @@
+diff --git a/Android.bp b/Android.bp
+index 2373253..c0b2349 100644
+--- a/Android.bp
++++ b/Android.bp
+@@ -39,4 +39,9 @@ rust_library {
+         "//apex_available:platform",
+         "com.android.virt",
+     ],
++    arch: {
++        arm: {
++            rustlibs: ["libcrossbeam_utils"],
++        },
++    },
+ }
diff --git a/src/aes.rs b/src/aes.rs
new file mode 100644
index 0000000..8997705
--- /dev/null
+++ b/src/aes.rs
@@ -0,0 +1,185 @@
+//! Implementation of the AES decryption for zip files.
+//!
+//! This was implemented according to the [WinZip specification](https://www.winzip.com/win/en/aes_info.html).
+//! Note that using CRC with AES depends on the used encryption specification, AE-1 or AE-2.
+//! If the file is marked as encrypted with AE-2 the CRC field is ignored, even if it isn't set to 0.
+
+use crate::aes_ctr;
+use crate::types::AesMode;
+use constant_time_eq::constant_time_eq;
+use hmac::{Hmac, Mac};
+use sha1::Sha1;
+use std::io::{self, Read};
+
+/// The length of the password verifcation value in bytes
+const PWD_VERIFY_LENGTH: usize = 2;
+/// The length of the authentication code in bytes
+const AUTH_CODE_LENGTH: usize = 10;
+/// The number of iterations used with PBKDF2
+const ITERATION_COUNT: u32 = 1000;
+
+/// Create a AesCipher depending on the used `AesMode` and the given `key`.
+///
+/// # Panics
+///
+/// This panics if `key` doesn't have the correct size for the chosen aes mode.
+fn cipher_from_mode(aes_mode: AesMode, key: &[u8]) -> Box<dyn aes_ctr::AesCipher> {
+    match aes_mode {
+        AesMode::Aes128 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes128>::new(key))
+            as Box<dyn aes_ctr::AesCipher>,
+        AesMode::Aes192 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes192>::new(key))
+            as Box<dyn aes_ctr::AesCipher>,
+        AesMode::Aes256 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes256>::new(key))
+            as Box<dyn aes_ctr::AesCipher>,
+    }
+}
+
+// An aes encrypted file starts with a salt, whose length depends on the used aes mode
+// followed by a 2 byte password verification value
+// then the variable length encrypted data
+// and lastly a 10 byte authentication code
+pub struct AesReader<R> {
+    reader: R,
+    aes_mode: AesMode,
+    data_length: u64,
+}
+
+impl<R: Read> AesReader<R> {
+    pub fn new(reader: R, aes_mode: AesMode, compressed_size: u64) -> AesReader<R> {
+        let data_length = compressed_size
+            - (PWD_VERIFY_LENGTH + AUTH_CODE_LENGTH + aes_mode.salt_length()) as u64;
+
+        Self {
+            reader,
+            aes_mode,
+            data_length,
+        }
+    }
+
+    /// Read the AES header bytes and validate the password.
+    ///
+    /// Even if the validation succeeds, there is still a 1 in 65536 chance that an incorrect
+    /// password was provided.
+    /// It isn't possible to check the authentication code in this step. This will be done after
+    /// reading and decrypting the file.
+    ///
+    /// # Returns
+    ///
+    /// If the password verification failed `Ok(None)` will be returned to match the validate
+    /// method of ZipCryptoReader.
+    pub fn validate(mut self, password: &[u8]) -> io::Result<Option<AesReaderValid<R>>> {
+        let salt_length = self.aes_mode.salt_length();
+        let key_length = self.aes_mode.key_length();
+
+        let mut salt = vec![0; salt_length];
+        self.reader.read_exact(&mut salt)?;
+
+        // next are 2 bytes used for password verification
+        let mut pwd_verification_value = vec![0; PWD_VERIFY_LENGTH];
+        self.reader.read_exact(&mut pwd_verification_value)?;
+
+        // derive a key from the password and salt
+        // the length depends on the aes key length
+        let derived_key_len = 2 * key_length + PWD_VERIFY_LENGTH;
+        let mut derived_key: Vec<u8> = vec![0; derived_key_len];
+
+        // use PBKDF2 with HMAC-Sha1 to derive the key
+        pbkdf2::pbkdf2::<Hmac<Sha1>>(password, &salt, ITERATION_COUNT, &mut derived_key);
+        let decrypt_key = &derived_key[0..key_length];
+        let hmac_key = &derived_key[key_length..key_length * 2];
+        let pwd_verify = &derived_key[derived_key_len - 2..];
+
+        // the last 2 bytes should equal the password verification value
+        if pwd_verification_value != pwd_verify {
+            // wrong password
+            return Ok(None);
+        }
+
+        let cipher = cipher_from_mode(self.aes_mode, decrypt_key);
+        let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
+
+        Ok(Some(AesReaderValid {
+            reader: self.reader,
+            data_remaining: self.data_length,
+            cipher,
+            hmac,
+            finalized: false,
+        }))
+    }
+}
+
+/// A reader for aes encrypted files, which has already passed the first password check.
+///
+/// There is a 1 in 65536 chance that an invalid password passes that check.
+/// After the data has been read and decrypted an HMAC will be checked and provide a final means
+/// to check if either the password is invalid or if the data has been changed.
+pub struct AesReaderValid<R: Read> {
+    reader: R,
+    data_remaining: u64,
+    cipher: Box<dyn aes_ctr::AesCipher>,
+    hmac: Hmac<Sha1>,
+    finalized: bool,
+}
+
+impl<R: Read> Read for AesReaderValid<R> {
+    /// This implementation does not fulfill all requirements set in the trait documentation.
+    ///
+    /// ```txt
+    /// "If an error is returned then it must be guaranteed that no bytes were read."
+    /// ```
+    ///
+    /// Whether this applies to errors that occur while reading the encrypted data depends on the
+    /// underlying reader. If the error occurs while verifying the HMAC, the reader might become
+    /// practically unusable, since its position after the error is not known.
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        if self.data_remaining == 0 {
+            return Ok(0);
+        }
+
+        // get the number of bytes to read, compare as u64 to make sure we can read more than
+        // 2^32 bytes even on 32 bit systems.
+        let bytes_to_read = self.data_remaining.min(buf.len() as u64) as usize;
+        let read = self.reader.read(&mut buf[0..bytes_to_read])?;
+        self.data_remaining -= read as u64;
+
+        // Update the hmac with the encrypted data
+        self.hmac.update(&buf[0..read]);
+
+        // decrypt the data
+        self.cipher.crypt_in_place(&mut buf[0..read]);
+
+        // if there is no data left to read, check the integrity of the data
+        if self.data_remaining == 0 {
+            assert!(
+                !self.finalized,
+                "Tried to use an already finalized HMAC. This is a bug!"
+            );
+            self.finalized = true;
+
+            // Zip uses HMAC-Sha1-80, which only uses the first half of the hash
+            // see https://www.winzip.com/win/en/aes_info.html#auth-faq
+            let mut read_auth_code = [0; AUTH_CODE_LENGTH];
+            self.reader.read_exact(&mut read_auth_code)?;
+            let computed_auth_code = &self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];
+
+            // use constant time comparison to mitigate timing attacks
+            if !constant_time_eq(computed_auth_code, &read_auth_code) {
+                return Err(
+                    io::Error::new(
+                        io::ErrorKind::InvalidData,
+                        "Invalid authentication code, this could be due to an invalid password or errors in the data"
+                    )
+                );
+            }
+        }
+
+        Ok(read)
+    }
+}
+
+impl<R: Read> AesReaderValid<R> {
+    /// Consumes this decoder, returning the underlying reader.
+    pub fn into_inner(self) -> R {
+        self.reader
+    }
+}
diff --git a/src/aes_ctr.rs b/src/aes_ctr.rs
new file mode 100644
index 0000000..0f34335
--- /dev/null
+++ b/src/aes_ctr.rs
@@ -0,0 +1,281 @@
+//! A counter mode (CTR) for AES to work with the encryption used in zip files.
+//!
+//! 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.
+
+use aes::cipher::generic_array::GenericArray;
+use aes::{BlockEncrypt, NewBlockCipher};
+use byteorder::WriteBytesExt;
+use std::{any, fmt};
+
+/// Internal block size of an AES cipher.
+const AES_BLOCK_SIZE: usize = 16;
+
+/// AES-128.
+#[derive(Debug)]
+pub struct Aes128;
+/// AES-192
+#[derive(Debug)]
+pub struct Aes192;
+/// AES-256.
+#[derive(Debug)]
+pub struct Aes256;
+
+/// An AES cipher kind.
+pub trait AesKind {
+    /// Key type.
+    type Key: AsRef<[u8]>;
+    /// Cipher used to decrypt.
+    type Cipher;
+}
+
+impl AesKind for Aes128 {
+    type Key = [u8; 16];
+    type Cipher = aes::Aes128;
+}
+
+impl AesKind for Aes192 {
+    type Key = [u8; 24];
+    type Cipher = aes::Aes192;
+}
+
+impl AesKind for Aes256 {
+    type Key = [u8; 32];
+    type Cipher = aes::Aes256;
+}
+
+/// An AES-CTR key stream generator.
+///
+/// Implements the slightly non-standard AES-CTR variant used by WinZip AES encryption.
+///
+/// Typical AES-CTR implementations combine a nonce with a 64 bit counter. WinZIP AES instead uses
+/// no nonce and also uses a different byte order (little endian) than NIST (big endian).
+///
+/// The stream implements the `Read` trait; encryption or decryption is performed by XOR-ing the
+/// bytes from the key stream with the ciphertext/plaintext.
+pub struct AesCtrZipKeyStream<C: AesKind> {
+    /// Current AES counter.
+    counter: u128,
+    /// AES cipher instance.
+    cipher: C::Cipher,
+    /// Stores the currently available keystream bytes.
+    buffer: [u8; AES_BLOCK_SIZE],
+    /// Number of bytes already used up from `buffer`.
+    pos: usize,
+}
+
+impl<C> fmt::Debug for AesCtrZipKeyStream<C>
+where
+    C: AesKind,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "AesCtrZipKeyStream<{}>(counter: {})",
+            any::type_name::<C>(),
+            self.counter
+        )
+    }
+}
+
+impl<C> AesCtrZipKeyStream<C>
+where
+    C: AesKind,
+    C::Cipher: NewBlockCipher,
+{
+    /// Creates a new zip variant AES-CTR key stream.
+    ///
+    /// # Panics
+    ///
+    /// This panics if `key` doesn't have the correct size for cipher `C`.
+    pub fn new(key: &[u8]) -> AesCtrZipKeyStream<C> {
+        AesCtrZipKeyStream {
+            counter: 1,
+            cipher: C::Cipher::new(GenericArray::from_slice(key)),
+            buffer: [0u8; AES_BLOCK_SIZE],
+            pos: AES_BLOCK_SIZE,
+        }
+    }
+}
+
+impl<C> AesCipher for AesCtrZipKeyStream<C>
+where
+    C: AesKind,
+    C::Cipher: BlockEncrypt,
+{
+    /// Decrypt or encrypt `target`.
+    #[inline]
+    fn crypt_in_place(&mut self, mut target: &mut [u8]) {
+        while !target.is_empty() {
+            if self.pos == AES_BLOCK_SIZE {
+                // Note: AES block size is always 16 bytes, same as u128.
+                self.buffer
+                    .as_mut()
+                    .write_u128::<byteorder::LittleEndian>(self.counter)
+                    .expect("did not expect u128 le conversion to fail");
+                self.cipher
+                    .encrypt_block(GenericArray::from_mut_slice(&mut self.buffer));
+                self.counter += 1;
+                self.pos = 0;
+            }
+
+            let target_len = target.len().min(AES_BLOCK_SIZE - self.pos);
+
+            xor(
+                &mut target[0..target_len],
+                &self.buffer[self.pos..(self.pos + target_len)],
+            );
+            target = &mut target[target_len..];
+            self.pos += target_len;
+        }
+    }
+}
+
+/// This trait allows using generic AES ciphers with different key sizes.
+pub trait AesCipher {
+    fn crypt_in_place(&mut self, target: &mut [u8]);
+}
+
+/// XORs a slice in place with another slice.
+#[inline]
+fn xor(dest: &mut [u8], src: &[u8]) {
+    assert_eq!(dest.len(), src.len());
+
+    for (lhs, rhs) in dest.iter_mut().zip(src.iter()) {
+        *lhs ^= *rhs;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{Aes128, Aes192, Aes256, AesCipher, AesCtrZipKeyStream, AesKind};
+    use aes::{BlockEncrypt, NewBlockCipher};
+
+    /// 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,
+    {
+        let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key);
+
+        let mut plaintext: Vec<u8> = ciphertext.to_vec();
+        key_stream.crypt_in_place(plaintext.as_mut_slice());
+        assert_eq!(plaintext, expected_plaintext.to_vec());
+
+        // Round-tripping should yield the ciphertext again.
+        let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key);
+        key_stream.crypt_in_place(&mut plaintext);
+        assert_eq!(plaintext, ciphertext.to_vec());
+    }
+
+    #[test]
+    #[should_panic]
+    fn new_with_wrong_key_size() {
+        AesCtrZipKeyStream::<Aes128>::new(&[1, 2, 3, 4, 5]);
+    }
+
+    // The data used in these tests was generated with p7zip without any compression.
+    // It's not possible to recreate the exact same data, since a random salt is used for encryption.
+    // `7z a -phelloworld -mem=AES256 -mx=0 aes256_40byte.zip 40byte_data.txt`
+    #[test]
+    fn crypt_aes_256_0_byte() {
+        let mut ciphertext = [];
+        let expected_plaintext = &[];
+        let key = [
+            0x0b, 0xec, 0x2e, 0xf2, 0x46, 0xf0, 0x7e, 0x35, 0x16, 0x54, 0xe0, 0x98, 0x10, 0xb3,
+            0x18, 0x55, 0x24, 0xa3, 0x9e, 0x0e, 0x40, 0xe7, 0x92, 0xad, 0xb2, 0x8a, 0x48, 0xf4,
+            0x5c, 0xd0, 0xc0, 0x54,
+        ];
+
+        roundtrip::<Aes256>(&key, &mut ciphertext, expected_plaintext);
+    }
+
+    #[test]
+    fn crypt_aes_128_5_byte() {
+        let mut ciphertext = [0x98, 0xa9, 0x8c, 0x26, 0x0e];
+        let expected_plaintext = b"asdf\n";
+        let key = [
+            0xe0, 0x25, 0x7b, 0x57, 0x97, 0x6a, 0xa4, 0x23, 0xab, 0x94, 0xaa, 0x44, 0xfd, 0x47,
+            0x4f, 0xa5,
+        ];
+
+        roundtrip::<Aes128>(&key, &mut ciphertext, expected_plaintext);
+    }
+
+    #[test]
+    fn crypt_aes_192_5_byte() {
+        let mut ciphertext = [0x36, 0x55, 0x5c, 0x61, 0x3c];
+        let expected_plaintext = b"asdf\n";
+        let key = [
+            0xe4, 0x4a, 0x88, 0x52, 0x8f, 0xf7, 0x0b, 0x81, 0x7b, 0x75, 0xf1, 0x74, 0x21, 0x37,
+            0x8c, 0x90, 0xad, 0xbe, 0x4a, 0x65, 0xa8, 0x96, 0x0e, 0xcc,
+        ];
+
+        roundtrip::<Aes192>(&key, &mut ciphertext, expected_plaintext);
+    }
+
+    #[test]
+    fn crypt_aes_256_5_byte() {
+        let mut ciphertext = [0xc2, 0x47, 0xc0, 0xdc, 0x56];
+        let expected_plaintext = b"asdf\n";
+        let key = [
+            0x79, 0x5e, 0x17, 0xf2, 0xc6, 0x3d, 0x28, 0x9b, 0x4b, 0x4b, 0xbb, 0xa9, 0xba, 0xc9,
+            0xa5, 0xee, 0x3a, 0x4f, 0x0f, 0x4b, 0x29, 0xbd, 0xe9, 0xb8, 0x41, 0x9c, 0x41, 0xa5,
+            0x15, 0xb2, 0x86, 0xab,
+        ];
+
+        roundtrip::<Aes256>(&key, &mut ciphertext, expected_plaintext);
+    }
+
+    #[test]
+    fn crypt_aes_128_40_byte() {
+        let mut ciphertext = [
+            0xcf, 0x72, 0x6b, 0xa1, 0xb2, 0x0f, 0xdf, 0xaa, 0x10, 0xad, 0x9c, 0x7f, 0x6d, 0x1c,
+            0x8d, 0xb5, 0x16, 0x7e, 0xbb, 0x11, 0x69, 0x52, 0x8c, 0x89, 0x80, 0x32, 0xaa, 0x76,
+            0xa6, 0x18, 0x31, 0x98, 0xee, 0xdd, 0x22, 0x68, 0xb7, 0xe6, 0x77, 0xd2,
+        ];
+        let expected_plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
+        let key = [
+            0x43, 0x2b, 0x6d, 0xbe, 0x05, 0x76, 0x6c, 0x9e, 0xde, 0xca, 0x3b, 0xf8, 0xaf, 0x5d,
+            0x81, 0xb6,
+        ];
+
+        roundtrip::<Aes128>(&key, &mut ciphertext, expected_plaintext);
+    }
+
+    #[test]
+    fn crypt_aes_192_40_byte() {
+        let mut ciphertext = [
+            0xa6, 0xfc, 0x52, 0x79, 0x2c, 0x6c, 0xfe, 0x68, 0xb1, 0xa8, 0xb3, 0x07, 0x52, 0x8b,
+            0x82, 0xa6, 0x87, 0x9c, 0x72, 0x42, 0x3a, 0xf8, 0xc6, 0xa9, 0xc9, 0xfb, 0x61, 0x19,
+            0x37, 0xb9, 0x56, 0x62, 0xf4, 0xfc, 0x5e, 0x7a, 0xdd, 0x55, 0x0a, 0x48,
+        ];
+        let expected_plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
+        let key = [
+            0xac, 0x92, 0x41, 0xba, 0xde, 0xd9, 0x02, 0xfe, 0x40, 0x92, 0x20, 0xf6, 0x56, 0x03,
+            0xfe, 0xae, 0x1b, 0xba, 0x01, 0x97, 0x97, 0x79, 0xbb, 0xa6,
+        ];
+
+        roundtrip::<Aes192>(&key, &mut ciphertext, expected_plaintext);
+    }
+
+    #[test]
+    fn crypt_aes_256_40_byte() {
+        let mut ciphertext = [
+            0xa9, 0x99, 0xbd, 0xea, 0x82, 0x9b, 0x8f, 0x2f, 0xb7, 0x52, 0x2f, 0x6b, 0xd8, 0xf6,
+            0xab, 0x0e, 0x24, 0x51, 0x9e, 0x18, 0x0f, 0xc0, 0x8f, 0x54, 0x15, 0x80, 0xae, 0xbc,
+            0xa0, 0x5c, 0x8a, 0x11, 0x8d, 0x14, 0x7e, 0xc5, 0xb4, 0xae, 0xd3, 0x37,
+        ];
+        let expected_plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
+        let key = [
+            0x64, 0x7c, 0x7a, 0xde, 0xf0, 0xf2, 0x61, 0x49, 0x1c, 0xf1, 0xf1, 0xe3, 0x37, 0xfc,
+            0xe1, 0x4d, 0x4a, 0x77, 0xd4, 0xeb, 0x9e, 0x3d, 0x75, 0xce, 0x9a, 0x3e, 0x10, 0x50,
+            0xc2, 0x07, 0x36, 0xb6,
+        ];
+
+        roundtrip::<Aes256>(&key, &mut ciphertext, expected_plaintext);
+    }
+}
diff --git a/src/compression.rs b/src/compression.rs
index 5fdde07..abd8b53 100644
--- a/src/compression.rs
+++ b/src/compression.rs
@@ -9,8 +9,9 @@
 /// contents to be read without context.
 ///
 /// When creating ZIP files, you may choose the method to use with
-/// [`zip::write::FileOptions::compression_method`]
+/// [`crate::write::FileOptions::compression_method`]
 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
+#[non_exhaustive]
 pub enum CompressionMethod {
     /// Store the file as is
     Stored,
@@ -24,6 +25,15 @@
     /// Compress the file using BZIP2
     #[cfg(feature = "bzip2")]
     Bzip2,
+    /// Encrypted using AES.
+    ///
+    /// The actual compression method has to be taken from the AES extra data field
+    /// or from `ZipFileData`.
+    #[cfg(feature = "aes-crypto")]
+    Aes,
+    /// Compress the file using ZStandard
+    #[cfg(feature = "zstd")]
+    Zstd,
     /// Unsupported compression method
     #[deprecated(since = "0.5.7", note = "use the constants instead")]
     Unsupported(u16),
@@ -60,12 +70,19 @@
     pub const IBM_ZOS_CMPSC: Self = CompressionMethod::Unsupported(16);
     pub const IBM_TERSE: Self = CompressionMethod::Unsupported(18);
     pub const ZSTD_DEPRECATED: Self = CompressionMethod::Unsupported(20);
+    #[cfg(feature = "zstd")]
+    pub const ZSTD: Self = CompressionMethod::Zstd;
+    #[cfg(not(feature = "zstd"))]
     pub const ZSTD: Self = CompressionMethod::Unsupported(93);
     pub const MP3: Self = CompressionMethod::Unsupported(94);
     pub const XZ: Self = CompressionMethod::Unsupported(95);
     pub const JPEG: Self = CompressionMethod::Unsupported(96);
     pub const WAVPACK: Self = CompressionMethod::Unsupported(97);
     pub const PPMD: Self = CompressionMethod::Unsupported(98);
+    #[cfg(feature = "aes-crypto")]
+    pub const AES: Self = CompressionMethod::Aes;
+    #[cfg(not(feature = "aes-crypto"))]
+    pub const AES: Self = CompressionMethod::Unsupported(99);
 }
 impl CompressionMethod {
     /// Converts an u16 to its corresponding CompressionMethod
@@ -85,6 +102,10 @@
             8 => CompressionMethod::Deflated,
             #[cfg(feature = "bzip2")]
             12 => CompressionMethod::Bzip2,
+            #[cfg(feature = "zstd")]
+            93 => CompressionMethod::Zstd,
+            #[cfg(feature = "aes-crypto")]
+            99 => CompressionMethod::Aes,
 
             v => CompressionMethod::Unsupported(v),
         }
@@ -107,6 +128,11 @@
             CompressionMethod::Deflated => 8,
             #[cfg(feature = "bzip2")]
             CompressionMethod::Bzip2 => 12,
+            #[cfg(feature = "aes-crypto")]
+            CompressionMethod::Aes => 99,
+            #[cfg(feature = "zstd")]
+            CompressionMethod::Zstd => 93,
+
             CompressionMethod::Unsupported(v) => v,
         }
     }
@@ -119,13 +145,28 @@
     }
 }
 
+/// The compression methods which have been implemented.
+pub const SUPPORTED_COMPRESSION_METHODS: &[CompressionMethod] = &[
+    CompressionMethod::Stored,
+    #[cfg(any(
+        feature = "deflate",
+        feature = "deflate-miniz",
+        feature = "deflate-zlib"
+    ))]
+    CompressionMethod::Deflated,
+    #[cfg(feature = "bzip2")]
+    CompressionMethod::Bzip2,
+    #[cfg(feature = "zstd")]
+    CompressionMethod::Zstd,
+];
+
 #[cfg(test)]
 mod test {
-    use super::CompressionMethod;
+    use super::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
 
     #[test]
     fn from_eq_to() {
-        for v in 0..(::std::u16::MAX as u32 + 1) {
+        for v in 0..(u16::MAX as u32 + 1) {
             #[allow(deprecated)]
             let from = CompressionMethod::from_u16(v as u16);
             #[allow(deprecated)]
@@ -134,20 +175,6 @@
         }
     }
 
-    fn methods() -> Vec<CompressionMethod> {
-        let mut methods = Vec::new();
-        methods.push(CompressionMethod::Stored);
-        #[cfg(any(
-            feature = "deflate",
-            feature = "deflate-miniz",
-            feature = "deflate-zlib"
-        ))]
-        methods.push(CompressionMethod::Deflated);
-        #[cfg(feature = "bzip2")]
-        methods.push(CompressionMethod::Bzip2);
-        methods
-    }
-
     #[test]
     fn to_eq_from() {
         fn check_match(method: CompressionMethod) {
@@ -160,7 +187,7 @@
             assert_eq!(to, back);
         }
 
-        for method in methods() {
+        for &method in SUPPORTED_COMPRESSION_METHODS {
             check_match(method);
         }
     }
@@ -173,7 +200,7 @@
             assert_eq!(debug_str, display_str);
         }
 
-        for method in methods() {
+        for &method in SUPPORTED_COMPRESSION_METHODS {
             check_match(method);
         }
     }
diff --git a/src/cp437.rs b/src/cp437.rs
index f994814..4dba9af 100644
--- a/src/cp437.rs
+++ b/src/cp437.rs
@@ -6,7 +6,8 @@
     type Target;
 
     /// Function that does the conversion from cp437.
-    /// Gennerally allocations will be avoided if all data falls into the ASCII range.
+    /// Generally allocations will be avoided if all data falls into the ASCII range.
+    #[allow(clippy::wrong_self_convention)]
     fn from_cp437(self) -> Self::Target;
 }
 
diff --git a/src/crc32.rs b/src/crc32.rs
index b351aa0..ebace89 100644
--- a/src/crc32.rs
+++ b/src/crc32.rs
@@ -10,15 +10,20 @@
     inner: R,
     hasher: Hasher,
     check: u32,
+    /// Signals if `inner` stores aes encrypted data.
+    /// AE-2 encrypted data doesn't use crc and sets the value to 0.
+    ae2_encrypted: bool,
 }
 
 impl<R> Crc32Reader<R> {
-    /// Get a new Crc32Reader which check the inner reader against checksum.
-    pub fn new(inner: R, checksum: u32) -> Crc32Reader<R> {
+    /// Get a new Crc32Reader which checks the inner reader against checksum.
+    /// The check is disabled if `ae2_encrypted == true`.
+    pub(crate) fn new(inner: R, checksum: u32, ae2_encrypted: bool) -> Crc32Reader<R> {
         Crc32Reader {
             inner,
             hasher: Hasher::new(),
             check: checksum,
+            ae2_encrypted,
         }
     }
 
@@ -33,8 +38,10 @@
 
 impl<R: Read> Read for Crc32Reader<R> {
     fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        let invalid_check = !buf.is_empty() && !self.check_matches() && !self.ae2_encrypted;
+
         let count = match self.inner.read(buf) {
-            Ok(0) if !buf.is_empty() && !self.check_matches() => {
+            Ok(0) if invalid_check => {
                 return Err(io::Error::new(io::ErrorKind::Other, "Invalid checksum"))
             }
             Ok(n) => n,
@@ -55,10 +62,10 @@
         let data: &[u8] = b"";
         let mut buf = [0; 1];
 
-        let mut reader = Crc32Reader::new(data, 0);
+        let mut reader = Crc32Reader::new(data, 0, false);
         assert_eq!(reader.read(&mut buf).unwrap(), 0);
 
-        let mut reader = Crc32Reader::new(data, 1);
+        let mut reader = Crc32Reader::new(data, 1, false);
         assert!(reader
             .read(&mut buf)
             .unwrap_err()
@@ -71,7 +78,7 @@
         let data: &[u8] = b"1234";
         let mut buf = [0; 1];
 
-        let mut reader = Crc32Reader::new(data, 0x9be3e0a3);
+        let mut reader = Crc32Reader::new(data, 0x9be3e0a3, false);
         assert_eq!(reader.read(&mut buf).unwrap(), 1);
         assert_eq!(reader.read(&mut buf).unwrap(), 1);
         assert_eq!(reader.read(&mut buf).unwrap(), 1);
@@ -86,7 +93,7 @@
         let data: &[u8] = b"1234";
         let mut buf = [0; 5];
 
-        let mut reader = Crc32Reader::new(data, 0x9be3e0a3);
+        let mut reader = Crc32Reader::new(data, 0x9be3e0a3, false);
         assert_eq!(reader.read(&mut buf[..0]).unwrap(), 0);
         assert_eq!(reader.read(&mut buf).unwrap(), 4);
     }
diff --git a/src/lib.rs b/src/lib.rs
index 3b39ab4..0fee99c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,15 +1,38 @@
-//! An ergonomic API for reading and writing ZIP files.
+//! A library for reading and writing ZIP archives.
+//! ZIP is a format designed for cross-platform file "archiving".
+//! That is, storing a collection of files in a single datastream
+//! to make them easier to share between computers.
+//! Additionally, ZIP is able to compress and encrypt files in its
+//! archives.
 //!
 //! The current implementation is based on [PKWARE's APPNOTE.TXT v6.3.9](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT)
-// TODO(#184): Decide on the crate's bias: Do we prioritise permissiveness/correctness/speed/ergonomics?
+//!
+//! ---
+//!
+//! [`zip`](`crate`) has support for the most common ZIP archives found in common use.
+//! However, in special cases,
+//! there are some zip archives that are difficult to read or write.
+//!
+//! This is a list of supported features:
+//!
+//! |         | Reading | Writing |
+//! | ------- | ------  | ------- |
+//! | Deflate | ✅ [->](`crate::ZipArchive::by_name`)      | ✅ [->](`crate::write::FileOptions::compression_method`) |
+//!
+//!
+//!
 
 #![warn(missing_docs)]
 
-pub use crate::compression::CompressionMethod;
+pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
 pub use crate::read::ZipArchive;
 pub use crate::types::DateTime;
 pub use crate::write::ZipWriter;
 
+#[cfg(feature = "aes-crypto")]
+mod aes;
+#[cfg(feature = "aes-crypto")]
+mod aes_ctr;
 mod compression;
 mod cp437;
 mod crc32;
diff --git a/src/read.rs b/src/read.rs
index 3aac00f..c619f24 100644
--- a/src/read.rs
+++ b/src/read.rs
@@ -1,19 +1,20 @@
 //! Types for reading ZIP archives
 
+#[cfg(feature = "aes-crypto")]
+use crate::aes::{AesReader, AesReaderValid};
 use crate::compression::CompressionMethod;
+use crate::cp437::FromCp437;
 use crate::crc32::Crc32Reader;
 use crate::result::{InvalidPassword, ZipError, ZipResult};
 use crate::spec;
-use crate::zipcrypto::ZipCryptoReader;
-use crate::zipcrypto::ZipCryptoReaderValid;
+use crate::types::{AesMode, AesVendorVersion, AtomicU64, DateTime, System, ZipFileData};
+use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator};
+use byteorder::{LittleEndian, ReadBytesExt};
 use std::borrow::Cow;
 use std::collections::HashMap;
 use std::io::{self, prelude::*};
 use std::path::{Component, Path};
-
-use crate::cp437::FromCp437;
-use crate::types::{DateTime, System, ZipFileData};
-use byteorder::{LittleEndian, ReadBytesExt};
+use std::sync::Arc;
 
 #[cfg(any(
     feature = "deflate",
@@ -25,39 +26,62 @@
 #[cfg(feature = "bzip2")]
 use bzip2::read::BzDecoder;
 
+#[cfg(feature = "zstd")]
+use zstd::stream::read::Decoder as ZstdDecoder;
+
 mod ffi {
     pub const S_IFDIR: u32 = 0o0040000;
     pub const S_IFREG: u32 = 0o0100000;
 }
 
-/// ZIP archive reader
-///
-/// ```no_run
-/// use std::io::prelude::*;
-/// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> {
-///     let mut zip = zip::ZipArchive::new(reader)?;
-///
-///     for i in 0..zip.len() {
-///         let mut file = zip.by_index(i)?;
-///         println!("Filename: {}", file.name());
-///         std::io::copy(&mut file, &mut std::io::stdout());
-///     }
-///
-///     Ok(())
-/// }
-/// ```
-#[derive(Clone, Debug)]
-pub struct ZipArchive<R: Read + io::Seek> {
-    reader: R,
-    files: Vec<ZipFileData>,
-    names_map: HashMap<String, usize>,
-    offset: u64,
-    comment: Vec<u8>,
+// Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely
+pub(crate) mod zip_archive {
+    /// Extract immutable data from `ZipArchive` to make it cheap to clone
+    #[derive(Debug)]
+    pub(crate) struct Shared {
+        pub(super) files: Vec<super::ZipFileData>,
+        pub(super) names_map: super::HashMap<String, usize>,
+        pub(super) offset: u64,
+        pub(super) comment: Vec<u8>,
+    }
+
+    /// ZIP archive reader
+    ///
+    /// At the moment, this type is cheap to clone if this is the case for the
+    /// reader it uses. However, this is not guaranteed by this crate and it may
+    /// change in the future.
+    ///
+    /// ```no_run
+    /// use std::io::prelude::*;
+    /// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> {
+    ///     let mut zip = zip::ZipArchive::new(reader)?;
+    ///
+    ///     for i in 0..zip.len() {
+    ///         let mut file = zip.by_index(i)?;
+    ///         println!("Filename: {}", file.name());
+    ///         std::io::copy(&mut file, &mut std::io::stdout());
+    ///     }
+    ///
+    ///     Ok(())
+    /// }
+    /// ```
+    #[derive(Clone, Debug)]
+    pub struct ZipArchive<R> {
+        pub(super) reader: R,
+        pub(super) shared: super::Arc<Shared>,
+    }
 }
 
+pub use zip_archive::ZipArchive;
+#[allow(clippy::large_enum_variant)]
 enum CryptoReader<'a> {
     Plaintext(io::Take<&'a mut dyn Read>),
     ZipCrypto(ZipCryptoReaderValid<io::Take<&'a mut dyn Read>>),
+    #[cfg(feature = "aes-crypto")]
+    Aes {
+        reader: AesReaderValid<io::Take<&'a mut dyn Read>>,
+        vendor_version: AesVendorVersion,
+    },
 }
 
 impl<'a> Read for CryptoReader<'a> {
@@ -65,6 +89,8 @@
         match self {
             CryptoReader::Plaintext(r) => r.read(buf),
             CryptoReader::ZipCrypto(r) => r.read(buf),
+            #[cfg(feature = "aes-crypto")]
+            CryptoReader::Aes { reader: r, .. } => r.read(buf),
         }
     }
 }
@@ -75,8 +101,24 @@
         match self {
             CryptoReader::Plaintext(r) => r,
             CryptoReader::ZipCrypto(r) => r.into_inner(),
+            #[cfg(feature = "aes-crypto")]
+            CryptoReader::Aes { reader: r, .. } => r.into_inner(),
         }
     }
+
+    /// Returns `true` if the data is encrypted using AE2.
+    pub fn is_ae2_encrypted(&self) -> bool {
+        #[cfg(feature = "aes-crypto")]
+        return matches!(
+            self,
+            CryptoReader::Aes {
+                vendor_version: AesVendorVersion::Ae2,
+                ..
+            }
+        );
+        #[cfg(not(feature = "aes-crypto"))]
+        false
+    }
 }
 
 enum ZipFileReader<'a> {
@@ -91,6 +133,8 @@
     Deflated(Crc32Reader<flate2::read::DeflateDecoder<CryptoReader<'a>>>),
     #[cfg(feature = "bzip2")]
     Bzip2(Crc32Reader<BzDecoder<CryptoReader<'a>>>),
+    #[cfg(feature = "zstd")]
+    Zstd(Crc32Reader<ZstdDecoder<'a, io::BufReader<CryptoReader<'a>>>>),
 }
 
 impl<'a> Read for ZipFileReader<'a> {
@@ -107,6 +151,8 @@
             ZipFileReader::Deflated(r) => r.read(buf),
             #[cfg(feature = "bzip2")]
             ZipFileReader::Bzip2(r) => r.read(buf),
+            #[cfg(feature = "zstd")]
+            ZipFileReader::Zstd(r) => r.read(buf),
         }
     }
 }
@@ -126,6 +172,8 @@
             ZipFileReader::Deflated(r) => r.into_inner().into_inner().into_inner(),
             #[cfg(feature = "bzip2")]
             ZipFileReader::Bzip2(r) => r.into_inner().into_inner().into_inner(),
+            #[cfg(feature = "zstd")]
+            ZipFileReader::Zstd(r) => r.into_inner().finish().into_inner().into_inner(),
         }
     }
 }
@@ -138,7 +186,7 @@
 }
 
 fn find_content<'a>(
-    data: &mut ZipFileData,
+    data: &ZipFileData,
     reader: &'a mut (impl Read + Seek),
 ) -> ZipResult<io::Take<&'a mut dyn Read>> {
     // Parse local header
@@ -152,17 +200,23 @@
     let file_name_length = reader.read_u16::<LittleEndian>()? as u64;
     let extra_field_length = reader.read_u16::<LittleEndian>()? as u64;
     let magic_and_header = 4 + 22 + 2 + 2;
-    data.data_start = data.header_start + magic_and_header + file_name_length + extra_field_length;
+    let data_start = data.header_start + magic_and_header + file_name_length + extra_field_length;
+    data.data_start.store(data_start);
 
-    reader.seek(io::SeekFrom::Start(data.data_start))?;
+    reader.seek(io::SeekFrom::Start(data_start))?;
     Ok((reader as &mut dyn Read).take(data.compressed_size))
 }
 
+#[allow(clippy::too_many_arguments)]
 fn make_crypto_reader<'a>(
     compression_method: crate::compression::CompressionMethod,
     crc32: u32,
+    last_modified_time: DateTime,
+    using_data_descriptor: bool,
     reader: io::Take<&'a mut dyn io::Read>,
     password: Option<&[u8]>,
+    aes_info: Option<(AesMode, AesVendorVersion)>,
+    #[cfg(feature = "aes-crypto")] compressed_size: u64,
 ) -> ZipResult<Result<CryptoReader<'a>, InvalidPassword>> {
     #[allow(deprecated)]
     {
@@ -171,23 +225,51 @@
         }
     }
 
-    let reader = match password {
-        None => CryptoReader::Plaintext(reader),
-        Some(password) => match ZipCryptoReader::new(reader, password).validate(crc32)? {
-            None => return Ok(Err(InvalidPassword)),
-            Some(r) => CryptoReader::ZipCrypto(r),
-        },
+    let reader = match (password, aes_info) {
+        #[cfg(not(feature = "aes-crypto"))]
+        (Some(_), Some(_)) => {
+            return Err(ZipError::UnsupportedArchive(
+                "AES encrypted files cannot be decrypted without the aes-crypto feature.",
+            ))
+        }
+        #[cfg(feature = "aes-crypto")]
+        (Some(password), Some((aes_mode, vendor_version))) => {
+            match AesReader::new(reader, aes_mode, compressed_size).validate(password)? {
+                None => return Ok(Err(InvalidPassword)),
+                Some(r) => CryptoReader::Aes {
+                    reader: r,
+                    vendor_version,
+                },
+            }
+        }
+        (Some(password), None) => {
+            let validator = if using_data_descriptor {
+                ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
+            } else {
+                ZipCryptoValidator::PkzipCrc32(crc32)
+            };
+            match ZipCryptoReader::new(reader, password).validate(validator)? {
+                None => return Ok(Err(InvalidPassword)),
+                Some(r) => CryptoReader::ZipCrypto(r),
+            }
+        }
+        (None, Some(_)) => return Ok(Err(InvalidPassword)),
+        (None, None) => CryptoReader::Plaintext(reader),
     };
     Ok(Ok(reader))
 }
 
-fn make_reader<'a>(
+fn make_reader(
     compression_method: CompressionMethod,
     crc32: u32,
-    reader: CryptoReader<'a>,
-) -> ZipFileReader<'a> {
+    reader: CryptoReader,
+) -> ZipFileReader {
+    let ae2_encrypted = reader.is_ae2_encrypted();
+
     match compression_method {
-        CompressionMethod::Stored => ZipFileReader::Stored(Crc32Reader::new(reader, crc32)),
+        CompressionMethod::Stored => {
+            ZipFileReader::Stored(Crc32Reader::new(reader, crc32, ae2_encrypted))
+        }
         #[cfg(any(
             feature = "deflate",
             feature = "deflate-miniz",
@@ -195,12 +277,17 @@
         ))]
         CompressionMethod::Deflated => {
             let deflate_reader = DeflateDecoder::new(reader);
-            ZipFileReader::Deflated(Crc32Reader::new(deflate_reader, crc32))
+            ZipFileReader::Deflated(Crc32Reader::new(deflate_reader, crc32, ae2_encrypted))
         }
         #[cfg(feature = "bzip2")]
         CompressionMethod::Bzip2 => {
             let bzip2_reader = BzDecoder::new(reader);
-            ZipFileReader::Bzip2(Crc32Reader::new(bzip2_reader, crc32))
+            ZipFileReader::Bzip2(Crc32Reader::new(bzip2_reader, crc32, ae2_encrypted))
+        }
+        #[cfg(feature = "zstd")]
+        CompressionMethod::Zstd => {
+            let zstd_reader = ZstdDecoder::new(reader).unwrap();
+            ZipFileReader::Zstd(Crc32Reader::new(zstd_reader, crc32, ae2_encrypted))
         }
         _ => panic!("Compression method not supported"),
     }
@@ -209,7 +296,7 @@
 impl<R: Read + io::Seek> ZipArchive<R> {
     /// Get the directory start offset and number of files. This is done in a
     /// separate function to ease the control flow design.
-    fn get_directory_counts(
+    pub(crate) fn get_directory_counts(
         reader: &mut R,
         footer: &spec::CentralDirectoryEnd,
         cde_start_pos: u64,
@@ -295,7 +382,7 @@
                 let directory_start = footer
                     .central_directory_offset
                     .checked_add(archive_offset)
-                    .ok_or_else(|| {
+                    .ok_or({
                         ZipError::InvalidArchive("Invalid central directory size or offset")
                     })?;
 
@@ -324,7 +411,7 @@
         let mut files = Vec::new();
         let mut names_map = HashMap::new();
 
-        if let Err(_) = reader.seek(io::SeekFrom::Start(directory_start)) {
+        if reader.seek(io::SeekFrom::Start(directory_start)).is_err() {
             return Err(ZipError::InvalidArchive(
                 "Could not seek to start of central directory",
             ));
@@ -336,13 +423,14 @@
             files.push(file);
         }
 
-        Ok(ZipArchive {
-            reader,
+        let shared = Arc::new(zip_archive::Shared {
             files,
             names_map,
             offset: archive_offset,
             comment: footer.zip_file_comment,
-        })
+        });
+
+        Ok(ZipArchive { reader, shared })
     }
     /// Extract a Zip archive into a directory, overwriting files if they
     /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`].
@@ -385,7 +473,7 @@
 
     /// Number of files contained in this zip.
     pub fn len(&self) -> usize {
-        self.files.len()
+        self.shared.files.len()
     }
 
     /// Whether this zip archive contains no files
@@ -398,20 +486,32 @@
     /// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size
     /// of that prepended data.
     pub fn offset(&self) -> u64 {
-        self.offset
+        self.shared.offset
     }
 
     /// Get the comment of the zip archive.
     pub fn comment(&self) -> &[u8] {
-        &self.comment
+        &self.shared.comment
     }
 
     /// Returns an iterator over all the file and directory names in this archive.
     pub fn file_names(&self) -> impl Iterator<Item = &str> {
-        self.names_map.keys().map(|s| s.as_str())
+        self.shared.names_map.keys().map(|s| s.as_str())
     }
 
     /// Search for a file entry by name, decrypt with given password
+    ///
+    /// # Warning
+    ///
+    /// The implementation of the cryptographic algorithms has not
+    /// gone through a correctness review, and you should assume it is insecure:
+    /// passwords used with this API may be compromised.
+    ///
+    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
+    /// to check for a 1/256 chance that the password is correct.
+    /// There are many passwords out there that will also pass the validity checks
+    /// we are able to perform. This is a weakness of the ZipCrypto algorithm,
+    /// due to its fairly primitive approach to cryptography.
     pub fn by_name_decrypt<'a>(
         &'a mut self,
         name: &str,
@@ -430,7 +530,7 @@
         name: &str,
         password: Option<&[u8]>,
     ) -> ZipResult<Result<ZipFile<'a>, InvalidPassword>> {
-        let index = match self.names_map.get(name) {
+        let index = match self.shared.names_map.get(name) {
             Some(index) => *index,
             None => {
                 return Err(ZipError::FileNotFound);
@@ -440,6 +540,18 @@
     }
 
     /// Get a contained file by index, decrypt with given password
+    ///
+    /// # Warning
+    ///
+    /// The implementation of the cryptographic algorithms has not
+    /// gone through a correctness review, and you should assume it is insecure:
+    /// passwords used with this API may be compromised.
+    ///
+    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
+    /// to check for a 1/256 chance that the password is correct.
+    /// There are many passwords out there that will also pass the validity checks
+    /// we are able to perform. This is a weakness of the ZipCrypto algorithm,
+    /// due to its fairly primitive approach to cryptography.
     pub fn by_index_decrypt<'a>(
         &'a mut self,
         file_number: usize,
@@ -449,17 +561,18 @@
     }
 
     /// Get a contained file by index
-    pub fn by_index<'a>(&'a mut self, file_number: usize) -> ZipResult<ZipFile<'a>> {
+    pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
         Ok(self
             .by_index_with_optional_password(file_number, None)?
             .unwrap())
     }
 
     /// Get a contained file by index without decompressing it
-    pub fn by_index_raw<'a>(&'a mut self, file_number: usize) -> ZipResult<ZipFile<'a>> {
+    pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
         let reader = &mut self.reader;
-        self.files
-            .get_mut(file_number)
+        self.shared
+            .files
+            .get(file_number)
             .ok_or(ZipError::FileNotFound)
             .and_then(move |data| {
                 Ok(ZipFile {
@@ -475,23 +588,30 @@
         file_number: usize,
         mut password: Option<&[u8]>,
     ) -> ZipResult<Result<ZipFile<'a>, InvalidPassword>> {
-        if file_number >= self.files.len() {
-            return Err(ZipError::FileNotFound);
-        }
-        let data = &mut self.files[file_number];
+        let data = self
+            .shared
+            .files
+            .get(file_number)
+            .ok_or(ZipError::FileNotFound)?;
 
         match (password, data.encrypted) {
-            (None, true) => {
-                return Err(ZipError::UnsupportedArchive(
-                    "Password required to decrypt file",
-                ))
-            }
+            (None, true) => return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)),
             (Some(_), false) => password = None, //Password supplied, but none needed! Discard.
             _ => {}
         }
         let limit_reader = find_content(data, &mut self.reader)?;
 
-        match make_crypto_reader(data.compression_method, data.crc32, limit_reader, password) {
+        match make_crypto_reader(
+            data.compression_method,
+            data.crc32,
+            data.last_modified_time,
+            data.using_data_descriptor,
+            limit_reader,
+            password,
+            data.aes_mode,
+            #[cfg(feature = "aes-crypto")]
+            data.compressed_size,
+        ) {
             Ok(Ok(crypto_reader)) => Ok(Ok(ZipFile {
                 crypto_reader: Some(crypto_reader),
                 reader: ZipFileReader::NoReader,
@@ -514,7 +634,8 @@
     Err(ZipError::UnsupportedArchive(detail))
 }
 
-fn central_header_to_zip_file<R: Read + io::Seek>(
+/// Parse a central directory entry to collect the information for the file.
+pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
     reader: &mut R,
     archive_offset: u64,
 ) -> ZipResult<ZipFileData> {
@@ -530,6 +651,7 @@
     let flags = reader.read_u16::<LittleEndian>()?;
     let encrypted = flags & 1 == 1;
     let is_utf8 = flags & (1 << 11) != 0;
+    let using_data_descriptor = flags & (1 << 3) != 0;
     let compression_method = reader.read_u16::<LittleEndian>()?;
     let last_mod_time = reader.read_u16::<LittleEndian>()?;
     let last_mod_date = reader.read_u16::<LittleEndian>()?;
@@ -564,57 +686,108 @@
         system: System::from_u8((version_made_by >> 8) as u8),
         version_made_by: version_made_by as u8,
         encrypted,
+        using_data_descriptor,
         compression_method: {
             #[allow(deprecated)]
             CompressionMethod::from_u16(compression_method)
         },
+        compression_level: None,
         last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time),
         crc32,
         compressed_size: compressed_size as u64,
         uncompressed_size: uncompressed_size as u64,
         file_name,
         file_name_raw,
+        extra_field,
         file_comment,
         header_start: offset,
         central_header_start,
-        data_start: 0,
+        data_start: AtomicU64::new(0),
         external_attributes: external_file_attributes,
+        large_file: false,
+        aes_mode: None,
     };
 
-    match parse_extra_field(&mut result, &*extra_field) {
+    match parse_extra_field(&mut result) {
         Ok(..) | Err(ZipError::Io(..)) => {}
         Err(e) => return Err(e),
     }
 
+    let aes_enabled = result.compression_method == CompressionMethod::AES;
+    if aes_enabled && result.aes_mode.is_none() {
+        return Err(ZipError::InvalidArchive(
+            "AES encryption without AES extra data field",
+        ));
+    }
+
     // Account for shifted zip offsets.
-    result.header_start += archive_offset;
+    result.header_start = result
+        .header_start
+        .checked_add(archive_offset)
+        .ok_or(ZipError::InvalidArchive("Archive header is too large"))?;
 
     Ok(result)
 }
 
-fn parse_extra_field(file: &mut ZipFileData, data: &[u8]) -> ZipResult<()> {
-    let mut reader = io::Cursor::new(data);
+fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
+    let mut reader = io::Cursor::new(&file.extra_field);
 
-    while (reader.position() as usize) < data.len() {
+    while (reader.position() as usize) < file.extra_field.len() {
         let kind = reader.read_u16::<LittleEndian>()?;
         let len = reader.read_u16::<LittleEndian>()?;
         let mut len_left = len as i64;
-        // Zip64 extended information extra field
-        if kind == 0x0001 {
-            if file.uncompressed_size == 0xFFFFFFFF {
-                file.uncompressed_size = reader.read_u64::<LittleEndian>()?;
-                len_left -= 8;
+        match kind {
+            // Zip64 extended information extra field
+            0x0001 => {
+                if file.uncompressed_size == spec::ZIP64_BYTES_THR {
+                    file.large_file = true;
+                    file.uncompressed_size = reader.read_u64::<LittleEndian>()?;
+                    len_left -= 8;
+                }
+                if file.compressed_size == spec::ZIP64_BYTES_THR {
+                    file.large_file = true;
+                    file.compressed_size = reader.read_u64::<LittleEndian>()?;
+                    len_left -= 8;
+                }
+                if file.header_start == spec::ZIP64_BYTES_THR {
+                    file.header_start = reader.read_u64::<LittleEndian>()?;
+                    len_left -= 8;
+                }
             }
-            if file.compressed_size == 0xFFFFFFFF {
-                file.compressed_size = reader.read_u64::<LittleEndian>()?;
-                len_left -= 8;
+            0x9901 => {
+                // AES
+                if len != 7 {
+                    return Err(ZipError::UnsupportedArchive(
+                        "AES extra data field has an unsupported length",
+                    ));
+                }
+                let vendor_version = reader.read_u16::<LittleEndian>()?;
+                let vendor_id = reader.read_u16::<LittleEndian>()?;
+                let aes_mode = reader.read_u8()?;
+                let compression_method = reader.read_u16::<LittleEndian>()?;
+
+                if vendor_id != 0x4541 {
+                    return Err(ZipError::InvalidArchive("Invalid AES vendor"));
+                }
+                let vendor_version = match vendor_version {
+                    0x0001 => AesVendorVersion::Ae1,
+                    0x0002 => AesVendorVersion::Ae2,
+                    _ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")),
+                };
+                match aes_mode {
+                    0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version)),
+                    0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version)),
+                    0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version)),
+                    _ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")),
+                };
+                file.compression_method = {
+                    #[allow(deprecated)]
+                    CompressionMethod::from_u16(compression_method)
+                };
             }
-            if file.header_start == 0xFFFFFFFF {
-                file.header_start = reader.read_u64::<LittleEndian>()?;
-                len_left -= 8;
+            _ => {
+                // Other fields are ignored
             }
-            // Unparsed fields:
-            // u32: disk start number
         }
 
         // We could also check for < 0 to check for errors
@@ -797,9 +970,14 @@
         self.data.crc32
     }
 
+    /// Get the extra data of the zip header for this file
+    pub fn extra_data(&self) -> &[u8] {
+        &self.data.extra_field
+    }
+
     /// Get the starting offset of the data of the compressed file
     pub fn data_start(&self) -> u64 {
-        self.data.data_start
+        self.data.data_start.load()
     }
 
     /// Get the starting offset of the zip header for this file
@@ -907,26 +1085,31 @@
         system: System::from_u8((version_made_by >> 8) as u8),
         version_made_by: version_made_by as u8,
         encrypted,
+        using_data_descriptor,
         compression_method,
+        compression_level: None,
         last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time),
         crc32,
         compressed_size: compressed_size as u64,
         uncompressed_size: uncompressed_size as u64,
         file_name,
         file_name_raw,
+        extra_field,
         file_comment: String::new(), // file comment is only available in the central directory
         // header_start and data start are not available, but also don't matter, since seeking is
         // not available.
         header_start: 0,
-        data_start: 0,
+        data_start: AtomicU64::new(0),
         central_header_start: 0,
         // The external_attributes field is only available in the central directory.
         // We set this to zero, which should be valid as the docs state 'If input came
         // from standard input, this field is set to zero.'
         external_attributes: 0,
+        large_file: false,
+        aes_mode: None,
     };
 
-    match parse_extra_field(&mut result, &extra_field) {
+    match parse_extra_field(&mut result) {
         Ok(..) | Err(ZipError::Io(..)) => {}
         Err(e) => return Err(e),
     }
@@ -942,8 +1125,18 @@
 
     let result_crc32 = result.crc32;
     let result_compression_method = result.compression_method;
-    let crypto_reader =
-        make_crypto_reader(result_compression_method, result_crc32, limit_reader, None)?.unwrap();
+    let crypto_reader = make_crypto_reader(
+        result_compression_method,
+        result_crc32,
+        result.last_modified_time,
+        result.using_data_descriptor,
+        limit_reader,
+        None,
+        None,
+        #[cfg(feature = "aes-crypto")]
+        result.compressed_size,
+    )?
+    .unwrap();
 
     Ok(Some(ZipFile {
         data: Cow::Owned(result),
@@ -984,7 +1177,7 @@
         let mut v = Vec::new();
         v.extend_from_slice(include_bytes!("../tests/data/zip64_demo.zip"));
         let reader = ZipArchive::new(io::Cursor::new(v)).unwrap();
-        assert!(reader.len() == 1);
+        assert_eq!(reader.len(), 1);
     }
 
     #[test]
@@ -995,7 +1188,7 @@
         let mut v = Vec::new();
         v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
         let mut reader = ZipArchive::new(io::Cursor::new(v)).unwrap();
-        assert!(reader.comment() == b"");
+        assert_eq!(reader.comment(), b"");
         assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77);
     }
 
@@ -1008,9 +1201,8 @@
         v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
         let mut reader = io::Cursor::new(v);
         loop {
-            match read_zipfile_from_stream(&mut reader).unwrap() {
-                None => break,
-                _ => (),
+            if read_zipfile_from_stream(&mut reader).unwrap().is_none() {
+                break;
             }
         }
     }
@@ -1046,14 +1238,14 @@
         let mut buf3 = [0; 5];
         let mut buf4 = [0; 5];
 
-        file1.read(&mut buf1).unwrap();
-        file2.read(&mut buf2).unwrap();
-        file1.read(&mut buf3).unwrap();
-        file2.read(&mut buf4).unwrap();
+        file1.read_exact(&mut buf1).unwrap();
+        file2.read_exact(&mut buf2).unwrap();
+        file1.read_exact(&mut buf3).unwrap();
+        file2.read_exact(&mut buf4).unwrap();
 
         assert_eq!(buf1, buf2);
         assert_eq!(buf3, buf4);
-        assert!(buf1 != buf3);
+        assert_ne!(buf1, buf3);
     }
 
     #[test]
diff --git a/src/result.rs b/src/result.rs
index e8b7d05..72a30e4 100644
--- a/src/result.rs
+++ b/src/result.rs
@@ -1,37 +1,81 @@
 //! Error types that can be emitted from this library
 
+use std::error::Error;
+use std::fmt;
 use std::io;
 
-use thiserror::Error;
-
 /// Generic result type with ZipError as its error variant
 pub type ZipResult<T> = Result<T, ZipError>;
 
 /// The given password is wrong
-#[derive(Error, Debug)]
-#[error("invalid password for file in archive")]
+#[derive(Debug)]
 pub struct InvalidPassword;
 
+impl fmt::Display for InvalidPassword {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        write!(fmt, "invalid password for file in archive")
+    }
+}
+
+impl Error for InvalidPassword {}
+
 /// Error type for Zip
-#[derive(Debug, Error)]
+#[derive(Debug)]
 pub enum ZipError {
     /// An Error caused by I/O
-    #[error(transparent)]
-    Io(#[from] io::Error),
+    Io(io::Error),
 
     /// This file is probably not a zip archive
-    #[error("invalid Zip archive")]
     InvalidArchive(&'static str),
 
     /// This archive is not supported
-    #[error("unsupported Zip archive")]
     UnsupportedArchive(&'static str),
 
     /// The requested file could not be found in the archive
-    #[error("specified file not found in archive")]
     FileNotFound,
 }
 
+impl From<io::Error> for ZipError {
+    fn from(err: io::Error) -> ZipError {
+        ZipError::Io(err)
+    }
+}
+
+impl fmt::Display for ZipError {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ZipError::Io(err) => write!(fmt, "{}", err),
+            ZipError::InvalidArchive(err) => write!(fmt, "invalid Zip archive: {}", err),
+            ZipError::UnsupportedArchive(err) => write!(fmt, "unsupported Zip archive: {}", err),
+            ZipError::FileNotFound => write!(fmt, "specified file not found in archive"),
+        }
+    }
+}
+
+impl Error for ZipError {
+    fn source(&self) -> Option<&(dyn Error + 'static)> {
+        match self {
+            ZipError::Io(err) => Some(err),
+            _ => None,
+        }
+    }
+}
+
+impl ZipError {
+    /// The text used as an error when a password is required and not supplied
+    ///
+    /// ```rust,no_run
+    /// # use zip::result::ZipError;
+    /// # let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&[])).unwrap();
+    /// match archive.by_index(1) {
+    ///     Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)) => eprintln!("a password is needed to unzip this file"),
+    ///     _ => (),
+    /// }
+    /// # ()
+    /// ```
+    pub const PASSWORD_REQUIRED: &'static str = "Password required to decrypt file";
+}
+
 impl From<ZipError> for io::Error {
     fn from(err: ZipError) -> io::Error {
         io::Error::new(io::ErrorKind::Other, err)
diff --git a/src/spec.rs b/src/spec.rs
index 91966b6..3ffcf73 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -9,6 +9,9 @@
 pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06064b50;
 const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: u32 = 0x07064b50;
 
+pub const ZIP64_BYTES_THR: u64 = u32::MAX as u64;
+pub const ZIP64_ENTRY_THR: usize = u16::MAX as usize;
+
 pub struct CentralDirectoryEnd {
     pub disk_number: u16,
     pub disk_with_central_directory: u16,
@@ -117,6 +120,14 @@
             number_of_disks,
         })
     }
+
+    pub fn write<T: Write>(&self, writer: &mut T) -> ZipResult<()> {
+        writer.write_u32::<LittleEndian>(ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE)?;
+        writer.write_u32::<LittleEndian>(self.disk_with_central_directory)?;
+        writer.write_u64::<LittleEndian>(self.end_of_central_directory_offset)?;
+        writer.write_u32::<LittleEndian>(self.number_of_disks)?;
+        Ok(())
+    }
 }
 
 pub struct Zip64CentralDirectoryEnd {
@@ -179,4 +190,18 @@
             "Could not find ZIP64 central directory end",
         ))
     }
+
+    pub fn write<T: Write>(&self, writer: &mut T) -> ZipResult<()> {
+        writer.write_u32::<LittleEndian>(ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE)?;
+        writer.write_u64::<LittleEndian>(44)?; // record size
+        writer.write_u16::<LittleEndian>(self.version_made_by)?;
+        writer.write_u16::<LittleEndian>(self.version_needed_to_extract)?;
+        writer.write_u32::<LittleEndian>(self.disk_number)?;
+        writer.write_u32::<LittleEndian>(self.disk_with_central_directory)?;
+        writer.write_u64::<LittleEndian>(self.number_of_files_on_this_disk)?;
+        writer.write_u64::<LittleEndian>(self.number_of_files)?;
+        writer.write_u64::<LittleEndian>(self.central_directory_size)?;
+        writer.write_u64::<LittleEndian>(self.central_directory_offset)?;
+        Ok(())
+    }
 }
diff --git a/src/types.rs b/src/types.rs
index 154e3c2..b65fad4 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,4 +1,48 @@
 //! Types that specify what is contained in a ZIP.
+#[cfg(doc)]
+use {crate::read::ZipFile, crate::write::FileOptions};
+
+#[cfg(not(any(
+    all(target_arch = "arm", target_pointer_width = "32"),
+    target_arch = "mips",
+    target_arch = "powerpc"
+)))]
+use std::sync::atomic;
+
+#[cfg(any(
+    all(target_arch = "arm", target_pointer_width = "32"),
+    target_arch = "mips",
+    target_arch = "powerpc"
+))]
+mod atomic {
+    use crossbeam_utils::sync::ShardedLock;
+    pub use std::sync::atomic::Ordering;
+
+    #[derive(Debug, Default)]
+    pub struct AtomicU64 {
+        value: ShardedLock<u64>,
+    }
+
+    impl AtomicU64 {
+        pub fn new(v: u64) -> Self {
+            Self {
+                value: ShardedLock::new(v),
+            }
+        }
+        pub fn get_mut(&mut self) -> &mut u64 {
+            self.value.get_mut().unwrap()
+        }
+        pub fn load(&self, _: Ordering) -> u64 {
+            *self.value.read().unwrap()
+        }
+        pub fn store(&self, value: u64, _: Ordering) {
+            *self.value.write().unwrap() = value;
+        }
+    }
+}
+
+#[cfg(feature = "time")]
+use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum System {
@@ -19,19 +63,23 @@
     }
 }
 
-/// A DateTime field to be used for storing timestamps in a zip file
+/// Representation of a moment in time.
 ///
-/// This structure does bounds checking to ensure the date is able to be stored in a zip file.
+/// Zip files use an old format from DOS to store timestamps,
+/// with its own set of peculiarities.
+/// For example, it has a resolution of 2 seconds!
 ///
-/// When constructed manually from a date and time, it will also check if the input is sensible
-/// (e.g. months are from [1, 12]), but when read from a zip some parts may be out of their normal
-/// bounds (e.g. month 0, or hour 31).
+/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`],
+/// or read from one with [`ZipFile::last_modified`]
 ///
 /// # Warning
 ///
-/// Some utilities use alternative timestamps to improve the accuracy of their
-/// ZIPs, but we don't parse them yet. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904),
-/// however this API shouldn't be considered complete.
+/// Because there is no timezone associated with the [`DateTime`], they should ideally only
+/// be used for user-facing descriptions. This also means [`DateTime::to_time`] returns an
+/// [`OffsetDateTime`] (which is the equivalent of chrono's `NaiveDateTime`).
+///
+/// Modern zip files store more precise timestamps, which are ignored by [`crate::read::ZipArchive`],
+/// so keep in mind that these timestamps are unreliable. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904).
 #[derive(Debug, Clone, Copy)]
 pub struct DateTime {
     year: u16,
@@ -62,7 +110,7 @@
         let seconds = (timepart & 0b0000000000011111) << 1;
         let minutes = (timepart & 0b0000011111100000) >> 5;
         let hours = (timepart & 0b1111100000000000) >> 11;
-        let days = (datepart & 0b0000000000011111) >> 0;
+        let days = datepart & 0b0000000000011111;
         let months = (datepart & 0b0000000111100000) >> 5;
         let years = (datepart & 0b1111111000000000) >> 9;
 
@@ -85,6 +133,7 @@
     /// * hour: [0, 23]
     /// * minute: [0, 59]
     /// * second: [0, 60]
+    #[allow(clippy::result_unit_err)]
     pub fn from_date_and_time(
         year: u16,
         month: u8,
@@ -93,8 +142,7 @@
         minute: u8,
         second: u8,
     ) -> Result<DateTime, ()> {
-        if year >= 1980
-            && year <= 2107
+        if (1980..=2107).contains(&year)
             && month >= 1
             && month <= 12
             && day >= 1
@@ -117,30 +165,19 @@
     }
 
     #[cfg(feature = "time")]
-    /// Converts a ::time::Tm object to a DateTime
+    /// Converts a OffsetDateTime object to a DateTime
     ///
     /// Returns `Err` when this object is out of bounds
-    pub fn from_time(tm: ::time::Tm) -> Result<DateTime, ()> {
-        if tm.tm_year >= 80
-            && tm.tm_year <= 207
-            && tm.tm_mon >= 0
-            && tm.tm_mon <= 11
-            && tm.tm_mday >= 1
-            && tm.tm_mday <= 31
-            && tm.tm_hour >= 0
-            && tm.tm_hour <= 23
-            && tm.tm_min >= 0
-            && tm.tm_min <= 59
-            && tm.tm_sec >= 0
-            && tm.tm_sec <= 60
-        {
+    #[allow(clippy::result_unit_err)]
+    pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, ()> {
+        if dt.year() >= 1980 && dt.year() <= 2107 {
             Ok(DateTime {
-                year: (tm.tm_year + 1900) as u16,
-                month: (tm.tm_mon + 1) as u8,
-                day: tm.tm_mday as u8,
-                hour: tm.tm_hour as u8,
-                minute: tm.tm_min as u8,
-                second: tm.tm_sec as u8,
+                year: (dt.year()) as u16,
+                month: (dt.month()) as u8,
+                day: dt.day() as u8,
+                hour: dt.hour() as u8,
+                minute: dt.minute() as u8,
+                second: dt.second() as u8,
             })
         } else {
             Err(())
@@ -158,20 +195,14 @@
     }
 
     #[cfg(feature = "time")]
-    /// Converts the datetime to a Tm structure
-    ///
-    /// The fields `tm_wday`, `tm_yday`, `tm_utcoff` and `tm_nsec` are set to their defaults.
-    pub fn to_time(&self) -> ::time::Tm {
-        ::time::Tm {
-            tm_sec: self.second as i32,
-            tm_min: self.minute as i32,
-            tm_hour: self.hour as i32,
-            tm_mday: self.day as i32,
-            tm_mon: self.month as i32 - 1,
-            tm_year: self.year as i32 - 1900,
-            tm_isdst: -1,
-            ..::time::empty_tm()
-        }
+    /// Converts the DateTime to a OffsetDateTime structure
+    pub fn to_time(&self) -> Result<OffsetDateTime, ComponentRange> {
+        use std::convert::TryFrom;
+
+        let date =
+            Date::from_calendar_date(self.year as i32, Month::try_from(self.month)?, self.day)?;
+        let time = Time::from_hms(self.hour, self.minute, self.second)?;
+        Ok(PrimitiveDateTime::new(date, time).assume_utc())
     }
 
     /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018.
@@ -180,26 +211,46 @@
     }
 
     /// Get the month, where 1 = january and 12 = december
+    ///
+    /// # Warning
+    ///
+    /// When read from a zip file, this may not be a reasonable value
     pub fn month(&self) -> u8 {
         self.month
     }
 
     /// Get the day
+    ///
+    /// # Warning
+    ///
+    /// When read from a zip file, this may not be a reasonable value
     pub fn day(&self) -> u8 {
         self.day
     }
 
     /// Get the hour
+    ///
+    /// # Warning
+    ///
+    /// When read from a zip file, this may not be a reasonable value
     pub fn hour(&self) -> u8 {
         self.hour
     }
 
     /// Get the minute
+    ///
+    /// # Warning
+    ///
+    /// When read from a zip file, this may not be a reasonable value
     pub fn minute(&self) -> u8 {
         self.minute
     }
 
     /// Get the second
+    ///
+    /// # Warning
+    ///
+    /// When read from a zip file, this may not be a reasonable value
     pub fn second(&self) -> u8 {
         self.second
     }
@@ -207,6 +258,37 @@
 
 pub const DEFAULT_VERSION: u8 = 46;
 
+/// A type like `AtomicU64` except it implements `Clone` and has predefined
+/// ordering.
+///
+/// It uses `Relaxed` ordering because it is not used for synchronisation.
+#[derive(Debug)]
+pub struct AtomicU64(atomic::AtomicU64);
+
+impl AtomicU64 {
+    pub fn new(v: u64) -> Self {
+        Self(atomic::AtomicU64::new(v))
+    }
+
+    pub fn load(&self) -> u64 {
+        self.0.load(atomic::Ordering::Relaxed)
+    }
+
+    pub fn store(&self, val: u64) {
+        self.0.store(val, atomic::Ordering::Relaxed)
+    }
+
+    pub fn get_mut(&mut self) -> &mut u64 {
+        self.0.get_mut()
+    }
+}
+
+impl Clone for AtomicU64 {
+    fn clone(&self) -> Self {
+        Self(atomic::AtomicU64::new(self.load()))
+    }
+}
+
 /// Structure representing a ZIP file.
 #[derive(Debug, Clone)]
 pub struct ZipFileData {
@@ -216,8 +298,12 @@
     pub version_made_by: u8,
     /// True if the file is encrypted.
     pub encrypted: bool,
+    /// True if the file uses a data-descriptor section
+    pub using_data_descriptor: bool,
     /// Compression method used to store the file
     pub compression_method: crate::compression::CompressionMethod,
+    /// Compression level to store the file
+    pub compression_level: Option<i32>,
     /// Last modified time. This will only have a 2 second precision.
     pub last_modified_time: DateTime,
     /// CRC32 checksum
@@ -230,6 +316,8 @@
     pub file_name: String,
     /// Raw file name. To be used when file_name was incorrectly decoded.
     pub file_name_raw: Vec<u8>,
+    /// Extra field usually used for storage expansion
+    pub extra_field: Vec<u8>,
     /// File comment
     pub file_comment: String,
     /// Specifies where the local header of the file starts
@@ -239,9 +327,13 @@
     /// Note that when this is not known, it is set to 0
     pub central_header_start: u64,
     /// Specifies where the compressed data of the file starts
-    pub data_start: u64,
+    pub data_start: AtomicU64,
     /// External file attributes
     pub external_attributes: u32,
+    /// Reserve local ZIP64 extra field
+    pub large_file: bool,
+    /// AES mode if applicable
+    pub aes_mode: Option<(AesMode, AesVendorVersion)>,
 }
 
 impl ZipFileData {
@@ -265,25 +357,63 @@
 
         ::std::path::Path::new(&filename)
             .components()
-            .filter(|component| match *component {
-                ::std::path::Component::Normal(..) => true,
-                _ => false,
-            })
+            .filter(|component| matches!(*component, ::std::path::Component::Normal(..)))
             .fold(::std::path::PathBuf::new(), |mut path, ref cur| {
                 path.push(cur.as_os_str());
                 path
             })
     }
 
+    pub fn zip64_extension(&self) -> bool {
+        self.uncompressed_size > 0xFFFFFFFF
+            || self.compressed_size > 0xFFFFFFFF
+            || self.header_start > 0xFFFFFFFF
+    }
+
     pub fn version_needed(&self) -> u16 {
-        match self.compression_method {
+        // higher versions matched first
+        match (self.zip64_extension(), self.compression_method) {
             #[cfg(feature = "bzip2")]
-            crate::compression::CompressionMethod::Bzip2 => 46,
+            (_, crate::compression::CompressionMethod::Bzip2) => 46,
+            (true, _) => 45,
             _ => 20,
         }
     }
 }
 
+/// The encryption specification used to encrypt a file with AES.
+///
+/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
+/// does not make use of the CRC check.
+#[derive(Copy, Clone, Debug)]
+pub enum AesVendorVersion {
+    Ae1,
+    Ae2,
+}
+
+/// AES variant used.
+#[derive(Copy, Clone, Debug)]
+pub enum AesMode {
+    Aes128,
+    Aes192,
+    Aes256,
+}
+
+#[cfg(feature = "aes-crypto")]
+impl AesMode {
+    pub fn salt_length(&self) -> usize {
+        self.key_length() / 2
+    }
+
+    pub fn key_length(&self) -> usize {
+        match self {
+            Self::Aes128 => 16,
+            Self::Aes192 => 24,
+            Self::Aes256 => 32,
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     #[test]
@@ -303,18 +433,23 @@
             system: System::Dos,
             version_made_by: 0,
             encrypted: false,
+            using_data_descriptor: false,
             compression_method: crate::compression::CompressionMethod::Stored,
+            compression_level: None,
             last_modified_time: DateTime::default(),
             crc32: 0,
             compressed_size: 0,
             uncompressed_size: 0,
             file_name: file_name.clone(),
             file_name_raw: file_name.into_bytes(),
+            extra_field: Vec::new(),
             file_comment: String::new(),
             header_start: 0,
-            data_start: 0,
+            data_start: AtomicU64::new(0),
             central_header_start: 0,
             external_attributes: 0,
+            large_file: false,
+            aes_mode: None,
         };
         assert_eq!(
             data.file_name_sanitized(),
@@ -323,6 +458,7 @@
     }
 
     #[test]
+    #[allow(clippy::unusual_byte_groupings)]
     fn datetime_default() {
         use super::DateTime;
         let dt = DateTime::default();
@@ -331,6 +467,7 @@
     }
 
     #[test]
+    #[allow(clippy::unusual_byte_groupings)]
     fn datetime_max() {
         use super::DateTime;
         let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap();
@@ -358,57 +495,25 @@
     }
 
     #[cfg(feature = "time")]
+    use time::{format_description::well_known::Rfc3339, OffsetDateTime};
+
+    #[cfg(feature = "time")]
     #[test]
     fn datetime_from_time_bounds() {
         use super::DateTime;
+        use time::macros::datetime;
 
         // 1979-12-31 23:59:59
-        assert!(DateTime::from_time(::time::Tm {
-            tm_sec: 59,
-            tm_min: 59,
-            tm_hour: 23,
-            tm_mday: 31,
-            tm_mon: 11,  // tm_mon has number range [0, 11]
-            tm_year: 79, // 1979 - 1900 = 79
-            ..::time::empty_tm()
-        })
-        .is_err());
+        assert!(DateTime::from_time(datetime!(1979-12-31 23:59:59 UTC)).is_err());
 
         // 1980-01-01 00:00:00
-        assert!(DateTime::from_time(::time::Tm {
-            tm_sec: 0,
-            tm_min: 0,
-            tm_hour: 0,
-            tm_mday: 1,
-            tm_mon: 0,   // tm_mon has number range [0, 11]
-            tm_year: 80, // 1980 - 1900 = 80
-            ..::time::empty_tm()
-        })
-        .is_ok());
+        assert!(DateTime::from_time(datetime!(1980-01-01 00:00:00 UTC)).is_ok());
 
         // 2107-12-31 23:59:59
-        assert!(DateTime::from_time(::time::Tm {
-            tm_sec: 59,
-            tm_min: 59,
-            tm_hour: 23,
-            tm_mday: 31,
-            tm_mon: 11,   // tm_mon has number range [0, 11]
-            tm_year: 207, // 2107 - 1900 = 207
-            ..::time::empty_tm()
-        })
-        .is_ok());
+        assert!(DateTime::from_time(datetime!(2107-12-31 23:59:59 UTC)).is_ok());
 
         // 2108-01-01 00:00:00
-        assert!(DateTime::from_time(::time::Tm {
-            tm_sec: 0,
-            tm_min: 0,
-            tm_hour: 0,
-            tm_mday: 1,
-            tm_mon: 0,    // tm_mon has number range [0, 11]
-            tm_year: 208, // 2108 - 1900 = 208
-            ..::time::empty_tm()
-        })
-        .is_err());
+        assert!(DateTime::from_time(datetime!(2108-01-01 00:00:00 UTC)).is_err());
     }
 
     #[test]
@@ -424,7 +529,7 @@
 
         #[cfg(feature = "time")]
         assert_eq!(
-            format!("{}", dt.to_time().rfc3339()),
+            dt.to_time().unwrap().format(&Rfc3339).unwrap(),
             "2018-11-17T10:38:30Z"
         );
     }
@@ -441,10 +546,7 @@
         assert_eq!(dt.second(), 62);
 
         #[cfg(feature = "time")]
-        assert_eq!(
-            format!("{}", dt.to_time().rfc3339()),
-            "2107-15-31T31:63:62Z"
-        );
+        assert!(dt.to_time().is_err());
 
         let dt = DateTime::from_msdos(0x0000, 0x0000);
         assert_eq!(dt.year(), 1980);
@@ -455,10 +557,7 @@
         assert_eq!(dt.second(), 0);
 
         #[cfg(feature = "time")]
-        assert_eq!(
-            format!("{}", dt.to_time().rfc3339()),
-            "1980-00-00T00:00:00Z"
-        );
+        assert!(dt.to_time().is_err());
     }
 
     #[cfg(feature = "time")]
@@ -467,8 +566,8 @@
         use super::DateTime;
 
         // 2020-01-01 00:00:00
-        let clock = ::time::Timespec::new(1577836800, 0);
-        let tm = ::time::at_utc(clock);
-        assert!(DateTime::from_time(tm).is_ok());
+        let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
+
+        assert!(DateTime::from_time(clock).is_ok());
     }
 }
diff --git a/src/write.rs b/src/write.rs
index bc68817..551b4e3 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -1,11 +1,11 @@
 //! Types for creating ZIP archives
 
 use crate::compression::CompressionMethod;
-use crate::read::ZipFile;
+use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile};
 use crate::result::{ZipError, ZipResult};
 use crate::spec;
-use crate::types::{DateTime, System, ZipFileData, DEFAULT_VERSION};
-use byteorder::{LittleEndian, WriteBytesExt};
+use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION};
+use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
 use crc32fast::Hasher;
 use std::default::Default;
 use std::io;
@@ -22,6 +22,12 @@
 #[cfg(feature = "bzip2")]
 use bzip2::write::BzEncoder;
 
+#[cfg(feature = "time")]
+use time::OffsetDateTime;
+
+#[cfg(feature = "zstd")]
+use zstd::stream::write::Encoder as ZstdEncoder;
+
 enum GenericZipWriter<W: Write + io::Seek> {
     Closed,
     Storer(W),
@@ -33,44 +39,52 @@
     Deflater(DeflateEncoder<W>),
     #[cfg(feature = "bzip2")]
     Bzip2(BzEncoder<W>),
+    #[cfg(feature = "zstd")]
+    Zstd(ZstdEncoder<'static, W>),
 }
-
-/// ZIP archive generator
-///
-/// Handles the bookkeeping involved in building an archive, and provides an
-/// API to edit its contents.
-///
-/// ```
-/// # fn doit() -> zip::result::ZipResult<()>
-/// # {
-/// # use zip::ZipWriter;
-/// use std::io::Write;
-/// use zip::write::FileOptions;
-///
-/// // We use a buffer here, though you'd normally use a `File`
-/// let mut buf = [0; 65536];
-/// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
-///
-/// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
-/// zip.start_file("hello_world.txt", options)?;
-/// zip.write(b"Hello, World!")?;
-///
-/// // Apply the changes you've made.
-/// // Dropping the `ZipWriter` will have the same effect, but may silently fail
-/// zip.finish()?;
-///
-/// # Ok(())
-/// # }
-/// # doit().unwrap();
-/// ```
-pub struct ZipWriter<W: Write + io::Seek> {
-    inner: GenericZipWriter<W>,
-    files: Vec<ZipFileData>,
-    stats: ZipWriterStats,
-    writing_to_file: bool,
-    comment: String,
-    writing_raw: bool,
+// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
+pub(crate) mod zip_writer {
+    use super::*;
+    /// ZIP archive generator
+    ///
+    /// Handles the bookkeeping involved in building an archive, and provides an
+    /// API to edit its contents.
+    ///
+    /// ```
+    /// # fn doit() -> zip::result::ZipResult<()>
+    /// # {
+    /// # use zip::ZipWriter;
+    /// use std::io::Write;
+    /// use zip::write::FileOptions;
+    ///
+    /// // We use a buffer here, though you'd normally use a `File`
+    /// let mut buf = [0; 65536];
+    /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
+    ///
+    /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
+    /// zip.start_file("hello_world.txt", options)?;
+    /// zip.write(b"Hello, World!")?;
+    ///
+    /// // Apply the changes you've made.
+    /// // Dropping the `ZipWriter` will have the same effect, but may silently fail
+    /// zip.finish()?;
+    ///
+    /// # Ok(())
+    /// # }
+    /// # doit().unwrap();
+    /// ```
+    pub struct ZipWriter<W: Write + io::Seek> {
+        pub(super) inner: GenericZipWriter<W>,
+        pub(super) files: Vec<ZipFileData>,
+        pub(super) stats: ZipWriterStats,
+        pub(super) writing_to_file: bool,
+        pub(super) writing_to_extra_field: bool,
+        pub(super) writing_to_central_extra_field_only: bool,
+        pub(super) writing_raw: bool,
+        pub(super) comment: Vec<u8>,
+    }
 }
+pub use zip_writer::ZipWriter;
 
 #[derive(Default)]
 struct ZipWriterStats {
@@ -89,8 +103,10 @@
 #[derive(Copy, Clone)]
 pub struct FileOptions {
     compression_method: CompressionMethod,
+    compression_level: Option<i32>,
     last_modified_time: DateTime,
     permissions: Option<u32>,
+    large_file: bool,
 }
 
 impl FileOptions {
@@ -109,11 +125,13 @@
                 feature = "deflate-zlib"
             )))]
             compression_method: CompressionMethod::Stored,
+            compression_level: None,
             #[cfg(feature = "time")]
-            last_modified_time: DateTime::from_time(time::now()).unwrap_or_default(),
+            last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(),
             #[cfg(not(feature = "time"))]
             last_modified_time: DateTime::default(),
             permissions: None,
+            large_file: false,
         }
     }
 
@@ -121,16 +139,32 @@
     ///
     /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
     /// disabled, `CompressionMethod::Stored` becomes the default.
-    /// otherwise.
+    #[must_use]
     pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
         self.compression_method = method;
         self
     }
 
+    /// Set the compression level for the new file
+    ///
+    /// `None` value specifies default compression level.
+    ///
+    /// Range of values depends on compression method:
+    /// * `Deflated`: 0 - 9. Default is 6
+    /// * `Bzip2`: 0 - 9. Default is 6
+    /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
+    /// * others: only `None` is allowed
+    #[must_use]
+    pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
+        self.compression_level = level;
+        self
+    }
+
     /// Set the last modified time
     ///
     /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
     /// otherwise
+    #[must_use]
     pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
         self.last_modified_time = mod_time;
         self
@@ -141,10 +175,22 @@
     /// The format is represented with unix-style permissions.
     /// The default is `0o644`, which represents `rw-r--r--` for files,
     /// and `0o755`, which represents `rwxr-xr-x` for directories
+    #[must_use]
     pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
         self.permissions = Some(mode & 0o777);
         self
     }
+
+    /// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
+    ///
+    /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`,
+    /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are
+    /// wasted. The default is `false`.
+    #[must_use]
+    pub fn large_file(mut self, large: bool) -> FileOptions {
+        self.large_file = large;
+        self
+    }
 }
 
 impl Default for FileOptions {
@@ -163,11 +209,24 @@
         }
         match self.inner.ref_mut() {
             Some(ref mut w) => {
-                let write_result = w.write(buf);
-                if let Ok(count) = write_result {
-                    self.stats.update(&buf[0..count]);
+                if self.writing_to_extra_field {
+                    self.files.last_mut().unwrap().extra_field.write(buf)
+                } else {
+                    let write_result = w.write(buf);
+                    if let Ok(count) = write_result {
+                        self.stats.update(&buf[0..count]);
+                        if self.stats.bytes_written > spec::ZIP64_BYTES_THR
+                            && !self.files.last_mut().unwrap().large_file
+                        {
+                            let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed);
+                            return Err(io::Error::new(
+                                io::ErrorKind::Other,
+                                "Large file option has not been set",
+                            ));
+                        }
+                    }
+                    write_result
                 }
-                write_result
             }
             None => Err(io::Error::new(
                 io::ErrorKind::BrokenPipe,
@@ -194,6 +253,48 @@
     }
 }
 
+impl<A: Read + Write + io::Seek> ZipWriter<A> {
+    /// Initializes the archive from an existing ZIP archive, making it ready for append.
+    pub fn new_append(mut readwriter: A) -> ZipResult<ZipWriter<A>> {
+        let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
+
+        if footer.disk_number != footer.disk_with_central_directory {
+            return Err(ZipError::UnsupportedArchive(
+                "Support for multi-disk files is not implemented",
+            ));
+        }
+
+        let (archive_offset, directory_start, number_of_files) =
+            ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?;
+
+        if readwriter
+            .seek(io::SeekFrom::Start(directory_start))
+            .is_err()
+        {
+            return Err(ZipError::InvalidArchive(
+                "Could not seek to start of central directory",
+            ));
+        }
+
+        let files = (0..number_of_files)
+            .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset))
+            .collect::<Result<Vec<_>, _>>()?;
+
+        let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it
+
+        Ok(ZipWriter {
+            inner: GenericZipWriter::Storer(readwriter),
+            files,
+            stats: Default::default(),
+            writing_to_file: false,
+            writing_to_extra_field: false,
+            writing_to_central_extra_field_only: false,
+            comment: footer.zip_file_comment,
+            writing_raw: true, // avoid recomputing the last file's header
+        })
+    }
+}
+
 impl<W: Write + io::Seek> ZipWriter<W> {
     /// Initializes the archive.
     ///
@@ -204,8 +305,10 @@
             files: Vec::new(),
             stats: Default::default(),
             writing_to_file: false,
-            comment: String::new(),
+            writing_to_extra_field: false,
+            writing_to_central_extra_field_only: false,
             writing_raw: false,
+            comment: Vec::new(),
         }
     }
 
@@ -214,7 +317,15 @@
     where
         S: Into<String>,
     {
-        self.comment = comment.into();
+        self.set_raw_comment(comment.into().into())
+    }
+
+    /// Set ZIP archive comment.
+    ///
+    /// This sets the raw bytes of the comment. The comment
+    /// is typically expected to be encoded in UTF-8
+    pub fn set_raw_comment(&mut self, comment: Vec<u8>) {
+        self.comment = comment;
     }
 
     /// Start a new file for with the requested options.
@@ -229,8 +340,7 @@
     {
         self.finish_file()?;
 
-        let is_raw = raw_values.is_some();
-        let raw_values = raw_values.unwrap_or_else(|| ZipRawValues {
+        let raw_values = raw_values.unwrap_or(ZipRawValues {
             crc32: 0,
             compressed_size: 0,
             uncompressed_size: 0,
@@ -245,24 +355,29 @@
                 system: System::Unix,
                 version_made_by: DEFAULT_VERSION,
                 encrypted: false,
+                using_data_descriptor: false,
                 compression_method: options.compression_method,
+                compression_level: options.compression_level,
                 last_modified_time: options.last_modified_time,
                 crc32: raw_values.crc32,
                 compressed_size: raw_values.compressed_size,
                 uncompressed_size: raw_values.uncompressed_size,
                 file_name: name.into(),
                 file_name_raw: Vec::new(), // Never used for saving
+                extra_field: Vec::new(),
                 file_comment: String::new(),
                 header_start,
-                data_start: 0,
+                data_start: AtomicU64::new(0),
                 central_header_start: 0,
                 external_attributes: permissions << 16,
+                large_file: options.large_file,
+                aes_mode: None,
             };
             write_local_file_header(writer, &file)?;
 
             let header_end = writer.seek(io::SeekFrom::Current(0))?;
             self.stats.start = header_end;
-            file.data_start = header_end;
+            *file.data_start.get_mut() = header_end;
 
             self.stats.bytes_written = 0;
             self.stats.hasher = Hasher::new();
@@ -270,18 +385,15 @@
             self.files.push(file);
         }
 
-        self.writing_raw = is_raw;
-        self.inner.switch_to(if is_raw {
-            CompressionMethod::Stored
-        } else {
-            options.compression_method
-        })?;
-
         Ok(())
     }
 
     fn finish_file(&mut self) -> ZipResult<()> {
-        self.inner.switch_to(CompressionMethod::Stored)?;
+        if self.writing_to_extra_field {
+            // Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
+            self.end_extra_data()?;
+        }
+        self.inner.switch_to(CompressionMethod::Stored, None)?;
         let writer = self.inner.get_plain();
 
         if !self.writing_raw {
@@ -316,13 +428,15 @@
         }
         *options.permissions.as_mut().unwrap() |= 0o100000;
         self.start_entry(name, options, None)?;
+        self.inner
+            .switch_to(options.compression_method, options.compression_level)?;
         self.writing_to_file = true;
         Ok(())
     }
 
     /// Starts a file, taking a Path as argument.
     ///
-    /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal'
+    /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal'
     /// Components, such as a starting '/' or '..' and '.'.
     #[deprecated(
         since = "0.5.7",
@@ -336,6 +450,171 @@
         self.start_file(path_to_string(path), options)
     }
 
+    /// Create an aligned file in the archive and start writing its' contents.
+    ///
+    /// Returns the number of padding bytes required to align the file.
+    ///
+    /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
+    pub fn start_file_aligned<S>(
+        &mut self,
+        name: S,
+        options: FileOptions,
+        align: u16,
+    ) -> Result<u64, ZipError>
+    where
+        S: Into<String>,
+    {
+        let data_start = self.start_file_with_extra_data(name, options)?;
+        let align = align as u64;
+        if align > 1 && data_start % align != 0 {
+            let pad_length = (align - (data_start + 4) % align) % align;
+            let pad = vec![0; pad_length as usize];
+            self.write_all(b"za").map_err(ZipError::from)?; // 0x617a
+            self.write_u16::<LittleEndian>(pad.len() as u16)
+                .map_err(ZipError::from)?;
+            self.write_all(&pad).map_err(ZipError::from)?;
+            assert_eq!(self.end_local_start_central_extra_data()? % align, 0);
+        }
+        let extra_data_end = self.end_extra_data()?;
+        Ok(extra_data_end - data_start)
+    }
+
+    /// Create a file in the archive and start writing its extra data first.
+    ///
+    /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`].
+    /// Optionally, distinguish local from central extra data with
+    /// [`ZipWriter::end_local_start_central_extra_data`].
+    ///
+    /// Returns the preliminary starting offset of the file data without any extra data allowing to
+    /// align the file data by calculating a pad length to be prepended as part of the extra data.
+    ///
+    /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`]
+    ///
+    /// ```
+    /// use byteorder::{LittleEndian, WriteBytesExt};
+    /// use zip::{ZipArchive, ZipWriter, result::ZipResult};
+    /// use zip::{write::FileOptions, CompressionMethod};
+    /// use std::io::{Write, Cursor};
+    ///
+    /// # fn main() -> ZipResult<()> {
+    /// let mut archive = Cursor::new(Vec::new());
+    ///
+    /// {
+    ///     let mut zip = ZipWriter::new(&mut archive);
+    ///     let options = FileOptions::default()
+    ///         .compression_method(CompressionMethod::Stored);
+    ///
+    ///     zip.start_file_with_extra_data("identical_extra_data.txt", options)?;
+    ///     let extra_data = b"local and central extra data";
+    ///     zip.write_u16::<LittleEndian>(0xbeef)?;
+    ///     zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
+    ///     zip.write_all(extra_data)?;
+    ///     zip.end_extra_data()?;
+    ///     zip.write_all(b"file data")?;
+    ///
+    ///     let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?;
+    ///     let extra_data = b"local extra data";
+    ///     zip.write_u16::<LittleEndian>(0xbeef)?;
+    ///     zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
+    ///     zip.write_all(extra_data)?;
+    ///     let data_start = data_start as usize + 4 + extra_data.len() + 4;
+    ///     let align = 64;
+    ///     let pad_length = (align - data_start % align) % align;
+    ///     assert_eq!(pad_length, 19);
+    ///     zip.write_u16::<LittleEndian>(0xdead)?;
+    ///     zip.write_u16::<LittleEndian>(pad_length as u16)?;
+    ///     zip.write_all(&vec![0; pad_length])?;
+    ///     let data_start = zip.end_local_start_central_extra_data()?;
+    ///     assert_eq!(data_start as usize % align, 0);
+    ///     let extra_data = b"central extra data";
+    ///     zip.write_u16::<LittleEndian>(0xbeef)?;
+    ///     zip.write_u16::<LittleEndian>(extra_data.len() as u16)?;
+    ///     zip.write_all(extra_data)?;
+    ///     zip.end_extra_data()?;
+    ///     zip.write_all(b"file data")?;
+    ///
+    ///     zip.finish()?;
+    /// }
+    ///
+    /// let mut zip = ZipArchive::new(archive)?;
+    /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data");
+    /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data");
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn start_file_with_extra_data<S>(
+        &mut self,
+        name: S,
+        mut options: FileOptions,
+    ) -> ZipResult<u64>
+    where
+        S: Into<String>,
+    {
+        if options.permissions.is_none() {
+            options.permissions = Some(0o644);
+        }
+        *options.permissions.as_mut().unwrap() |= 0o100000;
+        self.start_entry(name, options, None)?;
+        self.writing_to_file = true;
+        self.writing_to_extra_field = true;
+        Ok(self.files.last().unwrap().data_start.load())
+    }
+
+    /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`].
+    ///
+    /// Returns the final starting offset of the file data.
+    pub fn end_local_start_central_extra_data(&mut self) -> ZipResult<u64> {
+        let data_start = self.end_extra_data()?;
+        self.files.last_mut().unwrap().extra_field.clear();
+        self.writing_to_extra_field = true;
+        self.writing_to_central_extra_field_only = true;
+        Ok(data_start)
+    }
+
+    /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`].
+    ///
+    /// Returns the final starting offset of the file data.
+    pub fn end_extra_data(&mut self) -> ZipResult<u64> {
+        // Require `start_file_with_extra_data()`. Ensures `file` is some.
+        if !self.writing_to_extra_field {
+            return Err(ZipError::Io(io::Error::new(
+                io::ErrorKind::Other,
+                "Not writing to extra field",
+            )));
+        }
+        let file = self.files.last_mut().unwrap();
+
+        validate_extra_data(file)?;
+
+        let data_start = file.data_start.get_mut();
+
+        if !self.writing_to_central_extra_field_only {
+            let writer = self.inner.get_plain();
+
+            // Append extra data to local file header and keep it for central file header.
+            writer.write_all(&file.extra_field)?;
+
+            // Update final `data_start`.
+            let header_end = *data_start + file.extra_field.len() as u64;
+            self.stats.start = header_end;
+            *data_start = header_end;
+
+            // Update extra field length in local file header.
+            let extra_field_length =
+                if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
+            writer.seek(io::SeekFrom::Start(file.header_start + 28))?;
+            writer.write_u16::<LittleEndian>(extra_field_length)?;
+            writer.seek(io::SeekFrom::Start(header_end))?;
+
+            self.inner
+                .switch_to(file.compression_method, file.compression_level)?;
+        }
+
+        self.writing_to_extra_field = false;
+        self.writing_to_central_extra_field_only = false;
+        Ok(*data_start)
+    }
+
     /// Add a new file using the already compressed data from a ZIP file being read and renames it, this
     /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again.
     /// Any `ZipFile` metadata is copied and not checked, for example the file CRC.
@@ -366,11 +645,12 @@
     where
         S: Into<String>,
     {
-        let options = FileOptions::default()
+        let mut options = FileOptions::default()
+            .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
             .last_modified_time(file.last_modified())
             .compression_method(file.compression());
         if let Some(perms) = file.unix_mode() {
-            options.unix_permissions(perms);
+            options = options.unix_permissions(perms);
         }
 
         let raw_values = ZipRawValues {
@@ -381,6 +661,7 @@
 
         self.start_entry(name, options, Some(raw_values))?;
         self.writing_to_file = true;
+        self.writing_raw = true;
 
         io::copy(file.get_raw_reader(), self)?;
 
@@ -478,14 +759,40 @@
             }
             let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start;
 
+            if self.files.len() > spec::ZIP64_ENTRY_THR
+                || central_size.max(central_start) > spec::ZIP64_BYTES_THR
+            {
+                let zip64_footer = spec::Zip64CentralDirectoryEnd {
+                    version_made_by: DEFAULT_VERSION as u16,
+                    version_needed_to_extract: DEFAULT_VERSION as u16,
+                    disk_number: 0,
+                    disk_with_central_directory: 0,
+                    number_of_files_on_this_disk: self.files.len() as u64,
+                    number_of_files: self.files.len() as u64,
+                    central_directory_size: central_size,
+                    central_directory_offset: central_start,
+                };
+
+                zip64_footer.write(writer)?;
+
+                let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
+                    disk_with_central_directory: 0,
+                    end_of_central_directory_offset: central_start + central_size,
+                    number_of_disks: 1,
+                };
+
+                zip64_footer.write(writer)?;
+            }
+
+            let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16;
             let footer = spec::CentralDirectoryEnd {
                 disk_number: 0,
                 disk_with_central_directory: 0,
-                number_of_files_on_this_disk: self.files.len() as u16,
-                number_of_files: self.files.len() as u16,
-                central_directory_size: central_size as u32,
-                central_directory_offset: central_start as u32,
-                zip_file_comment: self.comment.as_bytes().to_vec(),
+                zip_file_comment: self.comment.clone(),
+                number_of_files_on_this_disk: number_of_files,
+                number_of_files,
+                central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32,
+                central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32,
             };
 
             footer.write(writer)?;
@@ -499,14 +806,18 @@
     fn drop(&mut self) {
         if !self.inner.is_closed() {
             if let Err(e) = self.finalize() {
-                let _ = write!(&mut io::stderr(), "ZipWriter drop failed: {:?}", e);
+                let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
             }
         }
     }
 }
 
 impl<W: Write + io::Seek> GenericZipWriter<W> {
-    fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> {
+    fn switch_to(
+        &mut self,
+        compression: CompressionMethod,
+        compression_level: Option<i32>,
+    ) -> ZipResult<()> {
         match self.current_compression() {
             Some(method) if method == compression => return Ok(()),
             None => {
@@ -529,6 +840,8 @@
             GenericZipWriter::Deflater(w) => w.finish()?,
             #[cfg(feature = "bzip2")]
             GenericZipWriter::Bzip2(w) => w.finish()?,
+            #[cfg(feature = "zstd")]
+            GenericZipWriter::Zstd(w) => w.finish()?,
             GenericZipWriter::Closed => {
                 return Err(io::Error::new(
                     io::ErrorKind::BrokenPipe,
@@ -541,7 +854,15 @@
         *self = {
             #[allow(deprecated)]
             match compression {
-                CompressionMethod::Stored => GenericZipWriter::Storer(bare),
+                CompressionMethod::Stored => {
+                    if compression_level.is_some() {
+                        return Err(ZipError::UnsupportedArchive(
+                            "Unsupported compression level",
+                        ));
+                    }
+
+                    GenericZipWriter::Storer(bare)
+                }
                 #[cfg(any(
                     feature = "deflate",
                     feature = "deflate-miniz",
@@ -549,12 +870,50 @@
                 ))]
                 CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
                     bare,
-                    flate2::Compression::default(),
+                    flate2::Compression::new(
+                        clamp_opt(
+                            compression_level
+                                .unwrap_or(flate2::Compression::default().level() as i32),
+                            deflate_compression_level_range(),
+                        )
+                        .ok_or(ZipError::UnsupportedArchive(
+                            "Unsupported compression level",
+                        ))? as u32,
+                    ),
                 )),
                 #[cfg(feature = "bzip2")]
-                CompressionMethod::Bzip2 => {
-                    GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::Default))
+                CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(
+                    bare,
+                    bzip2::Compression::new(
+                        clamp_opt(
+                            compression_level
+                                .unwrap_or(bzip2::Compression::default().level() as i32),
+                            bzip2_compression_level_range(),
+                        )
+                        .ok_or(ZipError::UnsupportedArchive(
+                            "Unsupported compression level",
+                        ))? as u32,
+                    ),
+                )),
+                CompressionMethod::AES => {
+                    return Err(ZipError::UnsupportedArchive(
+                        "AES compression is not supported for writing",
+                    ))
                 }
+                #[cfg(feature = "zstd")]
+                CompressionMethod::Zstd => GenericZipWriter::Zstd(
+                    ZstdEncoder::new(
+                        bare,
+                        clamp_opt(
+                            compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL),
+                            zstd::compression_level_range(),
+                        )
+                        .ok_or(ZipError::UnsupportedArchive(
+                            "Unsupported compression level",
+                        ))?,
+                    )
+                    .unwrap(),
+                ),
                 CompressionMethod::Unsupported(..) => {
                     return Err(ZipError::UnsupportedArchive("Unsupported compression"))
                 }
@@ -575,15 +934,14 @@
             GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
             #[cfg(feature = "bzip2")]
             GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
+            #[cfg(feature = "zstd")]
+            GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write),
             GenericZipWriter::Closed => None,
         }
     }
 
     fn is_closed(&self) -> bool {
-        match *self {
-            GenericZipWriter::Closed => true,
-            _ => false,
-        }
+        matches!(*self, GenericZipWriter::Closed)
     }
 
     fn get_plain(&mut self) -> &mut W {
@@ -604,6 +962,8 @@
             GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
             #[cfg(feature = "bzip2")]
             GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
+            #[cfg(feature = "zstd")]
+            GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd),
             GenericZipWriter::Closed => None,
         }
     }
@@ -616,6 +976,39 @@
     }
 }
 
+#[cfg(any(
+    feature = "deflate",
+    feature = "deflate-miniz",
+    feature = "deflate-zlib"
+))]
+fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> {
+    let min = flate2::Compression::none().level() as i32;
+    let max = flate2::Compression::best().level() as i32;
+    min..=max
+}
+
+#[cfg(feature = "bzip2")]
+fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> {
+    let min = bzip2::Compression::none().level() as i32;
+    let max = bzip2::Compression::best().level() as i32;
+    min..=max
+}
+
+#[cfg(any(
+    feature = "deflate",
+    feature = "deflate-miniz",
+    feature = "deflate-zlib",
+    feature = "bzip2",
+    feature = "zstd"
+))]
+fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> {
+    if range.contains(&value) {
+        Some(value)
+    } else {
+        None
+    }
+}
+
 fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
     // local file header signature
     writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
@@ -636,19 +1029,25 @@
     writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
     // crc-32
     writer.write_u32::<LittleEndian>(file.crc32)?;
-    // compressed size
-    writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
-    // uncompressed size
-    writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+    // compressed size and uncompressed size
+    if file.large_file {
+        writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
+        writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
+    } else {
+        writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+        writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+    }
     // file name length
     writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
     // extra field length
-    let extra_field = build_extra_field(file)?;
-    writer.write_u16::<LittleEndian>(extra_field.len() as u16)?;
+    let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16;
+    writer.write_u16::<LittleEndian>(extra_field_length)?;
     // file name
     writer.write_all(file.file_name.as_bytes())?;
-    // extra field
-    writer.write_all(&extra_field)?;
+    // zip64 extra field
+    if file.large_file {
+        write_local_zip64_extra_field(writer, file)?;
+    }
 
     Ok(())
 }
@@ -660,12 +1059,29 @@
     const CRC32_OFFSET: u64 = 14;
     writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
     writer.write_u32::<LittleEndian>(file.crc32)?;
-    writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
-    writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+    if file.large_file {
+        update_local_zip64_extra_field(writer, file)?;
+    } else {
+        // check compressed size as well as it can also be slightly larger than uncompressed size
+        if file.compressed_size > spec::ZIP64_BYTES_THR {
+            return Err(ZipError::Io(io::Error::new(
+                io::ErrorKind::Other,
+                "Large file option has not been set",
+            )));
+        }
+        writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+        // uncompressed size is already checked on write to catch it as soon as possible
+        writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+    }
     Ok(())
 }
 
 fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
+    // buffer zip64 extra field to determine its variable length
+    let mut zip64_extra_field = [0; 28];
+    let zip64_extra_field_length =
+        write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
+
     // central file header signature
     writer.write_u32::<LittleEndian>(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?;
     // version made by
@@ -689,14 +1105,13 @@
     // crc-32
     writer.write_u32::<LittleEndian>(file.crc32)?;
     // compressed size
-    writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+    writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
     // uncompressed size
-    writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+    writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
     // file name length
     writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
     // extra field length
-    let extra_field = build_extra_field(file)?;
-    writer.write_u16::<LittleEndian>(extra_field.len() as u16)?;
+    writer.write_u16::<LittleEndian>(zip64_extra_field_length + file.extra_field.len() as u16)?;
     // file comment length
     writer.write_u16::<LittleEndian>(0)?;
     // disk number start
@@ -706,21 +1121,135 @@
     // external file attributes
     writer.write_u32::<LittleEndian>(file.external_attributes)?;
     // relative offset of local header
-    writer.write_u32::<LittleEndian>(file.header_start as u32)?;
+    writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
     // file name
     writer.write_all(file.file_name.as_bytes())?;
+    // zip64 extra field
+    writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
     // extra field
-    writer.write_all(&extra_field)?;
+    writer.write_all(&file.extra_field)?;
     // file comment
     // <none>
 
     Ok(())
 }
 
-fn build_extra_field(_file: &ZipFileData) -> ZipResult<Vec<u8>> {
-    let writer = Vec::new();
-    // Future work
-    Ok(writer)
+fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
+    let mut data = file.extra_field.as_slice();
+
+    if data.len() > spec::ZIP64_ENTRY_THR {
+        return Err(ZipError::Io(io::Error::new(
+            io::ErrorKind::InvalidData,
+            "Extra data exceeds extra field",
+        )));
+    }
+
+    while !data.is_empty() {
+        let left = data.len();
+        if left < 4 {
+            return Err(ZipError::Io(io::Error::new(
+                io::ErrorKind::Other,
+                "Incomplete extra data header",
+            )));
+        }
+        let kind = data.read_u16::<LittleEndian>()?;
+        let size = data.read_u16::<LittleEndian>()? as usize;
+        let left = left - 4;
+
+        if kind == 0x0001 {
+            return Err(ZipError::Io(io::Error::new(
+                io::ErrorKind::Other,
+                "No custom ZIP64 extra data allowed",
+            )));
+        }
+
+        #[cfg(not(feature = "unreserved"))]
+        {
+            if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) {
+                return Err(ZipError::Io(io::Error::new(
+                    io::ErrorKind::Other,
+                    format!(
+                        "Extra data header ID {:#06} requires crate feature \"unreserved\"",
+                        kind,
+                    ),
+                )));
+            }
+        }
+
+        if size > left {
+            return Err(ZipError::Io(io::Error::new(
+                io::ErrorKind::Other,
+                "Extra data size exceeds extra field",
+            )));
+        }
+
+        data = &data[size..];
+    }
+
+    Ok(())
+}
+
+fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
+    // This entry in the Local header MUST include BOTH original
+    // and compressed file size fields.
+    writer.write_u16::<LittleEndian>(0x0001)?;
+    writer.write_u16::<LittleEndian>(16)?;
+    writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
+    writer.write_u64::<LittleEndian>(file.compressed_size)?;
+    // Excluded fields:
+    // u32: disk start number
+    Ok(())
+}
+
+fn update_local_zip64_extra_field<T: Write + io::Seek>(
+    writer: &mut T,
+    file: &ZipFileData,
+) -> ZipResult<()> {
+    let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64;
+    writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?;
+    writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
+    writer.write_u64::<LittleEndian>(file.compressed_size)?;
+    // Excluded fields:
+    // u32: disk start number
+    Ok(())
+}
+
+fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
+    // The order of the fields in the zip64 extended
+    // information record is fixed, but the fields MUST
+    // only appear if the corresponding Local or Central
+    // directory record field is set to 0xFFFF or 0xFFFFFFFF.
+    let mut size = 0;
+    let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR;
+    let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR;
+    let header_start = file.header_start > spec::ZIP64_BYTES_THR;
+    if uncompressed_size {
+        size += 8;
+    }
+    if compressed_size {
+        size += 8;
+    }
+    if header_start {
+        size += 8;
+    }
+    if size > 0 {
+        writer.write_u16::<LittleEndian>(0x0001)?;
+        writer.write_u16::<LittleEndian>(size)?;
+        size += 4;
+
+        if uncompressed_size {
+            writer.write_u64::<LittleEndian>(file.uncompressed_size)?;
+        }
+        if compressed_size {
+            writer.write_u64::<LittleEndian>(file.compressed_size)?;
+        }
+        if header_start {
+            writer.write_u64::<LittleEndian>(file.header_start)?;
+        }
+        // Excluded fields:
+        // u32: disk start number
+    }
+    Ok(size)
 }
 
 fn path_to_string(path: &std::path::Path) -> String {
@@ -789,12 +1318,14 @@
         let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
         let options = FileOptions {
             compression_method: CompressionMethod::Stored,
+            compression_level: None,
             last_modified_time: DateTime::default(),
             permissions: Some(33188),
+            large_file: false,
         };
         writer.start_file("mimetype", options).unwrap();
         writer
-            .write(b"application/vnd.oasis.opendocument.text")
+            .write_all(b"application/vnd.oasis.opendocument.text")
             .unwrap();
         let result = writer.finish().unwrap();
 
@@ -819,3 +1350,12 @@
         assert_eq!(path_str, "windows/system32");
     }
 }
+
+#[cfg(not(feature = "unreserved"))]
+const EXTRA_FIELD_MAPPING: [u16; 49] = [
+    0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016,
+    0x0017, 0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605,
+    0x2705, 0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356,
+    0x5455, 0x554e, 0x5855, 0x6375, 0x6542, 0x7075, 0x756e, 0x7855, 0xa11e, 0xa220, 0xfd4a, 0x9901,
+    0x9902,
+];
diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs
index 32e8af8..91d4039 100644
--- a/src/zipcrypto.rs
+++ b/src/zipcrypto.rs
@@ -47,7 +47,7 @@
     }
 
     fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> {
-        return (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize]);
+        (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize])
     }
 }
 
@@ -57,6 +57,11 @@
     keys: ZipCryptoKeys,
 }
 
+pub enum ZipCryptoValidator {
+    PkzipCrc32(u32),
+    InfoZipMsdosTime(u16),
+}
+
 impl<R: std::io::Read> ZipCryptoReader<R> {
     /// Note: The password is `&[u8]` and not `&str` because the
     /// [zip specification](https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.3.TXT)
@@ -66,7 +71,7 @@
     /// password byte sequence that is unrepresentable in UTF-8.
     pub fn new(file: R, password: &[u8]) -> ZipCryptoReader<R> {
         let mut result = ZipCryptoReader {
-            file: file,
+            file,
             keys: ZipCryptoKeys::new(),
         };
 
@@ -81,7 +86,7 @@
     /// Read the ZipCrypto header bytes and validate the password.
     pub fn validate(
         mut self,
-        crc32_plaintext: u32,
+        validator: ZipCryptoValidator,
     ) -> Result<Option<ZipCryptoReaderValid<R>>, std::io::Error> {
         // ZipCrypto prefixes a file with a 12 byte header
         let mut header_buf = [0u8; 12];
@@ -90,13 +95,30 @@
             *byte = self.keys.decrypt_byte(*byte);
         }
 
-        // PKZIP before 2.0 used 2 byte CRC check.
-        // PKZIP 2.0+ used 1 byte CRC check. It's more secure.
-        // We also use 1 byte CRC.
+        match validator {
+            ZipCryptoValidator::PkzipCrc32(crc32_plaintext) => {
+                // PKZIP before 2.0 used 2 byte CRC check.
+                // PKZIP 2.0+ used 1 byte CRC check. It's more secure.
+                // We also use 1 byte CRC.
 
-        if (crc32_plaintext >> 24) as u8 != header_buf[11] {
-            return Ok(None); // Wrong password
+                if (crc32_plaintext >> 24) as u8 != header_buf[11] {
+                    return Ok(None); // Wrong password
+                }
+            }
+            ZipCryptoValidator::InfoZipMsdosTime(last_mod_time) => {
+                // Info-ZIP modification to ZipCrypto format:
+                // If bit 3 of the general purpose bit flag is set
+                // (indicates that the file uses a data-descriptor section),
+                // it uses high byte of 16-bit File Time.
+                // Info-ZIP code probably writes 2 bytes of File Time.
+                // We check only 1 byte.
+
+                if (last_mod_time >> 8) as u8 != header_buf[11] {
+                    return Ok(None); // Wrong password
+                }
+            }
         }
+
         Ok(Some(ZipCryptoReaderValid { reader: self }))
     }
 }
@@ -107,11 +129,11 @@
 }
 
 impl<R: std::io::Read> std::io::Read for ZipCryptoReaderValid<R> {
-    fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result<usize> {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
         // Note: There might be potential for optimization. Inspiration can be found at:
         // https://github.com/kornelski/7z/blob/master/CPP/7zip/Crypto/ZipCrypto.cpp
 
-        let result = self.reader.file.read(&mut buf);
+        let result = self.reader.file.read(buf);
         for byte in buf.iter_mut() {
             *byte = self.reader.keys.decrypt_byte(*byte);
         }
diff --git a/tests/aes_encryption.rs b/tests/aes_encryption.rs
new file mode 100644
index 0000000..4b393eb
--- /dev/null
+++ b/tests/aes_encryption.rs
@@ -0,0 +1,80 @@
+#![cfg(feature = "aes-crypto")]
+
+use std::io::{self, Read};
+use zip::ZipArchive;
+
+const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
+
+const PASSWORD: &[u8] = b"helloworld";
+
+#[test]
+fn aes256_encrypted_uncompressed_file() {
+    let mut v = Vec::new();
+    v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+    let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+    let mut file = archive
+        .by_name_decrypt("secret_data_256_uncompressed", PASSWORD)
+        .expect("couldn't find file in archive")
+        .expect("invalid password");
+    assert_eq!("secret_data_256_uncompressed", file.name());
+
+    let mut content = String::new();
+    file.read_to_string(&mut content)
+        .expect("couldn't read encrypted file");
+    assert_eq!(SECRET_CONTENT, content);
+}
+
+#[test]
+fn aes256_encrypted_file() {
+    let mut v = Vec::new();
+    v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+    let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+    let mut file = archive
+        .by_name_decrypt("secret_data_256", PASSWORD)
+        .expect("couldn't find file in archive")
+        .expect("invalid password");
+    assert_eq!("secret_data_256", file.name());
+
+    let mut content = String::new();
+    file.read_to_string(&mut content)
+        .expect("couldn't read encrypted and compressed file");
+    assert_eq!(SECRET_CONTENT, content);
+}
+
+#[test]
+fn aes192_encrypted_file() {
+    let mut v = Vec::new();
+    v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+    let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+    let mut file = archive
+        .by_name_decrypt("secret_data_192", PASSWORD)
+        .expect("couldn't find file in archive")
+        .expect("invalid password");
+    assert_eq!("secret_data_192", file.name());
+
+    let mut content = String::new();
+    file.read_to_string(&mut content)
+        .expect("couldn't read encrypted file");
+    assert_eq!(SECRET_CONTENT, content);
+}
+
+#[test]
+fn aes128_encrypted_file() {
+    let mut v = Vec::new();
+    v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+    let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+    let mut file = archive
+        .by_name_decrypt("secret_data_128", PASSWORD)
+        .expect("couldn't find file in archive")
+        .expect("invalid password");
+    assert_eq!("secret_data_128", file.name());
+
+    let mut content = String::new();
+    file.read_to_string(&mut content)
+        .expect("couldn't read encrypted file");
+    assert_eq!(SECRET_CONTENT, content);
+}
diff --git a/tests/data/aes_archive.zip b/tests/data/aes_archive.zip
new file mode 100644
index 0000000..4cf1fd2
--- /dev/null
+++ b/tests/data/aes_archive.zip
Binary files differ
diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs
index b826f54..25d0c54 100644
--- a/tests/end_to_end.rs
+++ b/tests/end_to_end.rs
@@ -1,81 +1,152 @@
+use byteorder::{LittleEndian, WriteBytesExt};
 use std::collections::HashSet;
 use std::io::prelude::*;
 use std::io::{Cursor, Seek};
 use std::iter::FromIterator;
 use zip::write::FileOptions;
-use zip::CompressionMethod;
+use zip::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
 
 // This test asserts that after creating a zip file, then reading its contents back out,
 // the extracted data will *always* be exactly the same as the original data.
 #[test]
 fn end_to_end() {
-    let file = &mut Cursor::new(Vec::new());
+    for &method in SUPPORTED_COMPRESSION_METHODS {
+        let file = &mut Cursor::new(Vec::new());
 
-    write_to_zip(file).expect("file written");
+        println!("Writing file with {} compression", method);
+        write_test_archive(file, method).expect("Couldn't write test zip archive");
 
-    check_zip_contents(file, ENTRY_NAME);
+        println!("Checking file contents");
+        check_archive_file(file, ENTRY_NAME, Some(method), LOREM_IPSUM);
+    }
 }
 
 // This test asserts that after copying a `ZipFile` to a new `ZipWriter`, then reading its
 // contents back out, the extracted data will *always* be exactly the same as the original data.
 #[test]
 fn copy() {
-    let src_file = &mut Cursor::new(Vec::new());
-    write_to_zip(src_file).expect("file written");
+    for &method in SUPPORTED_COMPRESSION_METHODS {
+        let src_file = &mut Cursor::new(Vec::new());
+        write_test_archive(src_file, method).expect("Couldn't write to test file");
 
-    let mut tgt_file = &mut Cursor::new(Vec::new());
-
-    {
-        let mut src_archive = zip::ZipArchive::new(src_file).unwrap();
-        let mut zip = zip::ZipWriter::new(&mut tgt_file);
+        let mut tgt_file = &mut Cursor::new(Vec::new());
 
         {
-            let file = src_archive.by_name(ENTRY_NAME).expect("file found");
-            zip.raw_copy_file(file).unwrap();
+            let mut src_archive = zip::ZipArchive::new(src_file).unwrap();
+            let mut zip = zip::ZipWriter::new(&mut tgt_file);
+
+            {
+                let file = src_archive
+                    .by_name(ENTRY_NAME)
+                    .expect("Missing expected file");
+
+                zip.raw_copy_file(file).expect("Couldn't copy file");
+            }
+
+            {
+                let file = src_archive
+                    .by_name(ENTRY_NAME)
+                    .expect("Missing expected file");
+
+                zip.raw_copy_file_rename(file, COPY_ENTRY_NAME)
+                    .expect("Couldn't copy and rename file");
+            }
         }
 
-        {
-            let file = src_archive.by_name(ENTRY_NAME).expect("file found");
-            zip.raw_copy_file_rename(file, COPY_ENTRY_NAME).unwrap();
-        }
+        let mut tgt_archive = zip::ZipArchive::new(tgt_file).unwrap();
+
+        check_archive_file_contents(&mut tgt_archive, ENTRY_NAME, LOREM_IPSUM);
+        check_archive_file_contents(&mut tgt_archive, COPY_ENTRY_NAME, LOREM_IPSUM);
     }
-
-    let mut tgt_archive = zip::ZipArchive::new(tgt_file).unwrap();
-
-    check_zip_file_contents(&mut tgt_archive, ENTRY_NAME);
-    check_zip_file_contents(&mut tgt_archive, COPY_ENTRY_NAME);
 }
 
-fn write_to_zip(file: &mut Cursor<Vec<u8>>) -> zip::result::ZipResult<()> {
+// This test asserts that after appending to a `ZipWriter`, then reading its contents back out,
+// both the prior data and the appended data will be exactly the same as their originals.
+#[test]
+fn append() {
+    for &method in SUPPORTED_COMPRESSION_METHODS {
+        let mut file = &mut Cursor::new(Vec::new());
+        write_test_archive(file, method).expect("Couldn't write to test file");
+
+        {
+            let mut zip = zip::ZipWriter::new_append(&mut file).unwrap();
+            zip.start_file(
+                COPY_ENTRY_NAME,
+                FileOptions::default().compression_method(method),
+            )
+            .unwrap();
+            zip.write_all(LOREM_IPSUM).unwrap();
+            zip.finish().unwrap();
+        }
+
+        let mut zip = zip::ZipArchive::new(&mut file).unwrap();
+        check_archive_file_contents(&mut zip, ENTRY_NAME, LOREM_IPSUM);
+        check_archive_file_contents(&mut zip, COPY_ENTRY_NAME, LOREM_IPSUM);
+    }
+}
+
+// Write a test zip archive to buffer.
+fn write_test_archive(
+    file: &mut Cursor<Vec<u8>>,
+    method: CompressionMethod,
+) -> zip::result::ZipResult<()> {
     let mut zip = zip::ZipWriter::new(file);
 
     zip.add_directory("test/", Default::default())?;
 
     let options = FileOptions::default()
-        .compression_method(CompressionMethod::Stored)
+        .compression_method(method)
         .unix_permissions(0o755);
+
     zip.start_file("test/☃.txt", options)?;
     zip.write_all(b"Hello, World!\n")?;
 
-    zip.start_file(ENTRY_NAME, Default::default())?;
+    zip.start_file_with_extra_data("test_with_extra_data/🐢.txt", options)?;
+    zip.write_u16::<LittleEndian>(0xbeef)?;
+    zip.write_u16::<LittleEndian>(EXTRA_DATA.len() as u16)?;
+    zip.write_all(EXTRA_DATA)?;
+    zip.end_extra_data()?;
+    zip.write_all(b"Hello, World! Again.\n")?;
+
+    zip.start_file(ENTRY_NAME, options)?;
     zip.write_all(LOREM_IPSUM)?;
 
     zip.finish()?;
     Ok(())
 }
 
-fn read_zip<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchive<R>> {
-    let archive = zip::ZipArchive::new(zip_file).unwrap();
+// Load an archive from buffer and check for test data.
+fn check_test_archive<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchive<R>> {
+    let mut archive = zip::ZipArchive::new(zip_file).unwrap();
 
-    let expected_file_names = ["test/", "test/☃.txt", ENTRY_NAME];
-    let expected_file_names = HashSet::from_iter(expected_file_names.iter().map(|&v| v));
-    let file_names = archive.file_names().collect::<HashSet<_>>();
-    assert_eq!(file_names, expected_file_names);
+    // Check archive contains expected file names.
+    {
+        let expected_file_names = [
+            "test/",
+            "test/☃.txt",
+            "test_with_extra_data/🐢.txt",
+            ENTRY_NAME,
+        ];
+        let expected_file_names = HashSet::from_iter(expected_file_names.iter().copied());
+        let file_names = archive.file_names().collect::<HashSet<_>>();
+        assert_eq!(file_names, expected_file_names);
+    }
+
+    // Check an archive file for extra data field contents.
+    {
+        let file_with_extra_data = archive.by_name("test_with_extra_data/🐢.txt")?;
+        let mut extra_data = Vec::new();
+        extra_data.write_u16::<LittleEndian>(0xbeef)?;
+        extra_data.write_u16::<LittleEndian>(EXTRA_DATA.len() as u16)?;
+        extra_data.write_all(EXTRA_DATA)?;
+        assert_eq!(file_with_extra_data.extra_data(), extra_data.as_slice());
+    }
 
     Ok(archive)
 }
 
-fn read_zip_file<R: Read + Seek>(
+// Read a file in the archive as a string.
+fn read_archive_file<R: Read + Seek>(
     archive: &mut zip::ZipArchive<R>,
     name: &str,
 ) -> zip::result::ZipResult<String> {
@@ -83,26 +154,52 @@
 
     let mut contents = String::new();
     file.read_to_string(&mut contents).unwrap();
+
     Ok(contents)
 }
 
-fn check_zip_contents(zip_file: &mut Cursor<Vec<u8>>, name: &str) {
-    let mut archive = read_zip(zip_file).unwrap();
-    check_zip_file_contents(&mut archive, name);
+// Check a file in the archive contains expected data and properties.
+fn check_archive_file(
+    zip_file: &mut Cursor<Vec<u8>>,
+    name: &str,
+    expected_method: Option<CompressionMethod>,
+    expected_data: &[u8],
+) {
+    let mut archive = check_test_archive(zip_file).unwrap();
+
+    if let Some(expected_method) = expected_method {
+        // Check the file's compression method.
+        let file = archive.by_name(name).unwrap();
+        let real_method = file.compression();
+
+        assert_eq!(
+            expected_method, real_method,
+            "File does not have expected compression method"
+        );
+    }
+
+    check_archive_file_contents(&mut archive, name, expected_data);
 }
 
-fn check_zip_file_contents<R: Read + Seek>(archive: &mut zip::ZipArchive<R>, name: &str) {
-    let file_contents: String = read_zip_file(archive, name).unwrap();
-    assert!(file_contents.as_bytes() == LOREM_IPSUM);
+// Check a file in the archive contains the given data.
+fn check_archive_file_contents<R: Read + Seek>(
+    archive: &mut zip::ZipArchive<R>,
+    name: &str,
+    expected: &[u8],
+) {
+    let file_contents: String = read_archive_file(archive, name).unwrap();
+    assert_eq!(file_contents.as_bytes(), expected);
 }
 
-const LOREM_IPSUM : &'static [u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
+const LOREM_IPSUM : &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
 molestie. Proin blandit ornare dui, a tempor nisl accumsan in. Praesent a consequat felis. Morbi metus diam, auctor in auctor vel, feugiat id odio. Curabitur ex ex,
 dictum quis auctor quis, suscipit id lorem. Aliquam vestibulum dolor nec enim vehicula, porta tristique augue tincidunt. Vivamus ut gravida est. Sed pellentesque, dolor
 vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam id lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
 inceptos himenaeos. Maecenas feugiat velit in ex ultrices scelerisque id id neque.
 ";
 
+const EXTRA_DATA: &[u8] = b"Extra Data";
+
 const ENTRY_NAME: &str = "test/lorem_ipsum.txt";
 
 const COPY_ENTRY_NAME: &str = "test/lorem_ipsum_renamed.txt";
diff --git a/tests/issue_234.rs b/tests/issue_234.rs
new file mode 100644
index 0000000..bd01d1d
--- /dev/null
+++ b/tests/issue_234.rs
@@ -0,0 +1,31 @@
+use zip::result::ZipError;
+
+const BUF: &[u8] = &[
+    0, 80, 75, 1, 2, 127, 120, 0, 3, 3, 75, 80, 232, 3, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 7, 0, 0, 0,
+    0, 65, 0, 1, 0, 0, 0, 4, 0, 0, 224, 255, 0, 255, 255, 255, 255, 255, 255, 20, 39, 221, 221,
+    221, 221, 221, 221, 205, 221, 221, 221, 42, 221, 221, 221, 221, 221, 221, 221, 221, 38, 34, 34,
+    219, 80, 75, 5, 6, 0, 0, 0, 0, 5, 96, 0, 1, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 234, 236, 124,
+    221, 221, 37, 221, 221, 221, 221, 221, 129, 4, 0, 0, 221, 221, 80, 75, 1, 2, 127, 120, 0, 4, 0,
+    0, 2, 127, 120, 0, 79, 75, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0,
+    234, 0, 0, 0, 3, 8, 4, 232, 3, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 3, 0,
+    221, 209, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 58, 58, 42, 75, 9, 2, 127,
+    120, 0, 99, 99, 99, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0, 213, 213, 213, 213, 213, 213, 213,
+    213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0, 213, 213,
+    213, 213, 213, 213, 213, 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236, 234, 0, 0, 0, 3, 8,
+    0, 0, 0, 12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 58, 58, 58, 42, 175, 221, 253, 221,
+    221, 221, 221, 221, 80, 75, 9, 2, 127, 120, 0, 99, 99, 99, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0,
+    213, 213, 213, 213, 213, 213, 213, 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236, 221, 221,
+    221, 221, 221, 80, 75, 9, 2, 127, 120, 0, 99, 99, 99, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0, 213,
+    213, 213, 213, 213, 213, 213, 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236,
+];
+
+#[test]
+fn invalid_header() {
+    let reader = std::io::Cursor::new(&BUF);
+    let archive = zip::ZipArchive::new(reader);
+    match archive {
+        Err(ZipError::InvalidArchive(_)) => {}
+        value => panic!("Unexpected value: {:?}", value),
+    }
+}
diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs
index cae6b1f..6c4d6b8 100644
--- a/tests/zip_crypto.rs
+++ b/tests/zip_crypto.rs
@@ -47,9 +47,9 @@
         // No password
         let file = archive.by_index(0);
         match file {
-            Err(zip::result::ZipError::UnsupportedArchive("Password required to decrypt file")) => {
-                ()
-            }
+            Err(zip::result::ZipError::UnsupportedArchive(
+                zip::result::ZipError::PASSWORD_REQUIRED,
+            )) => (),
             Err(_) => panic!(
                 "Expected PasswordRequired error when opening encrypted file without password"
             ),